From 88912b2557620fbeafc5a4263298af35f38b7814 Mon Sep 17 00:00:00 2001 From: seanyu Date: Wed, 15 Mar 2023 23:02:14 +0800 Subject: [PATCH] add webclient support --- .../MetadataTransferAutoConfiguration.java | 24 +++++ .../EncodeTransferMedataWebClientFilter.java | 95 +++++++++++++++++++ .../metadata/core/TransHeadersTransfer.java | 27 ++++++ ...RateLimitRuleArgumentReactiveResolver.java | 3 +- .../ratelimit-callee-service/pom.xml | 5 + .../service/callee/BusinessController.java | 78 ++++++++++++++- .../callee/CustomLabelResolverReactive.java | 44 +++++++++ .../callee/RateLimitCalleeService.java | 8 ++ 8 files changed, 277 insertions(+), 7 deletions(-) create mode 100644 spring-cloud-starter-tencent-metadata-transfer/src/main/java/com/tencent/cloud/metadata/core/EncodeTransferMedataWebClientFilter.java create mode 100644 spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/CustomLabelResolverReactive.java diff --git a/spring-cloud-starter-tencent-metadata-transfer/src/main/java/com/tencent/cloud/metadata/config/MetadataTransferAutoConfiguration.java b/spring-cloud-starter-tencent-metadata-transfer/src/main/java/com/tencent/cloud/metadata/config/MetadataTransferAutoConfiguration.java index 8f0159d80..2a1b8f236 100644 --- a/spring-cloud-starter-tencent-metadata-transfer/src/main/java/com/tencent/cloud/metadata/config/MetadataTransferAutoConfiguration.java +++ b/spring-cloud-starter-tencent-metadata-transfer/src/main/java/com/tencent/cloud/metadata/config/MetadataTransferAutoConfiguration.java @@ -28,6 +28,7 @@ import com.tencent.cloud.metadata.core.DecodeTransferMetadataServletFilter; import com.tencent.cloud.metadata.core.EncodeTransferMedataFeignInterceptor; import com.tencent.cloud.metadata.core.EncodeTransferMedataRestTemplateInterceptor; import com.tencent.cloud.metadata.core.EncodeTransferMedataScgFilter; +import com.tencent.cloud.metadata.core.EncodeTransferMedataWebClientFilter; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; @@ -39,6 +40,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; import static jakarta.servlet.DispatcherType.ASYNC; import static jakarta.servlet.DispatcherType.ERROR; @@ -140,4 +142,26 @@ public class MetadataTransferAutoConfiguration { }); } } + + /** + * Create when WebClient.Builder exists. + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = { "reactor.core.publisher.Mono", "reactor.core.publisher.Flux" }) + protected static class MetadataTransferWebClientConfig { + @Autowired(required = false) + private List webClientBuilder = Collections.emptyList(); + + @Bean + public EncodeTransferMedataWebClientFilter encodeTransferMedataWebClientFilter() { + return new EncodeTransferMedataWebClientFilter(); + } + + @Bean + public SmartInitializingSingleton addEncodeTransferMetadataFilterForWebClient(EncodeTransferMedataWebClientFilter filter) { + return () -> webClientBuilder.forEach(webClient -> { + webClient.filter(filter); + }); + } + } } diff --git a/spring-cloud-starter-tencent-metadata-transfer/src/main/java/com/tencent/cloud/metadata/core/EncodeTransferMedataWebClientFilter.java b/spring-cloud-starter-tencent-metadata-transfer/src/main/java/com/tencent/cloud/metadata/core/EncodeTransferMedataWebClientFilter.java new file mode 100644 index 000000000..65e7eba9c --- /dev/null +++ b/spring-cloud-starter-tencent-metadata-transfer/src/main/java/com/tencent/cloud/metadata/core/EncodeTransferMedataWebClientFilter.java @@ -0,0 +1,95 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + */ + +package com.tencent.cloud.metadata.core; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Map; + +import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.cloud.common.metadata.MetadataContextHolder; +import com.tencent.cloud.common.util.JacksonUtils; +import com.tencent.cloud.metadata.util.DefaultTransferMedataUtils; +import reactor.core.publisher.Mono; + +import org.springframework.util.CollectionUtils; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeFunction; + +import static com.tencent.cloud.common.constant.ContextConstant.UTF_8; +import static com.tencent.cloud.common.constant.MetadataConstant.HeaderName.CUSTOM_DISPOSABLE_METADATA; +import static com.tencent.cloud.common.constant.MetadataConstant.HeaderName.CUSTOM_METADATA; +import static com.tencent.cloud.common.constant.MetadataConstant.HeaderName.DEFAULT_DISPOSABLE_METADATA; + +/** + * web client filter used for writing metadata in HTTP request header. + * + * @author sean yu + */ +public class EncodeTransferMedataWebClientFilter implements ExchangeFilterFunction { + + @Override + public Mono filter(ClientRequest clientRequest, ExchangeFunction next) { + MetadataContext metadataContext = MetadataContextHolder.get(); + Map customMetadata = metadataContext.getCustomMetadata(); + Map disposableMetadata = metadataContext.getDisposableMetadata(); + Map transHeaders = metadataContext.getTransHeadersKV(); + Map defaultMetadata = DefaultTransferMedataUtils.getDefaultTransferMedata(); + + ClientRequest.Builder requestBuilder = ClientRequest.from(clientRequest); + + this.buildMetadataHeader(requestBuilder, customMetadata, CUSTOM_METADATA); + this.buildMetadataHeader(requestBuilder, disposableMetadata, CUSTOM_DISPOSABLE_METADATA); + this.buildMetadataHeader(requestBuilder, defaultMetadata, DEFAULT_DISPOSABLE_METADATA); + this.buildTransmittedHeader(requestBuilder, transHeaders); + + ClientRequest request = requestBuilder.build(); + + TransHeadersTransfer.transfer(request); + return next.exchange(request); + } + + private void buildTransmittedHeader(ClientRequest.Builder requestBuilder, Map transHeaders) { + if (!CollectionUtils.isEmpty(transHeaders)) { + transHeaders.forEach(requestBuilder::header); + } + } + + + /** + * Set metadata into the request header for {@link ClientRequest} . + * @param requestBuilder instance of {@link ClientRequest.Builder} + * @param metadata metadata map . + * @param headerName target metadata http header name . + */ + private void buildMetadataHeader(ClientRequest.Builder requestBuilder, Map metadata, String headerName) { + if (!CollectionUtils.isEmpty(metadata)) { + String encodedMetadata = JacksonUtils.serialize2Json(metadata); + try { + requestBuilder.header(headerName, URLEncoder.encode(encodedMetadata, UTF_8)); + } + catch (UnsupportedEncodingException e) { + requestBuilder.header(headerName, encodedMetadata); + } + } + } + +} diff --git a/spring-cloud-starter-tencent-metadata-transfer/src/main/java/com/tencent/cloud/metadata/core/TransHeadersTransfer.java b/spring-cloud-starter-tencent-metadata-transfer/src/main/java/com/tencent/cloud/metadata/core/TransHeadersTransfer.java index a285500a6..b3ee8ae71 100644 --- a/spring-cloud-starter-tencent-metadata-transfer/src/main/java/com/tencent/cloud/metadata/core/TransHeadersTransfer.java +++ b/spring-cloud-starter-tencent-metadata-transfer/src/main/java/com/tencent/cloud/metadata/core/TransHeadersTransfer.java @@ -32,6 +32,7 @@ import jakarta.servlet.http.HttpServletRequest; import org.springframework.http.HttpHeaders; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.CollectionUtils; +import org.springframework.web.reactive.function.client.ClientRequest; /** * According to request and trans-headers(key list in string type) in metadata, build @@ -96,4 +97,30 @@ public final class TransHeadersTransfer { } } } + + /** + * According to {@link ClientRequest} and trans-headers(key list in string type) in metadata, build + * the complete headers(key-value list in map type) into metadata. + * @param clientRequest + */ + public static void transfer(ClientRequest clientRequest) { + // transHeaderMetadata: for example, {"trans-headers" : {"header1,header2,header3":""}} + Map transHeaderMetadata = MetadataContextHolder.get().getTransHeaders(); + if (!CollectionUtils.isEmpty(transHeaderMetadata)) { + String transHeaders = transHeaderMetadata.keySet().stream().findFirst().orElse(""); + String[] transHeaderArray = transHeaders.split(","); + HttpHeaders headers = clientRequest.headers(); + Set headerKeys = headers.keySet(); + for (String httpHeader : headerKeys) { + Arrays.stream(transHeaderArray).forEach(transHeader -> { + if (transHeader.equals(httpHeader)) { + List list = headers.get(httpHeader); + String httpHeaderValue = JacksonUtils.serialize2Json(list); + // for example, {"trans-headers-kv" : {"header1":"v1","header2":"v2"...}} + MetadataContextHolder.get().setTransHeadersKV(httpHeader, httpHeaderValue); + } + }); + } + } + } } diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/resolver/RateLimitRuleArgumentReactiveResolver.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/resolver/RateLimitRuleArgumentReactiveResolver.java index ec4886948..724a5eab6 100644 --- a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/resolver/RateLimitRuleArgumentReactiveResolver.java +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/resolver/RateLimitRuleArgumentReactiveResolver.java @@ -30,7 +30,6 @@ import java.util.stream.Collectors; import com.tencent.cloud.common.metadata.MetadataContextHolder; import com.tencent.cloud.polaris.context.ServiceRuleManager; -import com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckServletFilter; import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelReactiveResolver; import com.tencent.polaris.ratelimit.api.rpc.Argument; import com.tencent.polaris.specification.api.v1.traffic.manage.RateLimitProto; @@ -51,7 +50,7 @@ import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata */ public class RateLimitRuleArgumentReactiveResolver { - private static final Logger LOG = LoggerFactory.getLogger(QuotaCheckServletFilter.class); + private static final Logger LOG = LoggerFactory.getLogger(RateLimitRuleArgumentReactiveResolver.class); private final ServiceRuleManager serviceRuleManager; diff --git a/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/pom.xml b/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/pom.xml index 4bbda499b..613cf24f7 100644 --- a/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/pom.xml +++ b/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/pom.xml @@ -18,6 +18,11 @@ spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-webflux + + com.tencent.cloud spring-cloud-starter-tencent-polaris-discovery diff --git a/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/BusinessController.java b/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/BusinessController.java index 11d31fd58..bc4120a9b 100644 --- a/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/BusinessController.java +++ b/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/BusinessController.java @@ -17,15 +17,22 @@ package com.tencent.cloud.ratelimit.example.service.callee; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -33,6 +40,8 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.HttpClientErrorException.TooManyRequests; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; /** * Rate limit controller. @@ -49,6 +58,8 @@ public class BusinessController { private final AtomicLong lastTimestamp = new AtomicLong(0); @Autowired private RestTemplate restTemplate; + @Autowired + private WebClient.Builder WebClientBuilder; @Value("${spring.application.name}") private String appName; @@ -56,11 +67,55 @@ public class BusinessController { * Get information. * @return information */ - @RequestMapping("/info") + @GetMapping("/info") public String info() { return "hello world for ratelimit service " + index.incrementAndGet(); } + @GetMapping("/info/webclient") + public Mono infoWebClient() { + return Mono.just("hello world for ratelimit service " + index.incrementAndGet()); + } + + @GetMapping("/invoke/webclient") + public String invokeInfoWebClient() throws InterruptedException, ExecutionException { + StringBuffer builder = new StringBuffer(); + WebClient webClient = WebClientBuilder.baseUrl("http://" + appName).build(); + List> monoList = new ArrayList<>(); + for (int i = 0; i < 30; i++) { + Mono response = webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/business/info/webclient") + .queryParam("yyy", "yyy") + .build() + ) + .header("xxx", "xxx") + .retrieve() + .bodyToMono(String.class) + .doOnSuccess(s -> builder.append(s + "\n")) + .doOnError(e -> { + if (e instanceof WebClientResponseException) { + if (((WebClientResponseException)e).getRawStatusCode() == 429) { + builder.append("TooManyRequests ").append(index.incrementAndGet() + "\n"); + } + } + }) + .onErrorReturn(""); + monoList.add(response); + } + for (Mono mono : monoList){ + mono.toFuture().get(); + } + index.set(0); + return builder.toString(); + } + + /** + * Get information 30 times per 1 second. + * + * @return result of 30 calls. + * @throws InterruptedException exception + */ @GetMapping("/invoke") public String invokeInfo() throws InterruptedException { StringBuffer builder = new StringBuffer(); @@ -68,19 +123,31 @@ public class BusinessController { for (int i = 0; i < 30; i++) { new Thread(() -> { try { - ResponseEntity entity = restTemplate.getForEntity("http://" + appName + "/business/info", - String.class); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add("xxx","xxx"); + ResponseEntity entity = restTemplate.exchange( + "http://" + appName + "/business/info?yyy={yyy}", + HttpMethod.GET, + new HttpEntity<>(httpHeaders), + String.class, + "yyy" + ); builder.append(entity.getBody() + "\n"); } catch (RestClientException e) { if (e instanceof TooManyRequests) { - builder.append("TooManyRequests " + index.incrementAndGet() + "\n"); + builder.append("TooManyRequests ").append(index.incrementAndGet() + "\n"); } else { throw e; } } - count.countDown(); + catch (Exception e) { + e.printStackTrace(); + } + finally { + count.countDown(); + } }).start(); } count.await(); @@ -90,6 +157,7 @@ public class BusinessController { /** * Get information with unirate. + * * @return information */ @GetMapping("/unirate") diff --git a/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/CustomLabelResolverReactive.java b/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/CustomLabelResolverReactive.java new file mode 100644 index 000000000..eb6e01b03 --- /dev/null +++ b/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/CustomLabelResolverReactive.java @@ -0,0 +1,44 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.ratelimit.example.service.callee; + +import java.util.HashMap; +import java.util.Map; + +import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelReactiveResolver; + +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; + +/** + * resolver custom label from request. + * + * @author sean yu + */ +@Component +public class CustomLabelResolverReactive implements PolarisRateLimiterLabelReactiveResolver { + @Override + public Map resolve(ServerWebExchange exchange) { + // rate limit by some request params. such as query params, headers .. + + Map labels = new HashMap<>(); + labels.put("user", "zhangsan"); + + return labels; + } +} diff --git a/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/RateLimitCalleeService.java b/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/RateLimitCalleeService.java index 12a9270cc..c8a49d2a7 100644 --- a/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/RateLimitCalleeService.java +++ b/spring-cloud-tencent-examples/polaris-ratelimit-example/ratelimit-callee-service/src/main/java/com/tencent/cloud/ratelimit/example/service/callee/RateLimitCalleeService.java @@ -22,6 +22,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; /** * Rate limit application. @@ -40,4 +41,11 @@ public class RateLimitCalleeService { public RestTemplate restTemplate() { return new RestTemplate(); } + + + @LoadBalanced + @Bean + WebClient.Builder webClientBuilder() { + return WebClient.builder(); + } }