Spring AOP与注解双剑合璧,具体场景举例。

目录

1. 目标

2. When使用AOP

3. 场景-添加LOG

4. 场景-参数处理

4.1. 解密

4.2. 补全

5. 总结


默认了解AOP和用法,你可以回顾AOP。 

1. 目标

列举几种使用AOP的业务场景

2. When使用AOP

设计或重构时,发现接口业务逻辑前后部分存在类似的处理,而且这些处理与业务逻辑关系不大,可以考虑采取切面编程。

对于功能已经开发完毕的情况,无论切面有无规律可循,都可以使用注解。当然在设计阶段就考虑到AOP,也可使用注解来定位切面。所以,不局限于AOP,无论什么时候用到注解,都是为了起到标记的作用

3. 场景-添加LOG

很好理解,采用around advice方式,在接口调用前后生成log,可用于追踪、统计。

输出内容可以包括:具体接口、调用时间、调用方、主要参数、结果、调用RT、调用链ID等

4. 场景-参数处理

针对具体的业务场景,在执行业务逻辑前,需要对入参进行通用性处理。

4.1. 解密

when

思考如下场景,考虑安全性,需要client和server双方协定加解密方式,无论对称加密还是非对称加密,都需要server对入参进行解密。为了让接口仅关注业务逻辑,所以将这些接口视为切面,将解密过程抽离为advice。

考虑的问题

  • 如何匹配join point?
    • 考虑灵活性、需要指定具体参数,所以选择注解
  • 选择哪个advice类型?
    • 官方文档推荐根据场景选择advice范围,不要扩大范围。所以我个人会选择before advice。
  • 如何修改入参数据?
    • advice方法入参JoinPoint,提供joinPoint.getArgs()行为,通过源码可以发现,其返回克隆数据,所以无法更改参数数据。
    • 上述选择before为合适范围,所以没有proceed类似方法,无法通过直接传递参数。
    • 只能借助前文讲到的MethodInvocationProceedingJoinPoint

实现代码

如下实现,适用于接口参数类型为基本类型的情况,如果是自定义包装类,需要修改定位和赋值逻辑。

定义几个注解,例如用于定位join point,明确需要解密的参数,明确秘钥相关参数等。

/**
 * 使用该注解的方法,需使用@{@link DecryptParam}指明需要解密的参数,使用@{@link SysCode}指明sysCode参数。
 * <br/>
 * 须在具体类中使用,因为若在接口中使用,无法获取到方法添加的注解。
 * @author ChenHailong
 **/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedDecrypt {
}
/**
 * 参数需要解密,方法必须@{@link NeedDecrypt}
 * <br/>
 * 须在具体类中使用,因为若在接口中使用,无法获取到方法添加的注解。
 * <br/>
 * 为什么没有直接采用参数index的方式?避免改变方法参数数量或顺序时,未更改配置的index,导致错误的情况
 * @author ChenHailong
 **/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptParam {
}
/**
 * 指明sysCode参数,参考@{@link DecryptParam}
 * <br/>
 * 须在具体类中使用,因为若在接口中使用,无法获取到方法添加的注解。
 * @author ChenHailong
 **/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface SysCode {
}

 定义具体切面

/**
 *
 * 注意:必须在实现类的方法上添加@DecryptAspect,必须使用@SysCode和@DecryptParam指明具体参数
 * @author ChenHailong
 **/
@Aspect
@Component
public class DecryptAspect {

    @Pointcut("@annotation(NeedDecrypt)")
    public void pointcut() {}

