Spring MVC 实战:三种方式获取登录用户信息

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 前言Web 项目中,维持用户登录状态的常用方式有三种,分别是 Cookie、Session、Token,不管哪种方案,都需要获取到用户信息供业务层使用。

前言


Web 项目中,维持用户登录状态的常用方式有三种,分别是 Cookie、Session、Token,不管哪种方案,都需要获取到用户信息供业务层使用。

Web 项目中,维持用户登录状态的常用方式有三种,分别是 Cookie、Session、Token,不管哪种方案,都需要获取到用户信息供业务层使用。


由于获取用户信息与具体业务无关,因此在普通的 Java Web 项目中常用 Filter 拦截请求,获取到用户信息后存至 ThreadLocal 供业务层获取。


Spring MVC 项目中,Filter 的概念逐渐淡出大家的视野,通常改用 HandlerInterceptor 替代 Filter 拦截请求获取用户信息并存至 ThreadLocal 供业务层获取。


我们知道 Spring MVC 支持直接注入 HttpServletRequest 和不同的处理器方法参数,那么能不能注入表示用户信息的类型以及将这个用户类作为处理器方法参数呢?如果能够实现这个目的,我们获取用户信息的方式将进一步简化和灵活。本篇将尝试实现这两个功能。


实战


入门:拦截器获取登录用户信息

不管业务层采用哪种方式获取用户信息,都需要先解析出用户信息,Spring MVC 中常见的做法是使用拦截器解析,然后存至 ThreadLocal 中供业务层使用。拦截器解析用户信息是后面两种实现方式的基础,因此先看下拦截器如何实现。


假定登录用户信息使用 User 类表示。

.

@Data
@Accessors(chain = true)
public class User {
    private Long id;
    private String name;
}


从 session 或 token 中解析用户信息后存至如下的 UserHolder 类中的 ThreadLocal 中。不同的线程中 ThreadLocal 存放的对象是独立的,因此不会出现线程安全问题。


public class UserHolder {
    private static final ThreadLocal<User> HOLDER = new ThreadLocal<>();
    public static void set(User user) {
        HOLDER.set(user);
    }
    public static User get() {
        return HOLDER.get();
    }
    public static void remove() {
        HOLDER.remove();
    }
}


拦截器可以如下定义。


public class LoginHandlerInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        UserHolder.set(new User().setId(1L).setName("zhangsan"));
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.remove();
    }
}

拦截器在处理器方法执行前解析用户信息为 User 实例,并存至 UserHodler 类中的 ThreadLocal 中,待处理器方法执行后移除。


再把拦截器添加到 Spring MVC 中就可以了。了解更多 Spring MVC 拦截器知识,可参考我我前面文章《Spring MVC 系列之拦截器 Interceptor 最全总结》。


@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Bean
    public LoginHandlerInterceptor loginHandlerInterceptor() {
        return new LoginHandlerInterceptor();
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginHandlerInterceptor()).addPathPatterns("/**");
    }
}


最后,我们写一个测试接口。


@RestController
public class UserController {
    @GetMapping("/user")
    public User getUser() {
        return UserHolder.get();
    }
}


接口调用返回内容如下。


{
    "id": 1,
    "name": "zhangsan"
}


处理器方法中成功获取到了拦截器中存放的用户信息。


进阶:处理器方法参数获取登录用户信息


Spring MVC 支持定义不同的处理器方法参数获取请求头、请求体等信息,我们希望 Spring MVC 将我们定义的 User 类作为处理器方法参数,以获取登录用户信息。


@RestController
public class UserController {
    @GetMapping("/user")
    public User getUser(User user) {
        return user;
    }   
}


想要实现这样的功能,我们可以参考 Spring MVC 默认支持参数的实现。Spring MVC 处理器方法调用时,使用的是 HandlerMethodArgumentResolver 接口解析方法参数值的,看下这个接口的定义。


public interface HandlerMethodArgumentResolver {
  // 是否支持方法参数
  boolean supportsParameter(MethodParameter parameter);
  // 解析方法参数
  @Nullable
  Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
               NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}


如果给定的处理器方法参数解析器支持方法参数,那么就使用这个处理器方法参数解析器解析方法参数,默认的处理器方法参数解析器及能够解析的方法参数如下图所示。


