Hatena::ブログ(Diary)

達人プログラマーを目指して このページをアンテナに追加 RSSフィード Twitter

2011-02-03

Spring MVCでJSONデータを返すための手順

バージョン3以降のSpring MVCでは、コントローラーのメソッドからJSON文字列を簡単に返却させることができます。以前のバージョンでもビュークラスを独自に拡張することでJSONの返却は一応可能でしたが、最新バージョンでは非常に簡単にJSON対応させることができ、Ajaxライブラリーを使ったWebアプリケーションやサービスの開発が容易になっています。

Ajax Simplifications in Spring 3.0

コントローラーをJSON対応させる手順

コントローラーをJSON対応させるためには、まず、JacksonというJSON処理ライブラリーをクラスパスに通す必要があります。Mavenを使う場合は、pomに以下を追加します。

<dependency>
	<groupId>org.codehaus.jackson</groupId>
	<artifactId>jackson-core-asl</artifactId>
	<version>1.6.4</version>
</dependency>
<dependency>
	<groupId>org.codehaus.jackson</groupId>
	<artifactId>jackson-mapper-asl</artifactId>
	<version>1.6.4</version>
</dependency>

また、Spring MVCの設定ファイルで

	<mvc:annotation-driven />

の記述があることを確認します。このSpringの名前空間タグを使った自動生成により、MappingJacksonHttpMessageConverterという変換クラスが自動的にAnnotationMethodHandlerAdapterに対してインジェクションされます。(名前空間タグを使った設定は便利な反面、ブラックボックス化により初心者の人は内部のしくみが理解しにくくなるという欠点があります。何が設定されているかは、spring-webmvc.jarに含まれるAnnotationDrivenBeanDefinitionParserというクラスを読むとなんとなく理解できます。)

あとは、コントローラーのハンドラーメソッドから任意のPOJOを返却させ、メソッドに@ResponseBodyアノテーションを付けるだけです。

@Controller
@RequestMapping("/home")
public class HomeController {

	@RequestMapping(value="/jsonList", method=RequestMethod.GET)
	@ResponseBody
	public List<Person> jsonList() {
		return findPersonList();		
	}

以上のようなメソッドにリクエストを出すと

[{"name":"test1","id":1,"age":10},{"name":"test2","id":2,"age":20},{"name":"test3","id":3,"age":30}]

のようなJSON文字列が返されることを確認できます。

POJOのリストをJSONの2次元配列に変換するには

ただし、Ext-JsjQueryプラグインのテーブルなど、ほとんどのJavaScriptのテーブルコンポーネントは、以上のようなオブジェクト配列ではなく、純粋に2次元の配列を期待します。したがって、以上の形式のJSON文字列を直接テーブルのデータソースとして利用できません。この場合、いったんPOJOのリストをJavaの2次元配列に転記してからJSON化すればよいのですが、毎回毎回手動でつめかえを行うのは面倒で馬鹿げています。

このような場合は、以下に示すようにPOJOのリストを2次元配列に変換する汎用のクラスを作っておくと便利です。まず、2次元のデータ構造を抽象化するインターフェースを以下のように定義します。ここではこのインターフェースをGridと呼ぶことにします。

public interface Grid {

	Object get(int row, int column);
	
	void set(int row, int column, Object val);
	
	int columns();
	
	int rows();
}

次に、このインターフェースの実装例を以下に示します。


import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.BeanWrapperImpl;

public class BeanListGrid implements Grid {

	private List<Object> beanList;
	private String[] props;
	private BeanWrapperImpl beanWrapper = new BeanWrapperImpl();
	
	public BeanListGrid(List<?> beanList, String... props) {
		this.beanList = new ArrayList<Object>(beanList);
		this.props = props;
	}
	
	@Override
	public Object get(int row, int column) {
		beanWrapper.setWrappedInstance(beanList.get(row));
		return beanWrapper.getPropertyValue(props[column]);
	}

	@Override
	public void set(int row, int column, Object val) {
		beanWrapper.setWrappedInstance(beanList.get(row));
		beanWrapper.setPropertyValue(props[column], val);
	}

	@Override
	public int columns() {
		return props.length;
	}

