Skip to content

Commit

Permalink
Add Sentinel Spring Web MVC adapter module (alibaba#1104)
Browse files Browse the repository at this point in the history
- Add sentinel-spring-webmvc-adapter module and demo
  • Loading branch information
kaizi2009 authored and sczyh30 committed Nov 27, 2019
1 parent c705651 commit b14534f
Show file tree
Hide file tree
Showing 27 changed files with 1,508 additions and 0 deletions.
1 change: 1 addition & 0 deletions sentinel-adapter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<module>sentinel-spring-webflux-adapter</module>
<module>sentinel-api-gateway-adapter-common</module>
<module>sentinel-spring-cloud-gateway-adapter</module>
<module>sentinel-spring-webmvc-adapter</module>
</modules>

<dependencyManagement>
Expand Down
115 changes: 115 additions & 0 deletions sentinel-adapter/sentinel-spring-webmvc-adapter/README.md
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 |

55 changes: 55 additions & 0 deletions sentinel-adapter/sentinel-spring-webmvc-adapter/pom.xml
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>
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;
}
}
Loading

0 comments on commit b14534f

Please sign in to comment.