Spring 5新框架——WebFlux

目錄

1.基礎概念

1.1 Reactor模型

1.2 Spring WebFlux概述

1.3 WebHandler接口和運行流程

2.開發WebFlux服務端

2.1 依賴導入

2.2 開發持久層

2.3 開發服務層

2.4 開發控制層

2.5 配置服務

2.6 客戶端開發——WebClient

3.深入WebFlux服務端開發

3.1 類型轉換器——Converter

3.2 驗證器——Validator

3.3 訪問靜態資源

4.深入客戶端開發

4.1 處理服務端錯誤和轉換

4.2 設置請求頭

5.使用路由函數開發WebFlux

5.1開發處理器

5.2 開發請求路由

5.3使用過濾器


在互聯網的應用中,存在電商和金融等企業,這些企業對於業務的嚴謹性要求特別高,因爲他們的業務關係到用戶和商家的賬戶以及財產的安全,多以它們對於數據的一致性十分重視,在存在併發的時候,就需要通過鎖或者其他機制保證一些重要數據的一致性,但這也會造成性能的下降。對於另外一些互聯網應用則不一樣,如遊戲、視頻、新聞和廣告的網站,它們一般不會涉及賬戶和財產的問題,也就是不需要很高的數據一致性,但是對於併發數和響應速度卻十分在意,而使用傳統的開發模式會引入一致性機制,造成性能下降,爲此一些軟件設計者提出了響應式編程的理念。

爲了適應響應式編程的潮流,Spring 5發佈了新一代響應式的Web框架——Spring WebFlux,不過在討論WebFlux之前,需要先了解一下RxJava和Reactor,而Reactor是Spring WebFlux的默認實現,所以我們主要討論一下Reactor。

1.基礎概念

響應式編程是一種面向數據流和變化傳播的編程範式。對於響應式框架,是基於響應式宣言的理念所產生的編程方式。響應式宣言分爲4大理念:

  • 靈敏的:可以快速響應的,只要有任何可能,系統都應該能夠儘可能快地做出響應。
  • 可恢復的:系統在運行中可能出現問題,但是能夠有很強大的容錯機制和修復機制保持響應性。
  • 可伸縮的:在任何負載下,響應式編程都可以根據自身壓力變化,請求少時,通過減少資源釋放服務器壓力,負載大時能夠通過擴展算法和軟硬件的方式擴展服務壓力,以經濟實惠的方式實現可伸縮性。
  • 消息驅動的:響應式編程存在異步消息機制,事件之間的協作是通過消息進行連接的。

基於這些理念,響應式編程提出了各種模型來滿足響應式編程的理念,其中著名的有Reactor和RxJava,Spring5就是基於它們構建WebFlux,而默認情況下它會使用Reactor。

1.1 Reactor模型

傳統的編程模型中,當請求量大於系統能夠承受的最大線程數時,大量的線程就只能夠在隊列中等待或者被系統殺死,無法及時響應用戶。爲了克服這個問題,提出了Reactor(反應器)模式,其模型圖如下圖所示:

首先客戶端會先向服務器註冊其感興趣的事件(Event),這樣客戶端就訂閱了對應的事件,只是訂閱事件並不會給服務器發送請求。當客戶端發生一些已經註冊的事件時,就會觸發服務器的響應。當觸發服務器響應時,服務器存在一個Selector線程,這個線程只是負責輪詢客戶端發送過來的事件,並不處理請求,當它接收到有客戶端事件時,就會轉到對應的請求處理器,然後啓用另外一個線程運行處理器。因爲Selector線程只是輪詢,並不處理複雜的業務邏輯,所以它可以在輪詢後對請求做到實時響應,速度十分快。由於事件存在很多種,所以請求處理器也存在多個,因此還需要進行區分事件的類型,所以Selector存在一個路由問題。當請求處理器處理業務時,結果最終也會轉換爲數據流發送到客戶端。

從上面可以看出,Reactor是一種基於事件的模型,對於服務器線程而言,它也是一種異步的,首先是Selector線程輪詢到事件,然後通過路由找到處理器去運行對應的邏輯,處理器最後所返回的結果會轉換爲數據流。

1.2 Spring WebFlux概述

Spring WebFlux是Spring 5推出的新一代Web響應式編程框架,它十分適合那些需要高併發和大量請求的互聯網應用,特別是那些需要高速響應而對業務邏輯要求並不十分嚴格的網站,如遊戲、視頻和新聞瀏覽網站等。

參考地址:https://docs.spring.io/spring-framework/docs/5.2.2.RELEASE/spring-framework-reference/web-reactive.html#spring-webflux

下圖是Spring MVC和Spring WebFlux的對比:

WebFlux包含一個路由分發層,也就是根據請求的事件,決定採用什麼類的什麼方法處理客戶端發送過來的事件請求,類似於Reactor中的Selector,業務邏輯處理完成後,再將結果轉換爲數據流。

WebFlux需要能夠支持Servlet3.1+的容器,如Tomcat、Jetty和Undertow等,而在Java異步編程的領域,使用得最多的是Netty,所以WebFlux的starter中默認是依賴於Netty庫的。

1.3 WebHandler接口和運行流程

與Spring MVC使用DispatcherServlet不同的是Spring WebFlux使用的是WebHandler。它與DispatcherServlet十分類似。只是WebHandler是一個接口,爲此Spring WebFlux爲其提供了幾個實現類,以便於在不同的場景下使用。這幾個實現類中DispatcherHandler是我們關注的核心,它與DispatcherServlet是十分接近的 ,我們首先看一下DispatcherHandler的核心代碼handle方法:

	@Override
	public Mono<Void> handle(ServerWebExchange exchange) {
		if (this.handlerMappings == null) {
			return createNotFoundError();
		}
		return Flux.fromIterable(this.handlerMappings)
				.concatMap(mapping -> mapping.getHandler(exchange))
				.next()
				.switchIfEmpty(createNotFoundError())
				.flatMap(handler -> invokeHandler(exchange, handler))
				.flatMap(result -> handleResult(exchange, result));
	}

從源碼中我們可以看到,與Spring MVC一樣,都是從HandlerMapping中找到對應的處理器,找到處理器之後就會通過invokeHandler方法運行處理器,最後得到了處理結果的handleResult方法,通過它將結果轉變爲對應的數據流序列。下圖是它大概的流程圖:

下面我們以MongoDB作爲響應式編程的數據源,這裏之所以使用MongoDB,是因爲Spring WebFlux只能支持Spring Data Reactive,它是一種非阻塞的響應數據方式。遺憾的是,因爲數據庫的開發往往是阻塞的,所以Spring Data Reactive並不能對數據庫的開發給予有效支持。但Spring Data Reactive可以支持Redis、MongoDB等NoSQL的開發,而Redis功能受限,更加適合作爲緩存使用,所以我們選擇使用MongoDB作爲Spring WebFlux的實例,這也是最廣泛的使用方式。

2.開發WebFlux服務端

在Spring WebFlux中,存在兩種開發方式,一種是類似於Spring MVC的模式,另一種則是函數功能性編程,無論哪種都是允許的。我們這裏使用類似於Spring MVC的模式,其中@Controller、@ResponseMapping、@GetMapping、@PostMapping等Spring MVC註解依然有效,這就爲構建WebFlux的應用帶來了便利。

2.1 依賴導入

首先導入相關依賴:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </dependency>

        <!--MongoDb jpa-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

我們以MongoDB作爲響應式編程的數據源,所以引入了JPA和MongoDB的starter。與此同時將WebFlux的starter包引入進來了,而它會依賴於Spring Web的包,這裏還引入了Tomcat作爲默認的服務器。

2.2 開發持久層

這裏採用MongoDB作爲開發的數據源,先定義POJO:

package com.martin.flux.pojo;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;

import java.io.Serializable;

/**
 * @author: martin
 * @date: 2019/11/3 10:44
 * @description:
 */
@Document(collection = "users")
@Data
public class UserDO implements Serializable {
    @Id
    private Long id;
    @Field("user_name")
    private String userName;
    private String note;
    /**
     * 定義了性別的枚舉,可以通過typeHandler轉換
     */
    private String sex;
}

這裏採用JPA作爲持久層,而Spring WebFlux爲響應式提供了接口ReactiveMongoRepository,這樣就可以通過繼承它聲明瞭一個JPA接口:

package com.martin.flux.dao;

import com.martin.flux.pojo.UserDO;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;

/**
 * @author: martin
 * @date: 2020/2/7
 */
