Help us understand the problem. What is going on with this article?

Jackson に独自アノテーションへの処理を追加する

More than 3 years have passed since last update.

Jackson でシリアライズしてログに残すときに、認証トークンみたいなセンシティブなデータは潰したいし、それをアノテーションでお手軽に指定したいって思ったので調べた。

やりたいこと

こんな感じで POOJO のフィールドに @Sensitive って付けたプロパティは適当な文字列に置換して欲しい:

@lombok.Value
public class SomeRequest {
    /** 隠す必要無いプロパティ */
    private final String userId;

    /** センシティブな情報 */
    @Sensitive
    private final String token;
}

やり方

置換処理の実装と、その実装が働くアノテーションへの関連付けと、それらを ObjectMapper に登録するモジュールの3つのクラスが必要。

アノテーションがついたフィールドに対する処理をするクラス:

SensitiveFieldMaskingSerializer.java
public class SensitiveFieldMaskingSerializer extends StdSerializer<Object> {
    private static final long serialVersionUID = 3888199957574169748L;

    protected SensitiveFieldMaskingSerializer() {
        super(Object.class);
    }

    @Override
    public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        // 適当に置換する処理
        if (value instanceof Number) {
            gen.writeNumber(0);
        } else if (value instanceof String) {
            gen.writeString("Hidden: Sensitive string");
        } else {
            gen.writeNull();
        }
    }
}

↑の実装が起動するアノテーションを関連付けるクラス:

SensitiveFieldMaskingAnnotationIntrospector.java
public class SensitiveFieldMaskingAnnotationIntrospector extends NopAnnotationIntrospector {
    private static final long serialVersionUID = -4171975975956047379L;

    @Override
    public Object findSerializer(Annotated am) {
        if (am.hasAnnotation(Sensitive.class)) {
            return SensitiveFieldMaskingSerializer.class;
        }
        return null;
    }
}

↑を ObjectMapper に登録する処理クラス:

SensitiveFieldMaskingModule.java
static class SensitiveFieldMaskingModule extends Module {
    @Override
    public String getModuleName() {
        return getClass().getSimpleName();
    }

    @Override
    public Version version() {
        return Version.unknownVersion();
    }

    @Override
    public void setupModule(SetupContext context) {
        context.insertAnnotationIntrospector(new SensitiveFieldMaskingAnnotationIntrospector());
    }
}

これで必要な実装のすべてなので使ってみましょう:

public final class Main {
    public static void main(String[] args) throws Exception {
        ObjectMapper logObjectMapper = new ObjectMapper()
                .registerModule(new SensitiveFieldMaskingModule());

        FooRequest fooRequest = new FooRequest("ログに残していいやつ", "ログに残すのダメなやつ");

        System.out.println("シリアライズ結果: " + logObjectMapper.writeValueAsString(fooRequest));
        // シリアライズ結果: {"userId":"ログに残していいやつ","token":"Hidden: Sensitive string"}

        System.out.println("Map へ変換結果:  " + logObjectMapper.convertValue(fooRequest, Map.class));
        // Map へ変換結果:  {userId=ログに残していいやつ, token=Hidden: Sensitive string}
    }
}

両出力とも token が別の文字列に置換されているのがわかる。

おわり

上にもリンク張ったけど、一通り動くやつを gist に置いた。

余談だけど、今回のように通常用途とは違う ObjectMapper が必要な時は、他所で使ってる ObjectMapper を使いまわさず、新たにログを残すためのものとして用意したほうが無用なトラブルを避けられる。DI コンテナを使っている場合は面倒くさがらず名前付き Bean として定義しよう。

k_ui
ねこほしい
http://k-ui.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away