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