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();
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();
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(翻譯)”