Spring MVC 的异步
首先提出的一个问题是:多个客户端请求访问同一个方法时,Spring 是如何处理的。
参考网上博客的解释,这里仅给出自己的简单理解:对于每一个客户端请求,Spring会分配单独的线程来执行对应的方法,Controller 默认是单例模式,方法执行是多线程模式。而 Spring 中可执行线程的数量是有限的,当很多请求同时到来,所有的线程都已被分配并正在处理请求时,剩下的线程就只能排队等待。
当某个 Controller 方法中还需要调用另一个后端业务服务器的方法,在等待后者返回结果时,请求处理线程会处于阻塞状态。当类似请求很多,所有的可执行线程都被分配执行类似方法并处于阻塞状态时,新来的请求就无法再被处理,这也就影响了服务器的吞吐量。(有关该情况的更好解释请参见另一篇博客)
为了更好地发挥服务器的全部性能,就需要使用异步,大概思路是请求处理线程调起另外的业务处理线程来执行耗时或阻塞的操作,而前者很快执行完 Controller 方法而不立即将响应返回给客户端,之后便可以处理其他新的请求;后者则在执行完耗时或等待操作生成响应结果后,通过某种方式使服务器将响应返回给客户端。
在 Spring MVC 3.2 及以上版本增加了对请求的异步处理,是在 Servlet3 的基础上进行封装的。Spring 中的异步模式主要有两种:DefferedResult 和 WebAsyncTask(Callable),有关他们两个的原理我目前理解的不深,主要参考他人博客,详见参考资料,在此不再赘述。
建立 Spring Boot 项目
创建一个 Maven 项目:File->New->Other,选择 Maven Project,注意勾选 Create a simple project (skip archetype selection):
在 pom.xml 文件中导入如下配置:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</pluginRepository>
</pluginRepositories>
创建启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
创建 Controller 类
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
@RequestMapping("/greeting")
public byte[] greeting(@RequestParam(value="name", defaultValue="World") String name) {
byte[] bs = "123456".getBytes();
return bs;
}
}
在 Application 文件中右键 Run as -> Java Application。当看到 “Tomcat started on port(s): 8080 (http)” 字样说明启动成功。
打开浏览器访问 http://localhost:8080/greeting,结果如下:
Spring Boot 热部署
当我们修改文件和创建文件时,都需要重新启动项目。这样频繁的操作很浪费时间,配置热部署可以让项目自动加载变化的文件,省去的手动操作。
在 pom.xml 文件中添加如下配置:
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<scope>true</scope>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 没有该配置,devtools 不生效 -->
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
这样每次修改文件后,Spring Boot 都能自动检测改变,重新加载文件而不需要我们手动重启项目。
DefferedResult 测试
修改 GreetingController 如下:
@RestController
public class GreetingController {
ExecutorService exec = Executors.newCachedThreadPool();
@RequestMapping("/greeting")
public DeferredResult<byte[]> greeting(@RequestParam(value="name", defaultValue="World") String name) {
DeferredResult<byte[]> deferredResult = new DeferredResult<>();
exec.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
//等待三秒,模拟耗时或阻塞操作
TimeUnit.MILLISECONDS.sleep(3000);
System.out.println("业务处理线程方法执行完毕时间 : "+
TimeUnit.NANOSECONDS.toSeconds(System.nanoTime())+"秒");
byte[] bs = "123456".getBytes();
deferredResult.setResult(bs);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
System.out.println("请求处理线程方法执行完毕时间 : "+
TimeUnit.NANOSECONDS.toSeconds(System.nanoTime())+"秒");
return deferredResult;
}
}
在浏览器访问 http://localhost:8080/greeting,会三秒之后才返回结果,同时控制台打印如下信息,证明请求处理线程不会阻塞,立即返回,业务处理线程阻塞三秒,之后执行 deferredResult.setResult(bs);
返回响应结果给客户端:
WebAsyncTask测试
修改 GreetingController 如下:
@RestController
public class GreetingController {
ExecutorService exec = Executors.newCachedThreadPool();
@RequestMapping("/greeting")
public WebAsyncTask<byte[]> greeting(@RequestParam(value="name", defaultValue="World") String name) {
Callable<byte[]> callable = new Callable<byte[]>() {
@Override
public byte[] call() throws Exception {
// TODO Auto-generated method stub
try {
//等待三秒,模拟耗时或阻塞操作
TimeUnit.MILLISECONDS.sleep(3000);
System.out.println("业务处理线程方法执行完毕时间 : "+
TimeUnit.NANOSECONDS.toSeconds(System.nanoTime())+"秒");
byte[] bs = "123456".getBytes();
return bs;
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
};
System.out.println("请求处理线程方法执行完毕时间 : "+
TimeUnit.NANOSECONDS.toSeconds(System.nanoTime())+"秒");
return new WebAsyncTask<byte[]>(callable);
}
}
可得到和 DefferedResult 相同的结果。
参考资料
1 https://www.cnblogs.com/moonlightL/p/7891803.html “Spring Boot 入门之基础篇(一)”
2 https://www.cnblogs.com/guogangj/p/5457959.html “高性能的关键:Spring MVC的异步模式”
3 https://www.jianshu.com/p/acd4bbd83314 “Spring MVC异步处理-DeferedResult使用”
4 https://www.cnblogs.com/aheizi/p/5659030.html “理解Callable 和 Spring DeferredResult(翻译)”