前言:一个痛点
想象一下这样的场景:用户请求带着 JWT Token 进入你的系统,Filter 层面解析 Token 得到用户 ID,接下来需要:
- 在 Controller 层获取用户信息
- 在 Service 层进行权限验证
- 在某些业务逻辑中记录操作日志
每一个环节都需要知道"当前用户是谁",看看目前常用的解决方案。
传统方案的"缺陷"
方案一:ThreadLocal
1// 看起来很"Hack" 2private static final ThreadLocal<Long> currentUser = new ThreadLocal<>(); 3
- 线程安全问题:在异步环境下可能出现数据错乱
- 内存泄漏风险:ThreadLocal 使用不当可能导致内存泄漏
- 代码可读性差:隐式的数据传递方式让代码逻辑变得难以追踪
方案二:HttpServletRequest.setAttribute()
1// 看起来很"丑" 2@GetMapping("/me") 3public User getMyProfile(HttpServletRequest request) { 4 Long userId = (Long) request.getAttribute("currentUserId"); 5 return userService.getById(userId); 6} 7
- 类型不安全:需要手动进行类型转换,容易出现 ClassCastException
- 代码冗余:每个需要用户信息的 Controller 都要重复相同的逻辑
- 违反了 Controller 的"纯净性":引入了 Servlet API 依赖
- 字符串魔法值:属性名称容易写错,编译时无法检查
Spring MVC:HandlerMethodArgumentResolver
Spring MVC 提供了一个解决方案:自定义参数解析器(HandlerMethodArgumentResolver)。
这个设计模式体现了 Spring 框架一贯的"约定优于配置"的理念。它不要求我们改变 Filter 层面的实现,而是在参数解析这个环节做文章,通过扩展框架的能力来解决问题。
设计思路
Spring MVC 的 HandlerMethodArgumentResolver 机制实际上是一种"适配器模式"的应用。它将不同来源的参数(Request 参数、Path 变量、Header 信息、Session 数据等)统一适配成 Controller 方法可以直接使用的形式。
这种设计的巧妙之处在于:
- 职责分离:Filter 负责认证和设置状态,Resolver 负责参数转换
- 可扩展性:可以轻松添加新的参数解析逻辑
- 无侵入性:不影响现有的代码结构
这个方案的核心思想是:将 request.getAttribute() 操作,封装成类型安全的方法参数。
核心实现
第一步:创建自定义注解
1@Target(ElementType.PARAMETER) 2@Retention(RetentionPolicy.RUNTIME) 3public @interface CurrentUser { 4} 5
第二步:实现参数解析器
1@Component 2public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver { 3 4 @Override 5 public boolean supportsParameter(MethodParameter parameter) { 6 // 只解析被 @CurrentUser 标记的参数 7 return parameter.hasParameterAnnotation(CurrentUser.class); 8 } 9 10 @Override 11 public Object resolveArgument(MethodParameter parameter, 12 ModelAndViewContainer mavContainer, 13 NativeWebRequest webRequest, 14 WebDataBinderFactory binderFactory) throws Exception { 15 // 从 request 中获取之前设置的用户ID 16 return webRequest.getAttribute("currentUserId", WebRequest.SCOPE_REQUEST); 17 } 18} 19
第三步:注册解析器
1@Configuration 2public class WebMvcConfig implements WebMvcConfigurer { 3 4 @Autowired 5 private CurrentUserArgumentResolver currentUserArgumentResolver; 6 7 @Override 8 public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { 9 resolvers.add(currentUserArgumentResolver); 10 } 11} 12
方案示例
Filter 层面
1@Component 2public class JwtAuthenticationFilter extends OncePerRequestFilter { 3 4 @Autowired 5 private JwtService jwtService; 6 7 @Override 8 protected void doFilterInternal(HttpServletRequest request, 9 HttpServletResponse response, 10 FilterChain filterChain) throws ServletException, IOException { 11 12 String authorizationHeader = request.getHeader("Authorization"); 13 14 if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { 15 String token = authorizationHeader.substring(7); 16 try { 17 Long userId = jwtService.extractUserId(token); 18 // 标准:使用 request.setAttribute 设置 19 request.setAttribute("currentUserId", userId); 20 } catch (Exception e) { 21 // token 无效,继续执行后续逻辑 22 } 23 } 24 25 filterChain.doFilter(request, response); 26 } 27} 28
Controller 层面
1@RestController 2@RequestMapping("/api/users") 3public class UserController { 4 5 @Autowired 6 private UserService userService; 7 8 @GetMapping("/me") 9 public ResponseEntity<User> getCurrentUser(@CurrentUser Long userId) { 10 // userId 被"魔法般"地自动注入了! 11 User user = userService.findById(userId); 12 return ResponseEntity.ok(user); 13 } 14 15 @PutMapping("/me") 16 public ResponseEntity<User> updateCurrentUser(@CurrentUser Long userId, 17 @RequestBody UserUpdateRequest request) { 18 // 每个 Controller 方法都可以直接使用 @CurrentUser 19 User updatedUser = userService.updateUser(userId, request); 20 return ResponseEntity.ok(updatedUser); 21 } 22 23 @GetMapping("/permissions") 24 public ResponseEntity<List<Permission>> getUserPermissions(@CurrentUser Long userId) { 25 // 完全类型安全,无需手动类型转换 26 List<Permission> permissions = userService.getUserPermissions(userId); 27 return ResponseEntity.ok(permissions); 28 } 29} 30
原理解析
HandlerMethodArgumentResolver 工作流程
1graph TD 2 A[HTTP请求] --> B[Filter链] 3 B --> C[DispatcherServlet] 4 C --> D[HandlerMapping] 5 D --> E[HandlerAdapter] 6 E --> F[HandlerMethodArgumentResolver] 7 F --> G{supportsParameter?} 8 G -->|Yes| H[resolveArgument] 9 G -->|No| I[使用默认解析器] 10 H --> J[参数注入] 11 I --> J 12 J --> K[Controller方法执行] 13
在这个流程中,Spring MVC 会按照注册的顺序遍历所有的 HandlerMethodArgumentResolver,对于每个需要解析的参数,都会调用 supportsParameter() 方法判断是否支持,如果支持则调用 resolveArgument() 方法进行实际的参数解析。
方案优势
这个方案的优势不仅体现在代码层面,更重要的是它符合软件工程的多个重要原则:
1. 类型安全:编译时检查,避免运行时类型转换错误。当你错误地将 @CurrentUser Long 写成 @CurrentUser String 时,编译器会立刻提醒你。
2. 代码简洁:Controller 方法专注于业务逻辑。不再需要每次都写 request.getAttribute() 的样板代码,让业务逻辑更加清晰。
3. 可测试性:Mock 变得简单直接。在单元测试中,你只需要模拟参数值,而不需要构建整个 HttpServletRequest 对象。
4. 可维护性:统一的用户信息获取方式。当需要修改用户信息的获取逻辑时,只需要修改 Resolver,而不需要修改每个 Controller 方法。
5. 扩展性:轻松支持更多用户相关属性。通过修改 Resolver 的逻辑,可以支持返回 User 对象、用户权限、用户偏好设置等复杂信息。
6. 关注点分离:Filter 专注于认证逻辑,Resolver 专注于参数解析,Controller 专注于业务逻辑,各司其职,代码结构更加清晰。
进阶用法:传递完整用户对象
在真实的项目中,我们往往需要的不仅仅是用户 ID,而是完整的用户信息、权限数据、或者用户偏好设置。HandlerMethodArgumentResolver 的强大之处就在于它可以智能地根据参数类型返回不同的对象。
扩展解析器支持复杂对象
这里的核心思想是根据 Controller 方法的参数类型动态决定返回什么对象,这样可以最大程度地提高代码的灵活性和复用性。
1@Component 2public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver { 3 4 @Autowired 5 private UserService userService; 6 7 @Override 8 public boolean supportsParameter(MethodParameter parameter) { 9 return parameter.hasParameterAnnotation(CurrentUser.class); 10 } 11 12 @Override 13 public Object resolveArgument(MethodParameter parameter, 14 ModelAndViewContainer mavContainer, 15 NativeWebRequest webRequest, 16 WebDataBinderFactory binderFactory) throws Exception { 17 18 Long userId = (Long) webRequest.getAttribute("currentUserId", WebRequest.SCOPE_REQUEST); 19 20 if (userId == null) { 21 return null; // 或者抛出异常 22 } 23 24 // 根据参数类型决定返回什么 25 Class<?> parameterType = parameter.getParameterType(); 26 27 if (parameterType == Long.class || parameterType == long.class) { 28 return userId; 29 } else if (parameterType == User.class) { 30 return userService.findById(userId); 31 } else if (parameterType == UserProfile.class) { 32 return userService.getUserProfile(userId); 33 } 34 35 throw new IllegalArgumentException("Unsupported parameter type: " + parameterType); 36 } 37} 38
Controller 中的多种用法
1@RestController 2@RequestMapping("/api") 3public class AdvancedUserController { 4 5 @GetMapping("/user/id") 6 public ResponseEntity<String> getUserId(@CurrentUser Long userId) { 7 return ResponseEntity.ok("User ID: " + userId); 8 } 9 10 @GetMapping("/user/info") 11 public ResponseEntity<User> getUserInfo(@CurrentUser User user) { 12 return ResponseEntity.ok(user); 13 } 14 15 @GetMapping("/user/profile") 16 public ResponseEntity<UserProfile> getUserProfile(@CurrentUser UserProfile profile) { 17 return ResponseEntity.ok(profile); 18 } 19} 20
实战技巧与注意事项
在实际项目中使用 HandlerMethodArgumentResolver 时,还需要考虑一些实际的工程问题。下面是一些常见的场景和解决方案。
1. 异常处理
在用户未登录或者 Token 无效的情况下,我们需要优雅地处理异常情况,而不是让系统抛出难以理解的错误信息。
1@Component 2public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver { 3 4 @Override 5 public Object resolveArgument(...) throws Exception { 6 Long userId = (Long) webRequest.getAttribute("currentUserId", WebRequest.SCOPE_REQUEST); 7 8 if (userId == null) { 9 throw new UnauthorizedException("用户未登录"); 10 } 11 12 return userId; 13 } 14} 15 16@ControllerAdvice 17public class GlobalExceptionHandler { 18 19 @ExceptionHandler(UnauthorizedException.class) 20 public ResponseEntity<String> handleUnauthorized(UnauthorizedException e) { 21 return ResponseEntity.status(401).body(e.getMessage()); 22 } 23} 24
2. 与 Spring Security 集成
在许多企业级应用中,我们使用 Spring Security 进行认证和授权。如何将 Spring Security 的用户信息与我们的自定义 Resolver 结合使用是一个常见问题。
Spring Security 提供了 SecurityContextHolder 来存储当前用户的认证信息,我们可以直接从中获取用户详情,然后转换成我们需要的格式。这种集成方式的优势是可以复用 Spring Security 的完整认证体系,包括各种认证方式(JWT、OAuth2、Session 等)。
1@Component 2public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver { 3 4 @Override 5 public Object resolveArgument(...) throws Exception { 6 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 7 8 if (authentication != null && authentication.isAuthenticated()) { 9 UserDetails userDetails = (UserDetails) authentication.getPrincipal(); 10 return Long.parseLong(userDetails.getUsername()); // 假设username存储的是userId 11 } 12 13 return null; 14 } 15} 16
3. 多租户场景
在 SaaS 应用中,多租户是一个常见的需求。除了当前用户信息,我们还需要知道当前租户的信息。通过创建多个自定义注解和对应的 Resolver,我们可以轻松实现这种需求。
1@Target(ElementType.PARAMETER) 2@Retention(RetentionPolicy.RUNTIME) 3public @interface CurrentTenant { 4} 5 6@Component 7public class CurrentTenantArgumentResolver implements HandlerMethodArgumentResolver { 8 9 @Override 10 public boolean supportsParameter(MethodParameter parameter) { 11 return parameter.hasParameterAnnotation(CurrentTenant.class); 12 } 13 14 @Override 15 public Object resolveArgument(...) throws Exception { 16 return webRequest.getAttribute("currentTenantId", WebRequest.SCOPE_REQUEST); 17 } 18} 19 20@GetMapping("/tenant/data") 21public ResponseEntity<List<Data>> getTenantData(@CurrentUser Long userId, 22 @CurrentTenant Long tenantId) { 23 // 同时获取当前用户和租户信息 24 List<Data> data = dataService.findByUserAndTenant(userId, tenantId); 25 return ResponseEntity.ok(data); 26} 27
总结
通过 Spring MVC 的 HandlerMethodArgumentResolver我们实现了一个可以"跨 Filter 与 Controller 传参"的技术实现方案。将底层 Servlet API 的 request.getAttribute() 操作抽象为编译时类型安全的方法参数注入,实现了框架层面的参数解析适配,既保持了架构的纯净性,又提供了强大的扩展能力。
《SpringBoot实现隐式参数注入》 是转载文章,点击查看原文。