    @Before("pointcut()")
    public void decrypt(JoinPoint joinPoint) throws Exception {
        // 转换为确切的类型,避免getDeccaredField时出现不存在该field异常
        MethodInvocationProceedingJoinPoint methodInvocationProceedingJoinPoint = (MethodInvocationProceedingJoinPoint)joinPoint;
        ProxyMethodInvocation proxyMethodInvocation;
        try {
            Field methodInvocation = MethodInvocationProceedingJoinPoint.class.getDeclaredField("methodInvocation");
            // 避免出现不可访问异常
            methodInvocation.setAccessible(true);
            proxyMethodInvocation = (ProxyMethodInvocation)methodInvocation.get(methodInvocationProceedingJoinPoint);
        } catch (Exception e) {
            throw new Exception("系统异常");
        }

        // 得到具体参数数据。如果使用joinPoint.getArgs(),则只能拿到克隆的数据,更新不生效的。
        Object[] args = proxyMethodInvocation.getArguments();

        // 定位@SysCode @DecryptParam的index
        Method targetMethod = proxyMethodInvocation.getMethod();
        Annotation[][] methodParamAnnotations = targetMethod.getParameterAnnotations();
        int sysCodeIndex = -1;
        List<Integer> decryptParamsIndex = new ArrayList<>();
        for (int i = 0; i < methodParamAnnotations.length; i++) {
            Annotation[] paramAnnotation = methodParamAnnotations[i];
            if (paramAnnotation.length > 0) {
                for (Annotation annotation : paramAnnotation) {
                    if (annotation.annotationType().equals(DecryptParam.class)) {
                        decryptParamsIndex.add(i);
                    }
                    if (sysCodeIndex < 0 && annotation.annotationType().equals(SysCode.class)) {
                        sysCodeIndex = i;
                    }
                }
            }
        }
        // check
        if (sysCodeIndex < 0 || decryptParamsIndex.size() < 1) {
            throw new Exception("该接口参数解密配置错误");
        }
        // decrypt and replace args
        String sysCode = (String) args[sysCodeIndex];
        // TODO get key
        String paramKey = xxx;
        if (StringUtils.isBlank(paramKey)) {
            throw new Exception("xxxxx");
        }

        for (Integer paramsIndex : decryptParamsIndex) {
            String param = (String) args[paramsIndex];
            try {
                // TODO decrypt
                param = xxxx;
            } catch (Exception e) {
                throw new Exception("xxxxxx");
            }
            args[paramsIndex] = param;
        }
    }
}

4.2. 补全

when

面对如下场景,当前系统是一个平台,一般情况下,平台会为每个接入方提供一个ID,这个ID对应一系列基本信息。可能出于简化查询统计的目的,在创建数据模型时考虑反范式,计划存储除ID外的其他基本信息。每次入库前,就会面临补全数据的情况,如果写在逻辑中,到处都是重复代码;即使抽离为公共方法,也需要明确调用,而且与业务逻辑无关系。

考虑的问题

  • advice类型选择?
    • 如上
  • 如何定位join point?如何定位ID field?如何定位需要补全的field
    • 仍选择注解方式
    • 针对定位ID问题,因为系统设计、模型设计时一定统一该ID的命名了,所以定位field时可采取默认命名;当然也可以通过注解field满足灵活性。
    • 针对定位补全field问题,通过模型设计后,这些field的命名也就既定的,所以待补全field范围已定;
  • 如何修改自定义类对象中field数据?
    • 调用其对应的setter
    • 不直接使用field的原因
      • 不确定field归属的类层次,可能无法获取field
      • 当field在父类且非public时,无法获取field

代码实现

定义个注解

/**
 * @author ChenHailong
 **/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedFillPlatformUserInfo {
    /**
     * 方法的第几个入参,base 0
     */
    int argObjectIndex() default 0;

    /**
     * 入参对象中代表xxxID的field name
     * @return
     */
    String userIdFieldName() default "xxxx";
}

定义切面

/**
 * @author ChenHailong
 **/
@Aspect
@Component
public class FillPlatformUserInfoAspect {
    // 其他依赖

    @Pointcut("@annotation(NeedFillPlatformUserInfo)")
    public void pointcutOfFillPlatformUserInfoAspect(){}

    // cache
    private ConcurrentHashMap<String, CacheNode4NeedFillPlatformUserInfo> methodCache = new ConcurrentHashMap<>();

    @Before("pointcutOfFillPlatformUserInfoAspect()")
    public void fillPlatformUserInfo(JoinPoint joinPoint) throws FillPlatformUserInfoException {
        String methodPath = joinPoint.getSignature().getDeclaringTypeName() + "#" + joinPoint.getSignature().getName();
        CacheNode4NeedFillPlatformUserInfo cacheNode = getCacheNode4NeedFillPlatformUserInfo(joinPoint);

        // 从方法入参中取xxxID
        Object arg = joinPoint.getArgs()[cacheNode.argIndex];
        if (null == arg) {
            throw new FillPlatformUserInfoException(methodPath + " arg is null");
        }

        String xxxId;
        try {
            xxxId = String.valueOf(cacheNode.getUserIdMethod.invoke(arg));
        } catch (Exception e) {
           throw new FillPlatformUserInfoException(methodPath + " " + cacheNode.getUserIdMethod.getName() + " from " + JSON.toJSONString(arg), e);
        }

        if (StringUtils.isEmpty(xxxId)) {
            throw new FillPlatformUserInfoException(methodPath + " " + cacheNode.userIdFieldName + " value in arg is empty");
        }

        // TODO 根据xxxID,查询数据
        xxxContext context = xxxxx;
        if (null == context ) {
            throw new FillPlatformUserInfoException(methodPath + " context is null,id" + xxxId);
        }

        // 将平台类用户信息设置到方法入参
        try {
            cacheNode.setRealUserIdMethod.invoke(arg, context.getxxxId());
            cacheNode.setQdCodeMethod.invoke(arg, context.getQdCode());
            cacheNode.setProductCodeMethod.invoke(arg, context.getProductCode());
        } catch (Exception e) {
            throw new FillPlatformUserInfoException(methodPath + " set xxx xxx xxx ex", e);
        }
    }

