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