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