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