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

简介: 背景HTTP 作为一种无状态的协议采用的是请求-应答的模式,每当客户端发起的请求到达服务器,Servlet 容器通常会为每个请求使用一个线程来处理。为了避免线程创建和销毁的资源消耗,一般会采用线程池,而线程池中的线程数量是有限的,当线程池中的线程被全部使用,客户端只能等待有空闲线程处理请求。

背景


HTTP 作为一种无状态的协议采用的是请求-应答的模式,每当客户端发起的请求到达服务器,Servlet 容器通常会为每个请求使用一个线程来处理。为了避免线程创建和销毁的资源消耗,一般会采用线程池,而线程池中的线程数量是有限的,当线程池中的线程被全部使用,客户端只能等待有空闲线程处理请求。


18.png

实际场景中,部分线程可能因为等待数据库查询结果或远程 Web 资源被阻塞,如果阻塞时间过长,线程池中的线程很快就被耗尽,从而导致无法处理其他请求。


Servlet 异步处理


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


Java EE 自 Servlet 3.0 开始对 Servlet 和 Filter 提供了异步支持,如果 Servlet 和 Filter 在处理请求时可能会发生阻塞,可以将阻塞请求线程的操作分配到异步线程,然后将处理请求的线程归还到 Servlet 容器中的线程池,而不产生响应,当异步线程中的操作完成,异步线程可以直接产生响应或将请求重新分派到容器中的 Servlet 处理。


19.png


如果你已经对 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 还列出了进行异步操作的常用步骤。


先使用 ServletRequest#startAsync(ServletRequest, ServletResponse) 开启异步。

开启异步后使用新线程进行异步处理,执行耗时操作。

新线程耗时操作完成后可以使用取到的资源信息发起响应。

最后调用第一步开启异步支持返回的异步上下文 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 可以使用原始的方式输出响应,Spring 内部将这个类适配为 Callable,在异步处理的时候回调这个接口然后输出响应。


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


ResponseEntity 相比 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 方法返回类型的异同,上述中的几种类型的适配关系可以如下图所示。


20.png


图中下面的类型可以适配到上面的类型,最终由 WebAsyncManager 使用来开启异步处理。

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


image.png


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);
  }
}


Spring 先调用 WebAsyncUtils.getAsyncManager 方法获取异步管理器 WebAsyncManager,WebAsyncManager 是异步处理的核心类,WebAsyncManager 获取之后会 Spring 会将实例存储到 request 的属性中。代码如下。


public abstract class WebAsyncUtils {
  public static WebAsyncManager getAsyncManager(WebRequest webRequest) {
    int scope = RequestAttributes.SCOPE_REQUEST;
    WebAsyncManager asyncManager = null;
    Object asyncManagerAttr = webRequest.getAttribute(WEB_ASYNC_MANAGER_ATTRIBUTE, scope);
    if (asyncManagerAttr instanceof WebAsyncManager) {
      asyncManager = (WebAsyncManager) asyncManagerAttr;
    }
    if (asyncManager == null) {
      asyncManager = new WebAsyncManager();
      // 将实例存储至 request 属性
      webRequest.setAttribute(WEB_ASYNC_MANAGER_ATTRIBUTE, asyncManager, scope);
    }
    return asyncManager;
  }
}


然后 Spring 调用 WebAsyncManager#startCallableProcessing(Callable, Object...) 开始异步处理,包括设置回调、开启异步处理、执行异步任务等等,这里将用到 Servlet 原生的 API,由于代码较多,不再展示。执行异步任务后 Spring 会调用 AsyncContext#dispatch() 将请求重新派发到当前 controller。


当请求转发到当前 controller 时,RequestMappingHandlerAdapter 会再次执行 controller 方法,此时从 request 属性中取出 WebAsyncManager,发现已经产生异步处理的结果,然后对表示 controller 方法的 ServletInvocableHandlerMethod 加以包装,使其直接返回异步处理结果,后面和正常流程一样,最终将结果输出到客户端。这块代码可参考 RequestMappingHandlerAdapter#invokeHandlerMethod,不再具体展示。


