Skip to content

Commit b78b09d

Browse files
authored
feat: sentinel can feel exception though application has configured ExceptionHandler (#3409)
1 parent 02c97fe commit b78b09d

File tree

10 files changed

+178
-36
lines changed

10 files changed

+178
-36
lines changed

sentinel-adapter/sentinel-spring-webmvc-adapter/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ public class InterceptorConfig implements WebMvcConfigurer {
2828
// Add to the interceptor list.
2929
registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**");
3030
}
31+
32+
@Bean
33+
public SentinelExceptionAware sentinelExceptionAware(){
34+
//Make exception visible to Sentinel if you have configured ExceptionHandler
35+
return new SentinelExceptionAware();
36+
}
3137
}
3238
```
3339

sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@
1515
*/
1616
package com.alibaba.csp.sentinel.adapter.spring.webmvc;
1717

18-
import javax.servlet.http.HttpServletRequest;
19-
import javax.servlet.http.HttpServletResponse;
20-
2118
import com.alibaba.csp.sentinel.Entry;
2219
import com.alibaba.csp.sentinel.EntryType;
2320
import com.alibaba.csp.sentinel.ResourceTypeConstants;
@@ -29,14 +26,19 @@
2926
import com.alibaba.csp.sentinel.slots.block.BlockException;
3027
import com.alibaba.csp.sentinel.util.AssertUtil;
3128
import com.alibaba.csp.sentinel.util.StringUtil;
32-
29+
import org.springframework.web.context.request.RequestContextHolder;
30+
import org.springframework.web.context.request.ServletRequestAttributes;
3331
import org.springframework.web.servlet.HandlerInterceptor;
3432
import org.springframework.web.servlet.ModelAndView;
3533

34+
import javax.servlet.http.HttpServletRequest;
35+
import javax.servlet.http.HttpServletResponse;
36+
import java.util.Objects;
37+
3638
/**
3739
* Since request may be reprocessed in flow if any forwarding or including or other action
38-
* happened (see {@link javax.servlet.ServletRequest#getDispatcherType()}) we will only
39-
* deal with the initial request. So we use <b>reference count</b> to track in
40+
* happened (see {@link javax.servlet.ServletRequest#getDispatcherType()}) we will only
41+
* deal with the initial request. So we use <b>reference count</b> to track in
4042
* dispathing "onion" though which we could figure out whether we are in initial type "REQUEST".
4143
* That means the sub-requests which we rarely meet in practice will NOT be recorded in Sentinel.
4244
* <p>
@@ -71,15 +73,15 @@ public AbstractSentinelInterceptor(BaseWebMvcConfig config) {
7173
* @param step
7274
* @return reference count after increasing (initial value as zero to be increased)
7375
*/
74-
private Integer increaseReferece(HttpServletRequest request, String rcKey, int step) {
76+
private Integer increaseReference(HttpServletRequest request, String rcKey, int step) {
7577
Object obj = request.getAttribute(rcKey);
76-
78+
7779
if (obj == null) {
7880
// initial
79-
obj = Integer.valueOf(0);
81+
obj = 0;
8082
}
81-
82-
Integer newRc = (Integer)obj + step;
83+
84+
Integer newRc = (Integer) obj + step;
8385
request.setAttribute(rcKey, newRc);
8486
return newRc;
8587
}
@@ -93,8 +95,8 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons
9395
if (StringUtil.isEmpty(resourceName)) {
9496
return true;
9597
}
96-
97-
if (increaseReferece(request, this.baseWebMvcConfig.getRequestRefName(), 1) != 1) {
98+
99+
if (increaseReference(request, this.baseWebMvcConfig.getRequestRefName(), 1) != 1) {
98100
return true;
99101
}
100102

@@ -136,7 +138,7 @@ protected String getContextName(HttpServletRequest request) {
136138
@Override
137139
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
138140
Object handler, Exception ex) throws Exception {
139-
if (increaseReferece(request, this.baseWebMvcConfig.getRequestRefName(), -1) != 0) {
141+
if (increaseReference(request, this.baseWebMvcConfig.getRequestRefName(), -1) != 0) {
140142
return;
141143
}
142144

@@ -168,12 +170,21 @@ protected void removeEntryInRequest(HttpServletRequest request) {
168170
}
169171

170172
protected void traceExceptionAndExit(Entry entry, Exception ex) {
171-
if (entry != null) {
172-
if (ex != null) {
173-
Tracer.traceEntry(ex, entry);
174-
}
175-
entry.exit();
173+
if (entry == null) {
174+
return;
176175
}
176+
HttpServletRequest request = getHttpServletRequest();
177+
if (request != null
178+
&& ex == null
179+
&& increaseReference(request, this.baseWebMvcConfig.getRequestRefName() + ":" + BaseWebMvcConfig.REQUEST_REF_EXCEPTION_NAME, 1) == 1) {
180+
//Each interceptor can only catch exception once
181+
ex = (Exception) request.getAttribute(BaseWebMvcConfig.REQUEST_REF_EXCEPTION_NAME);
182+
}
183+
184+
if (ex != null) {
185+
Tracer.traceEntry(ex, entry);
186+
}
187+
entry.exit();
177188
}
178189

179190
protected void handleBlockException(HttpServletRequest request, HttpServletResponse response, BlockException e)
@@ -197,4 +208,9 @@ protected String parseOrigin(HttpServletRequest request) {
197208
return origin;
198209
}
199210

211+
private HttpServletRequest getHttpServletRequest() {
212+
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
213+
214+
return Objects.isNull(servletRequestAttributes) ? null : servletRequestAttributes.getRequest();
215+
}
200216
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 1999-2019 Alibaba Group Holding Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.alibaba.csp.sentinel.adapter.spring.webmvc;
17+
18+
import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.BaseWebMvcConfig;
19+
import com.alibaba.csp.sentinel.slots.block.BlockException;
20+
import org.springframework.core.annotation.Order;
21+
import org.springframework.web.servlet.HandlerExceptionResolver;
22+
import org.springframework.web.servlet.ModelAndView;
23+
24+
import javax.servlet.http.HttpServletRequest;
25+
import javax.servlet.http.HttpServletResponse;
26+
27+
/**
28+
* Make exception visible to Sentinel.SentinelExceptionAware should be front of ExceptionHandlerExceptionResolver
29+
* whose order is 0 {@link org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#handlerExceptionResolver}
30+
*
31+
* @author lemonJ
32+
*/
33+
@Order(-1)
34+
public class SentinelExceptionAware implements HandlerExceptionResolver {
35+
@Override
36+
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
37+
addExceptionToRequest(request, ex);
38+
return null;
39+
}
40+
41+
private void addExceptionToRequest(HttpServletRequest httpServletRequest, Exception exception) {
42+
if(BlockException.isBlockException(exception)){
43+
return;
44+
}
45+
httpServletRequest.setAttribute(BaseWebMvcConfig.REQUEST_REF_EXCEPTION_NAME, exception);
46+
}
47+
}

sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/BaseWebMvcConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
*/
2727
public abstract class BaseWebMvcConfig {
2828

29+
public final static String REQUEST_REF_EXCEPTION_NAME = "$$sentinel_spring_web_entry_attr-exception";
30+
2931
protected String requestAttributeName;
3032
protected String requestRefName;
3133
protected BlockExceptionHandler blockExceptionHandler;

sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
import com.alibaba.csp.sentinel.node.ClusterNode;
2525
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
26+
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
27+
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
2628
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
2729
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
2830
import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot;
@@ -128,6 +130,32 @@ public void testRuntimeException() throws Exception {
128130
assertEquals(1, cn.blockRequest(), 1);
129131
}
130132

133+
134+
@Test
135+
public void testExceptionPerception() throws Exception {
136+
String url = "/bizException";
137+
configureExceptionDegradeRulesFor(url, 2.6, null);
138+
int repeat = 3;
139+
for (int i = 0; i < repeat; i++) {
140+
this.mvc.perform(get(url))
141+
.andExpect(status().isOk())
142+
.andExpect(content().string(new ResultWrapper(-1, "Biz error").toJsonString()));
143+
144+
ClusterNode cn = ClusterBuilderSlot.getClusterNode(url);
145+
assertNotNull(cn);
146+
assertEquals(i + 1, cn.passQps(), 0.01);
147+
}
148+
149+
// This will be blocked and response json.
150+
this.mvc.perform(get(url))
151+
.andExpect(status().isOk())
152+
.andExpect(content().string(ResultWrapper.blocked().toJsonString()));
153+
ClusterNode cn = ClusterBuilderSlot.getClusterNode(url);
154+
assertNotNull(cn);
155+
assertEquals(repeat, cn.passQps(), 0.01);
156+
assertEquals(1, cn.blockRequest(), 1);
157+
}
158+
131159
private void configureRulesFor(String resource, int count, String limitApp) {
132160
FlowRule rule = new FlowRule()
133161
.setCount(count)
@@ -150,6 +178,20 @@ private void configureExceptionRulesFor(String resource, int count, String limit
150178
FlowRuleManager.loadRules(Collections.singletonList(rule));
151179
}
152180

181+
private void configureExceptionDegradeRulesFor(String resource, double count, String limitApp) {
182+
DegradeRule rule = new DegradeRule()
183+
.setCount(count)
184+
.setStatIntervalMs(1000)
185+
.setMinRequestAmount(1)
186+
.setTimeWindow(5)
187+
.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
188+
rule.setResource(resource);
189+
if (StringUtil.isNotBlank(limitApp)) {
190+
rule.setLimitApp(limitApp);
191+
}
192+
DegradeRuleManager.loadRules(Collections.singletonList(rule));
193+
}
194+
153195
@After
154196
public void cleanUp() {
155197
FlowRuleManager.loadRules(null);

sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/InterceptorConfig.java

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,16 @@
1515
*/
1616
package com.alibaba.csp.sentinel.adapter.spring.webmvc.config;
1717

18+
import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelExceptionAware;
1819
import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelWebInterceptor;
1920
import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelWebTotalInterceptor;
20-
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
2121
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
22-
import com.alibaba.csp.sentinel.slots.block.BlockException;
22+
import org.springframework.context.annotation.Bean;
2323
import org.springframework.context.annotation.Configuration;
2424
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
2525
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
2626

2727
import javax.servlet.http.HttpServletRequest;
28-
import javax.servlet.http.HttpServletResponse;
2928

3029
/**
3130
* Config sentinel interceptor
@@ -35,6 +34,11 @@
3534
@Configuration
3635
public class InterceptorConfig implements WebMvcConfigurer {
3736

37+
@Bean
38+
public SentinelExceptionAware sentinelExceptionAware() {
39+
return new SentinelExceptionAware();
40+
}
41+
3842
@Override
3943
public void addInterceptors(InterceptorRegistry registry) {
4044
//Add sentinel interceptor
@@ -48,19 +52,16 @@ private void addSpringMvcInterceptor(InterceptorRegistry registry) {
4852
//Config
4953
SentinelWebMvcConfig config = new SentinelWebMvcConfig();
5054

51-
config.setBlockExceptionHandler(new BlockExceptionHandler() {
52-
@Override
53-
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
54-
String resourceName = e.getRule().getResource();
55-
//Depending on your situation, you can choose to process or throw
56-
if ("/hello".equals(resourceName)) {
57-
//Do something ......
58-
//Write string or json string;
59-
response.getWriter().write("/Blocked by sentinel");
60-
} else {
61-
//Handle in global exception handling
62-
throw e;
63-
}
55+
config.setBlockExceptionHandler((request, response, e) -> {
56+
String resourceName = e.getRule().getResource();
57+
//Depending on your situation, you can choose to process or throw
58+
if ("/hello".equals(resourceName)) {
59+
//Do something ......
60+
//Write string or json string;
61+
response.getWriter().write("/Blocked by sentinel");
62+
} else {
63+
//Handle in global exception handling
64+
throw e;
6465
}
6566
});
6667

sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelSpringMvcBlockHandlerConfig.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.alibaba.csp.sentinel.adapter.spring.webmvc.config;
1717

1818
import com.alibaba.csp.sentinel.adapter.spring.webmvc.ResultWrapper;
19+
import com.alibaba.csp.sentinel.adapter.spring.webmvc.exception.BizException;
1920
import com.alibaba.csp.sentinel.slots.block.AbstractRule;
2021
import com.alibaba.csp.sentinel.slots.block.BlockException;
2122
import org.slf4j.Logger;
@@ -51,4 +52,11 @@ public ResultWrapper exceptionHandler(Exception e) {
5152
logger.error("System error", e.getMessage());
5253
return new ResultWrapper(-1, "System error");
5354
}
55+
56+
@ExceptionHandler(BizException.class)
57+
@ResponseBody
58+
public ResultWrapper bizExceptionHandler(BizException e) {
59+
logger.error("Biz error", e.getMessage());
60+
return new ResultWrapper(-1, "Biz error");
61+
}
5462
}

sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/controller/TestController.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.alibaba.csp.sentinel.adapter.spring.webmvc.controller;
1717

1818

19+
import com.alibaba.csp.sentinel.adapter.spring.webmvc.exception.BizException;
1920
import org.springframework.web.bind.annotation.GetMapping;
2021
import org.springframework.web.bind.annotation.PathVariable;
2122
import org.springframework.web.bind.annotation.RestController;
@@ -47,6 +48,11 @@ public String runtimeException() {
4748
return "runtimeException";
4849
}
4950

51+
@GetMapping("/bizException")
52+
public String bizException() {
53+
throw new BizException();
54+
}
55+
5056
@GetMapping("/exclude/{id}")
5157
public String apiExclude(@PathVariable("id") Long id) {
5258
return "Exclude " + id;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.alibaba.csp.sentinel.adapter.spring.webmvc.exception;
2+
3+
/**
4+
* @author lemonj
5+
*/
6+
public class BizException extends RuntimeException{
7+
}

sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/InterceptorConfig.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
*/
1616
package com.alibaba.csp.sentinel.demo.spring.webmvc.config;
1717

18+
import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelExceptionAware;
1819
import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelWebInterceptor;
1920
import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelWebTotalInterceptor;
2021
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.DefaultBlockExceptionHandler;
2122
import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcConfig;
2223
import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcTotalConfig;
23-
24+
import org.springframework.context.annotation.Bean;
2425
import org.springframework.context.annotation.Configuration;
2526
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
2627
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -39,6 +40,12 @@ public void addInterceptors(InterceptorRegistry registry) {
3940
addSpringMvcInterceptor(registry);
4041
}
4142

43+
@Bean
44+
public SentinelExceptionAware sentinelExceptionAware() {
45+
//Make exception visible to Sentinel if you have configured ExceptionHandler
46+
return new SentinelExceptionAware();
47+
}
48+
4249
private void addSpringMvcInterceptor(InterceptorRegistry registry) {
4350
SentinelWebMvcConfig config = new SentinelWebMvcConfig();
4451

0 commit comments

Comments
 (0)