使用Spring MVC开发的,应该都使用过@RequstBody接收json参数,转换成pojo对象,非常方便阿,但是功能并不是很全面,有点点瑕疵,并不能支持json key 方式注入到String、Integer 这类类型对象。
前端传值 | 后端接收 | 结果 |
---|---|---|
{"id": 3,"name":"xxx"} | User(id,name) | 成功注入 |
{"id": 3,"name":"xxx"} | (String name,int id) | 并不支持这个方式 |
有时候一个接口只有两个参数上传,我们并不想为此创建一个单独的对象,这样可能会导致每一个hangdler 方法可能都存在一个pojo对象,大部分对象可能并不能被其他handler 共享。 而是希望使用一两个参数就可以接收到。先看下@RequestBody
这个注解如何实现将json 转换成pojo对象的,通过spring HandlerMethodArgumentResolver 参数解析器来实现的,了解下接口。
public interface HandlerMethodArgumentResolver {
/**
* 是否支持方法上参数的处理,只有返回ture,才会执行下面方法
* @param parameter the method parameter to check
* @return {@code true} if this resolver supports the supplied parameter;
* {@code false} otherwise
*/
boolean supportsParameter(MethodParameter parameter);
/**
* 将方法参数解析为给定请求的参数值
* A {@link ModelAndViewContainer} provides access to the model for the
* request. A {@link WebDataBinderFactory} provides a way to create
* a {@link WebDataBinder} instance when needed for data binding and
* type conversion purposes.
* @param parameter the method parameter to resolve. This parameter must
* have previously been passed to {@link #supportsParameter} which must
* have returned {@code true}.
* @param mavContainer the ModelAndViewContainer for the current request
* @param webRequest the current request
* @param binderFactory a factory for creating {@link WebDataBinder} instances
* @return the resolved argument value, or {@code null} if not resolvable
* @throws Exception in case of errors with the preparation of argument values
*/
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
复制代码
在RequestMappingHandlerAdapter
下可以看见默认参数解析器
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30);
// Annotation-based argument resolution
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false)); //@RequestPart 文件注入
resolvers.add(new RequestParamMapMethodArgumentResolver()); //@RequestParam
resolvers.add(new PathVariableMethodArgumentResolver()); //@PathVariable
resolvers.add(new PathVariableMapMethodArgumentResolver()); //@PathVariable 会返回一个Map对象
resolvers.add(new MatrixVariableMethodArgumentResolver()); //@MatrixVariable
resolvers.add(new MatrixVariableMapMethodArgumentResolver()); //MatrixVariable 会返回Map 对象
resolvers.add(new ServletModelAttributeMethodProcessor(false)); //属性板顶
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); //@RequestBody 后面重点讲解
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice)); //@RequestPart
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); // @RequestHeader
resolvers.add(new RequestHeaderMapMethodArgumentResolver()); //@RequestHeader Map对象
resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory())); //Cookie 值 注入
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); //@Value
resolvers.add(new SessionAttributeMethodArgumentResolver()); //@SessionAttribute
resolvers.add(new RequestAttributeMethodArgumentResolver()); //@RequestAttribute
// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver()); //servlet api对象 HttpServletRequest 这类
resolvers.add(new ServletResponseMethodArgumentResolver()); //ServletResponse 对象注入
resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); //RequestEntity、HttpEntity
resolvers.add(new RedirectAttributesMethodArgumentResolver()); //重定向
resolvers.add(new ModelMethodProcessor()); //返回Model 对象
resolvers.add(new MapMethodProcessor()); // 处理方法参数返回一个Map
resolvers.add(new ErrorsMethodArgumentResolver()); //处理错误方法参数,返回最后一个对象
resolvers.add(new SessionStatusMethodArgumentResolver()); //SessionStatus
resolvers.add(new UriComponentsBuilderMethodArgumentResolver()); //UriComponentsBuilder
if (KotlinDetector.isKotlinPresent()) {
resolvers.add(new ContinuationHandlerMethodArgumentResolver());
}
// Custom arguments 自定义
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}
// Catch-all
resolvers.add(new PrincipalMethodArgumentResolver());
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
resolvers.add(new ServletModelAttributeMethodProcessor(true));
return resolvers;
}
复制代码
可以看见大部分方法参数都是通过上面处理器去实现的,重点放在RequestResponseBodyMethodProcessor 如何注入json的,主要看两个方法如何实现的。
@Override
public boolean supportsParameter(MethodParameter parameter) {
//只有使用了@RequestBody 注解默认就开启处理
return parameter.hasParameterAnnotation(RequestBody.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
//转换成对象了
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
//获取参数变量名称
String name = Conventions.getVariableNameForParameter(parameter);
if (binderFactory != null) { //使用WebDatabinder 对已经序列化对象属性绑定处理
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
复制代码
将json 序列号成对象实现都是在readWithMessageConverters 方法中
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
MediaType contentType;
boolean noContentType = false;
try {
contentType = inputMessage.getHeaders().getContentType();
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
if (contentType == null) {
noContentType = true;
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
Class<?> contextClass = parameter.getContainingClass();
Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
if (targetClass == null) {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
targetClass = (Class<T>) resolvableType.resolve();
}
HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
Object body = NO_VALUE;
EmptyBodyCheckingHttpInputMessage message;
try {
//创建重复读写流
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
//通过HttpMessageConverter 来对String json 转换成对象
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
if (message.hasBody()) {
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType); //前置处理
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType); //后置处理 类型通知
}
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
}
//缩减部分代码
return body;
}
复制代码
http 请求内容调用GenericHttpMessageConverter read 方法转换成对象,这时调用的是AbstractJackson2HttpMessageConverter
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
MediaType contentType = inputMessage.getHeaders().getContentType();
Charset charset = getCharset(contentType);
ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), contentType);
Assert.state(objectMapper != null, "No ObjectMapper for " + javaType);
boolean isUnicode = ENCODINGS.containsKey(charset.name()) ||
"UTF-16".equals(charset.name()) ||
"UTF-32".equals(charset.name());
try {
if (inputMessage instanceof MappingJacksonInputMessage) { //这个是使用了JsonView
Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
if (deserializationView != null) {
ObjectReader objectReader = objectMapper.readerWithView(deserializationView).forType(javaType);
if (isUnicode) {
return objectReader.readValue(inputMessage.getBody());
}
else {
Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
return objectReader.readValue(reader); //反序列化成Java 对象
}
}
}
if (isUnicode) {
return objectMapper.readValue(inputMessage.getBody(), javaType);
}
else {
Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
return objectMapper.readValue(reader, javaType);
}
}
catch (InvalidDefinitionException ex) {
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage);
}
}
复制代码
可以看见@RequestBody 是通过GenericHttpMessageConverter class 调用ObjectMapper readValue 进行转换的,只能将json转换成pojo 对象, 并不支持简单类型 String、Integer、Long这类转换的,有着天然缺陷。如果想将json key 注入到参数,需要手动实现一个参数解析器,下面展示简单实现代码。
动手实现
目标: 我们希望创建一个类似@RequestBody的注解,标注当前参数支持json key直接注入,这个注解也可以直接在方法修饰,表示整个方法参数都是用注解,当然也可以支持整个类,表示所有方法的参数都支持。在实现一个对应注解的参数解析器。
创建注解类
@Target({ElementType.PARAMETER,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonKeyValue {
/**
* json key 如何没有则使用 类型变量名
* @return
*/
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
}
复制代码
实现参数解析器
public class RequestJsonKeyValueMethodProcessor implements HandlerMethodArgumentResolver {
private ObjectMapper objectMapper ;
public RequestJsonKeyValueMethodProcessor(ObjectMapper objectMapper){
this.objectMapper = objectMapper;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean b = parameter.hasParameterAnnotation(JsonKeyValue.class);
if (!b) {
JsonKeyValue value = parameter.getMethodAnnotation(JsonKeyValue.class); //从方法上找注解
b = value != null;
if (!b){
value = parameter.getContainingClass().getAnnotation(JsonKeyValue.class); //从类上找注解
b = value != null;
}
}
return b;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
MediaType contentType = inputMessage.getHeaders().getContentType();
Charset charset = getCharset(contentType);
Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
JsonNode jsonNode = objectMapper.readTree(reader);
String parameterName = parameterName(parameter);
JsonNode path = jsonNode.path(parameterName);
Object o = objectMapper.convertValue(path, parameter.getNestedParameterType());
return o;
}
private String parameterName(MethodParameter parameter){
JsonKeyValue annotation = parameter.getParameterAnnotation(JsonKeyValue.class);
if (annotation != null){
String name = annotation.name();
if (StringUtils.hasText(name))
return annotation.name();
}
return parameter.getParameterName();
}
//抄袭 AbstractMessageConverterMethodArgumentResolver 主要是防止将流读入后,controller 方法不能在读了
private static class EmptyBodyCheckingHttpInputMessage implements HttpInputMessage {
private final HttpHeaders headers;
@Nullable
private final InputStream body;
public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
this.headers = inputMessage.getHeaders();
InputStream inputStream = inputMessage.getBody();
if (inputStream.markSupported()) {
inputStream.mark(1);
this.body = (inputStream.read() != -1 ? inputStream : null);
inputStream.reset();
}
else {
PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
int b = pushbackInputStream.read();
if (b == -1) {
this.body = null;
}
else {
this.body = pushbackInputStream;
pushbackInputStream.unread(b);
}
}
}
@Override
public HttpHeaders getHeaders() {
return this.headers;
}
@Override
public InputStream getBody() {
return (this.body != null ? this.body : StreamUtils.emptyInput());
}
public boolean hasBody() {
return (this.body != null);
}
}
private Charset getCharset(@Nullable MediaType contentType) {
if (contentType != null && contentType.getCharset() != null) {
return contentType.getCharset();
}
else {
return StandardCharsets.UTF_8;
}
}
}
复制代码
添加自定义参数解析器
@EnableWebMvc
@Configuration
public class WebMvcConfig implements WebMvcConfigurer{
private Logger logger = LoggerFactory.getLogger(WebMvcConfig.class);
@Autowired
private ObjectMapper objectMapper;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new RequestJsonKeyValueMethodProcessor(objectMapper));
}
}
复制代码
这里不用担心自定义参数解析器会覆盖spring 原生的解析器,这里会将addArgumentResolvers 添加解析器自动放入到CustomArgumentResolvers中。 一个简单自定义参数解析器就完成了,支持普通类的String、Integer 这些参数的解析。注意的是这个参数解析器并没有考虑参数为空的处理,Optional 这类情况只满足一些简单、快速开发的场景。不知道各位同行是否在日常开发中有没有使用到这些知识,我公司的基础框架就是使用这些技术来做快速开发的,对开发效率提升特别大的。