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;
    }
}

总结

这样实现来说相对是比较简洁的。除了代码中提到的,可以根据反射获取到返回值的类型,还有两个问题值得关注。

  1. 上面的实现代码层面虽然简洁了,但是从测试的角度来说,还是非常繁琐和复杂的,需要全部覆盖测试用例。显而易见,最终只有三个接口,但是上游接口是非常多的。
  2. 还有就是如果某个阶段是异步调用,使用ThreadLocal的方式,应该是有问题的,因为整条链路不是在一个线程中执行,ThreadLocal也就没有意义。这种情况下,可以尝试用ConcurrentHashMap来保存上下文,完成衔接。
  3. 还有个问题,就是如果一个请求有多个涉及切面的方法执行。比如func1有多次重试的逻辑,isBlankAndRemove()方法就会有问题。因为前一个请求会清除对应数据。
  4. 如果换用Map来暂时存储参数,如果请求量增多,会导致Map容量过大,继而引起内存以及可能的线程资源竞争问题,这些都要注意。
    以上,就是本篇的全部内容,感谢阅读~
上一篇下一篇

猜你喜欢

热点阅读