学习总结-spring webflux

spring webflux背压机制

https://www.baeldung.com/spring-webflux-backpressure
由于TCP本身的限制,相当于publisher<->TCP<->subscriber三者间的背压处理,借助request/limit/cancel实现
在这里插入图片描述

spring oauth2.0

https://www.baeldung.com/spring-webclient-oauth2
https://www.baeldung.com/spring-oauth-login-webflux

  1. 多oauth认证下webclient使用
  2. 如何获取认证信息
    在这里插入图片描述

provider: 资源提供者
Client Registration:要求认证的客户端

自定义从session中获取参数

参考WebSession/WebSessionMethodArgumentResolver和RequestBody/RequestBodyMethodArgumentResolver
主要是基于HandlerMethodArgumentResolver/WebFluxConfigurer,

@Configuration
public class WebfluxConfiguration implements WebFluxConfigurer {
	@Override
    public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
        configurer.addCustomResolver(userMethodArgumentResolver);
    }
}

CORS设置

https://www.baeldung.com/spring-webflux-cors
重点WebFluxConfigurer接口

非springboot方式启动

@ComponentScan(basePackages = {"com.baeldung.reactive.security"})
@EnableWebFlux
public class SpringSecurity5Application {

    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext context =
                     new AnnotationConfigApplicationContext(SpringSecurity5Application.class)) {
            context.getBean(DisposableServer.class).onDispose().block();
        }
    }

    @Bean
    public DisposableServer disposableServer(ApplicationContext context) {
        HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context)
                .build();
        ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
        HttpServer httpServer = HttpServer.create().host("localhost").port(8083);
        return httpServer.handle(adapter).bindNow();
    }

}

webflux线程模型

https://www.baeldung.com/spring-webflux-concurrency

  • We can use publishOn with a Scheduler anywhere in the chain, with that Scheduler affecting all the subsequent operators.
    影响所有后续的operator,如map/doOnNext/subscribe

  • While we can also use subscribeOn with a Scheduler anywhere in the chain, it will only affect the context of the source of emission.
    仅影响数据源的分发,如flatMap

url构建

1.UrlBuilder

会默认进行iso-8859-1编码

  • path参数
this.webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/{id}/attributes/{attributeId}")
    .build(2, 13))
  .retrieve();
verifyCalledUrl("/products/2/attributes/13");
  • get参数
this.webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("color", "black")
    .queryParam("category", "Phones", "Tablets")
    .build())
  .retrieve();
verifyCalledUrl("/products/?color=black&category=Phones&category=Tablets");

this.webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("name", "{title}")
    .queryParam("color", "{authorId}")
    .queryParam("deliveryDate", "{date}")
    .build("AndroidPhone", "black", "13/04/2019"))
    .queryParam("category", String.join(",", "Phones", "Tablets"))
  .retrieve();
verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13%2F04%2F2019&category=Phones,Tablets");

2.编码模式

TEMPLATE_AND_VALUES: Pre-encode the URI template and strictly encode URI variables when expanded
VALUES_ONLY: Do not encode the URI template, but strictly encode URI variables after expanding them into the template
URI_COMPONENTS: Encode URI component value after expending URI variables
NONE: No encoding will be applied

The default value is TEMPLATE_AND_VALUES

DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.URI_COMPONENT);
this.webClient = WebClient
  .builder()
  .uriBuilderFactory(factory)
  .baseUrl(BASE_URL)
  .exchangeFunction(exchangeFunction)
  .build();
3.URL match

https://www.baeldung.com/spring-5-mvc-url-matching
PathPatternParser & AntPathMatcher

3.1. spring5 webflux采用PathPatternParser进行url match
//Whatever we give in the path after “/spring5” will be stored in the path variable “id”
// 注意包括‘/’
@GetMapping("/spring5/{*id}")
public String URIVariableHandler(@PathVariable String id) {
    return id;
}

