(給ImportNew加星標,提高Java技能)
編譯:ImportNew/唐尤華
dzone.com/articles/effective-advice-on-spring-async-final-part-1
在之前的文章中,我們討論了 Spring Async 概念以及如何把它用好。如果想要重溫之前的文章,請檢視下麵連結:
[1]:Spring Aysnc 最佳實踐(1):原理與限制
[2]:Spring Async 最佳實踐(2):ExceptionHandler
在這一篇中,我們將討論 Spring Async 如何在 Web 應用中工作。
很高興能和大家分享關於 Spring Async 和 `HttpRequest` 的使用經驗。在最近參與的專案中遇到了一件有趣的事情,相信我的經歷可以為你在將來節省一些寶貴的時間。
讓我試著描述一下當時的場景:
標的
需要把資料從 UI 傳給後端 Controller,接著 Controller 將執行一些操作,最終呼叫非同步郵件服務傳送郵件。
一位初級工程師編寫了這部分程式碼。下麵是我根據功能復現的程式碼,你能找出中間的問題嗎?
Controller
Controller 透過接收 HTTP Servelet 請求從 UI 收集資訊,接著執行一些操作,並將請求轉給非同步郵件服務。
```java
package com.example.demo;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetController {
@Autowired
private AsyncMailTrigger greeter;
@RequestMapping(value = "/greet", method = RequestMethod.GET)
public String greet(HttpServletRequest request) throws Exception {
String name = request.getParameter("name");
greeter.asyncGreet(request);
System.out.println(Thread.currentThread() + " Says Name is " + name);
System.out.println(Thread.currentThread().getName() + " Hashcode" + request.hashCode());
return name;
}
}
```
非同步郵件服務 `AsyncMailTrigger` 類加上了 `@Component` 註解,你也可以改成 `@Service`。其中包含了 `asyncGreet` 方法,接受 `HttpRequest` 輸入,從中獲取資訊併傳送郵件(簡單起見,這一部分被略過)。**註意:** 這裡有一條 `Thread.sleep()` 陳述句,稍後我會討論它的作用。
```java
package com.example.demo;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class AsyncMailTrigger {
@Async
public void asyncGreet(HttpServletRequest request) throws Exception {
System.out.println("Trigger mail in a New Thread :: " + Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() + " greets before sleep" + request.getParameter("name"));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " greets" + request.getParameter("name"));
System.out.println(Thread.currentThread().getName() + " Hashcode" + request.hashCode());
}
}
```
下麵是 main class:
```java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class SpringAsyncWebApplication {
public static void main(String[] args) {
SpringApplication.run(SpringAsyncWebApplication.class, args);
}
}
```
執行程式,輸出結果如下:
```
Thread[http-nio-8080-exec-1,5,main] Says Name is Shamik
http-nio-8080-exec-1 Hashcode 821691136
Trigger mail in a New Thread:: task-1
task-1 greets before sleep Shamik
task-1 greets null task-1 Hashcode 821691136
```
仔細檢視輸出會發現:在 `sleep()` 呼叫前 `request` 資訊正確,但呼叫 `sleep()` 後 `request` 資訊就神奇地消失了。很奇怪,對吧?但從 hashcode 可以證明它們是同一個 request 物件。
到底發生了什麼?`request` 資訊消失的原因是什麼?我們的初級工程師遇到了這樣的情況,收件人資訊、收件人的姓名從 `request` 中消失了,郵件也沒有傳送成功。
讓我們仔細調查這個問題
`request` 出現問題很正常。要理解這個問題,首先要瞭解 `request` 的生命週期。
在呼叫 Servlet 方法前,Servlet 容器會建立 `request` 物件。Spring 透過 Dispatcher Servlet 傳遞 `request` ,根據對映找到對應的 Controller 並呼叫相應的方法。當 `request` 得到響應時,Servlet 容器要麼刪除要麼重置 `request` 物件的狀態(完全取決於容器的實現,這裡實際上維護了一個 request pool)。然而,這裡不打算深入探討關於容器如何維護 `request` 物件這個話題。
“但是請記住:” 一旦`request` 得到響應時,容器就會刪除或者重置 `request` 物件。
現在,讓我們思考 Spring Async 程式碼。Async 的工作是從執行緒池中分配一個執行緒讓它執行任務。上面的例子中,我們把 `request` 物件傳遞給非同步執行緒,併在 `asyncGreet` 方法中,試圖直接從 `request` 物件提取資訊。
然而,由於這裡的操作是非同步的,主執行緒(即 Controller 部分)不會等待執行緒完成。它會直接執行 `print` 陳述句,傳回 `response`,並掃清 `request` 物件的狀態。
這裡的問題在於,我們直接把 `request` 物件傳給了非同步執行緒。為了證明上面的推斷,這裡加上了一條 `sleep` 陳述句。當主執行緒在 `sleep` 結束前傳回 `response`,就能復現之前問題中的現象。
從這個實驗中可以學到什麼?
使用 Async 時,**不要**直接傳 `request` 物件或任何與 `Request/Response` 相關的物件。因為永遠不知道什麼時候會提交 `response` 並掃清狀態。如果這樣做,可能會遇到偶發性錯誤。
有什麼解決辦法?
如果需要傳遞 `request` 中的資訊,可以建立一個 `value` 物件。為物件設定資訊後,把 `value` 物件傳給 Spring Async。透過這種方式,可以解決上面的問題:
RequestVO 物件
```java
package com.example.demo;
public class RequestVO {
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
```
非同步郵件服務
```java
package com.example.demo;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class AsyncMailTrigger {
@Async
public void asyncGreet(RequestVO reqVO) throws Exception {
System.out.println("Trigger mail in a New Thread :: " + Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() + " greets before sleep" + reqVO.getName());
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " greets" + reqVO.getName());
}
}
```
Greet Controller
```java
package com.example.demo;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetController {
@Autowired
private AsyncMailTrigger greeter;
@RequestMapping(value = "/greet", method = RequestMethod.GET)
public String greet(HttpServletRequest request) throws Exception {
String name = request.getParameter("name");
RequestVO vo = new RequestVO();
vo.setName(name);
//greeter.asyncGreet(request);
greeter.asyncGreet(vo);
System.out.println(Thread.currentThread() + " Says Name is " + name);
System.out.println(Thread.currentThread().getName() + " Hashcode" + request.hashCode());
return name;
}
}
```
輸出
```
Thread[http-nio-8080-exec-1,5,main] Says Name is Shamik
http-nio-8080-exec-1 Hashcode 1669579896
Trigger mail in a New Thread:: task-1
task-1 greets before sleep Shamik
task-1 greets Shamik
```
總結
希望你喜歡這篇文章。如果有任何問題,歡迎在文後留言。
朋友會在“發現-看一看”看到你“在看”的內容