首先我还是以一个我遇到的新的问题来引发相关的讨论:
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value = "SystemUser对象", description = "")
public class SystemUser implements Serializable, UserDetails {
........省略一些私有的字段
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
// private void setAuthorities(Collection extends GrantedAuthority> ignored){} 没有解决反序列化问题
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled==0;
}
}
在这个代码片段中,我使用了User实体类去实现了SpringSecurity中的UserDetails接口,为了方便,将用户表的实现类同时作为安全框架中用户的详细信息类,可以看到在UserDetails接口源码中主要有五个方法:
```java
public interface UserDetails extends Serializable {
//获取用户权限列表,且返回的权限必须带有"ROLE_"开头
Collection extends GrantedAuthority> getAuthorities();
//获取密码
String getPassword();
//获取用户名
String getUsername();
//账户未过期
boolean isAccountNonExpired();
//账户未锁定
boolean isAccountNonLocked();
//凭证是否过期
boolean isCredentialsNonExpired();
//用户是否启用
boolean isEnabled();
}
```
当我实现接口时,同样在SystemUser类中也重写了这五个方法,且以public修饰,而我们的用户类SystemUser本身是需要在网络中传输的,所以它实现了序列化接口Serializable,到目前为止这看似没有任何问题。
但是当我写好了用户的添加接口,使用swagger进行添加用户操作时就出问了:看看请求体的内容
{
"accountNonExpired": true,
"accountNonLocked": true,
"authorities": [
{
"authority": "string"
}
],
"createTime": "2022-05-06T10:54:48.786Z",
"credentialsNonExpired": true,
"email": "290789798@qdqoidh",
"enabled": 0,
"headPortrait": "/loacalhost/protrait/",
"id": 1,
"lastPasswordResetTime": "2022-05-06T10:54:48.786Z",
"nickName": "张三",
"password": "123456",
"phone": "18324182141",
"sex": "男",
"username": "zkboort"
}
首先我们发现请求对象中,它多了三个属性,分别对应了UserDetails的用户未过期、用户未锁定、和一个用户权限列表数组;其他的属性都是我们用户实体类的属性。当我们填好信息点击测试接口时,我却得到了如下的返回结果:
{
"timestamp": "2022-05-06T13:56:51.675+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/systemUser/addSystemUser"
}
状态码是500,说明是后端的问题,我们将目光回到后台控制台看看信息:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Problem deserializing 'setterless' property 'authorities': get method returned null
at [Source: (PushbackInputStream); line: 4, column: 18] (through reference chain: com.example.carleasingclub_monolith.entity.SystemUser["authorities"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1764) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.deser.impl.SetterlessProperty.deserializeAndSet(SetterlessProperty.java:130) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:402) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:195) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4593) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3601) ~[jackson-databind-2.12.5.jar:2.12.5]
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:378) ~[spring-web-5.3.12.jar:5.3.12]
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:342) ~[spring-web-5.3.12.jar:5.3.12]
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:185) ~[spring-webmvc-5.3.12.jar:5.3.12]
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:160) ~[spring-webmvc-5.3.12.jar:5.3.12]
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:133) ~[spring-webmvc-5.3.12.jar:5.3.12]
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121) ~[spring-web-5.3.12.jar:5.3.12]
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:179) ~[spring-web-5.3.12.jar:5.3.12]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:146) ~[spring-web-5.3.12.jar:5.3.12]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-5.3.12.jar:5.3.12]
此时遇到一个奇怪的序列化转换失败的问题,根据信息'setterless' property 'authorities': get method returned null,我大致猜测是由于权限数组的反序列化问题,"serterless“表明可能与set()和get()方法有关,这里的反序列化是由jackson去做的,所以在分析为什么这样做会出现问题之前,需要了解jsckson。
Jackson是一个用于实现javaBean对象与JSON数据格式相互转换的工具,这样的常见的工具有四个:Jsonlib,Gson,fastjson,jackson。jackson使用其内部的readValue(json对象)方法将json数据格式转化为java对象,writeValue(参数1,Obj)将Obj对象转换为json对象并以参数1(File,Writer,OutPutString)指定的形式输出。
jackson在做java对象转json数据格式时,要求Java对象是一个POJO,即javaBean对象,在这个javaBean对象当中的字段如果是使用private进行修饰的话,则必须有对应的getXXX()方法,否则使用public修饰。
**jackson将javaBean对象中的每一个字段属性映射为json的key,而将字段属性的getXXX()和setXXX()方法用来决定json的value**。json对象的值大致可以分为基本类型、数组、字典,他们应该分别对应java对象的基本类型、数组、链表。
jackson的核心类是class ObjectMapper,其也实现了Versioned, Serializable 两个接口,这个类有许多的属性:
public class ObjectMapper extends ObjectCodec implements Versioned, Serializable {
private static final long serialVersionUID = 2L;
//
protected static final AnnotationIntrospector DEFAULT_ANNOTATION_INTROSPECTOR = new JacksonAnnotationIntrospector();
//
protected static final BaseSettings DEFAULT_BASE;
//
protected final JsonFactory _jsonFactory;
//
protected TypeFactory _typeFactory;
//
protected InjectableValues _injectableValues;
//
protected SubtypeResolver _subtypeResolver;
//
protected final ConfigOverrides _configOverrides;
//
protected final CoercionConfigs _coercionConfigs;
//
protected SimpleMixInResolver _mixIns;
//
protected SerializationConfig _serializationConfig;
//
protected DefaultSerializerProvider _serializerProvider;
//
protected SerializerFactory _serializerFactory;
//
protected DeserializationConfig _deserializationConfig;
//
protected DefaultDeserializationContext _deserializationContext;
//
protected Set _registeredModuleTypes;
//
protected final ConcurrentHashMap
}
了解了jackson基本的特征后,针对上面的将json请求体的内容转换为java对象的反序列化问题,我们知道是请求体中的”authorities“数组,在实体类中缺少对应的setter方法导致反序列失败。
@Override public Collection extends GrantedAuthority> getAuthorities() { return null; }
事实上最有可能的问题是,getAuthorites()方法指定返回的是一个权限Collection<>,返回值 return null有没有问题呢?,在通常情况下这其实没有什么问题,但是这里不能这样,jackson的key有相应的对应,java的Collection集合对象在前端json数据格式中对应的是一个数组对象,我们使用反序列化将json请求体中的元素:
“authorities”: [
{
“authority”: “string”
}
],
反序列化为Java对象时,如果Java实体类中的方法最终返回的是null值,他与前端的数组对象是不匹配的,所以真正的问题并不在于我们的反序列化方法依赖于javaBean对象的setter,而是此处的返回值导致的反序列化类型不匹配问题。
根据 UserDetails接口的getAuthorities()方法,我们的返回值应当是一个权限列表集合,且集合的元素皆是以”ROLE_“开头,在开发此用户相关的业务时,应当将权限角色等提前开发好,用户对象中具备权限相关的属性,保证当前用户对象可以获取到权限属性列表,
并且,重写Overide public Collection extends GrantedAuthority> getAuthoritesa()方法时,需要在方法体中去获取当前用户对象的权限列表,以List<>,接收后返回。
这样在测试的时候才能,带上当前请求的数组参数,并且成功反序列化为Java集合对象。