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}