HandlerMethodArgumentResolve 优化 Controller调用参数注入

Yuan.Sn

最近手搓出来一个电商系统 springMall ,基本完成了CRUD的接口编写。为了让 bite 更加的 “优雅” ,初步诊断了下“史山成分” ,发现 service层中 getCurrentUserId()这个获取用户ID的方法 重复次数较多,与 "克大哥" 深入沟通了下, 通过重构 HandlerMethodArgumentResolver 方法参数解析器 实现ID参数自动注入。

“史山”概述

后端接口调用时,部分需要获取用户的id值来做对应的处理。之前我们在 各个需要id查询的 Service 层写出 getCurrentUserId()方法,然后在Service层内具体需要id值的 **Controller接口服务 ** 中调用getCurrentUserId()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package site.geekie.shop.shoppingmall.service.impl;

import lombok.RequiredArgsConstructor;
...

/**
* 订单服务实现类, 根据用户 实现订单的CRUD
* 文件名: OrderServicellmpl.java
*/
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

private final OrderMapper orderMapper;
...

/**
* 获取当前登录用户ID (需要AOP重构的方法!!)
*/
private Long getCurrentUserId() {
SecurityUser securityUser = (SecurityUser) SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
return securityUser.getUser().getId(); <-- 这里
}

// 省略:buildOrderVO、管理员相关方法

/**
* 用户下单:从购物车生成订单
*/
@Override
@Transactional(rollbackFor = Exception.class)
public OrderVO createOrder(OrderDTO request) {
Long userId = getCurrentUserId(); <-- 这里
// ... 校验购物车、地址、库存,生成订单
return null;
}

/**
* 用户订单列表:查看我的全部订单
*/
@Override
public List<OrderVO> getMyOrders() {
Long userId = getCurrentUserId(); <-- 这里
// ... 查询并组装订单
return null;
}

...省略7处对 getCurrentUserId() 的调用

/**
* 确认收货:用户完成订单
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void confirmReceipt(String orderNo) {
OrderDO order = orderMapper.findByOrderNo(orderNo);
Long userId = getCurrentUserId(); <-- 这里
// ... 校验状态并更新完成时间
}
}

一个小小的 OrderServicellmpl.java文件就有10多出对 getCurrentUserId()方法的调用。然后在整个Service层 中,一共有4处需要 重新声明 并 调用 getCurrentUserId()方法 。It‘s horible!

image
image

HandlerMethodArgumentResolver重构 方案设计

在方案实施之前,补齐所需要的 Spring架构认知,先简要介绍一下 Spring 请求处理流程图:

image
image

HTTP 请求进入 DispatcherServlet后,通过 HandlerMapping 定位到对应的 Controller 方法,由 HandlerAdapter 调用HandlerMethodArgumentResolver 解析方法参数,完成参数绑定后执行 Controller,最终返回响应。

而我们需要扩展HandlerAdapter 层的 HandlerMethodArgumentResolver 处理器功能,通过自定义解析器,实现 @CurrentUserId 参数自动注入

Let's Restructure

Step 1:@CurrentUserId 注解定义 (CurrenUserld.java)

先写出一个 注解定义的接口。

1
2
3
4
@Target(ElementType.PARAMETER)   // 只能用在方法参数上
@Retention(RetentionPolicy.RUNTIME) // 运行时可见(反射能读到)
public @interface CurrentUserId {
}

关键元注解说明

元注解 作用
@Target(PARAMETER) 限制注解只能标注在方法参数上
@Retention(RUNTIME) 保留到运行时,否则反射读不到

Step 2:实现 @CurrentUserId参数解析器 (CurrenUserldResolver.java)

然后实现解析器HandlerMethodArgumentResolver的 重写,拦截 @CurrentUserId 注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component  // 注册为 Spring Bean
public class CurrentUserIdResolver implements HandlerMethodArgumentResolver {

@Override
public boolean supportsParameter(MethodParameter parameter) {
// 条件1:参数上有 @CurrentUserId 注解
// 条件2:参数类型是 Long
return parameter.hasParameterAnnotation(CurrentUserId.class)
&& Long.class.isAssignableFrom(parameter.getParameterType());
}

@Override
public Object resolveArgument(...) {
// 从 SecurityContext 获取当前用户
var auth = SecurityContextHolder.getContext().getAuthentication();
SecurityUser securityUser = (SecurityUser) auth.getPrincipal();
return securityUser.getUser().getId();
}
}

Step 3:注册解析器 CurrentUserIdResolver 到 MVC (WebMvcConfig)

把新建的解析器注册到 MVC Framework当中。

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

private final CurrentUserIdResolver currentUserIdResolver;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(currentUserIdResolver); // 注册自定义解析器
}
}

执行流程图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
用户请求 GET /api/v1/addresses

携带 JWT Token

┌─────────────────────────────────────────────────┐
│ JwtAuthenticationFilter │
│ 解析 Token → 设置 SecurityContext │
│ SecurityContextHolder.setContext(authentication)│
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│ DispatcherServlet │
│ 找到 AddressController.getAddressList() │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│ CurrentUserIdResolver │
│ │
│ 1. supportsParameter() → true │
│ (参数有 @CurrentUserId 且类型是 Long) │
│ │
│ 2. resolveArgument() │
│ SecurityContext → Authentication → Principal │
│ → SecurityUser → User → getId() → 返回id │
└─────────────────────────────────────────────────┘

Controller 方法执行
getAddressList(userId = 查询值)

返回地址列表

涉及的文件结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
site.geekie.shop.shoppingmall/
├── annotation/
│ └── CurrentUserId.java # 注解定义

├── config/
│ ├── CurrentUserIdResolver.java # 参数解析器
│ ├── WebMvcConfig.java # 注册解析器
│ └── SecurityConfig.java # 安全配置

├── security/
│ ├── SecurityUser.java # 用户信息封装
│ └── JwtAuthenticationFilter.java # JWT 过滤器

└── controller/
└── AddressController.java # 使用示例
Comments