1、使用SpringWebFlux
1)、pom.xml引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
WebFlux 的默认嵌入式服务器是 Netty 而不是 Tomcat。
①返回单个值:
@GetMapping("/{id}")
public Mono<UserInfo> userInfoById(@PathVariable("id") Long id) {
return userRepository.findById(id);
}
public interface UserRepository extends ReactiveCrudRepository<UserInfo, Long> {
}
②响应式地处理输入:
@PostMapping(consumes="application/json")
@ResponseStatus(HttpStatus.CREATED)
public User postUser(@RequestBody User user) {
return userRepo.save(user);
}
上面可以改成下面这种完全无阻塞的请求处理方法:
@PostMapping(consumes="application/json")
@ResponseStatus(HttpStatus.CREATED)
public Mono<User> postUser(@RequestBody Mono<User> userMono) {
// saveAll() 方法返回一个 Flux<Taco>,可以调用 next() 来获取
// postUser() 返回的 Mono<User>
return userRepo.saveAll(userMono).next();
}
③使用 RxJava 类型
@GetMapping("/recent")
public Observable<User> recentRegisterRecords() {
return userService.getRecentRegisterRecords();
}
@GetMapping("/{id}")
public Single<User> userById(@PathVariable("id") Long id) {
return userService.findUser(id);
}
2、定义函数式请求处理程序
类型 | 说明 |
---|---|
RequestPredicate | 声明将会被处理的请求类型 |
RouteFunction | 声明一个匹配的请求应该如何被路由到处理代码中 |
ServerRequest | 表示 HTTP 请求,包括对头和正文信息的访问 |
ServerResponse | 表示 HTTP 响应,包括头和正文信息 |
下面是一个将所有类型组合在一起:
package demo;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
import static reactor.core.publisher.Mono.just;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
@Configuration
public class RouterFunctionConfig {
@Bean
public RouterFunction<?> helloRouterFunction() {
return route(GET("/hello"), request -> ok().body(just("Hello World!"), String.class));
}
}
上面写了一个只处理一种请求的路由器函数。如果要处理不同类型的请求,不用编写另一个@Bean。
如:
@Bean
public RouterFunction<?> helloRouterFunction() {
return route(GET("/hello"), request -> ok().body(just("Hello World!"), String.class))
.andRoute(GET("/bye"),request -> ok().body(just("See ya!"), String.class));
}
post请求:
@Bean
public RouterFunction<?> routerFunction() {
return route(GET("/hello"), request -> ok().body(just("Hello!"), String.class))
.andRoute(POST("/bye"),request -> ok().body(just("See ya!"), String.class));
}
当比较简短时,可以用lambdas,最好是单独在一个方法里,如:
// get请求/hello 由lambdas处理
@Bean
public RouterFunction<?> routerFunction() {
return route(GET("/hello"), request -> ok().body(just("Hello!"), String.class))
.andRoute(POST("/bye"),this::bye);
}
// post请求/bye 有bye方法处理
public Mono<ServerResponse> bye(ServerRequest request) {
return ServerResponse.ok()
.body(just("See ya!"), String.class)
}
3、测试
WebTestClient。
它可以用于测试任何类型的HTTP方法,包括GET、POST、PUT、Patch、DELETE和HEAD请求。
HTTP 方法 | WebTestClient方法 |
---|---|
GET | .get() |
POST | .post() |
PUT | .put() |
PATCH | .patch() |
DELETE | .delete() |
HEAD | .head() |
1)测试get请求,如:
@Test
public void helloControllerTest() {
// 创建测试数据
User[] users = {
testUser("a"), testUser("b"),
testUser("c"), testUser("d")};
Flux<User> userFlux = Flux.just(users);
// Mocks UserRepository
UserRepository userRepo = Mockito.mock(UserRepository .class);
when(userRepo .findAll()).thenReturn(userFlux);
// 创建一个 WebTestClient
WebTestClient testClient = WebTestClient.bindToController(
new HelloController(userRepo ))
.build();
testClient.get().uri("/users/recent")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$").isArray()
.jsonPath("$").isNotEmpty()
// ...
.jsonPath("$[0].id").isEqualTo(users[0].getId().toString());
// ...
}
2)在Netty或者Tomcat等服务器 测试 web flux。
@RunWith
和@SpringBootTest
注释测试类,就像任何其他SpringBoot集成测试一样。
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
public class DesignTacoControllerWebTest {
@Autowired
private WebTestClient testClient;
}
@Test
public void recentUsers() throws IOException {
testClient.get().uri("/user/recent")
.accept(MediaType.APPLICATION_JSON).exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[?(@.id == 'Usera')].name")
.isEqualTo("zhangsan");
}
4、响应式消费rest api
在spring3.0版本引入RestTemplate,之前用RestTemplate来发请求。RestTemple请求处理的都是非响应式领域类型和集合。
如果想以响应式的方式处理响应的数据,需要用Flux或Mono来包装。
如果已经有一个Flux或Mono,并且希望在POST或PUT请求中发送它,需要在请求之前将数据提取非响应式中。
spring5有WebClient
作为RestTemplate
的响应式替代。
创建WebClient的实例(或注入WebClient bean)
指定要发送的请求的HTTP方法
指定请求中URI和头信息
提交请求
消费响应
1)、获取资源
构建请求,获取响应并抽取一个会发布Ingredient对象的Mono。
Mono<Ingredient> ingredient = WebClient.create()
.get()
.uri("http://localhost:8080/ingredients/{id}", ingredientId)
.retrieve()
.bodyToMono(Ingredient.class);
ingredient.subscribe(i -> { //... })
在这里,使用create()创建一个新的WebClient实例,
然后使用get()和uri()定义一个GET请求,以http://localhost:8080/ingredients/{id},
其中{id}占位符将被ingredientId中的值替换。 然后,retrieve()会执行请求。
最后,调用bodyToMono()将响应体body的有效载荷提取到Mono<Ingredient>中,就可以继续应用添加Mono操作。
需要注意的是,要在请求发送之前对其进行订阅。
发送请求获取值的集合,如获取所有配料:
Flux<Ingredient> ingredients = WebClient.create()
.get()
.uri("http://localhost:8080/ingredients")
.retrieve()
.bodyToFlux(Ingredient.class);
ingredients.subscribe(i -> {// ... })
上面使用bodyToFlux()将响应体抽取为Flux。与bodyToMono()类似,在数据流过之前,对Flux添加一些额外操作(过滤、映射)。注意,要订阅结果所形成的Flux,否则请求将始终不会发送。
①使用基础URI发送请求
一个通用的基础URI,如:
@Bean
public WebClient webClient() {
return WebClient.create("http://localhost:8080");
}
然后,在要使用基础URI的地方,将WebClient Bean注入进来,如下:
因为,WebClient已经创建好了,可以通过get()方法直接使用。指定相对于URI的相对路径即可。
@Autowired
WebClient webClient;
public Mono<Ingredient> getIngredientById(String ingredientId) {
Mono<Ingredient> ingredient = webClient
.get()
.uri("/ingredients/{id}", ingredientId)
.retrieve()
.bodyToMono(Ingredient.class);
ingredient.subscribe(i -> {// ... })
}
②对长时间运行的请求进行超时处理
为了避免客户端请求被缓慢的网络或服务阻塞,可以使用Flux或Mono的timeout方法,来限制等待数据发布的过程设置一个时长限制。如:
Flux<Ingredient> ingredients = WebClient.create()
.get()
.uri("http://localhost:8080/ingredients")
.retrieve()
.bodyToFlux(Ingredient.class);
ingredients
.timeout(Duration.ofSeconds(1))
.subscribe(
i -> { // ... },
e -> {
// handle timeout error
})
在订阅flux之前,调用了timeout()方法。将持续时间设置成1秒。如果请求在1秒内返回,就不会有问题。否则就会超时,第二个错误参数将被调用。
2)、发送资源
使用WebClient发送数据与接收资源没有太大的差异。如:如果有一个Mono,想要将Mono发布的Ingredient对象以POST请求发送到相对路径“/ingredients”的URI上。就是用Post()方法替换get(),并通过body()方法指明要使用Mono来填充请求体:
Mono<Ingredient> ingredientMono = ...;
Mono<Ingredient> result = webClient
.post()
.uri("/ingredients")
.body(ingredientMono, Ingredient.class)
.retrieve()
.bodyToMono(Ingredient.class);
result.subscribe(i -> { ... })
如果没有要发送的Mono或者Flux,而只有原始的领域对象,可以使用syncBody()方法。
如:
Ingedient ingredient = ...;
Mono<Ingredient> result = webClient
.post()
.uri("/ingredients")
.syncBody(ingredient)
.retrieve()
.bodyToMono(Ingredient.class);
result.subscribe(i -> { ... })
如果想PUT请求更新一个Ingredient,可以使用put(),来替换post,并调整URI路径。
Mono<Void> result = webClient
.put()
.uri("/ingredients/{id}", ingredient.getId())
.syncBody(ingredient)
.retrieve()
.bodyToMono(Void.class)
.subscribe();
put()请求的响应载荷一般是空的。一旦订阅改Mono,请求就会立即发送。
3)、删除资源
WebClient还支持通过delete()方法删除资源。如:
Mono<Void> result = webClient
.delete()
.uri("/ingredients/{id}", ingredientId)
.retrieve()
.bodyToMono(Void.class)
.subscribe();
与put类似,delete请求的响应也不会有载荷。
4)、处理错误
没有400或500状态代码的响应。 如果返回任何一种错误状态,WebClient会记录失败信息。
如果要处理这些错误,可以调用onStatus()
。
Mono<Ingredient> ingredientMono = webClient
.get()
.uri("http://localhost:8080/ingredients/{id}", ingredientId)
.retrieve()
.bodyToMono(Ingredient.class);
// 如果找到了资源,则subscribe的第一个lambda(数据消费者)将会被调用,并且将匹配的Ingredient对象传递过来。
// 如果找不到资源,将会得到一个HTTP 404的状态码响应。导致第二个lambda表达式(错误消费者)被调用。并且会传递一个WebClientResponseException。
ingredientMono.subscribe(
ingredient -> {
// handle the ingredient data
...
},
error-> {
// deal with the error
...
});
WebClientResponseException无法明确指出导致Mono失败的原因。
可以自定义一个错误处理器
,通过添加自定义错误处理程序,在其中提供将状态代码转换为自己选择的Throwable的代码。
如, 一个请求资源的失败时,生成一个UnknownIngredientException
的异常。在调用retrieve()之后,添加一个on Status():
Mono<Ingredient> ingredientMono = webClient
.get()
.uri("http://localhost:8080/ingredients/{id}", ingredientId)
.retrieve()
//的第一个参数是断言,接受一个HttpStatus,如果状态码是我们想要处理的,就将返回true。
// 如果是,响应会传递给第二个参数的函数并按需进行处理。最终返回Throwable的Mono。
// 这里状态码400级别的,这回导致ingredientMono因为该异常而失败。
.onStatus(HttpStatus::is4xxClientError,
response -> Mono.just(new UnknownIngredientException()))
.bodyToMono(Ingredient.class);
如果想要更精确的检查http404状态,可以这样:
Mono<Ingredient> ingredientMono = webClient
.get()
.uri("http://localhost:8080/ingredients/{id}", ingredientId)
.retrieve()
.onStatus(status -> status == HttpStatus.NOT_FOUND,
response -> Mono.just(new UnknownIngredientException()))
.bodyToMono(Ingredient.class);
顺便说一句,可以按需调用onStatus()任意多次,来处理响应中可能返回的各种Http状态码。
5)、交换请求
exchange()还可以访问响应的头信息或cookie的值。
exchange()和retrieve()相似之处
:
exchange:
Mono<Ingredient> ingredientMono = webClient
.get()
.uri("http://localhost:8080/ingredients/{id}", ingredientId)
.exchange()
.flatMap(cr -> cr.bodyToMono(Ingredient.class));
retrieve:
Mono<Ingredient> ingredientMono = webClient
.get()
.uri("http://localhost:8080/ingredients/{id}", ingredientId)
.retrieve()
.bodyToMono(Ingredient.class);
将ClientResponse映射为Mono,这样扁平化为最终现有的Mono。
exchange()和retrieve()差异
:
有这么一个场景:如果请求的响应中包含一个名为X_UNAVAILABLE的头信息,如果它的值为true,则表明该配料是不可用的。
假设头信息存在,那么希望得到的Mono是空的,不返回任何内容,可以通过添加另一个flatMap()调用来实现:
Mono<Ingredient> ingredientMono = webClient
.get()
.uri("http://localhost:8080/ingredients/{id}", ingredientId)
.exchange()
.flatMap(cr -> {
if (cr.headers().header("X_UNAVAILABLE").contains("true")) {
return Mono.empty();
}
return Mono.just(cr);
})
.flatMap(cr -> cr.bodyToMono(Ingredient.class));
新的flatMap()调用会看给定的ClientrRequest对象的响应头,查看是否存在值为true的X_UNAVILABLE头信息。如果有,就将返回一个空的Mono;否则,返回一个包含CientResponse的新Mono。不管是那种情况,返回的Mono都会扁平化为下一个flatMap()操作所要使用的Mono。
5、保护响应式WebAPI
使用Spring WebFlux编写Web应用,不一定会用到Servlet。响应式Web还有可能构建在Netty或者其他非Servlet容器上。在保护Spring WebFlux应用,Servlet Fileter不是可行方案。
从spring5.0开始,SpringSecurity既能保护基于Servlet的SpringMVC,又能保护响应式的SpringWebFlux应用。
因为使用了Spring的WebFiler,Spring借鉴Servlet Filter类似方法,又不依赖于Servlet API。
添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
1)配置响应式Web应用的安全
配置Spring Security以确保Spring MVC Web应用程序的安全通常涉及创建一个新的配置类,该配置类扩展Web SecurityConfigurerAdapter,并使用@EnableWebSecurity注解。 这样的配置类将重写configuration()方法,以指定Web安全细节。
如,简单Spring Security配置 非响应式Spring MVC应用配置安全性:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/design", "/orders").hasAuthority("USER")
.antMatchers("/**").permitAll();
}
}
在响应式Spring WebFlux应用中改成:
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http) {
return http
.authorizeExchange()
.pathMatchers("/design", "/orders").hasAuthority("USER")
.anyExchange().permitAll()
.and()
.build();
}
}
上面这个类没有用到@EnableWebSecurity
注解。而且,配置类也没有扩展WebSecurityConfigurerAdapter
或其他基类,所以也没重写configure()。
为了取代cofigure()方法,这里用了SecurityWebFilterChain()
方法声明了一个SecurityWebFilterChain
类型的Bean。
配置是通过给定的ServerHttpSecurity
对象声明的,而不是用HttpSecurity
对象。用ServerHttpSecurity ,可以调用authorizeExchange(),大致等价于authorizeRequests(),来声明请求级的安全性。
ServerHttpSecurity是Spring Security5新引入的,在响应式编程中它模拟了HTTPSecurity的功能。
2)配置响应式的用户详情服务
在扩展WebSecurityConfigurerAdapter 时候,会重写configure()方法来声明安全规则。还会重写configure()方法来配置认证逻辑。
如通常定义一个UserDetails:
@Autowired
UserRepository userRepo;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepo.findByUsername(username)
if (user == null) {
throw new UsernameNotFoundException(
username " + not found")
}
return user.toUserDetails();
}
});
}
在上面这个非响应式的配置中,重写了UserDetailsService。最终返回了UserDetails对象。
在响应式的安全配置中,不再重写configure()方法,而是声明一个ReactiveUserDetailsService
bean。ReactiveUserDetailsService是UserDetailsService的响应式等价形式。与UserDetailsService类似,ReactiveUserDetailsService 只要实现一个方法。Mono<UserDetails>的findByUsername()方法。
@Service
public ReactiveUserDetailsService userDetailsService(UserRepository userRepo) {
return new ReactiveUserDetailsService() {
@Override
public Mono<UserDetails> findByUsername(String username) {
return userRepo.findByUsername(username)
.map(user -> {
return user.toUserDetails();
});
}
};
}