你知道有哪些Spring MVC扩展点可以解析接口参数和处理返回值吗?

1.概述

Spring MVC 是一个灵活且强大的框架,它允许开发者在框架的基础上进行深度定制,以满足各种复杂的业务需求。HandlerMethodArgumentResolverHandlerMethodReturnValueHandlerSpring MVC 提供的两个重要扩展点,分别用于处理控制器方法的参数解析和返回值处理。本文将详细探讨这两个接口的作用、使用场景以及如何自定义实现。

关于Spring MVC的扩展点,我们之前已经总结过的两个扩展点:

谈谈@ControllerAdvice的使用及其实现原理:用于定义全局的异常处理、数据绑定、数据预处理等功能。

一文带你掌握SpringMVC扩展点RequestBodyAdvice和ResponseBodyAdvice如何使用及实现原理RequestBodyAdvice允许开发者在处理 HTTP 请求体之前或之后插入自定义逻辑。它通过与 HttpMessageConverter 紧密集成,在请求体读取和转换的过程中提供了扩展点。了解其工作原理有助于在复杂的请求处理场景中实现更强大的功能,如日志记录、数据预处理加解密和签名验证 等。ResponseBodyAdvice 是一个强大的工具,允许在 Spring MVC 中对响应数据进行集中处理和修改。通过自定义 ResponseBodyAdvice 实现类,可以实现响应数据的加密、格式转换、统一包装 等多种功能,提升代码的可维护性和一致性。其实HandlerMethodArgumentResolverHandlerMethodReturnValueHandler实现的场景功能也是差不多的,都是对接口的参数解析和返回结果加工处理,但是今天这里就不再也对接口入参和返回结果加解密操作进行示例,感兴趣可以根据前面总结的文章提到的加解密需求功能用本文讲解的扩展点自行实现一波,那就真的学到了~~~

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就可以实现。

首先可以自定义一个注解标识@LoginUseruserInfo参数上,其作用不言而喻就是为了上面源码定义提到的方法#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.resolversHandlerMethodArgumentResolverComposite,它也实现了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.returnValueHandlersHandlerMethodReturnValueHandlerComposite,它也实现了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;
	}

可以看出在实现原理和流程上,HandlerMethodArgumentResolverHandlerMethodReturnValueHandler套路几乎是一样的。

5.总结

HandlerMethodArgumentResolverHandlerMethodReturnValueHandler 是 Spring MVC 框架中非常重要的扩展点,它们允许开发者根据业务需求对参数解析和返回值处理进行深度定制。通过合理使用这些扩展点,你可以在不改变核心框架的情况下,灵活地实现各种复杂的功能需求。希望本文对你理解和使用这两个接口有所帮助。

相关推荐
Renascence.4098 分钟前
力扣--649.Dota2参议院
java·数据结构·算法·leetcode
VaporGas16 分钟前
掌握Java封装:以猜拳小游戏为例,深入理解OOP
java·开发语言·学习·面向对象编程·oop·猜拳游戏·封装思想
小小工匠22 分钟前
加密与安全_ sm-crypto 国密算法sm2、sm3和sm4的Java库
java·算法·安全·sm2·sm3·sm4
程序员大金1 小时前
基于SpringBoot+Vue+MySQL的垃圾分类回收管理系统
java·vue.js·spring boot·后端·mysql·mybatis
陈小唬1 小时前
树形结构构建的两种方式
java·数据库·算法
CJH~1 小时前
Java入门:09.Java中三大特性(封装、继承、多态)01
java·开发语言·单例模式
打工人9961 小时前
反编译app
java
coding侠客1 小时前
Spring Boot 注解探秘:常用配置值读取注解的魔力
java·spring boot·后端·spring·spring cloud
暮志未晚Webgl1 小时前
94. UE5 GAS RPG 实现攻击击退效果
java·前端·ue5
程序员大金2 小时前
基于SpringBoot+Vue+MySQL的影院购票系统
java·vue.js·spring boot·后端·mysql·mybatis