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

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


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

# オブジェクトの内容

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

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

    // getterとかsettertとか
}
```

```java:Syohin.java
package jp.co.pmtech.iwata;

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

    // getterとかsettertとか
}
```

```java: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の詳細は、おまけの部分に後述してあります）

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

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

## アノテーションの付与

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

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

## ReflectionToStringBuilderの拡張

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

``` java: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](https://etc9.hatenablog.com/entry/20100703/1278173514)

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

``` java: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);
    }
```

## 動かしてみる

``` java:App.config
    String str = LoggingToStringBuilder.toString(cart, new LoggingToStringStyle("jp.co.pmtech.iwata"));
    System.out.println(str);
```
``` console:結果
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シリアライズの対象外になります。

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

priceの値が出ないですね。

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


## commons-langのRecursiveToStringStyleで再帰出力

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

``` java:App.java
    String str = ReflectionToStringBuilder.toString(cart, new RecursiveToStringStyle());
    System.out.println(str);
```
```console:結果
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](https://github.com/pmt-iwata/LoggingToStringBuilder)