28.png


由于默认的处理器方法参数解析器会处理不带注解的类型,因此我们需要自定义一个注解作为解析 User 类型的标识,这里定义的是 @Authorization 注解。


@Documented
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Authorization {
}


如下是我们自定义的解析 User 类型的 UserHandlerMethodArgumentResolver。


public class UserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(Authorization.class) && User.class == parameter.getParameterType();
    }
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        return UserHolder.get();
    }
}


如果处理器方法参数上存在 @Authorization 注解并且类型为 User,则使用我们定义的解析器从前面定义的 UserHodler 类获取 User 类实例。


Spring MVC 预留了添加自定义处理器参数解析器的功能,实现接口 WebMvcConfigurer 并重写 #addArgumentResolvers 方法即可。


@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new UserHandlerMethodArgumentResolver());
    }
}


最后定义一个示例接口。


@RestController
public class UserController {
    @GetMapping("/user")
    public User getUser(@Authorization User user) {
        return user;
    }
}


调用后成功返回内容如下。


{
    "id": 1,
    "name": "zhangsan"
}


使用处理器方法参数成功获取到了拦截器中存储的 User 信息。


高手:@Autowired 注入获取登录用户信息

入 HttpServletRequest,我们尝试参考下这个实现,支持注入我们定义的 User 类型。我们知道,Spring MVC 除了支持 HttpServletRequest 作为请求参数还支持直接注

vletRequest 注入原理分析HttpSer


查阅 Spring MVC 源码可以知道,Spring 会在 BeanFactoryPostProcessor 回调前注册 Web 环境相关的依赖,代码如下。


29.png


注意,Spring MVC 调用的是 ConfigurableListableBeanFactory#registerResolvableDependency 方法,这个方法向 Spring 容器中注册的并不是一个 bean,从方法名字可以看出这是一个 Spring 可解析的依赖,这个依赖在 Spring 中属于游离对象,Spring 不会管理游离对象的生命周期。通过 @Autowired 注入依赖时 Spring 会优先从游离的对象中查找。


看下 Spring MVC 为 ServletRequest 注册的 RequestObjectFactory 依赖。


  private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable {
    @Override
    public ServletRequest getObject() {
      return currentRequestAttributes().getRequest();
    }
    @Overrid
    public String toString() {
      return "Current HttpServletRequest";
    }
  }


这是一个 ObjectFactory 接口的一个实现,每次获取的都是当前请求的 HttpServletRequest 实例。Spring 在注入依赖时,如果发现游离对象类型为 ObjectFactory 就会调用 #getObject 方法获取真正的依赖。更多 Spring 依赖解析内容,可参考《浅析 Spring 依赖解析实现》。Spring 从游离对象解析依赖的相关代码如下。


image.png


Spring 发现游离对象中存在给定的类型,就将游离对象添加到候选列表。上图中还调用了 AutowireUtils.resolveAutowiringValue 方法,这个方法正是用来处理类型为 ObjectFactory 的游离对象的,看下代码。


image.png


如果游离对象类型为 ObjectFactory 并且需要的类型和游离对象类型不一致,会再次处理。如果游离对象类型实现了 Serializable 接口并且需要的类型是一个接口,Spring 会创建一个代理对象,否则直接将 ObjectFactory#getObject 的结果作为依赖注入。


由于 RequestObjectFactory 实现了接口 Serializable 并且需要的类型 HttpServletRequest 是一个接口,因此 Spring 会创建一个代理对象作为依赖,代理对象的实现如下。


31.png


每次方法调用时仅仅使用 ObjectFactory 获取最新的对象,然后使用这个对象调用方法,由于 RequestObjectFactory 每次获取的都是当前请求的 HttpServletRequest,因此调用注入的 HttpServletRequest 的方法不会出现线程安全问题。


@Autowired 注入 User 实现方式一

参照 Spring MVC 对 HttpServletRequest 的实现,由于 Spring MVC 只会为接口创建代理对象,我们需要定义一个 IUser 接口。


public interface IUser {
    Long getId();
    String getName();
}
@Data
@Accessors(chain = true)
public class User implements IUser {
    private Long id;
    private String name;
}


为 IUser 定义的 ObjectFactory 如下,注意这个类实现了 Serializable 接口。


