SpringのBeanWrapperで文字列で指定したプロパティに動的アクセスする

## 1. はじめに

今回はSpring Frameworkの`BeanWrapper`を利用して、文字列で指定したプロパティに動的にアクセスする方法について紹介したいと思います。
`BeanWrapper`について簡単に説明すると「Setter,Getterを使わずに、プロパティの値を設定したり取得したりするAPI」です。
プロパティに動的にアクセスするライブラリというと「[Apache Commons BeanUtils](http://commons.apache.org/proper/commons-beanutils/)」を思い浮かべる人もいるかと思いますが、同じような目的の機能です。

### 1.1. BeanWrapperとは？

Spring Framework(Core)の公式ガイドライン「[3.4. Bean manipulation and the BeanWrapper](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-beans)」でもしっかりと紹介されています。
基本的な使い方は以下の通りです。

* `BeanWrapper`のインスタンスを生成する
*  `setPropertyValue`メソッドでプロパティに値を設定する
*  `getPropertyValue`メソッドでプロパティの値を取得する
*  プロパティは決められたルール（Expression）で指定する

プロパティを指定するルール（Expression）も公式ガイドラインに記載されています。

> Table 11. Examples of properties

| Expression | Explanation |
|------------|-------------|
| name |Indicates the property name corresponding to the methods `getName()` or `isName()` and `setName(..)` |
| account.name | Indicates the nested property name of the property account corresponding e.g. to the methods `getAccount().setName()` or `getAccount().getName()` |
| account[2] | Indicates the third element of the indexed property `account.` Indexed properties can be of type `array`, `list` or other `naturally ordered` collection |
| account[COMPANYNAME] |Indicates the value of the map entry indexed by the key **COMPANYNAME** of the Map property account |

基本的な使い方は説明されていますが、残念なのはコレクション（`account[2]`や`account[COMPANYNAME]`）を操作するサンプルが記載されていないことです。
今回はこのコレクションのサンプルについて説明したいと思います。

## 2.1. BeanWrapperの使い方

コレクションのプロパティを操作する場合、操作する深さによって使い方が異なります。今回は以下の２つのパターンについて説明したいと思います。

* コレクションに格納する要素を設定する
* コレクションに格納されている要素のプロパティに値を設定する

```java:BeanWrapperDemo.java
package demo;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;

public class BeanWrapperDemo {

	public static void main(String[] args) {
		// コレクションに格納する要素を設定する
		test01();
		// コレクションに格納されている要素のプロパティに値を設定する
		test02();
	}

	// ★ポイント１
	private static void setPropertyValues(Object target, Map<String, Object> input) {
		BeanWrapper wrapper = new BeanWrapperImpl(target);
		for (String key : input.keySet()) {
			wrapper.setPropertyValue(key, input.get(key));
		}
	}
	
	// コレクションに格納する要素を設定する
	private static void test01() {
		try {
			// ★ポイント２
			Team team = new Team();
			List<Member> memberList = new ArrayList<>();
			Map<String, Item> items = new HashMap<>();
			team.setMemberList(memberList);
			team.setItems(items);
			
			Member taro = new Member();
			taro.setMail("test@test.com");
			taro.setName("TARO YAMADA");
			Member hanako = new Member();
			hanako.setMail("hanako@example.com");
			hanako.setName("HANAKO TOYOSU");
			
			Item notepc = new Item();
			notepc.setItemName("ノートPC");
			notepc.setPrice(100000);
			Item book = new Item();
			book.setItemName("Spring徹底入門");
			book.setPrice(4000);
			
			Map<String, Object> input = new HashMap<>();
			input.put("teamId", "01234");
			input.put("teamName", "HelloWorld");
			input.put("point", 100);
			// ★ポイント３
			input.put("memberList[0]", taro);
			input.put("memberList[1]", hanako);
			input.put("items[A01]", notepc);
			input.put("items[A02]", book);
			
			setPropertyValues(team, input);
			System.out.println(team);
		} catch(Exception e) {
			e.printStackTrace();
		}
	}

	// コレクションに格納されている要素のプロパティに値を設定する
	private static void test02() {
		try {
			// ★ポイント４
			Team team = new Team();
			List<Member> memberList = new ArrayList<>();
			memberList.add(new Member());
			memberList.add(new Member());
			Map<String, Item> items = new HashMap<>();
			items.put("A01", new Item());
			items.put("A02", new Item());

			team.setMemberList(memberList);
			team.setItems(items);
			
			Map<String, Object> input = new HashMap<>();
			input.put("teamId", "01234");
			input.put("teamName", "HelloWorld");
			input.put("point", 100);
			// ★ポイント５
			input.put("memberList[0].mail", "test@test.com");
			input.put("memberList[0].name", "TARO YAMADA");
			input.put("memberList[1].mail", "hanako@example.com");
			input.put("memberList[1].name", "HANAKO TOYOSU");
			input.put("items[A01].itemName", "ノートPC");
			input.put("items[A01].price", 100000);
			input.put("items[A02].itemName", "Spring徹底入門");
			input.put("items[A02].price", 4000);
			
			setPropertyValues(team, input);
			System.out.println(team);
		} catch(Exception e) {
			e.printStackTrace();
		}
	}

}
```

**★ポイント１**
`BeanWrapper`のインスタンスを生成します。
`org.springframework.beans.BeanWrapper`はインターフェースです。
実装クラスは`org.springframework.beans.BeanWrapperImpl`で、コンストラクタはいくつかありますが、今回はオブジェクトを引数に取るものを利用します。
値を設定するには`setPropertyValue`メソッドを利用します。第１引数はプロパティを指定するルール（Expression）、第２引数は設定したい値です。
詳細についてはJavadocを参照してください。

* https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/beans/BeanWrapper.html
* https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/beans/BeanWrapperImpl.html

**★ポイント２**
「コレクションに格納する要素を設定する」場合、対象となるコレクションがインスタンス化されている必要があります。
コレクションの中身は不要です。

**★ポイント３**
コレクションに格納する要素を設定するには`[]`でコレクションの要素の位置を指定します。

* `List(順序付のリスト、配列)`の場合、`[]`に操作対象となるインデックス（0から開始）を指定します。
* `Map`の場合、`[]`に操作対象となるキーを指定します。

**★ポイント４**
「コレクションに格納されている要素のプロパティに値を設定する」場合、対象となるコレクションのその要素までインスタンス化されている必要があります。

* `List(順序付のリスト、配列)`の場合、操作対象となるインデックスまで要素を追加してください。
* `Map`の場合、操作対象となるキーで要素を追加してください。

**★ポイント５**
`[]`でコレクションの位置を指定した後、`.プロパティ名`で設定対象となるプロパティを指定します。
対象となるコレクションの要素がインスタンス化されていない場合、例外が発生します。
そのため、事前に必ず★ポイント４で操作対象の要素をインスタンス化する必要があります。

## 2.2. （参考）モデルクラス（POJO）の定義

参考までに、入れ子のコレクションが分かるようにモデルクラスを記載します。

```java:Team.java
package demo;

import java.util.List;
import java.util.Map;

public class Team {
	private String teamId;
	private String teamName;
	private int point;
	private List<Member> memberList;
	private Map<String, Item> items;

	// setter,getter omitted
	// override toString generated by eclipse
	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append("Team [teamId=");
		builder.append(teamId);
		builder.append(", teamName=");
		builder.append(teamName);
		builder.append(", point=");
		builder.append(point);
		builder.append(", memberList=");
		builder.append(memberList);
		builder.append(", items=");
		builder.append(items);
		builder.append("]");
		return builder.toString();
	}
}
```

```java:Member.java
package demo;

import java.util.Map;

public class Member {
	private String mail;
	private String name;
	private Map<String, String> memo;

	// setter,getter omitted
	// override toString generated by eclipse
	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append("Member [mail=");
		builder.append(mail);
		builder.append(", name=");
		builder.append(name);
		builder.append(", memo=");
		builder.append(memo);
		builder.append("]");
		return builder.toString();
	}
}
```

```java:Item.java
package demo;

public class Item {
	private String itemName;
	private int price;

	// setter,getter omitted
	// override toString generated by eclipse
	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append("Item [itemName=");
		builder.append(itemName);
		builder.append(", price=");
		builder.append(price);
		builder.append("]");
		return builder.toString();
	}
}
```

## 3. さいごに

今回はSpring Frameworkの`BeanWrapper`を利用して文字列で指定したプロパティに動的にアクセスする方法について説明しました。
**便利な`BeanWrapper`ですが、処理性能についてはもちろん普通の'Setter,Getter'を利用した方がいいです。**
`BeanWrapper`の使いどころとしては、①動的アクセスが必要となる汎用的な処理や、②文字列でプロパティを指定できる利便性を利用してテストクラスで入力値を設定する等かと思います。