001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.activemq.broker.jmx;
018
019import java.io.IOException;
020import java.lang.reflect.Method;
021import java.rmi.NoSuchObjectException;
022import java.rmi.registry.LocateRegistry;
023import java.rmi.registry.Registry;
024import java.rmi.server.UnicastRemoteObject;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029import java.util.concurrent.ConcurrentHashMap;
030import java.util.concurrent.atomic.AtomicBoolean;
031
032import javax.management.Attribute;
033import javax.management.InstanceNotFoundException;
034import javax.management.JMException;
035import javax.management.MBeanServer;
036import javax.management.MBeanServerFactory;
037import javax.management.MBeanServerInvocationHandler;
038import javax.management.MalformedObjectNameException;
039import javax.management.ObjectInstance;
040import javax.management.ObjectName;
041import javax.management.QueryExp;
042import javax.management.remote.JMXConnectorServer;
043import javax.management.remote.JMXConnectorServerFactory;
044import javax.management.remote.JMXServiceURL;
045
046import org.apache.activemq.Service;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049import org.slf4j.MDC;
050
051/**
052 * An abstraction over JMX mbean registration
053 *
054 * @org.apache.xbean.XBean
055 *
056 */
057public class ManagementContext implements Service {
058
059    /**
060     * Default activemq domain
061     */
062    public static final String DEFAULT_DOMAIN = "org.apache.activemq";
063
064    static {
065        String option = Boolean.TRUE.toString();
066        try {
067            option = System.getProperty("org.apache.activemq.broker.jmx.createConnector", "true");
068        } catch (Exception ex) {
069        }
070
071        DEFAULT_CREATE_CONNECTOR = Boolean.valueOf(option);
072    }
073
074    public static final boolean DEFAULT_CREATE_CONNECTOR;
075
076    private static final Logger LOG = LoggerFactory.getLogger(ManagementContext.class);
077    private MBeanServer beanServer;
078    private String jmxDomainName = DEFAULT_DOMAIN;
079    private boolean useMBeanServer = true;
080    private boolean createMBeanServer = true;
081    private boolean locallyCreateMBeanServer;
082    private boolean createConnector = DEFAULT_CREATE_CONNECTOR;
083    private boolean findTigerMbeanServer = true;
084    private String connectorHost = "localhost";
085    private int connectorPort = 1099;
086    private Map<String, ?> environment;
087    private int rmiServerPort;
088    private String connectorPath = "/jmxrmi";
089    private final AtomicBoolean started = new AtomicBoolean(false);
090    private final AtomicBoolean connectorStarting = new AtomicBoolean(false);
091    private JMXConnectorServer connectorServer;
092    private ObjectName namingServiceObjectName;
093    private Registry registry;
094    private final Map<ObjectName, ObjectName> registeredMBeanNames = new ConcurrentHashMap<ObjectName, ObjectName>();
095    private boolean allowRemoteAddressInMBeanNames = true;
096    private String brokerName;
097    private String suppressMBean;
098    private List<ObjectName> suppressMBeanList;
099
100    public ManagementContext() {
101        this(null);
102    }
103
104    public ManagementContext(MBeanServer server) {
105        this.beanServer = server;
106    }
107
108    @Override
109    public void start() throws Exception {
110        // lets force the MBeanServer to be created if needed
111        if (started.compareAndSet(false, true)) {
112
113            populateMBeanSuppressionMap();
114
115            // fallback and use localhost
116            if (connectorHost == null) {
117                connectorHost = "localhost";
118            }
119
120            // force mbean server to be looked up, so we have it
121            getMBeanServer();
122
123            if (connectorServer != null) {
124                try {
125                    if (getMBeanServer().isRegistered(namingServiceObjectName)) {
126                        LOG.debug("Invoking start on mbean: {}", namingServiceObjectName);
127                        getMBeanServer().invoke(namingServiceObjectName, "start", null, null);
128                    }
129                } catch (Throwable ignore) {
130                    LOG.debug("Error invoking start on MBean {}. This exception is ignored.", namingServiceObjectName, ignore);
131                }
132
133                Thread t = new Thread("JMX connector") {
134                    @Override
135                    public void run() {
136                        // ensure we use MDC logging with the broker name, so people can see the logs if MDC was in use
137                        if (brokerName != null) {
138                            MDC.put("activemq.broker", brokerName);
139                        }
140                        try {
141                            JMXConnectorServer server = connectorServer;
142                            if (started.get() && server != null) {
143                                LOG.debug("Starting JMXConnectorServer...");
144                                connectorStarting.set(true);
145                                try {
146                                    // need to remove MDC as we must not inherit MDC in child threads causing leaks
147                                    MDC.remove("activemq.broker");
148                                    server.start();
149                                } finally {
150                                    if (brokerName != null) {
151                                        MDC.put("activemq.broker", brokerName);
152                                    }
153                                    connectorStarting.set(false);
154                                }
155                                LOG.info("JMX consoles can connect to {}", server.getAddress());
156                            }
157                        } catch (IOException e) {
158                            LOG.warn("Failed to start JMX connector {}. Will restart management to re-create JMX connector, trying to remedy this issue.", e.getMessage());
159                            LOG.debug("Reason for failed JMX connector start", e);
160                        } finally {
161                            MDC.remove("activemq.broker");
162                        }
163                    }
164                };
165                t.setDaemon(true);
166                t.start();
167            }
168        }
169    }
170
171    private void populateMBeanSuppressionMap() throws Exception {
172        if (suppressMBean != null) {
173            suppressMBeanList = new LinkedList<>();
174            for (String pair : suppressMBean.split(",")) {
175                suppressMBeanList.add(new ObjectName(jmxDomainName + ":*," + pair));
176            }
177        }
178    }
179
180    @Override
181    public void stop() throws Exception {
182        if (started.compareAndSet(true, false)) {
183            MBeanServer mbeanServer = getMBeanServer();
184
185            // unregister the mbeans we have registered
186            if (mbeanServer != null) {
187                for (Map.Entry<ObjectName, ObjectName> entry : registeredMBeanNames.entrySet()) {
188                    ObjectName actualName = entry.getValue();
189                    if (actualName != null && beanServer.isRegistered(actualName)) {
190                        LOG.debug("Unregistering MBean {}", actualName);
191                        mbeanServer.unregisterMBean(actualName);
192                    }
193                }
194            }
195            registeredMBeanNames.clear();
196
197            JMXConnectorServer server = connectorServer;
198            connectorServer = null;
199            if (server != null) {
200                try {
201                    if (!connectorStarting.get()) {
202                        LOG.debug("Stopping jmx connector");
203                        server.stop();
204                    }
205                } catch (IOException e) {
206                    LOG.warn("Failed to stop jmx connector: {}", e.getMessage());
207                }
208                // stop naming service mbean
209                try {
210                    if (namingServiceObjectName != null && getMBeanServer().isRegistered(namingServiceObjectName)) {
211                        LOG.debug("Stopping MBean {}", namingServiceObjectName);
212                        getMBeanServer().invoke(namingServiceObjectName, "stop", null, null);
213                        LOG.debug("Unregistering MBean {}", namingServiceObjectName);
214                        getMBeanServer().unregisterMBean(namingServiceObjectName);
215                    }
216                } catch (Throwable ignore) {
217                    LOG.warn("Error stopping and unregsitering MBean {} due to {}", namingServiceObjectName, ignore.getMessage());
218                }
219                namingServiceObjectName = null;
220            }
221
222            if (locallyCreateMBeanServer && beanServer != null) {
223                // check to see if the factory knows about this server
224                List<MBeanServer> list = MBeanServerFactory.findMBeanServer(null);
225                if (list != null && !list.isEmpty() && list.contains(beanServer)) {
226                    LOG.debug("Releasing MBeanServer {}", beanServer);
227                    MBeanServerFactory.releaseMBeanServer(beanServer);
228                }
229            }
230            beanServer = null;
231        }
232
233        // Un-export JMX RMI registry, if it was created
234        if (registry != null) {
235            try {
236                UnicastRemoteObject.unexportObject(registry, true);
237                LOG.debug("Unexported JMX RMI Registry");
238            } catch (NoSuchObjectException e) {
239                LOG.debug("Error occurred while unexporting JMX RMI registry. This exception will be ignored.");
240            }
241
242            registry = null;
243        }
244    }
245
246    /**
247     * Gets the broker name this context is used by, may be <tt>null</tt>
248     * if the broker name was not set.
249     */
250    public String getBrokerName() {
251        return brokerName;
252    }
253
254    /**
255     * Sets the broker name this context is being used by.
256     */
257    public void setBrokerName(String brokerName) {
258        this.brokerName = brokerName;
259    }
260
261    /**
262     * @return Returns the jmxDomainName.
263     */
264    public String getJmxDomainName() {
265        return jmxDomainName;
266    }
267
268    /**
269     * @param jmxDomainName The jmxDomainName to set.
270     */
271    public void setJmxDomainName(String jmxDomainName) {
272        this.jmxDomainName = jmxDomainName;
273    }
274
275    /**
276     * Get the MBeanServer
277     *
278     * @return the MBeanServer
279     */
280    public MBeanServer getMBeanServer() {
281        if (this.beanServer == null) {
282            this.beanServer = findMBeanServer();
283        }
284        return beanServer;
285    }
286
287    /**
288     * Set the MBeanServer
289     *
290     * @param beanServer
291     */
292    public void setMBeanServer(MBeanServer beanServer) {
293        this.beanServer = beanServer;
294    }
295
296    /**
297     * @return Returns the useMBeanServer.
298     */
299    public boolean isUseMBeanServer() {
300        return useMBeanServer;
301    }
302
303    /**
304     * @param useMBeanServer The useMBeanServer to set.
305     */
306    public void setUseMBeanServer(boolean useMBeanServer) {
307        this.useMBeanServer = useMBeanServer;
308    }
309
310    /**
311     * @return Returns the createMBeanServer flag.
312     */
313    public boolean isCreateMBeanServer() {
314        return createMBeanServer;
315    }
316
317    /**
318     * @param enableJMX Set createMBeanServer.
319     */
320    public void setCreateMBeanServer(boolean enableJMX) {
321        this.createMBeanServer = enableJMX;
322    }
323
324    public boolean isFindTigerMbeanServer() {
325        return findTigerMbeanServer;
326    }
327
328    public boolean isConnectorStarted() {
329        return connectorStarting.get() || (connectorServer != null && connectorServer.isActive());
330    }
331
332    /**
333     * Enables/disables the searching for the Java 5 platform MBeanServer
334     */
335    public void setFindTigerMbeanServer(boolean findTigerMbeanServer) {
336        this.findTigerMbeanServer = findTigerMbeanServer;
337    }
338
339    /**
340     * Formulate and return the MBean ObjectName of a custom control MBean
341     *
342     * @param type
343     * @param name
344     * @return the JMX ObjectName of the MBean, or <code>null</code> if
345     *         <code>customName</code> is invalid.
346     */
347    public ObjectName createCustomComponentMBeanName(String type, String name) {
348        ObjectName result = null;
349        String tmp = jmxDomainName + ":" + "type=" + sanitizeString(type) + ",name=" + sanitizeString(name);
350        try {
351            result = new ObjectName(tmp);
352        } catch (MalformedObjectNameException e) {
353            LOG.error("Couldn't create ObjectName from: {}, {}", type, name);
354        }
355        return result;
356    }
357
358    /**
359     * The ':' and '/' characters are reserved in ObjectNames
360     *
361     * @param in
362     * @return sanitized String
363     */
364    private static String sanitizeString(String in) {
365        String result = null;
366        if (in != null) {
367            result = in.replace(':', '_');
368            result = result.replace('/', '_');
369            result = result.replace('\\', '_');
370        }
371        return result;
372    }
373
374    /**
375     * Retrieve an System ObjectName
376     *
377     * @param domainName
378     * @param containerName
379     * @param theClass
380     * @return the ObjectName
381     * @throws MalformedObjectNameException
382     */
383    public static ObjectName getSystemObjectName(String domainName, String containerName, Class<?> theClass) throws MalformedObjectNameException, NullPointerException {
384        String tmp = domainName + ":" + "type=" + theClass.getName() + ",name=" + getRelativeName(containerName, theClass);
385        return new ObjectName(tmp);
386    }
387
388    private static String getRelativeName(String containerName, Class<?> theClass) {
389        String name = theClass.getName();
390        int index = name.lastIndexOf(".");
391        if (index >= 0 && (index + 1) < name.length()) {
392            name = name.substring(index + 1);
393        }
394        return containerName + "." + name;
395    }
396
397    public Object newProxyInstance(ObjectName objectName, Class<?> interfaceClass, boolean notificationBroadcaster){
398        return MBeanServerInvocationHandler.newProxyInstance(getMBeanServer(), objectName, interfaceClass, notificationBroadcaster);
399    }
400
401    public Object getAttribute(ObjectName name, String attribute) throws Exception{
402        return getMBeanServer().getAttribute(name, attribute);
403    }
404
405    public ObjectInstance registerMBean(Object bean, ObjectName name) throws Exception{
406        ObjectInstance result = null;
407        if (isAllowedToRegister(name)) {
408            result = getMBeanServer().registerMBean(bean, name);
409            this.registeredMBeanNames.put(name, result.getObjectName());
410        }
411        return result;
412    }
413
414    protected boolean isAllowedToRegister(ObjectName name) {
415        boolean result = true;
416        if (suppressMBean != null && suppressMBeanList != null) {
417            for (ObjectName attr : suppressMBeanList) {
418                if (attr.apply(name)) {
419                    result = false;
420                    break;
421                }
422            }
423        }
424        return result;
425    }
426
427    public Set<ObjectName> queryNames(ObjectName name, QueryExp query) throws Exception{
428        if (name != null) {
429            ObjectName actualName = this.registeredMBeanNames.get(name);
430            if (actualName != null) {
431                return getMBeanServer().queryNames(actualName, query);
432            }
433        }
434        return getMBeanServer().queryNames(name, query);
435    }
436
437    public ObjectInstance getObjectInstance(ObjectName name) throws InstanceNotFoundException {
438        return getMBeanServer().getObjectInstance(name);
439    }
440
441    /**
442     * Unregister an MBean
443     *
444     * @param name
445     * @throws JMException
446     */
447    public void unregisterMBean(ObjectName name) throws JMException {
448        ObjectName actualName = this.registeredMBeanNames.get(name);
449        if (beanServer != null && actualName != null && beanServer.isRegistered(actualName) && this.registeredMBeanNames.remove(name) != null) {
450            LOG.debug("Unregistering MBean {}", actualName);
451            beanServer.unregisterMBean(actualName);
452        }
453    }
454
455    protected synchronized MBeanServer findMBeanServer() {
456        MBeanServer result = null;
457
458        try {
459            if (useMBeanServer) {
460                if (findTigerMbeanServer) {
461                    result = findTigerMBeanServer();
462                }
463                if (result == null) {
464                    // lets piggy back on another MBeanServer - we could be in an appserver!
465                    List<MBeanServer> list = MBeanServerFactory.findMBeanServer(null);
466                    if (list != null && list.size() > 0) {
467                        result = list.get(0);
468                    }
469                }
470            }
471            if (result == null && createMBeanServer) {
472                result = createMBeanServer();
473            }
474        } catch (NoClassDefFoundError e) {
475            LOG.error("Could not load MBeanServer", e);
476        } catch (Throwable e) {
477            // probably don't have access to system properties
478            LOG.error("Failed to initialize MBeanServer", e);
479        }
480        return result;
481    }
482
483    public MBeanServer findTigerMBeanServer() {
484        String name = "java.lang.management.ManagementFactory";
485        Class<?> type = loadClass(name, ManagementContext.class.getClassLoader());
486        if (type != null) {
487            try {
488                Method method = type.getMethod("getPlatformMBeanServer", new Class[0]);
489                if (method != null) {
490                    Object answer = method.invoke(null, new Object[0]);
491                    if (answer instanceof MBeanServer) {
492                        if (createConnector) {
493                            createConnector((MBeanServer)answer);
494                        }
495                        return (MBeanServer)answer;
496                    } else {
497                        LOG.warn("Could not cast: {} into an MBeanServer. There must be some classloader strangeness in town", answer);
498                    }
499                } else {
500                    LOG.warn("Method getPlatformMBeanServer() does not appear visible on type: {}", type.getName());
501                }
502            } catch (Exception e) {
503                LOG.warn("Failed to call getPlatformMBeanServer() due to: ", e);
504            }
505        } else {
506            LOG.trace("Class not found: {} so probably running on Java 1.4", name);
507        }
508        return null;
509    }
510
511    private static Class<?> loadClass(String name, ClassLoader loader) {
512        try {
513            return loader.loadClass(name);
514        } catch (ClassNotFoundException e) {
515            try {
516                return Thread.currentThread().getContextClassLoader().loadClass(name);
517            } catch (ClassNotFoundException e1) {
518                return null;
519            }
520        }
521    }
522
523    /**
524     * @return an MBeanServer instance
525     * @throws NullPointerException
526     * @throws MalformedObjectNameException
527     * @throws IOException
528     */
529    protected MBeanServer createMBeanServer() throws MalformedObjectNameException, IOException {
530        MBeanServer mbeanServer = MBeanServerFactory.createMBeanServer(jmxDomainName);
531        locallyCreateMBeanServer = true;
532        if (createConnector) {
533            createConnector(mbeanServer);
534        }
535        return mbeanServer;
536    }
537
538    /**
539     * @param mbeanServer
540     * @throws MalformedObjectNameException
541     * @throws IOException
542     */
543    private void createConnector(MBeanServer mbeanServer) throws MalformedObjectNameException, IOException {
544        // Create the NamingService, needed by JSR 160
545        try {
546            if (registry == null) {
547                LOG.debug("Creating RMIRegistry on port {}", connectorPort);
548                registry = LocateRegistry.createRegistry(connectorPort);
549            }
550            namingServiceObjectName = ObjectName.getInstance("naming:type=rmiregistry");
551
552            // Do not use the createMBean as the mx4j jar may not be in the
553            // same class loader than the server
554            Class<?> cl = Class.forName("mx4j.tools.naming.NamingService");
555            mbeanServer.registerMBean(cl.newInstance(), namingServiceObjectName);
556
557            // set the naming port
558            Attribute attr = new Attribute("Port", Integer.valueOf(connectorPort));
559            mbeanServer.setAttribute(namingServiceObjectName, attr);
560        } catch(ClassNotFoundException e) {
561            LOG.debug("Probably not using JRE 1.4: {}", e.getLocalizedMessage());
562        } catch (Throwable e) {
563            LOG.debug("Failed to create local registry. This exception will be ignored.", e);
564        }
565
566        // Create the JMXConnectorServer
567        String rmiServer = "";
568        if (rmiServerPort != 0) {
569            // This is handy to use if you have a firewall and need to force JMX to use fixed ports.
570            rmiServer = ""+getConnectorHost()+":" + rmiServerPort;
571        }
572        String serviceURL = "service:jmx:rmi://" + rmiServer + "/jndi/rmi://" +getConnectorHost()+":" + connectorPort + connectorPath;
573        JMXServiceURL url = new JMXServiceURL(serviceURL);
574        connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, environment, mbeanServer);
575
576        LOG.debug("Created JMXConnectorServer {}", connectorServer);
577    }
578
579    public String getConnectorPath() {
580        return connectorPath;
581    }
582
583    public void setConnectorPath(String connectorPath) {
584        this.connectorPath = connectorPath;
585    }
586
587    public int getConnectorPort() {
588        return connectorPort;
589    }
590
591    /**
592     * @org.apache.xbean.Property propertyEditor="org.apache.activemq.util.MemoryIntPropertyEditor"
593     */
594    public void setConnectorPort(int connectorPort) {
595        this.connectorPort = connectorPort;
596    }
597
598    public int getRmiServerPort() {
599        return rmiServerPort;
600    }
601
602    /**
603     * @org.apache.xbean.Property propertyEditor="org.apache.activemq.util.MemoryIntPropertyEditor"
604     */
605    public void setRmiServerPort(int rmiServerPort) {
606        this.rmiServerPort = rmiServerPort;
607    }
608
609    public boolean isCreateConnector() {
610        return createConnector;
611    }
612
613    /**
614     * @org.apache.xbean.Property propertyEditor="org.apache.activemq.util.BooleanEditor"
615     */
616    public void setCreateConnector(boolean createConnector) {
617        this.createConnector = createConnector;
618    }
619
620    /**
621     * Get the connectorHost
622     * @return the connectorHost
623     */
624    public String getConnectorHost() {
625        return this.connectorHost;
626    }
627
628    /**
629     * Set the connectorHost
630     * @param connectorHost the connectorHost to set
631     */
632    public void setConnectorHost(String connectorHost) {
633        this.connectorHost = connectorHost;
634    }
635
636    public Map<String, ?> getEnvironment() {
637        return environment;
638    }
639
640    public void setEnvironment(Map<String, ?> environment) {
641        this.environment = environment;
642    }
643
644    public boolean isAllowRemoteAddressInMBeanNames() {
645        return allowRemoteAddressInMBeanNames;
646    }
647
648    public void setAllowRemoteAddressInMBeanNames(boolean allowRemoteAddressInMBeanNames) {
649        this.allowRemoteAddressInMBeanNames = allowRemoteAddressInMBeanNames;
650    }
651
652    /**
653     * Allow selective MBeans registration to be suppressed. Any Mbean ObjectName that matches any
654     * of the supplied attribute values will not be registered with the MBeanServer.
655     * eg: "endpoint=dynamicProducer,endpoint=Consumer" will suppress the registration of *all* dynamic producer and consumer mbeans.
656     *
657     * @param commaListOfAttributeKeyValuePairs  the comma separated list of attribute key=value pairs to match.
658     */
659    public void setSuppressMBean(String commaListOfAttributeKeyValuePairs) {
660        this.suppressMBean = commaListOfAttributeKeyValuePairs;
661    }
662
663    public String getSuppressMBean() {
664        return suppressMBean;
665    }
666}