@Test
public void whenMultipleURIVariablePattern_thenGotPathVariable() {
        
    client.get()
      .uri("/spring5/baeldung/tutorial")
      .exchange()
      .expectStatus()
      .is2xxSuccessful()
      .expectBody()
      .equals("/baeldung/tutorial");

    client.get()
      .uri("/spring5/baeldung")
      .exchange()
      .expectStatus()
      .is2xxSuccessful()
      .expectBody()
      .equals("/baeldung");
}

快速定义资源访问目录

private RouterFunction<ServerResponse> routingFunction() { 
    return RouterFunctions.resources(
      "/files/{*filepaths}", 
      new ClassPathResource("files/"))); 
}
3.2. 特殊字符
  • ‘?’ Matches Exactly One Character
    If we specify the path pattern as: “/t?st“, this will match paths like: “/test” and “/tast”, but not “/tst” and “/teest”.
  • ‘*’ Matches 0 or More Characters Within a Path Segment
private RouterFunction<ServerResponse> routingFunction() { 
    returnroute(
      GET("/baeldung/*Id"), 
      serverRequest -> ok().body(fromValue("/baeldung/*Id path was accessed"))); }

@Test
public void whenGetMultipleCharWildcard_thenGotPathPattern() 
  throws Exception {
      client.get()
        .uri("/baeldung/tutorialId")
        .exchange()
        .expectStatus()
        .isOk()
        .expectBody(String.class)
        .isEqualTo("/baeldung/*Id path was accessed");
}
  • ‘**’ Matches 0 or More Path Segments Until the End of the Path
private RouterFunction<ServerResponse> routingFunction() { 
    return RouterFunctions.resources(
      "/resources/**", 
      new ClassPathResource("resources/"))); 
}

@Test
public void whenAccess_thenGot() throws Exception {
    client.get()
      .uri("/resources/test/test.txt")
      .exchange()
      .expectStatus()
      .isOk()
      .expectBody(String.class)
      .isEqualTo("content of file test.txt");
}
  • ‘{baeldung:[a-z]+}’ Regex in Path Variable
    if our pattern is like “/{baeldung:[a-z]+}”, the value of path variable “baeldung” will be any path segment that matches the gives regex:
private RouterFunction<ServerResponse> routingFunction() { 
   return route(GET("/{baeldung:[a-z]+}"), 
     serverRequest ->  ok()
       .body(fromValue("/{baeldung:[a-z]+} was accessed and "
       + "baeldung=" + serverRequest.pathVariable("baeldung")))); 
}

@Test
public void whenGetRegexInPathVarible_thenGotPathVariable() 
 throws Exception {

     client.get()
       .uri("/abcd")
       .exchange()
       .expectStatus()
       .isOk()
       .expectBody(String.class)
       .isEqualTo("/{baeldung:[a-z]+} was accessed and "
         + "baeldung=abcd");
}
  • /{var1}_{var2}’ Multiple Path Variables in Same Path Segment
    仅限于下划线分隔符
private RouterFunction<ServerResponse> routingFunction() { 
 
    return route(
      GET("/{var1}_{var2}"),
      serverRequest -> ok()
        .body(fromValue( serverRequest.pathVariable("var1") + " , " 
        + serverRequest.pathVariable("var2"))));
 }

@Test
public void whenGetMultiplePathVaribleInSameSegment_thenGotPathVariables() 
  throws Exception {
      client.get()
        .uri("/baeldung_tutorial")
        .exchange()
        .expectStatus()
        .isOk()
        .expectBody(String.class)
        .isEqualTo("baeldung , tutorial");
}

webTestClient

1.Binding to a Server
WebTestClient testClient = WebTestClient
  .bindToServer()
  .baseUrl("http://localhost:8080")
  .build();
2.Binding to a Router
RouterFunction function = RouterFunctions.route(
  RequestPredicates.GET("/resource"),
  request -> ServerResponse.ok().build()
);

