Java

[Java最強関数] BeanUtils.copyPropertiesのすゝめ

全Javaユーザーが知るべき最強関数は何だと思いますか?

私は迷わず、以下を強く推します。

BeanUtils.copyProperties

この関数を用いることによって、実装の可読性・保守性を飛躍的に向上させることができ、
導入段階からチーム展開すると、開発スピードもかなり上がることが期待できます。

ただ、仕様をアバウトに理解して使うと痛い目を見る可能性があるのもこの関数です。

チーム展開する前にどのように拡張して導入すれば、問題が発生しなくなるのか、サンプルも記載しました。

とても、便利なのでご一読知らない方は特にご一読頂ければと思います。

copyPropertiesを使わない書き方

サンプル

そもそもcopyPropertiesが何なのかを説明するために、サンプルを起こします。

Bean.java
======================
package test;

public class Bean {

	private String item1;
	private String item2;
	private Integer item3;
	private Integer item4;

	// 以下全てsetter/getterのみ

	public String getItem1() {
		return item1;
	}

	public void setItem1(String item1) {
		this.item1 = item1;
	}

	public String getItem2() {
		return item2;
	}

	public void setItem2(String item2) {
		this.item2 = item2;
	}

	public Integer getItem3() {
		return item3;
	}

	public void setItem3(Integer item3) {
		this.item3 = item3;
	}

	public Integer getItem4() {
		return item4;
	}

	public void setItem4(Integer item4) {
		this.item4 = item4;
	}

}
Test.java
======================
package test;

public class Test {

	public static void main(String args[]) {
		Bean bean1 = new Bean();

		bean1.setItem1("テスト1");
		bean1.setItem2("テスト2");
		bean1.setItem3(3);
		bean1.setItem4(4);

		// ----------------------------------
		// 中略 ※bean1の中身を変えたりすることを想定
		// ----------------------------------
		Bean bean2 = new Bean();

		// ↓↓↓↓↓↓↓ここの書き方↓↓↓↓↓↓↓
		bean2.setItem1(bean1.getItem1());
		bean2.setItem2(bean1.getItem2());
		bean2.setItem3(bean1.getItem3());
		bean2.setItem4(bean1.getItem4());
		// ↑↑↑↑↑↑↑ここの書き方↑↑↑↑↑↑↑

		System.out.println(bean2.getItem1());
		System.out.println(bean2.getItem2());
		System.out.println(bean2.getItem3());
		System.out.println(bean2.getItem4());

	}
}

【実行結果】
テスト1
テスト2
3
4

はい。当たり前ですが上記のような結果はなります。

なお、今回のサンプルは同じ型のBeanでパラメータの受け渡しをしていますが、
別の型で同名のパラメータの受け渡しと考えて頂いても結構です。

実際の実装に置き換えて考えてみると、
Service層とDao層の間などのパラメータの受け渡しの際にこんな感じの実装になっていたりします。

この書き方のメリット

  • だれでもわかる書き方のため、新人プログラマーでもすぐに理解できる。
  • bean1のitem1をbean2のitem1にセットするという、図式が浮かびやすい。直観的。

この書き方のデメリット

  • 項目追加による影響を受けやすい。
  • 項目数が多いと、コピペミスによるバグが発生しやすい。
  • 項目数が多いと、コーディングをコピペで行ったとしても、コストがかかる。
  • 項目数が多いと、ステップ数が無駄に増えるため、読み解く度にコストがかかる。

他にもメリット・デメリットはあるでしょうが、見ていただいたらわかる通り、
このコーディング方法の保守性は高くありません。

せめて同じ項目名を簡単に移動させることができたなら、、、

それを可能にしてくれるのがBeanUtils.copyPropertiesです。

copyPropertiesを使った書き方

サンプル

まずはサンプルを見てみましょう。

Test.java
======================
package test;

import org.apache.commons.beanutils.BeanUtils;

public class Test {

	public static void main(String args[]) {
		Bean bean1 = new Bean();

		bean1.setItem1("テスト1");
		bean1.setItem2("テスト2");
		bean1.setItem3(3);
		bean1.setItem4(4);

		// ----------------------------------
		// 中略 ※bean1の中身を変えたりすることを想定
		// ----------------------------------
		Bean bean2 = new Bean();

		// ↓↓↓↓↓↓↓ここの書き方↓↓↓↓↓↓↓
		try {
			BeanUtils.copyProperties(bean2, bean1);
		} catch (IllegalAccessException | InvocationTargetException e) {
			// TODO 各アプリのエラー処理
		}
		// ↑↑↑↑↑↑↑ここの書き方↑↑↑↑↑↑↑

		System.out.println(bean2.getItem1());
		System.out.println(bean2.getItem2());
		System.out.println(bean2.getItem3());
		System.out.println(bean2.getItem4());

	}
}

【実行結果】
テスト1
テスト2
3
4

同じ結果を得ることができました。

