package com.exteso.lab.pf.config.reload.reloader;
import com.exteso.lab.pf.config.reload.listener.springreload.JHipsterHandlerMappingListener;
import com.exteso.lab.pf.config.reload.listener.springreload.SpringReloadListener;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.autoproxy.BeanFactoryAdvisorRetrievalHelper;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
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.data.jpa.repository.support.JpaRepositoryFactory;
import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor;
import org.springframework.data.repository.util.TxUtils;
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.web.bind.annotation.RestController;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.*;
/**
* Reloads Spring Beans.
*/
public class SpringReloader implements InitializingBean {
private final Logger log = LoggerFactory.getLogger(SpringReloader.class);
private final ConfigurableApplicationContext applicationContext;
private final BeanFactoryAdvisorRetrievalHelper beanFactoryAdvisorRetrievalHelper;
private JpaRepositoryFactory jpaRepositoryFactory;
private final List<SpringReloadListener> springReloadListeners = new ArrayList<>();
private Set<Class<?>> toReloadBeans = new LinkedHashSet<>();
private List<Class<?>> newToWaitFromBeans = new ArrayList<>();
private Map<String, Class<?>> existingToWaitFromBeans = new HashMap<>();
@PersistenceContext
private EntityManager entityManager;
public SpringReloader(ConfigurableApplicationContext applicationContext) {
log.debug("Hot reloading Spring Beans enabled");
this.applicationContext = applicationContext;
this.beanFactoryAdvisorRetrievalHelper = new BeanFactoryAdvisorRetrievalHelper(applicationContext.getBeanFactory());
// register listeners
registerListeners();
}
public void reloadEvent(Class<?> clazz) {
toReloadBeans.add(clazz);
}
/**
* Call when an entity bean is loaded
* Perhaps new or existing bean are waiting for the entity
*/
public void hasNewEntityBean() {
List<Class> newSpringBeans = new ArrayList<>();
List<Class> existingSpringBeans = new ArrayList<>();
newSpringBeans.addAll(newToWaitFromBeans);
existingSpringBeans.addAll(existingToWaitFromBeans.values());
start(newSpringBeans, existingSpringBeans);
}
public boolean hasBeansToReload() {
return toReloadBeans.size() > 0;
}
public void start() {
List<Class> newSpringBeans = new ArrayList<>();
List<Class> existingSpringBeans = new ArrayList<>();
start(newSpringBeans, existingSpringBeans);
}
private void start(List<Class> newSpringBeans, List<Class> existingSpringBeans) {
try {
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getBeanFactory();
//1) Split between new/existing beans
for (Class toReloadBean : toReloadBeans) {
log.trace("Hot reloading Spring bean: {}", toReloadBean.getName());
Annotation annotation = getSpringClassAnnotation(toReloadBean);
String beanName = constructBeanName(annotation, toReloadBean);
if (!beanFactory.containsBeanDefinition(beanName)) {
newSpringBeans.add(toReloadBean);
// Check if this new class is a dependent class.
// If so add this dependent class to the newSpringBeans list
if (newToWaitFromBeans.size() > 0) {
newSpringBeans.addAll(newToWaitFromBeans);
newToWaitFromBeans.clear();
}
} else {
existingSpringBeans.add(toReloadBean);
if (existingToWaitFromBeans.containsKey(toReloadBean.getName())) {
existingSpringBeans.add(existingToWaitFromBeans.get(toReloadBean.getName()));
existingToWaitFromBeans.remove(toReloadBean.getName());
}
}
}
//2) Declare new beans prior to instanciation for cross bean references
for (Class clazz : newSpringBeans) {
Annotation annotation = getSpringClassAnnotation(clazz);
String beanName = constructBeanName(annotation, clazz);
String scope = getScope(clazz);
RootBeanDefinition bd = new RootBeanDefinition(clazz, AbstractBeanDefinition.AUTOWIRE_BY_TYPE, true);
bd.setScope(scope);
beanFactory.registerBeanDefinition(beanName, bd);
}
//3) Instanciate new beans
for (Class clazz : newSpringBeans) {
Annotation annotation = getSpringClassAnnotation(clazz);
String beanName = constructBeanName(annotation, clazz);
try {
// This is a spring data interface
if (ClassUtils.isAssignable(clazz, org.springframework.data.repository.Repository.class)) {
final Object repository = jpaRepositoryFactory.getRepository(clazz);
beanFactory.registerSingleton(beanName, repository);
} else {
beanFactory.getBean(beanName);
}
processListener(clazz, true);
toReloadBeans.remove(clazz);
log.info("JHipster reload - New Spring bean '{}' has been reloaded.", clazz);
} catch (Exception e) {
log.trace("The Spring bean can't be loaded at this time. Keep it to reload it later", e);
// remove the registration bean to treat this class as new class
beanFactory.removeBeanDefinition(beanName);
newToWaitFromBeans.add(clazz);
toReloadBeans.remove(clazz);
}
}
//4) Resolve dependencies for existing beans
for (Class clazz : existingSpringBeans) {
Object beanInstance = applicationContext.getBean(clazz);
log.trace("Existing bean, autowiring fields");
if (AopUtils.isCglibProxy(beanInstance)) {
log.trace("This is a CGLIB proxy, getting the real object");
addAdvisorIfNeeded(clazz, beanInstance);
final Advised advised = (Advised) beanInstance;
beanInstance = advised.getTargetSource().getTarget();
} else if (AopUtils.isJdkDynamicProxy(beanInstance)) {
log.trace("This is a JDK proxy, getting the real object");
addAdvisorIfNeeded(clazz, beanInstance);
final Advised advised = (Advised) beanInstance;
beanInstance = advised.getTargetSource().getTarget();
} else {
log.trace("This is a normal Java object");
}
boolean failedToUpdate = false;
Field[] fields = beanInstance.getClass().getDeclaredFields();
for (Field field : fields) {
if (AnnotationUtils.getAnnotation(field, Inject.class) != null ||
AnnotationUtils.getAnnotation(field, Autowired.class) != null) {
log.trace("@Inject/@Autowired annotation found on field {}", field.getName());
ReflectionUtils.makeAccessible(field);
if (ReflectionUtils.getField(field, beanInstance) != null) {
log.trace("Field is already injected, not doing anything");
} else {
log.trace("Field is null, injecting a Spring bean");
try {
Object beanToInject = applicationContext.getBean(field.getType());
ReflectionUtils.setField(field, beanInstance, beanToInject);
} catch (NoSuchBeanDefinitionException bsbde) {
log.debug("JHipster reload - Spring bean '{}' does not exist, " +
"wait until this class will be available.", field.getType());
failedToUpdate = true;
existingToWaitFromBeans.put(field.getType().getName(), clazz);
}
}
}
}
toReloadBeans.remove(clazz);
if (!failedToUpdate) {
processListener(clazz, false);
}
log.info("JHipster reload - Existing Spring bean '{}' has been reloaded.", clazz);
}
for (SpringReloadListener springReloadListener : springReloadListeners) {
springReloadListener.execute();
}
} catch (Exception e) {
log.warn("Could not hot reload Spring bean!", e);
}
}
/**
* AOP uses advisor to intercept any annotations.
*/
private void addAdvisorIfNeeded(Class clazz, Object beanInstance) {
final Advised advised = (Advised) beanInstance;
final List<Advisor> candidateAdvisors = this.beanFactoryAdvisorRetrievalHelper.findAdvisorBeans();
final List<Advisor> advisorsThatCanApply = AopUtils.findAdvisorsThatCanApply(candidateAdvisors, clazz);
for (Advisor advisor : advisorsThatCanApply) {
// Add the advisor to the advised if it doesn't exist
if (advised.indexOf(advisor) == -1) {
advised.addAdvisor(advisor);
}
}
}
private void processListener(Class<?> clazz, boolean isNewClass) {
for (SpringReloadListener springReloadListener : springReloadListeners) {
if (springReloadListener.support(clazz)) {
springReloadListener.process(clazz, isNewClass);
}
}
}
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, RestController.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;
}
private void registerListeners() {
springReloadListeners.add(new JHipsterHandlerMappingListener());
for (SpringReloadListener springReloadListener : springReloadListeners) {
springReloadListener.register(applicationContext);
}
}
@Override
public void afterPropertiesSet() {
applicationContext.getAutowireCapableBeanFactory().autowireBean(this);
this.jpaRepositoryFactory = new JpaRepositoryFactory(entityManager);
try {
// Make sure calls to the repository instance are intercepted for annotated transactions
Class transactionalRepositoryProxyPostProcessor = Class.forName("org.springframework.data.repository.core.support.TransactionalRepositoryProxyPostProcessor");
final Constructor constructor = transactionalRepositoryProxyPostProcessor.getConstructor(ListableBeanFactory.class, String.class);
final RepositoryProxyPostProcessor repositoryProxyPostProcessor = (RepositoryProxyPostProcessor)
constructor.newInstance(applicationContext.getBeanFactory(), TxUtils.DEFAULT_TRANSACTION_MANAGER);
jpaRepositoryFactory.addRepositoryProxyPostProcessor(repositoryProxyPostProcessor);
} catch (Exception e) {
log.error("Failed to initialize the TransactionalRepositoryProxyPostProcessor class", e);
}
}
}