【Spring MVC 系列】接口性能优化,还可以试试异步处理

2023年2月21日09:55:55

背景

HTTP 作为一种无状态的协议采用的是请求-应答的模式,每当客户端发起的请求到达服务器,Servlet 容器通常会为每个请求使用一个线程来处理。为了避免线程创建和销毁的资源消耗,一般会采用线程池,而线程池中的线程数量是有限的,当线程池中的线程被全部使用,客户端只能等待有空闲线程处理请求。
【Spring MVC 系列】接口性能优化,还可以试试异步处理
实际场景中,部分线程可能因为等待数据库查询结果或远程 Web 资源被阻塞,如果阻塞时间过长,线程池中的线程很快就被耗尽,从而导致无法处理其他请求。

Servlet 异步处理

为了提高系统的吞吐量,我们需要尽量使处理请求的线程处于非空闲状态。如果能够将那些长时间阻塞的线程利用起来处理新请求,由其他线程等资源满足时再继续处理前面的请求,这样对吞吐量的提升就会有很大的帮助。

Java EE 自 Servlet 3.0 开始对 Servlet 和 Filter 提供了异步支持,如果 Servlet 和 Filter 在处理请求时可能会发生阻塞,可以将阻塞请求线程的操作分配到异步线程,然后将处理请求的线程归还到 Servlet 容器中的线程池,而不产生响应,当异步线程中的操作完成,异步线程可以直接产生响应或将请求重新分派到容器中的 Servlet 处理。
【Spring MVC 系列】接口性能优化,还可以试试异步处理如果你已经对 Servlet 异步处理有所熟悉,可跳过下面的部分直接看 Spring MVC 异步处理。

Servlet 异步处理实战

先通过一个案例了解如何使用 Servlet 中的异步处理。

默认情况下 Servlet 和 Filter 都不支持异步,需要在部署描述符或注解中开启异步支持。

部署描述符开启异步支持示例如下。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
          http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    
    <servlet>
        <servlet-name>asyncA</servlet-name>
        <servlet-class>com.zzuhkp.mvc.AsyncServlet</servlet-class>
        <!--支持异步处理-->
        <async-supported>true</async-supported>
    </servlet>
    <servlet-mapping>
        <servlet-name>asyncA</servlet-name>
        <url-pattern>/async/a</url-pattern>
    </servlet-mapping>

    <filter>
        <filter-name>asyncFilter</filter-name>
        <filter-class>com.zzuhkp.mvc.AsyncFilter</filter-class>
        <!--支持异步处理-->
        <async-supported>true</async-supported>
    </filter>
    <filter-mapping>
        <filter-name>asyncFilter</filter-name>
        <servlet-name>asyncA</servlet-name>
    </filter-mapping>
</web-app>

部署描述符开启异步支持的重点是设置 servletfilter 标签下的 async-supported 值为 true。

注解开启异步支持的示例如下。

@WebFilter(value = "/async/a", asyncSupported = true)
public class AsyncFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(request, response);
    }
}