public class UserObjectFactory implements ObjectFactory<IUser>, Serializable {
    @Override
    public IUser getObject() throws BeansException {
        return UserHolder.get();
    }
}


还需要解决的一个问题是如何把这个 UserObjectFactory 注册到 Spring 中。


Spring MVC 环境下的 ApplicationContext 重写了 AbstractApplicationContext#postProcessBeanFactory 方法,刷新应用上下文时,在 BeanFactoryPostProcessor#postProcessBeanFactory 方法被回调前注册了游离对象,代码如下。


11111.png


我们没办法修改 Spring 框架的代码,不过 Spring MVC 环境下,在应用上下文刷新前会回调 ApplicationContextInitializer 接口,看下这个接口的定义。


public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
  void initialize(C applicationContext);
}


在这个接口的方法中,我们可以拿到上下文 ConfigurableApplicationContext,从而为 IUser 类型注册依赖,代码如下。


public class UserRegisterInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        applicationContext.getBeanFactory().registerResolvableDependency(IUser.class, new UserObjectFactory());
    }
}


普通的 Spring MVC 项目,可以为 DispatchServlet 配置一个 contextInitializerClasses 参数或为 ServletContext 配置一个 globalInitializerClasses 参数指定 ApplicationContextInitializer,示例代码如下。


<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
          http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <!--全局 ApplicationContextInitializer-->
    <context-param>
        <param-name>globalInitializerClasses</param-name>
        <param-value>com.zzuhkp.mvc.UserRegisterInitializer</param-value>
    </context-param>
    <!--DispatcherServlet ApplicationContextInitializer-->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextInitializerClasses</param-name>
            <param-value>com.zzuhkp.mvc.UserRegisterInitializer</param-value>
        </init-param>
    </servlet>
</web-app>


Spring Boot 环境下的 Spring MVC 项目,可以在类路径下的 META-INF/spring.factories 文件指定 ApplicationContextInitializer。这里我们使用的是 Spring Boot,文件内容如下。


org.springframework.context.ApplicationContextInitializer=com.zzuhkp.springboot.demo.config.UserRegisterInitializer


示例接口如下。

@RestController
public class UserController {
    @Autowired
    private IUser user;
    @GetMapping("/user")
    public IUser getUser() {
        return user;
    }
}


调用后返回内容如下:

{
    "name": "zhangsan",
    "id": 1
}


成功将与请求有关的 IUser 类型进行了注入。


@Autowired 注入 User 实现方式二

上述代码中,我们注入的类型是 IUser 接口,如果注入它的实现 User 由于 Spring 无法找到依赖对象则会报错,那如何注入一个 User 对象呢?


参考 Spring MVC 自身的实现,本质上它是为给定的类型创建了一个代理对象,当代理对象的方法调用时,改用最新的真实对象调用方法。因此,我们直接创建一个 User 类型的代理类作为游离对象就可以了。


修改 UserRegisterInitializer 内容如下。


public class UserRegisterInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(User.class);
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                return method.invoke(UserHolder.get(), args);
            }
        });
        User user = (User) enhancer.create();
        applicationContext.getBeanFactory().registerResolvableDependency(User.class, user);
    }
}


当代理对象方法调用时,调用拦截器中存放至 UserHolder 中的 User 对象的方法。

最后修改测试接口如下。


@RestController
public class UserController {
    @Autowired
    private User user;
    @GetMapping("/user")
    public User getUser() {
        return user;
    }
}


调用响应内容如下。


{
    "id": 1,
    "name": "zhangsan"
}


成功获取到了登录用户信息。


总结

本文主要介绍了三种获取登录用户信息的方式,包括从拦截器静态方法直接获取、处理器方法参数获取以及直接注入。这三种方式层层递进,都需要对 Spring 较为熟练的掌握才能实现。不得不说,熟练掌握 Spring 确实能简化我们的开发工作,最后欢迎留言交流。

