[springboot] 异步开发之异步请求

何为异步请求

Servlet 3.0之前,Servlet采用Thread-Per-Request的方式处理请求,即每一次Http请求都由某一个线程从头到尾负责处理。如果一个请求需要进行IO操作,比如访问数据库、调用第三方服务接口等,那么其所对应的线程将同步地等待IO操作完成, 而IO操作是非常慢的,所以此时的线程并不能及时地释放回线程池以供后续使用,在并发量越来越大的情况下,这将带来严重的性能问题。其请求流程大致为:

.

而在Servlet3.0发布后,提供了一个新特性:异步处理请求。可以先释放容器分配给请求的线程与相关资源,减轻系统负担,释放了容器所分配线程的请求,其响应将被延后,可以在耗时处理完成(例如长时间的运算)时再对客户端进行响应。其请求流程为:

.

Servlet 3.0后,我们可以从HttpServletRequest对象中获得一个AsyncContext对象,该对象构成了异步处理的上下文,RequestResponse对象都可从中获取。AsyncContext可以从当前线程传给另外的线程,并在新的线程中完成对请求的处理并返回结果给客户端,初始线程便可以还回给容器线程池以处理更多的请求。如此,通过将请求从一个线程传给另一个线程处理的过程便构成了Servlet 3.0中的异步处理。

多说几句:

随着Spring5发布,提供了一个响应式Web框架:Spring WebFlux。之后可能就不需要Servlet容器的支持了。以下是先后对比图:

.

左侧是传统的基于ServletSpring Web MVC框架,右侧是5.0版本新引入的基于Reactive StreamsSpring WebFlux框架,从上到下依次是Router FunctionsWebFluxReactive Streams三个新组件。对于其发展前景还是拭目以待吧,有时间也该去了解下Spring5了。

原生异步请求API说明

在编写实际代码之前,我们来了解下一些关于异步请求的api的调用说明。

  • 获取AsyncContext:根据HttpServletRequest对象获取。
AsyncContext asyncContext = request.startAsync();
  • 设置监听器:可设置其开始、完成、异常、超时等事件的回调处理

其监听器的接口代码:

public interface AsyncListener extends EventListener {
    void onComplete(AsyncEvent event) throws IOException;
    void onTimeout(AsyncEvent event) throws IOException;
    void onError(AsyncEvent event) throws IOException;
    void onStartAsync(AsyncEvent event) throws IOException;
}

说明:

  1. onStartAsync:异步线程开始时调用
  2. onError:异步线程出错时调用
  3. onTimeout:异步线程执行超时调用
  4. onComplete:异步执行完毕时调用

一般上,我们在超时或者异常时,会返回给前端相应的提示,比如说超时了,请再次请求等等,根据各业务进行自定义返回。同时,在异步调用完成时,一般需要执行一些清理工作或者其他相关操作。

需要注意的是只有在调用request.startAsync前将监听器添加到AsyncContext,监听器的onStartAsync方法才会起作用,而调用startAsyncAsyncContext还不存在,所以第一次调用startAsync是不会被监听器中的onStartAsync方法捕获的,只有在超时后又重新开始的情况下onStartAsync方法才会起作用。

  • 设置超时:通过setTimeout方法设置,单位:毫秒。

一定要设置超时时间,不能无限等待下去,不然和正常的请求就一样了。

Servlet方式实现异步请求

前面已经提到,可通过HttpServletRequest对象中获得一个AsyncContext对象,该对象构成了异步处理的上下文。所以,我们来实际操作下。

0.编写一个简单控制层

package com.bo.springboot.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Description: 使用servlet方式进行异步请求
 * @Author: wangboc
 * @Date: 2019/1/7 16:44
 */
@Slf4j
@RestController
@RequestMapping("/servlet")
public class ServletController {

