SpringのJavaBeansアクセスAPIと型変換サービスは単独で利用しても利用価値が高いという事実

Spring Frameworkはもともと、面倒なJavaEE環境における開発を簡易化する軽量のDIコンテナーとして有名になったので、あまり、そういうイメージがないのですが、実は、JavaSEのAPIを簡易化するためのライブラリーとしてもかなり良く設計されていると思います。実際、Springは低結合性、高凝集性、インターフェースに対するコーディングなどオブジェクト指向の設計がかなり徹底されているため、部分的な部品のつまみ食いも比較的容易なのです。ここでは、意外に知られていないSpring FrameworkのJavaSE簡易化機能についていくつか紹介したいと思います。これらの機能を流用して使いこなすことで、Webアプリケーションに限らず、さまざまなプログラムでJava言語を使った開発の生産性を向上させることができると思います。

JavaBeansに対するプロパティアクセスの簡易化

Springフレームワークはその設定ファイルでというタグが使われていることからもわかるように、歴史的には最初はJavaBeansの仕様を使った汎用的なメタプログラミング*1を簡易化するためのライブラリーとして開発されたという経緯があります。それゆえ、

  • DIの設定ファイルの読み込み
  • HTTPリクエスト文字列のBeanへのバインディング
  • SpELを使った式言語の評価

など、さまざまな場所で利用できる汎用のJavaBeansアクセス簡易化の仕組みがspring-beansというサブモジュールの中で実装されています。Springを利用しない多くのアプリケーションでは、同様の目的にcommons-beanutilを利用することも多いと思いますが、Springを利用するアプリケーションであれば、むしろ、Springの機能を利用した方が良いと思います。
SpringのJavaBeansアクセス機能を試してみるために、以下の簡単なJavaBeansを考えてみることにします。

import java.util.Date;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.joda.time.DateMidnight;
import org.springframework.format.annotation.DateTimeFormat;

public class Person {

    @NotNull
    private String name;

    private int age;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birthDate;

    public Person() {
        this("test", 30, new DateMidnight(2010, 5, 2).toDate());
    }

    public Person(String name, int age, Date birthDate) {
        this.name = name;
        this.age = age;
        this.birthDate = birthDate;
    }

    @Size(max = 15)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Date getBirthDate() {
        return birthDate;
    }

    public void setBirthDate(Date birthDate) {
        this.birthDate = birthDate;
    }
}

このBeanのインスタンスに対しては、

  • プロパティ経由でのアクセス(getter、setter経由でのアクセス)
  • 直接フィールドアクセス

の2種類の方法でアクセスすることができます。実際プロパティ経由でアクセスするには、以下のようにBeanWrapperのインスタンスを取得し、これを用いてアクセスすることが可能です。

    @Test
    public void beanPropertyAccess() {
        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(person);

        // プロパティの値
        assertThat((String) beanWrapper.getPropertyValue("name"), is("test"));
        // プロパティの型(クラス)
        assertThat(beanWrapper.getPropertyType("name"), isSameClassAs(String.class));

        // プロパティの型(TypeDescriptor)
        TypeDescriptor typeDesc = beanWrapper.getPropertyTypeDescriptor("name");
        assertThat(typeDesc.getType(), isSameClassAs(String.class));

        // プロパティのアノテーション
        // アノテーションはフィールドかgetterに付いていればプロパティとして取得できる。
        assertThat(typeDesc.getAnnotation(NotNull.class), is(NotNull.class));
        assertThat(typeDesc.getAnnotation(Size.class), is(Size.class));
    }

一方、直接フィールドにアクセスするには以下のようにConfigurablePropertyAccessorが利用できます。

    @Test
    public void beanDirectFieldAccess() {
        ConfigurablePropertyAccessor fieldAccessor = PropertyAccessorFactory.forDirectFieldAccess(person);

        // フィールドの値
        assertThat((String) fieldAccessor.getPropertyValue("name"), is("test"));
        // フィールドの型(クラス)
        assertThat(fieldAccessor.getPropertyType("name"), isSameClassAs(String.class));

        // フィールドの型(TypeDescriptor)
        TypeDescriptor typeDesc = fieldAccessor.getPropertyTypeDescriptor("name");
        assertThat(typeDesc.getType(), isSameClassAs(String.class));

        // フィールドのアノテーション
        // アノテーションはフィールドに付いていれば取得できる。
        assertThat(typeDesc.getAnnotation(NotNull.class), is(NotNull.class));
        assertThat(typeDesc.getAnnotation(Size.class), is(nullValue()));
    }

