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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM