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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 前言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 确实能简化我们的开发工作,最后欢迎留言交流。

目录
相关文章
|
8天前
|
JSON 前端开发 Java
【SpringMVC】基础入门实战(3)
SpringMVC获取Header,返回静态页面,返回数据(Controller),返回数据@ResponseBody,返回HTML代码片段,返回JSON,设置状态码,设置Header
|
16天前
|
设计模式 前端开发 Java
步步深入SpringMvc DispatcherServlet源码掌握springmvc全流程原理
通过对 `DispatcherServlet`源码的深入剖析,我们了解了SpringMVC请求处理的全流程。`DispatcherServlet`作为前端控制器,负责请求的接收和分发,处理器映射和适配负责将请求分派到具体的处理器方法,视图解析器负责生成和渲染视图。理解这些核心组件及其交互原理,有助于开发者更好地使用和扩展SpringMVC框架。
27 4
|
25天前
|
Java 数据库 数据安全/隐私保护
轻松掌握Spring依赖注入:打造你的登录验证系统
本文以轻松活泼的风格,带领读者走进Spring框架中的依赖注入和登录验证的世界。通过详细的步骤和代码示例,我们从DAO层的创建到Service层的实现,再到Spring配置文件的编写,最后通过测试类验证功能,一步步构建了一个简单的登录验证系统。文章不仅提供了实用的技术指导,还以口语化和生动的语言,让学习变得不再枯燥。
39 2
|
2月前
|
安全 Java 数据安全/隐私保护
如何使用Spring Boot进行表单登录身份验证:从基础到实践
如何使用Spring Boot进行表单登录身份验证:从基础到实践
50 5
|
2月前
|
前端开发 Java 开发者
Spring MVC中的请求映射:@RequestMapping注解深度解析
在Spring MVC框架中,`@RequestMapping`注解是实现请求映射的关键,它将HTTP请求映射到相应的处理器方法上。本文将深入探讨`@RequestMapping`注解的工作原理、使用方法以及最佳实践,为开发者提供一份详尽的技术干货。
131 2
|
3月前
|
自然语言处理 Java API
Spring Boot 接入大模型实战:通义千问赋能智能应用快速构建
【10月更文挑战第23天】在人工智能(AI)技术飞速发展的今天,大模型如通义千问(阿里云推出的生成式对话引擎)等已成为推动智能应用创新的重要力量。然而,对于许多开发者而言,如何高效、便捷地接入这些大模型并构建出功能丰富的智能应用仍是一个挑战。
329 6
|
3月前
|
缓存 NoSQL Java
Spring Boot与Redis:整合与实战
【10月更文挑战第15天】本文介绍了如何在Spring Boot项目中整合Redis,通过一个电商商品推荐系统的案例,详细展示了从添加依赖、配置连接信息到创建配置类的具体步骤。实战部分演示了如何利用Redis缓存提高系统响应速度,减少数据库访问压力,从而提升用户体验。
167 2
|
3月前
|
JSON 前端开发 Java
SSM:SpringMVC
本文介绍了SpringMVC的依赖配置、请求参数处理、注解开发、JSON处理、拦截器、文件上传下载以及相关注意事项。首先,需要在`pom.xml`中添加必要的依赖,包括Servlet、JSTL、Spring Web MVC等。接着,在`web.xml`中配置DispatcherServlet,并设置Spring MVC的相关配置,如组件扫描、默认Servlet处理器等。然后,通过`@RequestMapping`等注解处理请求参数,使用`@ResponseBody`返回JSON数据。此外,还介绍了如何创建和配置拦截器、文件上传下载的功能,并强调了JSP文件的放置位置,避免404错误。
|
4月前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
|
3月前
|
Java 数据库连接 Spring
【2021Spring编程实战笔记】Spring开发分享~(下)
【2021Spring编程实战笔记】Spring开发分享~(下)
37 1