BeanWrapperもConfigurablePropertyAccessorも共通の親インターフェースを持っていますから、ほとんど同様に利用することができます。ただし、取得できるアノテーションなどに両者の間で違いがあります。
これらの仕組みをフレームワーク内で利用することで、任意のBeanクラスに対して処理を行うような汎用的なフレームワークを簡単に構築できます。

汎用型変換サービスAPI

さらに、Spring3からはこのJavaBeansアクセスフレームワーク上にConversionServiceと呼ばれる、汎用的な型変換の仕組みが導入されています。以前のバージョンでもPropertyEditorという仕組みを使って、オブジェクトと文字列との間の相互変換が可能だったのですが、Spring3のConversionServiceでは文字列との変換に限らず、任意の型の変換が可能な仕組みになっています。

public interface ConversionService {
    boolean canConvert(Class<?> sourceType, Class<?> targetType);

    <T> T convert(Object source, Class<T> targetType);

    boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

ここで、通常のClassクラスを利用した変換に加えて、先日紹介したTypeDescriptorを使った変換が可能になっている点に注意してください。(SpringのTypeDescriptorを使うと型パラメーターを簡単に取得できる - 達人プログラマーを目指して)TypeDescriptorにより、総称型やアノテーションの情報が型の一部としてカプセル化されます。

ConversionServiceの生成

ConversionServiceはSpring MVCを利用する場合は、デフォルトで自動的にコンテナ内で生成されますが、プログラム中で以下のようにFormattingConversionServiceFactoryBeanを利用して生成することができます。これで、デフォルトの変換ロジックがあらかじめ設定されたConversionServiceのインスタンスが生成されます。

    @Before
    public void setUp() {
        FormattingConversionServiceFactoryBean factoryBean = new FormattingConversionServiceFactoryBean();
        factoryBean.afterPropertiesSet();
        conversionService = factoryBean.getObject();
    }
基本的な型の変換

ConversionServiceのインスタンスが得られれば、値の型を変換することは容易です。

    @Test
    public void basicConversion() {
        // String -> Integer 
        assertThat(conversionService.convert("123", Integer.class), is(123));

        // Integer -> String 
        assertThat(conversionService.convert(123, String.class), is("123"));

        // String -> Boolean 
        assertThat(conversionService.convert("true", Boolean.class), is(true));

        // Boolean -> String 
        assertThat(conversionService.convert(true, String.class), is("true"));
    }
enum型の変換

enum型も同様にして変換できます。標準ではenumの定数名とenumインスタンスを相互に変換できます。

    public static enum TestEnum {
        ITEM1, ITEM2
    }

    @Test
    public void enumConversion() {
        // String -> enum 
        assertThat(conversionService.convert("ITEM1", TestEnum.class), is(TestEnum.ITEM1));

        // enum -> String 
        assertThat(conversionService.convert(TestEnum.ITEM1, String.class), is("ITEM1"));
    }
コレクションや配列型の変換

コレクションや配列型の変換も行えます。ただし、この場合にはClassクラスではコレクションの要素型を表現できないため、代わりにTypeDescriptorを利用する必要があります。

    List<String> strList = Arrays.asList("1", "2", "3");
    List<Integer> intList = Arrays.asList(1, 2, 3);

    @SuppressWarnings("unchecked")
    @Test
    public void collectionConversion() {
        
        TypeDescriptor strListTypeDesc = new TypeDescriptor(ReflectionUtils.findField(getClass(), "strList"));
        TypeDescriptor intListTypeDesc = new TypeDescriptor(ReflectionUtils.findField(getClass(), "intList"));
        
        // List<String> -> List<Integer> 
        assertThat((List<Integer>)conversionService.convert(strList, strListTypeDesc, intListTypeDesc), is(intList));

        // List<String> -> List<Integer> 
        assertThat((List<String>)conversionService.convert(intList, intListTypeDesc, strListTypeDesc), is(strList));
    }

    String[] strArray = {"1", "2", "3"};
    int[] intArray = {1, 2, 3};

    @Test
    public void arrayConversion() {
        
        TypeDescriptor strArrayTypeDesc = new TypeDescriptor(ReflectionUtils.findField(getClass(), "strArray"));
        TypeDescriptor intArrayTypeDesc = new TypeDescriptor(ReflectionUtils.findField(getClass(), "intArray"));
        
        // String[] -> Integer[] 
        assertArrayEquals(intArray, (int[])conversionService.convert(strArray, strArrayTypeDesc, intArrayTypeDesc));

        // Integer[] -> String[] 
        assertArrayEquals(strArray, (String[])conversionService.convert(intArray, intArrayTypeDesc, strArrayTypeDesc));
    }
Dateと文字列の相互変換

joda timeがクラスパスに入っていれば、@DateTimeFormatアノテーションを用いて日付と文字列の間の相互変換も処理できます。

<dependency>
	<groupId>joda-time</groupId>
	<artifactId>joda-time</artifactId>
	<version>1.6.2</version>
</dependency>

この場合、ConversionServiceはフィールドやプロパティに設定されたアノテーションのメタ情報を利用することで、自動的に変換を行ってくれます。

