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

  • 0
    いいね
  • 0
    コメント

    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 として定義しよう。