@Repository
public interface UserDao extends ReactiveMongoRepository<UserDO, Long> {
    Flux<UserDO> findByUserNameLikeAndNoteLike(String userName, String note);
}

findByUserNameLikeAndNoteLike方法是一個按照JPA規則命名的方法,它的作用就是使用用戶名和備註進行模糊查詢。

2.3 開發服務層

先定義用戶服務接口UserService:

package com.martin.flux.service;

import com.martin.flux.pojo.UserDO;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * @author: martin
 * @date: 2020/2/7
 */
public interface UserService {
    Mono<UserDO> getUser(Long id);

    Mono<UserDO> insertUser(UserDO userDO);

    Mono<UserDO> updateUser(UserDO userDO);

    Mono<Void> deleteUser(Long id);

    Flux<UserDO> findUsers(String userName, String note);
}

然後實現這個接口:

package com.martin.flux.service;

import com.martin.flux.dao.UserDao;
import com.martin.flux.pojo.UserDO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * @author: martin
 * @date: 2020/2/7
 */
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;

    @Override
    public Mono<UserDO> getUser(Long id) {
        return userDao.findById(id);
    }

    @Override
    public Mono<UserDO> insertUser(UserDO userDO) {
        return userDao.save(userDO);
    }

    @Override
    public Mono<UserDO> updateUser(UserDO userDO) {
        return userDao.save(userDO);
    }

    @Override
    public Mono<Void> deleteUser(Long id) {
        return userDao.deleteById(id);
    }

    @Override
    public Flux<UserDO> findUsers(String userName, String note) {
        return userDao.findByUserNameLikeAndNoteLike(userName, note);
    }
}

2.4 開發控制層

對於WebFlux而言,使用Rest風格更加合適,這裏使用的也是Rest風格的控制器。首先我們定義前端視圖模型UserVO:

package com.martin.flux.pojo;

import lombok.Data;

/**
 * @author: martin
 * @date: 2020/2/7
 */
@Data
public class UserVO {
    private Long id;
    private String userName;
    private int sexCode;
    private String sexName;
    private String note;
}

接下來定義用戶控制器;

package com.martin.flux.controller;

import com.martin.flux.pojo.UserDO;
import com.martin.flux.pojo.UserVO;
import com.martin.flux.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * @author: martin
 * @date: 2020/2/7
 */
@RestController
public class UserController {
    @Autowired
    private UserService userService;

    //獲取用戶
    @GetMapping("/user/{id}")
    public Mono<UserVO> getUser(@PathVariable Long id) {
        return userService.getUser(id).map(u -> translate(u));
    }

    //新增用戶
    @PostMapping("/user")
    public Mono<UserVO> insertUser(@RequestBody UserDO userDO) {
        return userService.insertUser(userDO).map(u -> translate(userDO));
    }

    //更新用戶
    @PutMapping("/user")
    public Mono<UserVO> updateUser(@RequestBody UserDO userDO) {
        return userService.updateUser(userDO).map(u -> translate(userDO));
    }

    //刪除用戶
    @PutMapping("/user/{id}")
    public Mono<Void> updateUser(@PathVariable Long id) {
        return userService.deleteUser(id);
    }

    //查詢用戶
    @GetMapping("/user/{userName}/{note}")
    public Flux<UserVO> findUsers(@PathVariable String userName, @PathVariable String note) {
        return userService.findUsers(userName, note).map(u -> translate(u));
    }


    private UserVO translate(UserDO userDO) {
        UserVO userVO = new UserVO();
        userVO.setId(userDO.getId());
        userVO.setUserName(userDO.getUserName());
        userVO.setSexCode(Integer.valueOf(userDO.getSex()));
        userVO.setNote(userDO.getNote());
        return userVO;
    }
}

這裏的@RestController代表採用Rest風格的控制器,這樣Spring就知道將返回的內容轉換爲JSON數據序列。但是應注意的是,這裏的方法返回的或者是Flux<UserVO>或者是Mono<User>,Mono是一個0~1個數據流序列,而Flux是一個0~N個數據流序列。

2.5 配置服務

Spring Boot配置文件如下:

spring.data.mongodb.host=localhost
spring.data.mongodb.username=dbAdmin
spring.data.mongodb.password=password
spring.data.mongodb.port=27017
spring.data.mongodb.database=test-it

