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