歡迎光臨
每天分享高質量文章

Spring Async 最佳實踐(3):完結篇

(給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
```

 

總結 

 

希望你喜歡這篇文章。如果有任何問題,歡迎在文後留言。

贊(0)

分享創造快樂