天天看点

使用PathVariable注解获取路径中的参数出现HTTP状态 400 - 错误的请求

参考地址: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>
           

以上信息是我个人在学习过程中遇到的一些问题,以及自己查阅资料和调试部分代码后的一些个人观点,有错误的地方欢迎指正。