1.概述
Spring MVC
是一个灵活且强大的框架,它允许开发者在框架的基础上进行深度定制,以满足各种复杂的业务需求。HandlerMethodArgumentResolver
和 HandlerMethodReturnValueHandler
是 Spring MVC
提供的两个重要扩展点,分别用于处理控制器方法的参数解析和返回值处理。本文将详细探讨这两个接口的作用、使用场景以及如何自定义实现。
关于Spring MVC
的扩展点,我们之前已经总结过的两个扩展点:
谈谈@ControllerAdvice的使用及其实现原理:用于定义全局的异常处理、数据绑定、数据预处理等功能。
一文带你掌握SpringMVC扩展点RequestBodyAdvice和ResponseBodyAdvice如何使用及实现原理:RequestBodyAdvice
允许开发者在处理 HTTP 请求体之前或之后插入自定义逻辑。它通过与 HttpMessageConverter
紧密集成,在请求体读取和转换的过程中提供了扩展点。了解其工作原理有助于在复杂的请求处理场景中实现更强大的功能,如日志记录、数据预处理加解密和签名验证等。ResponseBodyAdvice
是一个强大的工具,允许在 Spring MVC
中对响应数据进行集中处理和修改。通过自定义 ResponseBodyAdvice
实现类,可以实现响应数据的加密、格式转换、统一包装等多种功能,提升代码的可维护性和一致性。其实HandlerMethodArgumentResolver
和 HandlerMethodReturnValueHandler
实现的场景功能也是差不多的,都是对接口的参数解析和返回结果加工处理,但是今天这里就不再也对接口入参和返回结果加解密操作进行示例,感兴趣可以根据前面总结的文章提到的加解密需求功能用本文讲解的扩展点自行实现一波,那就真的学到了~~~
2.HandlerMethodArgumentResolver
HandlerMethodArgumentResolver
接口的主要作用是将 HTTP 请求中的参数解析为控制器方法的参数。Spring MVC
默认提供了多种 HandlerMethodArgumentResolver
的实现,如解析 @RequestParam
、@PathVariable
、@RequestBody
等注解的参数。
当一个请求到达时,Spring MVC
会遍历已注册的 HandlerMethodArgumentResolver
,找到能够支持该方法参数的解析器,并调用其 resolveArgument
方法进行参数解析。定义如下:
public interface HandlerMethodArgumentResolver {
/**
* 是否支持解析该参数
*/
boolean supportsParameter(MethodParameter parameter);
/**
* 解析该参数
*
*/
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
从源码来看这个接口定义很简单,#supportsParameter()
是否支持解析该参数,如果支持就调用#resolveArgument()
,话不多说直接来看实际开发中的应用场景,我们都知道一般在业务系统开发中,客户端访问服务端接口一般都需要走登录认证,把当前用户信息放到请求的上下文,以便后续获取当前登陆用户信息做逻辑处理
RequestUserHolder.getCurrentUser()
但是有时候想把登录信息当做方法参数进行传递,如下所示:
@GetMapping("/user")
public User getUserInfo(@RequestParam("userId") Long userId) {
return userService.getUserInfo(userId);
}
这是错误的示范,个人标识通过客户端传参,这意味着客户端想传谁的userId
都行,这就导致了严重的数据安全问题。此时你可能在想有没有办法把userId
作为方法参数,但是不再通过客户端传参,而是登录认证之后在请求上下文中获取。完全可以,通过HandlerMethodArgumentResolver
就可以实现。
首先可以自定义一个注解标识@LoginUser
到userInfo
参数上,其作用不言而喻就是为了上面源码定义提到的方法#supportsParameter()
满足解析参数条件。
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LoginUser {
}
接下来就是实现HandlerMethodArgumentResolver
完成参数解析,也就是通过在请求上下文中获取登陆信息对参数userId
进行赋值操作:
@Slf4j
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
/**
* 入参筛选
*
* @param methodParameter 参数集合
* @return 格式化后的参数
*/
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.hasParameterAnnotation(LoginUser.class) && methodParameter.getParameterType().equals(UserSession.class);
}
/**
* @param methodParameter 入参集合
* @param modelAndViewContainer model 和 view
* @param nativeWebRequest web相关
* @param webDataBinderFactory 入参解析
* @return 包装对象
*/
@Override
public Object resolveArgument(MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory) {
return getCurrentUser(nativeWebRequest);
}
private UserSession getCurrentUser(NativeWebRequest webRequest) {
// 这里是获取当前用户的逻辑
// 1.你可以从请求信息中获取
// HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
// ...
// 2.也可以从登陆认证之后的上下文中获取
// UserSession currentUser = RequestUserHolder.getCurrentUser();
// 这里为了示例,就直接返回一个userSession进行模拟了
UserSession session = new UserSession();
session.setId(8L);
session.setName("张三");
session.setOrgId(6L);
return session;
}
}
当然,最后我们要实现 WebMvcConfigurer
接口的 #addArgumentResolvers()
方法,来增加这个自定义的处理器 LoginUserArgumentResolver
:
@Configuration
public class DefaultWebMvcConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginUserArgumentResolver());
}
}
通过上面参数解析配置之后,就可以通过参数解析赋值接口方法参数了。
@GetMapping("/loginUser")
public User getUser(@RequestParam("id") Long id, @LoginUser UserSession session) {
Long userId = session.getId();
User user = userService.getUser(userId);
return user;
}
postman调用接口:
从结果可以看出通过LoginUserArgumentResolver
参数解析器获得userSession的userId为8,去查询数据库返回。
接口方法中我们虽然使用@RequestParam("id"