目录
相关文章
SpringMVC入门到实战------5、域对象共享数据 Request、Session、Application、Model、ModelAndView、Map、ModelMap的详细使用及代码实例
这篇文章详细解释了在IntelliJ IDEA中如何使用Mute Breakpoints功能来快速跳过程序中的后续断点,并展示了如何一键清空所有设置的断点。
SpringMVC入门到实战------5、域对象共享数据 Request、Session、Application、Model、ModelAndView、Map、ModelMap的详细使用及代码实例
|
6天前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
SpringMVC入门到实战------ 十一 拦截器的使用
这篇文章介绍了SpringMVC中拦截器的使用,包括拦截器的配置、拦截器的三个抽象方法`preHandle`、`postHandle`和`afterCompletion`的作用,以及多个拦截器的执行顺序和规则。
SpringMVC入门到实战------ 十一 拦截器的使用
|
1月前
|
JSON 前端开发 JavaScript
SpringMVC入门到实战------九 HttpMessageConverter @RequestBody 、@ResponseBody 、RequestEntity、ResponseEntity
这篇文章详细介绍了SpringMVC中的`HttpMessageConverter`接口及其相关的`@RequestBody`、`@ResponseBody`、`RequestEntity`和`ResponseEntity`注解和类型的使用,包括如何将请求体转换为Java对象、如何将Java对象转换为响应体、以及如何处理JSON和AJAX请求。
SpringMVC入门到实战------九 HttpMessageConverter @RequestBody 、@ResponseBody 、RequestEntity、ResponseEntity
|
30天前
|
Java 数据库连接 Spring
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】
文章是关于Spring、SpringMVC、Mybatis三个后端框架的超详细入门教程,包括基础知识讲解、代码案例及SSM框架整合的实战应用,旨在帮助读者全面理解并掌握这些框架的使用。
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】
|
1月前
|
NoSQL Java Redis
Redis6入门到实战------ 八、Redis与Spring Boot整合
这篇文章详细介绍了如何在Spring Boot项目中整合Redis,包括在`pom.xml`中添加依赖、配置`application.properties`文件、创建配置类以及编写测试类来验证Redis的连接和基本操作。
Redis6入门到实战------ 八、Redis与Spring Boot整合
|
1月前
|
缓存 Java 应用服务中间件
SpringMVC入门到实战------七、SpringMVC创建JSP页面的详细过程+配置模板+实现页面跳转+配置Tomcat。JSP和HTML配置模板的差异对比(二)
这篇文章详细介绍了在SpringMVC中创建JSP页面的全过程,包括项目的创建、配置、Tomcat的设置,以及如何实现页面跳转和配置模板解析器,最后还对比了JSP和HTML模板解析的差异。
SpringMVC入门到实战------七、SpringMVC创建JSP页面的详细过程+配置模板+实现页面跳转+配置Tomcat。JSP和HTML配置模板的差异对比(二)
|
1月前
|
XML JSON 数据库
SpringMVC入门到实战------七、RESTful的详细介绍和使用 具体代码案例分析(一)
这篇文章详细介绍了RESTful的概念、实现方式,以及如何在SpringMVC中使用HiddenHttpMethodFilter来处理PUT和DELETE请求,并通过具体代码案例分析了RESTful的使用。
SpringMVC入门到实战------七、RESTful的详细介绍和使用 具体代码案例分析(一)
|
1月前
|
前端开发
SpringMVC入门到实战------六、SpringMVC的视图。ThymeleafView、转发视图、重定向视图、视图控制器的使用详解
这篇文章详细介绍了SpringMVC中的视图类型,包括ThymeleafView、转发视图、重定向视图和视图控制器的使用,以及如何通过源码查看确定使用的视图渲染器类型。
SpringMVC入门到实战------六、SpringMVC的视图。ThymeleafView、转发视图、重定向视图、视图控制器的使用详解
|
22天前
|
Java API UED
【实战秘籍】Spring Boot开发者的福音:掌握网络防抖动,告别无效请求,提升用户体验!
【8月更文挑战第29天】网络防抖动技术能有效处理频繁触发的事件或请求,避免资源浪费,提升系统响应速度与用户体验。本文介绍如何在Spring Boot中实现防抖动,并提供代码示例。通过使用ScheduledExecutorService,可轻松实现延迟执行功能,确保仅在用户停止输入后才触发操作,大幅减少服务器负载。此外,还可利用`@Async`注解简化异步处理逻辑。防抖动是优化应用性能的关键策略,有助于打造高效稳定的软件系统。
31 2