try-catchがあるので、当サンプルのステップ数はcopyPropertiesを使用しない実装と変わっていませんが、

Beanの項目数が多ければ多いほど恩恵にあずかれることになります。

また、Beanの項目が増えた時に「ここの書き方」で囲われてる箇所のset/getの箇所も増えたりしますが、
copyPropertiesを使用すると、手を加える必要がなくなります。(値をコピーするだけなので)

これによって、実装ミスも防げるし保守の際の修正箇所も減らすことができました。

しかし、この状態のまま使うことはあまりお勧めしません。

copyPropertiesには特殊な仕様があるため、拡張されていないと思わぬところでバグを生む可能性があります。

copyPropertiesを拡張する

拡張しないと使えない理由

以下のサンプルで問題が発生します。

Test.java
======================
package test;

import org.apache.commons.beanutils.BeanUtils;

public class Test {

	public static void main(String args[]) {
		Bean bean1 = new Bean();

		bean1.setItem1("テスト1");
		bean1.setItem2(null);
		bean1.setItem3(3);
		bean1.setItem4(null);

		// ----------------------------------
		// 中略 ※bean1の中身を変えたりすることを想定
		// ----------------------------------
		Bean bean2 = new Bean();
		// ↓↓↓↓↓↓↓ここの書き方↓↓↓↓↓↓↓
		try {
			BeanUtils.copyProperties(bean2, bean1);
		} catch (IllegalAccessException | InvocationTargetException e) {
			// TODO 各アプリのエラー処理
		}
		// ↑↑↑↑↑↑↑ここの書き方↑↑↑↑↑↑↑

		System.out.println(bean2.getItem1());
		System.out.println(bean2.getItem2());
		System.out.println(bean2.getItem3());
		System.out.println(bean2.getItem4());

	}
}

【実行結果】
テスト1
null
3
0

あれ、item4がnullではなく、0になってる。。。

はい。BeanUtil.copyPropertiesの仕様は特定の型はデフォルト初期値を返すようになっています。

「なんだよ、使えねーじゃん」って方、ごもっともです。私もハマりました。

ただ、BeanUtilを継承して、copyPropertiesを拡張することで回避することができます。

拡張サンプル

BeanUtil.java
======================
package test;

import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.Date;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.converters.BigDecimalConverter;
import org.apache.commons.beanutils.converters.BooleanConverter;
import org.apache.commons.beanutils.converters.ByteConverter;
import org.apache.commons.beanutils.converters.CharacterConverter;
import org.apache.commons.beanutils.converters.DateConverter;
import org.apache.commons.beanutils.converters.DoubleConverter;
import org.apache.commons.beanutils.converters.FloatConverter;
import org.apache.commons.beanutils.converters.IntegerConverter;
import org.apache.commons.beanutils.converters.LongConverter;
import org.apache.commons.beanutils.converters.ShortConverter;

public class BeanUtil extends BeanUtils {

	private BeanUtil() {
	}

	public static void copyProperties(Object dest, Object orig) {
		if (dest != null && orig != null) {
			try {
				ConvertUtils.register(new DateConverter(null), Date.class);
				ConvertUtils.register(null, Timestamp.class);
				ConvertUtils.register(new BooleanConverter(null), Boolean.class);
				ConvertUtils.register(new CharacterConverter(null), Character.class);
				ConvertUtils.register(new ByteConverter(null), Byte.class);
				ConvertUtils.register(new ShortConverter(null), Short.class);
				ConvertUtils.register(new IntegerConverter(null), Integer.class);
				ConvertUtils.register(new LongConverter(null), Long.class);
				ConvertUtils.register(new FloatConverter(null), Float.class);
				ConvertUtils.register(new DoubleConverter(null), Double.class);
				ConvertUtils.register(new BigDecimalConverter(null), BigDecimal.class);
				BeanUtils.copyProperties(dest, orig);
			} catch (IllegalAccessException | InvocationTargetException e) {
				// TODO 各アプリのエラー処理
			}
		}
	}

}
Test.java
======================
package test;

public class Test {

	public static void main(String args[]) {
		Bean bean1 = new Bean();

		bean1.setItem1("テスト1");
		bean1.setItem2(null);
		bean1.setItem3(3);
		bean1.setItem4(null);

		// ----------------------------------
		// 中略 ※bean1の中身を変えたりすることを想定
		// ----------------------------------
		Bean bean2 = new Bean();
		// ↓↓↓↓↓↓↓ここの書き方↓↓↓↓↓↓↓
		BeanUtil.copyProperties(bean2, bean1);
		// ↑↑↑↑↑↑↑ここの書き方↑↑↑↑↑↑↑

		System.out.println(bean2.getItem1());
		System.out.println(bean2.getItem2());
		System.out.println(bean2.getItem3());
		System.out.println(bean2.getItem4());

	}
}

【実行結果】
テスト1
null
3
null

期待通りになり、Testクラスもエラーを意識する必要がなくなりました。

