package com.sishuok.spring.dynamic;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.beans.factory.annotation.InjectionMetadata;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.scripting.groovy.GroovyScriptFactory;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.util.*;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.method.HandlerMethodSelector;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* <p>User: Zhang Kaitao
* <p>Date: 14-1-3
* <p>Version: 1.0
*/
public class DynamicDeployBeans {
protected static final Log logger = LogFactory.getLog(DynamicDeployBeans.class);
//RequestMappingHandlerMapping
private static Method detectHandlerMethodsMethod =
ReflectionUtils.findMethod(RequestMappingHandlerMapping.class, "detectHandlerMethods", Object.class);
private static Method getMappingForMethodMethod =
ReflectionUtils.findMethod(RequestMappingHandlerMapping.class, "getMappingForMethod", Method.class, Class.class);
private static Method getMappingPathPatternsMethod =
ReflectionUtils.findMethod(RequestMappingHandlerMapping.class, "getMappingPathPatterns", RequestMappingInfo.class);
private static Method getPathMatcherMethod =
ReflectionUtils.findMethod(RequestMappingHandlerMapping.class, "getPathMatcher");
private static Field handlerMethodsField =
ReflectionUtils.findField(RequestMappingHandlerMapping.class, "handlerMethods", Map.class);
private static Field urlMapField =
ReflectionUtils.findField(RequestMappingHandlerMapping.class, "urlMap", MultiValueMap.class);
private static Field injectionMetadataCacheField =
ReflectionUtils.findField(AutowiredAnnotationBeanPostProcessor.class, "injectionMetadataCache");
static {
detectHandlerMethodsMethod.setAccessible(true);
getMappingForMethodMethod.setAccessible(true);
getMappingPathPatternsMethod.setAccessible(true);
getPathMatcherMethod.setAccessible(true);
handlerMethodsField.setAccessible(true);
urlMapField.setAccessible(true);
injectionMetadataCacheField.setAccessible(true);
}
private ApplicationContext ctx;
private DefaultListableBeanFactory beanFactory;
private Map<String, Long> scriptLastModifiedMap = new ConcurrentHashMap<>();//in millis
public DynamicDeployBeans() {
this(-1L);
}
public DynamicDeployBeans(Long scriptCheckInterval) {
if (scriptCheckInterval > 0L) {
startScriptModifiedCheckThead(scriptCheckInterval);
}
}
@Autowired
public void setApplicationContext(ApplicationContext ctx) {
if (!DefaultListableBeanFactory.class.isAssignableFrom(ctx.getAutowireCapableBeanFactory().getClass())) {
throw new IllegalArgumentException("BeanFactory must be DefaultListableBeanFactory type");
}
this.ctx = ctx;
this.beanFactory = (DefaultListableBeanFactory) ctx.getAutowireCapableBeanFactory();
}
public void registerBean(Class<?> beanClass) {
registerBean(null, beanClass);
}
public void registerBean(String beanName, Class<?> beanClass) {
Assert.notNull(beanClass, "register bean class must not null");
GenericBeanDefinition bd = new GenericBeanDefinition();
bd.setBeanClass(beanClass);
if (StringUtils.hasText(beanName)) {
beanFactory.registerBeanDefinition(beanName, bd);
} else {
BeanDefinitionReaderUtils.registerWithGeneratedName(bd, beanFactory);
}
}
public void registerController(Class<?> controllerClass) {
Assert.notNull(controllerClass, "register controller bean class must not null");
if (!WebApplicationContext.class.isAssignableFrom(ctx.getClass())) {
throw new IllegalArgumentException("applicationContext must be WebApplicationContext type");
}
GenericBeanDefinition bd = new GenericBeanDefinition();
bd.setBeanClass(controllerClass);
String controllerBeanName = controllerClass.getName();
removeOldControllerMapping(controllerBeanName);
beanFactory.registerBeanDefinition(controllerBeanName, bd);
addControllerMapping(controllerBeanName);
}
public void registerGroovyController(String scriptLocation) throws IOException {
if (scriptNotExists(scriptLocation)) {
throw new IllegalArgumentException("script not exists : " + scriptLocation);
}
scriptLastModifiedMap.put(scriptLocation, scriptLastModified(scriptLocation));
// Create script factory bean definition.
GroovyScriptFactory groovyScriptFactory = new GroovyScriptFactory(scriptLocation);
groovyScriptFactory.setBeanFactory(beanFactory);
groovyScriptFactory.setBeanClassLoader(beanFactory.getBeanClassLoader());
Object controller =
groovyScriptFactory.getScriptedObject(new ResourceScriptSource(ctx.getResource(scriptLocation)));
String controllerBeanName = scriptLocation;
removeOldControllerMapping(controllerBeanName);
if (beanFactory.containsBean(controllerBeanName)) {
beanFactory.destroySingleton(controllerBeanName); //移除单例bean
removeInjectCache(controller); //移除注入缓存 否则Caused by: java.lang.IllegalArgumentException: object is not an instance of declaring class
}
beanFactory.registerSingleton(controllerBeanName, controller); //注册单例bean
beanFactory.autowireBean(controller); //自动注入
addControllerMapping(controllerBeanName);
}
private void removeOldControllerMapping(String controllerBeanName) {
if (!beanFactory.containsBean(controllerBeanName)) {
return;
}
RequestMappingHandlerMapping requestMappingHandlerMapping = requestMappingHandlerMapping();
//remove old
Class<?> handlerType = ctx.getType(controllerBeanName);
final Class<?> userType = ClassUtils.getUserClass(handlerType);
Map handlerMethods = (Map) ReflectionUtils.getField(handlerMethodsField, requestMappingHandlerMapping);
MultiValueMap urlMapping = (MultiValueMap) ReflectionUtils.getField(urlMapField, requestMappingHandlerMapping);
final RequestMappingHandlerMapping innerRequestMappingHandlerMapping = requestMappingHandlerMapping;
Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new ReflectionUtils.MethodFilter() {
@Override
public boolean matches(Method method) {
return ReflectionUtils.invokeMethod(
getMappingForMethodMethod,
innerRequestMappingHandlerMapping,
method, userType) != null;
}
});
for (Method method : methods) {
RequestMappingInfo mapping =
(RequestMappingInfo) ReflectionUtils.invokeMethod(getMappingForMethodMethod, requestMappingHandlerMapping, method, userType);
handlerMethods.remove(mapping);
Set<String> patterns =
(Set<String>) ReflectionUtils.invokeMethod(getMappingPathPatternsMethod, requestMappingHandlerMapping, mapping);
PathMatcher pathMatcher =
(PathMatcher) ReflectionUtils.invokeMethod(getPathMatcherMethod, requestMappingHandlerMapping);
for (String pattern : patterns) {
if (!pathMatcher.isPattern(pattern)) {
urlMapping.remove(pattern);
}
}
}
}
private void addControllerMapping(String controllerBeanName) {
removeOldControllerMapping(controllerBeanName);
RequestMappingHandlerMapping requestMappingHandlerMapping = requestMappingHandlerMapping();
//spring 3.1 开始
ReflectionUtils.invokeMethod(detectHandlerMethodsMethod, requestMappingHandlerMapping, controllerBeanName);
}
private RequestMappingHandlerMapping requestMappingHandlerMapping() {
try {
return ctx.getBean(RequestMappingHandlerMapping.class);
} catch (Exception e) {
throw new IllegalArgumentException("applicationContext must has RequestMappingHandlerMapping");
}
}
private void removeInjectCache(Object controller) {
AutowiredAnnotationBeanPostProcessor autowiredAnnotationBeanPostProcessor =
ctx.getBean(AutowiredAnnotationBeanPostProcessor.class);
Map<String, InjectionMetadata> injectionMetadataMap =
(Map<String, InjectionMetadata>) ReflectionUtils.getField(injectionMetadataCacheField, autowiredAnnotationBeanPostProcessor);
injectionMetadataMap.remove(controller.getClass().getName());
}
private void startScriptModifiedCheckThead(final Long scriptCheckInterval) {
new Thread() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(scriptCheckInterval);
Map<String, Long> copyMap = new HashMap<>(scriptLastModifiedMap);
for (String scriptLocation : copyMap.keySet()) {
if (scriptNotExists(scriptLocation)) {
scriptLastModifiedMap.remove(scriptLocation);
//TODO remove handler mapping ?
}
if (copyMap.get(scriptLocation) != scriptLastModified(scriptLocation)) {
registerGroovyController(scriptLocation);
}
}
} catch (Exception e) {
e.printStackTrace();
//ignore
}
}
}
}.start();
}
private long scriptLastModified(String scriptLocation) {
try {
return ctx.getResource(scriptLocation).getFile().lastModified();
} catch (Exception e) {
return -1;
}
}
private boolean scriptNotExists(String scriptLocation) {
return !ctx.getResource(scriptLocation).exists();
}
}