WebTestClient
  .bindToRouterFunction(function)
  .build().get().uri("/resource")
  .exchange()
  .expectStatus().isOk()
  .expectBody().isEmpty();
3.Binding to a Web Handler

WebHandler handler = exchange -> Mono.empty();
WebTestClient.bindToWebHandler(handler).build();

4.Binding to an Application Context

A more interesting situation occurs when we’re using the bindToApplicationContext method. It takes an ApplicationContext and analyses the context for controller beans and @EnableWebFlux configurations.

@Autowired
private ApplicationContext context;

WebTestClient testClient = WebTestClient.bindToApplicationContext(context)
  .build();
5.Binding to a Controller
@Autowired
private Controller controller;

WebTestClient testClient = WebTestClient.bindToController(controller).build();
6.Making a Request
WebTestClient
  .bindToServer()
    .baseUrl("http://localhost:8080")
    .build()
    .post()
    .uri("/resource")
  .exchange()
    .expectStatus().isCreated()
    .expectHeader().valueEquals("Content-Type", "application/json")
    .expectBody().jsonPath("field").isEqualTo("value");

webclient

1.Creating a WebClient Instance
WebClient client = WebClient.create();

WebClient client = WebClient.create("http://localhost:8080");

WebClient client = WebClient.builder()
  .baseUrl("http://localhost:8080")
  .defaultCookie("cookieKey", "cookieValue")
  .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) 
  .defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080"))
  .build();
2.设置超时

默认30s超时,自定义需要创建HttpClient

  • set the connection timeout via the ChannelOption.CONNECT_TIMEOUT_MILLIS option
  • set the read and write timeouts using a ReadTimeoutHandler and a WriteTimeoutHandler, respectively
  • configure a response timeout using the responseTimeout directive
HttpClient httpClient = HttpClient.create()
  .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
  .responseTimeout(Duration.ofMillis(5000))
  .doOnConnected(conn -> 
    conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
      .addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));

WebClient client = WebClient.builder()
  .clientConnector(new ReactorClientHttpConnector(httpClient))
  .build();

Note that while we can call timeout on our client request as well, this is a signal timeout, not an HTTP connection, a read/write, or a response timeout; it’s a timeout for the Mono/Flux publisher.

3.使用方式

the request spec variables (WebClient.UriSpec, WebClient.RequestBodySpec, WebClient.RequestHeadersSpec, WebClient.ResponseSpec)不能重用

  1. Define the Method

UriSpec uriSpec = client.method(HttpMethod.POST);

UriSpec uriSpec = client.post();

  1. Define the URL

RequestBodySpec bodySpec = uriSpec.uri("/resource");

RequestBodySpec bodySpec = uriSpec.uri(
uriBuilder -> uriBuilder.pathSegment("/resource").build());

RequestBodySpec bodySpec = uriSpec.uri(URI.create("/resource"));

  1. Define the Body

RequestHeadersSpec<?> headersSpec = bodySpec.bodyValue(“data”);

RequestHeadersSpec<?> headersSpec = bodySpec.body(
Mono.just(new Foo(“name”)), Foo.class);

RequestHeadersSpec<?> headersSpec = bodySpec.body(
BodyInserters.fromValue(“data”));

RequestHeadersSpec headersSpec = bodySpec.body(
BodyInserters.fromPublisher(Mono.just(“data”)),
String.class);

LinkedMultiValueMap map = new LinkedMultiValueMap();
map.add(“key1”, “value1”);
map.add(“key2”, “value2”);
RequestHeadersSpec<?> headersSpec = bodySpec.body(
BodyInserters.fromMultipartData(map));

The BodyInserter is an interface responsible for populating a ReactiveHttpOutputMessage body with a given output message and a context used during the insertion.
4. Define the Headers

