[2] WebAsyncManagerIntegrationFilter

WebAsyncManagerIntegrationFilter是Spring MVC中处理异步请求的关键组件,它与SecurityContextHolder结合,确保在异步任务中能获取到安全上下文信息。通过SecurityContextCallableProcessingInterceptor在异步调用前后保存和清除SecurityContext,使得WebAsyncTask能在不同线程中正确获取到用户认证信息,而Runnable则无法做到这一点。

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的作用密不可分。

[traceId:] 2025-08-07T15:35:00.097+08:00 DEBUG 487596 --- [ scheduling-1] c.y.s.d.O.selectList  : ==> Preparing: SELECT settlement_id,pay_no,biz_pay_no,order_number,pay_type,pay_amount,user_id,is_clearing,create_time,clearing_time,pay_status,dvy_status,version FROM tz_order_settlement WHERE (dvy_status IN (?) AND pay_type = ? AND pay_status = ? AND create_time BETWEEN ? AND ?) [traceId:] 2025-08-07T15:35:00.098+08:00 DEBUG 487596 --- [ scheduling-1] c.y.s.d.O.selectList  : ==> Parameters: 0(Integer), 1(Integer), 1(Integer), 2025-08-02 15:35:00.006(Timestamp), 2025-08-07 15:40:00.006(Timestamp) [traceId:] 2025-08-07T15:35:00.109+08:00 DEBUG 487596 --- [ scheduling-1] c.y.s.d.O.selectList  : <== Total: 0 [traceId:] 2025-08-07T15:35:00.109+08:00  INFO 487596 --- [ scheduling-1] c.y.shop.service.impl.OrderServiceImpl  : 没有获取到需要上传到微信发货的订单 [traceId:] 2025-08-07T15:35:58.229+08:00  INFO 577432 --- [ main] com.yami.shop.admin.WebApplication  : Starting WebApplication using Java 17.0.15 with PID 577432 (D:\shop-admin\yami-shop-admin\target\classes started by Administrator in D:\shop-admin) [traceId:] 2025-08-07T15:35:58.231+08:00 DEBUG 577432 --- [ main] com.yami.shop.admin.WebApplication  : Running with Spring Boot v3.0.7, Spring v6.0.9 [traceId:] 2025-08-07T15:35:58.233+08:00  INFO 577432 --- [ main] com.yami.shop.admin.WebApplication  : The following 1 profile is active: "tks-test" [traceId:] 2025-08-07T15:36:00.327+08:00  INFO 577432 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode [traceId:] 2025-08-07T15:36:00.330+08:00  INFO 577432 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Redis repositories in DEFAULT mode. [traceId:] 2025-08-07T15:36:00.356+08:00  INFO 577432 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 14 ms. Found 0 Redis repository interfaces. [traceId:] 2025-08-07T15:36:00.835+08:00  WARN 577432 --- [ main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: com.yami.shop.common.aspect.RedisLockAspect [traceId:] 2025-08-07T15:36:00.836+08:00  WARN 577432 --- [ main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: com.yami.shop.sys.aspect.SysLogAspect [traceId:] 2025-08-07T15:36:01.518+08:00  INFO 577432 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8085 (http) [traceId:] 2025-08-07T15:36:01.528+08:00  INFO 577432 --- [ main] o.apache.catalina.core.StandardService  : Starting service [Tomcat] [traceId:] 2025-08-07T15:36:01.528+08:00  INFO 577432 --- [ main] o.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/10.1.8] [traceId:] 2025-08-07T15:36:01.651+08:00  INFO 577432 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/]  : Initializing Spring embedded WebApplicationContext [traceId:] 2025-08-07T15:36:01.652+08:00  INFO 577432 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 3118 ms [traceId:] 2025-08-07T15:36:01.661+08:00  INFO 577432 --- [ main] c.y.s.s.c.a.DefaultAuthConfigAdapter  : not implement other AuthConfigAdapter, use DefaultAuthConfigAdapter... all url need auth... [traceId:] 2025-08-07T15:36:01.852+08:00  INFO 577432 --- [ main] org.redisson.Version  : Redisson 3.19.3 [traceId:] 2025-08-07T15:36:02.152+08:00  INFO 577432 --- [isson-netty-2-6] o.r.c.pool.MasterPubSubConnectionPool  : 1 connections initialized for localhost/127.0.0.1:6379 [traceId:] 2025-08-07T15:36:02.197+08:00  INFO 577432 --- [sson-netty-2-20] o.r.c.pool.MasterConnectionPool  : 24 connections initialized for localhost/127.0.0.1:6379 [traceId:] 2025-08-07T15:36:02.961+08:00 ERROR 577432 --- [ main] c.b.m.core.MybatisConfiguration  : mapper[com.yami.shop.dao.BasketMapper.getShopCartItems] is ignored, because it exists, maybe from xml file [traceId:] 2025-08-07T15:36:03.191+08:00  WARN 577432 --- [ main] c.b.m.core.injector.AbstractMethod  : [com.yami.shop.dao.DistributionProdMapper.selectPage] Has been loaded by XML or SqlProvider or Mybatis's Annotation, so ignoring this injection for [class com.baomidou.mybatisplus.core.injector.methods.SelectPage] [traceId:] 2025-08-07T15:36:03.966+08:00  WARN 577432 --- [ main] c.b.m.core.injector.AbstractMethod  : [com.yami.shop.dao.UserMerchantMchMapper.selectPage] Has been loaded by XML or SqlProvider or Mybatis's Annotation, so ignoring this injection for [class com.baomidou.mybatisplus.core.injector.methods.SelectPage] [traceId:] 2025-08-07T15:36:04.929+08:00 DEBUG 577432 --- [ main] c.y.s.c.t.TraceThreadPoolTaskExceutor  : Initializing ExecutorService [traceId:] 2025-08-07T15:36:04.930+08:00 DEBUG 577432 --- [ main] c.y.s.c.t.TraceThreadPoolTaskExceutor  : Initializing ExecutorService 'taskExecutor' [traceId:] 2025-08-07T15:36:06.438+08:00  WARN 577432 --- [ main] c.b.m.core.metadata.TableInfoHelper  : Can not find table primary key in Class: "com.yami.shop.bean.model.PayInfo". [traceId:] 2025-08-07T15:36:06.439+08:00  WARN 577432 --- [ main] c.b.m.core.injector.DefaultSqlInjector  : class com.yami.shop.bean.model.PayInfo ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method. [traceId:] 2025-08-07T15:36:06.729+08:00  INFO 577432 --- [ main] com.anji.captcha.util.ImageUtils  : 自定义resource底图:[SLIDING_BLOCK=[Ljava.lang.String;@295e989, ORIGINAL=[Ljava.lang.String;@504216ff, PIC_CLICK=[Ljava.lang.String;@4ce4c097] [traceId:] 2025-08-07T15:36:06.731+08:00  INFO 577432 --- [ main] c.a.c.s.impl.CaptchaServiceFactory  : supported-captchaCache-service:[redis, local] [traceId:] 2025-08-07T15:36:06.734+08:00  INFO 577432 --- [ main] c.a.c.s.impl.CaptchaServiceFactory  : supported-captchaTypes-service:[clickWord, default, blockPuzzle] [traceId:] 2025-08-07T15:36:06.735+08:00  INFO 577432 --- [ main] c.a.c.s.i.BlockPuzzleCaptchaServiceImpl  : --->>>初始化验证码底图<<<---blockPuzzle [traceId:] 2025-08-07T15:36:07.039+08:00 DEBUG 577432 --- [ main] c.y.s.c.t.TraceThreadPoolTaskExceutor  : Initializing ExecutorService [traceId:] 2025-08-07T15:36:07.040+08:00 DEBUG 577432 --- [ main] c.y.s.c.t.TraceThreadPoolTaskExceutor  : Initializing ExecutorService 'syncExecutor' [traceId:] 2025-08-07T15:36:07.174+08:00  WARN 577432 --- [ main] .s.s.UserDetailsServiceAutoConfiguration : Using generated security password: 3cb2d051-9d5a-455b-a825-3916c9f1ac8d This generated password is for development use only. Your security configuration must be updated before running your application in production. [traceId:] 2025-08-07T15:36:08.216+08:00  INFO 577432 --- [ main] o.s.b.a.e.web.EndpointLinksResolver  : Exposing 1 endpoint(s) beneath base path '/actuator' [traceId:] 2025-08-07T15:36:08.277+08:00  INFO 577432 --- [ main] o.s.s.web.DefaultSecurityFilterChain  : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@13ebbdab, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@74af4b0, org.springframework.security.web.context.SecurityContextHolderFilter@44c141ce, org.springframework.security.web.header.HeaderWriterFilter@42018e32, org.springframework.web.filter.CorsFilter@5b1295c0, org.springframework.security.web.authentication.logout.LogoutFilter@3a296643, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@642aa8ea, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5de769c8, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@bf3aa52, org.springframework.security.web.session.SessionManagementFilter@30926583, org.springframework.security.web.access.ExceptionTranslationFilter@50ba10de, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@1d413e98] [traceId:] 2025-08-07T15:36:08.946+08:00  WARN 577432 --- [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop' [traceId:] 2025-08-07T15:36:08.951+08:00  INFO 577432 --- [ main] o.s.j.e.a.AnnotationMBeanExporter  : Could not unregister MBean [com.github.tobato.fastdfs.domain.conn:name=fdfsConnectionPool,type=FdfsConnectionPool] as said MBean is not registered (perhaps already unregistered by an external process) [traceId:] 2025-08-07T15:36:08.953+08:00 DEBUG 577432 --- [ main] c.y.s.c.t.TraceThreadPoolTaskExceutor  : Shutting down ExecutorService 'syncExecutor' [traceId:] 2025-08-07T15:36:08.958+08:00 DEBUG 577432 --- [ main] c.y.s.c.t.TraceThreadPoolTaskExceutor  : Shutting down ExecutorService 'taskExecutor' [traceId:] 2025-08-07T15:36:08.991+08:00  INFO 577432 --- [ main] o.apache.catalina.core.StandardService  : Stopping service [Tomcat] [traceId:] 2025-08-07T15:36:08.995+08:00  WARN 577432 --- [ main] o.a.c.loader.WebappClassLoaderBase  : The web application [ROOT] appears to have started a thread named [Thread-2] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread: java.base@17.0.15/sun.net.dns.ResolverConfigurationImpl.notifyAddrChange0(Native Method) java.base@17.0.15/sun.net.dns.ResolverConfigurationImpl$AddressChangeListener.run(ResolverConfigurationImpl.java:176) [traceId:] 2025-08-07T15:36:09.009+08:00  INFO 577432 --- [ main] .s.b.a.l.ConditionEvaluationReportLogger : Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled. [traceId:] 2025-08-07T15:36:09.032+08:00 ERROR 577432 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter  : *************************** APPLICATION FAILED TO START *************************** Description: Web server failed to start. Port 8085 was already in use. Action: Identify and stop the process that's listening on port 8085 or configure this application to listen on another port.
最新发布
08-08
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值