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 中可以看到提交了一个任务给线程池来执行。

相关推荐
cfm_291440 分钟前
SpringBoot启动原理
java·spring boot·spring
漫随流水1 小时前
Java——springboot web案例
java·开发语言·spring boot
从此以后自律1 小时前
Java Object 类常用方法全讲解
java·开发语言
念何架构之路2 小时前
Docker 容器状态机 学习
java·学习·docker
你有我备注吗2 小时前
SQL学习之查询
java·sql·学习
xixi09243 小时前
JMeter5.6.3基础使用教程1
java·开发语言
带刺的坐椅3 小时前
用 Solon ReActAgent 落地发票识别与智能报销
java·llm·solon·react-agent
罗超驿3 小时前
15.Java异常处理:从束手无策到从容应对
java·开发语言
Co_Hui3 小时前
Java 基础
java·开发语言
半夜修仙3 小时前
RabbitMQ的推模式和拉模式
java·分布式·中间件·rabbitmq·github·java-rabbitmq