⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 风象南 「风象南」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前言:一个痛点

想象一下这样的场景:用户请求带着 JWT Token 进入你的系统,Filter 层面解析 Token 得到用户 ID,接下来需要:

  • 在 Controller 层获取用户信息
  • 在 Service 层进行权限验证
  • 在某些业务逻辑中记录操作日志

每一个环节都需要知道”当前用户是谁”,看看目前常用的解决方案。

传统方案的”缺陷”

方案一:ThreadLocal

// 看起来很"Hack"
private static final ThreadLocal<Long> currentUser = new ThreadLocal<>();
  • 线程安全问题:在异步环境下可能出现数据错乱
  • 内存泄漏风险:ThreadLocal 使用不当可能导致内存泄漏
  • 代码可读性差:隐式的数据传递方式让代码逻辑变得难以追踪

方案二:HttpServletRequest.setAttribute()

// 看起来很"丑"
@GetMapping("/me")
public User getMyProfile(HttpServletRequest request) {
Long userId = (Long) request.getAttribute("currentUserId");
return userService.getById(userId);
}
  • 类型不安全:需要手动进行类型转换,容易出现 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() 操作,封装成类型安全的方法参数。

核心实现

第一步:创建自定义注解

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

第二步:实现参数解析器

@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {

@Override
public boolean supportsParameter(MethodParameter parameter) {
// 只解析被 @CurrentUser 标记的参数
return parameter.hasParameterAnnotation(CurrentUser.class);
}

@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
// 从 request 中获取之前设置的用户ID
return webRequest.getAttribute("currentUserId", WebRequest.SCOPE_REQUEST);
}
}

第三步:注册解析器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

@Autowired
private CurrentUserArgumentResolver currentUserArgumentResolver;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(currentUserArgumentResolver);
}
}

方案示例

Filter 层面

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Autowired
private JwtService jwtService;

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

String authorizationHeader = request.getHeader("Authorization");

if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
String token = authorizationHeader.substring(7);
try {
Long userId = jwtService.extractUserId(token);
// 标准:使用 request.setAttribute 设置
request.setAttribute("currentUserId", userId);
} catch (Exception e) {
// token 无效,继续执行后续逻辑
}
}

filterChain.doFilter(request, response);
}
}

Controller 层面

@RestController
@RequestMapping("/api/users")
public class UserController {

@Autowired
private UserService userService;

@GetMapping("/me")
public ResponseEntity<User> getCurrentUser(@CurrentUser Long userId) {
// userId 被"魔法般"地自动注入了!
User user = userService.findById(userId);
return ResponseEntity.ok(user);
}

@PutMapping("/me")
public ResponseEntity<User> updateCurrentUser(@CurrentUser Long userId,
@RequestBody UserUpdateRequest request) {
// 每个 Controller 方法都可以直接使用 @CurrentUser
User updatedUser = userService.updateUser(userId, request);
return ResponseEntity.ok(updatedUser);
}

@GetMapping("/permissions")
public ResponseEntity<List<Permission>> getUserPermissions(@CurrentUser Long userId) {
// 完全类型安全,无需手动类型转换
List<Permission> permissions = userService.getUserPermissions(userId);
return ResponseEntity.ok(permissions);
}
}

原理解析

HandlerMethodArgumentResolver 工作流程

YesNoHTTP请求Filter链DispatcherServletHandlerMappingHandlerAdapterHandlerMethodArgumentResolversupportsParameter?resolveArgument使用默认解析器参数注入Controller方法执行

在这个流程中,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 方法的参数类型动态决定返回什么对象,这样可以最大程度地提高代码的灵活性和复用性。

@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {

@Autowired
private UserService userService;

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class);
}

@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {

Long userId = (Long) webRequest.getAttribute("currentUserId", WebRequest.SCOPE_REQUEST);

if (userId == null) {
return null; // 或者抛出异常
}

// 根据参数类型决定返回什么
Class<?> parameterType = parameter.getParameterType();

if (parameterType == Long.class || parameterType == long.class) {
return userId;
} else if (parameterType == User.class) {
return userService.findById(userId);
} else if (parameterType == UserProfile.class) {
return userService.getUserProfile(userId);
}

throw new IllegalArgumentException("Unsupported parameter type: " + parameterType);
}
}

Controller 中的多种用法

