package org.nigajuan.springloaded;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Scope;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springsource.loaded.Plugins;
import org.springsource.loaded.ReloadEventProcessorPlugin;
/**
* Automatically reloads Spring Beans when Spring Loaded triggers a hot reload event.
*
* <p>
* To have Spring Loaded working, run your Application class with these VM options:
* "-javaagent:spring_loaded/springloaded-1.1.5-dev.jar -noverify"
* </p>
*/
public class ReloadPlugin implements ReloadEventProcessorPlugin, Callable<Object> {
private static final Logger log = LoggerFactory.getLogger(ReloadPlugin.class);
private static ConfigurableApplicationContext applicationContext;
private Debouncer debouncer = new Debouncer(this, 2000);
private Object lock = new Object();
private Set<ToReloadBean> toReloadBeans = new HashSet<>();
@Override
public boolean shouldRerunStaticInitializer(String typename, Class<?> aClass, String encodedTimestamp) {
return false;
}
public void reloadEvent(String typename, Class<?> clazz, String encodedTimestamp) {
toReloadBeans.add(new ToReloadBean(typename , clazz , encodedTimestamp));
debouncer.call("debounce");
}
@Override
public synchronized Object call() throws Exception {
Set<ToReloadBean> toReloadBeansCopy = new HashSet<>(toReloadBeans);
toReloadBeans = new HashSet<>();
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getBeanFactory();
List<ToReloadBean> newSpringBeans = new ArrayList<>();
List<ToReloadBean> existingSpringBeans = new ArrayList<>();
//1) Split between new/existing beans
for (ToReloadBean toReloadBean : toReloadBeansCopy) {
Annotation annotation = getSpringClassAnnotation(toReloadBean.getClazz());
if (annotation != null){
String beanName = constructBeanName(annotation , toReloadBean.getClazz());
RootBeanDefinition beanDefinition = null;
try {
beanFactory.getBeanDefinition(beanName);
} catch (NoSuchBeanDefinitionException e) {
//not registered
log.error(e.getMessage() , e);
}
if (beanDefinition == null) {
newSpringBeans.add(toReloadBean);
}else{
existingSpringBeans.add(toReloadBean);
}
}
}
//2) Declare new beans prior to instanciation for cross bean references
for (ToReloadBean toReloadBean : newSpringBeans) {
Annotation annotation = getSpringClassAnnotation(toReloadBean.getClazz());
String beanName = constructBeanName(annotation , toReloadBean.getClazz());
String scope = getScope(toReloadBean.getClazz());
RootBeanDefinition bd = new RootBeanDefinition(toReloadBean.getClazz(), AbstractBeanDefinition.AUTOWIRE_BY_TYPE, true);
bd.setScope(scope);
beanFactory.registerBeanDefinition(beanName, bd);
}
//3) Instanciate new beans
for (ToReloadBean toReloadBean : newSpringBeans) {
Annotation annotation = getSpringClassAnnotation(toReloadBean.getClazz());
String beanName = constructBeanName(annotation , toReloadBean.getClazz());
try {
beanFactory.getBean(beanName);
} catch (BeansException e) {
//buggy bean, try later
log.error(e.getMessage(), e);
toReloadBeans.add(toReloadBean);
}
}
//4) Resolve deps for existing beans
for (ToReloadBean toReloadBean : existingSpringBeans) {
Object beanInstance = applicationContext.getBean(toReloadBean.getClazz());
log.debug("Existing bean, autowiring fields"); // We only support autowiring on fields
if (AopUtils.isCglibProxy(beanInstance)) {
log.trace("This is a CGLIB proxy, getting the real object");
beanInstance = ((Advised) beanInstance).getTargetSource().getTarget();
} else if (AopUtils.isJdkDynamicProxy(beanInstance)) {
log.trace("This is a JDK proxy, getting the real object");
beanInstance = ((Advised) beanInstance).getTargetSource().getTarget();
}
Field[] fields = beanInstance.getClass().getDeclaredFields();
for (Field field : fields) {
if (AnnotationUtils.getAnnotation(field, Autowired.class) != null) {
log.debug("@Inject annotation found on field {}", field.getName());
Object beanToInject = applicationContext.getBean(field.getType());
ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field, beanInstance, beanToInject);
}
}
}
return null;
}
private Annotation getSpringClassAnnotation(Class clazz){
Annotation classAnnotation = AnnotationUtils.findAnnotation(clazz, Component.class);
if (classAnnotation == null) {
classAnnotation = AnnotationUtils.findAnnotation(clazz, Controller.class);
}
if (classAnnotation == null) {
classAnnotation = AnnotationUtils.findAnnotation(clazz, Service.class);
}
if (classAnnotation == null) {
classAnnotation = AnnotationUtils.findAnnotation(clazz, Repository.class);
}
return classAnnotation;
}
private String getScope(Class clazz){
String scope = ConfigurableBeanFactory.SCOPE_SINGLETON;
Annotation scopeAnnotation = AnnotationUtils.findAnnotation(clazz, Scope.class);
if (scopeAnnotation != null){
scope = (String) AnnotationUtils.getValue(scopeAnnotation);
}
return scope;
}
private String constructBeanName(Annotation annotation , Class clazz){
String beanName = (String)AnnotationUtils.getValue(annotation);
if (beanName == null || beanName.isEmpty()){
beanName = StringUtils.uncapitalize(clazz.getSimpleName());
}
return beanName;
}
public static void register(ConfigurableApplicationContext ctx) {
log.trace("Registering JHipster hot reloading plugin - your Spring Beans should be automatically reloaded!");
ReloadPlugin.applicationContext = ctx;
Plugins.registerGlobalPlugin(new ReloadPlugin());
}
}