天天看點

SpringBoot之響應式程式設計

一 Spring WebFlux Framework說明

Spring WebFlux 是 Spring Framework 5.0 中引入的新 reactive web framework。與 Spring MVC 不同,它不需要 Servlet API,完全異步和 non-blocking,并通過反應堆項目實作Reactive Streams規範。

Spring WebFlux 有兩種版本:功能和 annotation-based。 annotation-based 一個非常接近 Spring MVC model,如下面的示例所示:

@RestController
@RequestMapping("/users")
public class MyRestController {

    @GetMapping("/{user}")
    public Mono<User> getUser(@PathVariable Long user) {
        // ...
    }

    @GetMapping("/{user}/customers")
    public Flux<Customer> getUserCustomers(@PathVariable Long user) {
        // ...
    }

    @DeleteMapping("/{user}")
    public Mono<User> deleteUser(@PathVariable Long user) {
        // ...
    }

}      

函數變量“WebFlux.fn”将路由配置與請求的實際處理分開,如下面的示例所示:

@Configuration
public class RoutingConfiguration {

    @Bean
    public RouterFunction<ServerResponse> monoRouterFunction(UserHandler userHandler) {
        return route(GET("/{user}").and(accept(APPLICATION_JSON)), userHandler::getUser)
                .andRoute(GET("/{user}/customers").and(accept(APPLICATION_JSON)), userHandler::getUserCustomers)
                .andRoute(DELETE("/{user}").and(accept(APPLICATION_JSON)), userHandler::deleteUser);
    }

}      
@Component
public class UserHandler {

    public Mono<ServerResponse> getUser(ServerRequest request) {
        // ...
    }

    public Mono<ServerResponse> getUserCustomers(ServerRequest request) {
        // ...
    }

    public Mono<ServerResponse> deleteUser(ServerRequest request) {
        // ...
    }
}      

WebFlux 是 Spring Framework 的一部分,其詳細資訊可在​​reference 文檔​​中找到。

您可以根據需要定義盡可能多的​

​RouterFunction​

​ beans 來子產品化 router 的定義。如果需要應用優先級,可以訂購 Beans。

要開始,請将​

​spring-boot-starter-webflux​

​子產品添加到 application。

在 application 中添加​

​spring-boot-starter-web​

​和​

​spring-boot-starter-webflux​

​子產品會導緻 Spring Boot auto-configuring Spring MVC,而不是 WebFlux。選擇此行為是因為許多 Spring 開發人員将​

​spring-boot-starter-webflux​

​添加到他們的 Spring MVC application 以使用 reactive ​

​WebClient​

​。您仍然可以通過将所選的 application 類型設定為​

​SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE)​

​來強制執行您的選擇。

Spring WebFlux Auto-configuration

Spring Boot 為 Spring WebFlux 提供 auto-configuration,适用于大多數 applications。

auto-configuration 在 Spring 的預設值之上添加以下 features:

  • HttpMessageReader

    HttpMessageWriter

    執行個體配置編解碼器(描述為稍後在本文檔中)。
  • 支援提供靜态資源,包括對 WebJars 的支援(描述為稍後在本文檔中)。

如果你想保留 Spring Boot WebFlux features 并且想要添加額外的​​WebFlux configuration​​,你可以添加自己的​

​@Configuration​

​ class 類型為​

​WebFluxConfigurer​

​但而不是 ​

​@EnableWebFlux​

​。

如果要完全控制 Spring WebFlux,可以添加自己的​

​@Configuration​

​注釋​

​@EnableWebFlux​

帶有 HttpMessageReaders 和 HttpMessageWriters 的 HTTP 編解碼器

Spring WebFlux 使用​

​HttpMessageReader​

​HttpMessageWriter​

​接口來轉換 HTTP 請求和響應。通過檢視 classpath 中可用的 libraries,它們配置為​

​CodecConfigurer​

​以具有合理的預設值。

Spring Boot 通過使用​

​CodecCustomizer​

​執行個體進一步自定義。例如,​

​spring.jackson.*​

​ configuration 鍵應用于 Jackson 編解碼器。

如果需要添加或自定義編解碼器,可以建立自定義​

​CodecCustomizer​

​ component,如下面的示例所示:

import org.springframework.boot.web.codec.CodecCustomizer;

@Configuration
public class MyConfiguration {

    @Bean
    public CodecCustomizer myCodecCustomizer() {
        return codecConfigurer -> {
            // ...
        }
    }

}      

你也可以利用Boot 的自定義 JSON 序列化器和反序列化器。

靜态内容

預設情況下,Spring Boot 為 classpath 中名為​

​/static​

​(或​

​/public​

​或​

​/resources​

​/META-INF/resources​

​)的目錄提供靜态内容。它使用來自 Spring WebFlux 的​

​ResourceWebHandler​

​,以便您可以通過添加自己的​

​WebFluxConfigurer​

​并覆寫​

​addResourceHandlers​

​方法來修改該行為。

預設情況下,資源映射到​

