This interceptor creates two levels of Sentinel resources for each request: + *
This dual-level design allows: + *
Supports: + *
Extracts resource name in format: {@code METHOD:scheme://host[:port]/path} + * + *
Examples: + *
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..90fd096477
--- /dev/null
+++ b/sentinel-adapter/sentinel-spring-restclient-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/restclient/SentinelRestClientInterceptorSimpleTest.java
@@ -0,0 +1,321 @@
+/*
+ * 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.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;
+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";
+
+ 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,
+ request -> "abc",
+ (a,b,c,d) -> new SentinelClientHttpResponse("ABC blocked!" ));
+ 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 with custom resource and fallback response",
+ result.contains("ABC blocked!"));
+ System.out.println("Custom config 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