springboot学习25

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();
			 });
		 }
	 };
}

猜你喜欢

转载自blog.csdn.net/tongwudi5093/article/details/113849884