啓動文件配置如下:

package com.martin.flux;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;
//定義掃描包
@SpringBootApplication(scanBasePackages = "com.martin.flux")
//因爲引入了JPA,所以默認情況下,需要配置數據源
//排除原有自動配置的數據源
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
//在WebFlux下,驅動MongoDB的JPA接口
@EnableReactiveMongoRepositories(basePackages = "com.martin.flux.dao")
public class FluxApplication {

    public static void main(String[] args) {
        SpringApplication.run(FluxApplication.class, args);
    }

}

因爲引入了JPA,所以在默認的情況下Spring Boot會嘗試裝配關係數據庫數據源(DataSource),而這裏使用的是MongoDB並沒有使用關係數據庫,所以使用@EnableAutoConfiguration去排除數據源的初始化,否則就會得到錯誤的啓動日誌。在WebFlux中使用響應式的MongoDB的JPA接口,需要使用註解@EnableReactiveMongoRepositories進行驅動,還定義了掃描的包,這樣就可以將代碼掃描到IOC容器中了。

MongoDB數據庫中導入如下數據:

我們啓動應用,並訪問http://localhost:8080/user/1,頁面會有如下返回結果:

顯然運行成功了,這樣就完成了簡單的WebFlux開發。

2.6 客戶端開發——WebClient

爲了方便測試,Spring WebFlux爲我們提供了WebClient類,它是一個比RestTemplate更爲強大的類,通過它就可以請求後端的服務。實例代碼如下:

package com.martin.flux;

import com.martin.flux.pojo.SexEnum;
import com.martin.flux.pojo.UserDO;
import com.martin.flux.pojo.UserVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;

/**
 * @author: martin
 * @date: 2020/2/9
 */
@Slf4j
public class FluxTest {
    public static void main(String[] args) {
        WebClient client = WebClient.create("http://localhost:8080");
        //新建一個用戶
        UserDO newUser = new UserDO();
        newUser.setId(2L);
        newUser.setUserName("mars");
        newUser.setSex(String.valueOf(SexEnum.FEMALE.getCode()));
        //新增用戶
        insertUser(client, newUser);
        getUser(client, 2L);
        //更新一個用戶
        UserDO updateUser = new UserDO();
        updateUser.setId(1L);
        updateUser.setUserName("mars");
        updateUser.setSex(String.valueOf(SexEnum.FEMALE.getCode()));
        updateUser.setNote("mars老師");
        updateUser(client, updateUser);
        //查詢用戶
        findUsers(client, "mars", "mars老師");
        //刪除用戶
        deleteUser(client, 2L);
    }

    private static void insertUser(WebClient client, UserDO newUser) {
        Mono<UserVO> userVOMono = client.post()
                .uri("/user")
                .contentType(MediaType.APPLICATION_STREAM_JSON)
                .body(Mono.just(newUser), UserDO.class)
                .accept(MediaType.APPLICATION_STREAM_JSON)
                .retrieve()
                .bodyToMono(UserVO.class);

        UserVO userVO = userVOMono.block();
        log.error("insert user success:{}", userVO.getUserName());
    }

    private static void getUser(WebClient client, long id) {
        Mono<UserVO> userVOMono = client.get()
                .uri("/user/{id}", id)
                .accept(MediaType.APPLICATION_STREAM_JSON)
                .retrieve()
                .bodyToMono(UserVO.class);
        UserVO userVO = userVOMono.block();
        log.error("get user success:{}", userVO.getUserName());
    }

    private static void updateUser(WebClient client, UserDO userDO) {
        Mono<UserVO> userVOMono = client.put()
                .uri("/user")
                .contentType(MediaType.APPLICATION_STREAM_JSON)
                .body(Mono.just(userDO), UserDO.class)
                .accept(MediaType.APPLICATION_STREAM_JSON)
                .retrieve()
                .bodyToMono(UserVO.class);
        UserVO userVO = userVOMono.block();
        log.error("update user success:{}", userVO.getUserName());
    }

    private static void findUsers(WebClient client, String userName, String note) {
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("userName", userName);
        paramMap.put("note", note);
        Mono<UserVO> userVOMono = client.get()
                .uri("/user/{userName}/{note}", paramMap)
                .accept(MediaType.APPLICATION_STREAM_JSON)
                .retrieve()
                .bodyToMono(UserVO.class);
        UserVO userVO = userVOMono.block();
        log.error("find user success:{}", userVO.getUserName());
    }

