オブジェクトの中身をまとめて文字列にして、ログに出力したいとかありますよね。
そのとき、このフィールドはログに出したくない!とかありませんか?
わざわざtoString()を実装するのは、ちょっとめんどくさいですよね。
やりたいこと
- オブジェクトの内容を文字列シリアライズしたい
- オブジェクトのフィールドには、MapとかListとか他のオブジェクトも入っている
- オブジェクトの中で、任意のフィールドは文字列シリアライズ対象外にしたい
オブジェクトの内容
package jp.co.pmtech.iwata;
public class Cart {
/** カートID */
private int id;
/** カートの中の商品 */
private List<Syohin> syohinList;
// getterとかsettertとか
}
package jp.co.pmtech.iwata;
public class Syohin {
/** 商品ID */
private int id;
/** 商品名 */
private String name;
/** 価格 */
private BigDecimal price;
// getterとかsettertとか
}
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);
}
}
実装してみる
以下のように実装することで実現できます。
- 文字列化しないフィールドにつけるアノテーションクラスを作る。
- 対象のフィールドにアノテーションをつける。
- ReflectionToStringBuilderを拡張して、アノテーションがついたフィールドを無視するようにする。
- ToStringStyleを拡張して、再帰的に文字列化するようにする。
ちなみに、commons-lang3.2からRecursiveToStringStyleが増えていて、これを使えば再帰的に文字列化することはできます。
ただ、内部でReflectionToStringBuilderを呼んでいるため、出力対象のフィールドの制御ができません。
そのため、今回はToStringStyleを自分で拡張することにしました。
(RecursiveToStringStyleの詳細は、おまけの部分に後述してあります)
アノテーションクラスの作成
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogIgnore {
}
アノテーションの付与
今回は、商品の価格(Syohin#price)を出さないようにしてみます。
public class Syohin {
/** 商品ID */
private int id;
/** 商品名 */
private String name;
/** 価格 */
@LogIgnore
private BigDecimal price;
}
ReflectionToStringBuilderの拡張
ReflectionToStringBuilder#accept()で文字列化対象のフィールドを決めているようなので、こいつをオーバーライドします。
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
クラスはコピペでも構いませんが、以下の箇所だけ修正してください。
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);
}
動かしてみる
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シリアライズの対象外になります。
public class Syohin {
/** 商品ID */
private int id;
/** 商品名 */
private String name;
/** 価格 */
@JsonIgnore
private BigDecimal price;
}
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の最後に下記コードを入れて実行します。
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