1. 程式人生 > >從SpringMVC獲取使用者資訊談起

從SpringMVC獲取使用者資訊談起

  • Github地址:https://github.com/andyslin/spring-ext
  • 編譯、執行環境:JDK 8 + Maven 3 + IDEA + Lombok
  • spring-boot:2.1.0.RELEASE(Spring:5.1.2.RELEASE)
  • 如要本地執行github上的專案,需要安裝lombok外掛

上週末拜讀了一位牛人的公眾號文章<<Token認證,如何快速方便獲取使用者資訊>>,語言風趣,引人入勝,為了表示濤濤敬仰之情,已經轉載到自己的公眾號了。

回顧一下文章內容,為了在Controller的方法中獲取已經認證過的使用者資訊(比如通過JWT-JSON Web Token傳輸的Token

),文中提供了三種方式:

  • 方式一(很挫)直接在Controller方法中獲取Token頭,然後解析;
  • 方式二(優雅)在過濾器Filter中驗證JWT後,直接使用HttpServletRequestWrapper偷樑換柱,覆蓋getHeader方法,然後在Controller方法中呼叫getHeader,這樣就不需要再次解析了;
  • 方式三(很優雅)同樣在過濾器Filter中使用HttpServletRequestWrapper,只是覆蓋getParameterNamesgetParameterValues(針對表單提交)和getInputStream(針對JSON提交),然後就可以和客戶端引數相同的方式獲取了。

方式一需要重複解析JWT,而且控制器和Servlet API繫結,不方便測試,但是勝在簡單直接。方式二和方式三雖然是一個很好的練習HttpServletRequestWrapper的示例,但是可能還算不上是優雅的獲取使用者資訊的方式。

不妨思考一下:

  • 除了獲取userId外,如果還想獲取JWT中PAYLOAD的其它資訊,能不能做到只修改Controller?還是需要再次修改驗證JWT的過濾器Filter呢?
  • HttpServletRequestgetInpustStream()方法,Web容器實現基本都是隻能呼叫一次的,因而方式三在擴充套件getInpustStream()的時候,先將其轉換為byte[]
    ,然後為了新增使用者資訊,再將byte[]反序列化為map,新增使用者資訊之後又序列化為byte[],反覆多次,這種方式效能怎麼樣?如果是檔案上傳,這種方式能否行得通?
  • 方式三中HttpServletRequestWrapper會無形中啟到遮蔽loginUserId引數的作用,但如果客戶端的的確確傳入了一個loginUserId的引數(當然,這種情況還是需要儘量避免),在Controller中怎麼又獲取到客戶端的這個引數?

有沒有什麼其它的方式呢?

SpringMVC中關於引數繫結有很多介面,其中很關鍵的一個是HandlerMethodArgumentResolver,可以通過新增新實現類來實現獲取使用者資訊嗎?當然可以,對應該介面的兩個方法,首先要能夠識別什麼情況下需要繫結使用者資訊,一般來說,可以根據引數的特殊型別,也可以根據引數的特殊註解;其次要能夠獲取到使用者資訊,類似於原文中做的那樣。雖然這樣做也可以實現功能,但是卻很繁瑣。

不如拋開怎麼獲取使用者資訊不談,先來看看SpringMVC在控制器的處理方法HandlerMethod中繫結引數是怎麼做的?

熟悉SpringMVC處理流程的朋友,自然知道,主控制器是DispatcherServlet,在doDispatch()方法中根據HandlerMapping找到處理器,然後找到可以呼叫該處理器的HandlerAdapter,其中最常用也最核心的莫過於RequestMappingHandlerMappingHandlerMethodRequestMappingHandlerAdapter組合了。檢視RequestMappingHandlerAdapter的原始碼,找到呼叫HandlerMethod的方法:

@Override
protected ModelAndView handleInternal(HttpServletRequest request,
        HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

    ModelAndView mav;
    checkRequest(request);

    // Execute invokeHandlerMethod in synchronized block if required.
    if (this.synchronizeOnSession) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            Object mutex = WebUtils.getSessionMutex(session);
            synchronized (mutex) {
                mav = invokeHandlerMethod(request, response, handlerMethod);
            }
        }
        else {
            // No HttpSession available -> no mutex necessary
            mav = invokeHandlerMethod(request, response, handlerMethod);
        }
    }
    else {
        // No synchronization on session demanded at all...
        mav = invokeHandlerMethod(request, response, handlerMethod);
    }

    if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
        if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
            applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
        }
        else {
            prepareResponse(response);
        }
    }

    return mav;
}

