Edited at

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

More than 1 year has 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 として定義しよう。