ResponseSpec responseSpec = headersSpec.header(
    HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
  .accept(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)
  .acceptCharset(StandardCharsets.UTF_8)
  .ifNoneMatch("*")
  .ifModifiedSince(ZonedDateTime.now())
  .retrieve();
  1. Getting a Response
    The final stage is sending the request and receiving a response. We can achieve this by using either the exchangeToMono/exchangeToFlux or the retrieve method.

The exchangeToMono and exchangeToFlux methods allow access to the ClientResponse along with its status and headers:

Mono<String> response = headersSpec.exchangeToMono(response -> {
  if (response.statusCode()
    .equals(HttpStatus.OK)) {
      return response.bodyToMono(String.class);
  } else if (response.statusCode()
    .is4xxClientError()) {
      return Mono.just("Error response");
  } else {
      return response.createException()
        .flatMap(Mono::error);
  }
});

While the retrieve method is the shortest path to fetching a body directly
It’s important to pay attention to the ResponseSpec.bodyToMono method, which will throw a WebClientException if the status code is 4xx (client error) or 5xx (server error).

Mono<String> response = headersSpec.retrieve()
  .bodyToMono(String.class);

直接转发请求内容用byte[].class或者ByteBuffer

https://stackoverflow.com/questions/56584790/how-to-handle-spring-webclient-get-application-octet-stream-as-body-inputstream

@GetMapping(path="/health3", produces = {MediaType.APPLICATION_JSON_VALUE})
public Flux<byte[]> test3(){
    return rewardWebClient.get().uri("/sys/health")
            .retrieve().bodyToFlux(byte[].class);
}
4.过滤器

更改请求地址

ExchangeFilterFunction urlModifyingFilter = (clientRequest, nextFilter) -> {
    String oldUrl = clientRequest.url().toString();
    URI newUrl = URI.create(oldUrl + "/" + version);
    ClientRequest filteredRequest = ClientRequest.from(clientRequest)
      .url(newUrl)
      .build();
    return nextFilter.exchange(filteredRequest);
};

添加basic authentication

WebClient webClient = WebClient.builder()
  .filter(ExchangeFilterFunctions.basicAuthentication(user, password))
  .build();

如打印日志

@Bean
    public WebClient rewardWebClient(ObjectMapper objectMapper) {
        WebClient.Builder webClientBuilder = WebClient.builder();

        Jackson2JsonEncoder encoder = new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON);
        Jackson2JsonDecoder decoder = new Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON);

        return webClientBuilder
                .baseUrl(rewardServiceInnerDomain)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
                .defaultHeader(HttpHeaders.ACCEPT_CHARSET, "UTF-8")
                .codecs(clientCodecConfigure -> {
                    clientCodecConfigure.defaultCodecs().jackson2JsonEncoder(encoder);
                    clientCodecConfigure.defaultCodecs().jackson2JsonDecoder(decoder);
                    clientCodecConfigure.defaultCodecs().maxInMemorySize(10 * 1024 * 1024);
                })
                .filters(exchangeFilterFunctions -> {
                    exchangeFilterFunctions.add(log());
                })
                .build();
    }

    ExchangeFilterFunction log(){
        return (request, next) -> {
            StringBuilder requestHeaderBuilder = new StringBuilder();
            request.headers().forEach((name, values) -> {
                    requestHeaderBuilder.append(String.format("%s: %s", name, Joiner.on(",").join(values)));
                    requestHeaderBuilder.append("; ");
            });

            StringBuilder requestBuilder = new StringBuilder();
            requestBuilder.append("[Request] method=").append(request.method())
                    .append("; ")
                    .append("url=; ").append(request.url())
                    .append("; ")
                    .append("headers=").append(requestHeaderBuilder);
            final String reqStr = requestBuilder.toString();

            return next.exchange(request).flatMap(response -> {
                StringBuilder responseHeaderBuilder = new StringBuilder();
                response.headers().asHttpHeaders().forEach((name, values) -> {
                    responseHeaderBuilder.append(String.format("%s: %s", name, Joiner.on(",").join(values)));
                    responseHeaderBuilder.append("; ");
                });

                StringBuilder responseBuilder = new StringBuilder();
                responseBuilder.append("[Response] statusCode=").append(response.statusCode())
                        .append("; ")
                        .append("headers=").append(responseHeaderBuilder);

                log.info("{} {}", reqStr, responseBuilder);
                return Mono.just(response);
            });
        };
    }