	@RequestMapping("/sync")
	public void todo(HttpServletRequest request, HttpServletResponse response){
		try {
			// 休眠3秒
			Thread.sleep(3000);
			response.getWriter().println("This is a normal sync response.");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@RequestMapping("/async")
	public void todoAsync(HttpServletRequest request, HttpServletResponse response){
		AsyncContext asyncContext = request.startAsync();
		// 设置异步监听器
		asyncContext.addListener(new AsyncListener() {

			@Override
			public void onComplete(AsyncEvent asyncEvent) throws IOException {
				// 这里可以做一些清理资源的操作
				log.info("Async execution completed.");
			}

			@Override
			public void onTimeout(AsyncEvent asyncEvent) throws IOException {
				// 做一些超时后的相关操作
				log.info("Async execution timeout.");
			}

			@Override
			public void onError(AsyncEvent asyncEvent) throws IOException {
				log.info("Async execution error.");
			}

			@Override
			public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
				log.info("Async execution started.");
			}
		});
		// 设置10秒超时
		asyncContext.setTimeout(10000);
		// 异步处理请求
		asyncContext.start(new Runnable() {
			@Override
			public void run() {
				try {
					// 休眠3秒
					Thread.sleep(3000);
					log.info("异步线程:" + Thread.currentThread().getName());
					asyncContext.getResponse().setCharacterEncoding("utf-8");
					asyncContext.getResponse().setContentType("text/html;charset=UTF-8");
					asyncContext.getResponse().getWriter().println("This is a async response.");
					// 通知异步上下文请求已完成并真正将响应信息返回给客户端
					// 如果最后不调用该方法则异步线程将一直阻塞直到给定的超时后触发complete()回调
					// 其实可以利用此特性 把连接挂起 进行多条消息的推送
					asyncContext.complete();
				} catch (Exception e) {
					log.error("Async execution exception", e);
				}

			}
		});
		//也可以不使用asyncContext而使用线程池等进行异步调用
/*		new Thread(new Runnable() {
			@Override
			public void run() {
				// To Do ..
				// Finally
				asyncContext.complete();
			}
		}).start();*/
		// 此时request的线程连接已经被释放了
		log.info("请求线程:" + Thread.currentThread().getName());
	}

}

注意:异步请求时,可以利用ThreadPoolExecutor自定义个线程池。

1.启动下应用,查看控制台输出就可以获悉是否在同一个线程里面了。同时,可设置下等待时间,之后就会调用超时回调方法了,大家可自己试试。

2019-01-07 17:14:32.844  INFO 5172 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 4 ms
2019-01-07 17:14:32.864  INFO 5172 --- [nio-8080-exec-1] c.b.s.controller.ServletController       : 请求线程:http-nio-8080-exec-1
2019-01-07 17:14:35.864  INFO 5172 --- [nio-8080-exec-2] c.b.s.controller.ServletController       : 异步线程:http-nio-8080-exec-2
2019-01-07 17:14:35.872  INFO 5172 --- [nio-8080-exec-3] c.b.s.controller.ServletController       : Async execution completed.

使用过滤器时,需要加入asyncSupportedtrue配置,开启异步请求支持。

@WebServlet(urlPatterns = "/test", asyncSupported = true )  
public class AsyncServlet extends HttpServlet ...

题外话:其实我们可以利用在未执行asyncContext.complete()方法时请求未结束这特性,可以做个简单的文件上传进度条之类的功能。但注意请求是会超时的,需要设置超时的时间下。

Spring方式实现异步请求

Spring中,有多种方式实现异步请求,比如callableDeferredResult或者WebAsyncTask。每个的用法略有不同,可根据不同的业务场景选择不同的方式。以下主要介绍一些常用的用法。

Callable

使用很简单,直接返回的参数包裹一层callable即可。

@Slf4j
@RestController
@RequestMapping("/spring")
public class SpringController {

	@RequestMapping("/callable")
	public Callable<String> callable(){
		log.info("请求线程:" + Thread.currentThread().getName());
		return new Callable<String>() {
			@Override
			public String call() throws Exception {
				log.info("异步线程:" + Thread.currentThread().getName());
				Thread.sleep(3000);
				return "callable";
			}
		};
	}
}

控制台输出:

2019-01-08 13:17:04.273  INFO 9716 --- [nio-8080-exec-1] c.b.s.controller.SpringController        : 请求线程:http-nio-8080-exec-1
2019-01-08 13:17:04.279  INFO 9716 --- [         task-1] c.b.s.controller.SpringController        : 异步线程:task-1

超时、自定义线程设置

从控制台可以看见,异步响应的线程使用的是名为:task-1的线程。第一次再访问时,就是task-2了。若采用默认设置,会无限的创建新线程去处理异步请求,所以正常都需要配置一个线程池及超时时间。

编写一个配置类:CustomAsyncPool.java

@Configuration
public class CustomAsyncPool implements WebMvcConfigurer {

