Skip to content

Commit 38b4619

Browse files
uuuyuqiclaude
andauthored
feat: support Spring RestClient (#3602)
* feat: support Spring RestClient Change-Id: I49c8b1cf7a0a6f357b68b58bd352efc5981b76b2 Co-developed-by: OpenCode <[email protected]> * add customized resourceExtractor and fallback test Change-Id: Ieacaf949222f5871f934d2f0aa3bd6d73d1a087b * fix: resolve markdown lint errors in README Change-Id: I2df54be42732b1c1786b4b9189639913b4598fd9 Co-developed-by: OpenCode <[email protected]> * fix: skip tests on Java < 17 for Spring 6.x compatibility Change-Id: Ia9dceb36c2ef3a9a86a61110446c6aec4de331a9 Co-developed-by: OpenCode <[email protected]> * fix: remove extra trailing blank line in README Change-Id: I10f1752fa921aa8980e640c4fe7726d3c1764f37 Co-developed-by: OpenCode <[email protected]> * 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 <[email protected]> Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * 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 <[email protected]> Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]>
1 parent 52eac45 commit 38b4619

16 files changed

Lines changed: 1329 additions & 0 deletions

File tree

sentinel-adapter/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<module>sentinel-spring-webmvc-v6x-adapter</module>
3535
<module>sentinel-zuul2-adapter</module>
3636
<module>sentinel-okhttp-adapter</module>
37+
<module>sentinel-spring-restclient-adapter</module>
3738
<module>sentinel-jax-rs-adapter</module>
3839
<module>sentinel-quarkus-adapter</module>
3940
<module>sentinel-motan-adapter</module>
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Sentinel Spring RestClient Adapter
2+
3+
## Overview
4+
5+
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`.
6+
7+
## Features
8+
9+
- Flow control (QPS limiting)
10+
- Circuit breaking (degradation)
11+
- Custom resource name extraction
12+
- Custom fallback responses
13+
- HTTP 5xx error tracing
14+
15+
## Requirements
16+
17+
- Spring Framework 6.0+
18+
- JDK 17+
19+
- Sentinel Core 1.8.0+
20+
21+
## Usage
22+
23+
### 1. Add Dependency
24+
25+
```xml
26+
<dependency>
27+
<groupId>com.alibaba.csp</groupId>
28+
<artifactId>sentinel-spring-restclient-adapter</artifactId>
29+
<version>${sentinel.version}</version>
30+
</dependency>
31+
```
32+
33+
### 2. Basic Usage
34+
35+
```java
36+
import com.alibaba.csp.sentinel.adapter.spring.restclient.SentinelRestClientInterceptor;
37+
import org.springframework.web.client.RestClient;
38+
39+
// Create RestClient with Sentinel interceptor
40+
RestClient restClient = RestClient.builder()
41+
.requestInterceptor(new SentinelRestClientInterceptor())
42+
.build();
43+
44+
// Use RestClient to send requests (protected by Sentinel)
45+
String result = restClient.get()
46+
.uri("https://httpbin.org/get")
47+
.retrieve()
48+
.body(String.class);
49+
```
50+
51+
### 3. Custom Configuration
52+
53+
```java
54+
import com.alibaba.csp.sentinel.adapter.spring.restclient.SentinelRestClientConfig;
55+
import com.alibaba.csp.sentinel.adapter.spring.restclient.SentinelRestClientInterceptor;
56+
import com.alibaba.csp.sentinel.adapter.spring.restclient.extractor.RestClientResourceExtractor;
57+
import com.alibaba.csp.sentinel.adapter.spring.restclient.fallback.RestClientFallback;
58+
59+
// Custom resource name extractor
60+
RestClientResourceExtractor customExtractor = request -> {
61+
// Example: normalize RESTful path parameters
62+
String path = request.getURI().getPath();
63+
if (path.matches("/users/\\d+")) {
64+
path = "/users/{id}";
65+
}
66+
return request.getMethod() + ":" + request.getURI().getHost() + path;
67+
};
68+
69+
// Custom fallback: throw a custom exception when blocked
70+
RestClientFallback customFallback = (request, body, execution, ex) -> {
71+
throw new RuntimeException("Service temporarily unavailable, please retry later", ex);
72+
};
73+
74+
// Create configuration
75+
SentinelRestClientConfig config = new SentinelRestClientConfig(
76+
"my-restclient:", // Resource name prefix
77+
customExtractor,
78+
customFallback
79+
);
80+
81+
// Create interceptor with custom configuration
82+
RestClient restClient = RestClient.builder()
83+
.requestInterceptor(new SentinelRestClientInterceptor(config))
84+
.build();
85+
```
86+
87+
### 4. Configure Sentinel Rules
88+
89+
```java
90+
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
91+
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
92+
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
93+
import java.util.Collections;
94+
95+
// Configure flow control rule
96+
FlowRule rule = new FlowRule("restclient:GET:https://httpbin.org/get");
97+
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
98+
rule.setCount(10); // Max 10 requests per second
99+
rule.setLimitApp("default");
100+
101+
FlowRuleManager.loadRules(Collections.singletonList(rule));
102+
```
103+
104+
## Core Components
105+
106+
### SentinelRestClientInterceptor
107+
108+
The main interceptor implementation responsible for:
109+
110+
- Creating Sentinel resources for each HTTP request
111+
- Catching BlockException and invoking fallback handler
112+
- Tracing exceptions and 5xx errors
113+
114+
### SentinelRestClientConfig
115+
116+
Configuration class containing:
117+
118+
- `resourcePrefix`: Resource name prefix (default: `restclient:`)
119+
- `resourceExtractor`: Resource name extractor
120+
- `fallback`: Fallback handler
121+
122+
### RestClientResourceExtractor
123+
124+
Interface for resource name extraction, allowing customization of resource name generation logic.
125+
126+
### RestClientFallback
127+
128+
Interface for fallback handling, invoked when requests are blocked by flow control or circuit breaking.
129+
130+
## Resource Name Format
131+
132+
The default resource name format: `{prefix}{METHOD}:{URL}`
133+
134+
Examples:
135+
136+
- `restclient:GET:https://httpbin.org/get`
137+
- `restclient:POST:http://localhost:8080/api/users`
138+
139+
## Notes
140+
141+
This adapter only supports `RestClient` from Spring Framework 6.0+, not `RestTemplate`.
142+
143+
## Integration with Spring Cloud Alibaba
144+
145+
This adapter provides basic Sentinel integration. For Spring Cloud Alibaba projects:
146+
147+
1. Add auto-configuration support in `spring-cloud-starter-alibaba-sentinel`
148+
2. Use `@SentinelRestClient` annotation for simplified configuration
149+
150+
## License
151+
152+
Apache License 2.0
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<parent>
6+
<groupId>com.alibaba.csp</groupId>
7+
<artifactId>sentinel-adapter</artifactId>
8+
<version>${revision}</version>
9+
<relativePath>../pom.xml</relativePath>
10+
</parent>
11+
<modelVersion>4.0.0</modelVersion>
12+
13+
<name>${project.groupId}:${project.artifactId}</name>
14+
15+
<artifactId>sentinel-spring-restclient-adapter</artifactId>
16+
<packaging>jar</packaging>
17+
18+
<properties>
19+
<spring-web.version>6.1.0</spring-web.version>
20+
<spring-boot.version>3.2.0</spring-boot.version>
21+
<spring-test.version>6.1.0</spring-test.version>
22+
23+
<skip.spring.v6x.test>false</skip.spring.v6x.test>
24+
</properties>
25+
26+
<dependencies>
27+
<dependency>
28+
<groupId>com.alibaba.csp</groupId>
29+
<artifactId>sentinel-core</artifactId>
30+
</dependency>
31+
32+
<dependency>
33+
<groupId>org.springframework</groupId>
34+
<artifactId>spring-web</artifactId>
35+
<version>${spring-web.version}</version>
36+
<scope>provided</scope>
37+
</dependency>
38+
39+
<dependency>
40+
<groupId>junit</groupId>
41+
<artifactId>junit</artifactId>
42+
<scope>test</scope>
43+
</dependency>
44+
<dependency>
45+
<groupId>org.mockito</groupId>
46+
<artifactId>mockito-core</artifactId>
47+
<scope>test</scope>
48+
</dependency>
49+
<dependency>
50+
<groupId>com.alibaba</groupId>
51+
<artifactId>fastjson</artifactId>
52+
<scope>test</scope>
53+
</dependency>
54+
55+
<dependency>
56+
<groupId>org.springframework.boot</groupId>
57+
<artifactId>spring-boot-starter-web</artifactId>
58+
<version>${spring-boot.version}</version>
59+
<scope>test</scope>
60+
</dependency>
61+
<dependency>
62+
<groupId>org.springframework.boot</groupId>
63+
<artifactId>spring-boot-test</artifactId>
64+
<version>${spring-boot.version}</version>
65+
<scope>test</scope>
66+
</dependency>
67+
<dependency>
68+
<groupId>org.springframework</groupId>
69+
<artifactId>spring-test</artifactId>
70+
<version>${spring-test.version}</version>
71+
<scope>test</scope>
72+
</dependency>
73+
</dependencies>
74+
75+
<build>
76+
<plugins>
77+
<plugin>
78+
<groupId>org.apache.maven.plugins</groupId>
79+
<artifactId>maven-surefire-plugin</artifactId>
80+
<version>${maven.surefire.version}</version>
81+
<configuration>
82+
<skipTests>${skip.spring.v6x.test}</skipTests>
83+
</configuration>
84+
</plugin>
85+
</plugins>
86+
</build>
87+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 1999-2020 Alibaba Group Holding Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.alibaba.csp.sentinel.adapter.spring.restclient;
17+
18+
import com.alibaba.csp.sentinel.adapter.spring.restclient.extractor.DefaultRestClientResourceExtractor;
19+
import com.alibaba.csp.sentinel.adapter.spring.restclient.extractor.RestClientResourceExtractor;
20+
import com.alibaba.csp.sentinel.adapter.spring.restclient.fallback.DefaultRestClientFallback;
21+
import com.alibaba.csp.sentinel.adapter.spring.restclient.fallback.RestClientFallback;
22+
import com.alibaba.csp.sentinel.util.AssertUtil;
23+
24+
/**
25+
* Configuration for Sentinel RestClient interceptor.
26+
*
27+
* @author QHT, uuuyuqi
28+
*/
29+
public class SentinelRestClientConfig {
30+
31+
public static final String DEFAULT_RESOURCE_PREFIX = "restclient:";
32+
33+
private final String resourcePrefix;
34+
private final RestClientResourceExtractor resourceExtractor;
35+
private final RestClientFallback fallback;
36+
37+
public SentinelRestClientConfig() {
38+
this(DEFAULT_RESOURCE_PREFIX);
39+
}
40+
41+
public SentinelRestClientConfig(String resourcePrefix) {
42+
this(resourcePrefix, new DefaultRestClientResourceExtractor(), new DefaultRestClientFallback());
43+
}
44+
45+
public SentinelRestClientConfig(RestClientResourceExtractor resourceExtractor, RestClientFallback fallback) {
46+
this(DEFAULT_RESOURCE_PREFIX, resourceExtractor, fallback);
47+
}
48+
49+
public SentinelRestClientConfig(String resourcePrefix,
50+
RestClientResourceExtractor resourceExtractor,
51+
RestClientFallback fallback) {
52+
AssertUtil.notNull(resourceExtractor, "resourceExtractor cannot be null");
53+
AssertUtil.notNull(fallback, "fallback cannot be null");
54+
this.resourcePrefix = resourcePrefix;
55+
this.resourceExtractor = resourceExtractor;
56+
this.fallback = fallback;
57+
}
58+
59+
public String getResourcePrefix() {
60+
return resourcePrefix;
61+
}
62+
63+
public RestClientResourceExtractor getResourceExtractor() {
64+
return resourceExtractor;
65+
}
66+
67+
public RestClientFallback getFallback() {
68+
return fallback;
69+
}
70+
71+
@Override
72+
public String toString() {
73+
return "SentinelRestClientConfig{" +
74+
"resourcePrefix='" + resourcePrefix + '\'' +
75+
", resourceExtractor=" + resourceExtractor +
76+
", fallback=" + fallback +
77+
'}';
78+
}
79+
}

0 commit comments

Comments
 (0)