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}