	public ThreadPoolTaskExecutor taskExecutor(){
		ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
		taskExecutor.setCorePoolSize(10);
		taskExecutor.setMaxPoolSize(20);
		taskExecutor.setQueueCapacity(500);
		taskExecutor.setKeepAliveSeconds(60);
		taskExecutor.setThreadNamePrefix("callable-");
		taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
		taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
		taskExecutor.setAwaitTerminationSeconds(60);
		taskExecutor.initialize();
		return taskExecutor;
	}

	@Override
	public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
		configurer.setDefaultTimeout(3*1000);
		configurer.setTaskExecutor(taskExecutor());
		configurer.registerCallableInterceptors(new CustomCallableProcessingInterceptor());
//		configurer.registerCallableInterceptors(new TimeoutCallableProcessingInterceptor());
	}

	public class CustomCallableProcessingInterceptor implements CallableProcessingInterceptor {
		@Override
		public <T> Object handleTimeout(NativeWebRequest request, Callable<T> task) throws Exception {
			HttpServletRequest httpRequest = request.getNativeRequest(HttpServletRequest.class);
			return new CustomAsyncRequestTimeoutException(httpRequest.getRequestURI());
		}
	}
}
自定义一个超时异常处理类:CustomAsyncRequestTimeoutException.java
public class CustomAsyncRequestTimeoutException extends RuntimeException {

	public CustomAsyncRequestTimeoutException(String url){
		super(url);
	}

	public CustomAsyncRequestTimeoutException(String url, Throwable cause){
		super(url, cause);
	}
}
同时,在统一异常处理加入对CustomAsyncRequestTimeoutException类的处理:
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

	@ExceptionHandler(CustomAsyncRequestTimeoutException.class)
	@ResponseBody
	public String asyncTimeout(HttpServletRequest request, Exception exp){
		log.error("Async request timed out: " + exp.getMessage());
		return "system busy!!";
	}
}

之后,再运行就可以看见使用了自定义的线程池了,超时模拟如下:

2019-01-08 15:45:53.299  INFO 8580 --- [nio-8080-exec-1] c.b.s.controller.SpringController        : 请求线程:http-nio-8080-exec-1
2019-01-08 15:45:53.303  INFO 8580 --- [     callable-1] c.b.s.controller.SpringController        : 异步线程:callable-1
2019-01-08 15:45:56.957 ERROR 8580 --- [nio-8080-exec-2] c.b.s.exception.GlobalExceptionHandler   : Async request timed out: /spring/callable

DeferredResult

相比于callableDeferredResult可以处理一些相对复杂一些的业务逻辑,最主要还是可以在另一个线程里面进行业务处理及返回,即可在两个完全不相干的线程间的通信。

    public static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
	
    @RequestMapping("/deferred-result")
	public DeferredResult<String> deferredResult(){
		log.info("请求线程:" + Thread.currentThread().getName());
		// 初始化时指定超时时间
		DeferredResult<String> result = new DeferredResult<>(3*1000L);
		// 处理超时任务采用委托机制
		result.onTimeout(new Runnable() {
			@Override
			public void run() {
				log.info("Async deferred result timed out");
				// 返回超时结果
				result.setErrorResult("task timeout");
			}
		});
		// setResult完毕之后,调用该方法
		result.onCompletion(new Runnable() {
			@Override
			public void run() {
				log.info("Async deferred result completed");
			}
		});
		// spring5.0新增 暂不知何时回调
		result.onError(exp -> {
			log.error("Async deferred result error", exp);
		});

		fixedThreadPool.execute(new Runnable() {
			@Override
			public void run() {
				log.info("异步线程:" + Thread.currentThread().getName());
				try {
					// 模拟IO处理
					Thread.sleep(1000);
					// 返回结果
					result.setResult("task finished");
				} catch (Exception e) {
					// 返回异常结果
					log.error("async task exception", e);
					result.setErrorResult("task error");
				}
			}
		});
		return result;
	}

