diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/README.md b/sentinel-adapter/sentinel-spring-webmvc-adapter/README.md index 10c49503c2..f311f05004 100755 --- a/sentinel-adapter/sentinel-spring-webmvc-adapter/README.md +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/README.md @@ -28,6 +28,12 @@ public class InterceptorConfig implements WebMvcConfigurer { // Add to the interceptor list. registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**"); } + + @Bean + public SentinelExceptionAware sentinelExceptionAware(){ + //Make exception visible to Sentinel if you have configured ExceptionHandler + return new SentinelExceptionAware(); + } } ``` diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java index e793aac73c..f26e07947f 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java @@ -15,9 +15,6 @@ */ 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.ResourceTypeConstants; @@ -30,13 +27,20 @@ import com.alibaba.csp.sentinel.util.AssertUtil; import com.alibaba.csp.sentinel.util.StringUtil; -import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.AsyncHandlerInterceptor; import org.springframework.web.servlet.ModelAndView; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Objects; + /** * Since request may be reprocessed in flow if any forwarding or including or other action - * happened (see {@link javax.servlet.ServletRequest#getDispatcherType()}) we will only - * deal with the initial request. So we use reference count to track in + * happened (see {@link javax.servlet.ServletRequest#getDispatcherType()}) we will only + * deal with the initial request. So we use reference count 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. *
@@ -48,11 +52,11 @@
* return mav;
* }
*
- *
+ *
* @author kaizi2009
* @since 1.7.1
*/
-public abstract class AbstractSentinelInterceptor implements HandlerInterceptor {
+public abstract class AbstractSentinelInterceptor implements AsyncHandlerInterceptor {
public static final String SENTINEL_SPRING_WEB_CONTEXT_NAME = "sentinel_spring_web_context";
private static final String EMPTY_ORIGIN = "";
@@ -64,40 +68,40 @@ public AbstractSentinelInterceptor(BaseWebMvcConfig config) {
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)
+ * @return reference count after increasing (initial value as zero to be increased)
*/
- private Integer increaseReferece(HttpServletRequest request, String rcKey, int step) {
+ private Integer increaseReference(HttpServletRequest request, String rcKey, int step) {
Object obj = request.getAttribute(rcKey);
-
+
if (obj == null) {
// initial
- obj = Integer.valueOf(0);
+ obj = 0;
}
-
- Integer newRc = (Integer)obj + step;
+
+ Integer newRc = (Integer) obj + step;
request.setAttribute(rcKey, newRc);
return newRc;
}
-
+
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
- throws Exception {
+ throws Exception {
try {
String resourceName = getResourceName(request);
if (StringUtil.isEmpty(resourceName)) {
return true;
}
-
- if (increaseReferece(request, this.baseWebMvcConfig.getRequestRefName(), 1) != 1) {
+
+ if (increaseReference(request, this.baseWebMvcConfig.getRequestRefName(), 1) != 1) {
return true;
}
-
+
// Parse the request origin using registered origin parser.
String origin = parseOrigin(request);
String contextName = getContextName(request);
@@ -133,13 +137,37 @@ protected String getContextName(HttpServletRequest request) {
return SENTINEL_SPRING_WEB_CONTEXT_NAME;
}
+
+ /**
+ * When a handler starts an asynchronous request, the DispatcherServlet exits without invoking postHandle and afterCompletion
+ * Called instead of postHandle and afterCompletion to exit the context and clean thread-local variables when the handler is being executed concurrently.
+ *
+ * @param request the current request
+ * @param response the current response
+ * @param handler the handler (or {@link HandlerMethod}) that started async
+ * execution, for type and/or instance examination
+ */
+ @Override
+ public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response,
+ Object handler) throws Exception {
+ exit(request);
+ }
+
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
- if (increaseReferece(request, this.baseWebMvcConfig.getRequestRefName(), -1) != 0) {
+ exit(request, ex);
+ }
+
+ private void exit(HttpServletRequest request) {
+ exit(request, null);
+ }
+
+ private void exit(HttpServletRequest request, Exception ex) {
+ if (increaseReference(request, this.baseWebMvcConfig.getRequestRefName(), -1) != 0) {
return;
}
-
+
Entry entry = getEntryInRequest(request, baseWebMvcConfig.getRequestAttributeName());
if (entry == null) {
// should not happen
@@ -147,7 +175,7 @@ public void afterCompletion(HttpServletRequest request, HttpServletResponse resp
getClass().getSimpleName(), baseWebMvcConfig.getRequestAttributeName());
return;
}
-
+
traceExceptionAndExit(entry, ex);
removeEntryInRequest(request);
ContextUtil.exit();
@@ -160,7 +188,7 @@ public void postHandle(HttpServletRequest request, HttpServletResponse response,
protected Entry getEntryInRequest(HttpServletRequest request, String attrKey) {
Object entryObject = request.getAttribute(attrKey);
- return entryObject == null ? null : (Entry)entryObject;
+ return entryObject == null ? null : (Entry) entryObject;
}
protected void removeEntryInRequest(HttpServletRequest request) {
@@ -168,16 +196,25 @@ protected void removeEntryInRequest(HttpServletRequest request) {
}
protected void traceExceptionAndExit(Entry entry, Exception ex) {
- if (entry != null) {
- if (ex != null) {
- Tracer.traceEntry(ex, entry);
- }
- entry.exit();
+ if (entry == null) {
+ return;
+ }
+ HttpServletRequest request = getHttpServletRequest();
+ if (request != null
+ && ex == null
+ && increaseReference(request, this.baseWebMvcConfig.getRequestRefName() + ":" + BaseWebMvcConfig.REQUEST_REF_EXCEPTION_NAME, 1) == 1) {
+ //Each interceptor can only catch exception once
+ ex = (Exception) request.getAttribute(BaseWebMvcConfig.REQUEST_REF_EXCEPTION_NAME);
}
+
+ if (ex != null) {
+ Tracer.traceEntry(ex, entry);
+ }
+ entry.exit();
}
protected void handleBlockException(HttpServletRequest request, HttpServletResponse response, BlockException e)
- throws Exception {
+ throws Exception {
if (baseWebMvcConfig.getBlockExceptionHandler() != null) {
baseWebMvcConfig.getBlockExceptionHandler().handle(request, response, e);
} else {
@@ -197,4 +234,9 @@ protected String parseOrigin(HttpServletRequest request) {
return origin;
}
+ private HttpServletRequest getHttpServletRequest() {
+ ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+
+ return Objects.isNull(servletRequestAttributes) ? null : servletRequestAttributes.getRequest();
+ }
}
diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelExceptionAware.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelExceptionAware.java
new file mode 100644
index 0000000000..c6c7e2675c
--- /dev/null
+++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelExceptionAware.java
@@ -0,0 +1,47 @@
+/*
+ * 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.BaseWebMvcConfig;
+import com.alibaba.csp.sentinel.slots.block.BlockException;
+import org.springframework.core.annotation.Order;
+import org.springframework.web.servlet.HandlerExceptionResolver;
+import org.springframework.web.servlet.ModelAndView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Make exception visible to Sentinel.SentinelExceptionAware should be front of ExceptionHandlerExceptionResolver
+ * whose order is 0 {@link org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#handlerExceptionResolver}
+ *
+ * @author lemonJ
+ */
+@Order(-1)
+public class SentinelExceptionAware implements HandlerExceptionResolver {
+ @Override
+ public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
+ addExceptionToRequest(request, ex);
+ return null;
+ }
+
+ private void addExceptionToRequest(HttpServletRequest httpServletRequest, Exception exception) {
+ if(BlockException.isBlockException(exception)){
+ return;
+ }
+ httpServletRequest.setAttribute(BaseWebMvcConfig.REQUEST_REF_EXCEPTION_NAME, exception);
+ }
+}
diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/BaseWebMvcConfig.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/BaseWebMvcConfig.java
index e1bd1542fd..ad3f7997ae 100644
--- a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/BaseWebMvcConfig.java
+++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/BaseWebMvcConfig.java
@@ -26,6 +26,8 @@
*/
public abstract class BaseWebMvcConfig {
+ public final static String REQUEST_REF_EXCEPTION_NAME = "$$sentinel_spring_web_entry_attr-exception";
+
protected String requestAttributeName;
protected String requestRefName;
protected BlockExceptionHandler blockExceptionHandler;
diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java
index f1ddb937ce..7f8bbc051e 100644
--- a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java
+++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java
@@ -17,12 +17,16 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
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.context.ContextUtil;
import com.alibaba.csp.sentinel.node.ClusterNode;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
+import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
+import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
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;
@@ -64,6 +68,18 @@ public void testBase() throws Exception {
assertEquals(1, cn.passQps(), 0.01);
}
+ @Test
+ public void testAsync() throws Exception {
+ String url = "/async";
+ this.mvc.perform(get(url))
+ .andExpect(status().isOk());
+
+ ClusterNode cn = ClusterBuilderSlot.getClusterNode(url);
+ assertNotNull(cn);
+ assertEquals(1, cn.passQps(), 0.01);
+ assertNull(ContextUtil.getContext());
+ }
+
@Test
public void testOriginParser() throws Exception {
String springMvcPathVariableUrl = "/foo/{id}";
@@ -128,6 +144,32 @@ public void testRuntimeException() throws Exception {
assertEquals(1, cn.blockRequest(), 1);
}
+
+ @Test
+ public void testExceptionPerception() throws Exception {
+ String url = "/bizException";
+ configureExceptionDegradeRulesFor(url, 2.6, null);
+ int repeat = 3;
+ for (int i = 0; i < repeat; i++) {
+ this.mvc.perform(get(url))
+ .andExpect(status().isOk())
+ .andExpect(content().string(new ResultWrapper(-1, "Biz 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)
@@ -150,6 +192,20 @@ private void configureExceptionRulesFor(String resource, int count, String limit
FlowRuleManager.loadRules(Collections.singletonList(rule));
}
+ private void configureExceptionDegradeRulesFor(String resource, double count, String limitApp) {
+ DegradeRule rule = new DegradeRule()
+ .setCount(count)
+ .setStatIntervalMs(1000)
+ .setMinRequestAmount(1)
+ .setTimeWindow(5)
+ .setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
+ rule.setResource(resource);
+ if (StringUtil.isNotBlank(limitApp)) {
+ rule.setLimitApp(limitApp);
+ }
+ DegradeRuleManager.loadRules(Collections.singletonList(rule));
+ }
+
@After
public void cleanUp() {
FlowRuleManager.loadRules(null);
diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/InterceptorConfig.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/InterceptorConfig.java
index 5b167a0c1d..52f1b0cb67 100644
--- a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/InterceptorConfig.java
+++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/InterceptorConfig.java
@@ -15,17 +15,16 @@
*/
package com.alibaba.csp.sentinel.adapter.spring.webmvc.config;
+import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelExceptionAware;
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.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
/**
* Config sentinel interceptor
@@ -35,6 +34,11 @@
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
+ @Bean
+ public SentinelExceptionAware sentinelExceptionAware() {
+ return new SentinelExceptionAware();
+ }
+
@Override
public void addInterceptors(InterceptorRegistry registry) {
//Add sentinel interceptor
@@ -48,19 +52,16 @@ 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;
- }
+ 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 json string;
+ response.getWriter().write("/Blocked by sentinel");
+ } else {
+ //Handle in global exception handling
+ throw e;
}
});
diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelSpringMvcBlockHandlerConfig.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelSpringMvcBlockHandlerConfig.java
index b8e4660377..50c47bb313 100644
--- a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelSpringMvcBlockHandlerConfig.java
+++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelSpringMvcBlockHandlerConfig.java
@@ -16,6 +16,7 @@
package com.alibaba.csp.sentinel.adapter.spring.webmvc.config;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.ResultWrapper;
+import com.alibaba.csp.sentinel.adapter.spring.webmvc.exception.BizException;
import com.alibaba.csp.sentinel.slots.block.AbstractRule;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.slf4j.Logger;
@@ -51,4 +52,11 @@ public ResultWrapper exceptionHandler(Exception e) {
logger.error("System error", e.getMessage());
return new ResultWrapper(-1, "System error");
}
+
+ @ExceptionHandler(BizException.class)
+ @ResponseBody
+ public ResultWrapper bizExceptionHandler(BizException e) {
+ logger.error("Biz error", e.getMessage());
+ return new ResultWrapper(-1, "Biz error");
+ }
}
diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/controller/TestController.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/controller/TestController.java
index d8a42ab8fb..2a52d576d5 100644
--- a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/controller/TestController.java
+++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/controller/TestController.java
@@ -16,9 +16,12 @@
package com.alibaba.csp.sentinel.adapter.spring.webmvc.controller;
+import com.alibaba.csp.sentinel.adapter.spring.webmvc.exception.BizException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.DeferredResult;
/**
* @author kaizi2009
@@ -47,9 +50,26 @@ public String runtimeException() {
return "runtimeException";
}
+ @GetMapping("/bizException")
+ public String bizException() {
+ throw new BizException();
+ }
+
@GetMapping("/exclude/{id}")
public String apiExclude(@PathVariable("id") Long id) {
return "Exclude " + id;
}
+ @GetMapping("/async")
+ @ResponseBody
+ public DeferredResult