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..d3461cd377 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/README.md @@ -0,0 +1,152 @@ +# 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 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..df69ac0f9b --- /dev/null +++ b/sentinel-adapter/sentinel-spring-restclient-adapter/pom.xml @@ -0,0 +1,87 @@ + + + + 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 + + false + + + + + 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 + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire.version} + + ${skip.spring.v6x.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: + *

+ * + *

This dual-level design allows: + *

+ * + *

Supports: + *

+ * + * @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: + *

+ * + *

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