​/**​

​,但您可以通過設定​

​spring.webflux.static-path-pattern​

​ property 來調整它。例如,将所有資源重新定位到​

​/resources/**​

​可以實作如下:

spring.webflux.static-path-pattern=/resources/**      

您還可以使用​

​spring.resources.static-locations​

​自定義靜态資源位置。這樣做會将預設值替換為目錄位置清單。如果這樣做,預設的歡迎頁面檢測将切換到您的自定義位置。是以,如果您在啟動時的任何位置都有​

​index.html​

​,那麼它就是 application 的首頁。

除了前面列出的“标準”靜态資源位置之外,還為Webjars 内容做了一個特例。如果 jar files 包含在 Webjars 格式中,則中包含路徑的所有資源都将從 jar files 提供。

Spring WebFlux applications 并不嚴格依賴于 Servlet API,是以不能将它們部署為 war files 并且不要使用​

​src/main/webapp​

​目錄。

模闆引擎

與 REST web services 一樣,您也可以使用 Spring WebFlux 來提供動态 HTML 内容。 Spring WebFlux 支援各種模闆技術,包括 Thymeleaf,FreeMarker 和 Mustache。

Spring Boot 包括對以下模闆引擎的 auto-configuration 支援:

  • FreeMarker
  • Thymeleaf
  • 胡子

當您使用其中一個模闆引擎和預設的 configuration 時,您的模闆将從​

​src/main/resources/templates​

​自動擷取。

錯誤處理

Spring Boot 提供​

​WebExceptionHandler​

​,以合理的方式處理所有錯誤。它在處理 order 中的位置緊接在 WebFlux 提供的處理程式之前,這被認為是最後的。對于機器用戶端,它會生成一個 JSON 響應,其中包含錯誤,HTTP 狀态和 exception 消息的詳細資訊。對于浏覽器用戶端,有一個“whitelabel”錯誤處理程式,它以 HTML 格式呈現相同的資料。您還可以提供自己的 HTML 模闆來顯示錯誤(請參閱下一節)。

自定義此 feature 的第一個步驟通常涉及使用現有機制,但替換或擴充錯誤内容。為此,您可以添加​

​ErrorAttributes​

​類型的 bean。

要更改錯誤處理行為,可以實作​

​ErrorWebExceptionHandler​

​并注冊該類型的 bean 定義。因為​

​WebExceptionHandler​

​非常 low-level,是以 Spring Boot 還提供了一個友善的​

​AbstractErrorWebExceptionHandler​

​來讓你以 WebFlux 的方式處理錯誤,如下面的例子所示:

public class CustomErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {

    // Define constructor here

    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {

        return RouterFunctions
                .route(aPredicate, aHandler)
                .andRoute(anotherPredicate, anotherHandler);
    }

}      

要獲得更完整的圖檔,您還可以直接子類化​

​DefaultErrorWebExceptionHandler​

​并覆寫特定方法。

自定義錯誤頁面

如果要為給定狀态 code 顯示自定義 HTML 錯誤頁面,可以将檔案添加到​

​/error​

​檔案夾。錯誤頁面可以是靜态 HTML(即,添加到任何靜态資源檔案夾下)或使用模闆建構。檔案的 name 應該是确切的狀态 code 或系列掩碼。

例如,要 map ​

​404​

​到靜态 HTML 檔案,您的檔案夾結構如下:

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- public/
             +- error/
             |   +- 404.html
             +- <other public assets>      

要使用 Mustache 模闆 map 所有​

​5xx​

​錯誤,您的檔案夾結構如下:

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- templates/
             +- error/
             |   +- 5xx.mustache
             +- <other templates>      

Web 過濾器

Spring WebFlux 提供了一個​

​WebFilter​

​接口,可以實作過濾 HTTP request-response 交換。在 application context 中找到的​

​WebFilter​

​ beans 将自動用于過濾每個交換。

如果過濾器的 order 很重要,則可以實作​

​Ordered​

​或使用​

​@Order​

​進行注釋。 Spring Boot auto-configuration 可以為您配置 web 過濾器。執行此操作時,将使用以下 table 中顯示的訂單:

訂購

​MetricsWebFilter​

​Ordered.HIGHEST_PRECEDENCE + 1​

​WebFilterChainProxy​

​(Spring Security)

​-100​

​HttpTraceWebFilter​

​Ordered.LOWEST_PRECEDENCE - 10​

二 WebClient

Spring Boot 将 auto-detect 用于驅動​

​WebClient​

​,具體取決于 application classpath 上可用的 libraries。目前,支援 Reactor Netty 和 Jetty RS client。

​spring-boot-starter-webflux​

​ starter 預設依賴于​

​io.projectreactor.netty:reactor-netty​

​,它帶來了 server 和 client implementations。如果您選擇使用 Jetty 作為 reactive 伺服器,則應該在 Jetty Reactive HTTP client library,​

​org.eclipse.jetty:jetty-reactive-httpclient​

​上添加依賴項。對伺服器和 client 使用相同的技術具有優勢,因為它将自動在 client 和伺服器之間共享 HTTP 資源。

開發人員可以通過提供自定義​

​ReactorResourceFactory​

​JettyResourceFactory​

​ bean 來覆寫 Jetty 和 Reactor Netty 的資源 configuration - 這将應用于 clients 和伺服器。

如果您希望覆寫 client 的該選項,您可以定義自己的​

​ClientHttpConnector​

​ bean 并完全控制 client configuration。

您可以了解有關Spring Framework reference 文檔中的 WebClient configuration 選項的更多資訊。

WebClient 自定義

​WebClient​

​自定義有三種主要方法,具體取決于您希望自定義應用的廣泛程度。

要使任何自定義的範圍盡可能窄,請 inject auto-configured ​

​WebClient.Builder​

​然後根據需要調用其方法。 ​

​WebClient.Builder​

​執行個體是有狀态的:建構器上的任何更改都會反映在随後使用它建立的所有 client 中。如果要使用相同的建構器建立多個 client,還可以考慮使用​

​WebClient.Builder other = builder.clone();​

​克隆建構器。

要對所有​

​WebClient.Builder​

​執行個體進行 application-wide 添加自定義,可以聲明​

​WebClientCustomizer​

​ beans 并在注入點本地更改​

​WebClient.Builder​

最後,您可以回退到原始 API 并使用​

​WebClient.create()​

​。在這種情況下,不應用 auto-configuration 或​

​WebClientCustomizer​

三 代碼示範

  • pom.xml
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>      
  • 注:webflux不能和web共存,webflux啟動的是netty。
  • 實體類
public class User {

    private String id;
    
    private String name;
    
    

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public User(String id, String name) {
        super();
        this.id = id;
        this.name = name;
    }   
    
}      
  • Service層
@Service
public class UserService {

    
    private static final Map<String, User> dataMap = new HashMap<>();
    
    static{
        dataMap.put("1", new User("1", "小X老師"));
        dataMap.put("2", new User("2", "小D老師"));
        dataMap.put("3", new User("3", "小C老師"));
        dataMap.put("4", new User("4", "小L老師"));
        dataMap.put("5", new User("5", "小A老師"));
        dataMap.put("6", new User("6", "小S老師"));
        dataMap.put("7", new User("7", "小S老師"));
    }
    
    /**
     * 功能描述:傳回使用者清單
     * @return
     */
    public Flux<User> list(){
        Collection<User> list = UserService.dataMap.values();
        
        return Flux.fromIterable(list);
    }
    
    
    /**
     * 功能描述:根據id查找使用者
     * @param id
     * @return
     */
    public Mono<User> getById(final String id){
        return Mono.justOrEmpty(UserService.dataMap.get(id));
    }
    
   /**
    * 功能描述:根據id删除使用者
    * @param id
    * @return
    */
    public Mono<User> del(final String id){
        return Mono.justOrEmpty(UserService.dataMap.remove(id));
    }
    
    
    
}      
  • web層