    private static void deleteUser(WebClient client, long id) {
        Mono<Void> voidMono = client.delete()
                .uri("/user/{id}", id)
                .accept(MediaType.APPLICATION_STREAM_JSON)
                .retrieve()
                .bodyToMono(Void.class);
        Void voidResult = voidMono.block();
        log.error("delete user success:{}", voidMono);
    }
}

這裏採用了WebClient的靜態方法create來創建WebClient對象,該方法的參數是網站的基礎URI,這樣就能夠定位基礎的URI。Mono<UserVO>對象的初始化代碼,只是給後端註冊一個事件而已,並不會發送請求。使用Mono的block方法則是發送觸發事件,這樣服務器纔會響應事件,將數據流傳送到客戶端中。

3.深入WebFlux服務端開發

正如上面的例子,服務端的開發十分接近Spring MVC,在大部分情況下可以參考Spring MVC的內容。但Web Flux也有一些特殊的處理,比如新增加了參數轉換和驗證規則等,此外還有一些錯誤的處理。這些都是在實際開發中最常見到的內容,需要進一步地學習它們。

3.1 類型轉換器——Converter

例如,現在來實現一個類型轉換,約定用戶將以字符串格式{id}-{userName}-{sex}-{note}進行傳遞,然後通過類型轉換器(Converter)得到用戶數據。實現代碼如下:

package com.martin.flux.config;

import com.martin.common.constant.MarkCodeEnum;
import com.martin.flux.pojo.UserDO;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;

/**
 * @author: martin
 * @date: 2020/2/9
 */
@Configuration
public class WebFluxConfig implements WebFluxConfigurer {
    //註冊Converter
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(string2UserConverter());
    }

    /**
     * 定義String轉換成UserDO類型轉換器
     * 如果加上@Bean註解,Spring Boot會自動識別爲類型轉換器
     *
     * @return
     */
    public Converter<?, ?> string2UserConverter() {
        Converter<String, UserDO> converter = (s -> {
            String[] args = s.split(MarkCodeEnum.SEGMENT.getCode());
            UserDO userDO = new UserDO();
            Long id = Long.valueOf(args[0]);
            userDO.setId(id);
            userDO.setUserName(args[1]);
            userDO.setSex(args[2]);
            userDO.setNote(args[3]);
            return userDO;
        });

        return converter;
    }
}

上面的代碼實現了WebFluxConfigurer 接口,並覆蓋了addFormatters方法。該方法是加載轉換器和格式化器的,這裏使用了方法string2UserConverter來定義了一個Converter,這樣就能夠將字符串按照約定的格式轉換爲用戶類。我們也可以繼承WebFluxConfigurer接口,將string2UserConverter方法添加@Bean註解,這樣Spring Boot會自動識別這個Bean爲轉換器,就不需要實現addFormatters方法手動註冊了。

爲了測試轉換器,在UserController中添加如下方法:

    //新增用戶
    @PostMapping("/user2/{user}")
    public Mono<UserVO> insertUser2(@PathVariable("user") UserDO userDO) {
        return userService.insertUser(userDO).map(u -> translate(userDO));
    }

WebClient測試代碼如下:

package com.martin.flux;

import com.martin.flux.pojo.UserVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

/**
 * @author: martin
 * @date: 2020/2/9
 */
@Slf4j
public class ConvertTest {
    public static void main(String[] args) {
        WebClient client = WebClient.create("http://localhost:8080");
        Mono<UserVO> userVOMono = client.post()
                .uri("/user2/{user}", "3-convert-1-轉換器測試")
                .accept(MediaType.APPLICATION_STREAM_JSON)
                .retrieve()
                .bodyToMono(UserVO.class);

        UserVO userVO = userVOMono.block();
        log.error("user name is:{}", userVO.getUserName());
    }
}

我們傳入和服務端約定格式,就能夠將用戶信息存入到MongDB中了。關於日期格式化器,WebFlux允許我們通過application.properties進行配置,例如:

spring.webflux.date-format=yyyy-MM-dd

3.2 驗證器——Validator

