Seasar2 Formのsetter/getterのパラメータ型が異なると認識しなくなる

今更Seasar2の内容になりますが、ActionFormが自分の思った通りに動かなかったので、今後の自分のためにフレームワークの内容を調べてまとめて見ました。

## 実行環境

* Java 1.8
* Seasar 2.4.46
* SAStruts 1.0.4-sp9
* Jackson 2.9.8

## やりたかったこと

json形式のパラメータをbeanに詰め替えたいです。

Springであればメソッドのパラメータにjsonを想定した引数を指定してあげれば、spring側で自動的に詰めてくれるので、それと同じようなことをSeasarでも実現したいです。

```java:springの場合
String getName(@RequestBody PersonForm personForm) {
    return personForm.getFirstName() + personForm.getLastName();
}
```

しかしSeasarではFormの引数にオブジェクトを設定してもjson形式を自動でセットはしてくれないです。

```java:Seasarでやりたいけど出来ない
public void setPerson(PersonForm personForm) {
    this.personForm = personForm;
}
```

ではそのためにどうしたか。その結果どうなったかというのが今回の内容です。

## Formのsetter/getterでパラメータ型を変える（失敗します）

先ほどの通り、setの引数にオブジェクトを指定しても認識してくれないので、引数にはString型を指定しました。ここにjsonが格納されます。
setterメソッドの中で、jsonからBeanオブジェクトに詰め替えています。
getterメソッドでは、setter内で作成したBeanオブジェクトを返却するようにしています。

具体的なコードは以下のイメージです。


```java:ExampleForm
public class ExampleForm {

	private PersonForm personForm;

	public void setPerson(String person) {
		ObjectMapper mapper = new ObjectMapper();
		try {
			personForm = mapper.readValue(person, PersonForm.class);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public PersonForm getPerson() {
		return this.personForm;
	}
}
```

このコードに対して以下のようなjsonファイルをリクエストしても、PersonFormには値がセットされません。

```shell:リクエストするjson
{
  "firstName": "太郎",
  "lastName": "田中"
}
```

原因としては、setPersonの引数がString型、getPersonがPersonForm型と別の型になっていることで、Seasar側でset出来ないという理由からです。

SeasarではForm内のsetter・getterをリクエストごとに認識してリクエストパラメータ内の値を自動的にsetする仕様になっているのですが、その中でパラメータの型が異なるとエラーとなるようにしているからです。

詳細について興味がある方はSeasarの挙動について後述しますのでご覧ください。


## 対処法

私の考える対処法としては、メソッド名を変更することです。
以下のようなコードになります。

```java:ExampleForm
public class ExampleForm {

	private PersonForm personForm;

	public void setPerson(String person) {
		ObjectMapper mapper = new ObjectMapper();
		try {
			personForm = mapper.readValue(person, PersonForm.class);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public PersonForm getPersonForm() {
		return this.personForm;
	}
}
```

## おまけ：Seasar2のActionForm操作

おまけになりますが、Seasar2でどのようにActionFormを操作しているのかを確認しましたので記載しておきます。

```java:Seasar2でActionFormをセットするまでのスタックトレース
javax.servlet.http.HttpServlet.service(HttpServlet.java:660)
org.apache.struts.action.ActionServlet.doPost(ActionServlet.java:432)
org.apache.struts.action.ActionServlet.process(ActionServlet.java:1196)
org.seasar.struts.action.S2RequestProcessor.process(S2RequestProcessor.java:104)
org.seasar.struts.action.S2RequestProcessor.processPopulate(S2RequestProcessor.java:290)
org.seasar.struts.action.S2RequestProcessor.setProperty(S2RequestProcessor.java:398)
org.seasar.struts.action.S2RequestProcessor.setSimpleProperty(S2RequestProcessor.java:482)
org.seasar.framework.beans.impl.PropertyDescImpl.setValue(PropertyDescImpl.java:251)
org.seasar.framework.util.MethodUtil.invoke(MethodUtil.java:96)
java.lang.reflect.Method.invoke(Method.java:498)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
org.seasar.sastruts.example.form.ExampleForm.setPerson(ExampleForm.java:13)
```