可以看到,真正的呼叫是委託給invokeHandlerMethod()方法了:

@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
        HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    try {
        // 建立資料繫結工廠
        WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
        ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

        // 建立可呼叫的方法
        ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
        if (this.argumentResolvers != null) {
            invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
        }
        if (this.returnValueHandlers != null) {
            invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
        }
        invocableMethod.setDataBinderFactory(binderFactory);
        invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);

        ModelAndViewContainer mavContainer = new ModelAndViewContainer();
        mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
        modelFactory.initModel(webRequest, mavContainer, invocableMethod);
        mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

        // 省略非同步處理相關程式碼

        // 這裡才是真正的方法呼叫
        invocableMethod.invokeAndHandle(webRequest, mavContainer);
       
        // 處理返回結果
        return getModelAndView(mavContainer, modelFactory, webRequest);
    }
    finally {
        webRequest.requestCompleted();
    }
}

這個方法很關鍵,如果需要研讀SpringMVC,可以從這個方法著手。不過由於這篇文章關注的是引數繫結,所以這裡只關心WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);這句程式碼,接著看getDataBinderFactory()方法:

private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
    Class<?> handlerType = handlerMethod.getBeanType();
    Set<Method> methods = this.initBinderCache.get(handlerType);
    if (methods == null) {
        methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
        this.initBinderCache.put(handlerType, methods);
    }
    List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
    // Global methods first
    this.initBinderAdviceCache.forEach((clazz, methodSet) -> {
        if (clazz.isApplicableToBeanType(handlerType)) {
            Object bean = clazz.resolveBean();
            for (Method method : methodSet) {
                initBinderMethods.add(createInitBinderMethod(bean, method));
            }
        }
    });
    for (Method method : methods) {
        Object bean = handlerMethod.getBean();
        initBinderMethods.add(createInitBinderMethod(bean, method));
    }
    return createDataBinderFactory(initBinderMethods);
}

這個方法前面的程式碼都是一些準備工作,比如呼叫ControllerAdvice,最終還是呼叫createDataBinderFactory()方法:

protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)
            throws Exception {

    return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
}

終於看到資料繫結工廠例項的建立了,方法體非常簡單,只有一個new,而且非常幸運,這個方法是protected的,這說明,SpringMVC的設計者原本就預留了擴充套件點給我們,如果需要擴充套件資料繫結相關的功能,這裡應該是一個不錯的入口,具體做法是:

  1. 實現新的WebDataBinderFactory,當然,最好是繼承這裡的ServletRequestDataBinderFactory
  2. 繼承RequestMappingHandlerAdapter,覆蓋createDataBinderFactory()方法,返回新實現的WebDataBinderFactory例項;
  3. SpringMVC容器中使用新的RequestMappingHandlerAdapter

我們從後往前看:

有多種方式實現第3步,在SpringBoot應用中,比較簡單的是通過向容器註冊一個WebMvcRegistrations的實現類,這個介面定義如下:

public interface WebMvcRegistrations {

    default RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return null;
    }

    default RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
        return null;
    }

    default ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver() {
        return null;
    }
}

實現第二個方法就可以。

第2步更簡單,上面已經說明,這裡就不贅述了。

再看第1步,檢視ServletRequestDataBinderFactory的原始碼:

public class ServletRequestDataBinderFactory extends InitBinderDataBinderFactory {

    public ServletRequestDataBinderFactory(@Nullable List<InvocableHandlerMethod> binderMethods,
            @Nullable WebBindingInitializer initializer) {
        super(binderMethods, initializer);
    }

    @Override
    protected ServletRequestDataBinder createBinderInstance(
            @Nullable Object target, String objectName, NativeWebRequest request) throws Exception  {

        return new ExtendedServletRequestDataBinder(target, objectName);
    }
}

除了建構函式,只定義了一個createBinderInstance()方法(一個工廠類建立一種例項,很熟悉的味道吧?),返回ExtendedServletRequestDataBinder的例項,真正的繫結邏輯在這個類裡面,還需要擴充套件這個類:

public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder {

    public ExtendedServletRequestDataBinder(@Nullable Object target) {
        super(target);
    }

    public ExtendedServletRequestDataBinder(@Nullable Object target, String objectName) {
        super(target, objectName);
    }

    @Override
    @SuppressWarnings("unchecked")
    protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
        String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
        Map<String, String> uriVars = (Map<String, String>) request.getAttribute(attr);
        if (uriVars != null) {
            uriVars.forEach((name, value) -> {
                if (mpvs.contains(name)) {
                    if (logger.isWarnEnabled()) {
                        logger.warn("Skipping URI variable '" + name +
                                "' because request contains bind value with same name.");
                    }
                }
                else {
                    mpvs.addPropertyValue(name, value);
                }
            });
        }
    }
}