    private CacheNode4NeedFillPlatformUserInfo getCacheNode4NeedFillPlatformUserInfo(JoinPoint joinPoint) throws FillPlatformUserInfoException {
        String methodPath = joinPoint.getSignature().getDeclaringTypeName() + "#" + joinPoint.getSignature().getName();
        CacheNode4NeedFillPlatformUserInfo cacheNode = methodCache.get(methodPath);
        if (null == cacheNode){
            Method method = getTargetMethod(joinPoint);
            if (!method.isAnnotationPresent(NeedFillPlatformUserInfo.class)){
                throw new FillPlatformUserInfoException( methodPath + " not use NeedFillPlatformUserInfo");
            }

            NeedFillPlatformUserInfo needFillPlatformUserInfo = method.getAnnotation(NeedFillPlatformUserInfo.class);
            int index = needFillPlatformUserInfo.argObjectIndex();
            String userIdFieldName = needFillPlatformUserInfo.userIdFieldName();
            
            if (method.getParameterTypes().length - 1 < index || StringUtils.isEmpty(userIdFieldName)){
                throw new FillPlatformUserInfoException(methodPath + " argIndex is out of args length or userIdFieldName is empty");
            }

            Method setRealUserIdMethod;
            Method setQdCodeMethod;
            Method setProductCodeMethod;
            Method getUserIdMethod;

            Class argClass = method.getParameterTypes()[index];
            setRealUserIdMethod = findMethod(argClass, "setRealUserId");
            setQdCodeMethod = findMethod(argClass, "setQdCode");
            setProductCodeMethod = findMethod(argClass, "setProductCode");

            String temp = userIdFieldName.substring(0, 1).toUpperCase() + userIdFieldName.substring(1, userIdFieldName.length());
            getUserIdMethod = findMethod(argClass, "get" + temp);

            cacheNode = new CacheNode4NeedFillPlatformUserInfo(index, userIdFieldName, setRealUserIdMethod, setQdCodeMethod, setProductCodeMethod, getUserIdMethod);
            methodCache.put(methodPath, cacheNode);
        }

        return cacheNode;
    }

    private Method findMethod(Class<?> clazz, String methodName) throws FillPlatformUserInfoException {
        for (Method method : clazz.getMethods()) {
            if (method.getName().equals(methodName)){
                return method;
            }
        }
        throw new FillPlatformUserInfoException(clazz.getName()+"#" + methodName + " not found");
    }

    private Method getTargetMethod(JoinPoint joinPoint) throws FillPlatformUserInfoException {
        MethodInvocationProceedingJoinPoint methodInvocationProceedingJoinPoint = (MethodInvocationProceedingJoinPoint)joinPoint;
        ProxyMethodInvocation proxyMethodInvocation = null;
        try {
            Field field = methodInvocationProceedingJoinPoint.getClass().getDeclaredField("methodInvocation");
            field.setAccessible(true);
            proxyMethodInvocation = (ProxyMethodInvocation)field.get(methodInvocationProceedingJoinPoint);
        } catch (Exception e) {
            throw new FillPlatformUserInfoException("系统异常");
        }
        return proxyMethodInvocation.getMethod();
    }

    class CacheNode4NeedFillPlatformUserInfo{
        int argIndex;
        String userIdFieldName;
        Method setRealUserIdMethod;
        Method setQdCodeMethod;
        Method setProductCodeMethod;
        Method getUserIdMethod;

        public CacheNode4NeedFillPlatformUserInfo(int index, String userIdFieldName, Method setRealUserIdMethod, Method setQdCodeMethod, Method setProductCodeMethod, Method getUserIdMethod) {
            this.argIndex = index;
            this.userIdFieldName = userIdFieldName;
            this.setRealUserIdMethod = setRealUserIdMethod;
            this.setQdCodeMethod = setQdCodeMethod;
            this.setProductCodeMethod = setProductCodeMethod;
            this.getUserIdMethod = getUserIdMethod;
        }
    }
}

5. 总结

注解+Spring AOP的组合,具有方便灵活的特点,可以很简洁的在方法维度抽离业务无关的逻辑。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值