add webclient support

pull/902/head
seanyu 3 years ago
parent 1321d6123e
commit 10ee323868

@ -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 javax.servlet.DispatcherType.ASYNC;
import static javax.servlet.DispatcherType.ERROR;
@ -143,4 +145,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<WebClient.Builder> webClientBuilder = Collections.emptyList();
@Bean
public EncodeTransferMedataWebClientFilter encodeTransferMedataWebClientFilter() {
return new EncodeTransferMedataWebClientFilter();
}
@Bean
public SmartInitializingSingleton addEncodeTransferMetadataFilterForWebClient(EncodeTransferMedataWebClientFilter filter) {
return () -> webClientBuilder.forEach(webClient -> {
webClient.filter(filter);
});
}
}
}

@ -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<ClientResponse> filter(ClientRequest clientRequest, ExchangeFunction next) {
MetadataContext metadataContext = MetadataContextHolder.get();
Map<String, String> customMetadata = metadataContext.getCustomMetadata();
Map<String, String> disposableMetadata = metadataContext.getDisposableMetadata();
Map<String, String> transHeaders = metadataContext.getTransHeadersKV();
Map<String, String> 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<String, String> 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<String, String> 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);
}
}
}
}

@ -31,6 +31,7 @@ import com.tencent.cloud.common.util.JacksonUtils;
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
@ -95,4 +96,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<String, String> transHeaderMetadata = MetadataContextHolder.get().getTransHeaders();
if (!CollectionUtils.isEmpty(transHeaderMetadata)) {
String transHeaders = transHeaderMetadata.keySet().stream().findFirst().orElse("");
String[] transHeaderArray = transHeaders.split(",");
HttpHeaders headers = clientRequest.headers();
Set<String> headerKeys = headers.keySet();
for (String httpHeader : headerKeys) {
Arrays.stream(transHeaderArray).forEach(transHeader -> {
if (transHeader.equals(httpHeader)) {
List<String> list = headers.get(httpHeader);
String httpHeaderValue = JacksonUtils.serialize2Json(list);
// for example, {"trans-headers-kv" : {"header1":"v1","header2":"v2"...}}
MetadataContextHolder.get().setTransHeadersKV(httpHeader, httpHeaderValue);
}
});
}
}
}
}

@ -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;

@ -18,6 +18,11 @@
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-starter-tencent-polaris-discovery</artifactId>

@ -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;
@ -62,6 +73,44 @@ public class BusinessController {
return "hello world for ratelimit service " + index.incrementAndGet();
}
@GetMapping("/info/webclient")
public Mono<String> 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<Mono<String>> monoList = new ArrayList<>();
for (int i = 0; i < 30; i++) {
Mono<String> 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<String> mono : monoList){
mono.toFuture().get();
}
index.set(0);
return builder.toString();
}
/**
* Get information 30 times per 1 second.
*
@ -75,19 +124,31 @@ public class BusinessController {
for (int i = 0; i < 30; i++) {
new Thread(() -> {
try {
ResponseEntity<String> entity = restTemplate.getForEntity("http://" + appName + "/business/info",
String.class);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("xxx","xxx");
ResponseEntity<String> 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();

@ -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<String, String> resolve(ServerWebExchange exchange) {
// rate limit by some request params. such as query params, headers ..
Map<String, String> labels = new HashMap<>();
labels.put("user", "zhangsan");
return labels;
}
}

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

Loading…
Cancel
Save