spring mvc async & webflux对比

https://www.baeldung.com/spring-mvc-async-vs-webflux

  • 压测工具 apache ab
    ab - Apache HTTP server benchmarking tool
  • 延迟处理
@RestController
public class AsyncController {
    @GetMapping("/async_result")
    @Async
    public CompletableFuture getResultAsyc(HttpServletRequest request) {
        // sleep for 500 ms
        return CompletableFuture.completedFuture("Result is ready!");
    }
}
@RestController
public class WebFluxController {

    @GetMapping("/flux_result")
    public Mono getResult(ServerHttpRequest request) {
       return Mono.defer(() -> Mono.just("Result is ready!"))
         .delaySubscription(Duration.ofMillis(500));
    }
}

状态码

默认200
@GetMapping(
  value = "/ok",
  produces = MediaType.APPLICATION_JSON_UTF8_VALUE
)
public Flux<String> ok() {
    return Flux.just("ok");
}
注解方式
@GetMapping(
  value = "/no-content",
  produces = MediaType.APPLICATION_JSON_UTF8_VALUE
)
@ResponseStatus(HttpStatus.NO_CONTENT)
public Flux<String> noContent() {
    return Flux.empty();
}
Changing the Status Programmatically
@GetMapping(
  value = "/accepted",
  produces = MediaType.APPLICATION_JSON_UTF8_VALUE
)
public Flux<String> accepted(ServerHttpResponse response) {
    response.setStatusCode(HttpStatus.ACCEPTED);
    return Flux.just("accepted");
}
异常拦截时的
@GetMapping(
  value = "/bad-request"
)
public Mono<String> badRequest() {
    return Mono.error(new IllegalArgumentException());
}
@ResponseStatus(
  value = HttpStatus.BAD_REQUEST,
  reason = "Illegal arguments")
@ExceptionHandler(IllegalArgumentException.class)
public void illegalArgumentHandler() {
    // 
}
with ResponseEntity
@GetMapping(
  value = "/unauthorized"
)
public ResponseEntity<Mono<String>> unathorized() {
    return ResponseEntity
      .status(HttpStatus.UNAUTHORIZED)
      .header("X-Reason", "user-invalid")
      .body(Mono.just("unauthorized"));
}

@GetMapping
public Mono<ResponseEntity<String>> mono(){
    return Mono.just(ResponseEntity
            .status(HttpStatus.UNAUTHORIZED)
            .header("X-Reason", "user-invalid")
            .body("unauthorized"));
}
With Functionals Endpoints
@Bean
public RouterFunction<ServerResponse> notFound() {
    return RouterFunctions
      .route(GET("/statuses/not-found"),
         request -> ServerResponse.notFound().build());
}

异常处理

https://www.baeldung.com/spring-webflux-errors
异常处理

onErrorReturn

异常时,返回固定值

public Mono<ServerResponse> handleRequest(ServerRequest request) {
    return sayHello(request)
      .onErrorReturn("Hello Stranger")
      .flatMap(s -> ServerResponse.ok()
      .contentType(MediaType.TEXT_PLAIN)
      .bodyValue(s));
}
onErrorResume

异常时,自救

public Mono<ServerResponse> handleRequest(ServerRequest request) {
    return ServerResponse.ok()
      .body(sayHello(request)
      .onErrorResume(e -> Mono.error(new NameRequiredException(
        HttpStatus.BAD_REQUEST, 
        "username is required", e))), String.class);
}
全局异常处理
  • Customize the Global Error Response Attributes
