001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.jmx.export; 018 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.LinkedHashMap; 024import java.util.LinkedHashSet; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028 029import javax.management.DynamicMBean; 030import javax.management.JMException; 031import javax.management.MBeanException; 032import javax.management.MBeanServer; 033import javax.management.MalformedObjectNameException; 034import javax.management.NotCompliantMBeanException; 035import javax.management.NotificationListener; 036import javax.management.ObjectName; 037import javax.management.StandardMBean; 038import javax.management.modelmbean.ModelMBean; 039import javax.management.modelmbean.ModelMBeanInfo; 040import javax.management.modelmbean.RequiredModelMBean; 041 042import org.springframework.aop.framework.ProxyFactory; 043import org.springframework.aop.scope.ScopedProxyUtils; 044import org.springframework.aop.support.AopUtils; 045import org.springframework.aop.target.LazyInitTargetSource; 046import org.springframework.beans.factory.BeanClassLoaderAware; 047import org.springframework.beans.factory.BeanFactory; 048import org.springframework.beans.factory.BeanFactoryAware; 049import org.springframework.beans.factory.CannotLoadBeanClassException; 050import org.springframework.beans.factory.DisposableBean; 051import org.springframework.beans.factory.InitializingBean; 052import org.springframework.beans.factory.ListableBeanFactory; 053import org.springframework.beans.factory.SmartInitializingSingleton; 054import org.springframework.beans.factory.config.ConfigurableBeanFactory; 055import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; 056import org.springframework.core.Constants; 057import org.springframework.jmx.export.assembler.AutodetectCapableMBeanInfoAssembler; 058import org.springframework.jmx.export.assembler.MBeanInfoAssembler; 059import org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler; 060import org.springframework.jmx.export.naming.KeyNamingStrategy; 061import org.springframework.jmx.export.naming.ObjectNamingStrategy; 062import org.springframework.jmx.export.naming.SelfNaming; 063import org.springframework.jmx.export.notification.ModelMBeanNotificationPublisher; 064import org.springframework.jmx.export.notification.NotificationPublisherAware; 065import org.springframework.jmx.support.JmxUtils; 066import org.springframework.jmx.support.MBeanRegistrationSupport; 067import org.springframework.lang.Nullable; 068import org.springframework.util.Assert; 069import org.springframework.util.ClassUtils; 070import org.springframework.util.CollectionUtils; 071import org.springframework.util.ObjectUtils; 072 073/** 074 * JMX exporter that allows for exposing any <i>Spring-managed bean</i> to a 075 * JMX {@link javax.management.MBeanServer}, without the need to define any 076 * JMX-specific information in the bean classes. 077 * 078 * <p>If a bean implements one of the JMX management interfaces, MBeanExporter can 079 * simply register the MBean with the server through its autodetection process. 080 * 081 * <p>If a bean does not implement one of the JMX management interfaces, MBeanExporter 082 * will create the management information using the supplied {@link MBeanInfoAssembler}. 083 * 084 * <p>A list of {@link MBeanExporterListener MBeanExporterListeners} can be registered 085 * via the {@link #setListeners(MBeanExporterListener[]) listeners} property, allowing 086 * application code to be notified of MBean registration and unregistration events. 087 * 088 * <p>This exporter is compatible with MBeans as well as MXBeans. 089 * 090 * @author Rob Harrop 091 * @author Juergen Hoeller 092 * @author Rick Evans 093 * @author Mark Fisher 094 * @author Stephane Nicoll 095 * @since 1.2 096 * @see #setBeans 097 * @see #setAutodetect 098 * @see #setAssembler 099 * @see #setListeners 100 * @see org.springframework.jmx.export.assembler.MBeanInfoAssembler 101 * @see MBeanExporterListener 102 */ 103public class MBeanExporter extends MBeanRegistrationSupport implements MBeanExportOperations, 104 BeanClassLoaderAware, BeanFactoryAware, InitializingBean, SmartInitializingSingleton, DisposableBean { 105 106 /** 107 * Autodetection mode indicating that no autodetection should be used. 108 */ 109 public static final int AUTODETECT_NONE = 0; 110 111 /** 112 * Autodetection mode indicating that only valid MBeans should be autodetected. 113 */ 114 public static final int AUTODETECT_MBEAN = 1; 115 116 /** 117 * Autodetection mode indicating that only the {@link MBeanInfoAssembler} should be able 118 * to autodetect beans. 119 */ 120 public static final int AUTODETECT_ASSEMBLER = 2; 121 122 /** 123 * Autodetection mode indicating that all autodetection mechanisms should be used. 124 */ 125 public static final int AUTODETECT_ALL = AUTODETECT_MBEAN | AUTODETECT_ASSEMBLER; 126 127 128 /** 129 * Wildcard used to map a {@link javax.management.NotificationListener} 130 * to all MBeans registered by the {@code MBeanExporter}. 131 */ 132 private static final String WILDCARD = "*"; 133 134 /** Constant for the JMX {@code mr_type} "ObjectReference". */ 135 private static final String MR_TYPE_OBJECT_REFERENCE = "ObjectReference"; 136 137 /** Prefix for the autodetect constants defined in this class. */ 138 private static final String CONSTANT_PREFIX_AUTODETECT = "AUTODETECT_"; 139 140 141 /** Constants instance for this class. */ 142 private static final Constants constants = new Constants(MBeanExporter.class); 143 144 /** The beans to be exposed as JMX managed resources, with JMX names as keys. */ 145 @Nullable 146 private Map<String, Object> beans; 147 148 /** The autodetect mode to use for this MBeanExporter. */ 149 @Nullable 150 private Integer autodetectMode; 151 152 /** Whether to eagerly initialize candidate beans when autodetecting MBeans. */ 153 private boolean allowEagerInit = false; 154 155 /** Stores the MBeanInfoAssembler to use for this exporter. */ 156 private MBeanInfoAssembler assembler = new SimpleReflectiveMBeanInfoAssembler(); 157 158 /** The strategy to use for creating ObjectNames for an object. */ 159 private ObjectNamingStrategy namingStrategy = new KeyNamingStrategy(); 160 161 /** Indicates whether Spring should modify generated ObjectNames. */ 162 private boolean ensureUniqueRuntimeObjectNames = true; 163 164 /** Indicates whether Spring should expose the managed resource ClassLoader in the MBean. */ 165 private boolean exposeManagedResourceClassLoader = true; 166 167 /** A set of bean names that should be excluded from autodetection. */ 168 private Set<String> excludedBeans = new HashSet<>(); 169 170 /** The MBeanExporterListeners registered with this exporter. */ 171 @Nullable 172 private MBeanExporterListener[] listeners; 173 174 /** The NotificationListeners to register for the MBeans registered by this exporter. */ 175 @Nullable 176 private NotificationListenerBean[] notificationListeners; 177 178 /** Map of actually registered NotificationListeners. */ 179 private final Map<NotificationListenerBean, ObjectName[]> registeredNotificationListeners = new LinkedHashMap<>(); 180 181 /** Stores the ClassLoader to use for generating lazy-init proxies. */ 182 @Nullable 183 private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); 184 185 /** Stores the BeanFactory for use in autodetection process. */ 186 @Nullable 187 private ListableBeanFactory beanFactory; 188 189 190 /** 191 * Supply a {@code Map} of beans to be registered with the JMX 192 * {@code MBeanServer}. 193 * <p>The String keys are the basis for the creation of JMX object names. 194 * By default, a JMX {@code ObjectName} will be created straight 195 * from the given key. This can be customized through specifying a 196 * custom {@code NamingStrategy}. 197 * <p>Both bean instances and bean names are allowed as values. 198 * Bean instances are typically linked in through bean references. 199 * Bean names will be resolved as beans in the current factory, respecting 200 * lazy-init markers (that is, not triggering initialization of such beans). 201 * @param beans a Map with JMX names as keys and bean instances or bean names 202 * as values 203 * @see #setNamingStrategy 204 * @see org.springframework.jmx.export.naming.KeyNamingStrategy 205 * @see javax.management.ObjectName#ObjectName(String) 206 */ 207 public void setBeans(Map<String, Object> beans) { 208 this.beans = beans; 209 } 210 211 /** 212 * Set whether to autodetect MBeans in the bean factory that this exporter 213 * runs in. Will also ask an {@code AutodetectCapableMBeanInfoAssembler} 214 * if available. 215 * <p>This feature is turned off by default. Explicitly specify 216 * {@code true} here to enable autodetection. 217 * @see #setAssembler 218 * @see AutodetectCapableMBeanInfoAssembler 219 * @see #isMBean 220 */ 221 public void setAutodetect(boolean autodetect) { 222 this.autodetectMode = (autodetect ? AUTODETECT_ALL : AUTODETECT_NONE); 223 } 224 225 /** 226 * Set the autodetection mode to use. 227 * @throws IllegalArgumentException if the supplied value is not 228 * one of the {@code AUTODETECT_} constants 229 * @see #setAutodetectModeName(String) 230 * @see #AUTODETECT_ALL 231 * @see #AUTODETECT_ASSEMBLER 232 * @see #AUTODETECT_MBEAN 233 * @see #AUTODETECT_NONE 234 */ 235 public void setAutodetectMode(int autodetectMode) { 236 if (!constants.getValues(CONSTANT_PREFIX_AUTODETECT).contains(autodetectMode)) { 237 throw new IllegalArgumentException("Only values of autodetect constants allowed"); 238 } 239 this.autodetectMode = autodetectMode; 240 } 241 242 /** 243 * Set the autodetection mode to use by name. 244 * @throws IllegalArgumentException if the supplied value is not resolvable 245 * to one of the {@code AUTODETECT_} constants or is {@code null} 246 * @see #setAutodetectMode(int) 247 * @see #AUTODETECT_ALL 248 * @see #AUTODETECT_ASSEMBLER 249 * @see #AUTODETECT_MBEAN 250 * @see #AUTODETECT_NONE 251 */ 252 public void setAutodetectModeName(String constantName) { 253 if (!constantName.startsWith(CONSTANT_PREFIX_AUTODETECT)) { 254 throw new IllegalArgumentException("Only autodetect constants allowed"); 255 } 256 this.autodetectMode = (Integer) constants.asNumber(constantName); 257 } 258 259 /** 260 * Specify whether to allow eager initialization of candidate beans 261 * when autodetecting MBeans in the Spring application context. 262 * <p>Default is "false", respecting lazy-init flags on bean definitions. 263 * Switch this to "true" in order to search lazy-init beans as well, 264 * including FactoryBean-produced objects that haven't been initialized yet. 265 */ 266 public void setAllowEagerInit(boolean allowEagerInit) { 267 this.allowEagerInit = allowEagerInit; 268 } 269 270 /** 271 * Set the implementation of the {@code MBeanInfoAssembler} interface to use 272 * for this exporter. Default is a {@code SimpleReflectiveMBeanInfoAssembler}. 273 * <p>The passed-in assembler can optionally implement the 274 * {@code AutodetectCapableMBeanInfoAssembler} interface, which enables it 275 * to participate in the exporter's MBean autodetection process. 276 * @see org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler 277 * @see org.springframework.jmx.export.assembler.AutodetectCapableMBeanInfoAssembler 278 * @see org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler 279 * @see #setAutodetect 280 */ 281 public void setAssembler(MBeanInfoAssembler assembler) { 282 this.assembler = assembler; 283 } 284 285 /** 286 * Set the implementation of the {@code ObjectNamingStrategy} interface 287 * to use for this exporter. Default is a {@code KeyNamingStrategy}. 288 * @see org.springframework.jmx.export.naming.KeyNamingStrategy 289 * @see org.springframework.jmx.export.naming.MetadataNamingStrategy 290 */ 291 public void setNamingStrategy(ObjectNamingStrategy namingStrategy) { 292 this.namingStrategy = namingStrategy; 293 } 294 295 /** 296 * Indicates whether Spring should ensure that {@link ObjectName ObjectNames} 297 * generated by the configured {@link ObjectNamingStrategy} for 298 * runtime-registered MBeans ({@link #registerManagedResource}) should get 299 * modified: to ensure uniqueness for every instance of a managed {@code Class}. 300 * <p>The default value is {@code true}. 301 * @see #registerManagedResource 302 * @see JmxUtils#appendIdentityToObjectName(javax.management.ObjectName, Object) 303 */ 304 public void setEnsureUniqueRuntimeObjectNames(boolean ensureUniqueRuntimeObjectNames) { 305 this.ensureUniqueRuntimeObjectNames = ensureUniqueRuntimeObjectNames; 306 } 307 308 /** 309 * Indicates whether or not the managed resource should be exposed on the 310 * {@link Thread#getContextClassLoader() thread context ClassLoader} before 311 * allowing any invocations on the MBean to occur. 312 * <p>The default value is {@code true}, exposing a {@link SpringModelMBean} 313 * which performs thread context ClassLoader management. Switch this flag off to 314 * expose a standard JMX {@link javax.management.modelmbean.RequiredModelMBean}. 315 */ 316 public void setExposeManagedResourceClassLoader(boolean exposeManagedResourceClassLoader) { 317 this.exposeManagedResourceClassLoader = exposeManagedResourceClassLoader; 318 } 319 320 /** 321 * Set the list of names for beans that should be excluded from autodetection. 322 */ 323 public void setExcludedBeans(String... excludedBeans) { 324 this.excludedBeans.clear(); 325 Collections.addAll(this.excludedBeans, excludedBeans); 326 } 327 328 /** 329 * Add the name of bean that should be excluded from autodetection. 330 */ 331 public void addExcludedBean(String excludedBean) { 332 Assert.notNull(excludedBean, "ExcludedBean must not be null"); 333 this.excludedBeans.add(excludedBean); 334 } 335 336 /** 337 * Set the {@code MBeanExporterListener}s that should be notified 338 * of MBean registration and unregistration events. 339 * @see MBeanExporterListener 340 */ 341 public void setListeners(MBeanExporterListener... listeners) { 342 this.listeners = listeners; 343 } 344 345 /** 346 * Set the {@link NotificationListenerBean NotificationListenerBeans} 347 * containing the 348 * {@link javax.management.NotificationListener NotificationListeners} 349 * that will be registered with the {@link MBeanServer}. 350 * @see #setNotificationListenerMappings(java.util.Map) 351 * @see NotificationListenerBean 352 */ 353 public void setNotificationListeners(NotificationListenerBean... notificationListeners) { 354 this.notificationListeners = notificationListeners; 355 } 356 357 /** 358 * Set the {@link NotificationListener NotificationListeners} to register 359 * with the {@link javax.management.MBeanServer}. 360 * <P>The key of each entry in the {@code Map} is a {@link String} 361 * representation of the {@link javax.management.ObjectName} or the bean 362 * name of the MBean the listener should be registered for. Specifying an 363 * asterisk ({@code *}) for a key will cause the listener to be 364 * associated with all MBeans registered by this class at startup time. 365 * <p>The value of each entry is the 366 * {@link javax.management.NotificationListener} to register. For more 367 * advanced options such as registering 368 * {@link javax.management.NotificationFilter NotificationFilters} and 369 * handback objects see {@link #setNotificationListeners(NotificationListenerBean[])}. 370 */ 371 public void setNotificationListenerMappings(Map<?, ? extends NotificationListener> listeners) { 372 Assert.notNull(listeners, "'listeners' must not be null"); 373 List<NotificationListenerBean> notificationListeners = 374 new ArrayList<>(listeners.size()); 375 376 listeners.forEach((key, listener) -> { 377 // Get the listener from the map value. 378 NotificationListenerBean bean = new NotificationListenerBean(listener); 379 // Get the ObjectName from the map key. 380 if (key != null && !WILDCARD.equals(key)) { 381 // This listener is mapped to a specific ObjectName. 382 bean.setMappedObjectName(key); 383 } 384 notificationListeners.add(bean); 385 }); 386 387 this.notificationListeners = notificationListeners.toArray(new NotificationListenerBean[0]); 388 } 389 390 @Override 391 public void setBeanClassLoader(ClassLoader classLoader) { 392 this.beanClassLoader = classLoader; 393 } 394 395 /** 396 * This callback is only required for resolution of bean names in the 397 * {@link #setBeans(java.util.Map) "beans"} {@link Map} and for 398 * autodetection of MBeans (in the latter case, a 399 * {@code ListableBeanFactory} is required). 400 * @see #setBeans 401 * @see #setAutodetect 402 */ 403 @Override 404 public void setBeanFactory(BeanFactory beanFactory) { 405 if (beanFactory instanceof ListableBeanFactory) { 406 this.beanFactory = (ListableBeanFactory) beanFactory; 407 } 408 else { 409 logger.debug("MBeanExporter not running in a ListableBeanFactory: autodetection of MBeans not available."); 410 } 411 } 412 413 414 //--------------------------------------------------------------------- 415 // Lifecycle in bean factory: automatically register/unregister beans 416 //--------------------------------------------------------------------- 417 418 @Override 419 public void afterPropertiesSet() { 420 // If no server was provided then try to find one. This is useful in an environment 421 // where there is already an MBeanServer loaded. 422 if (this.server == null) { 423 this.server = JmxUtils.locateMBeanServer(); 424 } 425 } 426 427 /** 428 * Kick off bean registration automatically after the regular singleton instantiation phase. 429 * @see #registerBeans() 430 */ 431 @Override 432 public void afterSingletonsInstantiated() { 433 try { 434 logger.debug("Registering beans for JMX exposure on startup"); 435 registerBeans(); 436 registerNotificationListeners(); 437 } 438 catch (RuntimeException ex) { 439 // Unregister beans already registered by this exporter. 440 unregisterNotificationListeners(); 441 unregisterBeans(); 442 throw ex; 443 } 444 } 445 446 /** 447 * Unregisters all beans that this exported has exposed via JMX 448 * when the enclosing {@code ApplicationContext} is destroyed. 449 */ 450 @Override 451 public void destroy() { 452 logger.debug("Unregistering JMX-exposed beans on shutdown"); 453 unregisterNotificationListeners(); 454 unregisterBeans(); 455 } 456 457 458 //--------------------------------------------------------------------- 459 // Implementation of MBeanExportOperations interface 460 //--------------------------------------------------------------------- 461 462 @Override 463 public ObjectName registerManagedResource(Object managedResource) throws MBeanExportException { 464 Assert.notNull(managedResource, "Managed resource must not be null"); 465 ObjectName objectName; 466 try { 467 objectName = getObjectName(managedResource, null); 468 if (this.ensureUniqueRuntimeObjectNames) { 469 objectName = JmxUtils.appendIdentityToObjectName(objectName, managedResource); 470 } 471 } 472 catch (Throwable ex) { 473 throw new MBeanExportException("Unable to generate ObjectName for MBean [" + managedResource + "]", ex); 474 } 475 registerManagedResource(managedResource, objectName); 476 return objectName; 477 } 478 479 @Override 480 public void registerManagedResource(Object managedResource, ObjectName objectName) throws MBeanExportException { 481 Assert.notNull(managedResource, "Managed resource must not be null"); 482 Assert.notNull(objectName, "ObjectName must not be null"); 483 try { 484 if (isMBean(managedResource.getClass())) { 485 doRegister(managedResource, objectName); 486 } 487 else { 488 ModelMBean mbean = createAndConfigureMBean(managedResource, managedResource.getClass().getName()); 489 doRegister(mbean, objectName); 490 injectNotificationPublisherIfNecessary(managedResource, mbean, objectName); 491 } 492 } 493 catch (JMException ex) { 494 throw new UnableToRegisterMBeanException( 495 "Unable to register MBean [" + managedResource + "] with object name [" + objectName + "]", ex); 496 } 497 } 498 499 @Override 500 public void unregisterManagedResource(ObjectName objectName) { 501 Assert.notNull(objectName, "ObjectName must not be null"); 502 doUnregister(objectName); 503 } 504 505 506 //--------------------------------------------------------------------- 507 // Exporter implementation 508 //--------------------------------------------------------------------- 509 510 /** 511 * Register the defined beans with the {@link MBeanServer}. 512 * <p>Each bean is exposed to the {@code MBeanServer} via a 513 * {@code ModelMBean}. The actual implementation of the 514 * {@code ModelMBean} interface used depends on the implementation of 515 * the {@code ModelMBeanProvider} interface that is configured. By 516 * default the {@code RequiredModelMBean} class that is supplied with 517 * all JMX implementations is used. 518 * <p>The management interface produced for each bean is dependent on the 519 * {@code MBeanInfoAssembler} implementation being used. The 520 * {@code ObjectName} given to each bean is dependent on the 521 * implementation of the {@code ObjectNamingStrategy} interface being used. 522 */ 523 protected void registerBeans() { 524 // The beans property may be null, for example if we are relying solely on autodetection. 525 if (this.beans == null) { 526 this.beans = new HashMap<>(); 527 // Use AUTODETECT_ALL as default in no beans specified explicitly. 528 if (this.autodetectMode == null) { 529 this.autodetectMode = AUTODETECT_ALL; 530 } 531 } 532 533 // Perform autodetection, if desired. 534 int mode = (this.autodetectMode != null ? this.autodetectMode : AUTODETECT_NONE); 535 if (mode != AUTODETECT_NONE) { 536 if (this.beanFactory == null) { 537 throw new MBeanExportException("Cannot autodetect MBeans if not running in a BeanFactory"); 538 } 539 if (mode == AUTODETECT_MBEAN || mode == AUTODETECT_ALL) { 540 // Autodetect any beans that are already MBeans. 541 logger.debug("Autodetecting user-defined JMX MBeans"); 542 autodetect(this.beans, (beanClass, beanName) -> isMBean(beanClass)); 543 } 544 // Allow the assembler a chance to vote for bean inclusion. 545 if ((mode == AUTODETECT_ASSEMBLER || mode == AUTODETECT_ALL) && 546 this.assembler instanceof AutodetectCapableMBeanInfoAssembler) { 547 autodetect(this.beans, ((AutodetectCapableMBeanInfoAssembler) this.assembler)::includeBean); 548 } 549 } 550 551 if (!this.beans.isEmpty()) { 552 this.beans.forEach((beanName, instance) -> registerBeanNameOrInstance(instance, beanName)); 553 } 554 } 555 556 /** 557 * Return whether the specified bean definition should be considered as lazy-init. 558 * @param beanFactory the bean factory that is supposed to contain the bean definition 559 * @param beanName the name of the bean to check 560 * @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory#getBeanDefinition 561 * @see org.springframework.beans.factory.config.BeanDefinition#isLazyInit 562 */ 563 protected boolean isBeanDefinitionLazyInit(ListableBeanFactory beanFactory, String beanName) { 564 return (beanFactory instanceof ConfigurableListableBeanFactory && beanFactory.containsBeanDefinition(beanName) && 565 ((ConfigurableListableBeanFactory) beanFactory).getBeanDefinition(beanName).isLazyInit()); 566 } 567 568 /** 569 * Register an individual bean with the {@link #setServer MBeanServer}. 570 * <p>This method is responsible for deciding <strong>how</strong> a bean 571 * should be exposed to the {@code MBeanServer}. Specifically, if the 572 * supplied {@code mapValue} is the name of a bean that is configured 573 * for lazy initialization, then a proxy to the resource is registered with 574 * the {@code MBeanServer} so that the lazy load behavior is 575 * honored. If the bean is already an MBean then it will be registered 576 * directly with the {@code MBeanServer} without any intervention. For 577 * all other beans or bean names, the resource itself is registered with 578 * the {@code MBeanServer} directly. 579 * @param mapValue the value configured for this bean in the beans map; 580 * may be either the {@code String} name of a bean, or the bean itself 581 * @param beanKey the key associated with this bean in the beans map 582 * @return the {@code ObjectName} under which the resource was registered 583 * @throws MBeanExportException if the export failed 584 * @see #setBeans 585 * @see #registerBeanInstance 586 * @see #registerLazyInit 587 */ 588 protected ObjectName registerBeanNameOrInstance(Object mapValue, String beanKey) throws MBeanExportException { 589 try { 590 if (mapValue instanceof String) { 591 // Bean name pointing to a potentially lazy-init bean in the factory. 592 if (this.beanFactory == null) { 593 throw new MBeanExportException("Cannot resolve bean names if not running in a BeanFactory"); 594 } 595 String beanName = (String) mapValue; 596 if (isBeanDefinitionLazyInit(this.beanFactory, beanName)) { 597 ObjectName objectName = registerLazyInit(beanName, beanKey); 598 replaceNotificationListenerBeanNameKeysIfNecessary(beanName, objectName); 599 return objectName; 600 } 601 else { 602 Object bean = this.beanFactory.getBean(beanName); 603 ObjectName objectName = registerBeanInstance(bean, beanKey); 604 replaceNotificationListenerBeanNameKeysIfNecessary(beanName, objectName); 605 return objectName; 606 } 607 } 608 else { 609 // Plain bean instance -> register it directly. 610 if (this.beanFactory != null) { 611 Map<String, ?> beansOfSameType = 612 this.beanFactory.getBeansOfType(mapValue.getClass(), false, this.allowEagerInit); 613 for (Map.Entry<String, ?> entry : beansOfSameType.entrySet()) { 614 if (entry.getValue() == mapValue) { 615 String beanName = entry.getKey(); 616 ObjectName objectName = registerBeanInstance(mapValue, beanKey); 617 replaceNotificationListenerBeanNameKeysIfNecessary(beanName, objectName); 618 return objectName; 619 } 620 } 621 } 622 return registerBeanInstance(mapValue, beanKey); 623 } 624 } 625 catch (Throwable ex) { 626 throw new UnableToRegisterMBeanException( 627 "Unable to register MBean [" + mapValue + "] with key '" + beanKey + "'", ex); 628 } 629 } 630 631 /** 632 * Replace any bean names used as keys in the {@code NotificationListener} 633 * mappings with their corresponding {@code ObjectName} values. 634 * @param beanName the name of the bean to be registered 635 * @param objectName the {@code ObjectName} under which the bean will be registered 636 * with the {@code MBeanServer} 637 */ 638 private void replaceNotificationListenerBeanNameKeysIfNecessary(String beanName, ObjectName objectName) { 639 if (this.notificationListeners != null) { 640 for (NotificationListenerBean notificationListener : this.notificationListeners) { 641 notificationListener.replaceObjectName(beanName, objectName); 642 } 643 } 644 } 645 646 /** 647 * Registers an existing MBean or an MBean adapter for a plain bean 648 * with the {@code MBeanServer}. 649 * @param bean the bean to register, either an MBean or a plain bean 650 * @param beanKey the key associated with this bean in the beans map 651 * @return the {@code ObjectName} under which the bean was registered 652 * with the {@code MBeanServer} 653 */ 654 private ObjectName registerBeanInstance(Object bean, String beanKey) throws JMException { 655 ObjectName objectName = getObjectName(bean, beanKey); 656 Object mbeanToExpose = null; 657 if (isMBean(bean.getClass())) { 658 mbeanToExpose = bean; 659 } 660 else { 661 DynamicMBean adaptedBean = adaptMBeanIfPossible(bean); 662 if (adaptedBean != null) { 663 mbeanToExpose = adaptedBean; 664 } 665 } 666 667 if (mbeanToExpose != null) { 668 if (logger.isDebugEnabled()) { 669 logger.debug("Located MBean '" + beanKey + "': registering with JMX server as MBean [" + 670 objectName + "]"); 671 } 672 doRegister(mbeanToExpose, objectName); 673 } 674 else { 675 if (logger.isDebugEnabled()) { 676 logger.debug("Located managed bean '" + beanKey + "': registering with JMX server as MBean [" + 677 objectName + "]"); 678 } 679 ModelMBean mbean = createAndConfigureMBean(bean, beanKey); 680 doRegister(mbean, objectName); 681 injectNotificationPublisherIfNecessary(bean, mbean, objectName); 682 } 683 684 return objectName; 685 } 686 687 /** 688 * Register beans that are configured for lazy initialization with the 689 * {@code MBeanServer} indirectly through a proxy. 690 * @param beanName the name of the bean in the {@code BeanFactory} 691 * @param beanKey the key associated with this bean in the beans map 692 * @return the {@code ObjectName} under which the bean was registered 693 * with the {@code MBeanServer} 694 */ 695 private ObjectName registerLazyInit(String beanName, String beanKey) throws JMException { 696 Assert.state(this.beanFactory != null, "No BeanFactory set"); 697 698 ProxyFactory proxyFactory = new ProxyFactory(); 699 proxyFactory.setProxyTargetClass(true); 700 proxyFactory.setFrozen(true); 701 702 if (isMBean(this.beanFactory.getType(beanName))) { 703 // A straight MBean... Let's create a simple lazy-init CGLIB proxy for it. 704 LazyInitTargetSource targetSource = new LazyInitTargetSource(); 705 targetSource.setTargetBeanName(beanName); 706 targetSource.setBeanFactory(this.beanFactory); 707 proxyFactory.setTargetSource(targetSource); 708 709 Object proxy = proxyFactory.getProxy(this.beanClassLoader); 710 ObjectName objectName = getObjectName(proxy, beanKey); 711 if (logger.isDebugEnabled()) { 712 logger.debug("Located MBean '" + beanKey + "': registering with JMX server as lazy-init MBean [" + 713 objectName + "]"); 714 } 715 doRegister(proxy, objectName); 716 return objectName; 717 } 718 719 else { 720 // A simple bean... Let's create a lazy-init ModelMBean proxy with notification support. 721 NotificationPublisherAwareLazyTargetSource targetSource = new NotificationPublisherAwareLazyTargetSource(); 722 targetSource.setTargetBeanName(beanName); 723 targetSource.setBeanFactory(this.beanFactory); 724 proxyFactory.setTargetSource(targetSource); 725 726 Object proxy = proxyFactory.getProxy(this.beanClassLoader); 727 ObjectName objectName = getObjectName(proxy, beanKey); 728 if (logger.isDebugEnabled()) { 729 logger.debug("Located simple bean '" + beanKey + "': registering with JMX server as lazy-init MBean [" + 730 objectName + "]"); 731 } 732 ModelMBean mbean = createAndConfigureMBean(proxy, beanKey); 733 targetSource.setModelMBean(mbean); 734 targetSource.setObjectName(objectName); 735 doRegister(mbean, objectName); 736 return objectName; 737 } 738 } 739 740 /** 741 * Retrieve the {@code ObjectName} for a bean. 742 * <p>If the bean implements the {@code SelfNaming} interface, then the 743 * {@code ObjectName} will be retrieved using {@code SelfNaming.getObjectName()}. 744 * Otherwise, the configured {@code ObjectNamingStrategy} is used. 745 * @param bean the name of the bean in the {@code BeanFactory} 746 * @param beanKey the key associated with the bean in the beans map 747 * @return the {@code ObjectName} for the supplied bean 748 * @throws javax.management.MalformedObjectNameException 749 * if the retrieved {@code ObjectName} is malformed 750 */ 751 protected ObjectName getObjectName(Object bean, @Nullable String beanKey) throws MalformedObjectNameException { 752 if (bean instanceof SelfNaming) { 753 return ((SelfNaming) bean).getObjectName(); 754 } 755 else { 756 return this.namingStrategy.getObjectName(bean, beanKey); 757 } 758 } 759 760 /** 761 * Determine whether the given bean class qualifies as an MBean as-is. 762 * <p>The default implementation delegates to {@link JmxUtils#isMBean}, 763 * which checks for {@link javax.management.DynamicMBean} classes as well 764 * as classes with corresponding "*MBean" interface (Standard MBeans) 765 * or corresponding "*MXBean" interface (Java 6 MXBeans). 766 * @param beanClass the bean class to analyze 767 * @return whether the class qualifies as an MBean 768 * @see org.springframework.jmx.support.JmxUtils#isMBean(Class) 769 */ 770 protected boolean isMBean(@Nullable Class<?> beanClass) { 771 return JmxUtils.isMBean(beanClass); 772 } 773 774 /** 775 * Build an adapted MBean for the given bean instance, if possible. 776 * <p>The default implementation builds a JMX 1.2 StandardMBean 777 * for the target's MBean/MXBean interface in case of an AOP proxy, 778 * delegating the interface's management operations to the proxy. 779 * @param bean the original bean instance 780 * @return the adapted MBean, or {@code null} if not possible 781 */ 782 @SuppressWarnings("unchecked") 783 @Nullable 784 protected DynamicMBean adaptMBeanIfPossible(Object bean) throws JMException { 785 Class<?> targetClass = AopUtils.getTargetClass(bean); 786 if (targetClass != bean.getClass()) { 787 Class<?> ifc = JmxUtils.getMXBeanInterface(targetClass); 788 if (ifc != null) { 789 if (!ifc.isInstance(bean)) { 790 throw new NotCompliantMBeanException("Managed bean [" + bean + 791 "] has a target class with an MXBean interface but does not expose it in the proxy"); 792 } 793 return new StandardMBean(bean, ((Class<Object>) ifc), true); 794 } 795 else { 796 ifc = JmxUtils.getMBeanInterface(targetClass); 797 if (ifc != null) { 798 if (!ifc.isInstance(bean)) { 799 throw new NotCompliantMBeanException("Managed bean [" + bean + 800 "] has a target class with an MBean interface but does not expose it in the proxy"); 801 } 802 return new StandardMBean(bean, ((Class<Object>) ifc)); 803 } 804 } 805 } 806 return null; 807 } 808 809 /** 810 * Creates an MBean that is configured with the appropriate management 811 * interface for the supplied managed resource. 812 * @param managedResource the resource that is to be exported as an MBean 813 * @param beanKey the key associated with the managed bean 814 * @see #createModelMBean() 815 * @see #getMBeanInfo(Object, String) 816 */ 817 protected ModelMBean createAndConfigureMBean(Object managedResource, String beanKey) 818 throws MBeanExportException { 819 try { 820 ModelMBean mbean = createModelMBean(); 821 mbean.setModelMBeanInfo(getMBeanInfo(managedResource, beanKey)); 822 mbean.setManagedResource(managedResource, MR_TYPE_OBJECT_REFERENCE); 823 return mbean; 824 } 825 catch (Throwable ex) { 826 throw new MBeanExportException("Could not create ModelMBean for managed resource [" + 827 managedResource + "] with key '" + beanKey + "'", ex); 828 } 829 } 830 831 /** 832 * Create an instance of a class that implements {@code ModelMBean}. 833 * <p>This method is called to obtain a {@code ModelMBean} instance to 834 * use when registering a bean. This method is called once per bean during the 835 * registration phase and must return a new instance of {@code ModelMBean} 836 * @return a new instance of a class that implements {@code ModelMBean} 837 * @throws javax.management.MBeanException if creation of the ModelMBean failed 838 */ 839 protected ModelMBean createModelMBean() throws MBeanException { 840 return (this.exposeManagedResourceClassLoader ? new SpringModelMBean() : new RequiredModelMBean()); 841 } 842 843 /** 844 * Gets the {@code ModelMBeanInfo} for the bean with the supplied key 845 * and of the supplied type. 846 */ 847 private ModelMBeanInfo getMBeanInfo(Object managedBean, String beanKey) throws JMException { 848 ModelMBeanInfo info = this.assembler.getMBeanInfo(managedBean, beanKey); 849 if (logger.isInfoEnabled() && ObjectUtils.isEmpty(info.getAttributes()) && 850 ObjectUtils.isEmpty(info.getOperations())) { 851 logger.info("Bean with key '" + beanKey + 852 "' has been registered as an MBean but has no exposed attributes or operations"); 853 } 854 return info; 855 } 856 857 858 //--------------------------------------------------------------------- 859 // Autodetection process 860 //--------------------------------------------------------------------- 861 862 /** 863 * Performs the actual autodetection process, delegating to an 864 * {@code AutodetectCallback} instance to vote on the inclusion of a 865 * given bean. 866 * @param callback the {@code AutodetectCallback} to use when deciding 867 * whether to include a bean or not 868 */ 869 private void autodetect(Map<String, Object> beans, AutodetectCallback callback) { 870 Assert.state(this.beanFactory != null, "No BeanFactory set"); 871 Set<String> beanNames = new LinkedHashSet<>(this.beanFactory.getBeanDefinitionCount()); 872 Collections.addAll(beanNames, this.beanFactory.getBeanDefinitionNames()); 873 if (this.beanFactory instanceof ConfigurableBeanFactory) { 874 Collections.addAll(beanNames, ((ConfigurableBeanFactory) this.beanFactory).getSingletonNames()); 875 } 876 877 for (String beanName : beanNames) { 878 if (!isExcluded(beanName) && !isBeanDefinitionAbstract(this.beanFactory, beanName)) { 879 try { 880 Class<?> beanClass = this.beanFactory.getType(beanName); 881 if (beanClass != null && callback.include(beanClass, beanName)) { 882 boolean lazyInit = isBeanDefinitionLazyInit(this.beanFactory, beanName); 883 Object beanInstance = null; 884 if (!lazyInit) { 885 beanInstance = this.beanFactory.getBean(beanName); 886 if (!beanClass.isInstance(beanInstance)) { 887 continue; 888 } 889 } 890 if (!ScopedProxyUtils.isScopedTarget(beanName) && !beans.containsValue(beanName) && 891 (beanInstance == null || 892 !CollectionUtils.containsInstance(beans.values(), beanInstance))) { 893 // Not already registered for JMX exposure. 894 beans.put(beanName, (beanInstance != null ? beanInstance : beanName)); 895 if (logger.isDebugEnabled()) { 896 logger.debug("Bean with name '" + beanName + "' has been autodetected for JMX exposure"); 897 } 898 } 899 else { 900 if (logger.isTraceEnabled()) { 901 logger.trace("Bean with name '" + beanName + "' is already registered for JMX exposure"); 902 } 903 } 904 } 905 } 906 catch (CannotLoadBeanClassException ex) { 907 if (this.allowEagerInit) { 908 throw ex; 909 } 910 // otherwise ignore beans where the class is not resolvable 911 } 912 } 913 } 914 } 915 916 /** 917 * Indicates whether or not a particular bean name is present in the excluded beans list. 918 */ 919 private boolean isExcluded(String beanName) { 920 return (this.excludedBeans.contains(beanName) || 921 (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX) && 922 this.excludedBeans.contains(beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length())))); 923 } 924 925 /** 926 * Return whether the specified bean definition should be considered as abstract. 927 */ 928 private boolean isBeanDefinitionAbstract(ListableBeanFactory beanFactory, String beanName) { 929 return (beanFactory instanceof ConfigurableListableBeanFactory && beanFactory.containsBeanDefinition(beanName) && 930 ((ConfigurableListableBeanFactory) beanFactory).getBeanDefinition(beanName).isAbstract()); 931 } 932 933 934 //--------------------------------------------------------------------- 935 // Notification and listener management 936 //--------------------------------------------------------------------- 937 938 /** 939 * If the supplied managed resource implements the {@link NotificationPublisherAware} an instance of 940 * {@link org.springframework.jmx.export.notification.NotificationPublisher} is injected. 941 */ 942 private void injectNotificationPublisherIfNecessary( 943 Object managedResource, @Nullable ModelMBean modelMBean, @Nullable ObjectName objectName) { 944 945 if (managedResource instanceof NotificationPublisherAware && modelMBean != null && objectName != null) { 946 ((NotificationPublisherAware) managedResource).setNotificationPublisher( 947 new ModelMBeanNotificationPublisher(modelMBean, objectName, managedResource)); 948 } 949 } 950 951 /** 952 * Register the configured {@link NotificationListener NotificationListeners} 953 * with the {@link MBeanServer}. 954 */ 955 private void registerNotificationListeners() throws MBeanExportException { 956 if (this.notificationListeners != null) { 957 Assert.state(this.server != null, "No MBeanServer available"); 958 for (NotificationListenerBean bean : this.notificationListeners) { 959 try { 960 ObjectName[] mappedObjectNames = bean.getResolvedObjectNames(); 961 if (mappedObjectNames == null) { 962 // Mapped to all MBeans registered by the MBeanExporter. 963 mappedObjectNames = getRegisteredObjectNames(); 964 } 965 if (this.registeredNotificationListeners.put(bean, mappedObjectNames) == null) { 966 for (ObjectName mappedObjectName : mappedObjectNames) { 967 this.server.addNotificationListener(mappedObjectName, bean.getNotificationListener(), 968 bean.getNotificationFilter(), bean.getHandback()); 969 } 970 } 971 } 972 catch (Throwable ex) { 973 throw new MBeanExportException("Unable to register NotificationListener", ex); 974 } 975 } 976 } 977 } 978 979 /** 980 * Unregister the configured {@link NotificationListener NotificationListeners} 981 * from the {@link MBeanServer}. 982 */ 983 private void unregisterNotificationListeners() { 984 if (this.server != null) { 985 this.registeredNotificationListeners.forEach((bean, mappedObjectNames) -> { 986 for (ObjectName mappedObjectName : mappedObjectNames) { 987 try { 988 this.server.removeNotificationListener(mappedObjectName, bean.getNotificationListener(), 989 bean.getNotificationFilter(), bean.getHandback()); 990 } 991 catch (Throwable ex) { 992 if (logger.isDebugEnabled()) { 993 logger.debug("Unable to unregister NotificationListener", ex); 994 } 995 } 996 } 997 }); 998 } 999 this.registeredNotificationListeners.clear(); 1000 } 1001 1002 /** 1003 * Called when an MBean is registered. Notifies all registered 1004 * {@link MBeanExporterListener MBeanExporterListeners} of the registration event. 1005 * <p>Please note that if an {@link MBeanExporterListener} throws a (runtime) 1006 * exception when notified, this will essentially interrupt the notification process 1007 * and any remaining listeners that have yet to be notified will not (obviously) 1008 * receive the {@link MBeanExporterListener#mbeanRegistered(javax.management.ObjectName)} 1009 * callback. 1010 * @param objectName the {@code ObjectName} of the registered MBean 1011 */ 1012 @Override 1013 protected void onRegister(ObjectName objectName) { 1014 notifyListenersOfRegistration(objectName); 1015 } 1016 1017 /** 1018 * Called when an MBean is unregistered. Notifies all registered 1019 * {@link MBeanExporterListener MBeanExporterListeners} of the unregistration event. 1020 * <p>Please note that if an {@link MBeanExporterListener} throws a (runtime) 1021 * exception when notified, this will essentially interrupt the notification process 1022 * and any remaining listeners that have yet to be notified will not (obviously) 1023 * receive the {@link MBeanExporterListener#mbeanUnregistered(javax.management.ObjectName)} 1024 * callback. 1025 * @param objectName the {@code ObjectName} of the unregistered MBean 1026 */ 1027 @Override 1028 protected void onUnregister(ObjectName objectName) { 1029 notifyListenersOfUnregistration(objectName); 1030 } 1031 1032 1033 /** 1034 * Notifies all registered {@link MBeanExporterListener MBeanExporterListeners} of the 1035 * registration of the MBean identified by the supplied {@link ObjectName}. 1036 */ 1037 private void notifyListenersOfRegistration(ObjectName objectName) { 1038 if (this.listeners != null) { 1039 for (MBeanExporterListener listener : this.listeners) { 1040 listener.mbeanRegistered(objectName); 1041 } 1042 } 1043 } 1044 1045 /** 1046 * Notifies all registered {@link MBeanExporterListener MBeanExporterListeners} of the 1047 * unregistration of the MBean identified by the supplied {@link ObjectName}. 1048 */ 1049 private void notifyListenersOfUnregistration(ObjectName objectName) { 1050 if (this.listeners != null) { 1051 for (MBeanExporterListener listener : this.listeners) { 1052 listener.mbeanUnregistered(objectName); 1053 } 1054 } 1055 } 1056 1057 1058 //--------------------------------------------------------------------- 1059 // Inner classes for internal use 1060 //--------------------------------------------------------------------- 1061 1062 /** 1063 * Internal callback interface for the autodetection process. 1064 */ 1065 @FunctionalInterface 1066 private interface AutodetectCallback { 1067 1068 /** 1069 * Called during the autodetection process to decide whether 1070 * or not a bean should be included. 1071 * @param beanClass the class of the bean 1072 * @param beanName the name of the bean 1073 */ 1074 boolean include(Class<?> beanClass, String beanName); 1075 } 1076 1077 1078 /** 1079 * Extension of {@link LazyInitTargetSource} that will inject a 1080 * {@link org.springframework.jmx.export.notification.NotificationPublisher} 1081 * into the lazy resource as it is created if required. 1082 */ 1083 @SuppressWarnings("serial") 1084 private class NotificationPublisherAwareLazyTargetSource extends LazyInitTargetSource { 1085 1086 @Nullable 1087 private ModelMBean modelMBean; 1088 1089 @Nullable 1090 private ObjectName objectName; 1091 1092 public void setModelMBean(ModelMBean modelMBean) { 1093 this.modelMBean = modelMBean; 1094 } 1095 1096 public void setObjectName(ObjectName objectName) { 1097 this.objectName = objectName; 1098 } 1099 1100 @Override 1101 @Nullable 1102 public Object getTarget() { 1103 try { 1104 return super.getTarget(); 1105 } 1106 catch (RuntimeException ex) { 1107 if (logger.isInfoEnabled()) { 1108 logger.info("Failed to retrieve target for JMX-exposed bean [" + this.objectName + "]: " + ex); 1109 } 1110 throw ex; 1111 } 1112 } 1113 1114 @Override 1115 protected void postProcessTargetObject(Object targetObject) { 1116 injectNotificationPublisherIfNecessary(targetObject, this.modelMBean, this.objectName); 1117 } 1118 } 1119 1120}