[2] WebAsyncManagerIntegrationFilter

WebAsyncManagerIntegrationFilter

介绍

    字面意思这是一个Web异步管理集成过滤器,翻译成中文依然很抽象。我们从ThreadLocal讲起,ThreadLocal可以理解为一个与当前线程绑定的Map, key是当前线程,value是要存储的object。当我们在同一个线程中,可以通过get()方法获取到value。如果在A线程set(object),然后在B线程中调用get(),由于线程已切换,key是A,拿B自然取不出key为A的value。对于一个SSO系统,我们常常把当前登录的用户相关信息放到ThreadLocal中,避免每次使用的时候都去调RPC接口或者DB接口去查询。Spring Security的SecurityContextHolder就是通过ThreadLocal实现的。
WebAsyncManager,Spring文档有这么一句话:The central class for managing asynchronous request processing, mainly intended as an SPI and not typically used directly by application classes. 它主要用于异步请求,笔者发现这个东西在spring mvc请求分发处理中用到很多,时间有限,笔者也没有深入的了解,我们的主要关注点是WebAsyncManagerIntegrationFilter。

代码分析

步骤1

    WebAsyncManagerIntegrationFilter#doFilterInternal()关键代码如下:

@Override
protected void doFilterInternal(HttpServletRequest request,
        HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager
            .getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY);
    if (securityProcessingInterceptor == null) {
        asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY,
                new SecurityContextCallableProcessingInterceptor());
    }

    filterChain.doFilter(request, response);
}

步骤2

    SecurityContextCallableProcessingInterceptor,这个拦截器的关键作用,就是执行前后存储和清空SecurityContext,以便在WebAsyncTask异步调用中获取到SecurityContext,核心代码如下:

@Override
public <T> void beforeConcurrentHandling(NativeWebRequest request, Callable<T> task) {
    if (securityContext == null) {
        setSecurityContext(SecurityContextHolder.getContext());
    }
}

@Override
public <T> void preProcess(NativeWebRequest request, Callable<T> task) {
    SecurityContextHolder.setContext(securityContext);
}

@Override
public <T> void postProcess(NativeWebRequest request, Callable<T> task,
        Object concurrentResult) {
    SecurityContextHolder.clearContext();
}

private void setSecurityContext(SecurityContext securityContext) {
    this.securityContext = securityContext;
}

步骤3

    笔者做了一个实践,分别一个Runnable和WebAsyncTask中尝试从SecurityContextHolder获取当前登录用户,AuthHolder只是对SecurityContextHolder进行封装,本质上还是调用的SecurityContextHolder.get()获取线程上下文信息,代码如下:

@ApiOperation("[WebAsyncTask]异步获取上下文用户信息")
@GetMapping("/web_async_task")
public WebAsyncTask getContextByWebAsyncTask() {
    log.info("1:main线程开始[{}]", Thread.currentThread());
    WebAsyncTask<Oauth2User> task = new WebAsyncTask<>(300L, () -> {
        log.info("1:WebAsyncTask异步线程[{}], 开始执行异步任务", Thread.currentThread().getName());
        Oauth2User user = AuthHolder.user();
        log.info("2:WebAsyncTask异步线程[{}], 任务执行完成, 结果:{}",
            Thread.currentThread().getName(), JSON.toJSONString(user));
        return user;
    });

    log.info("2:main线程结束[{}]", Thread.currentThread());
    return task;
}

@ApiOperation("[Runnable]异步获取上下文用户信息")
@GetMapping("/runnable")
public R<Oauth2User> getContextByRunnable() throws Exception {
    log.info("1:main线程开始[{}]", Thread.currentThread());
    final List<Oauth2User> list = new ArrayList<>();
    CountDownLatch latch = new CountDownLatch(1);
    Thread async = new Thread(() -> {
        try {
            log.info("1:Runnable异步线程[{}], 开始执行异步任务", Thread.currentThread().getName());
            Oauth2User user = AuthHolder.user();
            list.add(user);
            log.info("2:Runnable异步线程[{}], 任务执行完成, 结果:{}",
                Thread.currentThread().getName(), JSON.toJSONString(user));
        } finally {
            latch.countDown();
        }
    });
    async.start();
    latch.await();
    log.info("2:main线程结束[{}]", Thread.currentThread());
    return R.success(list.isEmpty() ? null : list.get(0));
}

执行结果如下:
image.png    可以看出,WebAsyncTask从SecurityContextHolder获取到了身份认证信息,但Runnable却不可以。WebAsyncTask之所以能够在异步线程中从SecurityContextHolder中获取上下文信息,与WebAsyncManagerIntegrationFilter的作用密不可分。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章