前言
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; }
如果给定的处理器方法参数解析器支持方法参数,那么就使用这个处理器方法参数解析器解析方法参数,默认的处理器方法参数解析器及能够解析的方法参数如下图所示。
由于默认的处理器方法参数解析器会处理不带注解的类型,因此我们需要自定义一个注解作为解析 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 环境相关的依赖,代码如下。
注意,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 从游离对象解析依赖的相关代码如下。
Spring 发现游离对象中存在给定的类型,就将游离对象添加到候选列表。上图中还调用了 AutowireUtils.resolveAutowiringValue
方法,这个方法正是用来处理类型为 ObjectFactory
的游离对象的,看下代码。
如果游离对象类型为 ObjectFactory 并且需要的类型和游离对象类型不一致,会再次处理。如果游离对象类型实现了 Serializable 接口并且需要的类型是一个接口,Spring 会创建一个代理对象,否则直接将 ObjectFactory#getObject 的结果作为依赖注入。
由于 RequestObjectFactory 实现了接口 Serializable 并且需要的类型 HttpServletRequest 是一个接口,因此 Spring 会创建一个代理对象作为依赖,代理对象的实现如下。
每次方法调用时仅仅使用 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 方法被回调前注册了游离对象,代码如下。
我们没办法修改 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 确实能简化我们的开发工作,最后欢迎留言交流。