Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.ff4j.aop.FeatureAdvisor Maven / Gradle / Ivy
package org.ff4j.aop;
/*
* #%L ff4j-aop %% Copyright (C) 2013 Ff4J %% Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License. #L%
*/
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.lang.model.type.NullType;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.ff4j.FF4j;
import org.ff4j.core.FlippingExecutionContext;
import org.ff4j.core.FlippingStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
/**
* When Proxified, analyze bean to eventually invoke ANOTHER implementation (flip up).
*
* @author Cedrick LUNVEN
*/
@Component("ff.advisor")
public class FeatureAdvisor implements MethodInterceptor, BeanPostProcessor, ApplicationContextAware {
/** Log with target className. */
private final Map targetLoggers = new HashMap();
/** Processed Interfaces. */
private final Set targetInterfacesNames = new HashSet();
/** Strategies should be instantiate only once, keep references */
private final Map strategySingletons = new HashMap();
/** Spring Application Context. */
private ApplicationContext appCtx;
/** Injection of current FF4J bean. */
@Autowired
private FF4j ff4j;
/** {@inheritDoc} */
@Override
public Object invoke(final MethodInvocation pMInvoc) throws Throwable {
Method method = pMInvoc.getMethod();
Logger targetLogger = getLogger(method);
Flip ff = null;
if (method.isAnnotationPresent(Flip.class)) {
ff = method.getAnnotation(Flip.class);
} else if (method.getDeclaringClass().isAnnotationPresent(Flip.class)) {
ff = method.getDeclaringClass().getAnnotation(Flip.class);
}
if (ff != null) {
FlippingExecutionContext context = retrieveContext(ff, pMInvoc, targetLogger);
if (shouldFlip(ff, context)) {
// Required parameters
if (!assertRequiredParams(ff)) {
String msg = String.format("alterBeanName or alterClazz is required for {%s}", method.getDeclaringClass());
throw new IllegalArgumentException(msg);
}
if (shouldCallAlterBeanMethod(pMInvoc, ff.alterBean(), targetLogger)) {
return callAlterBeanMethod(pMInvoc, ff.alterBean(), targetLogger);
}
// Test alterClazz Property of annotation
if (shouldCallAlterClazzMethod(pMInvoc, ff.alterClazz(), targetLogger)) {
return callAlterClazzMethodOnFirst(pMInvoc, ff, targetLogger);
}
}
}
// do not catch throwable
return pMInvoc.proceed();
}
private FlippingExecutionContext retrieveContext(Flip ff, MethodInvocation methodInvocation, Logger logger) {
FlippingExecutionContext context = null;
if (ff.contextLocation() == ContextLocation.FF4J) {
context = getFf4j().getCurrentContext();
} else if (ff.contextLocation() == ContextLocation.PARAMETER) {
// We are looking for the first parameter (not argument!) that is an instance of FlippingExecutionContext
int p = 0;
for (Class> cls : methodInvocation.getMethod().getParameterTypes()) {
if (FlippingExecutionContext.class.isAssignableFrom(cls)) {
context = FlippingExecutionContext.class.cast(methodInvocation.getArguments()[p]);
break;
}
p++;
}
if (context == null) {
logger.warn("ff4j-aop: Context location is 'PARAMETER' and no context was found as parameter" +
" (maybe the argument was null)");
}
}
return context;
}
/**
* Check requirements for annoting methods.
*
* @param ff
* target annotation {@link Flip}
* @return is param are correct
*/
private boolean assertRequiredParams(Flip ff) {
boolean alterBeanPresent = ff.alterBean() != null && !ff.alterBean().isEmpty();
boolean alterClazPresent = (ff.alterClazz() != null) && (ff.alterClazz() != NullType.class);
return alterBeanPresent || alterClazPresent;
}
/** {@inheritDoc} */
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
// Before Initializing allow to check Annotations
Class> target = bean.getClass();
// Scan interface only once.
if (!target.isInterface() && target.getInterfaces() != null) {
// Get Interface
for (Class> currentInterface : target.getInterfaces()) {
String currentInterfaceName = currentInterface.getCanonicalName();
if (!currentInterfaceName.startsWith("java.") && !targetInterfacesNames.contains(currentInterfaceName)) {
targetInterfacesNames.add(currentInterfaceName);
}
}
}
return bean;
}
/** {@inheritDoc} */
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
return bean;
}
/**
* Store single instance of loggers but init if does not exist
*
* @param targetMethod
* current processed method
* @return singleton for class related to this execution
*/
private Logger getLogger(Method targetMethod) {
String methodName = targetMethod.getDeclaringClass().getCanonicalName();
// Register logger if require
if (!targetLoggers.containsKey(methodName)) {
targetLoggers.put(methodName, LoggerFactory.getLogger(targetMethod.getDeclaringClass()));
}
return targetLoggers.get(methodName);
}
/**
* Call if Flipped based on different parameters of the annotation
*
* @param ff
* annotation over current method
* @param context
* @return if flippinf should be considere
*/
private boolean shouldFlip(Flip ff, FlippingExecutionContext context) {
boolean shouldFlip;
if (ff.strategy() != NullType.class) {
// Does this strategy has already be invoked ?
String strategyClassName = ff.strategy().getCanonicalName();
if (!strategySingletons.containsKey(strategyClassName)) {
try {
strategySingletons.put(strategyClassName, (FlippingStrategy) ff.strategy().newInstance());
} catch (InstantiationException e) {
throw new IllegalArgumentException("ff4j-aop: Cannot instantiate alterbean " + strategyClassName
+ " please check default constructor existence & visibility", e);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("ff4j-aop: Cannot instantiate alterbean " + strategyClassName
+ " please check constructor visibility", e);
}
}
FlippingStrategy targetStrategy = strategySingletons.get(strategyClassName);
shouldFlip = getFf4j().checkOveridingStrategy(ff.name(), targetStrategy, context);
} else {
// no strategy, simple flip
shouldFlip = getFf4j().check(ff.name(), context);
}
return shouldFlip;
}
/**
* Flip with alterBean is realized only if 'alterBean' property is filled and valid.
*
* @param pMInvoc
* current method invocation
* @param alterBean
* target bean to call
* @param logger
* current logger for the class
* @return flag if alterBean should be invoked
*/
private boolean shouldCallAlterBeanMethod(final MethodInvocation pMInvoc, String alterBean, Logger logger) {
boolean callAlterBeanMethod = false;
Method method = pMInvoc.getMethod();
String currentBeanName = currentBeanName(pMInvoc);
if (alterBean != null && !alterBean.isEmpty()) {
if (alterBean.equals(currentBeanName)) {
logger.debug("FeatureFlipping on method:{} class:{} already on the alterBean {}", method.getName(), method
.getDeclaringClass().getName(), alterBean);
} else {
if (!appCtx.containsBean(alterBean)) {
throw new BeanCreationException("ff4j-aop : bean name '" + alterBean
+ "' has not been found in applicationContext still declared in 'alterBean' property of bean "
+ method.getDeclaringClass());
}
callAlterBeanMethod = true;
}
}
return callAlterBeanMethod;
}
private String currentBeanName(MethodInvocation pMInvoc) {
Class> targetClass = pMInvoc.getThis() != null ? AopUtils.getTargetClass(pMInvoc.getThis()) : null;
if (targetClass == null) {
throw new IllegalArgumentException("ff4j-aop: Static methods cannot be feature flipped");
}
Component component = targetClass.getAnnotation(Component.class);
if (component != null) {
return component.value();
}
Service service = targetClass.getAnnotation(Service.class);
if (service != null) {
return service.value();
}
Repository repo = targetClass.getAnnotation(Repository.class);
if (repo != null) {
return repo.value();
}
// There is no annotation on the bean, still be declared in applicationContext.xml
try {
// Use BeanDefinition names to loop on each bean and fetch target if proxified
for(String beanName : appCtx.getBeanDefinitionNames()) {
Object bean = appCtx.getBean(beanName);
if (AopUtils.isJdkDynamicProxy(bean)) {
bean = ((Advised) bean).getTargetSource().getTarget();
}
if (bean != null && bean.getClass().isAssignableFrom(targetClass)) {
return beanName;
}
}
} catch (Exception e) {
throw new RuntimeException("ff4j-aop: Cannot read bheind proxy target", e);
}
throw new IllegalArgumentException("ff4j-aop: Feature bean must be annotated as a Service or a Component");
}
private Object callAlterBeanMethod(final MethodInvocation pMInvoc, String alterBean, Logger targetLogger) throws Throwable {
Method method = pMInvoc.getMethod();
targetLogger.debug("FeatureFlipping on method:{} class:{}", method.getName(), method.getDeclaringClass().getName());
// invoke same method (interface) with another spring bean (ff.alterBean())
try {
return method.invoke(appCtx.getBean(alterBean, method.getDeclaringClass()), pMInvoc.getArguments());
} catch (InvocationTargetException invocationTargetException) {
if(!ff4j.isAlterBeanThrowInvocationTargetException() && invocationTargetException.getCause() != null) {
throw invocationTargetException.getCause();
}
throw makeIllegalArgumentException("ff4j-aop: Cannot invoke method " + method.getName() + " on bean " + alterBean, invocationTargetException);
} catch (Exception exception) {
throw makeIllegalArgumentException("ff4j-aop: Cannot invoke method " + method.getName() + " on bean " + alterBean, exception);
}
}
private boolean shouldCallAlterClazzMethod(final MethodInvocation pMInvoc, Class> alterClass, Logger logger) {
boolean callAlterBeanMethod = false;
Method method = pMInvoc.getMethod();
Class> currentClass = pMInvoc.getThis().getClass();
if (alterClass != null && (alterClass != NullType.class)) {
callAlterBeanMethod = !currentClass.equals(alterClass);
if (!callAlterBeanMethod) {
logger.debug("FeatureFlipping on method:{} class:{} already on the alterClazz {}", method.getName(), method
.getDeclaringClass().getName(), alterClass);
}
}
return callAlterBeanMethod;
}
private Object callAlterClazzMethodOnFirst(final MethodInvocation pMInvoc, Flip ff, Logger targetLogger) throws Throwable {
Map beans = appCtx.getBeansOfType(pMInvoc.getMethod().getDeclaringClass());
for (Object bean : beans.values()) {
if (isBeanAProxyOfAlterClass(bean, ff.alterClazz())) {
return callAlterClazzMethod(pMInvoc, bean, targetLogger);
}
}
throw new BeanCreationException("ff4j-aop : bean with class '" + ff.alterClazz()
+ "' has not been found in applicationContext still declared in 'alterClazz' property of bean "
+ pMInvoc.getMethod().getDeclaringClass());
}
private Object callAlterClazzMethod(final MethodInvocation pMInvoc, Object targetBean, Logger targetLogger) throws Throwable {
Method method = pMInvoc.getMethod();
String declaringClass = method.getDeclaringClass().getName();
targetLogger.debug("FeatureFlipping on method:{} class:{}", method.getName(), declaringClass);
try {
return method.invoke(targetBean, pMInvoc.getArguments());
} catch (IllegalAccessException e) {
throw makeIllegalArgumentException("ff4j-aop: Cannot invoke " + method.getName() + " on alterbean " + declaringClass
+ " please check visibility", e);
} catch (InvocationTargetException invocationTargetException) {
if(!ff4j.isAlterBeanThrowInvocationTargetException() && invocationTargetException.getCause() != null) {
throw invocationTargetException.getCause();
}
throw makeIllegalArgumentException("ff4j-aop: Cannot invoke " + method.getName() + " on alterbean " + declaringClass
+ " please check signatures", invocationTargetException);
} catch (Exception exception) {
throw makeIllegalArgumentException("ff4j-aop: Cannot invoke " + method.getName() + " on alterbean " + declaringClass
+ " please check signatures", exception);
}
}
private IllegalArgumentException makeIllegalArgumentException(String message, Exception exception) {
return new IllegalArgumentException(message, exception);
}
protected boolean isBeanAProxyOfAlterClass(Object proxy, Class> alterClass) {
if (AopUtils.isJdkDynamicProxy(proxy)) {
try {
return ((Advised) proxy).getTargetSource().getTarget().getClass().equals(alterClass);
} catch (Exception e) {
throw new IllegalArgumentException("ff4j-aop: Cannot evaluate is target bean is proxy", e);
}
} else {
// expected to be cglib proxy then, which is simply a specialized class
return proxy.getClass().equals(alterClass);
}
}
/** {@inheritDoc} */
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.appCtx = applicationContext;
}
/**
* Getter accessor for attribute 'ff4j'.
*
* @return current value of 'ff4j'
*/
public FF4j getFf4j() {
return ff4j;
}
/**
* Setter accessor for attribute 'ff4j'.
* @param ff4j
* new value for 'ff4j '
*/
public void setFf4j(FF4j ff4j) {
this.ff4j = ff4j;
}
}