1. 问题
测试提了一个 bug,使用 API 接口进行认证时,传入的请求参数类型错误,但认证成功了。
POST 请求,body 参数如下:
{
"username": "test",
"password": 111111
}
测试预期:参数校验失败,提示 “password” 只支持字符串类型。
实际:认证成功了。
原有的处理逻辑如下:
public class Test {
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
Auth auth = mapper.readValue("{\"username\":\"test\",\"password\":111111}", Auth.class);
System.out.println(auth);
}
public static class Auth {
private String username;
private String password;
public Auth() {
}
public Auth(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "Auth{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
}
2. 分析
分析:
预期,Auth auth = mapper.readValue("{“username”:“test”,“password”:111111}", Auth.class); 执行过程会抛出类型强转异常,因为实际传入的 password 值是 int 类型,但 Auth 的 password 类型定义的是 String。
但是,结果却并非如此,反序列化成功了,就说明是 ObjectMapper 在反序列化的时候,内部做了特殊处理,把 int 自动转换成了 String。
分析 ObjectMapper 源码,
@JacksonStdImpl
public class StringDeserializer extends StdScalarDeserializer<String> // non-final since 2.9
{
.....
@Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
{
if (p.hasToken(JsonToken.VALUE_STRING)) {
return p.getText();
}
JsonToken t = p.currentToken();
// [databind#381]
if (t == JsonToken.START_ARRAY) {
return _deserializeFromArray(p, ctxt);
}
// need to gracefully handle byte[] data, as base64
if (t == JsonToken.VALUE_EMBEDDED_OBJECT) {
Object ob = p.getEmbeddedObject();
if (ob == null) {
return null;
}
if (ob instanceof byte[]) {
return ctxt.getBase64Variant().encode((byte[]) ob, false);
}
// otherwise, try conversion using toString()...
return ob.toString();
}
// 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML)
if (t == JsonToken.START_OBJECT) {
return ctxt.extractScalarFromObject(p, this, _valueClass);
}
// allow coercions for other scalar types
// 17-Jan-2018, tatu: Related to [databind#1853] avoid FIELD_NAME by ensuring it's
// "real" scalar
if (t.isScalarValue()) {
String text = p.getValueAsString();
if (text != null) {
return text;
}
}
return (String) ctxt.handleUnexpectedToken(_valueClass, p);
}
......
可以看到,关键的处理逻辑在这,
if (t.isScalarValue()) {
String text = p.getValueAsString();
if (text != null) {
return text;
}
}
当目标值是 String 类型时,会把标量值转换为 String 进行处理。
所以,只需要修改这块逻辑,不做转换即可。
3. 解决
解决方案:
自定义 StringDeserizlizer ,重写 deserializer 方法即可。
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new SimpleModule().addDeserializer(String.class, new StringDeserializer() {
@Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
if (p.hasToken(JsonToken.VALUE_STRING)) {
return p.getText();
}
throw new IllegalArgumentException("Unsupported type," + p.currentName() + " only support String type");
}
}));
Auth auth = mapper.readValue("{\"username\":\"test\",\"password\":111111}", Auth.class);
System.out.println(auth);
}