有時候需要對參數進行驗證,比如我們需要驗證一下用戶名稱是否爲空。這個時候可以使用Spring MVC的Validator機制,首先新建用戶驗證器UserValidator:

package com.martin.flux.config;


import com.martin.flux.pojo.UserDO;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

/**
 * @author: martin
 * @date: 2020/2/9
 */
public class UserValidator implements Validator {
    //確定支持的驗證類型
    @Override
    public boolean supports(Class<?> clazz) {
        return clazz.equals(UserDO.class);
    }

    //驗證邏輯
    @Override
    public void validate(Object target, Errors errors) {
        UserDO userDO = (UserDO) target;
        if (StringUtils.isEmpty(userDO.getUserName())) {
            errors.rejectValue("userName", null, "用戶名不能爲空");
        }
    }
}

在WebFluxConfig中覆蓋WebFluxConfigurer接口的getValidator方法,代碼如下;

    @Override
    public Validator getValidator() {
        return new UserValidator();
    }

上面只是定義了驗證器,但並沒有啓用,爲了測試該驗證器,需要在Controller對應的方法中加入@Valid註解,實例代碼如下;

    //新增用戶
    @PostMapping("/user3/")
    public Mono<UserVO> insertUser3(@Valid @RequestBody UserDO userDO) {
        return userService.insertUser(userDO).map(u -> translate(userDO));
    }

這樣Spring就會啓用UserValidator進行參數驗證。但這裏是在全局中加入驗證器,有時候我們希望使用局部驗證器,而不是使用全局驗證器,這是可以仿照Spring MVC的辦法使用註解@InitBinder,將類和驗證器進行綁定。我們刪除在WebFluxConfig中的getValidator方法,然後在Controller中加入如下代碼:

    @InitBinder
    public void initBinder(DataBinder binder) {
        binder.setValidator(new UserValidator());
    }

這樣UserValidator就只能對當前的控制器有效,而不是全局有效了。

3.3 訪問靜態資源

有時候我們還需要訪問一些文件,如圖片、配置內容等。這時可以覆蓋WebFluxConfigurer的addResourceHandlers方法,代碼如下:

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/static/**")
                .addResourceLocations("/public", "classpath:/static/")
                .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
    }

通過這樣的限定,就可以直接通過URI來訪問/resources/static/下的靜態資源,而在Spring的上下文機制中還可以直接訪問/public路徑,它將能夠訪問classpath:/static/下的資源。這裏還設置了緩存的時限,設置爲1年(365天)。爲了區分靜態資源設置一個前綴,這樣便能把靜態資源和動態資源區分出來,爲了我們可以在application.properties文件中進行配置:

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

這樣在訪問靜態資源的時候就需要加入/static/前綴了。

4.深入客戶端開發

上面的客戶端開發只是考慮了正常的狀態,而一些特殊的要求卻沒有考慮,比如如何設置請求頭,服務端發生了錯誤,應當如何處理等。這些都是實踐中經常發生的,所以很有必要討論一下。

4.1 處理服務端錯誤和轉換

客戶端處理服務端錯誤的實例代碼如下:

    private static void getUser(WebClient client, long id) {
        Mono<UserVO> userVOMono = client.get()
                .uri("/user/{id}", id)
                .accept(MediaType.APPLICATION_STREAM_JSON)
                .retrieve()
                .onStatus(httpStatus -> httpStatus.is4xxClientError() || httpStatus.is5xxServerError(), clientResponse -> Mono.empty())
                .bodyToMono(UserVO.class);
        UserVO userVO = userVOMono.block();
        if (userVO == null) {
            System.out.println("[用戶名稱]" + userVO.getUserName());
        } else {
            System.out.println("服務端沒有返回該用戶");
        }
    }

這裏採用了onStatus方法,這個方法是監控服務器返回的方法。它的兩個參數都採用了Lambda表達式的方式,第一個Lambda表達式的參數是HttpStatus類型,它需要返回的是boolean值;第二個Lambda表達式參數爲ClientResponse類型的,它是在第一個Lambda表達式返回true時觸發,這裏是讓結果轉換爲空。

4.2 設置請求頭

有時候需要給HTTP請求頭設置一些屬性,以便於服務端的獲取。實例代碼如下:

    //更新用戶
    @PutMapping("/user/name")
    public Mono<UserVO> updateUserByHeader(@RequestHeader("id") Long id, @RequestHeader("userName") String userName) {
        Mono<UserDO> userDOMono = userService.getUser(id);
        UserDO userDO = userDOMono.block();
        if (userDO == null) {
            throw new IllegalArgumentException("找不到該用戶");
        }
        userDO.setUserName(userName);

        return userService.updateUser(userDO).map(u -> translate(userDO));
    }

服務端和Spring MVC一樣,使用@RequestHeader從請求頭中獲取參數,然後根據參數去查詢用戶。有了服務端的方法,接着使用客戶端進行測試:

 private static void updateUserByHeader(WebClient client, Long id,String userName) {
        Mono<UserVO> userVOMono = client.put()
                .uri("/user/name")
                .header("id",id+"")
                .header("userName",userName)
                .accept(MediaType.APPLICATION_STREAM_JSON)
                .retrieve()
                .bodyToMono(UserVO.class);
        UserVO userVO = userVOMono.block();
        log.error("update user success:{}", userVO.getUserName());
    }

這裏的兩個header方法,它們各自設置了用戶編號id和用戶名userName,這樣就可以以請求頭的形式給服務端傳遞參數了。

5.使用路由函數開發WebFlux

除了上述使用類似Spring MVC的開發方式以外,WebFlux還提供了路由函數開發方式來開發WebFlux。這樣的方式體現了高併發的特性,也符合近期興起的函數式編程的潮流。但是也會引入更多的API和長長的方法鏈。

5.1開發處理器

使用路由函數方法,首先需要開發一個處理器處理各種場景。UserHandler的實現代碼如下:

package com.martin.flux.controller;

import com.martin.flux.pojo.UserDO;
import com.martin.flux.pojo.UserVO;
import com.martin.flux.service.UserService;
import org.apache.catalina.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * @author: martin
 * @date: 2020/2/10
 */