@RestController
@RequestMapping("/api/v1/user")
public class UserController {
    

    //@Autowired
    //private UserService userService;
    
    private final UserService userService;
    
     public UserController(final UserService userService) {
        this.userService = userService;
    }
    
    
    @GetMapping("/test")
    public Mono<String> test(){
        return Mono.just("hello");
    }

    
    /**
     * 功能描述:根據id找使用者
     * @param id
     * @return
     */
    @GetMapping("find")
    public Mono<User> findByid(final String id){
        return userService.getById(id);
    }
    
    
    /**
     * 功能描述:删除使用者
     * @param id
     * @return
     */
    @GetMapping("del")
    public Mono<User> del(final String id){
        return userService.del(id);
    }
    
    /**
     * 功能描述:清單
     * @return
     */
    @GetMapping(value="list",produces=MediaType.APPLICATION_STREAM_JSON_VALUE)
    public Flux<User> list(){
        return userService.list().delayElements(Duration.ofSeconds(2));
    }
    
}
      
  • 測試類
//@RunWith(SpringRunner.class)
//@SpringBootTest
public class WebfluxApplicationTests {


    @Test
    public void testBase(){
        
        Mono<String> bodyMono = WebClient.create().get()
        .uri("http://localhost:8080/api/v1/user/find?id=1")
        .accept(MediaType.APPLICATION_JSON)
        .retrieve().bodyToMono(String.class);
        
        System.out.println(bodyMono.block());
        
    }
    

    @Test
    public void testBase2(){
        
        Mono<String> bodyMono = WebClient.create().get()
        .uri("http://localhost:8080/api/v1/user/find?id={id}",2)
        .accept(MediaType.APPLICATION_JSON)
        .retrieve().bodyToMono(String.class);
        
        System.out.println(bodyMono.block());
        
    }

}
注:着重講一下這個list方法,這個方法裡面延遲2秒,會展現出是流一樣,每隔2秒出現一條資料。

{"id":"1","name":"小X老師"}
{"id":"2","name":"小D老師"}
{"id":"3","name":"小C老師"}
{"id":"4","name":"小L老師"}
{"id":"5","name":"小A老師"}
{"id":"6","name":"小S老師"}
{"id":"7","name":"小S老師"}