從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上有一個簡單的測試示例,有興趣的朋友不妨一試。


免責聲明!

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



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