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