From b14534fb359a4b956e250aa554266a5a89d978cf Mon Sep 17 00:00:00 2001 From: kaizi2009 Date: Wed, 27 Nov 2019 14:47:04 +0800 Subject: [PATCH] Add Sentinel Spring Web MVC adapter module (#1104) - Add sentinel-spring-webmvc-adapter module and demo --- sentinel-adapter/pom.xml | 1 + .../sentinel-spring-webmvc-adapter/README.md | 115 +++++++++++++ .../sentinel-spring-webmvc-adapter/pom.xml | 55 +++++++ .../webmvc/AbstractSentinelInterceptor.java | 138 ++++++++++++++++ .../spring/webmvc/SentinelInterceptor.java | 80 +++++++++ .../webmvc/SentinelTotalInterceptor.java | 56 +++++++ .../callback/BlockExceptionHandler.java | 40 +++++ .../DefaultBlockExceptionHandler.java | 46 ++++++ .../webmvc/callback/RequestOriginParser.java | 34 ++++ .../spring/webmvc/callback/UrlCleaner.java | 32 ++++ .../webmvc/config/BaseWebMvcConfig.java | 55 +++++++ .../webmvc/config/SentinelWebMvcConfig.java | 49 ++++++ .../config/SentinelWebMvcTotalConfig.java | 47 ++++++ .../adapter/spring/webmvc/ResultWrapper.java | 53 ++++++ .../spring/webmvc/TestApplication.java | 31 ++++ .../spring/webmvc/TestInterceptor.java | 155 ++++++++++++++++++ .../webmvc/config/InterceptorConfig.java | 91 ++++++++++ .../SentinelSpringMvcBlockHandlerConfig.java | 54 ++++++ .../webmvc/controller/TestController.java | 55 +++++++ sentinel-demo/pom.xml | 1 + .../sentinel-demo-spring-webmvc/pom.xml | 44 +++++ .../spring/webmvc/WebMvcDemoApplication.java | 33 ++++ .../webmvc/config/InterceptorConfig.java | 77 +++++++++ .../SentinelSpringMvcBlockHandlerConfig.java | 45 +++++ .../controller/WebMvcTestController.java | 64 ++++++++ .../demo/spring/webmvc/vo/ResultWrapper.java | 56 +++++++ .../src/main/resources/application.properties | 1 + 27 files changed, 1508 insertions(+) create mode 100755 sentinel-adapter/sentinel-spring-webmvc-adapter/README.md create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/pom.xml create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelInterceptor.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelTotalInterceptor.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/BlockExceptionHandler.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/DefaultBlockExceptionHandler.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/RequestOriginParser.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/UrlCleaner.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/BaseWebMvcConfig.java create mode 100755 sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelWebMvcConfig.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelWebMvcTotalConfig.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/ResultWrapper.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/TestApplication.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/TestInterceptor.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/InterceptorConfig.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelSpringMvcBlockHandlerConfig.java create mode 100644 sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/controller/TestController.java create mode 100644 sentinel-demo/sentinel-demo-spring-webmvc/pom.xml create mode 100644 sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/WebMvcDemoApplication.java create mode 100644 sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/InterceptorConfig.java create mode 100644 sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/SentinelSpringMvcBlockHandlerConfig.java create mode 100644 sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java create mode 100644 sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/vo/ResultWrapper.java create mode 100644 sentinel-demo/sentinel-demo-spring-webmvc/src/main/resources/application.properties diff --git a/sentinel-adapter/pom.xml b/sentinel-adapter/pom.xml index 02b5268c..0abdf666 100755 --- a/sentinel-adapter/pom.xml +++ b/sentinel-adapter/pom.xml @@ -24,6 +24,7 @@ sentinel-spring-webflux-adapter sentinel-api-gateway-adapter-common sentinel-spring-cloud-gateway-adapter + sentinel-spring-webmvc-adapter diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/README.md b/sentinel-adapter/sentinel-spring-webmvc-adapter/README.md new file mode 100755 index 00000000..1693c281 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/README.md @@ -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 + + com.alibaba.csp + sentinel-spring-webmvc-adapter + x.y.z + +``` + +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. +```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:
1. The default value is null, you can hanlde `BlockException` in spring MVC;
2.Use `DefaultBlockExceptionHandler`;
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 | + diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/pom.xml b/sentinel-adapter/sentinel-spring-webmvc-adapter/pom.xml new file mode 100644 index 00000000..012480d1 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/pom.xml @@ -0,0 +1,55 @@ + + + + sentinel-adapter + com.alibaba.csp + 1.7.0-SNAPSHOT + + 4.0.0 + + sentinel-spring-webmvc-adapter + + + 5.1.8.RELEASE + 2.1.3.RELEASE + 3.1.0 + + + + + com.alibaba.csp + sentinel-core + + + javax.servlet + javax.servlet-api + ${servlet.api.version} + provided + + + org.springframework + spring-webmvc + ${spring.version} + provided + + + org.springframework.boot + spring-boot-starter-web + ${spring.boot.version} + test + + + org.springframework.boot + spring-boot-starter-test + ${spring.boot.version} + test + + + com.alibaba + fastjson + test + + + \ No newline at end of file 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 new file mode 100644 index 00000000..4c4d2757 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java @@ -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; + } +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelInterceptor.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelInterceptor.java new file mode 100644 index 00000000..a9c5c34e --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelInterceptor.java @@ -0,0 +1,80 @@ +/* + * 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.SentinelWebMvcConfig; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.UrlCleaner; +import com.alibaba.csp.sentinel.log.RecordLog; + +import javax.servlet.http.HttpServletRequest; + +import com.alibaba.csp.sentinel.util.StringUtil; +import org.springframework.web.servlet.HandlerMapping; + +/** + * Spring mvc interceptor that integrates with sentinel. + * + * @author kaizi2009 + */ +public class SentinelInterceptor extends AbstractSentinelInterceptor { + private SentinelWebMvcConfig config; + + public SentinelInterceptor(SentinelWebMvcConfig config) { + super(); + setConfig(config); + super.setBaseWebMvcConfig(config); + } + + public SentinelInterceptor() { + this(new SentinelWebMvcConfig()); + } + + public SentinelInterceptor setConfig(SentinelWebMvcConfig config) { + if (config == null) { + this.config = new SentinelWebMvcConfig(); + RecordLog.info("Config is null, use default config"); + } else { + this.config = config; + } + RecordLog.info(String.format("SentinelInterceptor config: requestAttributeName=%s, originParser=%s, httpMethodSpecify=%s, blockExceptionHandler=%s, urlCleaner=%s", config.getRequestAttributeName(), config.getOriginParser(), config.isHttpMethodSpecify(), config.getBlockExceptionHandler(), config.getUrlCleaner())); + return this; + } + + /** + * Get target in HttpServletRequest + * + * @param request + * @return + */ + @Override + protected String getResourceName(HttpServletRequest request) { + Object resourceNameObject = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); + if (resourceNameObject == null || !(resourceNameObject instanceof String)) { + return null; + } + String resourceName = (String) resourceNameObject; + UrlCleaner urlCleaner = config.getUrlCleaner(); + if (urlCleaner != null) { + resourceName = urlCleaner.clean(resourceName); + } + // Add method specification if necessary + if (StringUtil.isNotEmpty(resourceName) && config.isHttpMethodSpecify()) { + resourceName = request.getMethod().toUpperCase() + COLON + resourceName; + } + return resourceName; + } + +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelTotalInterceptor.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelTotalInterceptor.java new file mode 100644 index 00000000..5cda2aff --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelTotalInterceptor.java @@ -0,0 +1,56 @@ +/* + * 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.SentinelWebMvcTotalConfig; +import com.alibaba.csp.sentinel.log.RecordLog; + +import javax.servlet.http.HttpServletRequest; + +/** + * Spring mvc interceptor for all requests. + * + * @author kaizi2009 + */ +public class SentinelTotalInterceptor extends AbstractSentinelInterceptor { + private SentinelWebMvcTotalConfig config; + + public SentinelTotalInterceptor(SentinelWebMvcTotalConfig config) { + super(); + setConfig(config); + setBaseWebMvcConfig(config); + } + + public SentinelTotalInterceptor() { + this(new SentinelWebMvcTotalConfig()); + } + + public SentinelTotalInterceptor setConfig(SentinelWebMvcTotalConfig config) { + if (config == null) { + this.config = new SentinelWebMvcTotalConfig(); + RecordLog.info("Config is null, use default config"); + } else { + this.config = config; + } + RecordLog.info(String.format("SentinelInterceptor config: requestAttributeName=%s, originParser=%s, blockExceptionHandler=%s, totalResourceName=%s", config.getRequestAttributeName(), config.getOriginParser(), config.getBlockExceptionHandler(), config.getTotalResourceName())); + return this; + } + + @Override + protected String getResourceName(HttpServletRequest request) { + return config.getTotalResourceName(); + } +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/BlockExceptionHandler.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/BlockExceptionHandler.java new file mode 100644 index 00000000..fcf61809 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/BlockExceptionHandler.java @@ -0,0 +1,40 @@ +/* + * 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.callback; + +import com.alibaba.csp.sentinel.slots.block.BlockException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Handle BlockException + * + * @author kaizi2009 + */ +public interface BlockExceptionHandler { + + /** + * Handle BlockException + * + * @param request + * @param response + * @param e Depending on your situation, you can choose to process or throw BlockException + * @throws Exception + */ + void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception; + +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/DefaultBlockExceptionHandler.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/DefaultBlockExceptionHandler.java new file mode 100644 index 00000000..b1df0067 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/DefaultBlockExceptionHandler.java @@ -0,0 +1,46 @@ +/* + * 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.callback; + +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.util.StringUtil; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.PrintWriter; + +/** + * Default `BlockException` handler + * + * @author kaizi2009 + */ +public class DefaultBlockExceptionHandler implements BlockExceptionHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception { + StringBuffer url = request.getRequestURL(); + + if ("GET".equals(request.getMethod()) && StringUtil.isNotBlank(request.getQueryString())) { + url.append("?").append(request.getQueryString()); + } + + PrintWriter out = response.getWriter(); + out.print("Blocked by Sentinel (flow limiting)"); + out.flush(); + out.close(); + } + +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/RequestOriginParser.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/RequestOriginParser.java new file mode 100644 index 00000000..647e0183 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/RequestOriginParser.java @@ -0,0 +1,34 @@ +/* + * Copyright 1999-2018 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 + * + * http://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.callback; + +import javax.servlet.http.HttpServletRequest; + +/** + * The origin parser parses request origin (e.g. IP, user, appName) from HTTP request. + * + * @author kaizi2009 + */ +public interface RequestOriginParser { + + /** + * Parse the origin from given HTTP request. + * + * @param request HTTP request + * @return parsed origin + */ + String parseOrigin(HttpServletRequest request); +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/UrlCleaner.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/UrlCleaner.java new file mode 100644 index 00000000..9b7f7f89 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/UrlCleaner.java @@ -0,0 +1,32 @@ +/* + * 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.callback; + +/** + * Clean sentinel target + * + * @author kaizi2009 + */ +public interface UrlCleaner { + + /** + * Clean sentinel target + * + * @param originUrl + * @return + */ + String clean(String originUrl); +} 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 new file mode 100644 index 00000000..ef1da39d --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/BaseWebMvcConfig.java @@ -0,0 +1,55 @@ +/* + * 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 + * + * http://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.config; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; + +/** + * Common config + * + * @author kaizi2009 + */ +public abstract class BaseWebMvcConfig { + protected String requestAttributeName; + protected BlockExceptionHandler blockExceptionHandler; + protected RequestOriginParser originParser; + + public String getRequestAttributeName() { + return requestAttributeName; + } + + public void setRequestAttributeName(String requestAttributeName) { + this.requestAttributeName = requestAttributeName; + } + + public BlockExceptionHandler getBlockExceptionHandler() { + return blockExceptionHandler; + } + + public void setBlockExceptionHandler(BlockExceptionHandler blockExceptionHandler) { + this.blockExceptionHandler = blockExceptionHandler; + } + + public RequestOriginParser getOriginParser() { + return originParser; + } + + public void setOriginParser(RequestOriginParser originParser) { + this.originParser = originParser; + } + +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelWebMvcConfig.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelWebMvcConfig.java new file mode 100755 index 00000000..35f75533 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelWebMvcConfig.java @@ -0,0 +1,49 @@ +/* + * Copyright 1999-2018 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 + * + * http://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.config; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.UrlCleaner; + +/** + * @author kaizi2009 + */ +public class SentinelWebMvcConfig extends BaseWebMvcConfig { + + public static final String DEFAULT_REQUEST_ATTRIBUTE_NAME = "sentinel_spring_mvc_entry_container"; + private UrlCleaner urlCleaner; + protected boolean httpMethodSpecify; + + public SentinelWebMvcConfig() { + super(); + setRequestAttributeName(DEFAULT_REQUEST_ATTRIBUTE_NAME); + } + + public UrlCleaner getUrlCleaner() { + return urlCleaner; + } + + public void setUrlCleaner(UrlCleaner urlCleaner) { + this.urlCleaner = urlCleaner; + } + + public boolean isHttpMethodSpecify() { + return httpMethodSpecify; + } + + public void setHttpMethodSpecify(boolean httpMethodSpecify) { + this.httpMethodSpecify = httpMethodSpecify; + } +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelWebMvcTotalConfig.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelWebMvcTotalConfig.java new file mode 100644 index 00000000..bf90b2f8 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelWebMvcTotalConfig.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.config; + + +/** + * @author kaizi2009 + */ +public class SentinelWebMvcTotalConfig extends BaseWebMvcConfig { + public static final String DEFAULT_TOTAL_RESOURCE_NAME = "spring-mvc-total-url-request"; + public static final String DEFAULT_REQUEST_ATTRIBUTE_NAME = "sentinel_spring_mvc_total_entry_container"; + + private String totalResourceName = DEFAULT_TOTAL_RESOURCE_NAME; + + public SentinelWebMvcTotalConfig() { + super(); + setRequestAttributeName(DEFAULT_REQUEST_ATTRIBUTE_NAME); + } + + public String getTotalResourceName() { + return totalResourceName; + } + + /** + * Config total resource name + * + * @param totalResourceName + * @return + */ + public void setTotalResourceName(String totalResourceName) { + this.totalResourceName = totalResourceName; + } + +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/ResultWrapper.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/ResultWrapper.java new file mode 100644 index 00000000..66121fcd --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/ResultWrapper.java @@ -0,0 +1,53 @@ +/* + * 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.fastjson.JSONObject; + +/** + * @author kaizi2009 + */ +public class ResultWrapper { + + private Integer code; + private String message; + + public ResultWrapper(Integer code, String message) { + this.code = code; + this.message = message; + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public static ResultWrapper error() { + + return new ResultWrapper(-1, "System error"); + } + + public static ResultWrapper blocked() { + return new ResultWrapper(-2, "Blocked by Sentinel"); + } + + public String toJsonString() { + return JSONObject.toJSONString(this); + } +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/TestApplication.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/TestApplication.java new file mode 100644 index 00000000..da9b6bba --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/TestApplication.java @@ -0,0 +1,31 @@ +/* + * 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 org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +/** + * @author kaizi2009 + */ +@SpringBootApplication +public class TestApplication { + public static void main(String[] args) { + SpringApplication.run(TestApplication.class); + } +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/TestInterceptor.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/TestInterceptor.java new file mode 100644 index 00000000..c7304da8 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/TestInterceptor.java @@ -0,0 +1,155 @@ +package com.alibaba.csp.sentinel.adapter.spring.webmvc; +/* + * 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. + */ + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +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.node.ClusterNode; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +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; +import com.alibaba.csp.sentinel.util.StringUtil; + +import java.util.Collections; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +/** + * @author kaizi2009 + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestApplication.class) +@AutoConfigureMockMvc +public class TestInterceptor { + + private static final String HELLO_STR = "Hello!"; + @Autowired + private MockMvc mvc; + + @Test + public void testBase() throws Exception { + String url = "/hello"; + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(HELLO_STR)); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(1, cn.passQps(), 0.01); + } + + @Test + public void testOriginParser() throws Exception { + String springMvcPathVariableUrl = "/foo/{id}"; + String limitOrigin = "userA"; + final String headerName = "S-User"; + configureRulesFor(springMvcPathVariableUrl, 0, limitOrigin); + + this.mvc.perform(get("/foo/1").accept(MediaType.TEXT_PLAIN).header(headerName, "userB")) + .andExpect(status().isOk()) + .andExpect(content().string("foo 1")); + + // This will be blocked and reponse json. + this.mvc.perform( + get("/foo/2").accept(MediaType.APPLICATION_JSON).header(headerName, limitOrigin)) + .andExpect(status().isOk()) + .andExpect(content().json(ResultWrapper.blocked().toJsonString())); + this.mvc.perform(get("/foo/3").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string(ResultWrapper.blocked().toJsonString())); + + FlowRuleManager.loadRules(null); + } + + @Test + public void testTotalInterceptor() throws Exception { + String url = "/hello"; + String totalTarget = "my_spring_mvc_total_url_request"; + for (int i = 0; i < 3; i++) { + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(HELLO_STR)); + } + ClusterNode cn = ClusterBuilderSlot.getClusterNode(totalTarget); + assertNotNull(cn); + assertEquals(3, cn.passQps(), 0.01); + } + + @Test + public void testRuntimeException() throws Exception { + String url = "/runtimeException"; + configureExceptionRulesFor(url, 3, null); + int repeat = 3; + for (int i = 0; i < repeat; i++) { + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(ResultWrapper.error().toJsonString())); + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(i + 1, cn.passQps(), 0.01); + } + + // This will be blocked and reponse 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) + .setGrade(RuleConstant.FLOW_GRADE_QPS); + rule.setResource(resource); + if (StringUtil.isNotBlank(limitApp)) { + rule.setLimitApp(limitApp); + } + FlowRuleManager.loadRules(Collections.singletonList(rule)); + } + + private void configureExceptionRulesFor(String resource, int count, String limitApp) { + FlowRule rule = new FlowRule() + .setCount(count) + .setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO); + rule.setResource(resource); + if (StringUtil.isNotBlank(limitApp)) { + rule.setLimitApp(limitApp); + } + FlowRuleManager.loadRules(Collections.singletonList(rule)); + } + + @After + public void cleanUp() { + FlowRuleManager.loadRules(null); + ClusterBuilderSlot.resetClusterNodes(); + } +} 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 new file mode 100644 index 00000000..d6135ead --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/InterceptorConfig.java @@ -0,0 +1,91 @@ +/* + * 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.config; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelInterceptor; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelTotalInterceptor; +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.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 + * + * @author kaizi2009 + */ +@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) { + //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; + } + } + }); + + //Custom configuration if necessary + config.setHttpMethodSpecify(false); + config.setOriginParser(new RequestOriginParser() { + @Override + public String parseOrigin(HttpServletRequest request) { + return 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("/**"); + } +} 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 new file mode 100644 index 00000000..b8e46603 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelSpringMvcBlockHandlerConfig.java @@ -0,0 +1,54 @@ +/* + * 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.config; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.ResultWrapper; +import com.alibaba.csp.sentinel.slots.block.AbstractRule; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.Order; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * Config 'BlockException' handler, handler it in spring veb 'ExceptionHandler' + * + * @author kaizi2009 + */ +@ControllerAdvice +@Order(0) +public class SentinelSpringMvcBlockHandlerConfig { + private Logger logger = LoggerFactory.getLogger(this.getClass()); + + @ExceptionHandler(BlockException.class) + @ResponseBody + public ResultWrapper sentinelBlockHandler(BlockException e) { + AbstractRule rule = e.getRule(); + //Log + logger.info("Blocked by sentinel, {}", rule.toString()); + //Return object + return ResultWrapper.blocked(); + } + + @ExceptionHandler(Exception.class) + @ResponseBody + public ResultWrapper exceptionHandler(Exception e) { + logger.error("System error", e.getMessage()); + return new ResultWrapper(-1, "System 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 new file mode 100644 index 00000000..d8a42ab8 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/controller/TestController.java @@ -0,0 +1,55 @@ +/* + * 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.controller; + + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author kaizi2009 + */ +@RestController +public class TestController { + + @GetMapping("/hello") + public String apiHello() { + return "Hello!"; + } + + @GetMapping("/err") + public String apiError() { + return "Oops..."; + } + + @GetMapping("/foo/{id}") + public String apiFoo(@PathVariable("id") Long id) { + return "foo " + id; + } + + @GetMapping("/runtimeException") + public String runtimeException() { + int i = 1 / 0; + return "runtimeException"; + } + + @GetMapping("/exclude/{id}") + public String apiExclude(@PathVariable("id") Long id) { + return "Exclude " + id; + } + +} diff --git a/sentinel-demo/pom.xml b/sentinel-demo/pom.xml index 24670928..5b0423d5 100755 --- a/sentinel-demo/pom.xml +++ b/sentinel-demo/pom.xml @@ -35,6 +35,7 @@ sentinel-demo-spring-cloud-gateway sentinel-demo-zuul-gateway sentinel-demo-etcd-datasource + sentinel-demo-spring-webmvc diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/pom.xml b/sentinel-demo/sentinel-demo-spring-webmvc/pom.xml new file mode 100644 index 00000000..054649d5 --- /dev/null +++ b/sentinel-demo/sentinel-demo-spring-webmvc/pom.xml @@ -0,0 +1,44 @@ + + + + sentinel-demo + com.alibaba.csp + 1.7.0-SNAPSHOT + + 4.0.0 + + sentinel-demo-spring-webmvc + + + 2.1.3.RELEASE + + + + + com.alibaba.csp + sentinel-core + + + com.alibaba.csp + sentinel-transport-simple-http + + + com.alibaba.csp + sentinel-spring-webmvc-adapter + ${project.version} + + + org.springframework.boot + spring-boot-starter-web + ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-test + ${spring.boot.version} + + + + \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/WebMvcDemoApplication.java b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/WebMvcDemoApplication.java new file mode 100644 index 00000000..4004e562 --- /dev/null +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/WebMvcDemoApplication.java @@ -0,0 +1,33 @@ +/* + * 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 + * + * http://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.demo.spring.webmvc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author kaizi2009 + * + * -Dcsp.sentinel.dashboard.server=127.0.0.1:8080 -Dproject.name=sentinel-demo-spring-webmvc + * + */ +@SpringBootApplication +public class WebMvcDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(WebMvcDemoApplication.class); + } +} 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 new file mode 100644 index 00000000..308e28fc --- /dev/null +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/InterceptorConfig.java @@ -0,0 +1,77 @@ +/* + * 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.demo.spring.webmvc.config; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelInterceptor; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelTotalInterceptor; +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.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Config sentinel interceptor + * + * @author kaizi2009 + */ +@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) { + //Config + SentinelWebMvcConfig config = new SentinelWebMvcConfig(); + + config.setBlockExceptionHandler((request, response, e) -> { + //Depending on your situation, you can choose to process or throw + boolean needThrow = true; + if (needThrow) { + throw e; + } else { + //Write string or json string; + response.getWriter().write("Blocked by sentinel"); + } + }); + + //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) { + //Config + 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("/**"); + } +} diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/SentinelSpringMvcBlockHandlerConfig.java b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/SentinelSpringMvcBlockHandlerConfig.java new file mode 100644 index 00000000..8dc3d258 --- /dev/null +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/SentinelSpringMvcBlockHandlerConfig.java @@ -0,0 +1,45 @@ +/* + * 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.demo.spring.webmvc.config; + +import com.alibaba.csp.sentinel.demo.spring.webmvc.vo.ResultWrapper; +import com.alibaba.csp.sentinel.slots.block.AbstractRule; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.Order; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * Config blocked handler + * @author kaizi2009 + */ +@ControllerAdvice +@Order(0) +public class SentinelSpringMvcBlockHandlerConfig { + private Logger logger = LoggerFactory.getLogger(this.getClass()); + @ExceptionHandler(BlockException.class) + @ResponseBody + public ResultWrapper sentinelBlockHandler(BlockException e) { + AbstractRule rule = e.getRule(); + //Log + logger.info("Blocked by sentinel, {}", rule.toString()); + //Return object + return ResultWrapper.blocked(); + } +} 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 new file mode 100644 index 00000000..c70342c8 --- /dev/null +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java @@ -0,0 +1,64 @@ +/* + * 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 + * + * http://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.demo.spring.webmvc.controller; + +import java.util.Random; +import java.util.concurrent.TimeUnit; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +/** + * Test controller + * @author kaizi2009 + */ +@RestController +public class WebMvcTestController { + + @GetMapping("/hello") + public String apiHello() { + doBusiness(); + return "Hello!"; + } + + @GetMapping("/err") + public String apiError() { + doBusiness(); + return "Oops..."; + } + + @GetMapping("/foo/{id}") + public String apiFoo(@PathVariable("id") Long id) { + doBusiness(); + return "Hello " + id; + } + + @GetMapping("/exclude/{id}") + public String apiExclude(@PathVariable("id") Long id) { + doBusiness(); + return "Exclude " + id; + } + + private void doBusiness() { + Random random = new Random(1); + try { + TimeUnit.MILLISECONDS.sleep(random.nextInt(100)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + +} diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/vo/ResultWrapper.java b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/vo/ResultWrapper.java new file mode 100644 index 00000000..c4c45abc --- /dev/null +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/vo/ResultWrapper.java @@ -0,0 +1,56 @@ +/* + * 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 + * + * http://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.demo.spring.webmvc.vo; + +import com.alibaba.fastjson.JSONObject; + +/** + * @author kaizi2009 + */ +public class ResultWrapper { + + private Integer code; + private String message; + + public ResultWrapper(Integer code, String message) { + this.code = code; + this.message = message; + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public static ResultWrapper blocked() { + return new ResultWrapper(-1, "Blocked by Sentinel"); + } + + public String toJsonString() { + return JSONObject.toJSONString(this); + } +} diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/resources/application.properties b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/resources/application.properties new file mode 100644 index 00000000..397dea55 --- /dev/null +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=10000 \ No newline at end of file