From 3b6e844144fbb64c2f35d390cf11d11078ec517a Mon Sep 17 00:00:00 2001 From: uuuyuqi Date: Tue, 10 Mar 2026 11:32:15 +0800 Subject: [PATCH 1/7] feat: support Spring RestClient Change-Id: I49c8b1cf7a0a6f357b68b58bd352efc5981b76b2 Co-developed-by: OpenCode --- sentinel-adapter/pom.xml | 1 + .../README.md | 150 +++++++++ .../pom.xml | 72 ++++ .../SentinelClientHttpResponse.java | 80 +++++ .../restclient/SentinelRestClientConfig.java | 79 +++++ .../SentinelRestClientInterceptor.java | 155 +++++++++ .../DefaultRestClientResourceExtractor.java | 50 +++ .../RestClientResourceExtractor.java | 34 ++ .../fallback/DefaultRestClientFallback.java | 38 +++ .../fallback/RestClientFallback.java | 43 +++ .../adapter/spring/restclient/ManualTest.java | 148 ++++++++ .../SentinelClientHttpResponseTest.java | 56 ++++ .../SentinelRestClientConfigTest.java | 43 +++ ...ntinelRestClientInterceptorSimpleTest.java | 317 ++++++++++++++++++ .../restclient/app/TestApplication.java | 16 + .../spring/restclient/app/TestController.java | 45 +++ ...efaultRestClientResourceExtractorTest.java | 70 ++++ .../DefaultRestClientFallbackTest.java | 51 +++ 18 files changed, 1448 insertions(+) create mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/README.md create mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/pom.xml create mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponse.java create mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientConfig.java create mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptor.java create mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/extractor/DefaultRestClientResourceExtractor.java create mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/extractor/RestClientResourceExtractor.java create mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/DefaultRestClientFallback.java create mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/RestClientFallback.java create mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/ManualTest.java create mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponseTest.java create mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientConfigTest.java create mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptorSimpleTest.java create mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/app/TestApplication.java create mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/app/TestController.java create mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/extractor/DefaultRestClientResourceExtractorTest.java create mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/DefaultRestClientFallbackTest.java diff --git a/sentinel-adapter/pom.xml b/sentinel-adapter/pom.xml index 97b9eb785c..b075b54f0f 100755 --- a/sentinel-adapter/pom.xml +++ b/sentinel-adapter/pom.xml @@ -34,6 +34,7 @@ sentinel-spring-webmvc-v6x-adapter sentinel-zuul2-adapter sentinel-okhttp-adapter + sentinel-spring-restclient-adapter sentinel-jax-rs-adapter sentinel-quarkus-adapter sentinel-motan-adapter diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/README.md b/sentinel-adapter/sentinel-spring-restclient-adapter/README.md new file mode 100644 index 0000000000..1544b05078 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/README.md @@ -0,0 +1,150 @@ +# Sentinel Spring RestClient Adapter + +## Overview + +Sentinel Spring RestClient Adapter provides Sentinel integration for Spring Framework 6.0+ `RestClient`. With this adapter, you can easily add flow control, circuit breaking, and degradation features to HTTP requests made via `RestClient`. + +## Features + +- Flow control (QPS limiting) +- Circuit breaking (degradation) +- Custom resource name extraction +- Custom fallback responses +- HTTP 5xx error tracing + +## Requirements + +- Spring Framework 6.0+ +- JDK 17+ +- Sentinel Core 1.8.0+ + +## Usage + +### 1. Add Dependency + +```xml + + com.alibaba.csp + sentinel-spring-restclient-adapter + ${sentinel.version} + +``` + +### 2. Basic Usage + +```java +import com.alibaba.csp.sentinel.adapter.spring.restclient.SentinelRestClientInterceptor; +import org.springframework.web.client.RestClient; + +// Create RestClient with Sentinel interceptor +RestClient restClient = RestClient.builder() + .requestInterceptor(new SentinelRestClientInterceptor()) + .build(); + +// Use RestClient to send requests (protected by Sentinel) +String result = restClient.get() + .uri("https://httpbin.org/get") + .retrieve() + .body(String.class); +``` + +### 3. Custom Configuration + +```java +import com.alibaba.csp.sentinel.adapter.spring.restclient.SentinelRestClientConfig; +import com.alibaba.csp.sentinel.adapter.spring.restclient.SentinelRestClientInterceptor; +import com.alibaba.csp.sentinel.adapter.spring.restclient.extractor.RestClientResourceExtractor; +import com.alibaba.csp.sentinel.adapter.spring.restclient.fallback.RestClientFallback; + +// Custom resource name extractor +RestClientResourceExtractor customExtractor = request -> { + // Example: normalize RESTful path parameters + String path = request.getURI().getPath(); + if (path.matches("/users/\\d+")) { + path = "/users/{id}"; + } + return request.getMethod() + ":" + request.getURI().getHost() + path; +}; + +// Custom fallback response +RestClientFallback customFallback = (request, body, execution, ex) -> { + return new SentinelClientHttpResponse("Service temporarily unavailable, please retry later"); +}; + +// Create configuration +SentinelRestClientConfig config = new SentinelRestClientConfig( + "my-restclient:", // Resource name prefix + customExtractor, + customFallback +); + +// Create interceptor with custom configuration +RestClient restClient = RestClient.builder() + .requestInterceptor(new SentinelRestClientInterceptor(config)) + .build(); +``` + +### 4. Configure Sentinel Rules + +```java +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import java.util.Collections; + +// Configure flow control rule +FlowRule rule = new FlowRule("restclient:GET:https://httpbin.org/get"); +rule.setGrade(RuleConstant.FLOW_GRADE_QPS); +rule.setCount(10); // Max 10 requests per second +rule.setLimitApp("default"); + +FlowRuleManager.loadRules(Collections.singletonList(rule)); +``` + +## Core Components + +### SentinelRestClientInterceptor + +The main interceptor implementation responsible for: +- Creating Sentinel resources for each HTTP request +- Catching BlockException and invoking fallback handler +- Tracing exceptions and 5xx errors + +### SentinelRestClientConfig + +Configuration class containing: +- `resourcePrefix`: Resource name prefix (default: `restclient:`) +- `resourceExtractor`: Resource name extractor +- `fallback`: Fallback handler + +### RestClientResourceExtractor + +Interface for resource name extraction, allowing customization of resource name generation logic. + +### RestClientFallback + +Interface for fallback handling, invoked when requests are blocked by flow control or circuit breaking. + +## Resource Name Format + +The default resource name format: `{prefix}{METHOD}:{URL}` + +Examples: +- `restclient:GET:https://httpbin.org/get` +- `restclient:POST:http://localhost:8080/api/users` + +## Notes + +This adapter only supports `RestClient` from Spring Framework 6.0+, not `RestTemplate`. + + +## Integration with Spring Cloud Alibaba + +This adapter provides basic Sentinel integration. For Spring Cloud Alibaba projects: + +1. Add auto-configuration support in `spring-cloud-starter-alibaba-sentinel` +2. Use `@SentinelRestClient` annotation for simplified configuration + +## License + +Apache License 2.0 \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/pom.xml b/sentinel-adapter/sentinel-spring-restclient-adapter/pom.xml new file mode 100644 index 0000000000..9bc0df0e42 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/pom.xml @@ -0,0 +1,72 @@ + + + + com.alibaba.csp + sentinel-adapter + ${revision} + ../pom.xml + + 4.0.0 + + ${project.groupId}:${project.artifactId} + + sentinel-spring-restclient-adapter + jar + + + 6.1.0 + 3.2.0 + 6.1.0 + + + + + com.alibaba.csp + sentinel-core + + + + org.springframework + spring-web + ${spring-web.version} + provided + + + + junit + junit + test + + + org.mockito + mockito-core + test + + + com.alibaba + fastjson + test + + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot.version} + test + + + org.springframework.boot + spring-boot-test + ${spring-boot.version} + test + + + org.springframework + spring-test + ${spring-test.version} + test + + + \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponse.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponse.java new file mode 100644 index 0000000000..ae865fb9b5 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponse.java @@ -0,0 +1,80 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.alibaba.csp.sentinel.adapter.spring.restclient; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpResponse; + +/** + * Default HTTP response when request is blocked by Sentinel. + * + * @author QHT, uuuyuqi + */ +public class SentinelClientHttpResponse implements ClientHttpResponse { + + private String blockResponse = "Request blocked by Sentinel"; + + public SentinelClientHttpResponse() { + } + + public SentinelClientHttpResponse(String blockResponse) { + this.blockResponse = blockResponse; + } + + @Override + public HttpStatus getStatusCode() throws IOException { + return HttpStatus.OK; + } + + @Override + public int getRawStatusCode() throws IOException { + return HttpStatus.OK.value(); + } + + @Override + public String getStatusText() throws IOException { + return blockResponse; + } + + @Override + public void close() { + } + + @Override + public InputStream getBody() throws IOException { + return new ByteArrayInputStream(blockResponse.getBytes()); + } + + @Override + public HttpHeaders getHeaders() { + Map> headers = new HashMap<>(); + headers.put(HttpHeaders.CONTENT_TYPE, + Arrays.asList(MediaType.APPLICATION_JSON_VALUE)); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.putAll(headers); + return httpHeaders; + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientConfig.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientConfig.java new file mode 100644 index 0000000000..c2dba15c9d --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientConfig.java @@ -0,0 +1,79 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.alibaba.csp.sentinel.adapter.spring.restclient; + +import com.alibaba.csp.sentinel.adapter.spring.restclient.extractor.DefaultRestClientResourceExtractor; +import com.alibaba.csp.sentinel.adapter.spring.restclient.extractor.RestClientResourceExtractor; +import com.alibaba.csp.sentinel.adapter.spring.restclient.fallback.DefaultRestClientFallback; +import com.alibaba.csp.sentinel.adapter.spring.restclient.fallback.RestClientFallback; +import com.alibaba.csp.sentinel.util.AssertUtil; + +/** + * Configuration for Sentinel RestClient interceptor. + * + * @author QHT, uuuyuqi + */ +public class SentinelRestClientConfig { + + public static final String DEFAULT_RESOURCE_PREFIX = "restclient:"; + + private final String resourcePrefix; + private final RestClientResourceExtractor resourceExtractor; + private final RestClientFallback fallback; + + public SentinelRestClientConfig() { + this(DEFAULT_RESOURCE_PREFIX); + } + + public SentinelRestClientConfig(String resourcePrefix) { + this(resourcePrefix, new DefaultRestClientResourceExtractor(), new DefaultRestClientFallback()); + } + + public SentinelRestClientConfig(RestClientResourceExtractor resourceExtractor, RestClientFallback fallback) { + this(DEFAULT_RESOURCE_PREFIX, resourceExtractor, fallback); + } + + public SentinelRestClientConfig(String resourcePrefix, + RestClientResourceExtractor resourceExtractor, + RestClientFallback fallback) { + AssertUtil.notNull(resourceExtractor, "resourceExtractor cannot be null"); + AssertUtil.notNull(fallback, "fallback cannot be null"); + this.resourcePrefix = resourcePrefix; + this.resourceExtractor = resourceExtractor; + this.fallback = fallback; + } + + public String getResourcePrefix() { + return resourcePrefix; + } + + public RestClientResourceExtractor getResourceExtractor() { + return resourceExtractor; + } + + public RestClientFallback getFallback() { + return fallback; + } + + @Override + public String toString() { + return "SentinelRestClientConfig{" + + "resourcePrefix='" + resourcePrefix + '\'' + + ", resourceExtractor=" + resourceExtractor + + ", fallback=" + fallback + + '}'; + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptor.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptor.java new file mode 100644 index 0000000000..7a16f05ea3 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptor.java @@ -0,0 +1,155 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.alibaba.csp.sentinel.adapter.spring.restclient; + +import java.io.IOException; +import java.net.URI; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.adapter.spring.restclient.extractor.RestClientResourceExtractor; +import com.alibaba.csp.sentinel.adapter.spring.restclient.fallback.RestClientFallback; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.csp.sentinel.util.StringUtil; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; + +/** + * {@link ClientHttpRequestInterceptor} for integrating Sentinel with Spring's + * {@link org.springframework.web.client.RestClient}. + * + *

