参考地址:https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc
源码地址:https://gitee.com/qinshizhang/spring-mvc-learn
返回信息如下:
<body><h1>HTTP状态 400 - 错误的请求</h1>
<hr class="line"/>
<p><b>类型</b> 状态报告</p>
<p><b>描述</b> 由于被认为是客户端对错误(例如:畸形的请求语法、无效的请求信息帧或者虚拟的请求路由),服务器无法或不会处理当前请求。</p>
<hr class="line"/>
<h3>Apache Tomcat/9.0.41</h3></body>
情况一、获取Long类型的参数
在不使用@InitBinder注册用户的自定义的java.beans.PropertyEditor类型情况
Java代码如下:
@RestController
@RequestMapping(path = "v1/hello/")
public class HelloController {
@GetMapping(path = "showLong/{userId}")
public long showLong(@PathVariable long userId) {
return userId;
}
}
请求参数如下:
### 测试long类型
GET http://localhost:9090/v1/hello/showLong/12345L
Accept: application/json
源码跟踪关键流程。
org.springframework.validation.DataBinder#convertIfNecessary(java.lang.Object, java.lang.Class<T>, org.springframework.core.MethodParameter)
---->
org.springframework.beans.TypeConverterSupport#convertIfNecessary(java.lang.Object, java.lang.Class<T>, org.springframework.core.MethodParameter)
---->
org.springframework.beans.TypeConverterSupport#convertIfNecessary(java.lang.Object, java.lang.Class<T>, org.springframework.core.convert.TypeDescriptor)
---->
org.springframework.beans.TypeConverterDelegate#convertIfNecessary(java.lang.String, java.lang.Object, java.lang.Object, java.lang.Class<T>, org.springframework.core.convert.TypeDescriptor)
---->
org.springframework.core.convert.support.GenericConversionService#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)
---->
org.springframework.core.convert.support.ConversionUtils#invokeConverter
---->
org.springframework.core.convert.support.GenericConversionService.ConverterFactoryAdapter#convert
---->
org.springframework.core.convert.support.StringToNumberConverterFactory.StringToNumber#convert
---->
org.springframework.util.NumberUtils#parseNumber(java.lang.String, java.lang.Class<T>)
org.springframework.util.NumberUtils#parseNumber(java.lang.String, java.lang.Class<T>)的源码如下:
public static <T extends Number> T parseNumber(String text, Class<T> targetClass) {
Assert.notNull(text, "Text must not be null");
Assert.notNull(targetClass, "Target class must not be null");
String trimmed = StringUtils.trimAllWhitespace(text);
if (Byte.class == targetClass) {
return (T) (isHexNumber(trimmed) ? Byte.decode(trimmed) : Byte.valueOf(trimmed));
}
else if (Short.class == targetClass) {
return (T) (isHexNumber(trimmed) ? Short.decode(trimmed) : Short.valueOf(trimmed));
}
else if (Integer.class == targetClass) {
return (T) (isHexNumber(trimmed) ? Integer.decode(trimmed) : Integer.valueOf(trimmed));
}
else if (Long.class == targetClass) {
return (T) (isHexNumber(trimmed) ? Long.decode(trimmed) : Long.valueOf(trimmed));
}
else if (BigInteger.class == targetClass) {
return (T) (isHexNumber(trimmed) ? decodeBigInteger(trimmed) : new BigInteger(trimmed));
}
else if (Float.class == targetClass) {
return (T) Float.valueOf(trimmed);
}
else if (Double.class == targetClass) {
return (T) Double.valueOf(trimmed);
}
else if (BigDecimal.class == targetClass || Number.class == targetClass) {
return (T) new BigDecimal(trimmed);
}
else {
throw new IllegalArgumentException(
"Cannot convert String [" + text + "] to target class [" + targetClass.getName() + "]");
}
}
使用@InitBinder注册用户的自定义的java.beans.PropertyEditor类型情况
Java代码如下:(这里我使用com.sun.beans.editors.LongEditor,也可自己实现)
@RestController
@RequestMapping(path = "v1/hello/")
public class HelloController {
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(long.class, new LongEditor());
binder.registerCustomEditor(Long.class, new LongEditor());
}
@GetMapping(path = "showLong/{userId}")
public long showLong(@PathVariable long userId) {
return userId;
}
}
请求参数如下:
### 测试long类型
GET http://localhost:9090/v1/hello/showLong/12345L
Accept: application/json
源码跟踪的关键流程如下:
org.springframework.validation.DataBinder#convertIfNecessary(java.lang.Object, java.lang.Class<T>, org.springframework.core.MethodParameter)
---->
org.springframework.beans.TypeConverterSupport#convertIfNecessary(java.lang.Object, java.lang.Class<T>, org.springframework.core.MethodParameter)
---->
org.springframework.beans.TypeConverterSupport#convertIfNecessary(java.lang.Object, java.lang.Class<T>, org.springframework.core.convert.TypeDescriptor)
---->
org.springframework.beans.TypeConverterDelegate#convertIfNecessary(java.lang.String, java.lang.Object, java.lang.Object, java.lang.Class<T>, org.springframework.core.convert.TypeDescriptor)
---->
org.springframework.beans.TypeConverterDelegate#doConvertValue
---->
org.springframework.beans.TypeConverterDelegate#doConvertTextValue
org.springframework.beans.TypeConverterDelegate#doConvertTextValue的源码如下:
private Object doConvertTextValue(@Nullable Object oldValue, String newTextValue, PropertyEditor editor) {
try {
editor.setValue(oldValue);
} catch (Exception var5) {
if (logger.isDebugEnabled()) {
logger.debug("PropertyEditor [" + editor.getClass().getName() + "] does not support setValue call", var5);
}
}
editor.setAsText(newTextValue);
return editor.getValue();
}
com.sun.beans.editors.LongEditor#setAsText的源码如下:
public void setAsText(String var1) throws IllegalArgumentException {
this.setValue(var1 == null ? null : Long.decode(var1));
}
仔细查看最终的参数转换都是使用Lond.decode(String value)方法或者Long.valueOf(String value)。而我们传递的参数是12345L,这两个方法执行都会抛出
java.lang.NumberFormatException: For input string: "12345L"
至此Long类型的参数出现上述问题的根因找出,解决方法也就明了了。
方式一:传递Long类型参数时不要时L标注参数类型,直接传递相应的数值即可。如下示例:
### 测试long类型
GET http://localhost:9090/v1/hello/showLong/12345
Accept: application/json
方式二:直接写一个实现java.beans.PropertyEditor接口或者继承com.sun.beans.editors.NumberEditor实现一个自定义的Editor,将字符串后面的L或者l给删除后在使用Long.decode()和Long.valueOf()方法。
情况二:获取java.util.Date类型的参数
1、使用@PathVariable注解取路径中的参数值
Java代码如下:
@RestController
@RequestMapping(path = "v1/hello/")
public class HelloController {
@GetMapping(path = "showDate/{initDate}")
public Date showDate(@PathVariable Date initDate) {
return initDate;
}
}
请求如下:
### 测试Date类型
GET http://localhost:9090/v1/hello/showDate/2021-01-01 14:30:34
Accept: application/json
代码跟踪路径
org.springframework.validation.DataBinder#convertIfNecessary(java.lang.Object, java.lang.Class<T>, org.springframework.core.MethodParameter)
---->
org.springframework.beans.TypeConverterSupport#convertIfNecessary(java.lang.Object, java.lang.Class<T>, org.springframework.core.MethodParameter)
---->
org.springframework.beans.TypeConverterSupport#convertIfNecessary(java.lang.Object, java.lang.Class<T>, org.springframework.core.convert.TypeDescriptor)
---->
org.springframework.beans.TypeConverterDelegate#convertIfNecessary(java.lang.String, java.lang.Object, java.lang.Object, java.lang.Class<T>, org.springframework.core.convert.TypeDescriptor)
---->
org.springframework.core.convert.support.GenericConversionService#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)
---->
org.springframework.core.convert.support.ConversionUtils#invokeConverter
---->差异步骤,每个GenericConverter中的convert方法
org.springframework.core.convert.support.ObjectToObjectConverter#convert
默认配置的情况下看流程都是都一个模版,一个差异为如下方法返回的对象不同。
org.springframework.core.convert.support.GenericConversionService#getConverter
这个方法方法返回不同的org.springframework.core.convert.converter.GenericConverter类型。例如当前这种传递参数是java.lang.String类型,而接受类型是java.util.Date类型,其返回的结果是
org.springframework.core.convert.support.ObjectToObjectConverter
在ObjectToObjectConverter对象中的convert方法中通过反射获取了一个Date(String str)的一个构造器,而java.util.Date没有相应的构造器,从而导致出现java.lang.IllegalArgumentException。
所以这种情况我目前只知道使用官网提供的使用@InitBinder注解注册一个org.springframework.beans.propertyeditors.CustomDateEditor,当然也可以是自己实现的。
解决方案代码如下:
@RestController
@RequestMapping(path = "v1/hello/")
public class HelloController {
private static final DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Date.class, new CustomDateEditor(df, false));
}
@GetMapping(path = "showDate/{initDate}")
public Date showDate(@PathVariable Date initDate) {
return initDate;
}
}
使用@InitBinder注解后其执行流程就合上述的Long类型一致了,这里就不再介绍一次了。需要注意的是传递的参数格式必须和DateFormat的pattern的格式一致,不然也是会报错。
2、使用@RequestBody注解时POJO对象中有java.util.Date类型的属性
Java代码如下:(前提是引入了jackson的相关核心依赖)
package org.jackson.mvc.web.controllers.hello;
import org.jackson.mvc.web.po.RequestInfo;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping(path = "v1/hello/")
public class HelloController {
@PostMapping(path = "showRequestInfo")
public RequestInfo showRequestInfo(@RequestBody RequestInfo requestInfo) {
return requestInfo;
}
}
RequestInfo定义如下:
package org.jackson.mvc.web.po;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
public class RequestInfo implements Serializable {
private String username;
private Long userId;
private int userAge;
private double userHeight; // 身高
private Date birthday;
private BigDecimal balance; // 余额
// 省略get和set方法
}
请求定义如下:
### 测试Pojo类型
POST http://localhost:9090/v1/hello/showRequestInfo
Content-Type: application/json
{
"username" : "java",
"userId" : "123455",
"userAge" : "23",
"userHeight" : "170.5",
"birthday" : "2020-12-19 12:12:12",
"balance" : "20.55"
}
代码跟踪关键方法栈如下:
org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#read
--->
com.fasterxml.jackson.databind.ObjectMapper#_readMapAndClose
--->
com.fasterxml.jackson.databind.deser.BeanDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
--->
com.fasterxml.jackson.databind.deser.BeanDeserializer#deserializeFromObject
--->
com.fasterxml.jackson.databind.deser.impl.MethodProperty#deserializeAndSet
在转换传递参数的birthday属性报错,相应的调用方法如下:
com.fasterxml.jackson.databind.deser.std.DateDeserializers.DateBasedDeserializer#_parseDate
报错信息如下:
Cannot deserialize value of type `java.util.Date` from String "2020-12-19 12:12:12": not a valid representation (error: Failed to parse Date value '2020-12-19 12:12:12': Cannot parse date "2020-12-19 12:12:12": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSX', parsing fails (leniency? null))
跟踪发现com.fasterxml.jackson.databind.ObjectMapper中默认创建的的DateFormat是com.fasterxml.jackson.databind.util.StdDateFormat,该对象的pattern为:
yyyy-MM-dd'T'HH:mm:ss.SSSX
看到这里,我们的一个解决方案就是按照com.fasterxml.jackson.databind.util.StdDateFormat中的pattern格式传递时间参数,例如需改请求为:
### 测试Pojo类型
POST http://localhost:9090/v1/hello/showRequestInfo
Content-Type: application/json
{
"username" : "java",
"userId" : "123455",
"userAge" : "23",
"userHeight" : "170.5",
"birthday" : "2021-01-01T22:43:56.145+08",
"balance" : "20.55"
}
响应结果如下:
POST http://localhost:9090/v1/hello/showRequestInfo
HTTP/1.1 200
Content-Type: application/xml;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 01 Jan 2021 14:44:33 GMT
Keep-Alive: timeout=20
Connection: keep-alive
<RequestInfo>
<username>java</username>
<userId>123455</userId>
<userAge>23</userAge>
<userHeight>170.5</userHeight>
<birthday>1609512236145</birthday>
<balance>20.55</balance>
</RequestInfo>
但是这显然不是我们想要的结果,并且也不推荐这样。
解决方案二:使用@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解
修改RequestInfo对象信息,修改后代码如下:
package org.jackson.mvc.web.po;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
public class RequestInfo implements Serializable {
private String username;
private Long userId;
private int userAge;
private double userHeight; // 身高
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date birthday;
private BigDecimal balance; // 余额
// 省略get/set方法
}
请求信息如下:(注意:必须保证日期参数的格式和定义的pattern保持一致)
### 测试Pojo类型
POST http://localhost:9090/v1/hello/showRequestInfo
Content-Type: application/json
{
"username" : "java",
"userId" : "123455",
"userAge" : "23",
"userHeight" : "170.5",
"birthday" : "2021-01-01 22:43:56",
"balance" : "20.55"
}
响应结果如下:
<RequestInfo>
<username>java</username>
<userId>123455</userId>
<userAge>23</userAge>
<userHeight>170.5</userHeight>
<birthday>2021-01-01 22:43:56</birthday>
<balance>20.55</balance>
</RequestInfo>
使用@JsonFormat注解虽然可以确保时间格式的一致,但是需要在相应的java.util.Date属性上进行配置。一个系统中使用java.util.Date类型作为属性的地方不可能只有几个。当需求确实需要特定的时间格式时可以使用这个注解,通常情况下我们还是希望系统中所有的java.util.Date属性都使用统一的格式进行转换。使用方案三可以做到这点。
方案三:重写configureMessageConverters()方法,替换SpringMVC默认创建的。
方法全路径信息如下:
org.springframework.web.servlet.config.annotation.WebMvcConfigurer#configureMessageConverters(List<HttpMessageConverter<?>> converters)
Java代码如下:
package org.jackson.mvc.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.annotation.WebFilter;
import java.text.SimpleDateFormat;
import java.util.List;
@Configuration
@ComponentScan(basePackages = {"org.jackson.mvc.web"},
excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {WebFilter.class})})
@EnableWebMvc
public class ServletConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter =
new MappingJackson2HttpMessageConverter();
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
.indentOutput(true)
.dateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));
converters.add(mappingJackson2HttpMessageConverter);
}
}
配置后容器中MessageConverter的列表信息为:
0 = {[email protected]}
1 = {[email protected]}
2 = {[email protected]}
重新修改RequestInfo对象信息,修改后代码如下:
package org.jackson.mvc.web.po;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
public class RequestInfo implements Serializable {
private String username;
private Long userId;
private int userAge;
private double userHeight; // 身高
private Date birthday;
private BigDecimal balance; // 余额
// 省略get和set方法
}
请求信息如下:
### 测试Pojo类型
POST http://localhost:9090/v1/hello/showRequestInfo
Content-Type: application/json
{
"username" : "java",
"userId" : "123455",
"userAge" : "23",
"userHeight" : "170.5",
"birthday" : "2021-01-01 22:43:56",
"balance" : "20.55"
}
响应信息如下:
POST http://localhost:9090/v1/hello/showRequestInfo
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 01 Jan 2021 15:16:23 GMT
Keep-Alive: timeout=20
Connection: keep-alive
{
"username": "java",
"userId": 123455,
"userAge": 23,
"userHeight": 170.5,
"birthday": "2021-01-01 22:43:56",
"balance": 20.55
}
使用的jackson依赖信息如下:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-cbor</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>${jackson.version}</version>
</dependency>
以上信息是我个人在学习过程中遇到的一些问题,以及自己查阅资料和调试部分代码后的一些个人观点,有错误的地方欢迎指正。