@WebServlet(urlPatterns = "/async/a", asyncSupported = true)
public class AsyncServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1. 开启异步处理
        AsyncContext asyncContext = req.startAsync(req, resp);
        Executors.newSingleThreadExecutor().submit(new Runnable() {
            @Override
            public void run() {
                try {
                    // 2. 使用新线程执行耗时操作
                    Thread.sleep(10000L);
                    // 3. 耗时操作完成后进行响应
                    asyncContext.getResponse().getWriter().write("this is a async servlet");
                    // 4. 通知容器异步操作完成
                    asyncContext.complete();
                } catch (IOException | InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

通过注解开启异步支持的重点是设置 @WebFilter@WebServlet 中的 asyncSupported 为 true。

注意上述 Servlet 还列出了进行异步操作的常用步骤。

  1. 先使用 ServletRequest#startAsync(ServletRequest, ServletResponse) 开启异步。
  2. 开启异步后使用新线程进行异步处理,执行耗时操作。
  3. 新线程耗时操作完成后可以使用取到的资源信息发起响应。
  4. 最后调用第一步开启异步支持返回的异步上下文 AsyncContext#complete 方法通知容器异步处理已经结束。

Servlet 异步处理详解

下面介绍异步处理常见的操作及对应 API,这些 API 将在 Spring MVC 异步处理中使用。


开启异步支持:

开启异步支持有两个方法,分别如下。

  • ServletRequest#startAsync(ServletRequest,ServletResponse)
  • ServletRequest#startAsync()

这两个参数都将返回一个异步处理的上下文 AsyncContext,不同的是如果使用了无参的 #startAsync 方法,AsyncContext 内部持有的 request、response 将是原始的,无论 Filter 是否对 request、response 进行了包装。


结束异步处理:

异步处理完成后有两种结束的方式。一种如上面的示例通知容器返回响应到客户端,另一种是通知容器使用其他 Servlet 继续处理请求。关联的方法有4个。

  • AsyncContext#complete
  • AsyncContext#dispatch()
  • AsyncContext#dispatch(String)
  • AsyncContext#dispatch(ServletContext, String)

AsyncContext 中的 #complete 用于在异步线程中通知容器向客户端发出响应,此后异步线程不可再产生响应。

AsyncContext 中的 #dispatch 用于通知容器重新派发请求。无参数的重载方法重新派发请求到当前请求路径,有参数的重载方法可以指定派发请求的路径。


派发类型判断:

由于异步处理后可以重新派发请求到当前 URL,因此需要判断派发类型,知道当前请求是从哪里产生的,从而使用不同处理逻辑,这可以通过 ServletRequest#getDispatcherType 方法来实现,这个方法返回的是一个 DispatcherType 枚举类型,每个枚举值的含义如下。

public enum DispatcherType {
    // request.getRequestDispatcher("/path").forward(request,response) 产生的请求
    FORWARD,
    // request.getRequestDispatcher("/path").include(request,response) 产生的请求
    INCLUDE,
    // 客户端正常发起请求
    REQUEST,
    // 异步处理 AsyncContext#dispatch 分派的请求
    ASYNC,
    // Servlet 产生错误,转发请求到错误页面
    ERROR
}

异步处理监听:

异步处理开始和结束之间,容器还会产生一些事件,可以通过 AsyncContext#addListener(AsyncListener) 方法添加对异步事件的监听,具体可以监听的事件如下。

public interface AsyncListener extends EventListener {
    // 异步处理完成
    public void onComplete(AsyncEvent event) throws IOException;
    // 异步处理超时
    public void onTimeout(AsyncEvent event) throws IOException;
    // 异步处理发生异常
    public void onError(AsyncEvent event) throws IOException;
    // ServletRequest#startAsync 重新开启异步
    public void onStartAsync(AsyncEvent event) throws IOException;     
}

异步处理默认的超时时间是 30 秒,可以通过 AsyncContext#setTimeout 设置超时时间,以设置时间重新计算。

Spring MVC 异步处理

Spring MVC 结合自身特性,对 Servlet 中的异步处理进行了封装,使异步处理更为简便。

快速体验 Spring MVC 异步处理

Spring MVC 手动配置 DispatcherServlet 需要指定 async-supported 为 true,Spring Boot 环境下已经默认开启了异步处理的支持。

在 Spring MVC 中使用异步处理最简单的方式是在 controller 方法中直接返回 Callable 类型,示例代码如下。

@RestController
public class AsyncController {

    @GetMapping("/test")
    public Callable<String> test() {
        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "this is a test";
            }
        };
        return callable;
    }
}

controller 方法返回 Callable 类型之后,Spring 会自动使用异步线程池调用 Callable#call 方法,然后对 #call 方法返回值重新解析,解析方式和普通的 controller 方法一致,上述示例代码将向浏览器输出一段文字。

Spring MVC 异步处理常用的两种方式

Callable

Callable 作为 controller 方法返回值是最常用的一种方式,这种方式会使用 Spring 默认的线程池进行异步处理。具体可以参见上面的示例。

DeferredResult

如果需要指定异步处理的线程池,将 DeferredResult 作为 controller 方法的返回值是更好的选择,DeferredResult 不仅可以手动指定线程池,还可以配置异步处理的回调,如超时、完成、错误。示例代码如下。

@RestController
public class AsyncController {

    @GetMapping("/test")
    public DeferredResult<String> test() {
        DeferredResult<String> deferredResult = new DeferredResult<>();
        Executors.newSingleThreadExecutor().submit(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                // 模拟耗时的操作
                Thread.sleep(5000L);
                // 设置异步处理结果
                deferredResult.setResult("this is a test");
            }
        });
        // 设置异步处理回调
        deferredResult.onTimeout(() -> System.out.println("异步处理超时"));
        deferredResult.onCompletion(() -> System.out.println("异步处理完成"));
        deferredResult.onError((throwable) -> System.out.println("异步处理错误:" + throwable.getMessage()));

        return deferredResult;
    }
}

