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}