LoginSignup
5
0

More than 3 years have passed since last update.

オブジェクトを再帰的に文字列シリアライズするとき、出力したくないメンバーをアノテーションを使って設定できるようにする方法

Last updated at Posted at 2019-11-22

オブジェクトの中身をまとめて文字列にして、ログに出力したいとかありますよね。
そのとき、このフィールドはログに出したくない!とかありませんか?
わざわざtoString()を実装するのは、ちょっとめんどくさいですよね。

やりたいこと

  • オブジェクトの内容を文字列シリアライズしたい
  • オブジェクトのフィールドには、MapとかListとか他のオブジェクトも入っている
  • オブジェクトの中で、任意のフィールドは文字列シリアライズ対象外にしたい

オブジェクトの内容

Cart.java
package jp.co.pmtech.iwata;

public class Cart {
    /** カートID */
    private int id;
    /** カートの中の商品 */
    private List<Syohin> syohinList;

    // getterとかsettertとか
}
Syohin.java
package jp.co.pmtech.iwata;

public class Syohin {
    /** 商品ID */
    private int id;
    /** 商品名 */
    private String name;
    /** 価格 */
    private BigDecimal price;

    // getterとかsettertとか
}
App.java
public final class App {
    public static void main(String[] args) {
        Syohin syohin1 = new Syohin();
        syohin1.setId(1);
        syohin1.setName("ボールペン");
        syohin1.setPrice(new BigDecimal(120));

        Syohin syohin2 = new Syohin();
        syohin2.setId(2);
        syohin2.setName("ティッシュ");
        syohin2.setPrice(new BigDecimal(298));

        List<Syohin> list = new ArrayList<>();
        list.add(syohin1);
        list.add(syohin2);

        Cart cart = new Cart();
        cart.setId(1);
        cart.setSyohinList(list);
    }
}

実装してみる

以下のように実装することで実現できます。

  1. 文字列化しないフィールドにつけるアノテーションクラスを作る。
  2. 対象のフィールドにアノテーションをつける。
  3. ReflectionToStringBuilderを拡張して、アノテーションがついたフィールドを無視するようにする。
  4. ToStringStyleを拡張して、再帰的に文字列化するようにする。

ちなみに、commons-lang3.2からRecursiveToStringStyleが増えていて、これを使えば再帰的に文字列化することはできます。
ただ、内部でReflectionToStringBuilderを呼んでいるため、出力対象のフィールドの制御ができません。
そのため、今回はToStringStyleを自分で拡張することにしました。
(RecursiveToStringStyleの詳細は、おまけの部分に後述してあります)

アノテーションクラスの作成

LogIgnore.java
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogIgnore {
}

アノテーションの付与

今回は、商品の価格(Syohin#price)を出さないようにしてみます。

Syohin.java
public class Syohin {
    /** 商品ID */
    private int id;
    /** 商品名 */
    private String name;
    /** 価格 */
    @LogIgnore
    private BigDecimal price;
}

ReflectionToStringBuilderの拡張

ReflectionToStringBuilder#accept()で文字列化対象のフィールドを決めているようなので、こいつをオーバーライドします。

LoggingToStringBuilder.java
public class LoggingToStringBuilder extends ReflectionToStringBuilder {

    public LoggingToStringBuilder(final Object object, final ToStringStyle style) {
        super(object, style);
    }

    public static String reflectionToString(final Object object, final ToStringStyle style) {
        LoggingToStringBuilder builder = new LoggingToStringBuilder(object, style) {
            @Override
            protected boolean accept(final Field field) {
                if (!super.accept(field)) {
                    return false;
                }
                if (field.getAnnotation(LogIgnore.class) != null) {
                    return false;
                }
                return true;
            }
        };

        return builder.toString();
    }
}

ToStringStyleの拡張

commons-lang3.2以前は、自前で作ってたみたいですね。

以下の記事を参考に、ToStringStyleを拡張したクラスを作ります。
(名前が偶然にもcommons-langとカブってるので、今回はLoggingToStringStyleというクラス名で作成しました。)
reflectionToString で再帰的出力 - A Memorandum

クラスはコピペでも構いませんが、以下の箇所だけ修正してください。

LoggingToStringStyle.java
    protected void appendDetail(StringBuffer buffer, String fieldName, Object value) {
        for(String packaz : recursivePackags) {
            if (value.getClass().getCanonicalName().startsWith(packaz)) {
                // buffer.append(ToStringBuilder.reflectionToString(value, this));
                //                 ↓ここを変える 
                buffer.append(LoggingToStringBuilder.reflectionToString(value, this));
                return;
            }
        }
        buffer.append(value);
    }

動かしてみる

App.config
    String str = LoggingToStringBuilder.toString(cart, new LoggingToStringStyle("jp.co.pmtech.iwata"));
    System.out.println(str);
結果
jp.co.pmtech.iwata.Cart[id=1,syohinList=[jp.co.pmtech.iwata.Syohin[id=1,name=ボールペン],jp.co.pmtech.iwata.Syohin[id=2,name=ティッシュ]]]

CartとSyohinの中身がちゃんとでていて、priceも表示されていません。

おまけ:他の方法

JSON化

今回の目的はログへの出力なので、出力形式は特に気にしていません。
Jacksonを使ってJSON化したものを表示してみます。
JsonIgnoreアノテーションをつければ、JSONシリアライズの対象外になります。

Syohin.java
public class Syohin {
    /** 商品ID */
    private int id;
    /** 商品名 */
    private String name;
    /** 価格 */
    @JsonIgnore
    private BigDecimal price;
}
App.java
    ObjectMapper mapper = new ObjectMapper();
    String str = mapper.writeValueAsString(cart);
    System.out.println(str);
結果
{"id":1,"syohinList":[{"id":1,"name":"ボールペン"},{"id":2,"name":"ティッシュ"}]}

priceの値が出ないですね。

ただ1つ問題があって、SpringBootのRestControllerはJacksonでJSON化しているようなんですよね。
APIの返却にはpriceを含めたかったので、私はこの方法は使いませんでした。
他にJackson使っていなければこの方法でも良いと思います。

commons-langのRecursiveToStringStyleで再帰出力

前述した通り、commons-langの3.2からRecursiveToStringStyleが増えています。
ちょっとやってみましょう。
mainの最後に下記コードを入れて実行します。

App.java
    String str = ReflectionToStringBuilder.toString(cart, new RecursiveToStringStyle());
    System.out.println(str);
結果
jp.co.pmtech.iwata.Cart@610455d6[id=1,syohinList=java.util.ArrayList@2c7b84de{jp.co.pmtech.iwata.Syohin@5a07e868[id=1,name=ボールペン,price=java.math.BigDecimal@36baf30c[intVal=<null>,scale=0]],jp.co.pmtech.iwata.Syohin@76ed5528[id=2,name=ティッシュ,price=java.math.BigDecimal@24d46ca6[intVal=<null>,scale=0]]}] 

再帰的に文字列化してくれているようですが、オブジェクトIDが表示されていてちょっとうるさいです。
しかもBigDecimalの値出てないし・・・。

おわりに

使いどころですが、恐らくログの吐き出しくらいしかないかと・・・
セキュリティ上、個人情報やパスワードなどをログに吐き出したくないとかとか。
あとは、めちゃめちゃ大きいオブジェクトで、ログに出すものを端折るとか??

要件によっては、JSONだったりcommons-langのRecursiveToStringStyleでも良いと思います。
ソースコードはgithubにあげていますので、参考にしてください。
https://github.com/pmt-iwata/LoggingToStringBuilder

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0