解决实体类中的接口字段序JSON列化异常:com.fasterxml.jackson.databind.exc.InvalidDefinitionException

比如我们有一个最简单的UserDetails实现类:

@Data
public class MyUser implements UserDetails {
    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;
    private boolean accountNonExpired;
    private boolean accountNonLocked;
    private boolean credentialsNonExpired;
    private boolean enabled;
}

编写一个测试

@Test
void test_2022_01_16_18_31_35() throws JsonProcessingException {
    ObjectMapper objectMapper = new ObjectMapper();
    MyUser user = new MyUser();
    user.setUsername("闰土");
    user.setPassword("cha");
    user.setAuthorities(Collections.singletonList(new SimpleGrantedAuthority("ADMIN")));
    String jsonStr = objectMapper.writeValueAsString(user);
    System.out.println(jsonStr);
    objectMapper.readValue(jsonStr, MyUser.class);
}

输出为

image.png

{"username":"闰土","password":"cha","authorities":[{"authority":"ADMIN"}],"accountNonExpired":false,"accountNonLocked":false,"credentialsNonExpired":false,"enabled":false}

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.springframework.security.core.GrantedAuthority` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
 at [Source: (String)"{"username":"闰土","password":"cha","authorities":[{"authority":"ADMIN"}],"accountNonExpired":false,"accountNonLocked":false,"credentialsNonExpired":false,"enabled":false}"; line: 1, column: 50] (through reference chain: com.example.testdemo.MyUser["authorities"]->java.util.ArrayList[0])
...

进程已结束,退出代码为 -1

虽然MyUser可以被正确序列化,但是无法被正确反序列化,这是因为jackson在反序列化时,遇到 GrantedAuthority 这个接口字段,不知道选择哪个实现类去构造对象。

当然我们也可以直接使用实现类如 SimpleGrantedAuthority 。但实际上 SimpleGrantedAuthority 本身也会引起同样的报错,这是因为 SimpleGrantedAuthority 没有默认无参构造器。这时候我们就需要Mixin或者@JsonDeserialize了

巧的是,springSecurity本身内置了一个MixIn org.springframework.security.jackson2.SimpleGrantedAuthorityMixin

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE,
      getterVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY, isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
public abstract class SimpleGrantedAuthorityMixin {

   /**
    * Mixin Constructor.
    * @param role the role
    */
   @JsonCreator
   public SimpleGrantedAuthorityMixin(@JsonProperty("authority") String role) {
   }

}

我们使用这个Mixin并将字段改为实现类试试

@Data
public class MyUser2 implements UserDetails {
    private String username;
    private String password;
    private Collection<SimpleGrantedAuthority> authorities;
    private boolean accountNonExpired;
    private boolean accountNonLocked;
    private boolean credentialsNonExpired;
    private boolean enabled;
}

我们在ObjectMapper 添加一行 objectMapper.addMixIn(SimpleGrantedAuthority.class,SimpleGrantedAuthorityMixin.class);

@Test
void test_2022_01_16_18_31_35() throws JsonProcessingException {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.addMixIn(SimpleGrantedAuthority.class,SimpleGrantedAuthorityMixin.class);
    MyUser2 user = new MyUser2();
    user.setUsername("闰土");
    user.setPassword("cha");
    user.setAuthorities(Collections.singletonList(new SimpleGrantedAuthority("ADMIN")));
    String jsonStr = objectMapper.writeValueAsString(user);
    System.out.println(jsonStr);
    objectMapper.readValue(jsonStr, MyUser2.class);
}
{"username":"闰土","password":"cha","authorities":[{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"ADMIN"}],"accountNonExpired":false,"accountNonLocked":false,"credentialsNonExpired":false,"enabled":false}

进程已结束,退出代码为 0

MyUser2正确序列化和反序列化了,但是json字符串里多了一个 @class 的属性,这是因为官方提供的Mixin中有一个注解 @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) 这会将类的完全限定名作为属性包括在内,如果有多个GrantedAuthority 输出就会像

{
    "username": "闰土",
    "password": "cha",
    "authorities": [
        {
            "@class": "org.springframework.security.core.authority.SimpleGrantedAuthority",
            "authority": "ADMIN"
        },
        {
            "@class": "org.springframework.security.core.authority.SimpleGrantedAuthority",
            "authority": "ADMIN2"
        },
        {
            "@class": "org.springframework.security.core.authority.SimpleGrantedAuthority",
            "authority": "ADMIN3"
        }
    ],
    "accountNonExpired": false,
    "accountNonLocked": false,
    "credentialsNonExpired": false,
    "enabled": false
}

如果我们想要去除这个 @class 或者像MyUser直接使用 GrantedAuthority 接口来兼容不同实现类,并且要求反序列化的时候根据属性自动转换为不同的实现类,我们需要一个自定义的 JsonDeserializer

比如我们需要自动反序列化 OidcUserAuthorityOAuth2UserAuthority

我们知道 OidcUserAuthority 有区别于其他实现类独有的字段 userInfo
OAuth2UserAuthority 有区别于其他实现类独有的字段 attributes (OidcUserAuthority也有,所以我们需要按照先后顺序检测)

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.util.ClassUtils;

import java.io.IOException;
import java.util.*;

/**
 * 通用GrantedAuthority反序列化器,用于自动序列化其实现类
 */
@Slf4j
public class GenericAuthorityJsonDeserializer extends JsonDeserializer<GrantedAuthority> {
    private static final Map<String, TypeReference<? extends GrantedAuthority>> GrantedAuthorityTypes = new LinkedHashMap<>();
    private static final TypeReference<SimpleGrantedAuthority> defaultTypeReference = new TypeReference<SimpleGrantedAuthority>() {};

    static {
        ClassLoader classLoader = GenericAuthorityJsonDeserializer.class.getClassLoader();
        if (ClassUtils.isPresent("org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority", classLoader)) {
            GrantedAuthorityTypes.put("idToken", new TypeReference<OidcUserAuthority>() {});
        }
        if (ClassUtils.isPresent("org.springframework.security.oauth2.core.user.OAuth2UserAuthority", classLoader)) {
            GrantedAuthorityTypes.put("attributes", new TypeReference<OAuth2UserAuthority>() {});
        }
        GrantedAuthorityTypes.put("authority", defaultTypeReference);
    }

    @Override
    public GrantedAuthority deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {
        ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();
        ObjectNode authorityNode = mapper.readTree(jsonParser);
        return getGrantedAuthority(mapper, authorityNode);
    }

    @SuppressWarnings("unchecked")
    private <T extends GrantedAuthority> T getGrantedAuthority(ObjectMapper mapper, ObjectNode authorityNode) {
        for (String key : GrantedAuthorityTypes.keySet()) {
            if (authorityNode.has(key)) {
                return mapper.convertValue(authorityNode, (TypeReference<T>) GrantedAuthorityTypes.get(key));
            }
        }
        return mapper.convertValue(authorityNode, (TypeReference<T>) defaultTypeReference);
    }
}

这个 GenericAuthorityJsonDeserializer 的作用就是检测是否有其特别字段来自动选择实现类,如果找不到就选择 SimpleGrantedAuthority 作为默认实现类

然后我们实现一个 SimpleModule 可以将反序列化和mixin一同注册,由于 GenericAuthorityJsonDeserializer 解析的是接口,所以不能以Mixin类上加 @JsonDeserialize(using = GenericAuthorityJsonDeserializer.class) 注解的方式注册,只能在JacksonModule中注册或者直接把 @JsonDeserialize(using = GenericAuthorityJsonDeserializer.class) 放在对应字段上,否则会导致无限循环

public class GrantedAuthorityJacksonModule extends SimpleModule {
    public GrantedAuthorityJacksonModule() {
        super(GrantedAuthorityJacksonModule.class.getName(), new Version(1, 0, 0, null, null, null));
    }

    @Override
    public void setupModule(SetupContext context) {
        ClassLoader classLoader = GrantedAuthorityJacksonModule.class.getClassLoader();
        context.addDeserializers(GrantedAuthority.class,GenericAuthorityJsonDeserializer.class);
        if (ClassUtils.isPresent("org.springframework.security.oauth2.core.user.OAuth2UserAuthority", classLoader)) {
            context.setMixInAnnotations(OAuth2UserAuthority.class, OAuth2UserAuthorityMixin.class);
        }
        if (ClassUtils.isPresent("org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority", classLoader)) {
            context.setMixInAnnotations(OidcUserAuthority.class, OidcUserAuthorityMixin.class);
        }
        if (ClassUtils.isPresent("org.springframework.security.core.authority.SimpleGrantedAuthority", classLoader)) {
            context.setMixInAnnotations(SimpleGrantedAuthority.class, SimpleGrantedAuthorityMixin.class);
        }
    }

    @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
            isGetterVisibility = JsonAutoDetect.Visibility.NONE)
    @JsonIgnoreProperties(ignoreUnknown = true)
    abstract static class OAuth2UserAuthorityMixin {

        @JsonCreator
        OAuth2UserAuthorityMixin(@JsonProperty("authority") String authority,
                                 @JsonProperty("attributes") Map<String, Object> attributes) {
        }

    }

    @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
            isGetterVisibility = JsonAutoDetect.Visibility.NONE)
    @JsonIgnoreProperties(value = {"attributes"}, ignoreUnknown = true)
    abstract static class OidcUserAuthorityMixin {

        @JsonCreator
        OidcUserAuthorityMixin(@JsonProperty("authority") String authority, @JsonProperty("idToken") OidcIdToken idToken,
                               @JsonProperty("userInfo") OidcUserInfo userInfo) {
        }

    }

    @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE,
            getterVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY, isGetterVisibility = JsonAutoDetect.Visibility.NONE)
    @JsonIgnoreProperties(ignoreUnknown = true)
    abstract static class SimpleGrantedAuthorityMixin {

        @JsonCreator
        public SimpleGrantedAuthorityMixin(@JsonProperty("authority") String role) {
        }

    }

}

这样我们通过注册这个模块,就可以正确的自动选择实现类来序列化了

@Test
void test_2022_01_16_18_31_35() throws JsonProcessingException {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(new GrantedAuthorityJacksonModule());
    MyUser user = new MyUser();
    user.setUsername("闰土");
    user.setPassword("cha");
    user.setAuthorities(Arrays.asList(new SimpleGrantedAuthority("ADMIN"), new SimpleGrantedAuthority("ADMIN2"), new SimpleGrantedAuthority("ADMIN3")));
    String jsonStr = objectMapper.writeValueAsString(user);
    System.out.println(jsonStr);
    objectMapper.readValue(jsonStr, MyUser.class);
}

在springboot中,我们可以这样注册我们的模块:

@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
    return builder -> builder.modules(new GrantedAuthorityJacksonModule());
}

作者:木原金
链接:实体类字段为接口的json序列化报错的解决方法 以 SpringSecurity UserDetails实现类 GrantedAuthority 为例 - 掘金