站点图标

无须定义类,Spring 快速注入 Json 参数

2020-08-04折腾记录Spring
本文最后更新于 607 天前,文中所描述的信息可能已发生改变

前言

不知各位在开发的时候有没有遇到这种情况,当前后端分离的时候,前端时常会把很简单的参数使用 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,用于标注该参数注入的名称和是否必须注入等信息:


_14
@Target({ ElementType.PARAMETER })
_14
@Retention(RetentionPolicy.RUNTIME)
_14
@Documented
_14
public @interface JsonParam {
_14
@AliasFor("name")
_14
String value() default "";
_14
_14
@AliasFor("value")
_14
String name() default "";
_14
_14
boolean required() default true;
_14
_14
String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
_14
}

然后是用于标注在方法上的 @RequestJson,通过标注这个注解,我们就可以不需要使用 @JsonParam 注解来描述参数,而是使 Spring 通过参数的名称注入该参数:


_5
@Target({ ElementType.METHOD })
_5
@Retention(RetentionPolicy.RUNTIME)
_5
@Documented
_5
public @interface RequestJson {
_5
}

还有一些工具类这里就不写了,这些工具类主要是用于一些特性的实现,比如点语法等。具体可以到 这里 查看。

接着就是用于处理 JSON 格式参数的 HandlerMethodArgumentResolver 了:


_116
package me.ixk.json_inject.injector;
_116
_116
import com.fasterxml.jackson.databind.JsonNode;
_116
import com.fasterxml.jackson.databind.node.NullNode;
_116
import com.fasterxml.jackson.databind.node.TextNode;
_116
import java.io.IOException;
_116
import java.util.stream.Collectors;
_116
import javax.servlet.http.HttpServletRequest;
_116
import me.ixk.json_inject.annotation.JsonParam;
_116
import me.ixk.json_inject.annotation.RequestJson;
_116
import me.ixk.json_inject.utils.Helper;
_116
import me.ixk.json_inject.utils.JSON;
_116
import org.springframework.core.MethodParameter;
_116
import org.springframework.web.bind.MissingServletRequestParameterException;
_116
import org.springframework.web.bind.support.WebDataBinderFactory;
_116
import org.springframework.web.context.request.NativeWebRequest;
_116
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
_116
import org.springframework.web.method.support.ModelAndViewContainer;
_116
_116
public class JsonArgumentResolver implements HandlerMethodArgumentResolver {
_116
private static final String JSON_REQUEST_ATTRIBUTE_NAME = "JSON_REQUEST_BODY";
_116
_116
@Override
_116
public boolean supportsParameter(final MethodParameter methodParameter) {
_116
// 当参数有 JsonParam 或者 RequestJson 注解的时候,就视为传入的是 JSON 格式的参数
_116
return (
_116
methodParameter.hasParameterAnnotation(JsonParam.class) ||
_116
methodParameter.hasMethodAnnotation(RequestJson.class)
_116
);
_116
}
_116
_116
@Override
_116
public Object resolveArgument(
_116
final MethodParameter methodParameter,
_116
final ModelAndViewContainer modelAndViewContainer,
_116
final NativeWebRequest nativeWebRequest,
_116
final WebDataBinderFactory webDataBinderFactory
_116
)
_116
throws Exception {
_116
// 读取并解析 JSON,记得把 JSON 存起来,一是可以避免多次解析,二是 Body 如果没有进行处理的话默认只能读取一次,当第二次读取的时候则会为空。
_116
final JsonNode body = this.getJsonBody(nativeWebRequest);
_116
// 判断是否是读取整个 JSON (因为有的时候传入的可能是一个数组或者其他类型的数据,而不是 JSON 对象)
_116
if ("$body".equals(methodParameter.getParameterName())) {
_116
return JSON.convertToObject(body, methodParameter.getParameterType());
_116
}
_116
final JsonParam jsonParam = methodParameter.getParameterAnnotation(
_116
JsonParam.class
_116
);
_116
final RequestJson requestJson = methodParameter.getMethodAnnotation(
_116
RequestJson.class
_116
);
_116
JsonNode node = NullNode.getInstance();
_116
if (jsonParam != null) {
_116
// 通过 JsonParam 中设置的名称读取
_116
node = Helper.dataGet(body, jsonParam.name(), NullNode.getInstance());
_116
} else if (requestJson != null) {
_116
// 通过参数名称读取
_116
node = body.get(methodParameter.getParameterName());
_116
}
_116
if (node.isNull()) {
_116
if (jsonParam != null) {
_116
// 若为空并且必须传入,则抛出异常
_116
if (jsonParam.required()) {
_116
throw new MissingServletRequestParameterException(
_116
jsonParam.name(),
_116
methodParameter.getParameterType().getTypeName()
_116
);
_116
} else {
_116
// 否则传入默认参数
_116
node = TextNode.valueOf(jsonParam.defaultValue());
_116
}
_116
} else {
_116
return null;
_116
}
_116
}
_116
// 转换数据类型
_116
return JSON.convertToObject(node, methodParameter.getParameterType());
_116
}
_116
_116
private JsonNode getJsonBody(final NativeWebRequest nativeWebRequest) {
_116
final HttpServletRequest servletRequest = nativeWebRequest.getNativeRequest(
_116
HttpServletRequest.class
_116
);
_116
_116
JsonNode body = (JsonNode) nativeWebRequest.getAttribute(
_116
JSON_REQUEST_ATTRIBUTE_NAME,
_116
NativeWebRequest.SCOPE_REQUEST
_116
);
_116
_116
if (body == null) {
_116
try {
_116
if (servletRequest != null) {
_116
body =
_116
JSON.parse(
_116
servletRequest
_116
.getReader()
_116
.lines()
_116
.collect(Collectors.joining("\n"))
_116
);
_116
}
_116
} catch (final IOException e) {
_116
//
_116
}
_116
if (body == null) {
_116
body = NullNode.getInstance();
_116
}
_116
nativeWebRequest.setAttribute(
_116
JSON_REQUEST_ATTRIBUTE_NAME,
_116
body,
_116
NativeWebRequest.SCOPE_REQUEST
_116
);
_116
}
_116
_116
return body;
_116
}
_116
}

