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