您的位置:首页 > 编程语言 > Java开发

从SpringMVC获取用户信息谈起

2019-09-30 10:15 1461 查看
  • Github地址:https://github.com/andyslin/spring-ext
  • 编译、运行环境:JDK 8 + Maven 3 + IDEA + Lombok
  • spring-boot:2.1.0.RELEASE(Spring:5.1.2.RELEASE)
  • 如要本地运行g 20c4 ithub上的项目,需要安装lombok插件

上周末拜读了一位牛人的公众号文章<<Token认证,如何快速方便获取用户信息>>,语言风趣,引人入胜,为了表示涛涛敬仰之情,已经转载到自己的公众号了。

回顾一下文章内容,为了在

Controller
的方法中获取已经认证过的用户信息(比如通过JWT-JSON Web Token传输的
Token
),文中提供了三种方式:

  • 方式一(很挫)直接在
    Controller
    方法中获取
    Token
    头,然后解析;
  • 方式二(优雅)在过滤器
    Filter
    中验证
    JWT
    后,直接使用
    HttpServletRequestWrapper
    偷梁换柱,覆盖
    getHeader
    方法,然后在
    Controller
    方法中调用
    getHeader
    ,这样就不需要再次解析了;
  • 方式三(很优雅)同样在过滤器
    Filter
    中使用
    HttpServletRequestWrapper
    ,只是覆盖
    getParameterNames
    getParameterValues
    (针对表单提交)和
    getInputStream
    (针对JSON提交),然后就可以和客户端参数相同的方式获取了。

方式一需要重复解析

JWT
,而且控制器和
Servlet API
绑定,不方便测试,但是胜在简单直接。方式二和方式三虽然是一个很好的练习
HttpServletRequestWrapper
的示例,但是可能还算不上是优雅的获取用户信息的方式。

不妨思考一下:

  • 除了获取
    userId
    外,如果还想获取JWT中
    PAYLOAD
    的其它信息,能不能做到只修改
    Controller
    ?还是需要再次修改验证
    JWT
    的过滤器
    Filter
    呢?
  • HttpServletRequest
    getInpustStream()
    方法,Web容器实现基本都是只能调用一次的,因而方式三在扩展
    getInpustStream()
    的时候,先将其转换为
    byte[]
    ,然后为了添加用户信息,再将
    byte[]
    反序列化为
    map
    ,添加用户信息之后又序列化为
    byte[]
    ,反复多次,这种方式性能怎么样?如果是文件上传,这种方式能否行得通?
  • 方式三中
    HttpServletRequestWrapper
    会无形中启到屏蔽
    loginUserId
    参数的作用,但如果客户端的的确确传入了一个
    loginUserId
    的参数(当然,这种情况还是需要尽量避免),在
    Controller
    中怎么又获取到客户端的这个参数?

有没有什么其它的方式呢?

SpringMVC
中关于参数绑定有很多接口,其中很关键的一个是
HandlerMethodArgumentResolver
,可以通过添加新实现类来实现获取用户信息吗?当然可以,对应该接口的两个方法,首先要能够识别什么情况下需要绑定用户信息,一般来说,可以根据参数的特殊类型,也可以根据参数的特殊注解;其次要能够获取到用户信息,类似于原文中做的那样。虽然这样做也可以实现功能,但是却很繁琐。

不如抛开怎么获取用户信息不谈,先来看看

SpringMVC
在控制器的处理方法
HandlerMethod
中绑定参数是怎么做的?

熟悉

SpringMVC
处理流程的朋友,自然知道,主控制器是
DispatcherServlet
,在
doDispatch()
方法中根据
HandlerMapping
找到处理器,然后找到可以调用该处理器的
HandlerAdapter
,其中最常用也最核心的莫过于
RequestMappingHandlerMapping
HandlerMethod
RequestMappingHandlerAdapter
组合了。查看
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(creat
576
eInitBinderMethod(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()
    方法,返回新实现的
    WebDataBinderFact
    aec
    ory
    实例;
  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 +
"&
105a
#39; 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自动配置类:

[code]@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上有一个简单的测试示例,有兴趣的朋友不妨一试。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: