當JAVA註解、AOP、SpEL相遇,更多可能變為了現實

架構悟道發表於2022-06-20

常規情況下,我們可以通過業務定製化的註解,藉助AOP機制來實現某些通用的處理策略。比如定義個@Permission註解,可以用於標識在具體的方法上,然後用來指定某個方法必須要指定角色的人才能夠訪問呼叫。


    // 標識只有管理員角色才能呼叫此介面
    @Permission(role = UserRole.ADMIN)
    public void deleteResource(DeleteResourceReqBody reqBody) {
        // do something here...
    }

這裡,註解裡面傳入的引數始終是編碼的時候就可以確定下來的固定值(role = UserRole.ADMIN)。

在業務開發中,也許你會遇到另一種場景:

比如有個文件資源控制介面,你需要判斷出當前使用者操作的目標文件ID,然後去判斷這個使用者是否有此文件的操作許可權。

我們希望能夠使用註解的方式來實現,需要能夠將動態的文件ID通過註解傳遞,然後在Aspect處理類中獲取到文件ID然後進行對應的許可權控制。但是按照常規方式去寫程式碼的時候,會發現並不支援直接傳遞一個請求物件到註解中。

這個時候,就輪到我們的主角“SpEL表示式”上場了,藉助EL表示式,可以讓我們將上面的想法變為現實。

下面講一下具體的做法。

  1. 先定義一個業務註解,其中引數支援傳入EL表示式

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface ResourceAccessPermission {
    /**
     * 操作的目標資源的唯一ID, 支援EL表示式
     *
     * @return  ID
     */
    String objectId();
}

  1. 編寫EL表示式的解析器,如下所示:

public class ExpressionEvaluator<T> extends CachedExpressionEvaluator {
    private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer();
    private final Map<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<>(64);
    private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64);


    public EvaluationContext createEvaluationContext(Object object, Class<?> targetClass, Method method, Object[] args) {
        Method targetMethod = getTargetMethod(targetClass, method);
        ExpressionRootObject root = new ExpressionRootObject(object, args);
        return new MethodBasedEvaluationContext(root, targetMethod, args, this.paramNameDiscoverer);
    }


    public T condition(String conditionExpression, AnnotatedElementKey elementKey, EvaluationContext evalContext, Class<T> clazz) {
        return getExpression(this.conditionCache, elementKey, conditionExpression).getValue(evalContext, clazz);
    }

    private Method getTargetMethod(Class<?> targetClass, Method method) {
        AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
        Method targetMethod = this.targetMethodCache.get(methodKey);
        if (targetMethod == null) {
            targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
            this.targetMethodCache.put(methodKey, targetMethod);
        }
        return targetMethod;
    }

}

@Getter
@ToString
@AllArgsConstructor
public class ExpressionRootObject {
    private final Object object;
    private final Object[] args;
}

  1. 編寫對應的Aspect切換處理類,藉助上面的EL解析器進行獲取註解中的傳入的EL表示式,然後獲取方法的入參,讀取EL表示式代表的真實的引數值,進而按照業務需要的邏輯進行處理。

@Component
@Aspect
@Slf4j
public class ResourceAccessPermissionAspect {
    private ExpressionEvaluator<String> evaluator = new ExpressionEvaluator<>();

    @Pointcut("@annotation(com.vzn.demo.ResourceAccessPermission)")
    private void pointCut() {

    }

    @Before("pointCut()")
    public void doPermission(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
         ResourceAccessPermission permission = method.getAnnotation(ResourceAccessPermission.class);
        if (joinPoint.getArgs() == null) {
            return;
        }

        // [重點]EL表示式的方式讀取對應引數值
         EvaluationContext evaluationContext = evaluator.createEvaluationContext(joinPoint.getTarget(),
                joinPoint.getTarget().getClass(), ((MethodSignature) joinPoint.getSignature()).getMethod(),
                joinPoint.getArgs());
         AnnotatedElementKey methodKey =
                 new AnnotatedElementKey(((MethodSignature) joinPoint.getSignature()).getMethod(),
                        joinPoint.getTarget().getClass());

        // 讀取objectID,如果以#開頭則按照EL處理,否則按照普通字串處理
        String objectId;
        if (StringUtils.startsWith(permission.objectId(), "#")) {
            objectId = evaluator.condition(permission.objectId(), methodKey, evaluationContext, String.class);
        } else {
            objectId = permission.objectId();
        }

        // TODO 對objectID進行業務自定義邏輯處理
    }
}

至此,通過EL表示式動態註解引數傳遞與解析處理的邏輯就都構建完成了。

  1. 具體業務使用的時候,直接通過EL表示式從請求體中動態的獲取到對應的引數值然後傳入到註解aspect切面處理邏輯中,按照定製的業務邏輯進行統一處理。

    @ResourceAccessPermission(objectId = "#reqBody.docUniqueId")
    public void deleteResource(DeleteResourceReqBody reqBody) {
        // do something here...
    }

藉助JAVA註解 + AOP + SpEL的組合,會讓我們在很多實際問題的處理上變得遊刃有餘,可以抽象出很多公共通用的處理邏輯,實現通用邏輯與業務邏輯的解耦,便於業務層程式碼的開發。


我是悟道君,聊技術、又不僅僅聊技術~
期待與你一起探討,一起成長為更好的自己。

相關文章