@Component
public class GlobalErrorAttributes extends DefaultErrorAttributes{
    
    @Override
    public Map<String, Object> getErrorAttributes(ServerRequest request, 
      ErrorAttributeOptions options) {
        Map<String, Object> map = super.getErrorAttributes(
          request, options);
        map.put("status", HttpStatus.BAD_REQUEST);
        map.put("message", "username is required");
        return map;
    }

}
  • Implement the Global Error Handler
/**
In this example, we set the order of our global error handler to -2. This is to give it a higher priority than the DefaultErrorWebExceptionHandler which is registered at @Order(-1).
*/
@Component
@Order(-2)
public class GlobalErrorWebExceptionHandler extends 
    AbstractErrorWebExceptionHandler {

    // constructors

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

        return RouterFunctions.route(
          RequestPredicates.all(), this::renderErrorResponse);
    }

    private Mono<ServerResponse> renderErrorResponse(
       ServerRequest request) {

       Map<String, Object> errorPropertiesMap = getErrorAttributes(request, 
         ErrorAttributeOptions.defaults());

       return ServerResponse.status(HttpStatus.BAD_REQUEST)
         .contentType(MediaType.APPLICATION_JSON)
         .body(BodyInserters.fromValue(errorPropertiesMap));
    }
}

static content

By default, Spring Boot serves static content from the following locations:

/public
/static
/resources
/META-INF/resources

All files from these paths are served under the /[resource-file-name] path.

If we want to change the default path for Spring WebFlux, we need to add this property to our application.properties file:

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

Routing example

路径都是相对于classpath

@Bean
public RouterFunction<ServerResponse> htmlRouter(
  @Value("classpath:/public/index.html") Resource html) {
    return route(GET("/"), request
      -> ok().contentType(MediaType.TEXT_HTML).syncBody(html)
    );
}

Let’s see how to serve images from the src/main/resources/img directory using the /img/** path:

@Bean
public RouterFunction imgRouter() {
return RouterFunctions
.resources("/img/**", new ClassPathResource(“img/”));
}

webflux filter

  • WebFilter
  • HandlerFilterFunctions.

The main difference between them is that WebFilter implementations work for all endpoints and HandlerFilterFunction implementations will only work for Router-based ones.

websocket

https://github.com/eugenp/tutorials/blob/master/spring-5-reactive/src/main/java/com/baeldung/websocket/ReactiveJavaClientWebSocket.java

import java.net.URI;
import java.time.Duration;
import org.springframework.web.reactive.socket.WebSocketMessage;
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
import org.springframework.web.reactive.socket.client.WebSocketClient;

import reactor.core.publisher.Mono;

public class ReactiveJavaClientWebSocket {
	public static void main(String[] args) throws InterruptedException {
		  
        WebSocketClient client = new ReactorNettyWebSocketClient();
        client.execute(
          URI.create("ws://localhost:8080/event-emitter"), 
          session -> session.send(
            Mono.just(session.textMessage("event-spring-reactive-client-websocket")))
            .thenMany(session.receive()
              .map(WebSocketMessage::getPayloadAsText)
              .log())
            .then())
            .block(Duration.ofSeconds(10L));
    }

}

文件上传

@RestController
@RequestMapping("upload")
public class UploadController {
    private final Path basePath = Paths.get("upload");

    @PostMapping("file/single")
    public Mono<Void> upload(@RequestPart("userName") String name, @RequestPart("file") Mono<FilePart> filePartMono) {
        return filePartMono
                .doOnNext(fp -> System.out.println("received file: " + fp.filename()))
                .flatMap(fp -> fp.transferTo(basePath.resolve(fp.filename())))
                .then();
    }

    @PostMapping("file/body")
    public Mono<Void> upload(@RequestBody Flux<ByteBuffer> filePart) {
        // 请求体为上传内容
        return Mono.empty();
    }

    // use single FilePart for single file upload
    @PostMapping(value = "/upload-filePart", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_STREAM_JSON_VALUE)
    @ResponseStatus(value = HttpStatus.OK)
    public Mono<List<String>> upload(@RequestPart("file") FilePart filePart) {

        /*
          To see the response beautifully we are returning strings as Mono List
          of String. We could have returned Flux<String> from here.
          If you are curious enough then just return Flux<String> from here and
          see the response on Postman
         */
        return fileUploadService.getLines(filePart).collectList();
    }

    // use Mono<MultiValueMap<String, Part>> for both single and multiple file upload under `files` param key
    @PostMapping(value = "/upload-multiValueMap", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_STREAM_JSON_VALUE)
    @ResponseStatus(value = HttpStatus.OK)
    public Mono<List<String>> uploadFileMap(@RequestBody Mono<MultiValueMap<String, Part>> filePartMap) {
        /*
          To see the response beautifully we are returning strings as Mono List
          of String. We could have returned Flux<String> from here.
          If you are curious enough then just return Flux<String> from here and
          see the response on Postman
         */
        return fileUploadService.getLinesFromMap(filePartMap).collectList();
    }

    @PostMapping("file/multi")
    public Mono<Void> upload(@RequestPart("userName") String name, @RequestPart("files") Flux<FilePart> partFlux) {
        return partFlux
                .doOnNext(fp -> System.out.println("received file: " + fp.filename()))
                .flatMap(fp -> fp.transferTo(basePath.resolve(fp.filename())))
                .then();
    }
}

记录request/response日志

import com.crypto.reward.util.UrlUtil;
import com.google.common.base.Joiner;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.channels.Channels;
import java.util.List;

@Component
@Slf4j
public class LoggingWebFilter implements WebFilter {

    public static final List<String> UID_NA = List.of("N/A");

    @Override
    public @NotNull Mono<Void> filter(ServerWebExchange serverWebExchange,
                                      WebFilterChain webFilterChain) {

        ServerHttpRequest request = serverWebExchange.getRequest();

        log.info(
                "[Request] UID={}, METHOD={}, URL={}, queryParams={{}}",
                request.getHeaders().getOrDefault("X-CRYPTO-USER-UUID", UID_NA),
                request.getMethodValue(),
                request.getPath(),
                UrlUtil.toQueryString(request.getQueryParams())
        );

        if (log.isDebugEnabled()) {
            request.getHeaders().forEach((t, v) -> {
                log.debug("{}: {}", t, Joiner.on(",").join(v));
            });

            serverWebExchange = serverWebExchange.mutate()
                    .request(new LoggingServerHttpRequestDecorator(serverWebExchange.getRequest()))
                    .build();
        }

        final ServerWebExchange exchange = serverWebExchange;
        serverWebExchange.getResponse().beforeCommit(() -> {
            log.info("[Response] code={}", exchange.getResponse().getRawStatusCode());
            return Mono.empty();
        });

        return webFilterChain.filter(serverWebExchange);
    }

    private class LoggingServerHttpRequestDecorator extends ServerHttpRequestDecorator{

        public LoggingServerHttpRequestDecorator(ServerHttpRequest delegate) {
            super(delegate);
        }

        String requestBody = "";

        @Override
        public Flux<DataBuffer> getBody() {
            return super.getBody().doOnNext(dataBuffer -> {
                try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
                    Channels.newChannel(byteArrayOutputStream).write(dataBuffer.asByteBuffer().asReadOnlyBuffer());
                    requestBody = IOUtils.toString(byteArrayOutputStream.toByteArray(), "UTF-8");
                    log.debug(requestBody);
                } catch (IOException e) {
                    log.error("fail to log incoming http request", e);
                }
            });
        }
    }
}

测试用例中断导致redis 6379占用

在这里插入图片描述
找到相关进程kill -9