控制台输出:

2019-01-08 17:25:13.240  INFO 8072 --- [nio-8080-exec-1] c.b.s.controller.SpringController        : 请求线程:http-nio-8080-exec-1
2019-01-08 17:25:13.242  INFO 8072 --- [pool-1-thread-1] c.b.s.controller.SpringController        : 异步线程:pool-1-thread-1
2019-01-08 17:25:14.300  INFO 8072 --- [nio-8080-exec-2] c.b.s.controller.SpringController        : Async deferred result completed

注意:返回结果时记得调用下setResult方法。另外,利用DeferredResult可实现一些长连接的功能,比如当某个操作是异步时,我们可以保存这个DeferredResult对象,当异步通知回来时,我们再找回这个DeferredResult对象,setResult返回结果,提高性能。

WebAsyncTask

使用方法与DeferedResult类似,只是WebAsyncTask是直接返回了。

	@RequestMapping("/webAsyncTask")
	public WebAsyncTask<String> webAsyncTask(){
		log.info("请求线程:" + Thread.currentThread().getName());
		// 初始化时指定超时时间
		WebAsyncTask<String> result = new WebAsyncTask<>(3 * 1000L, new Callable<String>() {
			@Override
			public String call() throws Exception {
				log.info("异步线程:" + Thread.currentThread().getName());
				try {
					Thread.sleep(1000);
					// int t = 1/0;
				} catch (Exception e) {
					log.error("task exception", e);
					return "task error";
				}
				return "task finished";
			}
		});
		// 任务超时后回调
		result.onTimeout(new Callable<String>() {
			@Override
			public String call() throws Exception {
				log.info("Web async task timed out");
				return "task timeout";
			}
		});
		// 超时后也会执行该回调
		result.onCompletion(new Runnable() {
			@Override
			public void run() {
				log.info("Web async task completed");
			}
		});
		// // spring5.0新增 暂不知如何回调
		result.onError(new Callable<String>() {
			@Override
			public String call() throws Exception {
				log.error("Web async task error");
				return "task error";
			}
		});

		return result;
	}

控制台输出:

2019-01-08 17:54:17.303  INFO 4116 --- [nio-8080-exec-1] c.b.s.controller.SpringController        : 请求线程:http-nio-8080-exec-1
2019-01-08 17:54:17.308  INFO 4116 --- [     callable-1] c.b.s.controller.SpringController        : 异步线程:callable-1
2019-01-08 17:54:18.360  INFO 4116 --- [nio-8080-exec-2] c.b.s.controller.SpringController        : Web async task completed

小结

本节主要是讲解了异步请求的使用及相关配置,如超时,异常等处理。设置异步请求时,不要忘记设置超时时间。异步请求只是提高了服务的吞吐量,提高单位时间内处理的请求数,并不会加快处理效率,这点需要注意。

参考资料

慕课手记:异步开发之异步请求

docs.spring.io#mvc-ann-async

  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Spring Boot中进行异步请求可以使用Spring的异步支持。以下是一种常见的实现方式: 1. 首先,在Spring Boot应用程序的配置类或主类上添加`@EnableAsync`注解,以启用异步支持。 ```java import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @EnableAsync @SpringBootApplication public class YourApplication { public static void main(String[] args) { SpringApplication.run(YourApplication.class, args); } } ``` 2. 在需要进行异步请求的方法上添加`@Async`注解,表明该方法是一个异步方法。 ```java import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @Service public class YourService { @Async public void doAsyncOperation() { // 异步执行的逻辑 } } ``` 3. 在需要调用异步方法的地方,通过依赖注入的方式获取到异步方法所在的类的实例,并调用相应的异步方法。 ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class YourController { @Autowired private YourService yourService; @GetMapping("/async") public String asyncRequest() { yourService.doAsyncOperation(); return "Async request submitted"; } } ``` 以上代码示例中,`YourService`类中的`doAsyncOperation`方法是一个异步方法,在`YourController`类中的`asyncRequest`方法中调用了该异步方法。在调用异步方法后,控制权会立即返回给调用方,而异步方法会在后台线程中执行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值