forked from alibaba/Sentinel
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Sentinel Spring Web MVC adapter module (alibaba#1104)
- Add sentinel-spring-webmvc-adapter module and demo
- Loading branch information
Showing
27 changed files
with
1,508 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
115 changes: 115 additions & 0 deletions
115
sentinel-adapter/sentinel-spring-webmvc-adapter/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
# Sentinel Spring MVC Interceptor | ||
|
||
Sentinel provides Spring MVC Interceptor integration to enable flow control for web requests, And support url like '/foo/{id}' | ||
|
||
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> | ||
``` | ||
|
||
Configure interceptor | ||
|
||
```java | ||
@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) { | ||
//Configure | ||
SentinelWebMvcConfig config = new SentinelWebMvcConfig(); | ||
//Custom configuration if necessary | ||
config.setHttpMethodSpecify(true); | ||
config.setOriginParser(request -> request.getHeader("S-user")); | ||
//Add sentinel interceptor | ||
registry.addInterceptor(new SentinelInterceptor(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 SentinelTotalInterceptor(config)).addPathPatterns("/**"); | ||
} | ||
} | ||
``` | ||
|
||
Configure 'BlockException' handler, there are three options: | ||
1. Global exception handling in spring MVC. <Recommend> | ||
```java | ||
@ControllerAdvice | ||
@Order(0) | ||
public class SentinelSpringMvcBlockHandlerConfig { | ||
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"; | ||
} | ||
} | ||
|
||
``` | ||
2. Use `DefaultBlockExceptionHandler` | ||
```java | ||
//SentinelWebMvcTotalConfig config = new SentinelWebMvcTotalConfig(); | ||
SentinelWebMvcConfig config = new SentinelWebMvcConfig(); | ||
config.setBlockExceptionHandler(new DefaultBlockExceptionHandler()); | ||
``` | ||
3. `implements BlockExceptionHandler` | ||
```java | ||
//SentinelWebMvcTotalConfig config = new SentinelWebMvcTotalConfig(); | ||
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 ...... | ||
//Write string or error page; | ||
response.getWriter().write("Blocked by sentinel"); | ||
} else { | ||
//Handle it in global exception handling | ||
throw e; | ||
} | ||
}); | ||
``` | ||
|
||
Configuration | ||
- Common configuration in `SentinelWebMvcConfig` and `SentinelWebMvcTotalConfig` | ||
|
||
| name | description | type | default value | | ||
|------|------------|------|-------| | ||
| blockExceptionHandler| The handler when blocked by sentinel, there are three options:<br/>1. The default value is null, you can hanlde `BlockException` in spring MVC;<br/>2.Use `DefaultBlockExceptionHandler`;<br/>3. `implements BlockExceptionHandler` | `BlockExceptionHandler` | `null` | | ||
| originParser | `RequestOriginParser` interface is useful for extracting request origin (e.g. IP or appName from HTTP Header) from HTTP request | `RequestOriginParser` | `null` | | ||
|
||
- `SentinelWebMvcConfig` configuration | ||
|
||
| name | description | type | default value | | ||
|------|------------|------|-------| | ||
| urlCleaner | The `UrlCleaner` interface is designed for clean and unify the URL resource. For REST APIs, you can to clean the URL resource (e.g. `/api/user/getById` and `/api/user/getByName` -> `/api/user/getBy*`), avoid the amount of context and will exceed the threshold | `UrlCleaner` | `null` | | ||
| requestAttributeName | Attribute name in request used by sentinel, please check record log, if it is already used, please set | `String` | sentinel_spring_mvc_entry_container | | ||
| httpMethodSpecify | Specify http method, for example: GET:/hello | `boolean` | `false` | | ||
|
||
|
||
`SentinelWebMvcTotalConfig` configuration | ||
|
||
| name | description | type | default value | | ||
|------|------------|------|-------| | ||
| totalResourceName | The resource name in `SentinelTotalInterceptor` | `String` | spring-mvc-total-url-request | | ||
| requestAttributeName | Attribute name in request used by sentinel, please check record log, if it is already used, please set | `String` | sentinel_spring_mvc_total_entry_container | | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<?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> | ||
<artifactId>sentinel-adapter</artifactId> | ||
<groupId>com.alibaba.csp</groupId> | ||
<version>1.7.0-SNAPSHOT</version> | ||
</parent> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<artifactId>sentinel-spring-webmvc-adapter</artifactId> | ||
|
||
<properties> | ||
<spring.version>5.1.8.RELEASE</spring.version> | ||
<spring.boot.version>2.1.3.RELEASE</spring.boot.version> | ||
<servlet.api.version>3.1.0</servlet.api.version> | ||
</properties> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>com.alibaba.csp</groupId> | ||
<artifactId>sentinel-core</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>javax.servlet</groupId> | ||
<artifactId>javax.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> | ||
</dependencies> | ||
</project> |
138 changes: 138 additions & 0 deletions
138
...main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
/* | ||
* 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 javax.servlet.http.HttpServletRequest; | ||
import javax.servlet.http.HttpServletResponse; | ||
|
||
import com.alibaba.csp.sentinel.Entry; | ||
import com.alibaba.csp.sentinel.EntryType; | ||
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.StringUtil; | ||
import org.springframework.web.servlet.HandlerInterceptor; | ||
import org.springframework.web.servlet.ModelAndView; | ||
|
||
/** | ||
* @author kaizi2009 | ||
*/ | ||
public abstract class AbstractSentinelInterceptor implements HandlerInterceptor { | ||
|
||
public static final String SPRING_MVC_CONTEXT_NAME = "spring_mvc_context"; | ||
private static final String EMPTY_ORIGIN = ""; | ||
protected static final String COLON = ":"; | ||
private BaseWebMvcConfig baseWebMvcConfig; | ||
|
||
@Override | ||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) | ||
throws Exception { | ||
|
||
try { | ||
String resourceName = getResourceName(request); | ||
|
||
if (StringUtil.isNotEmpty(resourceName)) { | ||
// Parse the request origin using registered origin parser. | ||
String origin = parseOrigin(request); | ||
ContextUtil.enter(SPRING_MVC_CONTEXT_NAME, origin); | ||
Entry entry = SphU.entry(resourceName, EntryType.IN); | ||
|
||
setEntryInRequest(request, baseWebMvcConfig.getRequestAttributeName(), entry); | ||
} | ||
return true; | ||
} catch (BlockException e) { | ||
handleBlockException(request, response, e); | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Get sentinel resource name. | ||
* @param request | ||
* @return | ||
*/ | ||
protected abstract String getResourceName(HttpServletRequest request); | ||
|
||
@Override | ||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, | ||
Object handler, Exception ex) throws Exception { | ||
Entry entry = getEntryInRequest(request, baseWebMvcConfig.getRequestAttributeName()); | ||
if (entry != null) { | ||
traceExceptionAndExit(entry, ex); | ||
removeEntryInRequest(request); | ||
} | ||
ContextUtil.exit(); | ||
} | ||
|
||
@Override | ||
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, | ||
ModelAndView modelAndView) throws Exception { | ||
} | ||
|
||
protected void setEntryInRequest(HttpServletRequest request, String name, Entry entry) { | ||
Object attrVal = request.getAttribute(name); | ||
if (attrVal != null) { | ||
RecordLog.warn(String.format("Already exist attribute name '%s' in request, please set `requestAttributeName`", name)); | ||
} else { | ||
request.setAttribute(name, entry); | ||
} | ||
} | ||
|
||
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, handle it in spring mvc | ||
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; | ||
} | ||
|
||
protected void setBaseWebMvcConfig(BaseWebMvcConfig config) { | ||
this.baseWebMvcConfig = config; | ||
} | ||
} |
Oops, something went wrong.