ps -ef|grep 6379

url接口的测试

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.reactive.server.EntityExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;

import static org.junit.jupiter.api.Assertions.assertNotEquals;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient(timeout = "PT1S")// 超时时间
@WithMockUser
public class TestApiSwitchWebFilter {
    @Autowired
    private WebTestClient webTestClient;
    @Autowired
    private ObjectMapper objectMapper;
    /**
  	* 自定义头部
  	*/
	@Bean
    public WebTestClientBuilderCustomizer headersWebTestClientBuilder(){
        return new WebTestClientBuilderCustomizer() {
            @Override
            public void customize(WebTestClient.Builder builder) {
                builder.defaultHeader(KongWebFilter.USER_UUID, "964d32da-c49e-41b8-8209-8293398c9974")
                        .defaultHeader(KongWebFilter.PREFERRED_LOCALE, Locale.CHINA.toString())
                        .defaultHeader(KongWebFilter.BASE_CURRENCY, Currency.getInstance(Locale.CHINA).getCurrencyCode())
                        .defaultHeader(KongWebFilter.APP_NAME, "testApp");
            }
        };
    }

    @Test
    public void testGet() {
    	UUID appUserId = UUID.fromString("5e948e0b-f6b3-4823-b4af-fc64bb41fa10");
        String appName = "testApp";
        /*
        不想额外解析的话用如下方式:
		var typeReference = new ParameterizedTypeReference<Result<UserProfile>>() {};
		webTestClient.expectBody(typeReference)
		*/
        EntityExchangeResult<String> result = webTestClient.get()
                .uri(uriBuilder ->
                        uriBuilder.path("/api/user_profiles/{uid}")
                                .queryParam("app_name", appName)
                                .build(appUserId))
                .header(KongWebFilter.USER_UUID, appUserId.toString())
                .header(KongWebFilter.PREFERRED_LOCALE, Locale.CHINA.toString())
                .header(KongWebFilter.BASE_CURRENCY, Currency.getInstance(Locale.CHINA).getCurrencyCode())
                .header(KongWebFilter.APP_NAME, appName)
                .exchange()
                .expectStatus().isOk()
                .expectBody(String.class)
                .returnResult();

        Result<UserProfile> profileResult = objectMapper.readValue(result.getResponseBody(),
                new TypeReference<Result<UserProfile>>() {});

		assertEquals(profileResult.getData().getAppUserId(), appUserId.toString());
    }

	@Test
    public void testPost() throws JsonProcessingException {
        UUID appUserId = UUID.randomUUID();
        UserProfileDto dto = UserProfileDto.builder()
                .name("crypto-1")
                .uid(appUserId.toString())
                .appName(DEFAULT_APP_NAME)
                .build();

        EntityExchangeResult<String> result = webTestClient.post()
                .uri("/api/user_profiles")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .body(Mono.just(dto), UserProfileDto.class)
                .exchange()
                .expectStatus().isOk()
                .expectBody(String.class)
                .returnResult();

        Result<UserProfile> profileResult = objectMapper.readValue(result.getResponseBody(),
                new TypeReference<Result<UserProfile>>() {});

        assertEquals(profileResult.getData().getAppUserId(), appUserId.toString());
    }

}

返回特定值或者forward请求

@Component
public class ApiSwitchWebFilter implements WebFilter {
    @Autowired
    private ObjectMapper objectMapper;

    @SneakyThrows
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        if (check(exchange)) {
            // method 1
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.NOT_FOUND);
            response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

            Result<String> result = Result.ofSuccess("it's developing!");

            DataBuffer dataBuffer = response.bufferFactory().wrap(objectMapper.writeValueAsBytes(result));
            return response.writeWith(Mono.just(dataBuffer));
            // method 2
//        return chain.filter(exchange.mutate().request(exchange.getRequest().mutate().path("/switchOff").build()).build());
        }

        return chain.filter(exchange);
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值