1. 程式人生 > >SpEL + AOP實現註解的動態賦值

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 AOP 、Java 反射 和 動態代理 的知識。

@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 過濾器的處理方式,為什麼呢?因為在做資料匯出的時候,這種方式沒辦法對二進位制流進行處理呀!汗~ 但是該方案畢竟耗費了我一個下午的心血,還是在此記錄一下,可能有它更好的適用場景!