Spring Boot 自定义参数解析器:实现 @JsonParam 注解解析 JSON 请求体

发布时间:2026/8/14 1:28:05
Spring Boot 自定义参数解析器:实现 @JsonParam 注解解析 JSON 请求体 摘要本文介绍如何在 Spring Boot 中自定义参数解析器实现类似RequestParam的功能来解析 JSON 请求体中的特定字段解决RequestBody需要接收整个 JSON 对象的问题。一、背景与问题在 Spring MVC 开发中我们经常遇到这样的场景前端传递 JSON 格式的请求体但后端只需要其中的几个字段。使用RequestParam无法接收 JSON 格式的请求体而使用RequestBody又需要定义完整的 DTO 对象来接收整个 JSON这在只需要少量字段时显得繁琐。Spring 自带的参数解析器不支持直接从 JSON 请求体中提取特定字段因此我们需要通过自定义参数解析器来解决这个问题。二、自定义注解 JsonParam首先创建一个自定义注解用于标记需要从 JSON 请求体中提取的参数package com.manqian.crm.resolver; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; Target(ElementType.PARAMETER) Retention(RetentionPolicy.RUNTIME) public interface JsonParam { String value(); boolean required() default true; String defaultValue() default ; }注解说明value(): JSON 路径表达式用于定位要提取的字段required(): 字段是否必须默认 truedefaultValue(): 字段不存在时的默认值三、自定义参数解析器实现实现HandlerMethodArgumentResolver接口创建JsonPathArgumentResolver类package com.manqian.crm.resolver; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.PathNotFoundException; import com.manqian.crm.api.exception.ParamCheckException; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.MethodParameter; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import javax.servlet.http.HttpServletRequest; import java.io.IOException; public class JsonPathArgumentResolver implements HandlerMethodArgumentResolver { private static final String JSON_REQUEST_BODY JSON_REQUEST_BODY; private static final Logger logger LoggerFactory.getLogger(JsonPathArgumentResolver.class); // 判断是否支持要转换的参数类型 Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(JsonParam.class); } // 当支持后进行相应的转换 Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { JsonParam annotation parameter.getParameterAnnotation(JsonParam.class); String jsonPath annotation.value(); logger.debug(开始解析参数 [{}]JSON 路径: {}, parameter.getParameterName(), jsonPath); String body getRequestBody(webRequest); logger.debug(请求体内容: {}, body); // 1. 请求体为空或非 JSON 格式处理 if (body null || body.trim().isEmpty()) { logger.warn(请求体为空参数: {}路径: {}, parameter.getParameterName(), jsonPath); if (annotation.required()) { throw new ParamCheckException(请求体不能为空); } return handleDefaultValue(parameter, annotation); } Object val null; try { // 2. 使用 JsonPath 提取字段 val JsonPath.read(body, jsonPath); logger.debug(成功提取字段 [{}]值: {}, jsonPath, val); // 3. 字段类型转换处理 val convertValue(val, parameter.getParameterType(), jsonPath); if (annotation.required() amp;amp;amp;amp; val null) { logger.error(必填字段 [{}] 值为空, jsonPath); throw new ParamCheckException(jsonPath 不能为空); } } catch (PathNotFoundException e) { logger.warn(JSON 路径 [{}] 不存在参数: {}, jsonPath, parameter.getParameterName()); if (annotation.required()) { throw new ParamCheckException(jsonPath 字段不存在); } return handleDefaultValue(parameter, annotation); } catch (Exception e) { logger.error(解析 JSON 路径 [{}] 时发生异常: {}, jsonPath, e.getMessage(), e); if (annotation.required()) { throw new ParamCheckException(字段 [ jsonPath ] 解析失败: e.getMessage()); } return handleDefaultValue(parameter, annotation); } return val; } private String getRequestBody(NativeWebRequest webRequest) { HttpServletRequest servletRequest webRequest.getNativeRequest(HttpServletRequest.class); String jsonBody (String) servletRequest.getAttribute(JSON_REQUEST_BODY); if (jsonBody null) { try { jsonBody IOUtils.toString(servletRequest.getInputStream(), UTF-8); servletRequest.setAttribute(JSON_REQUEST_BODY, jsonBody); logger.debug(已缓存请求体长度: {}, jsonBody.length()); } catch (IOException e) { logger.error(读取请求体失败, e); throw new RuntimeException(读取请求体失败, e); } } return jsonBody; } /** 处理字段类型转换 */ private Object convertValue(Object rawValue, Classlt;?gt; targetType, String jsonPath) { if (rawValue null) { return null; } try { if (targetType String.class) { return rawValue.toString(); } else if (targetType Integer.class || targetType int.class) { if (rawValue instanceof Number) { return ((Number) rawValue).intValue(); } else { return Integer.parseInt(rawValue.toString()); } } else if (targetType Long.class || targetType long.class) { if (rawValue instanceof Number) { return ((Number) rawValue).longValue(); } else { return Long.parseLong(rawValue.toString()); } } else if (targetType Double.class || targetType double.class) { if (rawValue instanceof Number) { return ((Number) rawValue).doubleValue(); } else { return Double.parseDouble(rawValue.toString()); } } else if (targetType Boolean.class || targetType boolean.class) { if (rawValue instanceof Boolean) { return rawValue; } else { return Boolean.parseBoolean(rawValue.toString()); } } // 其他类型直接返回由 Spring 后续转换 return rawValue; } catch (NumberFormatException e) { logger.error(字段 [{}] 类型转换失败原始值: {}目标类型: {}, jsonPath, rawValue, targetType.getSimpleName(), e); throw new ParamCheckException(字段 [ jsonPath ] 类型转换失败无法将 [ rawValue ] 转换为 targetType.getSimpleName()); } catch (Exception e) { logger.error(字段 [{}] 类型转换异常: {}, jsonPath, e.getMessage(), e); throw new ParamCheckException(字段 [ jsonPath ] 类型转换异常: e.getMessage()); } } /** 处理默认值 */ private Object handleDefaultValue(MethodParameter parameter, JsonParam annotation) { String defaultValue annotation.defaultValue(); if (!defaultValue.isEmpty()) { logger.debug(使用默认值 [{}] 作为参数 [{}] 的值, defaultValue, parameter.getParameterName()); return convertValue(defaultValue, parameter.getParameterType(), annotation.value()); } logger.debug(参数 [{}] 无默认值返回 null, parameter.getParameterName()); return null; } }解析器核心功能supportsParameter(): 检查参数是否带有JsonParam注解resolveArgument(): 使用 JsonPath 从 JSON 请求体中提取指定字段getRequestBody(): 缓存请求体避免重复读取 InputStream新增异常处理:请求体为空或非 JSON 格式时的处理字段类型转换失败如字符串转整数的处理关键步骤的日志记录四、注册自定义解析器4.1 Spring Boot 配置方式一package com.demo; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; import java.util.List; SpringBootApplication public class WebMvcConfiguration extends WebMvcConfigurationSupport { Override protected void addArgumentResolvers(Listlt;HandlerMethodArgumentResolvergt; argumentResolvers) { // 注册 JsonPathArgumentResolver 参数解析器 argumentResolvers.add(new JsonPathArgumentResolver()); } }4.2 Spring Boot 配置方式二package com.manqian.crm.resolver; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.List; Configuration public class ClientResourcesConfig implements WebMvcConfigurer { Override public void addArgumentResolvers(Listlt;HandlerMethodArgumentResolvergt; argumentResolvers) { argumentResolvers.add(new JsonPathArgumentResolver()); } }4.3 传统 XML 配置方式!-- 方式一 -- mvc:annotation-driven mvc:argument-resolvers bean classcom.manqian.crm.resolver.JsonPathArgumentResolver/ /mvc:argument-resolvers /mvc:annotation-driven !-- 方式二 -- bean classorg.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter property namecustomArgumentResolvers list bean classcom.manqian.crm.resolver.JsonPathArgumentResolver/ /list /property /bean五、使用示例5.1 添加依赖在pom.xml中添加 JsonPath 依赖dependency groupIdcom.jayway.jsonpath/groupId artifactIdjson-path/artifactId version2.9.0/version /dependency5.2 Controller 中使用 JsonParamRestController RequestMapping(/api) public class UserController { PostMapping(/user) public ResponseEntitylt;Stringgt; createUser( JsonParam($.name) String name, JsonParam($.age) Integer age, JsonParam($.email) String email) { // 直接使用提取的参数 User user new User(name, age, email); userService.save(user); return ResponseEntity.ok(用户创建成功); } }请求示例{ name: 张三, age: 25, email: zhangsanexample.com, address: 北京市朝阳区 }在这个例子中即使请求体包含address字段Controller 方法也只接收name、age和email三个参数。六、注意事项JsonPath 表达式以$.开头支持复杂的路径查询如果字段不存在且requiredtrue会抛出异常请求体会被缓存避免重复读取 InputStream支持基本类型、字符串和复杂对象的提取七、参考资源StackOverflow: Passing multiple variables in RequestBodySpring MVC 自定义参数解析器实践JsonPath GitHub 仓库八、总结通过自定义JsonParam注解和JsonPathArgumentResolver解析器我们实现了从 JSON 请求体中提取特定字段的功能避免了为每个接口定义完整的 DTO 对象。这种方法特别适用于只需要请求体中少量字段的场景前端传递的 JSON 结构可能变化的情况微服务间接口调用的参数简化这种方案既保持了RequestParam的简洁性又支持 JSON 请求体的灵活性是 Spring MVC 参数解析的有力扩展。