SpringBoot切面 + ThreadLocal 检查参数
2020-04-09 本文已影响0人
西5d
场景描述
最近有个需求,要对某个通用参数做限制检查,判断是否存在,不存在判定为异常请求。最直接的方式是通过给每个接口加判断,如果接口少的情况下,是最简单有效的。但当前项目的情况比较复杂,这种方式会造成大量冗余代码,也不易维护和修改。大致说下现在的情况,项目中有上百的http接口,绝大多数接口是不关心这个通用参数的。 如果统一验证该参数,如在切面验证所有接口,可能导致一些正常请求无法完成。因此,就需要筛选出指定的接口,来添加对参数的验证。
实现
根据上面的描述,现在的目标是筛选出指定的接口,如果人工来操作,仍然是非常繁琐的。这里有个特点,就是要验证参数的服务接口,下游依赖服务接口只有三个,所以只要在下游依赖服务最终调用的地方,来进行验证。如果参数异常,直接返回失败结果,参数正常则再去调用下游服务,这样就比较简洁的实现了需要的功能。但是有个关键,怎么将要验证的参数带到下游依赖服务调用入口来判断?这里使用了ThreadLocal,在上游已经存在的统一AOP切片处写入,然后在最终调用处获取来判断。也就不用从Controller入口一直将参数传递下去。如下是实现的代码。
代码详解
//threadLocal 工具类,设置和获取变量值
public class AppChannelUtils {
private static final ThreadLocal<String> appChannelLocalThread = new ThreadLocal<>();
public static String getAppChannel() {
return appChannelLocalThread.get();
}
public static void setAppChannel(String channel) {
appChannelLocalThread.set(channel);
}
public static boolean isBlankAndRemove() {
boolean result = StringUtils.isBlank(getAppChannel());
appChannelLocalThread.remove();
//在使用后清理状态
return result;
}
}
//检查参数拦截器
@Slf4j
@Aspect
@Component
public class BeanAttributesCheckInterceptor implements Ordered {
//设置返回msg
private static final String MSG = "app channel is empty";
//只拦截这三个最终调用的方法
@Pointcut("execution(public * com.company.xxx.method3(*)) "
+ "|| execution(public * com.company.xxx.method2(*))"
+ "|| execution(public * com.company.xxx.method1(*))")
private void pointcut() {
}
@Around(value = "pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//ThreadLocal中获取,进行验证。
if (AppChannelUtils.isBlankAndRemove()) {
String methodName = ((MethodSignature) joinPoint.getSignature()).getMethod().getName();
log.error("check app channel error,params:{}", new Gson().toJson(joinPoint.getArgs()));
//特殊方法的返回处理。
//补充:可以根据反射方法获取返回值类型,提供拦截后的返回值
if ("method1".equals(methodName)) {
CommonResp commonResp = new CommonResp();
commonResp.setMsg(MSG);
commonResp.setSuccess(false);
return commonResp;
} else {
ActionResp resp = new ActionResp();
resp.setResult(-1);
resp.setMsg(MSG);
return resp;
}
} else {
//参数正常,直接调用
return joinPoint.proceed();
}
}
//在前一个通用拦截器中设置参数值,所以顺序+1,保证在通用拦截器之后。
@Override
public int getOrder() {
return InterceptorOrderEnum.ENHANCE_REQUEST_CONTEXT.getPriority() + 1;
}
}
总结
这样实现来说相对是比较简洁的。除了代码中提到的,可以根据反射获取到返回值的类型,还有两个问题值得关注。
- 上面的实现代码层面虽然简洁了,但是从测试的角度来说,还是非常繁琐和复杂的,需要全部覆盖测试用例。显而易见,最终只有三个接口,但是上游接口是非常多的。
-
还有就是如果某个阶段是异步调用,使用
ThreadLocal的方式,应该是有问题的,因为整条链路不是在一个线程中执行,ThreadLocal也就没有意义。
这种情况下,可以尝试用ConcurrentHashMap
来保存上下文,完成衔接。 - 还有个问题,就是如果一个请求有多个涉及切面的方法执行。比如func1有多次重试的逻辑,
isBlankAndRemove()
方法就会有问题。因为前一个请求会清除对应数据。 - 如果换用Map来暂时存储参数,如果请求量增多,会导致Map容量过大,继而引起内存以及可能的线程资源竞争问题,这些都要注意。
以上,就是本篇的全部内容,感谢阅读~