Spring MVC 异步接口简析

背景


最近在一个历史遗留项目的重构工作中,遇到这样的一个需求"部分处理耗时高的接口需要做线程池隔离,避免大量耗时高的请求影响现有线程池工作";大致效果如下

前提


SpringBoot 中目前支持 webflux 同样可以接口请求异步处理,但是这些都不是基于 servlet 实现,原有业务代码中有较多使用 servlet 的地方,改为 webflux 需要做较大业务代码改动,暂时不考虑这个方案,而是采用 SpringBoot 内嵌 tomcat 开启异步的方案;

改造方法


其实比较简单,例如按照以下写法即可

java 复制代码
// 原接口
@PostMapping(value = "/mass/asycTest")
public Resp asycTest() {
    return new Resp(UUID.randomUUID().toString(), ResponseCode.SUCCESS);
}


// 改造后接口
@PostMapping(value = "/mass/asycTest")
public WebAsyncTask<Resp> asycTest() {
    System.out.println("1-thread name: " + Thread.currentThread().getName());
    return new WebAsyncTask<>(() -> {
        System.out.println("2-thread name: " + Thread.currentThread().getName());
        return new Resp(UUID.randomUUID().toString(), ResponseCode.SUCCESS);
    });
}

需要注意的是,如果有使用过滤器,那么需要给过滤器也启用异步,例如下方代码的 asyncSupported = true

java 复制代码
@WebFilter(urlPatterns = "/*", asyncSupported = true)
@Order(1)
public class HttpServletRequestWrapperFilter implements Filter {
      ......
}

否则有可能会出现如下异常

java 复制代码
java.lang.IllegalStateException: Async support must be enabled on a servlet and for all filters involved in async request processing. This is done in Java code using the Servlet API or by adding "<async-supported>true</async-supported>" to servlet and filter declarations in web.xml.
	at org.springframework.util.Assert.state(Assert.java:79) ~[spring-core-6.2.2.jar:6.2.2]
	at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.startAsync(StandardServletAsyncWebRequest.java:143) ~[spring-web-6.2.2.jar:6.2.2]
	at org.springframework.web.context.request.async.WebAsyncManager.startAsyncProcessing(WebAsyncManager.java:487) ~[spring-web-6.2.2.jar:6.2.2]

这里的 WebAsyncTask 里面有其他实现接口异步的方法 onTimeout()、onError()、onCompletion() 这些回调方法可以供我们做其他事情,例如接口请求返回响应后再删数据库数据等,如果 WebAsyncTask 里面提供的方法还是无法满足,可以试试以下几个类

异步响应类 用法
Callable 比较纯粹的异步处理方式;接口返回 Callable 时,则会在异步线程池中调用这个 Callable
ListenableFuture 在 spring 6.0 已经启用,推荐使用 CompletableFuture,用于支持处理响应结果回调的场景
CompletionStage 和 ListenableFuture、CompletableFuture 类似,其实都支持回调,区别在于支持多重异步;
WebAsyncTask 在 Callable 的基础上支持超时时间、异常处理更多场景
DeferredResult 需要等待得到结果的场景,例如第一次发请求时,请求到的接口响应结果是 DeferredResult,那个这个请求会一直等待,然后其他线程给这个 DeferredResult 设置真实的响应结果后才会把这个结果写回去给客户端; 例如我先买一张交通卡,里面没钱,需要等充钱进去这张交通卡才能用的有一个等待的过程;
ResponseBodyEmitter 支持写多个响应结果给客户端

如果上述异步响应对象都无法满足我们的场景,我们可以实现 HandlerMethodReturnValueHandler 接口来做自定义的响应结果处理;

线程池定义


在 SpringBoot 中,异步接口默认执行的线程池是 MvcSimpleAsyncTaskExecutor,这个类继承于 SimpleAsyncTaskExecutor,每次执行任务都会创建一个新的线程,开销非常大,这个类的内部其实也有提示我们不要用这个类。

如果使用的 JDK 版本是 21 或以上,可以配置 spring.threads.virtual.enabled = true 来开启虚拟线程,这样使用默认的线程池也不会导致由于线程频繁创建销毁带来的性能开销;如果使用 JDK 21 以下,可以使用以下方式自定义线程池;

java 复制代码
@Configuration
public class WebInterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
          ......
    }

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        configurer.setTaskExecutor(restMoExecutor());
    }

    /**
     * 线程池的配置参数需要另外更具实际情况配置
     **/
    public AsyncTaskExecutor restMoExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(25);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("rest-mo-executor");
        executor.initialize();
        return executor;
    }
}

源码分析


在实现接口异步功能的时候,通过 spring 的源码可以看到,其实是在处理返回值时对返回的类型做了判断,例如上方边表中提的 Callable、WebAsyncTask 等都有对应的 HandlerMethodReturnValueHandler 来处理;

我们进入到 startCallableProcessing 中可以看到提交了一个任务给线程池来执行。

相关推荐
un_fired几秒前
【Spring AI】基于专属知识库的RAG智能问答小程序开发——功能优化:用户鉴权
java·人工智能·spring
martian6652 分钟前
Java并发编程从入门到实战:同步、异步、多线程核心原理全解析
java·开发语言
计算机学姐11 分钟前
基于SpringBoot的电影售票系统
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
半升酒13 分钟前
Spring MVC
java·spring
M1A117 分钟前
走进Java异步编程的世界:开启高效编程之旅
java·后端
机智的人猿泰山35 分钟前
java 线程创建Executors 和 ThreadPoolExecutor 和 CompletableFuture 三者 区别
java·开发语言
努力的搬砖人.1 小时前
Tomcat相关的面试题
java·经验分享·后端·面试·tomcat
创码小奇客1 小时前
拿捏!Java 实现关系从青铜到王者攻略
java·spring boot·spring
还是鼠鼠1 小时前
Node.js 模块加载机制--详解
java·开发语言·前端·vscode·前端框架·npm·node.js
躲在云朵里`1 小时前
Spring MVC核心技术:从请求映射到异常处理
java·spring·mvc