001/*
002 * Copyright 2002-2016 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.context.support;
018
019import java.lang.management.ManagementFactory;
020import java.util.Collections;
021import java.util.Iterator;
022import java.util.LinkedHashSet;
023import java.util.Set;
024import javax.management.MBeanServer;
025import javax.management.ObjectName;
026
027import org.springframework.beans.factory.config.BeanDefinition;
028import org.springframework.beans.factory.config.ConfigurableBeanFactory;
029import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
030import org.springframework.context.ApplicationContext;
031import org.springframework.context.ApplicationContextAware;
032import org.springframework.context.ApplicationContextException;
033import org.springframework.context.ConfigurableApplicationContext;
034import org.springframework.util.Assert;
035import org.springframework.util.StringUtils;
036
037/**
038 * Adapter for live beans view exposure, building a snapshot of current beans
039 * and their dependencies from either a local {@code ApplicationContext} (with a
040 * local {@code LiveBeansView} bean definition) or all registered ApplicationContexts
041 * (driven by the {@value #MBEAN_DOMAIN_PROPERTY_NAME} environment property).
042 *
043 * <p>Note: This feature is still in beta and primarily designed for use with
044 * Spring Tool Suite 3.1 and higher.
045 *
046 * @author Juergen Hoeller
047 * @author Stephane Nicoll
048 * @since 3.2
049 * @see #getSnapshotAsJson()
050 * @see org.springframework.web.context.support.LiveBeansViewServlet
051 */
052public class LiveBeansView implements LiveBeansViewMBean, ApplicationContextAware {
053
054        public static final String MBEAN_DOMAIN_PROPERTY_NAME = "spring.liveBeansView.mbeanDomain";
055
056        public static final String MBEAN_APPLICATION_KEY = "application";
057
058        private static final Set<ConfigurableApplicationContext> applicationContexts =
059                        new LinkedHashSet<ConfigurableApplicationContext>();
060
061        private static String applicationName;
062
063
064        static void registerApplicationContext(ConfigurableApplicationContext applicationContext) {
065                String mbeanDomain = applicationContext.getEnvironment().getProperty(MBEAN_DOMAIN_PROPERTY_NAME);
066                if (mbeanDomain != null) {
067                        synchronized (applicationContexts) {
068                                if (applicationContexts.isEmpty()) {
069                                        try {
070                                                MBeanServer server = ManagementFactory.getPlatformMBeanServer();
071                                                applicationName = applicationContext.getApplicationName();
072                                                server.registerMBean(new LiveBeansView(),
073                                                                new ObjectName(mbeanDomain, MBEAN_APPLICATION_KEY, applicationName));
074                                        }
075                                        catch (Throwable ex) {
076                                                throw new ApplicationContextException("Failed to register LiveBeansView MBean", ex);
077                                        }
078                                }
079                                applicationContexts.add(applicationContext);
080                        }
081                }
082        }
083
084        static void unregisterApplicationContext(ConfigurableApplicationContext applicationContext) {
085                synchronized (applicationContexts) {
086                        if (applicationContexts.remove(applicationContext) && applicationContexts.isEmpty()) {
087                                try {
088                                        MBeanServer server = ManagementFactory.getPlatformMBeanServer();
089                                        String mbeanDomain = applicationContext.getEnvironment().getProperty(MBEAN_DOMAIN_PROPERTY_NAME);
090                                        server.unregisterMBean(new ObjectName(mbeanDomain, MBEAN_APPLICATION_KEY, applicationName));
091                                }
092                                catch (Throwable ex) {
093                                        throw new ApplicationContextException("Failed to unregister LiveBeansView MBean", ex);
094                                }
095                                finally {
096                                        applicationName = null;
097                                }
098                        }
099                }
100        }
101
102
103        private ConfigurableApplicationContext applicationContext;
104
105
106        @Override
107        public void setApplicationContext(ApplicationContext applicationContext) {
108                Assert.isTrue(applicationContext instanceof ConfigurableApplicationContext,
109                                "ApplicationContext does not implement ConfigurableApplicationContext");
110                this.applicationContext = (ConfigurableApplicationContext) applicationContext;
111        }
112
113
114        /**
115         * Generate a JSON snapshot of current beans and their dependencies,
116         * finding all active ApplicationContexts through {@link #findApplicationContexts()},
117         * then delegating to {@link #generateJson(java.util.Set)}.
118         */
119        @Override
120        public String getSnapshotAsJson() {
121                Set<ConfigurableApplicationContext> contexts;
122                if (this.applicationContext != null) {
123                        contexts = Collections.singleton(this.applicationContext);
124                }
125                else {
126                        contexts = findApplicationContexts();
127                }
128                return generateJson(contexts);
129        }
130
131        /**
132         * Find all applicable ApplicationContexts for the current application.
133         * <p>Called if no specific ApplicationContext has been set for this LiveBeansView.
134         * @return the set of ApplicationContexts
135         */
136        protected Set<ConfigurableApplicationContext> findApplicationContexts() {
137                synchronized (applicationContexts) {
138                        return new LinkedHashSet<ConfigurableApplicationContext>(applicationContexts);
139                }
140        }
141
142        /**
143         * Actually generate a JSON snapshot of the beans in the given ApplicationContexts.
144         * <p>This implementation doesn't use any JSON parsing libraries in order to avoid
145         * third-party library dependencies. It produces an array of context description
146         * objects, each containing a context and parent attribute as well as a beans
147         * attribute with nested bean description objects. Each bean object contains a
148         * bean, scope, type and resource attribute, as well as a dependencies attribute
149         * with a nested array of bean names that the present bean depends on.
150         * @param contexts the set of ApplicationContexts
151         * @return the JSON document
152         */
153        protected String generateJson(Set<ConfigurableApplicationContext> contexts) {
154                StringBuilder result = new StringBuilder("[\n");
155                for (Iterator<ConfigurableApplicationContext> it = contexts.iterator(); it.hasNext();) {
156                        ConfigurableApplicationContext context = it.next();
157                        result.append("{\n\"context\": \"").append(context.getId()).append("\",\n");
158                        if (context.getParent() != null) {
159                                result.append("\"parent\": \"").append(context.getParent().getId()).append("\",\n");
160                        }
161                        else {
162                                result.append("\"parent\": null,\n");
163                        }
164                        result.append("\"beans\": [\n");
165                        ConfigurableListableBeanFactory bf = context.getBeanFactory();
166                        String[] beanNames = bf.getBeanDefinitionNames();
167                        boolean elementAppended = false;
168                        for (String beanName : beanNames) {
169                                BeanDefinition bd = bf.getBeanDefinition(beanName);
170                                if (isBeanEligible(beanName, bd, bf)) {
171                                        if (elementAppended) {
172                                                result.append(",\n");
173                                        }
174                                        result.append("{\n\"bean\": \"").append(beanName).append("\",\n");
175                                        result.append("\"aliases\": ");
176                                        appendArray(result, bf.getAliases(beanName));
177                                        result.append(",\n");
178                                        String scope = bd.getScope();
179                                        if (!StringUtils.hasText(scope)) {
180                                                scope = BeanDefinition.SCOPE_SINGLETON;
181                                        }
182                                        result.append("\"scope\": \"").append(scope).append("\",\n");
183                                        Class<?> beanType = bf.getType(beanName);
184                                        if (beanType != null) {
185                                                result.append("\"type\": \"").append(beanType.getName()).append("\",\n");
186                                        }
187                                        else {
188                                                result.append("\"type\": null,\n");
189                                        }
190                                        result.append("\"resource\": \"").append(getEscapedResourceDescription(bd)).append("\",\n");
191                                        result.append("\"dependencies\": ");
192                                        appendArray(result, bf.getDependenciesForBean(beanName));
193                                        result.append("\n}");
194                                        elementAppended = true;
195                                }
196                        }
197                        result.append("]\n");
198                        result.append("}");
199                        if (it.hasNext()) {
200                                result.append(",\n");
201                        }
202                }
203                result.append("]");
204                return result.toString();
205        }
206
207        /**
208         * Determine whether the specified bean is eligible for inclusion in the
209         * LiveBeansView JSON snapshot.
210         * @param beanName the name of the bean
211         * @param bd the corresponding bean definition
212         * @param bf the containing bean factory
213         * @return {@code true} if the bean is to be included; {@code false} otherwise
214         */
215        protected boolean isBeanEligible(String beanName, BeanDefinition bd, ConfigurableBeanFactory bf) {
216                return (bd.getRole() != BeanDefinition.ROLE_INFRASTRUCTURE &&
217                                (!bd.isLazyInit() || bf.containsSingleton(beanName)));
218        }
219
220        /**
221         * Determine a resource description for the given bean definition and
222         * apply basic JSON escaping (backslashes, double quotes) to it.
223         * @param bd the bean definition to build the resource description for
224         * @return the JSON-escaped resource description
225         */
226        protected String getEscapedResourceDescription(BeanDefinition bd) {
227                String resourceDescription = bd.getResourceDescription();
228                if (resourceDescription == null) {
229                        return null;
230                }
231                StringBuilder result = new StringBuilder(resourceDescription.length() + 16);
232                for (int i = 0; i < resourceDescription.length(); i++) {
233                        char character = resourceDescription.charAt(i);
234                        if (character == '\\') {
235                                result.append('/');
236                        }
237                        else if (character == '"') {
238                                result.append("\\").append('"');
239                        }
240                        else {
241                                result.append(character);
242                        }
243                }
244                return result.toString();
245        }
246
247        private void appendArray(StringBuilder result, String[] arr) {
248                result.append('[');
249                if (arr.length > 0) {
250                        result.append('\"');
251                }
252                result.append(StringUtils.arrayToDelimitedString(arr, "\", \""));
253                if (arr.length > 0) {
254                        result.append('\"');
255                }
256                result.append(']');
257        }
258
259}