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}