Add webmvc-6x-adapter module
This commit is contained in:
parent
d995dfe2e7
commit
d1d3d36054
|
|
@ -28,6 +28,7 @@
|
||||||
<module>sentinel-api-gateway-adapter-common</module>
|
<module>sentinel-api-gateway-adapter-common</module>
|
||||||
<module>sentinel-spring-cloud-gateway-adapter</module>
|
<module>sentinel-spring-cloud-gateway-adapter</module>
|
||||||
<module>sentinel-spring-webmvc-adapter</module>
|
<module>sentinel-spring-webmvc-adapter</module>
|
||||||
|
<module>sentinel-spring-webmvc-6x-adapter</module>
|
||||||
<module>sentinel-zuul2-adapter</module>
|
<module>sentinel-zuul2-adapter</module>
|
||||||
<module>sentinel-okhttp-adapter</module>
|
<module>sentinel-okhttp-adapter</module>
|
||||||
<module>sentinel-jax-rs-adapter</module>
|
<module>sentinel-jax-rs-adapter</module>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
# Sentinel Spring MVC Adapter
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Sentinel provides integration for Spring Web to enable flow control for web requests.
|
||||||
|
|
||||||
|
Add the following dependency in `pom.xml` (if you are using Maven):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.csp</groupId>
|
||||||
|
<artifactId>sentinel-spring-webmvc-adapter</artifactId>
|
||||||
|
<version>x.y.z</version>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
Then we could add a configuration bean to configure the interceptor:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Configuration
|
||||||
|
public class InterceptorConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
SentinelWebMvcConfig config = new SentinelWebMvcConfig();
|
||||||
|
// Enable the HTTP method prefix.
|
||||||
|
config.setHttpMethodSpecify(true);
|
||||||
|
// Add to the interceptor list.
|
||||||
|
registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then Sentinel will extract URL patterns defined in Web Controller as the web resource (e.g. `/foo/{id}`).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Block handling
|
||||||
|
|
||||||
|
Sentinel Spring Web adapter provides a `BlockExceptionHandler` interface to handle the blocked requests.
|
||||||
|
We could set the handler via `SentinelWebMvcTotalConfig#setBlockExceptionHandler()` method.
|
||||||
|
|
||||||
|
By default the interceptor will throw out the `BlockException`.
|
||||||
|
We need to set a global exception handler function in Spring to handle it. An example:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ControllerAdvice
|
||||||
|
@Order(0)
|
||||||
|
public class SentinelBlockExceptionHandlerConfig {
|
||||||
|
private Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
|
||||||
|
@ExceptionHandler(BlockException.class)
|
||||||
|
@ResponseBody
|
||||||
|
public String sentinelBlockHandler(BlockException e) {
|
||||||
|
AbstractRule rule = e.getRule();
|
||||||
|
logger.info("Blocked by Sentinel: {}", rule.toString());
|
||||||
|
return "Blocked by Sentinel";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We've provided a `DefaultBlockExceptionHandler`. When a request is blocked, the handler will return a default page
|
||||||
|
indicating the request is rejected (`Blocked by Sentinel (flow limiting)`).
|
||||||
|
The HTTP status code of the default block page is **429 (Too Many Requests)**.
|
||||||
|
|
||||||
|
We could also implement our implementation of the `BlockExceptionHandler` interface and
|
||||||
|
set to the config object. An example:
|
||||||
|
|
||||||
|
```java
|
||||||
|
SentinelWebMvcConfig config = new SentinelWebMvcConfig();
|
||||||
|
config.setBlockExceptionHandler((request, response, e) -> {
|
||||||
|
String resourceName = e.getRule().getResource();
|
||||||
|
// Depending on your situation, you can choose to process or throw
|
||||||
|
if ("/hello".equals(resourceName)) {
|
||||||
|
// Do something ......
|
||||||
|
response.getWriter().write("Blocked by Sentinel");
|
||||||
|
} else {
|
||||||
|
// Handle it in global exception handling
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customized configuration
|
||||||
|
|
||||||
|
- Common configuration in `SentinelWebMvcConfig` and `SentinelWebMvcTotalConfig`:
|
||||||
|
|
||||||
|
| name | description | type | default value |
|
||||||
|
|------|------------|------|-------|
|
||||||
|
| `blockExceptionHandler`| The handler that handles the block request | `BlockExceptionHandler` | null (throw out the BlockException) |
|
||||||
|
| `originParser` | Extracting request origin (e.g. IP or appName from HTTP Header) from HTTP request | `RequestOriginParser` | - |
|
||||||
|
|
||||||
|
- `SentinelWebMvcConfig` configuration:
|
||||||
|
|
||||||
|
| name | description | type | default value |
|
||||||
|
|------|------------|------|-------|
|
||||||
|
| urlCleaner | The `UrlCleaner` interface is designed for clean and unify the URL resource. | `UrlCleaner` | - |
|
||||||
|
| requestAttributeName | Attribute key in request used by Sentinel (internal) | `String` | `$$sentinel_spring_web_entry_attr` |
|
||||||
|
| httpMethodSpecify | Specify whether the URL resource name should contain the HTTP method prefix (e.g. `POST:`). | `boolean` | `false` |
|
||||||
|
| webContextUnify | Specify whether unify web context(i.e. use the default context name). | `boolean` | `true` |
|
||||||
|
|
||||||
|
- `SentinelWebMvcTotalConfig` configuration:
|
||||||
|
|
||||||
|
| name | description | type | default value |
|
||||||
|
|------|------------|------|-------|
|
||||||
|
| totalResourceName | The resource name in `SentinelTotalInterceptor` | `String` | `spring-mvc-total-url-request` |
|
||||||
|
| requestAttributeName | Attribute key in request used by Sentinel (internal) | `String` | `$$sentinel_spring_web_total_entry_attr` |
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<parent>
|
||||||
|
<groupId>com.alibaba.csp</groupId>
|
||||||
|
<artifactId>sentinel-adapter</artifactId>
|
||||||
|
<version>2.0.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>sentinel-spring-webmvc-6x-adapter</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<spring.version>6.0.2</spring.version>
|
||||||
|
<spring.boot.version>3.0.0</spring.boot.version>
|
||||||
|
<servlet.api.version>6.0.0</servlet.api.version>
|
||||||
|
<java.source.version>17</java.source.version>
|
||||||
|
<java.target.version>17</java.target.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.csp</groupId>
|
||||||
|
<artifactId>sentinel-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>jakarta.servlet</groupId>
|
||||||
|
<artifactId>jakarta.servlet-api</artifactId>
|
||||||
|
<version>${servlet.api.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-webmvc</artifactId>
|
||||||
|
<version>${spring.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
<version>${spring.boot.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<version>${spring.boot.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba</groupId>
|
||||||
|
<artifactId>fastjson</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>${maven.compiler.version}</version>
|
||||||
|
<configuration>
|
||||||
|
<source>${java.source.version}</source>
|
||||||
|
<target>${java.target.version}</target>
|
||||||
|
<encoding>${java.encoding}</encoding>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 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
|
||||||
|
*
|
||||||
|
* https://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.webmvc;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
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.webmvc.config.BaseWebMvcConfig;
|
||||||
|
import com.alibaba.csp.sentinel.context.ContextUtil;
|
||||||
|
import com.alibaba.csp.sentinel.log.RecordLog;
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||||
|
import com.alibaba.csp.sentinel.util.AssertUtil;
|
||||||
|
import com.alibaba.csp.sentinel.util.StringUtil;
|
||||||
|
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since request may be reprocessed in flow if any forwarding or including or other action
|
||||||
|
* happened (see {@link jakarta.servlet.ServletRequest#getDispatcherType()}) we will only
|
||||||
|
* deal with the initial request. So we use <b>reference count</b> to track in
|
||||||
|
* dispathing "onion" though which we could figure out whether we are in initial type "REQUEST".
|
||||||
|
* That means the sub-requests which we rarely meet in practice will NOT be recorded in Sentinel.
|
||||||
|
* <p>
|
||||||
|
* How to implement a forward sub-request in your action:
|
||||||
|
* <pre>
|
||||||
|
* initalRequest() {
|
||||||
|
* ModelAndView mav = new ModelAndView();
|
||||||
|
* mav.setViewName("another");
|
||||||
|
* return mav;
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @author kaizi2009
|
||||||
|
* @since 1.7.1
|
||||||
|
*/
|
||||||
|
public abstract class AbstractSentinelInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
|
public static final String SENTINEL_SPRING_WEB_CONTEXT_NAME = "sentinel_spring_web_context";
|
||||||
|
private static final String EMPTY_ORIGIN = "";
|
||||||
|
|
||||||
|
private final BaseWebMvcConfig baseWebMvcConfig;
|
||||||
|
|
||||||
|
public AbstractSentinelInterceptor(BaseWebMvcConfig config) {
|
||||||
|
AssertUtil.notNull(config, "BaseWebMvcConfig should not be null");
|
||||||
|
AssertUtil.assertNotBlank(config.getRequestAttributeName(), "requestAttributeName should not be blank");
|
||||||
|
this.baseWebMvcConfig = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param request
|
||||||
|
* @param rcKey
|
||||||
|
* @param step
|
||||||
|
* @return reference count after increasing (initial value as zero to be increased)
|
||||||
|
*/
|
||||||
|
private Integer increaseReferece(HttpServletRequest request, String rcKey, int step) {
|
||||||
|
Object obj = request.getAttribute(rcKey);
|
||||||
|
|
||||||
|
if (obj == null) {
|
||||||
|
// initial
|
||||||
|
obj = Integer.valueOf(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer newRc = (Integer)obj + step;
|
||||||
|
request.setAttribute(rcKey, newRc);
|
||||||
|
return newRc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||||
|
throws Exception {
|
||||||
|
try {
|
||||||
|
String resourceName = getResourceName(request);
|
||||||
|
|
||||||
|
if (StringUtil.isEmpty(resourceName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (increaseReferece(request, this.baseWebMvcConfig.getRequestRefName(), 1) != 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the request origin using registered origin parser.
|
||||||
|
String origin = parseOrigin(request);
|
||||||
|
String contextName = getContextName(request);
|
||||||
|
ContextUtil.enter(contextName, origin);
|
||||||
|
Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
|
||||||
|
request.setAttribute(baseWebMvcConfig.getRequestAttributeName(), entry);
|
||||||
|
return true;
|
||||||
|
} catch (BlockException e) {
|
||||||
|
try {
|
||||||
|
handleBlockException(request, response, e);
|
||||||
|
} finally {
|
||||||
|
ContextUtil.exit();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the resource name of the target web resource.
|
||||||
|
*
|
||||||
|
* @param request web request
|
||||||
|
* @return the resource name of the target web resource.
|
||||||
|
*/
|
||||||
|
protected abstract String getResourceName(HttpServletRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the context name of the target web resource.
|
||||||
|
*
|
||||||
|
* @param request web request
|
||||||
|
* @return the context name of the target web resource.
|
||||||
|
*/
|
||||||
|
protected String getContextName(HttpServletRequest request) {
|
||||||
|
return SENTINEL_SPRING_WEB_CONTEXT_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
|
||||||
|
Object handler, Exception ex) throws Exception {
|
||||||
|
if (increaseReferece(request, this.baseWebMvcConfig.getRequestRefName(), -1) != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entry entry = getEntryInRequest(request, baseWebMvcConfig.getRequestAttributeName());
|
||||||
|
if (entry == null) {
|
||||||
|
// should not happen
|
||||||
|
RecordLog.warn("[{}] No entry found in request, key: {}",
|
||||||
|
getClass().getSimpleName(), baseWebMvcConfig.getRequestAttributeName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
traceExceptionAndExit(entry, ex);
|
||||||
|
removeEntryInRequest(request);
|
||||||
|
ContextUtil.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
|
||||||
|
ModelAndView modelAndView) throws Exception {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Entry getEntryInRequest(HttpServletRequest request, String attrKey) {
|
||||||
|
Object entryObject = request.getAttribute(attrKey);
|
||||||
|
return entryObject == null ? null : (Entry)entryObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void removeEntryInRequest(HttpServletRequest request) {
|
||||||
|
request.removeAttribute(baseWebMvcConfig.getRequestAttributeName());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void traceExceptionAndExit(Entry entry, Exception ex) {
|
||||||
|
if (entry != null) {
|
||||||
|
if (ex != null) {
|
||||||
|
Tracer.traceEntry(ex, entry);
|
||||||
|
}
|
||||||
|
entry.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void handleBlockException(HttpServletRequest request, HttpServletResponse response, BlockException e)
|
||||||
|
throws Exception {
|
||||||
|
if (baseWebMvcConfig.getBlockExceptionHandler() != null) {
|
||||||
|
baseWebMvcConfig.getBlockExceptionHandler().handle(request, response, e);
|
||||||
|
} else {
|
||||||
|
// Throw BlockException directly. Users need to handle it in Spring global exception handler.
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String parseOrigin(HttpServletRequest request) {
|
||||||
|
String origin = EMPTY_ORIGIN;
|
||||||
|
if (baseWebMvcConfig.getOriginParser() != null) {
|
||||||
|
origin = baseWebMvcConfig.getOriginParser().parseOrigin(request);
|
||||||
|
if (StringUtil.isEmpty(origin)) {
|
||||||
|
return EMPTY_ORIGIN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 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
|
||||||
|
*
|
||||||
|
* https://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.webmvc;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcConfig;
|
||||||
|
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.UrlCleaner;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.util.StringUtil;
|
||||||
|
import org.springframework.web.servlet.HandlerMapping;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Web MVC interceptor that integrates with Sentinel.
|
||||||
|
*
|
||||||
|
* @author kaizi2009
|
||||||
|
* @since 1.7.1
|
||||||
|
*/
|
||||||
|
public class SentinelWebInterceptor extends AbstractSentinelInterceptor {
|
||||||
|
|
||||||
|
private final SentinelWebMvcConfig config;
|
||||||
|
|
||||||
|
public SentinelWebInterceptor() {
|
||||||
|
this(new SentinelWebMvcConfig());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SentinelWebInterceptor(SentinelWebMvcConfig config) {
|
||||||
|
super(config);
|
||||||
|
if (config == null) {
|
||||||
|
// Use the default config by default.
|
||||||
|
this.config = new SentinelWebMvcConfig();
|
||||||
|
} else {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getResourceName(HttpServletRequest request) {
|
||||||
|
// Resolve the Spring Web URL pattern from the request attribute.
|
||||||
|
Object resourceNameObject = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
|
||||||
|
if (resourceNameObject == null || !(resourceNameObject instanceof String)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String resourceName = (String) resourceNameObject;
|
||||||
|
UrlCleaner urlCleaner = config.getUrlCleaner();
|
||||||
|
if (urlCleaner != null) {
|
||||||
|
resourceName = urlCleaner.clean(resourceName);
|
||||||
|
}
|
||||||
|
// Add method specification if necessary
|
||||||
|
if (StringUtil.isNotEmpty(resourceName) && config.isHttpMethodSpecify()) {
|
||||||
|
resourceName = request.getMethod().toUpperCase() + ":" + resourceName;
|
||||||
|
}
|
||||||
|
return resourceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getContextName(HttpServletRequest request) {
|
||||||
|
if (config.isWebContextUnify()) {
|
||||||
|
return super.getContextName(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getResourceName(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 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
|
||||||
|
*
|
||||||
|
* https://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.webmvc;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcTotalConfig;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The web interceptor for all requests, which will unify all URL as
|
||||||
|
* a single resource name (configured in {@link SentinelWebMvcTotalConfig}).
|
||||||
|
*
|
||||||
|
* @author kaizi2009
|
||||||
|
* @since 1.7.1
|
||||||
|
*/
|
||||||
|
public class SentinelWebTotalInterceptor extends AbstractSentinelInterceptor {
|
||||||
|
|
||||||
|
private final SentinelWebMvcTotalConfig config;
|
||||||
|
|
||||||
|
public SentinelWebTotalInterceptor(SentinelWebMvcTotalConfig config) {
|
||||||
|
super(config);
|
||||||
|
if (config == null) {
|
||||||
|
this.config = new SentinelWebMvcTotalConfig();
|
||||||
|
} else {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SentinelWebTotalInterceptor() {
|
||||||
|
this(new SentinelWebMvcTotalConfig());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getResourceName(HttpServletRequest request) {
|
||||||
|
return config.getTotalResourceName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 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
|
||||||
|
*
|
||||||
|
* https://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.webmvc.callback;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the blocked request.
|
||||||
|
*
|
||||||
|
* @author kaizi2009
|
||||||
|
*/
|
||||||
|
public interface BlockExceptionHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the request when blocked.
|
||||||
|
*
|
||||||
|
* @param request Servlet request
|
||||||
|
* @param response Servlet response
|
||||||
|
* @param e the block exception
|
||||||
|
* @throws Exception users may throw out the BlockException or other error occurs
|
||||||
|
*/
|
||||||
|
void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 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
|
||||||
|
*
|
||||||
|
* https://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.webmvc.callback;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default handler for the blocked request.
|
||||||
|
*
|
||||||
|
* @author kaizi2009
|
||||||
|
*/
|
||||||
|
public class DefaultBlockExceptionHandler implements BlockExceptionHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
|
||||||
|
// Return 429 (Too Many Requests) by default.
|
||||||
|
response.setStatus(429);
|
||||||
|
|
||||||
|
PrintWriter out = response.getWriter();
|
||||||
|
out.print("Blocked by Sentinel (flow limiting)");
|
||||||
|
out.flush();
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2018 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.webmvc.callback;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The origin parser parses request origin (e.g. IP, user, appName) from HTTP request.
|
||||||
|
*
|
||||||
|
* @author kaizi2009
|
||||||
|
*/
|
||||||
|
public interface RequestOriginParser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the origin from given HTTP request.
|
||||||
|
*
|
||||||
|
* @param request HTTP request
|
||||||
|
* @return parsed origin
|
||||||
|
*/
|
||||||
|
String parseOrigin(HttpServletRequest request);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 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
|
||||||
|
*
|
||||||
|
* https://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.webmvc.callback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unify the resource target.
|
||||||
|
*
|
||||||
|
* @author kaizi2009
|
||||||
|
*/
|
||||||
|
public interface UrlCleaner {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unify the resource target.
|
||||||
|
*
|
||||||
|
* @param originUrl the original URL
|
||||||
|
* @return the unified resource name
|
||||||
|
*/
|
||||||
|
String clean(String originUrl);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 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.webmvc.config;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
|
||||||
|
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common base configuration for Spring Web MVC adapter.
|
||||||
|
*
|
||||||
|
* @author kaizi2009
|
||||||
|
* @since 1.7.1
|
||||||
|
*/
|
||||||
|
public abstract class BaseWebMvcConfig {
|
||||||
|
|
||||||
|
protected String requestAttributeName;
|
||||||
|
protected String requestRefName;
|
||||||
|
protected BlockExceptionHandler blockExceptionHandler;
|
||||||
|
protected RequestOriginParser originParser;
|
||||||
|
|
||||||
|
public String getRequestAttributeName() {
|
||||||
|
return requestAttributeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequestAttributeName(String requestAttributeName) {
|
||||||
|
this.requestAttributeName = requestAttributeName;
|
||||||
|
this.requestRefName = this.requestAttributeName + "-rc";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paired with attr name used to track reference count.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getRequestRefName() {
|
||||||
|
return requestRefName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockExceptionHandler getBlockExceptionHandler() {
|
||||||
|
return blockExceptionHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBlockExceptionHandler(BlockExceptionHandler blockExceptionHandler) {
|
||||||
|
this.blockExceptionHandler = blockExceptionHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RequestOriginParser getOriginParser() {
|
||||||
|
return originParser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOriginParser(RequestOriginParser originParser) {
|
||||||
|
this.originParser = originParser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2018 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.webmvc.config;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.UrlCleaner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author kaizi2009
|
||||||
|
* @since 1.7.1
|
||||||
|
*/
|
||||||
|
public class SentinelWebMvcConfig extends BaseWebMvcConfig {
|
||||||
|
|
||||||
|
public static final String DEFAULT_REQUEST_ATTRIBUTE_NAME = "$$sentinel_spring_web_entry_attr";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify the URL cleaner that unifies the URL resources.
|
||||||
|
*/
|
||||||
|
private UrlCleaner urlCleaner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify whether the URL resource name should contain the HTTP method prefix (e.g. {@code POST:}).
|
||||||
|
*/
|
||||||
|
private boolean httpMethodSpecify;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify whether unify web context(i.e. use the default context name), and is true by default.
|
||||||
|
*
|
||||||
|
* @since 1.7.2
|
||||||
|
*/
|
||||||
|
private boolean webContextUnify = true;
|
||||||
|
|
||||||
|
public SentinelWebMvcConfig() {
|
||||||
|
super();
|
||||||
|
setRequestAttributeName(DEFAULT_REQUEST_ATTRIBUTE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UrlCleaner getUrlCleaner() {
|
||||||
|
return urlCleaner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SentinelWebMvcConfig setUrlCleaner(UrlCleaner urlCleaner) {
|
||||||
|
this.urlCleaner = urlCleaner;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHttpMethodSpecify() {
|
||||||
|
return httpMethodSpecify;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SentinelWebMvcConfig setHttpMethodSpecify(boolean httpMethodSpecify) {
|
||||||
|
this.httpMethodSpecify = httpMethodSpecify;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isWebContextUnify() {
|
||||||
|
return webContextUnify;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SentinelWebMvcConfig setWebContextUnify(boolean webContextUnify) {
|
||||||
|
this.webContextUnify = webContextUnify;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "SentinelWebMvcConfig{" +
|
||||||
|
"urlCleaner=" + urlCleaner +
|
||||||
|
", httpMethodSpecify=" + httpMethodSpecify +
|
||||||
|
", webContextUnify=" + webContextUnify +
|
||||||
|
", requestAttributeName='" + requestAttributeName + '\'' +
|
||||||
|
", blockExceptionHandler=" + blockExceptionHandler +
|
||||||
|
", originParser=" + originParser +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 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
|
||||||
|
*
|
||||||
|
* https://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.webmvc.config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author kaizi2009
|
||||||
|
* @since 1.7.1
|
||||||
|
*/
|
||||||
|
public class SentinelWebMvcTotalConfig extends BaseWebMvcConfig {
|
||||||
|
|
||||||
|
public static final String DEFAULT_TOTAL_RESOURCE_NAME = "spring-mvc-total-url-request";
|
||||||
|
public static final String DEFAULT_REQUEST_ATTRIBUTE_NAME = "$$sentinel_spring_web_total_entry_attr";
|
||||||
|
|
||||||
|
private String totalResourceName = DEFAULT_TOTAL_RESOURCE_NAME;
|
||||||
|
|
||||||
|
public SentinelWebMvcTotalConfig() {
|
||||||
|
super();
|
||||||
|
setRequestAttributeName(DEFAULT_REQUEST_ATTRIBUTE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTotalResourceName() {
|
||||||
|
return totalResourceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SentinelWebMvcTotalConfig setTotalResourceName(String totalResourceName) {
|
||||||
|
this.totalResourceName = totalResourceName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "SentinelWebMvcTotalConfig{" +
|
||||||
|
"totalResourceName='" + totalResourceName + '\'' +
|
||||||
|
", requestAttributeName='" + requestAttributeName + '\'' +
|
||||||
|
", blockExceptionHandler=" + blockExceptionHandler +
|
||||||
|
", originParser=" + originParser +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 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
|
||||||
|
*
|
||||||
|
* https://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.webmvc;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author kaizi2009
|
||||||
|
*/
|
||||||
|
public class ResultWrapper {
|
||||||
|
|
||||||
|
private Integer code;
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
public ResultWrapper(Integer code, String message) {
|
||||||
|
this.code = code;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCode(Integer code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResultWrapper error() {
|
||||||
|
|
||||||
|
return new ResultWrapper(-1, "System error");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResultWrapper blocked() {
|
||||||
|
return new ResultWrapper(-2, "Blocked by Sentinel");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toJsonString() {
|
||||||
|
return JSONObject.toJSONString(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 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
|
||||||
|
*
|
||||||
|
* https://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.webmvc;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
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 com.alibaba.csp.sentinel.util.StringUtil;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author kaizi2009
|
||||||
|
*/
|
||||||
|
@RunWith(SpringRunner.class)
|
||||||
|
@SpringBootTest(classes = TestApplication.class)
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
public class SentinelSpringMvcIntegrationTest {
|
||||||
|
|
||||||
|
private static final String HELLO_STR = "Hello!";
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mvc;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBase() throws Exception {
|
||||||
|
String url = "/hello";
|
||||||
|
this.mvc.perform(get(url))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().string(HELLO_STR));
|
||||||
|
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(url);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(1, cn.passQps(), 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOriginParser() throws Exception {
|
||||||
|
String springMvcPathVariableUrl = "/foo/{id}";
|
||||||
|
String limitOrigin = "userA";
|
||||||
|
final String headerName = "S-User";
|
||||||
|
configureRulesFor(springMvcPathVariableUrl, 0, limitOrigin);
|
||||||
|
|
||||||
|
// This will be passed since the caller is different: userB
|
||||||
|
this.mvc.perform(get("/foo/1").accept(MediaType.TEXT_PLAIN).header(headerName, "userB"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().string("foo 1"));
|
||||||
|
|
||||||
|
// This will be blocked since the caller is same: userA
|
||||||
|
this.mvc.perform(
|
||||||
|
get("/foo/2").accept(MediaType.APPLICATION_JSON).header(headerName, limitOrigin))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().json(ResultWrapper.blocked().toJsonString()));
|
||||||
|
|
||||||
|
// This will be passed since the caller is different: ""
|
||||||
|
this.mvc.perform(get("/foo/3").accept(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().string("foo 3"));
|
||||||
|
|
||||||
|
FlowRuleManager.loadRules(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTotalInterceptor() throws Exception {
|
||||||
|
String url = "/hello";
|
||||||
|
String totalTarget = "my_spring_mvc_total_url_request";
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
this.mvc.perform(get(url))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().string(HELLO_STR));
|
||||||
|
}
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(totalTarget);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(3, cn.passQps(), 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRuntimeException() throws Exception {
|
||||||
|
String url = "/runtimeException";
|
||||||
|
configureExceptionRulesFor(url, 3, null);
|
||||||
|
int repeat = 3;
|
||||||
|
for (int i = 0; i < repeat; i++) {
|
||||||
|
this.mvc.perform(get(url))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().string(ResultWrapper.error().toJsonString()));
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(url);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(i + 1, cn.passQps(), 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will be blocked and response json.
|
||||||
|
this.mvc.perform(get(url))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().string(ResultWrapper.blocked().toJsonString()));
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(url);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(repeat, cn.passQps(), 0.01);
|
||||||
|
assertEquals(1, cn.blockRequest(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureRulesFor(String resource, int count, String limitApp) {
|
||||||
|
FlowRule rule = new FlowRule()
|
||||||
|
.setCount(count)
|
||||||
|
.setGrade(RuleConstant.FLOW_GRADE_QPS);
|
||||||
|
rule.setResource(resource);
|
||||||
|
if (StringUtil.isNotBlank(limitApp)) {
|
||||||
|
rule.setLimitApp(limitApp);
|
||||||
|
}
|
||||||
|
FlowRuleManager.loadRules(Collections.singletonList(rule));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureExceptionRulesFor(String resource, int count, String limitApp) {
|
||||||
|
FlowRule rule = new FlowRule()
|
||||||
|
.setCount(count)
|
||||||
|
.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO);
|
||||||
|
rule.setResource(resource);
|
||||||
|
if (StringUtil.isNotBlank(limitApp)) {
|
||||||
|
rule.setLimitApp(limitApp);
|
||||||
|
}
|
||||||
|
FlowRuleManager.loadRules(Collections.singletonList(rule));
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void cleanUp() {
|
||||||
|
FlowRuleManager.loadRules(null);
|
||||||
|
ClusterBuilderSlot.resetClusterNodes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2018 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.webmvc;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcConfig;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Eric Zhao
|
||||||
|
*/
|
||||||
|
public class SentinelWebInterceptorTest {
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testPassIllegalConfig() {
|
||||||
|
SentinelWebMvcConfig config = new SentinelWebMvcConfig();
|
||||||
|
config.setRequestAttributeName(null);
|
||||||
|
SentinelWebInterceptor interceptor = new SentinelWebInterceptor(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 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
|
||||||
|
*
|
||||||
|
* https://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.webmvc;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author kaizi2009
|
||||||
|
*/
|
||||||
|
@SpringBootApplication
|
||||||
|
public class TestApplication {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(TestApplication.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 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
|
||||||
|
*
|
||||||
|
* https://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.webmvc.config;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelWebInterceptor;
|
||||||
|
import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelWebTotalInterceptor;
|
||||||
|
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
|
||||||
|
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config sentinel interceptor
|
||||||
|
*
|
||||||
|
* @author kaizi2009
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class InterceptorConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
//Add sentinel interceptor
|
||||||
|
addSpringMvcInterceptor(registry);
|
||||||
|
|
||||||
|
//If you want to sentinel the total flow, you can add total interceptor
|
||||||
|
addSpringMvcTotalInterceptor(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addSpringMvcInterceptor(InterceptorRegistry registry) {
|
||||||
|
//Config
|
||||||
|
SentinelWebMvcConfig config = new SentinelWebMvcConfig();
|
||||||
|
|
||||||
|
config.setBlockExceptionHandler(new BlockExceptionHandler() {
|
||||||
|
@Override
|
||||||
|
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
|
||||||
|
String resourceName = e.getRule().getResource();
|
||||||
|
//Depending on your situation, you can choose to process or throw
|
||||||
|
if ("/hello".equals(resourceName)) {
|
||||||
|
//Do something ......
|
||||||
|
//Write string or json string;
|
||||||
|
response.getWriter().write("/Blocked by sentinel");
|
||||||
|
} else {
|
||||||
|
//Handle in global exception handling
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//Custom configuration if necessary
|
||||||
|
config.setHttpMethodSpecify(false);
|
||||||
|
config.setWebContextUnify(true);
|
||||||
|
config.setOriginParser(new RequestOriginParser() {
|
||||||
|
@Override
|
||||||
|
public String parseOrigin(HttpServletRequest request) {
|
||||||
|
return request.getHeader("S-user");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//Add sentinel interceptor
|
||||||
|
registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addSpringMvcTotalInterceptor(InterceptorRegistry registry) {
|
||||||
|
//Configure
|
||||||
|
SentinelWebMvcTotalConfig config = new SentinelWebMvcTotalConfig();
|
||||||
|
|
||||||
|
//Custom configuration if necessary
|
||||||
|
config.setRequestAttributeName("my_sentinel_spring_mvc_total_entity_container");
|
||||||
|
config.setTotalResourceName("my_spring_mvc_total_url_request");
|
||||||
|
|
||||||
|
//Add sentinel interceptor
|
||||||
|
registry.addInterceptor(new SentinelWebTotalInterceptor(config)).addPathPatterns("/**");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 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
|
||||||
|
*
|
||||||
|
* https://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.webmvc.config;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.adapter.spring.webmvc.ResultWrapper;
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.AbstractRule;
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config 'BlockException' handler, handler it in spring veb 'ExceptionHandler'
|
||||||
|
*
|
||||||
|
* @author kaizi2009
|
||||||
|
*/
|
||||||
|
@ControllerAdvice
|
||||||
|
@Order(0)
|
||||||
|
public class SentinelSpringMvcBlockHandlerConfig {
|
||||||
|
private Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
|
||||||
|
@ExceptionHandler(BlockException.class)
|
||||||
|
@ResponseBody
|
||||||
|
public ResultWrapper sentinelBlockHandler(BlockException e) {
|
||||||
|
AbstractRule rule = e.getRule();
|
||||||
|
//Log
|
||||||
|
logger.info("Blocked by sentinel, {}", rule.toString());
|
||||||
|
//Return object
|
||||||
|
return ResultWrapper.blocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
@ResponseBody
|
||||||
|
public ResultWrapper exceptionHandler(Exception e) {
|
||||||
|
logger.error("System error", e.getMessage());
|
||||||
|
return new ResultWrapper(-1, "System error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 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
|
||||||
|
*
|
||||||
|
* https://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.webmvc.controller;
|
||||||
|
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author kaizi2009
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
public class TestController {
|
||||||
|
|
||||||
|
@GetMapping("/hello")
|
||||||
|
public String apiHello() {
|
||||||
|
return "Hello!";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/err")
|
||||||
|
public String apiError() {
|
||||||
|
return "Oops...";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/foo/{id}")
|
||||||
|
public String apiFoo(@PathVariable("id") Long id) {
|
||||||
|
return "foo " + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/runtimeException")
|
||||||
|
public String runtimeException() {
|
||||||
|
int i = 1 / 0;
|
||||||
|
return "runtimeException";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/exclude/{id}")
|
||||||
|
public String apiExclude(@PathVariable("id") Long id) {
|
||||||
|
return "Exclude " + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue