使用总结-
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
- 多oauth认证下webclient使用
- 如何获取认证信息
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)不能重用
- Define the Method
UriSpec uriSpec = client.method(HttpMethod.POST);
UriSpec uriSpec = client.post();
- Define the URL
RequestBodySpec bodySpec = uriSpec.uri("/resource");
RequestBodySpec bodySpec = uriSpec.uri(
uriBuilder -> uriBuilder.pathSegment("/resource").build());
RequestBodySpec bodySpec = uriSpec.uri(URI.create("/resource"));
- 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();
- 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);
}
}