From 6802f975285031ae48ba84d57dd4f8c30ec75ca1 Mon Sep 17 00:00:00 2001 From: Yuanqing Luo Date: Sun, 9 Dec 2018 23:08:03 +0800 Subject: [PATCH] Add HTTP-method level flow control support in Sentinel Web Servlet Filter (#282) - Add HTTP method level support for servlet filter with a init parameter `HTTP_METHOD_SPECIFY` as the switch. This is useful for REST APIs. - Add test cases and fix test bug by reset the cluster node in ClusterBuilderSlot --- .../adapter/servlet/CommonFilter.java | 24 +++- .../adapter/servlet/CommonFilterTest.java | 2 +- .../servletmethod/CommonFilterMethodTest.java | 133 ++++++++++++++++++ .../servletmethod/FilterMethodConfig.java | 25 ++++ .../servletmethod/TestApplication.java | 30 ++++ .../servletmethod/TestMethodController.java | 38 +++++ 6 files changed, 247 insertions(+), 5 deletions(-) create mode 100644 sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/CommonFilterMethodTest.java create mode 100644 sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/FilterMethodConfig.java create mode 100644 sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/TestApplication.java create mode 100644 sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/TestMethodController.java diff --git a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java index 76b25381..f24bbe55 100755 --- a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java +++ b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java @@ -46,17 +46,23 @@ import com.alibaba.csp.sentinel.util.StringUtil; */ public class CommonFilter implements Filter { + private final static String HTTP_METHOD_SPECIFY = "HTTP_METHOD_SPECIFY"; + private final static String COLON = ":"; + private boolean httpMethodSpecify = false; + @Override public void init(FilterConfig filterConfig) { - + httpMethodSpecify = Boolean.parseBoolean(filterConfig.getInitParameter(HTTP_METHOD_SPECIFY)); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - HttpServletRequest sRequest = (HttpServletRequest)request; + throws IOException, ServletException { + HttpServletRequest sRequest = (HttpServletRequest) request; Entry entry = null; + Entry methodEntry = null; + try { String target = FilterUtil.filterTarget(sRequest); // Clean and unify the URL. @@ -73,9 +79,16 @@ public class CommonFilter implements Filter { ContextUtil.enter(target, origin); entry = SphU.entry(target, EntryType.IN); + + // Add method specification if necessary + if (httpMethodSpecify) { + methodEntry = SphU.entry(sRequest.getMethod().toUpperCase() + COLON + target, + EntryType.IN); + } + chain.doFilter(request, response); } catch (BlockException e) { - HttpServletResponse sResponse = (HttpServletResponse)response; + HttpServletResponse sResponse = (HttpServletResponse) response; // Return the block page, or redirect to another URL. WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse, e); } catch (IOException e2) { @@ -88,6 +101,9 @@ public class CommonFilter implements Filter { Tracer.trace(e4); throw e4; } finally { + if (methodEntry != null) { + methodEntry.exit(); + } if (entry != null) { entry.exit(); } diff --git a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilterTest.java b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilterTest.java index 2602f17d..14e8b2a1 100644 --- a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilterTest.java +++ b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilterTest.java @@ -171,6 +171,6 @@ public class CommonFilterTest { @After public void cleanUp() { FlowRuleManager.loadRules(null); - ClusterBuilderSlot.getClusterNodeMap().clear(); + ClusterBuilderSlot.resetClusterNodes(); } } diff --git a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/CommonFilterMethodTest.java b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/CommonFilterMethodTest.java new file mode 100644 index 00000000..3df4c7d5 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/CommonFilterMethodTest.java @@ -0,0 +1,133 @@ +/* + * 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.servletmethod; + +import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig; +import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil; +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 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; + +import java.util.Collections; + +import static org.junit.Assert.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * @author Roger Law + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +public class CommonFilterMethodTest { + + private static final String HELLO_STR = "Hello!"; + + private static final String HELLO_POST_STR = "Hello Post!"; + + private static final String GET = "GET"; + + private static final String POST = "POST"; + + private static final String COLON = ":"; + + @Autowired + private MockMvc mvc; + + private void configureRulesFor(String resource, int count) { + configureRulesFor(resource, count, "default"); + } + + 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)); + } + + @Test + public void testCommonFilterMiscellaneous() throws Exception { + String url = "/hello"; + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(HELLO_STR)); + + ClusterNode cnGet = ClusterBuilderSlot.getClusterNode(GET + COLON + url); + assertNotNull(cnGet); + assertEquals(1, cnGet.passQps()); + + + ClusterNode cnPost = ClusterBuilderSlot.getClusterNode(POST + COLON + url); + assertNull(cnPost); + + this.mvc.perform(post(url)) + .andExpect(status().isOk()) + .andExpect(content().string(HELLO_POST_STR)); + + cnPost = ClusterBuilderSlot.getClusterNode(POST + COLON + url); + assertNotNull(cnPost); + assertEquals(1, cnPost.passQps()); + + testCommonBlockAndRedirectBlockPage(url, cnGet, cnPost); + + } + + private void testCommonBlockAndRedirectBlockPage(String url, ClusterNode cnGet, ClusterNode cnPost) throws Exception { + configureRulesFor(GET + ":" + url, 0); + // The request will be blocked and response is default block message. + this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()) + .andExpect(content().string(FilterUtil.DEFAULT_BLOCK_MSG)); + assertEquals(1, cnGet.blockQps()); + + // Test for post pass + this.mvc.perform(post(url)) + .andExpect(status().isOk()) + .andExpect(content().string(HELLO_POST_STR)); + + assertEquals(2, cnPost.passQps()); + + + FlowRuleManager.loadRules(null); + WebServletConfig.setBlockPage(""); + } + + + @After + public void cleanUp() { + FlowRuleManager.loadRules(null); + ClusterBuilderSlot.resetClusterNodes(); + } +} diff --git a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/FilterMethodConfig.java b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/FilterMethodConfig.java new file mode 100644 index 00000000..d2c9d21f --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/FilterMethodConfig.java @@ -0,0 +1,25 @@ +package com.alibaba.csp.sentinel.adapter.servletmethod; + +import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author: Roger Law + **/ +@Configuration +public class FilterMethodConfig { + + @Bean + public FilterRegistrationBean sentinelFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(new CommonFilter()); + registration.addUrlPatterns("/*"); + registration.addInitParameter("HTTP_METHOD_SPECIFY", "true"); + registration.setName("sentinelFilter"); + registration.setOrder(1); + + return registration; + } +} diff --git a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/TestApplication.java b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/TestApplication.java new file mode 100644 index 00000000..745d79a0 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/TestApplication.java @@ -0,0 +1,30 @@ +/* + * 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.servletmethod; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Eric Zhao + */ +@SpringBootApplication +public class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } +} diff --git a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/TestMethodController.java b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/TestMethodController.java new file mode 100644 index 00000000..04b08b40 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/TestMethodController.java @@ -0,0 +1,38 @@ +/* + * 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.servletmethod; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Roger Law + */ +@RestController +public class TestMethodController { + + @GetMapping("/hello") + public String apiHello() { + return "Hello!"; + } + + @PostMapping("/hello") + public String apiHelloPost() { + return "Hello Post!"; + } + +}