ついでにtry-catchもUtilクラス側に寄せたので、Beanコピーは1行で行えるようになりました。

おまけ

実装の最終系サンプル

今回の話からはずれますが、可読性を意識するとBean.javaにはコンストラクタがほしいですね。

今回紹介したモジュールはすべて載せておきます。

BeanUtil.java ※上記サンプルから変更なし
======================
package test;

import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.Date;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.converters.BigDecimalConverter;
import org.apache.commons.beanutils.converters.BooleanConverter;
import org.apache.commons.beanutils.converters.ByteConverter;
import org.apache.commons.beanutils.converters.CharacterConverter;
import org.apache.commons.beanutils.converters.DateConverter;
import org.apache.commons.beanutils.converters.DoubleConverter;
import org.apache.commons.beanutils.converters.FloatConverter;
import org.apache.commons.beanutils.converters.IntegerConverter;
import org.apache.commons.beanutils.converters.LongConverter;
import org.apache.commons.beanutils.converters.ShortConverter;

public class BeanUtil extends BeanUtils {

	private BeanUtil() {
	}

	public static void copyProperties(Object dest, Object orig) {
		if (dest != null && orig != null) {
			try {
				ConvertUtils.register(new DateConverter(null), Date.class);
				ConvertUtils.register(null, Timestamp.class);
				ConvertUtils.register(new BooleanConverter(null), Boolean.class);
				ConvertUtils.register(new CharacterConverter(null), Character.class);
				ConvertUtils.register(new ByteConverter(null), Byte.class);
				ConvertUtils.register(new ShortConverter(null), Short.class);
				ConvertUtils.register(new IntegerConverter(null), Integer.class);
				ConvertUtils.register(new LongConverter(null), Long.class);
				ConvertUtils.register(new FloatConverter(null), Float.class);
				ConvertUtils.register(new DoubleConverter(null), Double.class);
				ConvertUtils.register(new BigDecimalConverter(null), BigDecimal.class);
				BeanUtils.copyProperties(dest, orig);
			} catch (IllegalAccessException | InvocationTargetException e) {
				// TODO 各アプリのエラー処理
			}
		}
	}

}

 

Bean.java ※コンストラクタ追加
======================
package test;

public class Bean {

	public Bean() {
	}

	public Bean(Bean bean) {
		BeanUtil.copyProperties(this, bean);
	}

	private String item1;
	private String item2;
	private Integer item3;
	private Integer item4;

	public String getItem1() {
		return item1;
	}

	public void setItem1(String item1) {
		this.item1 = item1;
	}

	public String getItem2() {
		return item2;
	}

	public void setItem2(String item2) {
		this.item2 = item2;
	}

	public Integer getItem3() {
		return item3;
	}

	public void setItem3(Integer item3) {
		this.item3 = item3;
	}

	public Integer getItem4() {
		return item4;
	}

	public void setItem4(Integer item4) {
		this.item4 = item4;
	}

}

Test.java
=====================
package test;

public class Test {

	public static void main(String args[]) {
		Bean bean1 = new Bean();

		bean1.setItem1("テスト1");
		bean1.setItem2(null);
		bean1.setItem3(3);
		bean1.setItem4(null);

		// ----------------------------------
		// 中略 ※bean1の中身を変えたりすることを想定
		// ----------------------------------
		Bean bean2 = new Bean(bean1);

		System.out.println(bean2.getItem1());
		System.out.println(bean2.getItem2());
		System.out.println(bean2.getItem3());
		System.out.println(bean2.getItem4());

	}
}

【実行結果】
テスト1
null
3
null

ステップ数がだいぶ減ったと思いませんか?

プロジェクト導入段階で当関数を展開すれば、いいことずくしなので積極的に導入すべきと思います。

留意仕様

3点ほど、留意仕様があります。頭の片隅には入れておいたほうが良いです。

シャローコピー(参照コピー)であること

ディープコピー(値コピー)を行いたい場合は、シリアル化してcloneする必要があります。

コピーする内容は項目名で紐づけていること

コピーの際に型は見ていないので、以下のような場合でも同じ項目名であればコピーが発生します。
コピー元:int型
コピー先:String型

キャストエラーが発生しないように注意が必要ですが、
テーブルのカラムをEntityからそのままCSVに出力するなどの時に上手く使えたら楽できます。

Beanが実行モジュールと同じクラスにあると使えない

通常の開発ではあまりないと思いますが、
上記サンプルだと、Test.java内にBeanクラスを書いたときはコピーされません。(何をしても結果がnullになります。)

最後に

BeanUtil.copyPropertiesは本当に便利です。

こいつを使って炎上プロジェクトを立ち直らせたことがあるくらいです。

ステップ数が一気に減り、保守性が格段にあがるので、是非使ってみて下さい。

ご意見、ご感想等ございましたら、↓よりコメントお願いします。励みになります。

以上です。今後ともよろしくお願いします。

-Java

Copyright© 婿入りエンジニア、ブログ書く , 2019 All Rights Reserved.