@RestController
@RequestMapping("/api")
public class AdvancedUserController {

@GetMapping("/user/id")
public ResponseEntity<String> getUserId(@CurrentUser Long userId) {
return ResponseEntity.ok("User ID: " + userId);
}

@GetMapping("/user/info")
public ResponseEntity<User> getUserInfo(@CurrentUser User user) {
return ResponseEntity.ok(user);
}

@GetMapping("/user/profile")
public ResponseEntity<UserProfile> getUserProfile(@CurrentUser UserProfile profile) {
return ResponseEntity.ok(profile);
}
}

实战技巧与注意事项

在实际项目中使用 HandlerMethodArgumentResolver 时,还需要考虑一些实际的工程问题。下面是一些常见的场景和解决方案。

1. 异常处理

在用户未登录或者 Token 无效的情况下,我们需要优雅地处理异常情况,而不是让系统抛出难以理解的错误信息。

@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {

@Override
public Object resolveArgument(...) throws Exception {
Long userId = (Long) webRequest.getAttribute("currentUserId", WebRequest.SCOPE_REQUEST);

if (userId == null) {
throw new UnauthorizedException("用户未登录");
}

return userId;
}
}

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<String> handleUnauthorized(UnauthorizedException e) {
return ResponseEntity.status(401).body(e.getMessage());
}
}

2. 与 Spring Security 集成

在许多企业级应用中,我们使用 Spring Security 进行认证和授权。如何将 Spring Security 的用户信息与我们的自定义 Resolver 结合使用是一个常见问题。

Spring Security 提供了 SecurityContextHolder 来存储当前用户的认证信息,我们可以直接从中获取用户详情,然后转换成我们需要的格式。这种集成方式的优势是可以复用 Spring Security 的完整认证体系,包括各种认证方式(JWT、OAuth2、Session 等)。

@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {

@Override
public Object resolveArgument(...) throws Exception {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication != null && authentication.isAuthenticated()) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return Long.parseLong(userDetails.getUsername()); // 假设username存储的是userId
}

return null;
}
}

3. 多租户场景

在 SaaS 应用中,多租户是一个常见的需求。除了当前用户信息,我们还需要知道当前租户的信息。通过创建多个自定义注解和对应的 Resolver,我们可以轻松实现这种需求。

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

@Component
public class CurrentTenantArgumentResolver implements HandlerMethodArgumentResolver {

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentTenant.class);
}

@Override
public Object resolveArgument(...) throws Exception {
return webRequest.getAttribute("currentTenantId", WebRequest.SCOPE_REQUEST);
}
}

@GetMapping("/tenant/data")
public ResponseEntity<List<Data>> getTenantData(@CurrentUser Long userId,
@CurrentTenant Long tenantId) {
// 同时获取当前用户和租户信息
List<Data> data = dataService.findByUserAndTenant(userId, tenantId);
return ResponseEntity.ok(data);
}

总结

通过 Spring MVC 的 HandlerMethodArgumentResolver我们实现了一个可以”跨 Filter 与 Controller 传参”的技术实现方案。将底层 Servlet API 的 request.getAttribute() 操作抽象为编译时类型安全的方法参数注入,实现了框架层面的参数解析适配,既保持了架构的纯净性,又提供了强大的扩展能力。

文章目录
  1. 1. 前言:一个痛点
    1. 1.0.1. 传统方案的”缺陷”
  • 2. Spring MVC:HandlerMethodArgumentResolver
    1. 2.0.1. 设计思路
    2. 2.0.2. 核心实现
      1. 2.0.2.1. 第一步:创建自定义注解
      2. 2.0.2.2. 第二步:实现参数解析器
      3. 2.0.2.3. 第三步:注册解析器
  • 3. 方案示例
    1. 3.0.1. Filter 层面
    2. 3.0.2. Controller 层面
  • 4. 原理解析
    1. 4.0.1. HandlerMethodArgumentResolver 工作流程
    2. 4.0.2. 方案优势
  • 5. 进阶用法:传递完整用户对象
    1. 5.0.1. 扩展解析器支持复杂对象
    2. 5.0.2. Controller 中的多种用法
  • 6. 实战技巧与注意事项
    1. 6.0.1. 1. 异常处理
    2. 6.0.2. 2. 与 Spring Security 集成
    3. 6.0.3. 3. 多租户场景
  • 7. 总结