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
方法进行参数解析。定义如下:
java
public interface HandlerMethodArgumentResolver {
/**
* 是否支持解析该参数
*/
boolean supportsParameter(MethodParameter parameter);
/**
* 解析该参数
*
*/
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
从源码来看这个接口定义很简单,#supportsParameter()
是否支持解析该参数,如果支持就调用#resolveArgument()
,话不多说直接来看实际开发中的应用场景,我们都知道一般在业务系统开发中,客户端访问服务端接口一般都需要走登录认证,把当前用户信息放到请求的上下文,以便后续获取当前登陆用户信息做逻辑处理
java
RequestUserHolder.getCurrentUser()
但是有时候想把登录信息当做方法参数进行传递,如下所示:
java
@GetMapping("/user")
public User getUserInfo(@RequestParam("userId") Long userId) {
return userService.getUserInfo(userId);
}
这是错误的示范,个人标识通过客户端传参,这意味着客户端想传谁的userId
都行,这就导致了严重的数据安全问题。此时你可能在想有没有办法把userId
作为方法参数,但是不再通过客户端传参,而是登录认证之后在请求上下文中获取。完全可以,通过HandlerMethodArgumentResolver
就可以实现。
首先可以自定义一个注解标识@LoginUser
到userInfo
参数上,其作用不言而喻就是为了上面源码定义提到的方法#supportsParameter()
满足解析参数条件。
java
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LoginUser {
}
接下来就是实现HandlerMethodArgumentResolver
完成参数解析,也就是通过在请求上下文中获取登陆信息对参数userId
进行赋值操作:
java
@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
:
java
@Configuration
public class DefaultWebMvcConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginUserArgumentResolver());
}
}
通过上面参数解析配置之后,就可以通过参数解析赋值接口方法参数了。
java
@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") Long id
接收前端传参,后端也能正常接受到,但是我们并没有去使用它,如下所示:
3.HandlerMethodReturnValueHandler
在Spring MVC
中,当接口Controller方法执行完毕后,会遍历所有的HandlerMethodReturnValueHandler
,找到第一个 支持处理当前返回类型的HandlerMethodReturnValueHandler
,然后调用其handleReturnValue
方法处理返回值。定义如下所示:
java
public interface HandlerMethodReturnValueHandler {
/**
* 是否支持处理返回值
*/
boolean supportsReturnType(MethodParameter returnType);
/**
* 处理返回值
*/
void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}
有时候,你可能需要统一处理控制器的返回值,比如将所有的返回值包装在一个标准的响应格式中。格式定义如下:
java
@Data
public class ResponseVO<T> implements Serializable {
private Integer code;
private String msg;
private T data;
public ResponseVO() {
}
public ResponseVO(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public ResponseVO(Integer code, T data) {
this.code = code;
this.data = data;
}
public ResponseVO(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
private ResponseVO(ResponseStatusEnum resultStatus, T data) {
this.code = resultStatus.getCode();
this.msg = resultStatus.getMsg();
this.data = data;
}
/**
* 业务成功返回业务代码和描述信息
*/
public static ResponseVO<Void> success() {
return new ResponseVO<Void>(ResponseStatusEnum.SUCCESS, null);
}
/**
* 业务成功返回业务代码,描述和返回的参数
*/
public static <T> ResponseVO<T> success(T data) {
return new ResponseVO<T>(ResponseStatusEnum.SUCCESS, data);
}
}
这时,可以自定义 HandlerMethodReturnValueHandler
来实现:
java
public class ResponseReturnValueHandler implements HandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return true;
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
mavContainer.setRequestHandled(true);
HttpServletResponse servletResponse = webRequest.getNativeResponse(HttpServletResponse.class);
servletResponse.getWriter().write(new ObjectMapper().writeValueAsString(ResponseVO.success(returnValue)));
}
}
把自定义的处理器放到Spring MVC容器中:
java
@Configuration
public class DefaultWebMvcConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginUserArgumentResolver());
}
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
handlers.add(new ResponseReturnValueHandler());
}
}
重启项目再次调用上面接口,你会发现并没有返回ResponseVO
,而是直接返回了User
。这就很奇怪了,为啥没生效呢???其实我们上面强调了Spring MVC
在执行完接口方法之后会遍历所有的HandlerMethodReturnValueHandler
找到第一个 支持处理的处理器就返回了,通过debug我们发现,每次找到的都是RequestResponseBodyMethodProcessor
,其实原因也很简单,现在项目中大部分采用前后端分离的架构,采用这种架构的项目,在返回数据时,几乎都是采用返回 json 格式的数据。而 Spring 中返回 json 格式的数据一般采用 @RestController
或者 @ResponseBody
注解,RequestResponseBodyMethodProcessor
正是Spring MVC
框架中默认的@ResponseBody
的处理器,而我们自定义的处理器虽然加入了返回值处理器集合list中,但由于顺序比较靠后,遍历处理器集合时候先匹配上了RequestResponseBodyMethodProcessor
处理器就返回了,自然没有我们自定义处理器的什么事了。
通过上面一说,你有没有体会出我们对返回值的处理加工都是建立在RequestResponseBodyMethodProcessor
处理器基础上做一些改动,结果统一封装亦是如此,所以我们需要重新自定义下处理器:
java
public class ResponseReturnValueHandler implements HandlerMethodReturnValueHandler {
// 其实定义一个RequestResponseBodyMethodProcessor内部变量,在它基础之上完成逻辑封装
private HandlerMethodReturnValueHandler handler;
public ResponseReturnValueHandler(HandlerMethodReturnValueHandler handler) {
this.handler = handler;
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
// 和RequestResponseBodyMethodProcessor支持的一样,方便后续替换
return handler.supportsReturnType(returnType);
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
if (!(returnValue instanceof ResponseVO)) {
returnValue = ResponseVO.success(returnValue);
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
}
下面就是将我们自定义的ResponseReturnValueHandler
来代替RequestResponseBodyMethodProcessor
java
@Configuration
public class RequestArgumentAndReturnValueConfig implements InitializingBean {
@Resource
RequestMappingHandlerAdapter requestMappingHandlerAdapter;
@Override
public void afterPropertiesSet() throws Exception {
// 获取处理器list
List<HandlerMethodReturnValueHandler> originHandlers = requestMappingHandlerAdapter.getReturnValueHandlers();
List<HandlerMethodReturnValueHandler> newHandlers = new ArrayList<>(originHandlers.size());
// 遍历处理器list,替换掉RequestResponseBodyMethodProcessor
for (HandlerMethodReturnValueHandler handler : originHandlers) {
if (handler instanceof RequestResponseBodyMethodProcessor) {
newHandlers.add(new ResponseReturnValueHandler(handler));
}else{
newHandlers.add(handler);
}
}
// 把新的处理器list放入Spring MVC
requestMappingHandlerAdapter.setReturnValueHandlers(newHandlers);
}
}
重启项目再次执行上面的示例接口,输出如下:成功对返回结果进行了统一格式封装
json
{
"code": 200,
"msg": "OK",
"data": {
"id": 8,
"userNo": "001",
"gender": 0,
"name": "张三",
"birthday": "2024-08-07",
"phone": "12234",
"isDelete": 0,
"createTime": "2024-07-03T16:09:12"
}
}
项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用
Github地址:https://github.com/plasticene/plasticene-boot-starter-parent
Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent
微信公众号 :Shepherd进阶笔记
交流探讨qun:Shepherd_126
4.实现原理
再来看看一次接口请求在Spring MVC
的执行流程图:
都会进入核心控制器DispatcherServlet
的方法#doDispatch()
找到处理器适配器之后,执行:
java
// Actually invoke the handler. 真正执行处理器controller
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
后续源码执行流程大概如下:
RequestMappingHandlerAdapter.invokeAndHandle(webRequest, mavContainer);
--ServletInvocableHandlerMethod.invokeAndHandle(webRequest, mavContainer);
---`Object returnValue = InvocableHandlerMethod.invokeForRequest(webRequest, mavContainer, providedArgs);`
---Object[] args = InvocableHandlerMethod.getMethodArgumentValues(request, mavContainer, providedArgs);
来到ServletInvocableHandlerMethod
的方法#invokeAndHandle()
java
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 执行入口 重点
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);
if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
}
else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}
mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
// 处理返回值 重点
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}
接着来到InvocableHandlerMethod
的#invokeForRequest()
:
java
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
return doInvoke(args);
}
解析请求传输参数方法#getMethodArgumentValues()
java
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
Object[] args = new Object[parameters.length];
// 遍历接口方法参数
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
// 重点 判断参数是否有对应的解析器
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
// 重点 重点 重点 找到对应参数解析器解析参数
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}
这里的this.resolvers
是HandlerMethodArgumentResolverComposite
,它也实现了HandlerMethodArgumentResolver
,核心代码如下:
java
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 是否有解析当前参数的解析器
return getArgumentResolver(parameter) != null;
}
/**
* Iterate over registered
* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}
* and invoke the one that supports it.
* @throws IllegalArgumentException if no suitable argument resolver is found
*/
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unsupported parameter type [" +
parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
}
// 解析参数
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
/**
* Find a registered {@link HandlerMethodArgumentResolver} that supports
* the given method parameter.
*/
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
// 先从本地缓存中查找,没有的话再遍历
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
// 遍历解析器list
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
// 匹配上支持的
if (resolver.supportsParameter(parameter)) {
result = resolver;
// 放入缓存
this.argumentResolverCache.put(parameter, result);
// 中断遍历,返回解析器
break;
}
}
}
return result;
}
上面就是关于HandlerMethodArgumentResolver
的实现,下面我们再来看看HandlerMethodReturnValueHandler
是咋实现的。其实和上面流程差不多的,还是来到ServletInvocableHandlerMethod
的方法#invokeAndHandle()
中执行的返回值处理方法this.returnValueHandlers.handleReturnValue();
这里的this.returnValueHandlers
是HandlerMethodReturnValueHandlerComposite
,它也实现了HandlerMethodReturnValueHandler
,核心代码如下:
java
/**
* Whether the given {@linkplain MethodParameter method return type} is supported by any registered
* {@link HandlerMethodReturnValueHandler}.
*/
@Override
public boolean supportsReturnType(MethodParameter returnType) {
// 是否支持对结果进行处理
return getReturnValueHandler(returnType) != null;
}
@Nullable
private HandlerMethodReturnValueHandler getReturnValueHandler(MethodParameter returnType) {
// 遍历处理器
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
/**
* Iterate over registered {@link HandlerMethodReturnValueHandler HandlerMethodReturnValueHandlers} and invoke the one that supports it.
* @throws IllegalStateException if no suitable {@link HandlerMethodReturnValueHandler} is found.
*/
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
// 查找处理器
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
// 处理返回值
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
@Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
可以看出在实现原理和流程上,HandlerMethodArgumentResolver
与 HandlerMethodReturnValueHandler
套路几乎是一样的。
5.总结
HandlerMethodArgumentResolver
和 HandlerMethodReturnValueHandler
是 Spring MVC 框架中非常重要的扩展点,它们允许开发者根据业务需求对参数解析和返回值处理进行深度定制。通过合理使用这些扩展点,你可以在不改变核心框架的情况下,灵活地实现各种复杂的功能需求。希望本文对你理解和使用这两个接口有所帮助。