Springmvc藉助SimpleUrlHandlerMapping實現介面開關功能
一、介面開關功能
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(); @Autowired private RequestMappingHandlerAdapter handlerAdapter; @Override protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { HandlerExecutionChain mappedHandler = handlerMapping.getHandler(request); return handlerAdapter.handle(request, response, mappedHandler.getHandler()); } @Override public void afterPropertiesSet() { handlerMapping.afterPropertiesSet(); } private class BaseRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
//初始化子handler mapping @Override protected void initHandlerMethods() { detectHandlerMethods(BaseController.this); }
//合併父路徑和子handler路徑 @Override protected 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)) {//NOSONAR return; } 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; } @Override public 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,並設定介面開關邏輯攔截器。
至此,介面開關能力已經實現完畢。再也不用在擔心介面會直接暴露出去了,可以通過配置隨時更改介面的訪問許可權。