1. 序言

由Java的示例,其实我们在spring中也可以根据sentinel-core通过切面AOP来自定义实现一个拦截,这很容易,Sentinel自己也实现了Spring的使用

Sentinel自行实现了两套拦截的方案:

  • 一套是基于Filter的拦截,通过映射请求路径的方式,代码中在config中配置好路径对应的资源策略
  • 另一套是基于Interceptor方案,通过添加@SentinelResource,拦截对应的数据

2. 工程搭建

2.1. 基于Servlet的Filter

2.1.1. pom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-web-servlet</artifactId>
</dependency>
</dependencies>

2.1.2. 应用配置

启动器按照springBoot正常启动即可

配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

@Configuration
public class SentinelConfig implements InitializingBean {

/**
* 全局过滤器,用于开启Sentinel的监控
* @return
*/
@Bean
public FilterRegistrationBean sentinelFilterRegistration() {
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
CommonFilter filter = new CommonFilter();
registration.setFilter(filter);
registration.addUrlPatterns("/*");
registration.setName("sentinelFilter");
//registration.setOrder(1);
return registration;
}


/**
* 流控规则,主要是在FlowRuleManager中写入规则
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
FlowRule flowRule = new FlowRule();
// 资源路径,注意这里有”/“ 与请求路径保持一致
flowRule.setResource("/hello");
flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
flowRule.setCount(1);
flowRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);

//加载流控规则
FlowRuleManager.loadRules(Arrays.asList(flowRule));

/* 配置回调等相关的
WebCallbackManager.setUrlBlockHandler();
WebCallbackManager.setRequestOriginParser();
WebCallbackManager.setUrlCleaner();
*/
}
}

2.1.3. 应用中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

