package alien4cloud.plugin.aop; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.lang.reflect.UndeclaredThrowableException; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import javax.annotation.Resource; import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator; import org.springframework.aop.support.AopUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ApplicationContextEvent; import org.springframework.context.event.ContextStartedEvent; import org.springframework.context.event.ContextStoppedEvent; import org.springframework.context.event.GenericApplicationListenerAdapter; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Component; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.MethodCallback; import org.springframework.util.ReflectionUtils.MethodFilter; import com.google.common.collect.Maps; import alien4cloud.events.AlienEvent; import lombok.extern.slf4j.Slf4j; /** * Manage inter context proxies : thanks to it, we can define aspects upon main context beans in child contexts. Also broadcast {@link AlienEvent}s to child * contexts. * <p> * This {@link BeanPostProcessor} will search for {@link Overridable} annotation in any bean of the context (at type level, or method level). Each bean * containing this annotation is a candidate to be proxied and is in fact proxied by an internal {@link InvocationHandler}. * <p> * When a child context is started, it looks if some advice can apply to any of these candidates. In such case, the invocation handler will invoke methods on * proxy created using these advices. * <p> * Few notes: * <ul> * <li>candidate beans must have interface (we use JDK proxy feature). * <li>the annotation can be used on the methods, type, interface or interface method. * <li>you can use several advices for the same bean in child context. * <li>the bean can be already proxied in the main context: in this case, the annotation should be present at interface level. * <li>proxies are applied in the order child contexts are started. * </ul> */ @Component @Slf4j public class ChildContextAspectsManager implements ApplicationListener<ApplicationEvent>, BeanPostProcessor { /** All the candidates to be overriden by plugin child contexts. */ private Map<Object, ProxyRegistry> overridableCandidates = Maps.newHashMap(); /** All the referenced plugin child contexts. */ private Map<String, ApplicationContext> childContexts = Maps.newLinkedHashMap(); /** We store all the names of beans that implements {@link ApplicationListener} per child context. */ private Map<String, GenericApplicationListenerAdapter[]> childApplicationListeners = Maps.newHashMap(); private Lock lock = new ReentrantLock(); @Resource private ApplicationContext context; @Override public Object postProcessAfterInitialization(final Object bean, final String id) throws BeansException { if (log.isTraceEnabled()) { log.trace("post processing bean with id <{}> of type <{}>", id, bean.getClass().toString()); } if (AopUtils.isAopProxy(bean)) { log.debug("Spring is already managing proxy for bean of class {}.", bean.getClass().toString()); return bean; } if (AnnotationUtils.findAnnotation(bean.getClass(), Overridable.class) != null) { // the bean is annotated as Overridable candidate log.info("The bean with id <{}> of type <{}> is candidate to be overridden by plugin child contexts", id, bean.getClass().toString()); registerProxyCandidate(bean, id); } else { // let's look for annotation in methods ReflectionUtils.doWithMethods(bean.getClass(), new MethodCallback() { @Override public void doWith(Method m) throws IllegalArgumentException, IllegalAccessException { log.info("The method <{}> of bean <{}> is candidate to be overridden by plugin child contexts", m.toString(), id); registerProxyCandidate(bean, id); } }, new MethodFilter() { @Override public boolean matches(Method m) { // we search for public methods annotated as Overridable return (m.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC && AnnotationUtils.findAnnotation(m, Overridable.class) != null; } }); } // if the bean is a candidate, then return the proxy ProxyRegistry proxyRegistry = overridableCandidates.get(bean); if (proxyRegistry != null) { return proxyRegistry.proxy; } else { return bean; } } private void registerProxyCandidate(final Object bean, final String id) { ProxyRegistry proxyRegistry = overridableCandidates.get(bean); if (proxyRegistry == null) { proxyRegistry = new ProxyRegistry(); Object proxy = Proxy.newProxyInstance(bean.getClass().getClassLoader(), bean.getClass().getInterfaces(), new DynamicProxyInvocationHandler(bean)); proxyRegistry.proxy = proxy; proxyRegistry.target = bean; proxyRegistry.original = bean; proxyRegistry.beanName = id; overridableCandidates.put(bean, proxyRegistry); } } @Override public Object postProcessBeforeInitialization(Object bean, String id) throws BeansException { return bean; } private void onContextStarted(ApplicationContext ctx) { if (ctx == context) { return; } lock.lock(); try { childContexts.put(ctx.toString(), ctx); if (log.isDebugEnabled()) { log.debug("context started with id: {}", ctx.getId()); } decorateProxyCandidate(ctx); detectApplicationListeners(ctx); } finally { lock.unlock(); } } private void detectApplicationListeners(ApplicationContext ctx) { String[] applicationListenerBeanNames = ctx.getBeanNamesForType(ApplicationListener.class); if (applicationListenerBeanNames != null && applicationListenerBeanNames.length > 0) { if (log.isDebugEnabled()) { log.debug("The child context <{}> contains the following listeners: {}", ctx.getDisplayName(), applicationListenerBeanNames); } GenericApplicationListenerAdapter[] adapters = new GenericApplicationListenerAdapter[applicationListenerBeanNames.length]; int i = 0; for (String applicationListenerBeanName : applicationListenerBeanNames) { adapters[i++] = new GenericApplicationListenerAdapter((ApplicationListener<?>) ctx.getBean(applicationListenerBeanName)); } childApplicationListeners.put(ctx.toString(), adapters); } } private void onContextStopped(ApplicationContext ctx) { if (ctx.getId().endsWith(":leader")) { return; } lock.lock(); try { if (log.isDebugEnabled()) { log.debug("context stopped with id: {}", ctx.getId()); } ApplicationContext removed = childContexts.remove(ctx.toString()); childApplicationListeners.remove(ctx.toString()); if (removed == null) { log.warn("The stopped context {} can not be found in registered contexts", ctx); } else { // reset all proxy candidates (he target become the origin bean) for (ProxyRegistry candidateProxyRegistryEntry : overridableCandidates.values()) { candidateProxyRegistryEntry.reset(); } // rebuild proxies with the remaining child contexts for (ApplicationContext childContext : childContexts.values()) { decorateProxyCandidate(childContext); } } } finally { lock.unlock(); } } private void onApplicationContextEvent(ApplicationContextEvent e) { if (e instanceof ContextStartedEvent) { onContextStarted(e.getApplicationContext()); } else if (e instanceof ContextStoppedEvent) { onContextStopped(e.getApplicationContext()); } } @Override public void onApplicationEvent(ApplicationEvent e) { if (e instanceof ApplicationContextEvent) { onApplicationContextEvent((ApplicationContextEvent) e); } else if (e instanceof AlienEvent) { onAlienEvent((AlienEvent) e); } } /** * Broadcast {@link AlienEvent}s to child context {@link ApplicationListener} beans. */ private void onAlienEvent(AlienEvent e) { // Alien events are published to child contexts // we can't publish directly into child context because it will re-publish to it's parent causing a stack overflow ! // TODO In fact we should do it, so that child contexts can use @EventListener annotation. Use a boolean in AlienEvent.forwarded to check wether the // event has already been forwarded to child contexts for (Entry<String, GenericApplicationListenerAdapter[]> childListenersEntry : childApplicationListeners.entrySet()) { ApplicationContext ctx = childContexts.get(childListenersEntry.getKey()); if (ctx != null) { for (GenericApplicationListenerAdapter childListener : childListenersEntry.getValue()) { if (childListener.supportsEventType(e.getClass())) { childListener.onApplicationEvent(e); } } } } } private void decorateProxyCandidate(ApplicationContext ctx) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); // we need to use the child context classLoader Thread.currentThread().setContextClassLoader(ctx.getClassLoader()); try { AnnotationAwareAspectJAutoProxyCreator annotationAwareAspectJAutoProxyCreator = new AnnotationAwareAspectJAutoProxyCreator(); DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(ctx); annotationAwareAspectJAutoProxyCreator.setBeanFactory(lbf); for (ProxyRegistry candidateProxyRegistry : overridableCandidates.values()) { Object bean = candidateProxyRegistry.target; Object advicedBean = annotationAwareAspectJAutoProxyCreator.postProcessAfterInitialization(bean, candidateProxyRegistry.beanName); if (bean != advicedBean) { log.info("The bean with name {} is now proxied by {}", candidateProxyRegistry.beanName, advicedBean); candidateProxyRegistry.target = advicedBean; } } } finally { Thread.currentThread().setContextClassLoader(cl); } } /** * This {@link InvocationHandler} will invoke methods: * <ul> * <li>on a proxy if aspects have been defined for this bean in plugin child context. * <li>on the original bean if no plugin child context have defined any aspect for it. * </ul> */ private class DynamicProxyInvocationHandler implements InvocationHandler { /** * The original bean that is eventually overridden. */ private Object obj; public DynamicProxyInvocationHandler(Object obj) { super(); this.obj = obj; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { lock.lock(); Object target = obj; try { ProxyRegistry proxyRegistry = overridableCandidates.get(obj); if (proxyRegistry != null) { if (log.isDebugEnabled()) { if (proxyRegistry.target != proxyRegistry.original) { log.debug("Invoking method <{}> on proxy", method); } else { log.debug("Invoking method <{}> on native bean (no proxy found)", method); } } target = proxyRegistry.target; } else { if (log.isDebugEnabled()) { log.debug("Invoking method <{}> on native bean (no proxy registry found)", method); } } } finally { lock.unlock(); } Object result = null; try { result = ReflectionUtils.invokeMethod(method, target, args); } catch (Exception e) { try { ReflectionUtils.handleReflectionException(e); } catch (UndeclaredThrowableException ute) { throw ute.getUndeclaredThrowable(); } } return result; } } private static class ProxyRegistry { /** The bean name in the main application context. */ private String beanName; /** The dynamic proxy for the bean. */ private Object proxy; /** The target : the original bean eventually proxied by child context aspects. */ private Object target; /** The original bean that is candidate for being proxied by child context aspects. */ private Object original; /** The target become the origin, like just after main context startup. */ public void reset() { this.target = this.original; } } }