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.management.ManagementFactory;
021import java.lang.reflect.Method;
022import java.rmi.AccessException;
023import java.rmi.AlreadyBoundException;
024import java.rmi.NoSuchObjectException;
025import java.rmi.NotBoundException;
026import java.rmi.Remote;
027import java.rmi.RemoteException;
028import java.rmi.registry.Registry;
029import java.rmi.server.UnicastRemoteObject;
030import java.util.HashMap;
031import java.util.LinkedList;
032import java.util.List;
033import java.util.Map;
034import java.util.Set;
035import java.util.concurrent.ConcurrentHashMap;
036import java.util.concurrent.CountDownLatch;
037import java.util.concurrent.TimeUnit;
038import java.util.concurrent.atomic.AtomicBoolean;
039
040import javax.management.Attribute;
041import javax.management.InstanceNotFoundException;
042import javax.management.JMException;
043import javax.management.MBeanServer;
044import javax.management.MBeanServerFactory;
045import javax.management.MBeanServerInvocationHandler;
046import javax.management.MalformedObjectNameException;
047import javax.management.ObjectInstance;
048import javax.management.ObjectName;
049import javax.management.QueryExp;
050import javax.management.remote.JMXConnectorServer;
051import javax.management.remote.JMXServiceURL;
052import javax.management.remote.rmi.RMIConnectorServer;
053import javax.management.remote.rmi.RMIJRMPServerImpl;
054
055import org.apache.activemq.Service;
056import org.slf4j.Logger;
057import org.slf4j.LoggerFactory;
058import org.slf4j.MDC;
059
060/**
061 * An abstraction over JMX mbean registration
062 *
063 * @org.apache.xbean.XBean
064 *
065 */
066public class ManagementContext implements Service {
067
068    /**
069     * Default activemq domain
070     */
071    public static final String DEFAULT_DOMAIN = "org.apache.activemq";
072
073    /**
074     * Default registry lookup name
075     */
076    public static final String DEFAULT_LOOKUP_NAME = "jmxrmi";
077
078    static {
079        String option = Boolean.FALSE.toString();
080        try {
081            option = System.getProperty("org.apache.activemq.broker.jmx.createConnector", "false");
082        } catch (Exception ex) {
083        }
084
085        DEFAULT_CREATE_CONNECTOR = Boolean.valueOf(option);
086    }
087
088    public static final boolean DEFAULT_CREATE_CONNECTOR;
089
090    private static final Logger LOG = LoggerFactory.getLogger(ManagementContext.class);
091    private MBeanServer beanServer;
092    private String jmxDomainName = DEFAULT_DOMAIN;
093    private boolean useMBeanServer = true;
094    private boolean createMBeanServer = true;
095    private boolean locallyCreateMBeanServer;
096    private boolean createConnector = DEFAULT_CREATE_CONNECTOR;
097    private boolean findTigerMbeanServer = true;
098    private String connectorHost = "localhost";
099    private int connectorPort = 1099;
100    private Map<String, ?> environment;
101    private int rmiServerPort;
102    private String connectorPath = "/jmxrmi";
103    private String lookupName = DEFAULT_LOOKUP_NAME;
104    private final AtomicBoolean started = new AtomicBoolean(false);
105    private final CountDownLatch connectorStarted = new CountDownLatch(1);
106    private JMXConnectorServer connectorServer;
107    private ObjectName namingServiceObjectName;
108    private Registry registry;
109    private final Map<ObjectName, ObjectName> registeredMBeanNames = new ConcurrentHashMap<ObjectName, ObjectName>();
110    private boolean allowRemoteAddressInMBeanNames = true;
111    private String brokerName;
112    private String suppressMBean;
113    private List<ObjectName> suppressMBeanList;
114    private Remote serverStub;
115    private RMIJRMPServerImpl server;
116
117    public ManagementContext() {
118        this(null);
119    }
120
121    public ManagementContext(MBeanServer server) {
122        this.beanServer = server;
123    }
124
125    @Override
126    public void start() throws Exception {
127        // lets force the MBeanServer to be created if needed
128        if (started.compareAndSet(false, true)) {
129
130            populateMBeanSuppressionMap();
131
132            // fallback and use localhost
133            if (connectorHost == null) {
134                connectorHost = "localhost";
135            }
136
137            // force mbean server to be looked up, so we have it
138            getMBeanServer();
139
140            if (connectorServer != null) {
141                try {
142                    if (getMBeanServer().isRegistered(namingServiceObjectName)) {
143                        LOG.debug("Invoking start on mbean: {}", namingServiceObjectName);
144                        getMBeanServer().invoke(namingServiceObjectName, "start", null, null);
145                    }
146                } catch (Throwable ignore) {
147                    LOG.debug("Error invoking start on MBean {}. This exception is ignored.", namingServiceObjectName, ignore);
148                }
149
150                Thread t = new Thread("JMX connector") {
151                    @Override
152                    public void run() {
153                        // ensure we use MDC logging with the broker name, so people can see the logs if MDC was in use
154                        if (brokerName != null) {
155                            MDC.put("activemq.broker", brokerName);
156                        }
157                        try {
158                            if (started.get() && server != null) {
159                                LOG.debug("Starting JMXConnectorServer...");
160                                try {
161                                    // need to remove MDC as we must not inherit MDC in child threads causing leaks
162                                    MDC.remove("activemq.broker");
163                                    connectorServer.start();
164                                    serverStub = server.toStub();
165                                } finally {
166                                    if (brokerName != null) {
167                                        MDC.put("activemq.broker", brokerName);
168                                    }
169                                    connectorStarted.countDown();
170                                }
171                                LOG.info("JMX consoles can connect to {}", connectorServer.getAddress());
172                            }
173                        } catch (IOException e) {
174                            LOG.warn("Failed to start JMX connector {}. Will restart management to re-create JMX connector, trying to remedy this issue.", e.getMessage());
175                            LOG.debug("Reason for failed JMX connector start", e);
176                        } finally {
177                            MDC.remove("activemq.broker");
178                        }
179                    }
180                };
181                t.setDaemon(true);
182                t.start();
183            }
184        }
185    }
186
187    private void populateMBeanSuppressionMap() throws Exception {
188        if (suppressMBean != null) {
189            suppressMBeanList = new LinkedList<>();
190            for (String pair : suppressMBean.split(",")) {
191                suppressMBeanList.add(new ObjectName(jmxDomainName + ":*," + pair));
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 (connectorStarted.await(10, TimeUnit.SECONDS)) {
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    public 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 connectorStarted.getCount() == 0 || (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    protected boolean isAllowedToRegister(ObjectName name) {
431        boolean result = true;
432        if (suppressMBean != null && suppressMBeanList != null) {
433            for (ObjectName attr : suppressMBeanList) {
434                if (attr.apply(name)) {
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 = new JmxRegistry(connectorPort);
565            }
566
567            namingServiceObjectName = ObjectName.getInstance("naming:type=rmiregistry");
568
569            // Do not use the createMBean as the mx4j jar may not be in the
570            // same class loader than the server
571            Class<?> cl = Class.forName("mx4j.tools.naming.NamingService");
572            mbeanServer.registerMBean(cl.newInstance(), namingServiceObjectName);
573
574            // set the naming port
575            Attribute attr = new Attribute("Port", Integer.valueOf(connectorPort));
576            mbeanServer.setAttribute(namingServiceObjectName, attr);
577        } catch(ClassNotFoundException e) {
578            LOG.debug("Probably not using JRE 1.4: {}", e.getLocalizedMessage());
579        } catch (Throwable e) {
580            LOG.debug("Failed to create local registry. This exception will be ignored.", e);
581        }
582
583        // Create the JMXConnectorServer
584        String rmiServer = "";
585        if (rmiServerPort != 0) {
586            // This is handy to use if you have a firewall and need to force JMX to use fixed ports.
587            rmiServer = ""+getConnectorHost()+":" + rmiServerPort;
588        }
589
590        server = new RMIJRMPServerImpl(connectorPort, null, null, environment);
591
592        final String serviceURL = "service:jmx:rmi://" + rmiServer + "/jndi/rmi://" +getConnectorHost()+":" + connectorPort + connectorPath;
593        final JMXServiceURL url = new JMXServiceURL(serviceURL);
594
595        connectorServer = new RMIConnectorServer(url, environment, server, ManagementFactory.getPlatformMBeanServer());
596        LOG.debug("Created JMXConnectorServer {}", connectorServer);
597    }
598
599    public String getConnectorPath() {
600        return connectorPath;
601    }
602
603    public void setConnectorPath(String connectorPath) {
604        this.connectorPath = connectorPath;
605
606        if (connectorPath == null || connectorPath.length() == 0) {
607            this.lookupName = DEFAULT_LOOKUP_NAME;
608        } else {
609            this.lookupName = connectorPath.replaceAll("^/+", "").replaceAll("/+$", "");
610        }
611    }
612
613    public int getConnectorPort() {
614        return connectorPort;
615    }
616
617    /**
618     * @org.apache.xbean.Property propertyEditor="org.apache.activemq.util.MemoryIntPropertyEditor"
619     */
620    public void setConnectorPort(int connectorPort) {
621        this.connectorPort = connectorPort;
622    }
623
624    public int getRmiServerPort() {
625        return rmiServerPort;
626    }
627
628    /**
629     * @org.apache.xbean.Property propertyEditor="org.apache.activemq.util.MemoryIntPropertyEditor"
630     */
631    public void setRmiServerPort(int rmiServerPort) {
632        this.rmiServerPort = rmiServerPort;
633    }
634
635    public boolean isCreateConnector() {
636        return createConnector;
637    }
638
639    /**
640     * @org.apache.xbean.Property propertyEditor="org.apache.activemq.util.BooleanEditor"
641     */
642    public void setCreateConnector(boolean createConnector) {
643        this.createConnector = createConnector;
644    }
645
646    /**
647     * Get the connectorHost
648     * @return the connectorHost
649     */
650    public String getConnectorHost() {
651        return this.connectorHost;
652    }
653
654    /**
655     * Set the connectorHost
656     * @param connectorHost the connectorHost to set
657     */
658    public void setConnectorHost(String connectorHost) {
659        this.connectorHost = connectorHost;
660    }
661
662    public Map<String, ?> getEnvironment() {
663        return environment;
664    }
665
666    public void setEnvironment(Map<String, ?> environment) {
667        this.environment = environment;
668    }
669
670    public boolean isAllowRemoteAddressInMBeanNames() {
671        return allowRemoteAddressInMBeanNames;
672    }
673
674    public void setAllowRemoteAddressInMBeanNames(boolean allowRemoteAddressInMBeanNames) {
675        this.allowRemoteAddressInMBeanNames = allowRemoteAddressInMBeanNames;
676    }
677
678    /**
679     * Allow selective MBeans registration to be suppressed. Any Mbean ObjectName that matches any
680     * of the supplied attribute values will not be registered with the MBeanServer.
681     * eg: "endpoint=dynamicProducer,endpoint=Consumer" will suppress the registration of *all* dynamic producer and consumer mbeans.
682     *
683     * @param commaListOfAttributeKeyValuePairs  the comma separated list of attribute key=value pairs to match.
684     */
685    public void setSuppressMBean(String commaListOfAttributeKeyValuePairs) {
686        this.suppressMBean = commaListOfAttributeKeyValuePairs;
687    }
688
689    public String getSuppressMBean() {
690        return suppressMBean;
691    }
692
693    /*
694     * Better to use the internal API than re-invent the wheel.
695     */
696    @SuppressWarnings("restriction")
697    private class JmxRegistry extends sun.rmi.registry.RegistryImpl {
698
699
700        public JmxRegistry(int port) throws RemoteException {
701            super(port);
702        }
703
704        @Override
705        public Remote lookup(String s) throws RemoteException, NotBoundException {
706            return lookupName.equals(s) ? serverStub : null;
707        }
708
709        @Override
710        public void bind(String s, Remote remote) throws RemoteException, AlreadyBoundException, AccessException {
711        }
712
713        @Override
714        public void unbind(String s) throws RemoteException, NotBoundException, AccessException {
715        }
716
717        @Override
718        public void rebind(String s, Remote remote) throws RemoteException, AccessException {
719        }
720
721        @Override
722        public String[] list() throws RemoteException {
723            return new String[] {lookupName};
724        }
725    }
726}