在使用spring-data-cache
时,我们使用aop注解是支持spEL(Spring Expression Language)
的,
demo
@Override
@Cacheable(value = "#id",cacheNames = "CACHE1")
public UserInfo getById(String id) {
return userMappger.getById(id);
}
自定义的AOP如何支持支持spEL表达式呢?
背景
AclCheckPoint
假设有切入点
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface AclCheckPoint {
/**
* id-expression
* if "#paramX.name " means paramX.getName().
* if null, means args[0]
* if "#paramX" has no "." ,means paramX .
*
* @return
*/
String expression() default "";
}
业务代码
@Acl //自定义注解,后面会有说明
public class UserServiceImpl implements IUserService {
@Override
@AclCheckPoint(expression = "#dto.id")
public int update(UserDto dto) {
//do nothing
}
}
Aspect切面
@Aspect
public class AclCheckAspect {
@Before("@annotation(aclAnno)")
public void before(JoinPoint joinPoint, AclCheckPoint aclAnno) throws Exception {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Object args[] = joinPoint.getArgs(); //
String expression = aclAnno.getExpression(); // "#dto.id"
}
}
如何才能通过#dto.id
解析出来请求我们真正需要的参数呢?
基本解决
回顾一下被拦截的方法
@Override
@AclCheckPoint(expression = "#dto.id")
public int update(UserDto dto) {
//do nothing
}
我们已知请求参数args[]
和表达式#dto.id
,接下来的几个问题
- 首先我们要将表达式
split
为参数名称
和属性名称
两个部分 - 获取被拦截的方法的
参数名称
列表,匹配找到目标参数 - 最后反射执行对应属性的
getter
解析表达式
void findParamPair(Method method, String expression) {
String[] arr = expression.split("\\.");
String paramName = arr[0].replace("#", "");
String field = arr[1];
//todo xxxx
}
解析参数名称
JDK8 —不建议
public int getParam(String paramName,MethodSignature signature){
String[] parameterNames = signature.getParameterNames(); //依赖jdk8
int index = -1;
for(int i = 0 ; i < parameterNames.length ; i++){
if(parameterNames[i].equals(paramName)){
index = i;
break;
}
}
if(index == -1){
throw new RuntimeException("method [" + signature.getMethod().getName() + "] does not have parameter name:" + paramName);
}
return index; // param = args[index]
}
spring:LocalVariableTableParameterNameDiscoverer
public int getParam(String paramName,MethodSignature signature){
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
String[] definedParamNames = discoverer.getParameterNames(signature.getMethod());
int index = definedParamNames.length;
while (index >= 0) {
index--;
if (paramName.equals(definedParamNames[index])) {
break;
}
}
if (index < 0) {
throw new RuntimeException("method [" + signature.getMethod().getName() + "] does not have parameter name:" + paramName);
}
return index; // param = args[index]
}
获取getter
public Method getter(int index ,MethodSignature signature,String field) throws NoSuchMethodException {
Class parameterType = signature.getMethod().getParameterTypes()[index];
String getterName = "getter" + field.substring(0, 1).toUpperCase() + field.substring(1);
Method getter = parameterType.getMethod(getterName,null);
return getter;
}
AOP拦截获取expression-value
@Before("@annotation(aclAnno)")
public void before(JoinPoint joinPoint, AclCheckPoint aclAnno) throws Exception {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Object[] args = joinPoint.getArgs();
int index = getParam(paramName, signature, args);
Object param = args[index];
Method getter = getter(index, signature, field);
Object expressionValue = getter.invoke(param); //获取value
}
到此为止,似乎问题全部解决了,也能达到预期的目标, 那么这个解决方案还有优化的点吗?
优化解决
在使用spring-data-cache
时,在启动时会检测@Cacheable
的注解是否合法有效,那么在上面的解决方案中(饿汉
),只有在方法被AOP切面拦截的时候候才能发现问题(懒汉
),这就给发现问题带来了一定难度以及带来一定的性能损耗(上述的解析过程,反射等),如何实现饿汉
模式呢?
- 如果在Spring容器初始化的时候,就进行扫描出目标方法
- 对带有
AclCheckPoint
的方法,预先解析,并将解析方法存放于缓存之中 - AOP切面直接从缓存中寻找对应的解析方法,并执行
扫描目标方法
Acl
为了增加扫描的效率 我们新增一个注解,只对带有该注解的方法进行拦截
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Acl {
}
basepackage
为了更进一步加快扫描效率,可以只对目标路径进行扫描。
扫描: ClassPathScanningCandidateComponentProvider
private Set<BeanDefinition> scan() {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(Acl.class));
String[] packages = StringUtils.tokenizeToStringArray(this.basePackage, ",; \t\n");
Set<BeanDefinition> definitions = new HashSet<>();
for (String pkg : packages) {
definitions.addAll(scanner.findCandidateComponents(pkg));
}
return definitions;
}
初始化并缓存
扫描并初始化
public class CacheableIdDetectorFactory<T> implements I InitializingBean {
private static final Map<Method, IdDetector> cache = new HashedMap(); //自定义对象IdDetector
public IdDetector getIdDetector(Method method) { //供外部调用,从cache获取
IdDetector idDetector = cache.get(method);
if (idDetector == null) {
throw new RuntimeException("Did not find IdDetector for [" + method.getName() + "] ,maybe method's target-class does not have @Acl?");
}
return idDetector;
}
@Override
public void afterPropertiesSet() throws Exception {
Set<BeanDefinition> definitions = scan();
definitions.stream().map(bd -> {
String className = bd.getBeanClassName();
try {
Class clazz = Class.forName(className);
return clazz.getMethods();
} catch (ClassNotFoundException e) {
logger.error("[" + className + "] not found :", e);
System.exit(-1);
}
return null;
}).filter(Objects::nonNull).flatMap(Arrays::stream).forEach(this::initCache);
}
}
切面获取cache的处理方法
@Before("@annotation(aclAnno)")
public void before(JoinPoint joinPoint, AclCheckPoint aclAnno) throws Exception {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
IdDetector<String> idDetector = idDetectorFactory.getIdDetector(signature.getMethod());
Object[] args = joinPoint.getArgs();
String expressValue = idDetector.detect(args);
}
完整代码
自定义IdDetector
public class IdDetector<T> {
private final Function<Object[], T> function;
IdDetector(Function<Object[], T> f) {
this.function = f;
}
public T detect(Object[] args) {
return function.apply(args);
}
}
IdDetectorFactory
public interface IdDetectorFactory<T> {
IdDetector<T> getIdDetector(Method method) throws Exception;
String EXPRESSION_PREFIX = "#";
String EXPRESSION_SPLIT = "\\.";
String GETTER_PREFIX = "get";
default String getterMethodName(String field) {
return GETTER_PREFIX + field.substring(0, 1).toUpperCase() + field.substring(1);
}
default Method getterMethod(Class clazz, String field) throws NoSuchMethodException {
String fieldGetterName = getterMethodName(field);
Method getter = clazz.getDeclaredMethod(fieldGetterName);
return getter;
}
LocalVariableTableParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new LocalVariableTableParameterNameDiscoverer();
default Pair findParamPair(Method method, String expression) {
String[] arr = expression.split(EXPRESSION_SPLIT);
String paramName = arr[0].replace(EXPRESSION_PREFIX, "");
String[] definedParamNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method);
int index = definedParamNames.length;
while (index >= 0) {
index--;
if (paramName.equals(definedParamNames[index])) {
break;
}
}
if (index < 0) {
throw new RuntimeException("method [" + method.getName() + "] does not have parameter name:" + paramName);
}
if(arr.length == 1){
return new Pair(index, null);
}
return new Pair(index, arr[1]);
}
@Getter
@AllArgsConstructor
class Pair {
int index;
String field;
}
}
CacheableIdDetectorFactory
public class CacheableIdDetectorFactory<T> implements IdDetectorFactory, InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(CacheableIdDetectorFactory.class);
private static final Map<Method, IdDetector> cache = new HashedMap();
public IdDetector getIdDetector(Method method) {
IdDetector idDetector = cache.get(method);
if (idDetector == null) {
throw new RuntimeException("Did not find IdDetector for [" + method.getName() + "] ,maybe method's target-class does not have @Acl?");
}
return idDetector;
}
private void initCache(Method method) throws RuntimeException {
AclCheckPoint aclAnno = method.getAnnotation(AclCheckPoint.class);
if (aclAnno == null) {
return;
}
String expression = aclAnno.expression();
Function<Object[], T> function;
build_function_label:
{
if (Strings.isNullOrEmpty(expression)) {
//args[0]
function = (objs) -> (T) objs[0];
break build_function_label;
}
Pair pair = findParamPair(method, expression);
int index = pair.getIndex();
String field = pair.getField();
if (Strings.isNullOrEmpty(field)) {
//args[x]
function = (objs) -> (T) objs[index];
break build_function_label;
}
Class clazz = method.getParameterTypes()[index];
//getter
try {
Method fieldGetter = getterMethod(clazz, field);
function = (objs) -> {
try {
//args[x].getterField()
return (T) fieldGetter.invoke(objs[index]);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
};
} catch (NoSuchMethodException e) {
function = (objs) -> (T) new RuntimeException("throwable IdDetector --- pls check your code");
}
}
cache.put(method, new IdDetector(function));
logger.info("Generate a id-detector for : {}.{} ", method.getDeclaringClass().getSimpleName(), method.getName());
}
private final String basePackage;
public CacheableIdDetectorFactory(String basePackage) {
this.basePackage = basePackage;
}
private Set<BeanDefinition> scan() {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(Acl.class));
String[] packages = StringUtils.tokenizeToStringArray(this.basePackage, ",; \t\n");
Set<BeanDefinition> definitions = new HashSet<>();
for (String pkg : packages) {
definitions.addAll(scanner.findCandidateComponents(pkg));
}
return definitions;
}
@Override
public void afterPropertiesSet() throws Exception {
Set<BeanDefinition> definitions = scan();
definitions.stream().map(bd -> {
String className = bd.getBeanClassName();
try {
Class clazz = Class.forName(className);
return clazz.getMethods();
} catch (ClassNotFoundException e) {
logger.error("[" + className + "] not found :", e);
System.exit(-1);
}
return null;
}).filter(Objects::nonNull).flatMap(Arrays::stream).forEach(this::initCache);
}
}
Aspect
@Aspect
public class AclCheckAspect {
private final IdDetectorFactory idDetectorFactory;
public AclCheckAspect(IdDetectorFactory idDetectorFactory) {
this.idDetectorFactory = idDetectorFactory;
}
@Before("@annotation(aclAnno)")
public void before(JoinPoint joinPoint, AclCheckPoint aclAnno) throws Exception {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
IdDetector<String> idDetector = idDetectorFactory.getIdDetector(signature.getMethod());
Object[] args = joinPoint.getArgs();
String expressionValue = idDetector.detect(args);
//todo
}
}