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

简介: 前言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 确实能简化我们的开发工作,最后欢迎留言交流。

目录
相关文章
|
2月前
|
负载均衡 监控 Java
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
本文详细介绍了 Spring Cloud Gateway 的核心功能与实践配置。首先讲解了网关模块的创建流程,包括依赖引入(gateway、nacos 服务发现、负载均衡)、端口与服务发现配置,以及路由规则的设置(需注意路径前缀重复与优先级 order)。接着深入解析路由断言,涵盖 After、Before、Path 等 12 种内置断言的参数、作用及配置示例,并说明了自定义断言的实现方法。随后重点阐述过滤器机制,区分路由过滤器(如 AddRequestHeader、RewritePath、RequestRateLimiter 等)与全局过滤器的作用范围与配置方式,提
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
|
3月前
|
监控 Java API
Spring Boot 3.2 结合 Spring Cloud 微服务架构实操指南 现代分布式应用系统构建实战教程
Spring Boot 3.2 + Spring Cloud 2023.0 微服务架构实践摘要 本文基于Spring Boot 3.2.5和Spring Cloud 2023.0.1最新稳定版本,演示现代微服务架构的构建过程。主要内容包括: 技术栈选择:采用Spring Cloud Netflix Eureka 4.1.0作为服务注册中心,Resilience4j 2.1.0替代Hystrix实现熔断机制,配合OpenFeign和Gateway等组件。 核心实操步骤: 搭建Eureka注册中心服务 构建商品
641 3
|
1月前
|
监控 Cloud Native Java
Spring Boot 3.x 微服务架构实战指南
🌟蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕Spring Boot 3.x与微服务架构,探索云原生、性能优化与高可用系统设计。以代码为笔,在二进制星河中谱写极客诗篇。关注我,共赴技术星辰大海!(238字)
Spring Boot 3.x 微服务架构实战指南
|
1月前
|
XML Java 测试技术
《深入理解Spring》:IoC容器核心原理与实战
Spring IoC通过控制反转与依赖注入实现对象间的解耦,由容器统一管理Bean的生命周期与依赖关系。支持XML、注解和Java配置三种方式,结合作用域、条件化配置与循环依赖处理等机制,提升应用的可维护性与可测试性,是现代Java开发的核心基石。
|
1月前
|
前端开发 Java 微服务
《深入理解Spring》:Spring、Spring MVC与Spring Boot的深度解析
Spring Framework是Java生态的基石,提供IoC、AOP等核心功能;Spring MVC基于其构建,实现Web层MVC架构;Spring Boot则通过自动配置和内嵌服务器,极大简化了开发与部署。三者层层演进,Spring Boot并非替代,而是对前者的高效封装与增强,适用于微服务与快速开发,而深入理解Spring Framework有助于更好驾驭整体技术栈。
|
6月前
|
人工智能 Java API
Spring AI 实战|Spring AI入门之DeepSeek调用
本文介绍了Spring AI框架如何帮助Java开发者轻松集成和使用大模型API。文章从Spring AI的初探开始,探讨了其核心能力及应用场景,包括手动与自动发起请求、流式响应实现打字机效果,以及兼容不同AI服务(如DeepSeek、通义千问)的方法。同时,还详细讲解了如何在生产环境中添加监控以优化性能和成本管理。通过Spring AI,开发者可以简化大模型调用流程,降低复杂度,为企业智能应用开发提供强大支持。最后,文章展望了Spring AI在未来AI时代的重要作用,鼓励开发者积极拥抱这一技术变革。
2393 71
Spring AI 实战|Spring AI入门之DeepSeek调用
|
8月前
|
人工智能 搜索推荐 Java
Spring AI与DeepSeek实战三:打造企业知识库
本文基于Spring AI与RAG技术结合,通过构建实时知识库增强大语言模型能力,实现企业级智能搜索场景与个性化推荐,攻克LLM知识滞后与生成幻觉两大核心痛点。
895 7
|
8月前
|
前端开发 Java 测试技术
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
本文介绍了 `@RequestParam` 注解的使用方法及其与 `@PathVariable` 的区别。`@RequestParam` 用于从请求中获取参数值(如 GET 请求的 URL 参数或 POST 请求的表单数据),而 `@PathVariable` 用于从 URL 模板中提取参数。文章通过示例代码详细说明了 `@RequestParam` 的常用属性,如 `required` 和 `defaultValue`,并展示了如何用实体类封装大量表单参数以简化处理流程。最后,结合 Postman 测试工具验证了接口的功能。
461 0
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
|
3月前
|
人工智能 监控 安全
如何快速上手【Spring AOP】?核心应用实战(上篇)
哈喽大家好吖~欢迎来到Spring AOP系列教程的上篇 - 应用篇。在本篇,我们将专注于Spring AOP的实际应用,通过具体的代码示例和场景分析,帮助大家掌握AOP的使用方法和技巧。而在后续的下篇中,我们将深入探讨Spring AOP的实现原理和底层机制。 AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的核心特性之一,它能够帮助我们解决横切关注点(如日志记录、性能统计、安全控制、事务管理等)的问题,提高代码的模块化程度和复用性。