    @Test
    public void dateFormat() {
        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(person);
        
        TypeDescriptor fieldDesc = beanWrapper.getPropertyTypeDescriptor("birthDate");
        TypeDescriptor strDesc = TypeDescriptor.valueOf(String.class);

        // Date -> String
        String formattedValue = (String)conversionService.convert(beanWrapper.getPropertyValue("birthDate"), fieldDesc, strDesc);
        assertThat(formattedValue, is("2010-05-02"));

        // String -> Date
        Date parsedValue = (Date)conversionService.convert("2010-05-02", strDesc, fieldDesc);
        assertThat(parsedValue, is(person.getBirthDate()));
    }
汎用型変換サービス拡張用SPI

ConversionServiceではデフォルトでさまざまな変換ロジックが追加されていますが、以下のSPIインターフェースを実装して、登録することで、変換ロジックを自由に拡張することが可能な設計となっています。

SPIインターフェース 利用目的
Converter 型Sから型Tへの変換を実装する。
ConverterFactory 型Rのサブクラスに変換する一群のConverterを生成する。
GenericConverter TypeDescriptorで指定された変換を実装する。
Printer オブジェクトを文字列に変換する。
Parser 文字列をオブジェクトに変換する。
Formatter PrinterとParserの両方の機能を実装する。

詳しくはマニュアルを参照してください。
5. Validation, Data Binding, and Type Conversion

SpELによる式言語のサポート

さらに、Spring3からはSpELと呼ばれる式言語エンジンが組み込まれています。この式言語エンジンは、ここで説明したConversionServiceを利用して、データ型の変換を行うことができます。

    @Test
    public void spEL() {
        ExpressionParser parser = new SpelExpressionParser();
        Expression exp = parser.parseExpression("name = #{name}, age = #{age}, bitdhDate = #{birthDate}", new TemplateParserContext());

        StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
        StandardTypeConverter converter = new StandardTypeConverter(conversionService);
        evaluationContext.setTypeConverter(converter);
        
        String value = exp.getValue(evaluationContext, new Person(), String.class);
        
        assertThat(value , is("name = test, age = 30, bitdhDate = 2010-05-02"));
    }

SpELについては以下を参照してください。
6. Spring Expression Language (SpEL)

まとめ

ここではSpring Frameworkが提供する

  • JavaBeansの値や型情報へのアクセス
  • 型変換
  • オブジェクトのフォーマット(文字列との相互変換)
  • 式言語(SpEL)の評価

などそれ自身でも有用な基本サービスについて説明しました。模式的には以下のようなレイヤー構造で考えると理解しやすいと思います。

このレイヤーに上手く乗っかることで、プロパティアクセスや型変換をJava言語の環境でありながら、あたかも動的言語のように柔軟に処理させることができるのです。最近はJava言語は時代遅れであるという意見も多くありますが、開発環境の充実度や利用可能なライブラリーなどでは圧倒的なメリットがあり、こういった抽象化レイヤーやアスペクトライブラリーと組み合わせることで可能なメタプログラミングの充実度では今でも最高峰の環境が利用できるというのは事実でしょう。*2もちろん、Groovyなどの言語を利用すればよいのですが、開発環境の制約などを考えると、今のところ選択肢としてPure Javaで実装できるという点はメリットであると思います。
ここで紹介した機能は、もちろんDIコンテナやMVCフレームワークの中で利用されていますが、ユーティリティとして利用することで、バッチ処理におけるファイルの変換などさまざまな局面で再利用することができると思います。実際、COBOL時代以来、業務アプリケーションの大部分の工数はデータの転記と変換処理に費やされているというところがありますから、Springのこうした便利機能を開拓して、上手に再利用することで大幅に工数を削減できると思います。
なお、サンプルコードは以下にアップしてあります。
Spring's bean property access and conversion tests. · GitHub

*1:現在ではJavaBeansの概念は任意のPOJOに拡張されているため、必ずしもJavaBeansの仕様に従わないようなクラスを扱うこともできる

*2:それを最大限にいかした例がSpring Rooと考えられます。