zoukankan      html  css  js  c++  java
  • SpEL + AOP实现注解的动态赋值.

    一、自定义注解

    先聊聊这个需求,我需要根据用户的权限对数据进行一些处理,但是痛点在哪里呢?用户的权限是在请求的时候知道的,我怎么把用户的权限传递给处理规则呢?想了以下几种方案:

    1. Mybatis 拦截器:如果你的权限参数可以渗透到 Dao 层,那么这是最好的处理方式,直接在 Dao 层数据返回的时候,根据权限做数据处理。
    2. Dubbo 过滤器:如果 Dao 层没办法实现的话,只好考虑在 service 层做数据处理了。
    3. ResponseBodyAdvice :要是 service 层也没办法做到,只能在访问层数据返回的时候,根据权限做数据处理。(以下介绍的正是这种方式)

    那么现在有个难点就是:我怎么把 request 的权限参数传递到 response 中呢?当然可以在 Spring 拦截器中处理,但是我不想把这段代码侵入到完整的鉴权逻辑中。突然想到,我能不能像 spring-data-redis 中 @Cacheable 一样,利用注解和 SpEL 表达式动态的传递权限参数呢?然后在 ResponseBodyAdvice 读取这个注解的权限参数,进而对数据进行处理。

    首先,我们需要有个自定义注解,它有两个参数:key 表示 SpEL 表达式;userType 表示权限参数。

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ResponseSensitiveOverride {
    
        /**
         * SPEL 表达式
         *
         * @return
         */
        String key() default "";
    
        /**
         * 1:主账号、2:子账号
         */
        int userType() default 1;
    }
    

    然后,把这个注解放在路由地址上,key 写入获取权限参数的 SpEL 表达式:

        @ResponseSensitiveOverride(key = "#driverPageParam.getUserType()")
        @RequestMapping(value = "/queryPage", method = RequestMethod.POST)
        public ResponseData<PageVo<AdminDriverVo>> queryPage(@RequestBody AdminDriverPageParam driverPageParam) {
            return driverService.queryPageAdmin(driverPageParam);
        }
    

    二、SpEl + AOP 注解赋值

    现在 SpEL 表达式是有了,怎么把 SpEL 表达式的结果赋值给注解的 userType 参数呢?这就需要用 Spring AOPJava 反射动态代理 的知识。

    @Aspect
    @Component
    public class SensitiveAspect {
    
        private SpelExpressionParser spelParser = new SpelExpressionParser();
    
        /**
         * 返回通知
         */    
        @AfterReturning("@annotation(com.yungu.swift.base.model.annotation.ResponseSensitiveOverride) && @annotation(sensitiveOverride)")
        public void doAfter(JoinPoint joinPoint, ResponseSensitiveOverride sensitiveOverride) throws Exception {
            //获取方法的参数名和参数值
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            List<String> paramNameList = Arrays.asList(methodSignature.getParameterNames());
            List<Object> paramList = Arrays.asList(joinPoint.getArgs());
    
            //将方法的参数名和参数值一一对应的放入上下文中
            EvaluationContext ctx = new StandardEvaluationContext();
            for (int i = 0; i < paramNameList.size(); i++) {
                ctx.setVariable(paramNameList.get(i), paramList.get(i));
            }
    
            // 解析SpEL表达式获取结果
            String value = spelParser.parseExpression(sensitiveOverride.key()).getValue(ctx).toString();
            //获取 sensitiveOverride 这个代理实例所持有的 InvocationHandler
            InvocationHandler invocationHandler = Proxy.getInvocationHandler(sensitiveOverride);
            // 获取 invocationHandler 的 memberValues 字段
            Field hField = invocationHandler.getClass().getDeclaredField("memberValues");
            // 因为这个字段是 private final 修饰,所以要打开权限
            hField.setAccessible(true);
            // 获取 memberValues
            Map memberValues = (Map) hField.get(invocationHandler);
            // 修改 value 属性值
            memberValues.put("userType", Integer.parseInt(value));
    
        }
    }
    

    通过这种方式,我们就实现了为注解动态赋值。

    三、ResponseBodyAdvice 处理数据

    现在要做的事情就是在 ResponseBody 数据返回前,对数据进行拦截,然后读取注解上的权限参数,从而对数据进行处理,这里使用的是 SpringMVC 的 ResponseBodyAdvice 来实现:

    @Slf4j
    @RestControllerAdvice
    @Order(-1)
    public class ResponseBodyAdvice implements org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice {
    
        private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
            @Override
            protected Integer initialValue() {
                return SysUserDto.USER_TYPE_PRIMARY;
            }
        };
    
        @Override
        public boolean supports(MethodParameter returnType, Class converterType) {
            if (returnType.hasMethodAnnotation(ResponseSensitiveOverride.class)) {
                ResponseSensitiveOverride sensitiveOverride = returnType.getMethodAnnotation(ResponseSensitiveOverride.class);
                threadLocal.set(sensitiveOverride.userType());
                return true;
            }
            return false;
        }
    
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
            if (body != null && SysUserDto.USER_TYPE_SUB.equals(threadLocal.get())) {
                // 业务处理
            }
            return body;
        }
    }
    

    题外话,其实我最后还是摈弃了这个方案,选择了 Dubbo 过滤器的处理方式,为什么呢?因为在做数据导出的时候,这种方式没办法对二进制流进行处理呀!汗~ 但是该方案毕竟耗费了我一个下午的心血,还是在此记录一下,可能有它更好的适用场景!

  • 相关阅读:
    MyBatis学习笔记
    通过Executor来启动线程比用Thread的start()更好
    步骤2:JMeter 分布式测试(性能测试大并发、远程启动解决方案)
    步骤1:JMeter 录制脚本接口测试
    adb常用命令(手机测试)
    电商购物核心功能测试点
    APP测试基本流程以及APP测试要点
    Jmeter-jtl性能测试报告转换-2种导出方法
    Linux之find查找命令
    linux之top命令详解
  • 原文地址:https://www.cnblogs.com/jmcui/p/11890384.html
Copyright © 2011-2022 走看看