上述代码将 DeferredResult 作为 controller 返回值,然后在线程池中手动设置了返回的结果,相对来说更为灵活。

Spring MVC 异步处理的其他方式

除了上述 Callable 和 DeferredResult 两种类型作为 controller 方法返回值,还有其他几种使用相对没那么频繁的类型可以作为 controller 方法的返回值类型,这几种类型与 Callable 或 DeferredResult 相互适配。

StreamingResponseBody、ResponseEntity<StreamingResponseBody>

StreamingResponseBody 可以使用原始的方式输出响应,Spring 内部将这个类适配为 Callable,在异步处理的时候回调这个接口然后输出响应。

ResponseEntity<StreamingResponseBody> 与 StreamingResponseBody 在 Spring 内部处理处理方式相似,Spring 会先根据 ResponseEntity 设置 HTTP 响应码、响应头,然后解析出 StreamingResponseBody 处理。

StreamingResponseBody 示例代码如下。

@RestController
public class AsyncController {

    @GetMapping("/test")
    public StreamingResponseBody test() {
        StreamingResponseBody body = new StreamingResponseBody() {
            @Override
            public void writeTo(OutputStream outputStream) throws IOException {
                BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
                writer.write("this is a test");
            }
        };
        return body;
    }
}

WebAsyncTask

WebAsyncTask 是 Callable 最底层的实现,Callable 最终将适配为 WebAsyncTask,这个类和 DeferredResult 功能类似,可以指定异步执行线程池、异步执行回调,由于底层使用了 Callable ,因此不能手动指定何时产生响应。示例代码如下。

@RestController
public class AsyncController {

    @GetMapping("/test")
    public WebAsyncTask<String> test() {
        // 设置超时时间、线程池、异步任务
        WebAsyncTask<String> task = new WebAsyncTask<>(5000L, new SimpleAsyncTaskExecutor(), new Callable<String>() {
            @Override
            public String call() throws Exception {
                // 模拟耗时的操作
                Thread.sleep(5000L);
                // 返回异步处理结果
                return "this ia a test";
            }
        });

        // 设置异步处理回调
        task.onTimeout(() -> "异步处理超时");
        task.onCompletion(() -> System.out.println("异步处理完成"));
        task.onError(() -> "异步处理错误");

        return task;
    }
}

ListenableFuture

ListenableFuture 是 Spring 对 Future 扩展提出的接口,可以在任务执行成功或者失败时回调给定的接口方法。在异步处理中,如果 controller 方法返回这个类型,Spring 会将其适配为 DeferredResult,异步任务执行成功后设置异步处理的结果。从功能上来说弱于 DeferredResult,不能设置超时时间及超时回调。 示例代码如下。

@RestController
public class AsyncController {

    @GetMapping("/test")
    public ListenableFuture<String> test() {
        ListenableFutureTask<String> task = new ListenableFutureTask<>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                // 模拟耗时的操作
                Thread.sleep(5000L);
                // 返回异步处理结果
                return "this is a test";
            }
        });
        task.addCallback(new ListenableFutureCallback<String>() {
            @Override
            public void onFailure(Throwable ex) {
                System.out.println("异步任务异常:" + ex.getMessage());
            }

            @Override
            public void onSuccess(String result) {
                System.out.println("异步任务执行完成");
            }
        });
        // 提交异步任务
        Executors.newSingleThreadExecutor().submit(task);

        return task;
    }
}