目录
相关文章
|
30天前
|
前端开发 Java 微服务
《深入理解Spring》:Spring、Spring MVC与Spring Boot的深度解析
Spring Framework是Java生态的基石,提供IoC、AOP等核心功能;Spring MVC基于其构建,实现Web层MVC架构;Spring Boot则通过自动配置和内嵌服务器,极大简化了开发与部署。三者层层演进,Spring Boot并非替代,而是对前者的高效封装与增强,适用于微服务与快速开发,而深入理解Spring Framework有助于更好驾驭整体技术栈。
|
8月前
|
前端开发 Java 测试技术
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
本文介绍了 `@RequestParam` 注解的使用方法及其与 `@PathVariable` 的区别。`@RequestParam` 用于从请求中获取参数值(如 GET 请求的 URL 参数或 POST 请求的表单数据),而 `@PathVariable` 用于从 URL 模板中提取参数。文章通过示例代码详细说明了 `@RequestParam` 的常用属性,如 `required` 和 `defaultValue`,并展示了如何用实体类封装大量表单参数以简化处理流程。最后,结合 Postman 测试工具验证了接口的功能。
454 0
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
|
8月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestBody
`@RequestBody` 是 Spring 框架中的注解,用于将 HTTP 请求体中的 JSON 数据自动映射为 Java 对象。例如,前端通过 POST 请求发送包含 `username` 和 `password` 的 JSON 数据,后端可通过带有 `@RequestBody` 注解的方法参数接收并处理。此注解适用于传递复杂对象的场景,简化了数据解析过程。与表单提交不同,它主要用于接收 JSON 格式的实体数据。
699 0
|
8月前
|
前端开发 Java 微服务
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@PathVariable
`@PathVariable` 是 Spring Boot 中用于从 URL 中提取参数的注解,支持 RESTful 风格接口开发。例如,通过 `@GetMapping(&quot;/user/{id}&quot;)` 可以将 URL 中的 `{id}` 参数自动映射到方法参数中。若参数名不一致,可通过 `@PathVariable(&quot;自定义名&quot;)` 指定绑定关系。此外,还支持多参数占位符,如 `/user/{id}/{name}`,分别映射到方法中的多个参数。运行项目后,访问指定 URL 即可验证参数是否正确接收。
449 0
|
8月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestMapping
@RequestMapping 是 Spring MVC 中用于请求地址映射的注解,可作用于类或方法上。类级别定义控制器父路径,方法级别进一步指定处理逻辑。常用属性包括 value(请求地址)、method(请求类型,如 GET/POST 等,默认 GET)和 produces(返回内容类型)。例如:`@RequestMapping(value = &quot;/test&quot;, produces = &quot;application/json; charset=UTF-8&quot;)`。此外,针对不同请求方式还有简化注解,如 @GetMapping、@PostMapping 等。
398 0
|
8月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RestController
本文主要介绍 Spring Boot 中 MVC 开发常用的几个注解及其使用方式,包括 `@RestController`、`@RequestMapping`、`@PathVariable`、`@RequestParam` 和 `@RequestBody`。其中重点讲解了 `@RestController` 注解的构成与特点:它是 `@Controller` 和 `@ResponseBody` 的结合体,适用于返回 JSON 数据的场景。文章还指出,在需要模板渲染(如 Thymeleaf)而非前后端分离的情况下,应使用 `@Controller` 而非 `@RestController`
310 0
|
4月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
308 0
|
4月前
|
SQL Java 数据库连接
Spring、SpringMVC 与 MyBatis 核心知识点解析
我梳理的这些内容,涵盖了 Spring、SpringMVC 和 MyBatis 的核心知识点。 在 Spring 中,我了解到 IOC 是控制反转,把对象控制权交容器;DI 是依赖注入,有三种实现方式。Bean 有五种作用域,单例 bean 的线程安全问题及自动装配方式也清晰了。事务基于数据库和 AOP,有失效场景和七种传播行为。AOP 是面向切面编程,动态代理有 JDK 和 CGLIB 两种。 SpringMVC 的 11 步执行流程我烂熟于心,还有那些常用注解的用法。 MyBatis 里,#{} 和 ${} 的区别很关键,获取主键、处理字段与属性名不匹配的方法也掌握了。多表查询、动态
147 0
|
4月前
|
JSON 前端开发 Java
第05课:Spring Boot中的MVC支持
第05课:Spring Boot中的MVC支持
247 0
|
10月前
|
SQL Java 数据库连接
对Spring、SpringMVC、MyBatis框架的介绍与解释
Spring 框架提供了全面的基础设施支持,Spring MVC 专注于 Web 层的开发,而 MyBatis 则是一个高效的持久层框架。这三个框架结合使用,可以显著提升 Java 企业级应用的开发效率和质量。通过理解它们的核心特性和使用方法,开发者可以更好地构建和维护复杂的应用程序。
508 29