ActionFormの設定をしている中で、今回問題となったロジックがある箇所は、S2RequestProcessorクラスのsetSimplePropertyメソッドです。

このメソッドの処理の流れとしては、BeanDescとPropertyDescを生成して、PropertyDesc（変数名：pd）がisWritableであれば、setValueメソッドによってActionFormのフィールドにリクエストの内容をセットするとなっています。

BeanDescクラスはActionFrom自体を管理するクラス、PropertyDescクラスはActionForm内のフィールド（setter/getterなど）を管理するメソッドというイメージになります。


原因となったロジックとしては、BeanDescImplクラス（BeanDescクラスの具象クラス）の中にあります。

```java:BeanDescImplクラス
private void setupPropertyDescs() {
    Method[] methods = beanClass.getMethods();
    for (int i = 0; i < methods.length; i++) {
        Method m = methods[i];
        if (MethodUtil.isBridgeMethod(m) || MethodUtil.isSyntheticMethod(m)) {
            continue;
        }
        String methodName = m.getName();
        if (methodName.startsWith("get")) {
            if (m.getParameterTypes().length != 0
                    || methodName.equals("getClass")
                    || m.getReturnType() == void.class) {
                continue;
            }
            String propertyName = decapitalizePropertyName(methodName
                    .substring(3));
            setupReadMethod(m, propertyName);
        } else if (methodName.startsWith("is")) {
            if (m.getParameterTypes().length != 0
                    || !m.getReturnType().equals(Boolean.TYPE)
                    && !m.getReturnType().equals(Boolean.class)) {
                continue;
            }
            String propertyName = decapitalizePropertyName(methodName
                    .substring(2));
            setupReadMethod(m, propertyName);
        } else if (methodName.startsWith("set")) {
            if (m.getParameterTypes().length != 1
                    || methodName.equals("setClass")
                    || m.getReturnType() != void.class) {
                continue;
            }
            String propertyName = decapitalizePropertyName(methodName
                    .substring(3));
            setupWriteMethod(m, propertyName);
        }
    }
    for (Iterator i = invalidPropertyNames.iterator(); i.hasNext();) {
        propertyDescCache.remove(i.next());
    }
    invalidPropertyNames.clear();
}

private void setupReadMethod(Method readMethod, String propertyName) {
    Class propertyType = readMethod.getReturnType();
    PropertyDesc propDesc = getPropertyDesc0(propertyName);
    if (propDesc != null) {
        if (!propDesc.getPropertyType().equals(propertyType)) {
            invalidPropertyNames.add(propertyName);
        } else {
            propDesc.setReadMethod(readMethod);
        }
    } else {
        propDesc = new PropertyDescImpl(propertyName, propertyType,
                readMethod, null, null, this);
        addPropertyDesc(propDesc);
    }
}
```

このロジックをご覧いただければわかると思うのですが、メソッド名の先頭3文字が"get"や"set"の時には、setupReadMethod、setupWriteMethodが呼び出されてPropertyDescImplクラスを生成するという流れになっています。

PropertyDescImplクラスでは、propertyName（ActionFormのget/setを除いたメソッド名）がキー項目となっています。
ここで注目すべきは、setupReadMethodメソッド内の以下記述になります。

```java:setupReadMethod
if (!propDesc.getPropertyType().equals(propertyType)) {
    invalidPropertyNames.add(propertyName);
```
すでにPropertyDescImplが登録されている場合かつ、プロパティタイプ（引数の型・戻り値の型）が異なる場合は、invalidPropertyNamesに登録されるという流れになります。

以上のことから、Seasar2ではsetterの引数の型とgetterの戻り値の型が異なる場合には、プロパティの管理対象から除外されてしまい、自動でsetされないということになります。


### その他参考になる記事
上記の内容はFormでのjson操作でしたが、Springと同じように上位階層でjsonをオブジェクトに詰めることが出来るようになる方法は以下の記事を参考にしてください。
[SAStrutsとJSON @shienaさん](https://qiita.com/shiena/items/146a0504c026ba00fbd0)