前言
不知各位在开发的时候有没有遇到这种情况,当前后端分离的时候,前端时常会把很简单的参数使用 JSON 格式传入,当 Spring 要获取这些参数的时候每次都需要定义一个类,在使用的时候也需要使用对象的 Getter
方法,这样极其不方便。而如果要改前端使用 FormData 的方式传输,那么又会遇到另外一个问题:前端常用的请求库是 axios
,而 axios 传输的数据默认是采用 JSON 格式传输的,如果需要使用 FromData 的方式传输,那么需要再每个请求方法上增加 FormData 的 Content-Type
,或者添加到默认的配置中。
那么有什么办法可以在 Spring 中使 JSON 可以像 FormData 那样方便的注入呢?
思路
在 Spring 入参 Controller 的时候会经过一系列的 HandlerMethodArgumentResolver
,我们可以写一个 Resolver 实现该接口,并在 Spring 中增加这个 Resolver,那么只要符合 JSON 格式参数,那么就可以通过该 Resolver 实现注入。
实现
首先我们需要准备一些注解,用于标注是通过 JSON 格式的数据获取参数的:
首先是和 @RequestParam
一样的 @JsonParam
,用于标注该参数注入的名称和是否必须注入等信息:
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JsonParam {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
boolean required() default true;
String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
}
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JsonParam {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
boolean required() default true;
String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
}
然后是用于标注在方法上的 @RequestJson
,通过标注这个注解,我们就可以不需要使用 @JsonParam
注解来描述参数,而是使 Spring 通过参数的名称注入该参数:
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestJson {
}
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestJson {
}
还有一些工具类这里就不写了,这些工具类主要是用于一些特性的实现,比如点语法等。具体可以到 这里 查看。
接着就是用于处理 JSON 格式参数的 HandlerMethodArgumentResolver
了:
package me.ixk.json_inject.injector;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.TextNode;
import java.io.IOException;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import me.ixk.json_inject.annotation.JsonParam;
import me.ixk.json_inject.annotation.RequestJson;
import me.ixk.json_inject.utils.Helper;
import me.ixk.json_inject.utils.JSON;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.MissingServletRequestParameterException;
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;
public class JsonArgumentResolver implements HandlerMethodArgumentResolver {
private static final String JSON_REQUEST_ATTRIBUTE_NAME = "JSON_REQUEST_BODY";
@Override
public boolean supportsParameter(final MethodParameter methodParameter) {
// 当参数有 JsonParam 或者 RequestJson 注解的时候,就视为传入的是 JSON 格式的参数
return (
methodParameter.hasParameterAnnotation(JsonParam.class) ||
methodParameter.hasMethodAnnotation(RequestJson.class)
);
}
@Override
public Object resolveArgument(
final MethodParameter methodParameter,
final ModelAndViewContainer modelAndViewContainer,
final NativeWebRequest nativeWebRequest,
final WebDataBinderFactory webDataBinderFactory
)
throws Exception {
// 读取并解析 JSON,记得把 JSON 存起来,一是可以避免多次解析,二是 Body 如果没有进行处理的话默认只能读取一次,当第二次读取的时候则会为空。
final JsonNode body = this.getJsonBody(nativeWebRequest);
// 判断是否是读取整个 JSON (因为有的时候传入的可能是一个数组或者其他类型的数据,而不是 JSON 对象)
if ("$body".equals(methodParameter.getParameterName())) {
return JSON.convertToObject(body, methodParameter.getParameterType());
}
final JsonParam jsonParam = methodParameter.getParameterAnnotation(
JsonParam.class
);
final RequestJson requestJson = methodParameter.getMethodAnnotation(
RequestJson.class
);
JsonNode node = NullNode.getInstance();
if (jsonParam != null) {
// 通过 JsonParam 中设置的名称读取
node = Helper.dataGet(body, jsonParam.name(), NullNode.getInstance());
} else if (requestJson != null) {
// 通过参数名称读取
node = body.get(methodParameter.getParameterName());
}
if (node.isNull()) {
if (jsonParam != null) {
// 若为空并且必须传入,则抛出异常
if (jsonParam.required()) {
throw new MissingServletRequestParameterException(
jsonParam.name(),
methodParameter.getParameterType().getTypeName()
);
} else {
// 否则传入默认参数
node = TextNode.valueOf(jsonParam.defaultValue());
}
} else {
return null;
}
}
// 转换数据类型
return JSON.convertToObject(node, methodParameter.getParameterType());
}
private JsonNode getJsonBody(final NativeWebRequest nativeWebRequest) {
final HttpServletRequest servletRequest = nativeWebRequest.getNativeRequest(
HttpServletRequest.class
);
JsonNode body = (JsonNode) nativeWebRequest.getAttribute(
JSON_REQUEST_ATTRIBUTE_NAME,
NativeWebRequest.SCOPE_REQUEST
);
if (body == null) {
try {
if (servletRequest != null) {
body =
JSON.parse(
servletRequest
.getReader()
.lines()
.collect(Collectors.joining("\n"))
);
}
} catch (final IOException e) {
//
}
if (body == null) {
body = NullNode.getInstance();
}
nativeWebRequest.setAttribute(
JSON_REQUEST_ATTRIBUTE_NAME,
body,
NativeWebRequest.SCOPE_REQUEST
);
}
return body;
}
}
package me.ixk.json_inject.injector;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.TextNode;
import java.io.IOException;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import me.ixk.json_inject.annotation.JsonParam;
import me.ixk.json_inject.annotation.RequestJson;
import me.ixk.json_inject.utils.Helper;
import me.ixk.json_inject.utils.JSON;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.MissingServletRequestParameterException;
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;
public class JsonArgumentResolver implements HandlerMethodArgumentResolver {
private static final String JSON_REQUEST_ATTRIBUTE_NAME = "JSON_REQUEST_BODY";
@Override
public boolean supportsParameter(final MethodParameter methodParameter) {
// 当参数有 JsonParam 或者 RequestJson 注解的时候,就视为传入的是 JSON 格式的参数
return (
methodParameter.hasParameterAnnotation(JsonParam.class) ||
methodParameter.hasMethodAnnotation(RequestJson.class)
);
}
@Override
public Object resolveArgument(
final MethodParameter methodParameter,
final ModelAndViewContainer modelAndViewContainer,
final NativeWebRequest nativeWebRequest,
final WebDataBinderFactory webDataBinderFactory
)
throws Exception {
// 读取并解析 JSON,记得把 JSON 存起来,一是可以避免多次解析,二是 Body 如果没有进行处理的话默认只能读取一次,当第二次读取的时候则会为空。
final JsonNode body = this.getJsonBody(nativeWebRequest);
// 判断是否是读取整个 JSON (因为有的时候传入的可能是一个数组或者其他类型的数据,而不是 JSON 对象)
if ("$body".equals(methodParameter.getParameterName())) {
return JSON.convertToObject(body, methodParameter.getParameterType());
}
final JsonParam jsonParam = methodParameter.getParameterAnnotation(
JsonParam.class
);
final RequestJson requestJson = methodParameter.getMethodAnnotation(
RequestJson.class
);
JsonNode node = NullNode.getInstance();
if (jsonParam != null) {
// 通过 JsonParam 中设置的名称读取
node = Helper.dataGet(body, jsonParam.name(), NullNode.getInstance());
} else if (requestJson != null) {
// 通过参数名称读取
node = body.get(methodParameter.getParameterName());
}
if (node.isNull()) {
if (jsonParam != null) {
// 若为空并且必须传入,则抛出异常
if (jsonParam.required()) {
throw new MissingServletRequestParameterException(
jsonParam.name(),
methodParameter.getParameterType().getTypeName()
);
} else {
// 否则传入默认参数
node = TextNode.valueOf(jsonParam.defaultValue());
}
} else {
return null;
}
}
// 转换数据类型
return JSON.convertToObject(node, methodParameter.getParameterType());
}
private JsonNode getJsonBody(final NativeWebRequest nativeWebRequest) {
final HttpServletRequest servletRequest = nativeWebRequest.getNativeRequest(
HttpServletRequest.class
);
JsonNode body = (JsonNode) nativeWebRequest.getAttribute(
JSON_REQUEST_ATTRIBUTE_NAME,
NativeWebRequest.SCOPE_REQUEST
);
if (body == null) {
try {
if (servletRequest != null) {
body =
JSON.parse(
servletRequest
.getReader()
.lines()
.collect(Collectors.joining("\n"))
);
}
} catch (final IOException e) {
//
}
if (body == null) {
body = NullNode.getInstance();
}
nativeWebRequest.setAttribute(
JSON_REQUEST_ATTRIBUTE_NAME,
body,
NativeWebRequest.SCOPE_REQUEST
);
}
return body;
}
}
最后还需要将该 Resolver 添加到 Spring 中:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> resolvers
) {
resolvers.add(new JsonArgumentResolver());
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> resolvers
) {
resolvers.add(new JsonArgumentResolver());
}
}
至此实现的部分就完成了,让我们看看如何使用吧。
使用
@SpringBootApplication
@RestController
public class JsonInjectApplication {
public static void main(final String[] args) {
SpringApplication.run(JsonInjectApplication.class, args);
}
@PostMapping("/param")
public String param(@JsonParam(name = "key") final String key) {
return key;
}
@PostMapping("/method")
@RequestJson
public String method(final String key) {
return key;
}
@PostMapping("/data-get")
public String dataGet(@JsonParam(name = "key.sub") final String key) {
return key;
}
@PostMapping("/default-value")
public String defaultValue(
@JsonParam(name = "value", required = false) final String value
) {
return value;
}
@PostMapping("/convert")
public Integer convert(@JsonParam(name = "key") final Integer key) {
return key;
}
@GetMapping("/get")
public String get(String key) {
return key;
}
@PostMapping("/post")
public String post(String key) {
return key;
}
}
@SpringBootApplication
@RestController
public class JsonInjectApplication {
public static void main(final String[] args) {
SpringApplication.run(JsonInjectApplication.class, args);
}
@PostMapping("/param")
public String param(@JsonParam(name = "key") final String key) {
return key;
}
@PostMapping("/method")
@RequestJson
public String method(final String key) {
return key;
}
@PostMapping("/data-get")
public String dataGet(@JsonParam(name = "key.sub") final String key) {
return key;
}
@PostMapping("/default-value")
public String defaultValue(
@JsonParam(name = "value", required = false) final String value
) {
return value;
}
@PostMapping("/convert")
public Integer convert(@JsonParam(name = "key") final Integer key) {
return key;
}
@GetMapping("/get")
public String get(String key) {
return key;
}
@PostMapping("/post")
public String post(String key) {
return key;
}
}
可以看到使用的方式和 @RequestParam
无太大差别。
结语
最近打算开始接着折腾 Spring 和后端了,感觉我还是更喜欢后端,前端太折腾了 ?。
无须定义类,Spring 快速注入 Json 参数
https://blog.ixk.me/post/spring-quickly-inject-json-parameters许可协议
BY-NC-SA
本文作者
Otstar Lin
发布于
2020/08/04
转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!