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
This commit is contained in:
Yuanqing Luo 2018-12-09 23:08:03 +08:00 committed by Eric Zhao
parent ad1d9f0270
commit 6802f97528
6 changed files with 247 additions and 5 deletions

View File

@ -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();
}

View File

@ -171,6 +171,6 @@ public class CommonFilterTest {
@After
public void cleanUp() {
FlowRuleManager.loadRules(null);
ClusterBuilderSlot.getClusterNodeMap().clear();
ClusterBuilderSlot.resetClusterNodes();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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!";
}
}