@RestController
public class HelloController {


@GetMapping("/hello")
public Object hello(){
System.out.println("线程:"+Thread.currentThread().getName()+ "执行");
doBusiness();
return "hello";
}

private void doBusiness() {
try {
TimeUnit.MILLISECONDS.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

当我们访问/hello接口时便会加载CommonFilter过滤器执行方法

2.1.4. 代码分析

上面我们配置了过滤器,现在主要看过滤器CommonFilter中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest sRequest = (HttpServletRequest) request;
Entry urlEntry = null;

try {
//获取请求,/hello接口得到的数据就是/hello
String target = FilterUtil.filterTarget(sRequest);

//这里如果在WebCallbackManager中配置了则会改变url的返回。对于pathVairiable非常有必要
// For REST APIs, you have to clean the URL (e.g. `/foo/1` and `/foo/2` -> `/foo/:id`), or
UrlCleaner urlCleaner = WebCallbackManager.getUrlCleaner();
if (urlCleaner != null) {
target = urlCleaner.clean(target);
}

if (!StringUtil.isEmpty(target)) {
String origin = parseOrigin(sRequest);
String contextName = webContextUnify ? WebServletConfig.WEB_SERVLET_CONTEXT_NAME : target;

//设置上下文
ContextUtil.enter(contextName, origin);

//根据是否区分get post等请求类型来区分,这里`SphU.entry`就是开启的逻辑了
if (httpMethodSpecify) {
String pathWithHttpMethod = sRequest.getMethod().toUpperCase() + COLON + target;
urlEntry = SphU.entry(pathWithHttpMethod, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
} else {
urlEntry = SphU.entry(target, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
}
}
chain.doFilter(request, response);
} catch (BlockException e) {
HttpServletResponse sResponse = (HttpServletResponse) response;
//这里走配置好的异常
WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse, e);
} catch (IOException | ServletException | RuntimeException e2) {
Tracer.traceEntry(e2, urlEntry);
throw e2;
} finally {
//退出流控
if (urlEntry != null) {
urlEntry.exit();
}
//退出上下文
ContextUtil.exit();
}
}

从这里代码可以看到这里也是统一封装了sentinel-core的通用入口。

2.2. 基于SpringMvc拦截器实现

2.2.1. pom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
</dependency>
<!--这里主要将sentinel-web-servlet替换成了webmvc-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-webmvc-adapter</artifactId>
</dependency>
</dependencies>

2.2.2. 配置

springboot 启动类不变,这里主要配置拦截器信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Component
public class SentinelConfig implements WebMvcConfigurer, InitializingBean {

@Override
public void addInterceptors(InterceptorRegistry registry) {
SentinelWebMvcConfig webMvcConfig = new SentinelWebMvcConfig();
/*
配置回调等相关的
webMvcConfig.setBlockExceptionHandler();
webMvcConfig.setOriginParser();
webMvcConfig.setHttpMethodSpecify();
webMvcConfig.setUrlCleaner();
webMvcConfig.setWebContextUnify();
封装了原来filter方式的以下方法,可以不用了
WebCallbackManager.setUrlBlockHandler();
WebCallbackManager.setRequestOriginParser();
WebCallbackManager.setUrlCleaner();
*/

registry.addInterceptor(new SentinelWebInterceptor(webMvcConfig)).addPathPatterns("/**");
}

/**
* 流控规则,主要是在FlowRuleManager中写入规则
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
FlowRule flowRule = new FlowRule();
// 资源路径,注意这里有”/“ 与请求路径保持一致
flowRule.setResource("/hello");
flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
flowRule.setCount(1);
flowRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
//加载流控规则
FlowRuleManager.loadRules(Arrays.asList(flowRule));
}
}

更多使用见 官方文档

2.2.3. 应用中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

@RestController
public class HelloController {


@GetMapping("/hello")
public Object hello(){
System.out.println("线程:"+Thread.currentThread().getName()+ "执行");
doBusiness();
return "hello";
}

private void doBusiness() {
try {
TimeUnit.MILLISECONDS.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

2.2.4. 代码分析

上面我们配置了过滤器,现在主要看拦截器SentinelWebInterceptor中的preHandle方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

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

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

2.3. spring切面拦截方式

2.3.1. pom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
</dependency>
<!--这里主要将sentinel-web-servlet替换成了webmvc-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
</dependency>
</dependencies>

2.3.2. 配置

springboot 普通启动,配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration
public class SentinelConfig implements InitializingBean {

/**
* 这里需要注入切面管理的类,好理解,我们的aop也会将切面代理类加入bean中
* @return
*/
@Bean
SentinelResourceAspect sentinelResourceAspect(){
return new SentinelResourceAspect();
}

/**
* 流控规则,主要是在FlowRuleManager中写入规则
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
FlowRule flowRule = new FlowRule();
// 资源路径,注意这里要和@SentinelResource里的值保持一致
flowRule.setResource("hello");
flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
flowRule.setCount(1);
flowRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
//加载流控规则
FlowRuleManager.loadRules(Arrays.asList(flowRule));
}
}

这里需要注入切面管理的类,

2.3.3. 应用中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
public class HelloController {


@GetMapping("/hello")
@SentinelResource("hello")
public Object hello(){
System.out.println("线程:"+Thread.currentThread().getName()+ "执行");
doBusiness();
return "hello";
}


private void doBusiness() {
try {
TimeUnit.MILLISECONDS.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

2.3.4. 代码分析

spring aop的方式我们主要看注入的切面类SentinelResourceAspect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {
//切入点,通过SentinelResource注解切入
@Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
public void sentinelResourceAnnotationPointcut() {
}

@Around("sentinelResourceAnnotationPointcut()")
public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
Method originMethod = resolveMethod(pjp);

//获取到注解
SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);
if (annotation == null) {
throw new IllegalStateException("Wrong state for SentinelResource annotation");
}
String resourceName = getResourceName(annotation.value(), originMethod);
EntryType entryType = annotation.entryType();
int resourceType = annotation.resourceType();
Entry entry = null;
try {
//启动流控
entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
Object result = pjp.proceed();
return result;
} catch (BlockException ex) {
//处理异常
return handleBlockException(pjp, annotation, ex);
} catch (Throwable ex) {
Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
throw ex;
}
if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
traceException(ex);
return handleFallback(pjp, annotation, ex);
}
throw ex;
} finally {
if (entry != null) {
//退出流控
entry.exit(1, pjp.getArgs());
}
}
}
}

这种切面的方式也好理解,使用了我们通用的注解方式匹配的。

3. 总结

以上三种方式其实原理是一样的,核心都是调用到sentinel-core中的SphU.entry(),entry.exit()方法开启关闭流控