1. 程式人生 > >Springmvc藉助SimpleUrlHandlerMapping實現介面開關功能

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,並設定介面開關邏輯攔截器。

  至此,介面開關能力已經實現完畢。再也不用在擔心介面會直接暴露出去了,可以通過配置隨時更改介面的訪問許可權。