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 distribute() throws Exception{ + DeferredResult result = new DeferredResult<>(); + + Thread thread = new Thread(() -> result.setResult("async result.")); + thread.start(); + + Thread.yield(); + return result; + } + } diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/exception/BizException.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/exception/BizException.java new file mode 100644 index 0000000000..9fe26e9a9a --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/exception/BizException.java @@ -0,0 +1,7 @@ +package com.alibaba.csp.sentinel.adapter.spring.webmvc.exception; + +/** + * @author lemonj + */ +public class BizException extends RuntimeException{ +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/AbstractSentinelInterceptor.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/AbstractSentinelInterceptor.java index 271d1442da..dc2e273add 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/AbstractSentinelInterceptor.java +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/AbstractSentinelInterceptor.java @@ -15,7 +15,11 @@ */ package com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x; -import com.alibaba.csp.sentinel.*; +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.config.BaseWebMvcConfig; import com.alibaba.csp.sentinel.context.ContextUtil; import com.alibaba.csp.sentinel.log.RecordLog; @@ -24,7 +28,8 @@ import com.alibaba.csp.sentinel.util.StringUtil; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.AsyncHandlerInterceptor; import org.springframework.web.servlet.ModelAndView; /** @@ -45,7 +50,7 @@ * * @since 1.8.8 */ -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 = ""; @@ -124,9 +129,33 @@ 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 { + 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; } diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java index 69d21a325a..f7e7ac7796 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/SentinelSpringMvcIntegrationTest.java @@ -17,10 +17,12 @@ 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.flow.FlowRule; @@ -64,6 +66,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}"; @@ -78,7 +92,7 @@ public void testOriginParser() throws Exception { // This will be blocked since the caller is same: userA this.mvc.perform( - get("/foo/2").accept(MediaType.APPLICATION_JSON).header(headerName, limitOrigin)) + get("/foo/2").accept(MediaType.APPLICATION_JSON).header(headerName, limitOrigin)) .andExpect(status().isOk()) .andExpect(content().json(ResultWrapper.blocked().toJsonString())); diff --git a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/controller/TestController.java b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/controller/TestController.java index 9b9ecfe25b..cf16bff4db 100644 --- a/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/controller/TestController.java +++ b/sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc_v6x/controller/TestController.java @@ -18,7 +18,9 @@ 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 @@ -52,4 +54,16 @@ public String apiExclude(@PathVariable("id") Long id) { return "Exclude " + id; } + @GetMapping("/async") + @ResponseBody + public DeferredResult distribute() throws Exception { + DeferredResult result = new DeferredResult<>(); + + Thread thread = new Thread(() -> result.setResult("async result.")); + thread.start(); + + Thread.yield(); + return result; + } + } diff --git a/sentinel-demo/pom.xml b/sentinel-demo/pom.xml index 6c5062294b..dcbb7b3a03 100755 --- a/sentinel-demo/pom.xml +++ b/sentinel-demo/pom.xml @@ -42,6 +42,7 @@ sentinel-demo-annotation-cdi-interceptor sentinel-demo-motan sentinel-demo-transport-spring-mvc + sentinel-demo-servlet diff --git a/sentinel-demo/sentinel-demo-servlet/pom.xml b/sentinel-demo/sentinel-demo-servlet/pom.xml new file mode 100644 index 0000000000..f75694e80f --- /dev/null +++ b/sentinel-demo/sentinel-demo-servlet/pom.xml @@ -0,0 +1,55 @@ + + + + sentinel-parent + com.alibaba.csp + 1.8.8 + ../../pom.xml + + 4.0.0 + + sentinel-demo-servlet + war + + + + javax.servlet + javax.servlet-api + 3.0.1 + provided + + + + com.alibaba.csp + sentinel-core + + + + com.alibaba.csp + sentinel-transport-simple-http + + + + com.alibaba.csp + sentinel-web-servlet + ${project.version} + + + + + + + + org.apache.maven.plugins + maven-war-plugin + 3.3.2 + + sentinel-demo-servlet + + + + + + \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-servlet/src/main/java/com/alibaba/csp/sentinel/demo/servlet/config/SentinelConfig.java b/sentinel-demo/sentinel-demo-servlet/src/main/java/com/alibaba/csp/sentinel/demo/servlet/config/SentinelConfig.java new file mode 100644 index 0000000000..2205774c9d --- /dev/null +++ b/sentinel-demo/sentinel-demo-servlet/src/main/java/com/alibaba/csp/sentinel/demo/servlet/config/SentinelConfig.java @@ -0,0 +1,45 @@ +package com.alibaba.csp.sentinel.demo.servlet.config; + +import com.alibaba.csp.sentinel.adapter.servlet.callback.DefaultUrlBlockHandler; +import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlCleaner; +import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +/** + * class description + * + * @author zhangxunwei + * @date 2024/6/24 + */ +public class SentinelConfig implements ServletContextListener { + + @Override + public void contextInitialized(ServletContextEvent servletContextEvent) { + initConfig(); + } + + public static void initConfig() { + System.out.println("Init sentinel config"); + + WebCallbackManager.setUrlBlockHandler(new DefaultUrlBlockHandler()); + WebCallbackManager.setRequestOriginParser(request -> request.getHeader("S-user")); + WebCallbackManager.setUrlCleaner(new MyUrlCleaner()); + } + + static class MyUrlCleaner implements UrlCleaner { + @Override + public String clean(String originUrl) { + if (originUrl.matches("/foo/\\d+")) { + return "/foo/*"; + } + + return originUrl; + } + } + + @Override + public void contextDestroyed(ServletContextEvent servletContextEvent) { + } +} diff --git a/sentinel-demo/sentinel-demo-servlet/src/main/java/com/alibaba/csp/sentinel/demo/servlet/controller/DefaultServlet.java b/sentinel-demo/sentinel-demo-servlet/src/main/java/com/alibaba/csp/sentinel/demo/servlet/controller/DefaultServlet.java new file mode 100644 index 0000000000..85d2047aa3 --- /dev/null +++ b/sentinel-demo/sentinel-demo-servlet/src/main/java/com/alibaba/csp/sentinel/demo/servlet/controller/DefaultServlet.java @@ -0,0 +1,77 @@ +package com.alibaba.csp.sentinel.demo.servlet.controller; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * class description + * + * @author zhangxunwei + * @date 2024/6/24 + */ +public class DefaultServlet implements Servlet { + @Override + public void init(ServletConfig servletConfig) throws ServletException { + + } + + @Override + public ServletConfig getServletConfig() { + return null; + } + + @Override + public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { + String path = ((HttpServletRequest) servletRequest).getPathInfo(); + + if (path.startsWith("/foo")) { + handleFoo(servletRequest, servletResponse); + } else if (path.startsWith("/bar")) { + handleBar(servletRequest, servletResponse); + } else { + notFound(servletRequest, servletResponse); + } + } + + private void notFound(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; + + httpServletResponse.setStatus(404); + httpServletResponse.setContentType("text/plain"); + httpServletResponse.getWriter().write(httpServletRequest.getServletPath() + " not found."); + httpServletResponse.getWriter().close(); + } + + private void handleBar(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException { + HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; + + httpServletResponse.setStatus(200); + httpServletResponse.setContentType("text/plain"); + httpServletResponse.getWriter().write("bar"); + httpServletResponse.getWriter().close(); + } + + private void handleFoo(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; + String path = httpServletRequest.getPathInfo(); + String id = path.replaceAll("/foo/(\\d+)", "$1"); + + httpServletResponse.setStatus(200); + httpServletResponse.setContentType("text/plain"); + httpServletResponse.getWriter().write("Hello " + id); + httpServletResponse.getWriter().close(); + } + + @Override + public String getServletInfo() { + return null; + } + + @Override + public void destroy() { + } +} diff --git a/sentinel-demo/sentinel-demo-servlet/src/main/resources/sentinel.properties b/sentinel-demo/sentinel-demo-servlet/src/main/resources/sentinel.properties new file mode 100644 index 0000000000..fb44f0b08b --- /dev/null +++ b/sentinel-demo/sentinel-demo-servlet/src/main/resources/sentinel.properties @@ -0,0 +1,2 @@ +project.name=sentinel-demo-servlet +csp.sentinel.dashboard.server=http://localhost:8081 \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-servlet/src/main/webapp/WEB-INF/web.xml b/sentinel-demo/sentinel-demo-servlet/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..f7dea322df --- /dev/null +++ b/sentinel-demo/sentinel-demo-servlet/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,36 @@ + + + + + DefaultServlet + com.alibaba.csp.sentinel.demo.servlet.controller.DefaultServlet + + + + DefaultServlet + /* + + + + + SentinelCommonFilter + com.alibaba.csp.sentinel.adapter.servlet.CommonFilter + + HTTP_METHOD_SPECIFY + true + + + WEB_CONTEXT_UNIFY + true + + + + + SentinelCommonFilter + /* + + + diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/InterceptorConfig.java b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/InterceptorConfig.java index c90f51658e..f698b2a7c3 100644 --- a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/InterceptorConfig.java +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/InterceptorConfig.java @@ -15,12 +15,13 @@ */ package com.alibaba.csp.sentinel.demo.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.DefaultBlockExceptionHandler; import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcConfig; import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcTotalConfig; - +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; @@ -39,6 +40,12 @@ public void addInterceptors(InterceptorRegistry registry) { addSpringMvcInterceptor(registry); } + @Bean + public SentinelExceptionAware sentinelExceptionAware() { + //Make exception visible to Sentinel if you have configured ExceptionHandler + return new SentinelExceptionAware(); + } + private void addSpringMvcInterceptor(InterceptorRegistry registry) { SentinelWebMvcConfig config = new SentinelWebMvcConfig(); diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java index ac2aa97635..178762ec12 100644 --- a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java @@ -17,14 +17,17 @@ import java.util.Random; import java.util.concurrent.TimeUnit; + import org.springframework.stereotype.Controller; 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.context.request.async.DeferredResult; import org.springframework.web.servlet.ModelAndView; /** * Test controller + * * @author kaizi2009 */ @Controller @@ -57,7 +60,7 @@ public String apiExclude(@PathVariable("id") Long id) { doBusiness(); return "Exclude " + id; } - + @GetMapping("/forward") public ModelAndView apiForward() { ModelAndView mav = new ModelAndView(); @@ -65,6 +68,17 @@ public ModelAndView apiForward() { return mav; } + @GetMapping("/async") + @ResponseBody + public DeferredResult distribute() throws Exception { + DeferredResult result = new DeferredResult<>(4000L); + + Thread thread = new Thread(() -> result.setResult("async result")); + thread.start(); + + return result; + } + private void doBusiness() { Random random = new Random(1); try {