Jackson原理探究—Mixins其一

Sivan發表於2022-03-27

使用場景

Jackson mixins 用來實現在目標類的宣告以及定義的情況下,實現 Jackson 的註解新增至目標類的效果。尤其我們在使用第三方類庫的時候,這種機制就會顯得尤為有用。

接下來我們展示一些實際的使用場景示例:

以 spring security 為例

假設我們要反序列化這個類:

package org.springframework.security.web.csrf;
······
public final class DefaultCsrfToken implements CsrfToken {

    private final String token;

    private final String parameterName;

    private final String headerName;

    ······
    public DefaultCsrfToken(String headerName, String parameterName, String token) {
        Assert.hasLength(headerName, "headerName cannot be null or empty");
        Assert.hasLength(parameterName, "parameterName cannot be null or empty");
        Assert.hasLength(token, "token cannot be null or empty");
        this.headerName = headerName;
        this.parameterName = parameterName;
        this.token = token;
    }
    ······
}
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No dserializer found for class org.springframework.security.web.csrf and no properties discovered to create BeanDserializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

我們無法反序列化這兩個類,因為這是一個典型的不可變類,所有屬性欄位在構造器中完成初始化。而jackson 預設的反序列化策略需要一個無參的構造器,並提供欄位的內省 (注1) 函式。如果要更改反序列化策略,jackson 需要我們在物件上增加對應的註解。

現在讓我們來建立一個 mixin 類來解決這個問題!在我們的 mixin 中,我們將宣告我們反序列化所需要的註解:

package org.springframework.security.web.jackson2;
······
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
@JsonIgnoreProperties(ignoreUnknown = true)
class DefaultCsrfTokenMixin {
    ······
    @JsonCreator
    public DefaultCsrfTokenMixin(@JsonProperty("headerName") String headerName,
                                @JsonProperty("parameterName") String parameterName, @JsonProperty("token") String token) {
    }
}

我們建立了一個 mixin 類,併為其宣告瞭 typeInfo 資訊以及要用到的構造器資訊

之後,我們需要告訴 Jackson 使用我們的 Mixin。為此,我們需要通過 jackson 的 Module 擴充套件機制,實現自己的擴充套件器,告訴 jackson 需要使用我們的 mixin:

/**
 * Jackson module for spring-security-web. This module register {@link DefaultCsrfTokenMixin} and
 * {@link PreAuthenticatedAuthenticationTokenMixin}. If no default typing enabled by default then it'll enable
 * it because typing info is needed to properly serialize/deserialize objects.
 * In order to use this module just add this module into your ObjectMapper configuration.
 *
 * <pre>
 *     ObjectMapper mapper = new ObjectMapper();
 *     mapper.registerModule(new WebJackson2Module());
 * </pre>
 * <b>Note: use {@link SecurityJackson2Modules#getModules(ClassLoader)} to get list of all security modules.</b>
 *
 * @author Jitendra Singh
 * @see SecurityJackson2Modules
 * @since 4.2
 */
public class WebJackson2Module extends SimpleModule {

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

    @Override
    public void setupModule(SetupContext context) {
        SecurityJackson2Modules.enableDefaultTyping(context.getOwner());
        context.setMixInAnnotations(DefaultCsrfToken.class, DefaultCsrfTokenMixin.class);
        context.setMixInAnnotations(PreAuthenticatedAuthenticationToken.class, PreAuthenticatedAuthenticationTokenMixin.class);
    }
}

完成!

這段程式碼中,摘取自 org.srpingframework.security:spring-security-web:5.1.5.RELEASE

這個示例中,spring security 的設計者面對了一個典型的擴充套件性問題:
早在接入 jackson 之前, spring security 就已經完成了 DefaultCsrfToken 的設計,但是一開始的寫法卻不能很好的相容 jackson 的反序列化策略。

此時萬幸,jackson 提供了 mixins 機制,這個可以支援外掛式的序列化/反序列化策略宣告,從而避免了針對 spring security 核心資料結構的侵入性改變。不得不說,設計的真的很優良!

總結

mixins 機制的使用如上所述,以使用者角度來說,非常的簡便,完全是宣告式的。而且更重要的是,完全不需要對原來的資料結構設計做任何侵入性改變,這就太重要了,確保了我們的程式碼足夠乾淨,並可以便捷開放給其他 JSON 庫,例如 GSON。

那麼 mixins 實際上又是如何實現的呢?請聽下回分解:預留傳送門

備註

注1: 內省:introspector ,java 中指代 bean 的 getter/setter 規範

相關文章