一、接口開關功能
1、可配置化,依賴配置中心
2、接口訪問權限可控
3、springmvc不會掃描到,即不會直接的將接口暴露出去
二、接口開關使用場景
和業務沒什么關系,主要方便查詢系統中的一些狀態信息。比如系統的配置信息,中間件的狀態信息。這就需要寫一些特定的接口,不能對外直接暴露出去(即不能被springmvc掃描到,不能被swagger掃描到)。
三、SimpleUrlHandlerMapping官方解釋
SimpleUrlHandlerMapping實現HandlerMapping接口以從URL映射到請求處理程序bean。
支持映射到bean實例和映射到bean名稱;后者是非單身處理程序所必需的。
“urlMap”屬性適合用bean引用填充處理程序映射,例如通過XML bean定義中的map元素。
可以通過“mappings”屬性以java.util.Properties類接受的形式設置bean名稱的映射,如下所示:/welcome.html=ticketController /show.html=ticketController語法為PATH = HANDLER_BEAN_NAME。
如果路徑不以斜杠開頭,則前置一個。支持直接匹配(給定“/ test” - >注冊“/ test”)和“*”模式匹配(給定“/ test” - >注冊“/ t *”)。
四、接口開關實現
就像SimpleUrlHandlerMapping javadoc中描述的那樣,其執行原理簡單理解就是根據URL尋找對應的Handler。借助這種思想,我們在Handler中再借助RequestMappingHandlerMapping和RequestMappingHandlerAdapter來幫助我們完成URL的轉發。這樣做的好處是不需要直接暴露的接口開發規則只需要稍作修改,接下來將詳細介紹一下。
請求轉發流程如下
想法是好的,如何實現這一套流程呢?首先要解決以下問題。
1、定義的接口不能被springmvc掃描到。
2、接口定義還是要按照@RequestMaping規則方式編寫,這樣才能減少開發量并且能被RequestMappingHandlerMapping處理。
3、如何自動注冊url->handler到SimpleUrlHandlerMapping中去。
對于上面需要實現的,首先要了解一些springmvc相關源碼。
RequestMappingHandlerMapping初始化method mapping
/*** Scan beans in the ApplicationContext, detect and register handler methods.* @see #isHandler(Class)* @see #getMappingForMethod(Method, Class)* @see #handlerMethodsInitialized(Map)*/ protected void initHandlerMethods() {if (logger.isDebugEnabled()) {logger.debug("Looking for request mappings in application context: " + getApplicationContext());}String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :getApplicationContext().getBeanNamesForType(Object.class));for (String beanName : beanNames) {if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {Class<?> beanType = null;try {beanType = getApplicationContext().getType(beanName);}catch (Throwable ex) {// An unresolvable bean type, probably from a lazy bean - let's ignore it.if (logger.isDebugEnabled()) {logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);}}if (beanType != null && isHandler(beanType)) {detectHandlerMethods(beanName);}}}handlerMethodsInitialized(getHandlerMethods()); }
? isHandler方法【判斷方法是不是一個具體handler】邏輯如下
protected boolean isHandler(Class<?> beanType) {return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)); }
所以我們定義的開關接口為了不被springmvc掃描到,直接去掉類注釋上的@Controller注解和@RequestMapping注解就好了,如下。
@Component @ResponseBody public class CommonsStateController {@GetMapping("/url1")public String handleUrl1() {return null;}
@GetMapping("/url2")public String handleUrl2() {return null;}
}
按照如上的定義,url? -> handler(/message/state/* ->?CommonsStateController )形式已經出來了,但是還缺少父類路徑 /message/state/ 以及 如何讓RequestMappingHandlerMapping識別CommonsStateController這個handler 中的所有子handler。
抽象Handler以及自定義RequestMappingHandlerMapping
import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.AbstractController; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; import java.util.Objects;/*** @author hujunzheng* @create 2018-08-10 12:53**/ public abstract class BaseController extends AbstractController implements InitializingBean {private RequestMappingHandlerMapping handlerMapping = new BaseRequestMappingHandlerMapping();@Autowiredprivate RequestMappingHandlerAdapter handlerAdapter;@Overrideprotected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {HandlerExecutionChain mappedHandler = handlerMapping.getHandler(request);return handlerAdapter.handle(request, response, mappedHandler.getHandler());}@Overridepublic void afterPropertiesSet() {handlerMapping.afterPropertiesSet();}private class BaseRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
//初始化子handler mapping@Overrideprotected void initHandlerMethods() {detectHandlerMethods(BaseController.this);}
//合并父路徑和子handler路徑@Overrideprotected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {RequestMappingInfo info = super.getMappingForMethod(method, handlerType);if (!Objects.isNull(info) && StringUtils.isNotBlank(getBasePath())) {info = RequestMappingInfo.paths(getBasePath()).build().combine(info);}return info;}}//開關接口定義父路徑public abstract String getBasePath(); }
所有開關接口handler都繼承這個BaseController?抽象類,在對象初始時創建所有的子handler mapping。SimpleUrlHandlerMapping最終會調用開關接口的handleRequestInternal方法,方法內部通過RequestMappingHandlerMapping和RequestMappingHandlerAdapter 將請求轉發到具體的子handler。
@Component @ResponseBody public class CommonsStateController extends BaseController {@GetMapping("/url1")public String handleUrl1() {return null;}@GetMapping("/url2")public String handleUrl2() {return null;} }
?
自動注冊url-handler到SimpleUrlHandlerMapping
import org.apache.commons.lang3.StringUtils; import org.springframework.util.CollectionUtils; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;import java.util.HashMap; import java.util.List; import java.util.Map;/*** @author hujunzheng* @create 2018-08-10 13:57**/ public class EnhanceSimpleUrlHandlerMapping extends SimpleUrlHandlerMapping {public EnhanceSimpleUrlHandlerMapping(List<BaseController> controllers) {if (CollectionUtils.isEmpty(controllers)) {//NOSONARreturn;}Map<String, BaseController> urlMappings = new HashMap<>();controllers.forEach(controller -> {String basePath = controller.getBasePath();if (StringUtils.isNotBlank(basePath)) {if (!basePath.endsWith("/*")) {basePath = basePath + "/*";}urlMappings.put(basePath, controller);}});this.setUrlMap(urlMappings);} }
獲取BaseController父路徑,末尾加上‘/*’,然后將url -> handler關系注冊到SimpleUrlHandlerMapping的urlMap中去。這樣只要請求路徑是 父路徑/*的模式都會被SimpleUrlHandlerMapping處理并轉發給對應的handler(BaseController),然后在轉發給具體的子handler。
接口開關邏輯
import com.cmos.wmhopenapi.service.config.LimitConstants; import com.google.common.collect.Lists; import org.apache.commons.lang3.StringUtils; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.stream.Collectors;/*** @author hujunzheng* @create 2018-08-10 15:17**/ public class UrlHandlerInterceptor extends HandlerInterceptorAdapter {private SimpleUrlHandlerMapping mapping;private LimitConstants limitConstants;public UrlHandlerInterceptor(SimpleUrlHandlerMapping mapping, LimitConstants limitConstants) {this.mapping = mapping;this.limitConstants = limitConstants;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String lookupUrl = mapping.getUrlPathHelper().getLookupPathForRequest(request);String urllimits = limitConstants.getUrllimits();if (StringUtils.isNotBlank(urllimits)) {for (String urllimit : Lists.newArrayList(urllimits.split(",")).stream().map(value -> value.trim()).collect(Collectors.toList())) {if (mapping.getPathMatcher().match(urllimit, lookupUrl)) {return false;}}}return true;} }
基本思路就是通過 UrlPathHelper獲取到request的lookupUrl(例如 /message/state/url1) ,然后獲取到配置中心配置的patter path(例如message/state/*),最后通過 AntPathMatcher進行二者之間的匹配,如果成功則禁止接口訪問。
?五、接口開關配置
@Bean public SimpleUrlHandlerMapping simpleUrlHandlerMapping(ObjectProvider<List<BaseController>> controllers, LimitConstants limitConstants) {SimpleUrlHandlerMapping mapping = new EnhanceSimpleUrlHandlerMapping(controllers.getIfAvailable());mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);mapping.setInterceptors(new UrlHandlerInterceptor(mapping, limitConstants));return mapping; }
創建自定義的SimpleUrlHandlerMapping,然后將類型為BaseController所有handler以構造參數的形式傳給SimpleUrlHandlerMapping,并設置接口開關邏輯攔截器。
至此,接口開關能力已經實現完畢。再也不用在擔心接口會直接暴露出去了,可以通過配置隨時更改接口的訪問權限。