目录
默认了解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的组合,具有方便灵活的特点,可以很简洁的在方法维度抽离业务无关的逻辑。