CompletionStage

CompletionStage 是 JDK 1.8 提供的表示异步执行的其中一个阶段,可以在当前阶段完成后进入下一个阶段,典型的实现是 CompletableFuture。

使用 CompletableFuture 作为 controller 作为返回值,Spring 会将其适配为 DeferredResult,在当前阶段完成后设置异步处理的结果,从功能上来说强于 Callable,可以设置线程池,但不能设置回调和设置超时时间。示例代码如下。

@RestController
public class AsyncController {

    @GetMapping("/test")
    public CompletionStage<String> test() {
        CompletionStage<String> future = CompletableFuture.supplyAsync(new Supplier<String>() {
            @Override
            public String get() {
                return "this is a test";
            }
        }, Executors.newSingleThreadExecutor());

        return future;
    }
}

ResponseBodyEmitter、ResponseEntity<ResponseBodyEmitter>

ResponseBodyEmitter 类型的作用类似于 Servlet 异步处理原生的 API,支持用户多次发出响应,这个类型作为 controller 方法返回类型后,Spring 同样会将这个类型适配为 DeferredResult。这个类型支持异步处理回调、设置超时时间,指定线程池等。

ResponseEntity<ResponseBodyEmitter> 相比 ResponseBodyEmitter 多了设置响应码,响应头的能力。

ResponseBodyEmitter 示例代码如下。

@RestController
public class AsyncController {

    @GetMapping("/test")
    public ResponseBodyEmitter test() {

        ResponseBodyEmitter emitter = new ResponseBodyEmitter(5000L);

        // 异步线程池中执行耗时任务
        Executors.newSingleThreadExecutor().submit(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                // 设置异步处理回调
                emitter.onCompletion(() -> System.out.println("异步处理完成"));
                emitter.onTimeout(() -> System.out.println("异步处理"));
                emitter.onError((throwable) -> System.out.println("异步处理异常:" + throwable.getMessage()));

                // 模拟耗时操作
                Thread.sleep(3000L);

                // 发送响应
                emitter.send("this is ");
                emitter.send("a test");


                // 通知容器异步处理完成
                emitter.complete();
            }
        });

        return emitter;
    }
}

需要注意的是由于 Spring 需要等待 controller 方法返回后才能真正设置回调,因此如果异步任务如果在 controller 方法返回前就已经执行结束,回调将无法生效。

Spring MVC 异步处理方式总结

这里总结几种 controller 方法返回类型的异同,上述中的几种类型的适配关系可以如下图所示。
【Spring MVC 系列】接口性能优化,还可以试试异步处理图中下面的类型可以适配到上面的类型,最终由 WebAsyncManager 使用来开启异步处理。

各类型功能异同如下表,可根据需求选择合适的类型进行异步处理。

类型 是否支持设置线程池 是否需要手动开启异步线程 是否支持超时设置 是否支持异步回调 是否支持多次输出响应
Callable
DeferredResult
StreamingResponseBody
WebAsyncTask
ListenableFuture 仅支持成功失败回调
CompletionStage
ResponseBodyEmitter

Spring 异步处理流程

到了这里文章的内容已经很长了,但为了文章的完整性还是简单介绍下 Spring 在内部如何实现异步处理的吧。

首先 Spring 将按照正常的流程执行 controller 方法,方法返回后 Spring 处理和异步有关的几个类型值,然后开始异步处理。以 Callable 类型为例,处理这个返回值类型的代码如下。

public class CallableMethodReturnValueHandler implements HandlerMethodReturnValueHandler {

@Override
public boolean supportsReturnType(MethodParameter returnType) {
return Callable.class.isAssignableFrom(returnType.getParameterType());
}

@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

if (returnValue == null) {
mavContainer.setRequestHandled(true);
return;
}

Callable<?> callable = (Callable<?>) returnValue;
// 开启异步处理
WebAsyncUtils.getAsyncManager(webRequest).startCallableProcessing(callable, mavContainer);

  • 作者:大鹏cool
  • 原文链接:https://zzuhkp.blog.csdn.net/article/details/121418820
    更新时间:2023年2月21日09:55:55 ,共 10285 字。