@Service
public class UserHandler {
    @Autowired
    private UserService userService;

    public Mono<ServerResponse> getUser(ServerRequest request) {
        //獲取請求參數URI參數
        String idStr = request.pathVariable("id");
        Long id = Long.valueOf(idStr);
        Mono<UserVO> userVOMono = userService.getUser(id).map(u -> translate(u));
        return ServerResponse
                .ok() //響應成功
                .contentType(MediaType.APPLICATION_JSON_UTF8)  //響應體類型
                .body(userVOMono, UserVO.class);//響應體
    }

    public Mono<ServerResponse> insertUser(ServerRequest request) {
        //把請求頭轉換爲UserDO對象
        Mono<UserDO> userDOMono = request.bodyToMono(UserDO.class);
        Mono<UserVO> userVOMono = userDOMono
                .cache() //將數據流對象緩存起來,無需等待數據接收
                .flatMap(user -> userService.insertUser(user))
                .map(u -> translate(u));

        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(userVOMono, UserVO.class);
    }

    public Mono<ServerResponse> updateUser(ServerRequest request) {
        Mono<UserDO> userDOMono = request.bodyToMono(UserDO.class);
        Mono<UserVO> userVOMono = userDOMono.cache().flatMap(userDO -> userService.updateUser(userDO)).map(u -> translate(u));

        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(userVOMono, UserVO.class);
    }

    public Mono<ServerResponse> deleteUser(ServerRequest request) {
        //獲取請求參數URI參數
        String idStr = request.pathVariable("id");
        Long id = Long.valueOf(idStr);
        Mono<Void> monoVoid = userService.deleteUser(id);
        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(monoVoid, Void.class);
    }

    /**
     * 查詢用戶
     *
     * @param request
     * @return
     */
    public Mono<ServerResponse> findUsers(ServerRequest request) {
        //獲取請求參數URI參數
        String userName = request.pathVariable("userName");
        String note = request.pathVariable("note");
        //使用Flux封裝多個數據單元
        Flux<UserVO> userVOFlux = userService.findUsers(userName, note).map(u -> translate(u));

        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(userVOFlux, UserVO.class);
    }