	@Override
	public int rows() {
		return beanList.size();
	}
}

以上のクラスのコンストラクターで、オリジナルのリストと配列化する際に各カラムとしてデータを読み込むプロパティ名を受け取っています。ここでの実装のポイントはSpring Frameworkに含まれているBeanWrapperImplというクラスを利用することですね。従来、JavaでリフレクションやBeanのイントロスペクションといったAPIを直接使ってメタプログラミングをするのは、例外処理など非常に大変でしたが、このラッパークラスを用いるとプロパティの値を非常に簡単に設定・取得できます。さらに、ここでは詳しく説明しませんが、PropertyEditorやConversionService(バージョン3以降)といったデータ変換の仕組みが実装されており、数値や日付のフォーマットやパースが簡単にできるようにもなっています。SpringというとDIコンテナーというイメージが強いのですが、このように直接利用しても便利なクラスが結構含まれていて、いろいろ発掘すると有用なことがあります。Spring-Java/J2EEアプリケーションフレームワークリファレンスドキュメント(2章まで最新版)

さらに、以上の仕組みを簡単に呼び出すために、次のようなユーティリティクラスを作成します。

import java.util.List;

public class Grids {

	private Grids() {}
	
	public static Object[][] toArray(List<?> beanList, String... props) {
		Grid grid = new BeanListGrid(beanList, props);
		return toArray(grid);
	}
	
	public static Object[][] toArray(Grid grid) {
		Object[][] result = new Object[grid.rows()][grid.columns()];
		for (int row = 0; row < grid.rows(); row++) {
			for (int col = 0; col < grid.columns(); col++) {
				result[row][col] = grid.get(row, col);
			}	
		}
		
		return result;
	}
}

あとは、コントローラークラスで以下のようにして呼び出すことができます。

	@RequestMapping(value="/jsonTable", method=RequestMethod.GET)
	@ResponseBody
	public Object[][] jsonTable() {
		List<Person> personList = findPersonList();
		return Grids.toArray(personList, "id", "name", "age");
	}

そうすると、期待通り

[[1,"test1",10],[2,"test2",20],[3,"test3",30]]

というJSON文字列が得られます。なお、もう一工夫すると

	@RequestMapping(value="/jsonTable", method=RequestMethod.GET)
	@ResponseBody
	@ToGrid({"id", "name", "age"})
	public List<Person> jsonTable() {
		return findPersonList();
	}

のように@ToGridというアノテーションでメタ情報を宣言し、変換ロジックを完全に隠蔽させることもできますね。ここまでやれば、宣言的というか、かなりDSLに近いレベルで記述できていることになります。ただし、現状AnnotationMethodHandlerAdapterが非常に拡張しにくい設計となっていて、独自アノテーションを純粋なオブジェクト指向の正攻法で解釈して処理を織り込むのはかなり難しいと思います。Aspect Jを併用してコントローラーのメソッドの呼び出し時に処理を織り込むのが簡単でしょう。(詳しくはSpring MVCでコントローラーのリクエストハンドラメソッドのメタ情報を記録する方法 - 達人プログラマーを目指しての記事を参照。)

なお、ここで紹介したテーブルへの変換のテクニックはJSONだけでなく、CSVExcelダウンロードや読み込みにも応用ができますので、多くの業務アプリケーションの開発で有用だと思います。

サンプルコードは以下に含まれています。

GitHub - ryoasai/spring-mvc-exts: Some extensions to the Spring MVC web application framework.

aufhebenaufheben 2011/02/04 02:30 カラムの表示順の変更などを想定すると、オブジェクトのリストから2次元配列への変更は、クライアントで JavaScript を使って行う方がよ良いように思うのですがいかがですか?

ryoasairyoasai 2011/02/04 08:37 多くのAjaxライブラリーのサーバーサイドのサンプルではPHPが使われていますが、PHPでは一般的にarray関数でサーバーサイドで配列を生成して返すことが普通です。(DBの検索結果をそのまま配列に転記)
また、Excelダウンロードなどへの流用やJavaScript非対応へのクライアントを考えると、一般的には配列構造への変換はサーバーサイドで行うのがよいと思います。もちろん、絶対にこうするべきというわけではないですけれど。