常規情況下,我們可以通過業務定製化的註解
,藉助AOP
機制來實現某些通用的處理策略。比如定義個@Permission
註解,可以用於標識在具體的方法上,然後用來指定某個方法必須要指定角色的人才能夠訪問呼叫。
// 標識只有管理員角色才能呼叫此介面
@Permission(role = UserRole.ADMIN)
public void deleteResource(DeleteResourceReqBody reqBody) {
// do something here...
}
這裡,註解裡面傳入的引數始終是編碼的時候就可以確定下來的固定值(role = UserRole.ADMIN
)。
在業務開發中,也許你會遇到另一種場景:
比如有個文件資源控制介面,你需要判斷出當前使用者操作的目標文件ID,然後去判斷這個使用者是否有此文件的操作許可權。
我們希望能夠使用註解的方式來實現,需要能夠將動態的文件ID通過註解傳遞,然後在Aspect
處理類中獲取到文件ID然後進行對應的許可權控制。但是按照常規方式去寫程式碼的時候,會發現並不支援直接傳遞一個請求物件到註解中。
這個時候,就輪到我們的主角“SpEL表示式
”上場了,藉助EL表示式,可以讓我們將上面的想法變為現實。
下面講一下具體的做法。
- 先定義一個業務註解,其中引數支援傳入
EL表示式
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface ResourceAccessPermission {
/**
* 操作的目標資源的唯一ID, 支援EL表示式
*
* @return ID
*/
String objectId();
}
- 編寫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;
}
- 編寫對應的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表示式動態註解引數傳遞與解析處理的邏輯就都構建完成了。
- 具體業務使用的時候,直接通過EL表示式從請求體中動態的獲取到對應的引數值然後傳入到註解
aspect
切面處理邏輯中,按照定製的業務邏輯進行統一處理。
@ResourceAccessPermission(objectId = "#reqBody.docUniqueId")
public void deleteResource(DeleteResourceReqBody reqBody) {
// do something here...
}
藉助JAVA註解 + AOP + SpEL
的組合,會讓我們在很多實際問題的處理上變得遊刃有餘,可以抽象出很多公共通用的處理邏輯,實現通用邏輯與業務邏輯的解耦,便於業務層程式碼的開發。
我是悟道君,聊技術、又不僅僅聊技術~
期待與你一起探討,一起成長為更好的自己。