Springmvc藉助SimpleUrlHandlerMapping實現接口開關功能

1、接口開關功能

  一、可配置化,依賴配置中心html

  二、接口訪問權限可控java

  三、springmvc不會掃描到,即不會直接的將接口暴露出去web

2、接口開關使用場景

  和業務沒什麼關係,主要方便查詢系統中的一些狀態信息。好比系統的配置信息,中間件的狀態信息。這就須要寫一些特定的接口,不能對外直接暴露出去(即不能被springmvc掃描到,不能被swagger掃描到)。spring

3、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 *」)。apache

4、接口開關實現

  就像SimpleUrlHandlerMapping javadoc中描述的那樣,其執行原理簡單理解就是根據URL尋找對應的Handler。藉助這種思想,咱們在Handler中再借助RequestMappingHandlerMapping和RequestMappingHandlerAdapter來幫助咱們完成URL的轉發。這樣作的好處是不須要直接暴露的接口開發規則只須要稍做修改,接下來將詳細介紹一下。api

  請求轉發流程以下

  

  想法是好的,如何實現這一套流程呢?首先要解決如下問題。mvc

  一、定義的接口不能被springmvc掃描到。app

  二、接口定義仍是要按照@RequestMaping規則方式編寫,這樣才能減小開發量而且能被RequestMappingHandlerMapping處理。ide

  三、如何自動註冊url->handler到SimpleUrlHandlerMapping中去。ui

  對於上面須要實現的,首先要了解一些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進行兩者之間的匹配,若是成功則禁止接口訪問。

 5、接口開關配置

@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,並設置接口開關邏輯攔截器。

  至此,接口開關能力已經實現完畢。不再用在擔憂接口會直接暴露出去了,能夠經過配置隨時更改接口的訪問權限。

相關文章
相關標籤/搜索