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<Map.Entry<String,String>> 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 IOException {
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() {
172        if (suppressMBean != null) {
173            suppressMBeanList = new LinkedList<>();
174            for (String pair : suppressMBean.split(",")) {
175                final String[] keyValue = pair.split("=");
176                suppressMBeanList.add(new Map.Entry<String, String>() {
177                    @Override
178                    public String getKey() {
179                        return keyValue[0];
180                    }
181
182                    @Override
183                    public String getValue() {
184                        return keyValue[1];
185                    }
186
187                    @Override
188                    public String setValue(String value) {
189                        return null;
190                    }
191                });
192            }
193        }
194    }
195
196    @Override
197    public void stop() throws Exception {
198        if (started.compareAndSet(true, false)) {
199            MBeanServer mbeanServer = getMBeanServer();
200
201            // unregister the mbeans we have registered
202            if (mbeanServer != null) {
203                for (Map.Entry<ObjectName, ObjectName> entry : registeredMBeanNames.entrySet()) {
204                    ObjectName actualName = entry.getValue();
205                    if (actualName != null && beanServer.isRegistered(actualName)) {
206                        LOG.debug("Unregistering MBean {}", actualName);
207                        mbeanServer.unregisterMBean(actualName);
208                    }
209                }
210            }
211            registeredMBeanNames.clear();
212
213            JMXConnectorServer server = connectorServer;
214            connectorServer = null;
215            if (server != null) {
216                try {
217                    if (!connectorStarting.get()) {
218                        LOG.debug("Stopping jmx connector");
219                        server.stop();
220                    }
221                } catch (IOException e) {
222                    LOG.warn("Failed to stop jmx connector: {}", e.getMessage());
223                }
224                // stop naming service mbean
225                try {
226                    if (namingServiceObjectName != null && getMBeanServer().isRegistered(namingServiceObjectName)) {
227                        LOG.debug("Stopping MBean {}", namingServiceObjectName);
228                        getMBeanServer().invoke(namingServiceObjectName, "stop", null, null);
229                        LOG.debug("Unregistering MBean {}", namingServiceObjectName);
230                        getMBeanServer().unregisterMBean(namingServiceObjectName);
231                    }
232                } catch (Throwable ignore) {
233                    LOG.warn("Error stopping and unregsitering MBean {} due to {}", namingServiceObjectName, ignore.getMessage());
234                }
235                namingServiceObjectName = null;
236            }
237
238            if (locallyCreateMBeanServer && beanServer != null) {
239                // check to see if the factory knows about this server
240                List<MBeanServer> list = MBeanServerFactory.findMBeanServer(null);
241                if (list != null && !list.isEmpty() && list.contains(beanServer)) {
242                    LOG.debug("Releasing MBeanServer {}", beanServer);
243                    MBeanServerFactory.releaseMBeanServer(beanServer);
244                }
245            }
246            beanServer = null;
247        }
248
249        // Un-export JMX RMI registry, if it was created
250        if (registry != null) {
251            try {
252                UnicastRemoteObject.unexportObject(registry, true);
253                LOG.debug("Unexported JMX RMI Registry");
254            } catch (NoSuchObjectException e) {
255                LOG.debug("Error occurred while unexporting JMX RMI registry. This exception will be ignored.");
256            }
257
258            registry = null;
259        }
260    }
261
262    /**
263     * Gets the broker name this context is used by, may be <tt>null</tt>
264     * if the broker name was not set.
265     */
266    public String getBrokerName() {
267        return brokerName;
268    }
269
270    /**
271     * Sets the broker name this context is being used by.
272     */
273    public void setBrokerName(String brokerName) {
274        this.brokerName = brokerName;
275    }
276
277    /**
278     * @return Returns the jmxDomainName.
279     */
280    public String getJmxDomainName() {
281        return jmxDomainName;
282    }
283
284    /**
285     * @param jmxDomainName The jmxDomainName to set.
286     */
287    public void setJmxDomainName(String jmxDomainName) {
288        this.jmxDomainName = jmxDomainName;
289    }
290
291    /**
292     * Get the MBeanServer
293     *
294     * @return the MBeanServer
295     */
296    protected MBeanServer getMBeanServer() {
297        if (this.beanServer == null) {
298            this.beanServer = findMBeanServer();
299        }
300        return beanServer;
301    }
302
303    /**
304     * Set the MBeanServer
305     *
306     * @param beanServer
307     */
308    public void setMBeanServer(MBeanServer beanServer) {
309        this.beanServer = beanServer;
310    }
311
312    /**
313     * @return Returns the useMBeanServer.
314     */
315    public boolean isUseMBeanServer() {
316        return useMBeanServer;
317    }
318
319    /**
320     * @param useMBeanServer The useMBeanServer to set.
321     */
322    public void setUseMBeanServer(boolean useMBeanServer) {
323        this.useMBeanServer = useMBeanServer;
324    }
325
326    /**
327     * @return Returns the createMBeanServer flag.
328     */
329    public boolean isCreateMBeanServer() {
330        return createMBeanServer;
331    }
332
333    /**
334     * @param enableJMX Set createMBeanServer.
335     */
336    public void setCreateMBeanServer(boolean enableJMX) {
337        this.createMBeanServer = enableJMX;
338    }
339
340    public boolean isFindTigerMbeanServer() {
341        return findTigerMbeanServer;
342    }
343
344    public boolean isConnectorStarted() {
345        return connectorStarting.get() || (connectorServer != null && connectorServer.isActive());
346    }
347
348    /**
349     * Enables/disables the searching for the Java 5 platform MBeanServer
350     */
351    public void setFindTigerMbeanServer(boolean findTigerMbeanServer) {
352        this.findTigerMbeanServer = findTigerMbeanServer;
353    }
354
355    /**
356     * Formulate and return the MBean ObjectName of a custom control MBean
357     *
358     * @param type
359     * @param name
360     * @return the JMX ObjectName of the MBean, or <code>null</code> if
361     *         <code>customName</code> is invalid.
362     */
363    public ObjectName createCustomComponentMBeanName(String type, String name) {
364        ObjectName result = null;
365        String tmp = jmxDomainName + ":" + "type=" + sanitizeString(type) + ",name=" + sanitizeString(name);
366        try {
367            result = new ObjectName(tmp);
368        } catch (MalformedObjectNameException e) {
369            LOG.error("Couldn't create ObjectName from: {}, {}", type, name);
370        }
371        return result;
372    }
373
374    /**
375     * The ':' and '/' characters are reserved in ObjectNames
376     *
377     * @param in
378     * @return sanitized String
379     */
380    private static String sanitizeString(String in) {
381        String result = null;
382        if (in != null) {
383            result = in.replace(':', '_');
384            result = result.replace('/', '_');
385            result = result.replace('\\', '_');
386        }
387        return result;
388    }
389
390    /**
391     * Retrieve an System ObjectName
392     *
393     * @param domainName
394     * @param containerName
395     * @param theClass
396     * @return the ObjectName
397     * @throws MalformedObjectNameException
398     */
399    public static ObjectName getSystemObjectName(String domainName, String containerName, Class<?> theClass) throws MalformedObjectNameException, NullPointerException {
400        String tmp = domainName + ":" + "type=" + theClass.getName() + ",name=" + getRelativeName(containerName, theClass);
401        return new ObjectName(tmp);
402    }
403
404    private static String getRelativeName(String containerName, Class<?> theClass) {
405        String name = theClass.getName();
406        int index = name.lastIndexOf(".");
407        if (index >= 0 && (index + 1) < name.length()) {
408            name = name.substring(index + 1);
409        }
410        return containerName + "." + name;
411    }
412
413    public Object newProxyInstance(ObjectName objectName, Class<?> interfaceClass, boolean notificationBroadcaster){
414        return MBeanServerInvocationHandler.newProxyInstance(getMBeanServer(), objectName, interfaceClass, notificationBroadcaster);
415    }
416
417    public Object getAttribute(ObjectName name, String attribute) throws Exception{
418        return getMBeanServer().getAttribute(name, attribute);
419    }
420
421    public ObjectInstance registerMBean(Object bean, ObjectName name) throws Exception{
422        ObjectInstance result = null;
423        if (isAllowedToRegister(name)) {
424            result = getMBeanServer().registerMBean(bean, name);
425            this.registeredMBeanNames.put(name, result.getObjectName());
426        }
427        return result;
428    }
429
430    private boolean isAllowedToRegister(ObjectName name) {
431        boolean result = true;
432        if (suppressMBean != null && suppressMBeanList != null) {
433            for (Map.Entry<String,String> attr : suppressMBeanList) {
434                if (attr.getValue().equals(name.getKeyProperty(attr.getKey()))) {
435                    result = false;
436                    break;
437                }
438            }
439        }
440        return result;
441    }
442
443    public Set<ObjectName> queryNames(ObjectName name, QueryExp query) throws Exception{
444        if (name != null) {
445            ObjectName actualName = this.registeredMBeanNames.get(name);
446            if (actualName != null) {
447                return getMBeanServer().queryNames(actualName, query);
448            }
449        }
450        return getMBeanServer().queryNames(name, query);
451    }
452
453    public ObjectInstance getObjectInstance(ObjectName name) throws InstanceNotFoundException {
454        return getMBeanServer().getObjectInstance(name);
455    }
456
457    /**
458     * Unregister an MBean
459     *
460     * @param name
461     * @throws JMException
462     */
463    public void unregisterMBean(ObjectName name) throws JMException {
464        ObjectName actualName = this.registeredMBeanNames.get(name);
465        if (beanServer != null && actualName != null && beanServer.isRegistered(actualName) && this.registeredMBeanNames.remove(name) != null) {
466            LOG.debug("Unregistering MBean {}", actualName);
467            beanServer.unregisterMBean(actualName);
468        }
469    }
470
471    protected synchronized MBeanServer findMBeanServer() {
472        MBeanServer result = null;
473
474        try {
475            if (useMBeanServer) {
476                if (findTigerMbeanServer) {
477                    result = findTigerMBeanServer();
478                }
479                if (result == null) {
480                    // lets piggy back on another MBeanServer - we could be in an appserver!
481                    List<MBeanServer> list = MBeanServerFactory.findMBeanServer(null);
482                    if (list != null && list.size() > 0) {
483                        result = list.get(0);
484                    }
485                }
486            }
487            if (result == null && createMBeanServer) {
488                result = createMBeanServer();
489            }
490        } catch (NoClassDefFoundError e) {
491            LOG.error("Could not load MBeanServer", e);
492        } catch (Throwable e) {
493            // probably don't have access to system properties
494            LOG.error("Failed to initialize MBeanServer", e);
495        }
496        return result;
497    }
498
499    public MBeanServer findTigerMBeanServer() {
500        String name = "java.lang.management.ManagementFactory";
501        Class<?> type = loadClass(name, ManagementContext.class.getClassLoader());
502        if (type != null) {
503            try {
504                Method method = type.getMethod("getPlatformMBeanServer", new Class[0]);
505                if (method != null) {
506                    Object answer = method.invoke(null, new Object[0]);
507                    if (answer instanceof MBeanServer) {
508                        if (createConnector) {
509                            createConnector((MBeanServer)answer);
510                        }
511                        return (MBeanServer)answer;
512                    } else {
513                        LOG.warn("Could not cast: {} into an MBeanServer. There must be some classloader strangeness in town", answer);
514                    }
515                } else {
516                    LOG.warn("Method getPlatformMBeanServer() does not appear visible on type: {}", type.getName());
517                }
518            } catch (Exception e) {
519                LOG.warn("Failed to call getPlatformMBeanServer() due to: ", e);
520            }
521        } else {
522            LOG.trace("Class not found: {} so probably running on Java 1.4", name);
523        }
524        return null;
525    }
526
527    private static Class<?> loadClass(String name, ClassLoader loader) {
528        try {
529            return loader.loadClass(name);
530        } catch (ClassNotFoundException e) {
531            try {
532                return Thread.currentThread().getContextClassLoader().loadClass(name);
533            } catch (ClassNotFoundException e1) {
534                return null;
535            }
536        }
537    }
538
539    /**
540     * @return
541     * @throws NullPointerException
542     * @throws MalformedObjectNameException
543     * @throws IOException
544     */
545    protected MBeanServer createMBeanServer() throws MalformedObjectNameException, IOException {
546        MBeanServer mbeanServer = MBeanServerFactory.createMBeanServer(jmxDomainName);
547        locallyCreateMBeanServer = true;
548        if (createConnector) {
549            createConnector(mbeanServer);
550        }
551        return mbeanServer;
552    }
553
554    /**
555     * @param mbeanServer
556     * @throws MalformedObjectNameException
557     * @throws IOException
558     */
559    private void createConnector(MBeanServer mbeanServer) throws MalformedObjectNameException, IOException {
560        // Create the NamingService, needed by JSR 160
561        try {
562            if (registry == null) {
563                LOG.debug("Creating RMIRegistry on port {}", connectorPort);
564                registry = LocateRegistry.createRegistry(connectorPort);
565            }
566            namingServiceObjectName = ObjectName.getInstance("naming:type=rmiregistry");
567
568            // Do not use the createMBean as the mx4j jar may not be in the
569            // same class loader than the server
570            Class<?> cl = Class.forName("mx4j.tools.naming.NamingService");
571            mbeanServer.registerMBean(cl.newInstance(), namingServiceObjectName);
572
573            // set the naming port
574            Attribute attr = new Attribute("Port", Integer.valueOf(connectorPort));
575            mbeanServer.setAttribute(namingServiceObjectName, attr);
576        } catch(ClassNotFoundException e) {
577            LOG.debug("Probably not using JRE 1.4: {}", e.getLocalizedMessage());
578        } catch (Throwable e) {
579            LOG.debug("Failed to create local registry. This exception will be ignored.", e);
580        }
581
582        // Create the JMXConnectorServer
583        String rmiServer = "";
584        if (rmiServerPort != 0) {
585            // This is handy to use if you have a firewall and need to force JMX to use fixed ports.
586            rmiServer = ""+getConnectorHost()+":" + rmiServerPort;
587        }
588        String serviceURL = "service:jmx:rmi://" + rmiServer + "/jndi/rmi://" +getConnectorHost()+":" + connectorPort + connectorPath;
589        JMXServiceURL url = new JMXServiceURL(serviceURL);
590        connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, environment, mbeanServer);
591
592        LOG.debug("Created JMXConnectorServer {}", connectorServer);
593    }
594
595    public String getConnectorPath() {
596        return connectorPath;
597    }
598
599    public void setConnectorPath(String connectorPath) {
600        this.connectorPath = connectorPath;
601    }
602
603    public int getConnectorPort() {
604        return connectorPort;
605    }
606
607    /**
608     * @org.apache.xbean.Property propertyEditor="org.apache.activemq.util.MemoryIntPropertyEditor"
609     */
610    public void setConnectorPort(int connectorPort) {
611        this.connectorPort = connectorPort;
612    }
613
614    public int getRmiServerPort() {
615        return rmiServerPort;
616    }
617
618    /**
619     * @org.apache.xbean.Property propertyEditor="org.apache.activemq.util.MemoryIntPropertyEditor"
620     */
621    public void setRmiServerPort(int rmiServerPort) {
622        this.rmiServerPort = rmiServerPort;
623    }
624
625    public boolean isCreateConnector() {
626        return createConnector;
627    }
628
629    /**
630     * @org.apache.xbean.Property propertyEditor="org.apache.activemq.util.BooleanEditor"
631     */
632    public void setCreateConnector(boolean createConnector) {
633        this.createConnector = createConnector;
634    }
635
636    /**
637     * Get the connectorHost
638     * @return the connectorHost
639     */
640    public String getConnectorHost() {
641        return this.connectorHost;
642    }
643
644    /**
645     * Set the connectorHost
646     * @param connectorHost the connectorHost to set
647     */
648    public void setConnectorHost(String connectorHost) {
649        this.connectorHost = connectorHost;
650    }
651
652    public Map<String, ?> getEnvironment() {
653        return environment;
654    }
655
656    public void setEnvironment(Map<String, ?> environment) {
657        this.environment = environment;
658    }
659
660    public boolean isAllowRemoteAddressInMBeanNames() {
661        return allowRemoteAddressInMBeanNames;
662    }
663
664    public void setAllowRemoteAddressInMBeanNames(boolean allowRemoteAddressInMBeanNames) {
665        this.allowRemoteAddressInMBeanNames = allowRemoteAddressInMBeanNames;
666    }
667
668    /**
669     * Allow selective MBeans registration to be suppressed. Any Mbean ObjectName that matches any
670     * of the supplied attribute values will not be registered with the MBeanServer.
671     * eg: "endpoint=dynamicProducer,endpoint=Consumer" will suppress the registration of *all* dynamic producer and consumer mbeans.
672     *
673     * @param commaListOfAttributeKeyValuePairs  the comma separated list of attribute key=value pairs to match.
674     */
675    public void setSuppressMBean(String commaListOfAttributeKeyValuePairs) {
676        this.suppressMBean = commaListOfAttributeKeyValuePairs;
677    }
678
679    public String getSuppressMBean() {
680        return suppressMBean;
681    }
682}