AOP支持spEL表达式

在使用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 
    }

}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值