最后还需要将该 Resolver 添加到 Spring 中:


_10
@Configuration
_10
public class WebConfig implements WebMvcConfigurer {
_10
_10
@Override
_10
public void addArgumentResolvers(
_10
List<HandlerMethodArgumentResolver> resolvers
_10
) {
_10
resolvers.add(new JsonArgumentResolver());
_10
}
_10
}

至此实现的部分就完成了,让我们看看如何使用吧。

使用


_46
@SpringBootApplication
_46
@RestController
_46
public class JsonInjectApplication {
_46
_46
public static void main(final String[] args) {
_46
SpringApplication.run(JsonInjectApplication.class, args);
_46
}
_46
_46
@PostMapping("/param")
_46
public String param(@JsonParam(name = "key") final String key) {
_46
return key;
_46
}
_46
_46
@PostMapping("/method")
_46
@RequestJson
_46
public String method(final String key) {
_46
return key;
_46
}
_46
_46
@PostMapping("/data-get")
_46
public String dataGet(@JsonParam(name = "key.sub") final String key) {
_46
return key;
_46
}
_46
_46
@PostMapping("/default-value")
_46
public String defaultValue(
_46
@JsonParam(name = "value", required = false) final String value
_46
) {
_46
return value;
_46
}
_46
_46
@PostMapping("/convert")
_46
public Integer convert(@JsonParam(name = "key") final Integer key) {
_46
return key;
_46
}
_46
_46
@GetMapping("/get")
_46
public String get(String key) {
_46
return key;
_46
}
_46
_46
@PostMapping("/post")
_46
public String post(String key) {
_46
return key;
_46
}
_46
}

可以看到使用的方式和 @RequestParam 无太大差别。

结语

最近打算开始接着折腾 Spring 和后端了,感觉我还是更喜欢后端,前端太折腾了 ?。

无须定义类,Spring 快速注入 Json 参数

https://blog.ixk.me/post/spring-quickly-inject-json-parameters
  • 许可协议

    BY-NC-SA

  • 发布于

    2020-08-04

  • 本文作者

    Otstar Lin

转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!

浅谈缓存浅谈 Proxy 和 Aop