    /**
     * 修改用戶名
     *
     * @param request
     * @return
     */
    public Mono<ServerResponse> updateUserName(ServerRequest request) {
        //獲取請求頭數據
        String idStr = request.headers().header("id").get(0);
        String userName = request.headers().header("userName").get(0);
        //獲取原有用戶信息
        Mono<UserDO> userDOMono = userService.getUser(Long.valueOf(idStr));
        UserDO userDO = userDOMono.block();
        //修改用戶名
        userDO.setUserName(userName);
        Mono<UserVO> userVOMono = userService.insertUser(userDO).map(u -> translate(u));

        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(userVOMono, UserVO.class);
    }

    private UserVO translate(UserDO userDO) {
        UserVO userVO = new UserVO();
        userVO.setId(userDO.getId());
        userVO.setUserName(userDO.getUserName());
        userVO.setSexCode(Integer.valueOf(userDO.getSex()));
        userVO.setNote(userDO.getNote());
        return userVO;
    }
}

用戶處理器各個方法開發完成之後,還不能接收請求,因爲還沒有使它與請求URI對應起來,也沒有設置請求接收和相應的類型等信息。

5.2 開發請求路由

爲了讓HTTP請求能夠映射到方法上,還需要一個路由的功能,因此需要開發路由器,使得請求能夠映射到路由的方法上。

package com.martin.flux.config;

import com.martin.flux.controller.UserHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;

import static org.springframework.web.reactive.function.server.RequestPredicates.DELETE;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.RequestPredicates.PUT;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

/**
 * @author: martin
 * @date: 2020/2/11
 */
@Configuration
public class RouterConfig {
    @Autowired
    private UserHandler userHandler;

    //定義用戶路由
    @Bean
    public RouterFunction<ServerResponse> userRouter() {
        RouterFunction<ServerResponse> router = route(GET("/router/user/{id}").and(accept(MediaType.APPLICATION_STREAM_JSON)), userHandler::getUser)
                .andRoute(GET("/router/user/{userName}/{note}").and(accept(MediaType.APPLICATION_STREAM_JSON)), userHandler::findUsers)
                .andRoute(POST("/router/user").and(contentType(MediaType.APPLICATION_STREAM_JSON)).and(accept(MediaType.APPLICATION_STREAM_JSON)), userHandler::insertUser)
                .andRoute(PUT("/router/user").and(contentType(MediaType.APPLICATION_STREAM_JSON)).and(accept(MediaType.APPLICATION_STREAM_JSON)), userHandler::updateUser)
                .andRoute(DELETE("/router/user/{id}").and(accept(MediaType.APPLICATION_STREAM_JSON)), userHandler::deleteUser)
                .andRoute(PUT("/router/user/name").and(accept(MediaType.APPLICATION_STREAM_JSON)), userHandler::updateUserName);

        return router;
    }
}

5.3使用過濾器

在互聯網的環境中往往還需要保護這些業務請求,以避免網站被攻擊。這時可以採用過濾器的方式攔截請求,通過驗證身份後才處理業務邏輯。下面的實例代碼實現了在請求頭中存放用戶名和密碼,後端驗證後才處理業務邏輯。

package com.martin.flux.config;

import com.martin.flux.controller.UserHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

/**
 * @author: martin
 * @date: 2020/2/11
 */
@Configuration
public class RouterFiltConfig {
    @Autowired
    private UserHandler userHandler;

    public static final String HEADER_NAME = "user";
    public static final String HEADER_PWD = "password";

    //定義用戶路由
    @Bean
    public RouterFunction<ServerResponse> userRouter() {
        RouterFunction<ServerResponse> router =
                route(GET("/security/user/{id}").and(accept(MediaType.APPLICATION_STREAM_JSON)), userHandler::getUser)
                        .filter(((serverRequest, handlerFunction) -> filterLogic(serverRequest, handlerFunction)));

        return router;
    }

    private Mono<ServerResponse> filterLogic(ServerRequest serverRequest, HandlerFunction<ServerResponse> handlerFunction) {
        //取出請求頭
        String userName = serverRequest.headers().header(HEADER_NAME).get(0);
        String password = serverRequest.headers().header(HEADER_PWD).get(0);
        //驗證通過條件
        if (!StringUtils.isEmpty(userName) && !StringUtils.isEmpty(password) && !userName.equals(password)) {
            //接收請求
            return handlerFunction.handle(serverRequest);
        }
        //驗證不匹配,則不允許請求,返回未簽名錯誤
        return ServerResponse.status(HttpStatus.UNAUTHORIZED).build();
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章