要擴充套件一個類,首先還是找一下有哪些protected方法,可以看到有一個addBindValues()方法,然後再看這個方法被誰呼叫了,發現在父類ServletRequestDataBinder中有:

public void bind(ServletRequest request) {
    MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
    MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
    if (multipartRequest != null) {
        bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
    }
    // 繫結前新增繫結引數
    addBindValues(mpvs, request);
    // 執行引數繫結,包括引數格式化、引數校驗等
    doBind(mpvs);
    // 可以新增一些繫結之後的處理
}

至此,已經找到擴充套件接入點了,為了更好的對擴充套件開放,引入一個新的介面PropertyValuesProvider

/**
 * 屬性值提供器介面
 */
public interface PropertyValuesProvider {

    /**
     * 繫結前新增繫結屬性,仍然需要經過引數校驗
     */
    default void addBindValues(MutablePropertyValues mpvs, ServletRequest request, Object target, String name) {
    }

    /**
     * 繫結後修改目標物件,修改後的引數不需要經過引數校驗
     *
     */
    default void afterBindValues(PropertyAccessor accessor, ServletRequest request, Object target, String name) {
    }
}

然後實現新的DataBinder,整個程式碼如下:

class ArgsBindRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {

    private final List<PropertyValuesProvider> providers;

    public ArgsBindRequestMappingHandlerAdapter(List<PropertyValuesProvider> providers) {
        this.providers = providers;
    }

    @Override
    protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods) throws Exception {
        return new ArgsBindServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
    }

    private class ArgsBindServletRequestDataBinderFactory extends ServletRequestDataBinderFactory {

        public ArgsBindServletRequestDataBinderFactory(List<InvocableHandlerMethod> binderMethods, WebBindingInitializer initializer) {
            super(binderMethods, initializer);
        }

        @Override
        protected ServletRequestDataBinder createBinderInstance(Object target, String objectName, NativeWebRequest request) {
            return new ArgsBindServletRequestDataBinder(target, objectName);
        }
    }

    private class ArgsBindServletRequestDataBinder extends ExtendedServletRequestDataBinder {

        public ArgsBindServletRequestDataBinder(Object target, String objectName) {
            super(target, objectName);
        }

        /**
         * 屬性繫結前
         */
        @Override
        protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
            super.addBindValues(mpvs, request);
            if (null != providers) {
                Object target = getTarget();
                String name = getObjectName();
                providers.forEach(provider -> provider.addBindValues(mpvs, request, target, name));
            }
        }

        /**
         * 屬性繫結後
         */
        @Override
        public void bind(ServletRequest request) {
            super.bind(request);
            if (null != providers) {
                ConfigurablePropertyAccessor mpvs = getPropertyAccessor();
                Object target = getTarget();
                String name = getObjectName();
                providers.forEach(provider -> provider.afterBindValues(mpvs, request, target, name));
            }
        }
    }
}

最後,加上SpringBoot自動配置類:

@Configuration
public class ArgsBindAutoConfiguration {

    @Bean
    @ConditionalOnBean(PropertyValuesProvider.class)
    @ConditionalOnMissingBean(ArgsBindWebMvcRegistrations.class)
    public ArgsBindWebMvcRegistrations argsBindWebMvcRegistrations(List<PropertyValuesProvider> providers) {
        return new ArgsBindWebMvcRegistrations(providers);
    }

    static class ArgsBindWebMvcRegistrations implements WebMvcRegistrations {

        private final List<PropertyValuesProvider> providers;

        public ArgsBindWebMvcRegistrations(List<PropertyValuesProvider> providers) {
            this.providers = providers;
        }

        @Override
        public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
            return new ArgsBindRequestMappingHandlerAdapter(providers);
        }
    }
}

好了,有了新的介面,要實現文章開始的獲取使用者資訊的問題,也就是新增一個新介面PropertyValuesProvider的實現類,並注入到SpringMVC的容器中即可,如果需要獲取PAYLOAD中的其它資訊,或者有其它的自定義引數繫結邏輯,可以再加幾個實現類。

在我的Github上有一個簡單的測試示例,有興趣的朋友不妨一試