This interceptor creates two levels of Sentinel resources for each request: + *

    + *
  • Host-level resource: {@code METHOD:scheme://host[:port]}, + * e.g. {@code GET:https://httpbin.org}
  • + *
  • Path-level resource: extracted by {@link RestClientResourceExtractor}, + * by default: {@code METHOD:scheme://host[:port]/path}, + * e.g. {@code GET:https://httpbin.org/get}
  • + *
+ * + *

This dual-level design allows: + *

    + *
  • Host-level flow control for overall traffic to a service
  • + *
  • Path-level flow control for specific endpoints
  • + *
  • Circuit breaking at either level
  • + *
+ * + *

Supports: + *

    + *
  • Flow control (QPS limiting)
  • + *
  • Circuit breaking (degrade)
  • + *
  • Custom resource name extraction via {@link RestClientResourceExtractor}
  • + *
  • Custom fallback responses via {@link RestClientFallback}
  • + *
+ * + * @author QHT, uuuyuqi + * @see SentinelRestClientConfig + * @see RestClientResourceExtractor + * @see RestClientFallback + */ +public class SentinelRestClientInterceptor implements ClientHttpRequestInterceptor { + + private final SentinelRestClientConfig config; + + public SentinelRestClientInterceptor() { + this.config = new SentinelRestClientConfig(); + } + + public SentinelRestClientInterceptor(SentinelRestClientConfig config) { + AssertUtil.notNull(config, "config cannot be null"); + this.config = config; + } + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution) throws IOException { + URI uri = request.getURI(); + + String hostResource = buildHostResourceName(request, uri); + String pathResource = buildPathResourceName(request); + + boolean entryWithPath = !hostResource.equals(pathResource); + + Entry hostEntry = null; + Entry pathEntry = null; + + try { + hostEntry = SphU.entry(hostResource, ResourceTypeConstants.COMMON_WEB, EntryType.OUT); + + if (entryWithPath) { + pathEntry = SphU.entry(pathResource, ResourceTypeConstants.COMMON_WEB, EntryType.OUT); + } + + ClientHttpResponse response = execution.execute(request, body); + + if (response.getStatusCode().is5xxServerError()) { + RuntimeException ex = new RuntimeException("Server error: " + response.getStatusCode().value()); + Tracer.trace(ex); + } + + return response; + } catch (BlockException ex) { + return handleBlockException(request, body, execution, ex); + } catch (IOException ex) { + Tracer.traceEntry(ex, hostEntry); + throw ex; + } finally { + if (pathEntry != null) { + pathEntry.exit(); + } + if (hostEntry != null) { + hostEntry.exit(); + } + } + } + + private String buildHostResourceName(HttpRequest request, URI uri) { + String hostResource = request.getMethod().toString() + ":" + + uri.getScheme() + "://" + + uri.getHost() + + (uri.getPort() == -1 ? "" : ":" + uri.getPort()); + + if (StringUtil.isNotBlank(config.getResourcePrefix())) { + hostResource = config.getResourcePrefix() + hostResource; + } + + return hostResource; + } + + private String buildPathResourceName(HttpRequest request) { + String pathResource = config.getResourceExtractor().extract(request); + + if (StringUtil.isNotBlank(config.getResourcePrefix())) { + pathResource = config.getResourcePrefix() + pathResource; + } + + return pathResource; + } + + private ClientHttpResponse handleBlockException(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution, + BlockException ex) { + return config.getFallback().handle(request, body, execution, ex); + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/extractor/DefaultRestClientResourceExtractor.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/extractor/DefaultRestClientResourceExtractor.java new file mode 100644 index 0000000000..5a2a765d9b --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/extractor/DefaultRestClientResourceExtractor.java @@ -0,0 +1,50 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.alibaba.csp.sentinel.adapter.spring.restclient.extractor; + +import java.net.URI; + +import org.springframework.http.HttpRequest; + +/** + * Default resource extractor for RestClient. + * + *

Extracts resource name in format: {@code METHOD:scheme://host[:port]/path} + * + *

Examples: + *

    + *
  • {@code GET:https://httpbin.org/get}
  • + *
  • {@code POST:http://localhost:8080/api/users}
  • + *
  • {@code GET:http://localhost:8080/api/users/123}
  • + *
+ * + *

Note: Query parameters are not included in the resource name by default. + * Use a custom extractor if you need query parameters. + * + * @author QHT, uuuyuqi + */ +public class DefaultRestClientResourceExtractor implements RestClientResourceExtractor { + + @Override + public String extract(HttpRequest request) { + URI uri = request.getURI(); + return request.getMethod().toString() + ":" + + uri.getScheme() + "://" + + uri.getHost() + + (uri.getPort() == -1 ? "" : ":" + uri.getPort()) + + uri.getPath(); + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/extractor/RestClientResourceExtractor.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/extractor/RestClientResourceExtractor.java new file mode 100644 index 0000000000..96b9d55d80 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/extractor/RestClientResourceExtractor.java @@ -0,0 +1,34 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.alibaba.csp.sentinel.adapter.spring.restclient.extractor; + +import org.springframework.http.HttpRequest; + +/** + * Extractor for RestClient resource name. + * + * @author QHT, uuuyuqi + */ +public interface RestClientResourceExtractor { + + /** + * Extracts the resource name from the HTTP request. + * + * @param request HTTP request entity + * @return the resource name of current request + */ + String extract(HttpRequest request); +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/DefaultRestClientFallback.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/DefaultRestClientFallback.java new file mode 100644 index 0000000000..28ff1cbfc2 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/DefaultRestClientFallback.java @@ -0,0 +1,38 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.alibaba.csp.sentinel.adapter.spring.restclient.fallback; + +import com.alibaba.csp.sentinel.adapter.spring.restclient.SentinelClientHttpResponse; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpResponse; + +/** + * Default fallback handler for RestClient. + * + * @author QHT, uuuyuqi + */ +public class DefaultRestClientFallback implements RestClientFallback { + + @Override + public ClientHttpResponse handle(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution, BlockException ex) { + return new SentinelClientHttpResponse("RestClient request blocked by Sentinel: " + + ex.getClass().getSimpleName()); + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/RestClientFallback.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/RestClientFallback.java new file mode 100644 index 0000000000..1e970f9cb5 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/RestClientFallback.java @@ -0,0 +1,43 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.alibaba.csp.sentinel.adapter.spring.restclient.fallback; + +import com.alibaba.csp.sentinel.adapter.spring.restclient.SentinelClientHttpResponse; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpResponse; + +/** + * Fallback handler for RestClient when request is blocked by Sentinel. + * + * @author QHT, uuuyuqi + */ +public interface RestClientFallback { + + /** + * Handle the blocked request and return a fallback response. + * + * @param request HTTP request entity + * @param body request body + * @param execution request execution + * @param ex the block exception + * @return fallback response + */ + ClientHttpResponse handle(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution, BlockException ex); +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/ManualTest.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/ManualTest.java new file mode 100644 index 0000000000..45aed28aa8 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/ManualTest.java @@ -0,0 +1,148 @@ +package com.alibaba.csp.sentinel.adapter.spring.restclient; + +import com.alibaba.csp.sentinel.adapter.spring.restclient.extractor.RestClientResourceExtractor; +import com.alibaba.csp.sentinel.adapter.spring.restclient.fallback.RestClientFallback; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.web.client.RestClient; + +import com.alibaba.csp.sentinel.slots.block.BlockException; + +import java.util.Collections; + +/** + * Manual test for RestClient adapter. + * + * Run this class directly to test the adapter functionality. + * + * @author uuuyuqi + */ +public class ManualTest { + + public static void main(String[] args) { + System.out.println("=== Sentinel RestClient Adapter Manual Test ===\n"); + + testBasicUsage(); + testWithFlowControl(); + testWithCustomExtractor(); + + System.out.println("\n=== All manual tests completed! ==="); + } + + private static void testBasicUsage() { + System.out.println("Test 1: Basic Usage"); + System.out.println("--------------------"); + + RestClient restClient = RestClient.builder() + .requestInterceptor(new SentinelRestClientInterceptor()) + .build(); + + try { + String result = restClient.get() + .uri("https://httpbin.org/get") + .retrieve() + .body(String.class); + + System.out.println("✅ Request successful!"); + System.out.println("Response length: " + (result != null ? result.length() : 0) + " chars"); + + ClusterNode node = ClusterBuilderSlot.getClusterNode("restclient:GET:https://httpbin.org/get"); + if (node != null) { + System.out.println("✅ Sentinel statistics recorded!"); + System.out.println(" Total requests: " + node.totalRequest()); + System.out.println(" Success: " + node.totalSuccess()); + } + } catch (Exception e) { + System.out.println("❌ Test failed: " + e.getMessage()); + } + + System.out.println(); + } + + private static void testWithFlowControl() { + System.out.println("Test 2: Flow Control"); + System.out.println("---------------------"); + + String resourceName = "restclient:GET:https://httpbin.org/delay/1"; + + FlowRule rule = new FlowRule(resourceName); + rule.setGrade(RuleConstant.FLOW_GRADE_QPS); + rule.setCount(0); + rule.setLimitApp("default"); + FlowRuleManager.loadRules(Collections.singletonList(rule)); + + RestClient restClient = RestClient.builder() + .requestInterceptor(new SentinelRestClientInterceptor()) + .build(); + + try { + String result = restClient.get() + .uri("https://httpbin.org/delay/1") + .retrieve() + .body(String.class); + + System.out.println("Response: " + result); + System.out.println("✅ Flow control test completed!"); + } catch (Exception e) { + System.out.println("❌ Test error: " + e.getMessage()); + } + + FlowRuleManager.loadRules(Collections.emptyList()); + System.out.println(); + } + + private static void testWithCustomExtractor() { + System.out.println("Test 3: Custom Resource Extractor"); + System.out.println("-----------------------------------"); + + RestClientResourceExtractor customExtractor = request -> { + String path = request.getURI().getPath(); + if (path.matches("/status/\\d+")) { + path = "/status/{code}"; + } + return request.getMethod().toString() + ":" + + request.getURI().getHost() + path; + }; + + RestClientFallback customFallback = (HttpRequest request, byte[] body, + ClientHttpRequestExecution execution, BlockException ex) -> + new SentinelClientHttpResponse("Custom fallback: " + ex.getClass().getSimpleName()); + + SentinelRestClientConfig config = new SentinelRestClientConfig( + "custom:", + customExtractor, + customFallback + ); + + RestClient restClient = RestClient.builder() + .requestInterceptor(new SentinelRestClientInterceptor(config)) + .build(); + + try { + String result = restClient.get() + .uri("https://httpbin.org/status/200") + .retrieve() + .body(String.class); + + System.out.println("Response: " + (result != null ? "OK" : "Empty")); + System.out.println("✅ Custom extractor test completed!"); + + String expectedResource = "custom:GET:httpbin.org/status/{code}"; + ClusterNode node = ClusterBuilderSlot.getClusterNode(expectedResource); + if (node != null) { + System.out.println("✅ Resource name normalized correctly!"); + System.out.println(" Resource: " + expectedResource); + } + } catch (Exception e) { + System.out.println("Response: " + e.getMessage()); + } + + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponseTest.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponseTest.java new file mode 100644 index 0000000000..5becbc6c13 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponseTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.alibaba.csp.sentinel.adapter.spring.restclient; + +import org.junit.Test; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests for {@link SentinelClientHttpResponse}. + * + * @author uuuyuqi + */ +public class SentinelClientHttpResponseTest { + + @Test + public void testDefaultResponse() throws IOException { + SentinelClientHttpResponse response = new SentinelClientHttpResponse(); + assertEquals("Request blocked by Sentinel", response.getStatusText()); + } + + @Test + public void testCustomResponse() throws IOException { + String customMessage = "Custom blocked message"; + SentinelClientHttpResponse response = new SentinelClientHttpResponse(customMessage); + assertEquals(customMessage, response.getStatusText()); + } + + @Test + public void testResponseProperties() throws IOException { + SentinelClientHttpResponse response = new SentinelClientHttpResponse("test"); + + assertNotNull(response.getStatusCode()); + assertEquals(200, response.getRawStatusCode()); + assertNotNull(response.getBody()); + assertNotNull(response.getHeaders()); + assertTrue(response.getHeaders().containsKey("Content-Type")); + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientConfigTest.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientConfigTest.java new file mode 100644 index 0000000000..b36d1e8089 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientConfigTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.alibaba.csp.sentinel.adapter.spring.restclient; + +import com.alibaba.csp.sentinel.adapter.spring.restclient.extractor.RestClientResourceExtractor; +import com.alibaba.csp.sentinel.adapter.spring.restclient.fallback.DefaultRestClientFallback; +import org.junit.Test; + +/** + * Tests for {@link SentinelRestClientConfig}. + * + * @author uuuyuqi + */ +public class SentinelRestClientConfigTest { + + @Test(expected = IllegalArgumentException.class) + public void testConfigSetExtractorNull() { + new SentinelRestClientConfig(null, new DefaultRestClientFallback()); + } + + @Test(expected = IllegalArgumentException.class) + public void testConfigSetFallbackNull() { + new SentinelRestClientConfig(new RestClientResourceExtractor() { + @Override + public String extract(org.springframework.http.HttpRequest request) { + return request.getURI().toString(); + } + }, null); + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptorSimpleTest.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptorSimpleTest.java new file mode 100644 index 0000000000..5b37d887fc --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptorSimpleTest.java @@ -0,0 +1,317 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.alibaba.csp.sentinel.adapter.spring.restclient; + +import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.adapter.spring.restclient.app.TestApplication; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestClient; + +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.*; + +/** + * Simple integration tests for {@link SentinelRestClientInterceptor}. + * + * @author uuuyuqi + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, + properties = { + "server.port=8087" + }) +public class SentinelRestClientInterceptorSimpleTest { + + @Value("${server.port}") + private Integer port; + + @Before + public void setUp() { + Constants.ROOT.removeChildList(); + ClusterBuilderSlot.getClusterNodeMap().clear(); + FlowRuleManager.loadRules(Collections.emptyList()); + DegradeRuleManager.loadRules(Collections.emptyList()); + } + + @After + public void tearDown() { + Constants.ROOT.removeChildList(); + ClusterBuilderSlot.getClusterNodeMap().clear(); + FlowRuleManager.loadRules(Collections.emptyList()); + DegradeRuleManager.loadRules(Collections.emptyList()); + } + + @Test + public void testBasicRequest() { + String url = "http://localhost:" + port + "/test/hello"; + + RestClient restClient = RestClient.builder() + .requestInterceptor(new SentinelRestClientInterceptor()) + .build(); + + String result = restClient.get() + .uri(url) + .retrieve() + .body(String.class); + + assertEquals("Hello, Sentinel!", result); + System.out.println("Request completed successfully: " + result); + } + + @Test + public void testDualLevelResources() { + String url = "http://localhost:" + port + "/test/hello"; + String expectedHostResource = "restclient:GET:http://localhost:" + port; + String expectedPathResource = "restclient:GET:http://localhost:" + port + "/test/hello"; + + RestClient restClient = RestClient.builder() + .requestInterceptor(new SentinelRestClientInterceptor()) + .build(); + + String result = restClient.get() + .uri(url) + .retrieve() + .body(String.class); + + assertEquals("Hello, Sentinel!", result); + System.out.println("✅ Request completed successfully"); + + try { + ClusterNode hostNode = ClusterBuilderSlot.getClusterNode(expectedHostResource); + if (hostNode != null) { + System.out.println("✅ Host-level resource created: " + expectedHostResource); + System.out.println(" Total requests: " + hostNode.totalRequest()); + } + + ClusterNode pathNode = ClusterBuilderSlot.getClusterNode(expectedPathResource); + if (pathNode != null) { + System.out.println("✅ Path-level resource created: " + expectedPathResource); + System.out.println(" Total requests: " + pathNode.totalRequest()); + } + } catch (Exception e) { + System.out.println("Note: ClusterNode check skipped due to: " + e.getMessage()); + } + } + + @Test + public void testFlowControlBlocking() { + String url = "http://localhost:" + port + "/test/hello"; + String pathResource = "restclient:GET:" + url; + + FlowRule rule = new FlowRule(pathResource); + rule.setGrade(RuleConstant.FLOW_GRADE_QPS); + rule.setCount(0); + rule.setLimitApp("default"); + FlowRuleManager.loadRules(Collections.singletonList(rule)); + + RestClient restClient = RestClient.builder() + .requestInterceptor(new SentinelRestClientInterceptor()) + .build(); + + String result = restClient.get() + .uri(url) + .retrieve() + .body(String.class); + + assertNotNull("Should get fallback response", result); + System.out.println("Blocked response: " + result); + } + + @Test + public void testHostLevelFlowControl() throws InterruptedException { + String url = "http://localhost:" + port + "/test/hello"; + String rootResource = "restclient:GET:" + "http://localhost:" + port; + + FlowRule rule = new FlowRule(rootResource); + rule.setGrade(RuleConstant.FLOW_GRADE_QPS); + rule.setCount(0); + rule.setLimitApp("default"); + FlowRuleManager.loadRules(Collections.singletonList(rule)); + + RestClient restClient = RestClient.builder() + .requestInterceptor(new SentinelRestClientInterceptor()) + .build(); + + String result = restClient.get() + .uri(url) + .retrieve() + .body(String.class); + + assertNotNull("Should get fallback response", result); + System.out.println("Blocked response: " + result); + } + + + @Test + public void testCustomConfig() { + String customPrefix = "my-api:"; + String url = "http://localhost:" + port + "/test/hello"; + String pathResource = customPrefix + "GET:" + url; + + FlowRule rule = new FlowRule(pathResource); + rule.setGrade(RuleConstant.FLOW_GRADE_QPS); + rule.setCount(0); + rule.setLimitApp("default"); + FlowRuleManager.loadRules(Collections.singletonList(rule)); + + SentinelRestClientConfig config = new SentinelRestClientConfig(customPrefix); + RestClient restClient = RestClient.builder() + .requestInterceptor(new SentinelRestClientInterceptor(config)) + .build(); + + String result = restClient.get() + .uri(url) + .retrieve() + .body(String.class); + + assertNotNull("Should get fallback response when blocked", result); + assertTrue("Response should indicate blocking by Sentinel", + result.contains("blocked by Sentinel")); + System.out.println("Custom prefix flow control test completed: " + result); + } + + + @Test + public void testPostRequestFlowControl() { + String url = "http://localhost:" + port + "/test/users"; + String pathResource = "restclient:POST:" + url; + + FlowRule rule = new FlowRule(pathResource); + rule.setGrade(RuleConstant.FLOW_GRADE_QPS); + rule.setCount(0); + rule.setLimitApp("default"); + FlowRuleManager.loadRules(Collections.singletonList(rule)); + + RestClient restClient = RestClient.builder() + .requestInterceptor(new SentinelRestClientInterceptor()) + .build(); + + String result = restClient.post() + .uri(url) + .body("Test User") + .retrieve() + .body(String.class); + + assertNotNull("Should get fallback response when blocked", result); + assertTrue("Response should indicate blocking by Sentinel", + result.contains("blocked by Sentinel")); + System.out.println("POST request blocked by flow control: " + result); + } + + @Test + public void testDegradeByExceptionRatio() { + String url = "http://localhost:" + port + "/test/error"; + String resourceName = "restclient:GET:" + url; + + DegradeRule degradeRule = new DegradeRule(resourceName); + degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO); + degradeRule.setCount(0.99); + degradeRule.setMinRequestAmount(1); + degradeRule.setStatIntervalMs(10 * 1000); + degradeRule.setTimeWindow(30); + degradeRule.setLimitApp("default"); + DegradeRuleManager.loadRules(Collections.singletonList(degradeRule)); + + RestClient restClient = RestClient.builder() + .requestInterceptor(new SentinelRestClientInterceptor()) + .build(); + + try { + restClient.get() + .uri(url) + .retrieve() + .body(String.class); + } catch (Exception e) { + System.out.println("First request failed with exception (expected): " + e.getClass().getSimpleName()); + } + + try { + String result = restClient.get() + .uri(url) + .retrieve() + .body(String.class); + + assertNotNull("Should get fallback response after circuit opens", result); + assertTrue("Response should indicate blocking", + result.contains("blocked by Sentinel") || result.contains("DegradeException")); + System.out.println("Degrade fallback response: " + result); + } catch (Exception e) { + System.out.println("Second request also threw exception: " + e.getMessage()); + } + } + + @Test + public void testDegradeBySlowResponseTime() throws InterruptedException { + String url = "http://localhost:" + port + "/test/delay"; + String resourceName = "restclient:GET:" + url; + + DegradeRule degradeRule = new DegradeRule(resourceName); + degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_RT); + degradeRule.setCount(50); + degradeRule.setSlowRatioThreshold(0.1); + degradeRule.setMinRequestAmount(1); + degradeRule.setStatIntervalMs(10 * 1000); + degradeRule.setTimeWindow(30); + degradeRule.setLimitApp("default"); + DegradeRuleManager.loadRules(Collections.singletonList(degradeRule)); + + RestClient restClient = RestClient.builder() + .requestInterceptor(new SentinelRestClientInterceptor()) + .build(); + + String result = restClient.get() + .uri(url) + .retrieve() + .body(String.class); + + assertEquals("Delayed response", result); + System.out.println("First slow request completed: " + result); + + Thread.sleep(100); + + try { + result = restClient.get() + .uri(url) + .retrieve() + .body(String.class); + + assertNotNull("Should get response", result); + System.out.println("Second request response: " + result); + } catch (Exception e) { + System.out.println("Request failed: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/app/TestApplication.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/app/TestApplication.java new file mode 100644 index 0000000000..7a209d1b59 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/app/TestApplication.java @@ -0,0 +1,16 @@ +package com.alibaba.csp.sentinel.adapter.spring.restclient.app; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Test application for RestClient adapter. + * + * @author uuuyuqi + */ +@SpringBootApplication +public class TestApplication { + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/app/TestController.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/app/TestController.java new file mode 100644 index 0000000000..195b272ad3 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/app/TestController.java @@ -0,0 +1,45 @@ +package com.alibaba.csp.sentinel.adapter.spring.restclient.app; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Test controller for RestClient adapter tests. + * + * @author uuuyuqi + */ +@RestController +@RequestMapping("/test") +public class TestController { + + @GetMapping("/hello") + public String hello() { + return "Hello, Sentinel!"; + } + + @GetMapping("/users/{id}") + public String getUser(@PathVariable("id") Long id) { + return "User: " + id; + } + + @PostMapping("/users") + public String createUser(@RequestBody String user) { + return "Created: " + user; + } + + @GetMapping("/error") + public ResponseEntity error() { + return ResponseEntity.status(500).body("Server Error"); + } + + @GetMapping("/delay") + public String delay() throws InterruptedException { + Thread.sleep(100); + return "Delayed response"; + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/extractor/DefaultRestClientResourceExtractorTest.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/extractor/DefaultRestClientResourceExtractorTest.java new file mode 100644 index 0000000000..26b6ac1c27 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/extractor/DefaultRestClientResourceExtractorTest.java @@ -0,0 +1,70 @@ +package com.alibaba.csp.sentinel.adapter.spring.restclient.extractor; + +import org.junit.Test; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; + +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Tests for {@link DefaultRestClientResourceExtractor}. + * + * @author uuuyuqi + */ +public class DefaultRestClientResourceExtractorTest { + + @Test + public void testExtract() throws Exception { + DefaultRestClientResourceExtractor extractor = new DefaultRestClientResourceExtractor(); + + HttpRequest request = new HttpRequest() { + @Override + public HttpMethod getMethod() { + return HttpMethod.GET; + } + + @Override + public URI getURI() { + return URI.create("https://httpbin.org/get"); + } + + @Override + public org.springframework.http.HttpHeaders getHeaders() { + return new org.springframework.http.HttpHeaders(); + } + }; + + String resourceName = extractor.extract(request); + assertNotNull(resourceName); + assertEquals("GET:https://httpbin.org/get", resourceName); + } + + @Test + public void testExtractWithPort() throws Exception { + DefaultRestClientResourceExtractor extractor = new DefaultRestClientResourceExtractor(); + + HttpRequest request = new HttpRequest() { + @Override + public HttpMethod getMethod() { + return HttpMethod.POST; + } + + @Override + public URI getURI() { + return URI.create("http://localhost:8080/api/users"); + } + + @Override + public org.springframework.http.HttpHeaders getHeaders() { + return new org.springframework.http.HttpHeaders(); + } + }; + + String resourceName = extractor.extract(request); + assertNotNull(resourceName); + assertEquals("POST:http://localhost:8080/api/users", resourceName); + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/DefaultRestClientFallbackTest.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/DefaultRestClientFallbackTest.java new file mode 100644 index 0000000000..bd4fab95f9 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/DefaultRestClientFallbackTest.java @@ -0,0 +1,51 @@ +package com.alibaba.csp.sentinel.adapter.spring.restclient.fallback; + +import com.alibaba.csp.sentinel.slots.block.flow.FlowException; +import org.junit.Test; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpResponse; + +import java.io.IOException; +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests for {@link DefaultRestClientFallback}. + * + * @author uuuyuqi + */ +public class DefaultRestClientFallbackTest { + + @Test + public void testHandle() throws IOException { + DefaultRestClientFallback fallback = new DefaultRestClientFallback(); + + HttpRequest request = new HttpRequest() { + @Override + public HttpMethod getMethod() { + return HttpMethod.GET; + } + + @Override + public URI getURI() { + return URI.create("https://httpbin.org/get"); + } + + @Override + public org.springframework.http.HttpHeaders getHeaders() { + return new org.springframework.http.HttpHeaders(); + } + }; + + FlowException ex = new FlowException("test", "default"); + ClientHttpResponse response = fallback.handle(request, new byte[0], null, ex); + + assertNotNull(response); + assertTrue(response.getStatusText().contains("blocked by Sentinel")); + assertTrue(response.getStatusText().contains("FlowException")); + } +} \ No newline at end of file From 3aea5a63a4cbb6b711e61efdfbff226dc39b6352 Mon Sep 17 00:00:00 2001 From: uuuyuqi Date: Tue, 10 Mar 2026 11:52:22 +0800 Subject: [PATCH 2/7] add customized resourceExtractor and fallback test Change-Id: Ieacaf949222f5871f934d2f0aa3bd6d73d1a087b --- .../SentinelRestClientInterceptorSimpleTest.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptorSimpleTest.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptorSimpleTest.java index 5b37d887fc..90fd096477 100644 --- a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptorSimpleTest.java +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptorSimpleTest.java @@ -17,6 +17,8 @@ import com.alibaba.csp.sentinel.Constants; import com.alibaba.csp.sentinel.adapter.spring.restclient.app.TestApplication; +import com.alibaba.csp.sentinel.adapter.spring.restclient.extractor.DefaultRestClientResourceExtractor; +import com.alibaba.csp.sentinel.adapter.spring.restclient.fallback.DefaultRestClientFallback; import com.alibaba.csp.sentinel.node.ClusterNode; import com.alibaba.csp.sentinel.slots.block.RuleConstant; import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; @@ -179,15 +181,17 @@ public void testHostLevelFlowControl() throws InterruptedException { public void testCustomConfig() { String customPrefix = "my-api:"; String url = "http://localhost:" + port + "/test/hello"; - String pathResource = customPrefix + "GET:" + url; - FlowRule rule = new FlowRule(pathResource); + FlowRule rule = new FlowRule("my-api:abc"); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setCount(0); rule.setLimitApp("default"); FlowRuleManager.loadRules(Collections.singletonList(rule)); - SentinelRestClientConfig config = new SentinelRestClientConfig(customPrefix); + SentinelRestClientConfig config = new SentinelRestClientConfig( + customPrefix, + request -> "abc", + (a,b,c,d) -> new SentinelClientHttpResponse("ABC blocked!" )); RestClient restClient = RestClient.builder() .requestInterceptor(new SentinelRestClientInterceptor(config)) .build(); @@ -198,9 +202,9 @@ public void testCustomConfig() { .body(String.class); assertNotNull("Should get fallback response when blocked", result); - assertTrue("Response should indicate blocking by Sentinel", - result.contains("blocked by Sentinel")); - System.out.println("Custom prefix flow control test completed: " + result); + assertTrue("Response should indicate blocking by Sentinel with custom resource and fallback response", + result.contains("ABC blocked!")); + System.out.println("Custom config flow control test completed: " + result); } From 8776b5cd95b898f5bc43810d5def655490239d08 Mon Sep 17 00:00:00 2001 From: uuuyuqi Date: Tue, 10 Mar 2026 15:11:19 +0800 Subject: [PATCH 3/7] fix: resolve markdown lint errors in README Change-Id: I2df54be42732b1c1786b4b9189639913b4598fd9 Co-developed-by: OpenCode --- .../sentinel-spring-restclient-adapter/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/README.md b/sentinel-adapter/sentinel-spring-restclient-adapter/README.md index 1544b05078..61f4ea6b4d 100644 --- a/sentinel-adapter/sentinel-spring-restclient-adapter/README.md +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/README.md @@ -106,6 +106,7 @@ FlowRuleManager.loadRules(Collections.singletonList(rule)); ### SentinelRestClientInterceptor The main interceptor implementation responsible for: + - Creating Sentinel resources for each HTTP request - Catching BlockException and invoking fallback handler - Tracing exceptions and 5xx errors @@ -113,6 +114,7 @@ The main interceptor implementation responsible for: ### SentinelRestClientConfig Configuration class containing: + - `resourcePrefix`: Resource name prefix (default: `restclient:`) - `resourceExtractor`: Resource name extractor - `fallback`: Fallback handler @@ -130,6 +132,7 @@ Interface for fallback handling, invoked when requests are blocked by flow contr The default resource name format: `{prefix}{METHOD}:{URL}` Examples: + - `restclient:GET:https://httpbin.org/get` - `restclient:POST:http://localhost:8080/api/users` @@ -137,7 +140,6 @@ Examples: This adapter only supports `RestClient` from Spring Framework 6.0+, not `RestTemplate`. - ## Integration with Spring Cloud Alibaba This adapter provides basic Sentinel integration. For Spring Cloud Alibaba projects: @@ -147,4 +149,5 @@ This adapter provides basic Sentinel integration. For Spring Cloud Alibaba proje ## License -Apache License 2.0 \ No newline at end of file +Apache License 2.0 + From ab50edd239c5e83f5b5c4a4aa59c756ddabcc40d Mon Sep 17 00:00:00 2001 From: uuuyuqi Date: Tue, 10 Mar 2026 15:18:48 +0800 Subject: [PATCH 4/7] fix: skip tests on Java < 17 for Spring 6.x compatibility Change-Id: Ia9dceb36c2ef3a9a86a61110446c6aec4de331a9 Co-developed-by: OpenCode --- .../sentinel-spring-restclient-adapter/pom.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/pom.xml b/sentinel-adapter/sentinel-spring-restclient-adapter/pom.xml index 9bc0df0e42..df69ac0f9b 100644 --- a/sentinel-adapter/sentinel-spring-restclient-adapter/pom.xml +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/pom.xml @@ -19,6 +19,8 @@ 6.1.0 3.2.0 6.1.0 + + false @@ -69,4 +71,17 @@ test + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire.version} + + ${skip.spring.v6x.test} + + + + \ No newline at end of file From 9663e55e6b7201ba41804e62da15d57e31b73060 Mon Sep 17 00:00:00 2001 From: uuuyuqi Date: Tue, 10 Mar 2026 15:33:00 +0800 Subject: [PATCH 5/7] fix: remove extra trailing blank line in README Change-Id: I10f1752fa921aa8980e640c4fe7726d3c1764f37 Co-developed-by: OpenCode --- sentinel-adapter/sentinel-spring-restclient-adapter/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/README.md b/sentinel-adapter/sentinel-spring-restclient-adapter/README.md index 61f4ea6b4d..d3461cd377 100644 --- a/sentinel-adapter/sentinel-spring-restclient-adapter/README.md +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/README.md @@ -150,4 +150,3 @@ This adapter provides basic Sentinel integration. For Spring Cloud Alibaba proje ## License Apache License 2.0 - From 56c4054c8d79ebce4c2722ee58a2101b28f0ad84 Mon Sep 17 00:00:00 2001 From: uuuyuqi Date: Thu, 19 Mar 2026 11:55:34 +0800 Subject: [PATCH 6/7] fix: address PR review comments for restclient adapter - DefaultRestClientFallback now throws SentinelRpcException for consistency with other client adapters (okhttp, apache-httpclient) - SentinelClientHttpResponse uses text/plain instead of application/json since the body is plain text - Update tests to reflect new exception-throwing behavior Change-Id: I778bb9bba1bd24435e667f996ebbff19d734b14a Co-developed-by: Claude Co-Authored-By: Claude Sonnet 4.6 --- .../SentinelClientHttpResponse.java | 2 +- .../SentinelRestClientInterceptor.java | 20 ++--- .../fallback/DefaultRestClientFallback.java | 6 +- ...ntinelRestClientInterceptorSimpleTest.java | 30 ++----- .../DefaultRestClientFallbackTest.java | 88 ++++++++++++------- 5 files changed, 74 insertions(+), 72 deletions(-) diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponse.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponse.java index ae865fb9b5..ce6e67ab21 100644 --- a/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponse.java +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponse.java @@ -72,7 +72,7 @@ public InputStream getBody() throws IOException { public HttpHeaders getHeaders() { Map> headers = new HashMap<>(); headers.put(HttpHeaders.CONTENT_TYPE, - Arrays.asList(MediaType.APPLICATION_JSON_VALUE)); + Arrays.asList(MediaType.TEXT_PLAIN_VALUE)); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.putAll(headers); return httpHeaders; diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptor.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptor.java index 7a16f05ea3..a293494111 100644 --- a/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptor.java +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptor.java @@ -15,26 +15,20 @@ */ package com.alibaba.csp.sentinel.adapter.spring.restclient; -import java.io.IOException; -import java.net.URI; - -import com.alibaba.csp.sentinel.Entry; -import com.alibaba.csp.sentinel.EntryType; -import com.alibaba.csp.sentinel.ResourceTypeConstants; -import com.alibaba.csp.sentinel.SphU; -import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.*; import com.alibaba.csp.sentinel.adapter.spring.restclient.extractor.RestClientResourceExtractor; import com.alibaba.csp.sentinel.adapter.spring.restclient.fallback.RestClientFallback; import com.alibaba.csp.sentinel.slots.block.BlockException; -import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException; import com.alibaba.csp.sentinel.util.AssertUtil; import com.alibaba.csp.sentinel.util.StringUtil; - import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; +import java.io.IOException; +import java.net.URI; + /** * {@link ClientHttpRequestInterceptor} for integrating Sentinel with Spring's * {@link org.springframework.web.client.RestClient}. @@ -105,13 +99,17 @@ public ClientHttpResponse intercept(HttpRequest request, byte[] body, if (response.getStatusCode().is5xxServerError()) { RuntimeException ex = new RuntimeException("Server error: " + response.getStatusCode().value()); - Tracer.trace(ex); + Tracer.traceEntry(ex, hostEntry); + if (pathEntry != null) { + Tracer.traceEntry(ex, pathEntry); + } } return response; } catch (BlockException ex) { return handleBlockException(request, body, execution, ex); } catch (IOException ex) { + // Path entry does not need to be traced if an IO exception occurred. Tracer.traceEntry(ex, hostEntry); throw ex; } finally { diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/DefaultRestClientFallback.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/DefaultRestClientFallback.java index 28ff1cbfc2..4bc4e407fb 100644 --- a/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/DefaultRestClientFallback.java +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/DefaultRestClientFallback.java @@ -15,8 +15,8 @@ */ package com.alibaba.csp.sentinel.adapter.spring.restclient.fallback; -import com.alibaba.csp.sentinel.adapter.spring.restclient.SentinelClientHttpResponse; import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.SentinelRpcException; import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; @@ -32,7 +32,7 @@ public class DefaultRestClientFallback implements RestClientFallback { @Override public ClientHttpResponse handle(HttpRequest request, byte[] body, ClientHttpRequestExecution execution, BlockException ex) { - return new SentinelClientHttpResponse("RestClient request blocked by Sentinel: " - + ex.getClass().getSimpleName()); + // Just wrap and throw the exception. + throw new SentinelRpcException(ex); } } \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptorSimpleTest.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptorSimpleTest.java index 90fd096477..8147587e2c 100644 --- a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptorSimpleTest.java +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptorSimpleTest.java @@ -17,8 +17,6 @@ import com.alibaba.csp.sentinel.Constants; import com.alibaba.csp.sentinel.adapter.spring.restclient.app.TestApplication; -import com.alibaba.csp.sentinel.adapter.spring.restclient.extractor.DefaultRestClientResourceExtractor; -import com.alibaba.csp.sentinel.adapter.spring.restclient.fallback.DefaultRestClientFallback; import com.alibaba.csp.sentinel.node.ClusterNode; import com.alibaba.csp.sentinel.slots.block.RuleConstant; import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; @@ -26,7 +24,6 @@ import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; - import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -37,10 +34,6 @@ import org.springframework.web.client.RestClient; import java.util.Collections; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.*; @@ -128,7 +121,7 @@ public void testDualLevelResources() { } } - @Test + @Test(expected = com.alibaba.csp.sentinel.slots.block.SentinelRpcException.class) public void testFlowControlBlocking() { String url = "http://localhost:" + port + "/test/hello"; String pathResource = "restclient:GET:" + url; @@ -143,16 +136,13 @@ public void testFlowControlBlocking() { .requestInterceptor(new SentinelRestClientInterceptor()) .build(); - String result = restClient.get() + restClient.get() .uri(url) .retrieve() .body(String.class); - - assertNotNull("Should get fallback response", result); - System.out.println("Blocked response: " + result); } - @Test + @Test(expected = com.alibaba.csp.sentinel.slots.block.SentinelRpcException.class) public void testHostLevelFlowControl() throws InterruptedException { String url = "http://localhost:" + port + "/test/hello"; String rootResource = "restclient:GET:" + "http://localhost:" + port; @@ -167,13 +157,10 @@ public void testHostLevelFlowControl() throws InterruptedException { .requestInterceptor(new SentinelRestClientInterceptor()) .build(); - String result = restClient.get() + restClient.get() .uri(url) .retrieve() .body(String.class); - - assertNotNull("Should get fallback response", result); - System.out.println("Blocked response: " + result); } @@ -208,7 +195,7 @@ public void testCustomConfig() { } - @Test + @Test(expected = com.alibaba.csp.sentinel.slots.block.SentinelRpcException.class) public void testPostRequestFlowControl() { String url = "http://localhost:" + port + "/test/users"; String pathResource = "restclient:POST:" + url; @@ -223,16 +210,11 @@ public void testPostRequestFlowControl() { .requestInterceptor(new SentinelRestClientInterceptor()) .build(); - String result = restClient.post() + restClient.post() .uri(url) .body("Test User") .retrieve() .body(String.class); - - assertNotNull("Should get fallback response when blocked", result); - assertTrue("Response should indicate blocking by Sentinel", - result.contains("blocked by Sentinel")); - System.out.println("POST request blocked by flow control: " + result); } @Test diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/DefaultRestClientFallbackTest.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/DefaultRestClientFallbackTest.java index bd4fab95f9..d900b5652c 100644 --- a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/DefaultRestClientFallbackTest.java +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/DefaultRestClientFallbackTest.java @@ -1,17 +1,14 @@ package com.alibaba.csp.sentinel.adapter.spring.restclient.fallback; +import com.alibaba.csp.sentinel.slots.block.SentinelRpcException; import com.alibaba.csp.sentinel.slots.block.flow.FlowException; import org.junit.Test; import org.springframework.http.HttpMethod; import org.springframework.http.HttpRequest; -import org.springframework.http.client.ClientHttpResponse; -import java.io.IOException; import java.net.URI; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertSame; /** * Tests for {@link DefaultRestClientFallback}. @@ -20,32 +17,57 @@ */ public class DefaultRestClientFallbackTest { - @Test - public void testHandle() throws IOException { - DefaultRestClientFallback fallback = new DefaultRestClientFallback(); - - HttpRequest request = new HttpRequest() { - @Override - public HttpMethod getMethod() { - return HttpMethod.GET; - } - - @Override - public URI getURI() { - return URI.create("https://httpbin.org/get"); - } - - @Override - public org.springframework.http.HttpHeaders getHeaders() { - return new org.springframework.http.HttpHeaders(); - } - }; - - FlowException ex = new FlowException("test", "default"); - ClientHttpResponse response = fallback.handle(request, new byte[0], null, ex); - - assertNotNull(response); - assertTrue(response.getStatusText().contains("blocked by Sentinel")); - assertTrue(response.getStatusText().contains("FlowException")); - } + @Test(expected = SentinelRpcException.class) + public void testHandleThrowsException() { + DefaultRestClientFallback fallback = new DefaultRestClientFallback(); + + HttpRequest request = new HttpRequest() { + @Override + public HttpMethod getMethod() { + return HttpMethod.GET; + } + + @Override + public URI getURI() { + return URI.create("https://httpbin.org/get"); + } + + @Override + public org.springframework.http.HttpHeaders getHeaders() { + return new org.springframework.http.HttpHeaders(); + } + }; + + FlowException ex = new FlowException("test", "default"); + fallback.handle(request, new byte[0], null, ex); + } + + @Test + public void testHandleWrapsBlockException() { + DefaultRestClientFallback fallback = new DefaultRestClientFallback(); + + HttpRequest request = new HttpRequest() { + @Override + public HttpMethod getMethod() { + return HttpMethod.GET; + } + + @Override + public URI getURI() { + return URI.create("https://httpbin.org/get"); + } + + @Override + public org.springframework.http.HttpHeaders getHeaders() { + return new org.springframework.http.HttpHeaders(); + } + }; + + FlowException ex = new FlowException("test", "default"); + try { + fallback.handle(request, new byte[0], null, ex); + } catch (SentinelRpcException e) { + assertSame(ex, e.getCause()); + } + } } \ No newline at end of file From 9ebc14ec48b33a4bdbb19ff01b534f4eb1ce14f5 Mon Sep 17 00:00:00 2001 From: uuuyuqi Date: Thu, 19 Mar 2026 20:52:37 +0800 Subject: [PATCH 7/7] refactor: remove SentinelClientHttpResponse class No longer needed since DefaultRestClientFallback now throws SentinelRpcException. Update README, ManualTest, and integration tests accordingly. Change-Id: Ic38af10b9a82f99205aeb1bccde8411ec97d618b Co-developed-by: Claude Co-Authored-By: Claude Opus 4.6 (1M context) --- .../README.md | 4 +- .../SentinelClientHttpResponse.java | 80 ------------------- .../fallback/RestClientFallback.java | 1 - .../adapter/spring/restclient/ManualTest.java | 16 ++-- .../SentinelClientHttpResponseTest.java | 56 ------------- ...ntinelRestClientInterceptorSimpleTest.java | 11 +-- 6 files changed, 13 insertions(+), 155 deletions(-) delete mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponse.java delete mode 100644 sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponseTest.java diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/README.md b/sentinel-adapter/sentinel-spring-restclient-adapter/README.md index d3461cd377..2292fa732a 100644 --- a/sentinel-adapter/sentinel-spring-restclient-adapter/README.md +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/README.md @@ -66,9 +66,9 @@ RestClientResourceExtractor customExtractor = request -> { return request.getMethod() + ":" + request.getURI().getHost() + path; }; -// Custom fallback response +// Custom fallback: throw a custom exception when blocked RestClientFallback customFallback = (request, body, execution, ex) -> { - return new SentinelClientHttpResponse("Service temporarily unavailable, please retry later"); + throw new RuntimeException("Service temporarily unavailable, please retry later", ex); }; // Create configuration diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponse.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponse.java deleted file mode 100644 index ce6e67ab21..0000000000 --- a/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponse.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 1999-2020 Alibaba Group Holding Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * 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.alibaba.csp.sentinel.adapter.spring.restclient; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.client.ClientHttpResponse; - -/** - * Default HTTP response when request is blocked by Sentinel. - * - * @author QHT, uuuyuqi - */ -public class SentinelClientHttpResponse implements ClientHttpResponse { - - private String blockResponse = "Request blocked by Sentinel"; - - public SentinelClientHttpResponse() { - } - - public SentinelClientHttpResponse(String blockResponse) { - this.blockResponse = blockResponse; - } - - @Override - public HttpStatus getStatusCode() throws IOException { - return HttpStatus.OK; - } - - @Override - public int getRawStatusCode() throws IOException { - return HttpStatus.OK.value(); - } - - @Override - public String getStatusText() throws IOException { - return blockResponse; - } - - @Override - public void close() { - } - - @Override - public InputStream getBody() throws IOException { - return new ByteArrayInputStream(blockResponse.getBytes()); - } - - @Override - public HttpHeaders getHeaders() { - Map> headers = new HashMap<>(); - headers.put(HttpHeaders.CONTENT_TYPE, - Arrays.asList(MediaType.TEXT_PLAIN_VALUE)); - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.putAll(headers); - return httpHeaders; - } -} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/RestClientFallback.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/RestClientFallback.java index 1e970f9cb5..f507573803 100644 --- a/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/RestClientFallback.java +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/restclient/fallback/RestClientFallback.java @@ -15,7 +15,6 @@ */ package com.alibaba.csp.sentinel.adapter.spring.restclient.fallback; -import com.alibaba.csp.sentinel.adapter.spring.restclient.SentinelClientHttpResponse; import com.alibaba.csp.sentinel.slots.block.BlockException; import org.springframework.http.HttpRequest; diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/ManualTest.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/ManualTest.java index 45aed28aa8..e21c9619f7 100644 --- a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/ManualTest.java +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/ManualTest.java @@ -10,7 +10,6 @@ import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; -import org.springframework.http.client.ClientHttpResponse; import org.springframework.web.client.RestClient; import com.alibaba.csp.sentinel.slots.block.BlockException; @@ -83,15 +82,15 @@ private static void testWithFlowControl() { .build(); try { - String result = restClient.get() + restClient.get() .uri("https://httpbin.org/delay/1") .retrieve() .body(String.class); - - System.out.println("Response: " + result); - System.out.println("✅ Flow control test completed!"); + System.out.println("❌ Request should have been blocked!"); + } catch (com.alibaba.csp.sentinel.slots.block.SentinelRpcException e) { + System.out.println("✅ Request blocked as expected: " + e.getCause().getClass().getSimpleName()); } catch (Exception e) { - System.out.println("❌ Test error: " + e.getMessage()); + System.out.println("❌ Unexpected exception: " + e.getMessage()); } FlowRuleManager.loadRules(Collections.emptyList()); @@ -112,8 +111,9 @@ private static void testWithCustomExtractor() { }; RestClientFallback customFallback = (HttpRequest request, byte[] body, - ClientHttpRequestExecution execution, BlockException ex) -> - new SentinelClientHttpResponse("Custom fallback: " + ex.getClass().getSimpleName()); + ClientHttpRequestExecution execution, BlockException ex) -> { + throw new RuntimeException("Custom fallback: " + ex.getClass().getSimpleName(), ex); + }; SentinelRestClientConfig config = new SentinelRestClientConfig( "custom:", diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponseTest.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponseTest.java deleted file mode 100644 index 5becbc6c13..0000000000 --- a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelClientHttpResponseTest.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 1999-2020 Alibaba Group Holding Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * 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.alibaba.csp.sentinel.adapter.spring.restclient; - -import org.junit.Test; - -import java.io.IOException; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * Tests for {@link SentinelClientHttpResponse}. - * - * @author uuuyuqi - */ -public class SentinelClientHttpResponseTest { - - @Test - public void testDefaultResponse() throws IOException { - SentinelClientHttpResponse response = new SentinelClientHttpResponse(); - assertEquals("Request blocked by Sentinel", response.getStatusText()); - } - - @Test - public void testCustomResponse() throws IOException { - String customMessage = "Custom blocked message"; - SentinelClientHttpResponse response = new SentinelClientHttpResponse(customMessage); - assertEquals(customMessage, response.getStatusText()); - } - - @Test - public void testResponseProperties() throws IOException { - SentinelClientHttpResponse response = new SentinelClientHttpResponse("test"); - - assertNotNull(response.getStatusCode()); - assertEquals(200, response.getRawStatusCode()); - assertNotNull(response.getBody()); - assertNotNull(response.getHeaders()); - assertTrue(response.getHeaders().containsKey("Content-Type")); - } -} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptorSimpleTest.java b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptorSimpleTest.java index 8147587e2c..9d37fefe41 100644 --- a/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptorSimpleTest.java +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptorSimpleTest.java @@ -164,7 +164,7 @@ public void testHostLevelFlowControl() throws InterruptedException { } - @Test + @Test(expected = IllegalStateException.class) public void testCustomConfig() { String customPrefix = "my-api:"; String url = "http://localhost:" + port + "/test/hello"; @@ -178,20 +178,15 @@ public void testCustomConfig() { SentinelRestClientConfig config = new SentinelRestClientConfig( customPrefix, request -> "abc", - (a,b,c,d) -> new SentinelClientHttpResponse("ABC blocked!" )); + (a, b, c, d) -> { throw new IllegalStateException("custom fallback triggered"); }); RestClient restClient = RestClient.builder() .requestInterceptor(new SentinelRestClientInterceptor(config)) .build(); - String result = restClient.get() + restClient.get() .uri(url) .retrieve() .body(String.class); - - assertNotNull("Should get fallback response when blocked", result); - assertTrue("Response should indicate blocking by Sentinel with custom resource and fallback response", - result.contains("ABC blocked!")); - System.out.println("Custom config flow control test completed: " + result); }