m-namikiの日記

おもしろき こともなき世を おもしろく

SpringMVC3.2.1を利用してのWebアプリケーション #05

前回の続きです。前回はTiles3の適用を行いました。今回はJSONによるデータのやり取りを行います。

やりたいこと

ブラウザのフォームに入力した情報をJSONデータとしてサーバ側へ送信し、サーバ側では受け取ったJSONを基に情報を付加したJSONデータを作成・出力します。ブラウザでは受け取ったJSONデータを画面に表示します。

コントローラ

まずはJSONを受け取るコントローラから。

package jp.co.shantery.tutorial.web.json.controller;

import jp.co.shantery.tutorial.dto.JsonResponseDto;
import jp.co.shantery.tutorial.web.json.command.JsonCommand;

import org.apache.log4j.Logger;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * JSONの受け取り及び出力を行うコントローラクラスです。
 * 
 * @author m-namiki
 * 
 */
@Controller
@RequestMapping(value = "/json")
public class JsonController {
	/** コマンド名です。 */
	public static final String COMMAND_NAME = "jsonCommand";

	/** 入力画面のパスです。 */
	private static final String PAGE_INPUT = "json/input";

	/** ロガーです。 */
	private static Logger logger = Logger.getLogger(JsonController.class);

	/**
	 * コマンドの初期オブジェクトを作成します。<br>
	 * このメソッドを定義しなくてもコントローラとして動作しますが、バインドエラー時などに呼び出されるため存在しないとシステムエラーとなることがあります。
	 * 
	 * @return コマンド
	 */
	@ModelAttribute(COMMAND_NAME)
	public JsonCommand createCommand() {
		JsonCommand command = new JsonCommand();
		return command;
	}

	/**
	 * 初期表示の準備処理です。
	 * 
	 * @param model
	 *            画面モデル
	 * @return 入力画面のパス
	 */
	@RequestMapping(method = RequestMethod.GET)
	public String init(Model model) {
		logger.info("init() Start.");
		model.addAttribute(COMMAND_NAME, createCommand());
		logger.info("init() End.");
		return PAGE_INPUT;
	}

	/**
	 * JSONを受け取り、JSONを返却します。
	 * 
	 * @param command
	 *            コマンド
	 */
	@RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.POST)
	@ResponseBody
	public JsonResponseDto execute(@RequestBody JsonCommand command) {
		logger.info("execute() Start.");

		if (logger.isDebugEnabled()) {
			logger.debug(command);
		}

		JsonResponseDto dto = new JsonResponseDto();
		dto.setName(command.getName());
		dto.setAge(command.getAge());
		dto.setDepartmentName("開発部");

		if (logger.isDebugEnabled()) {
			logger.debug(dto);
		}

		logger.info("execute() End.");
		return dto;
	}
}

execute()が実際にJSONを受け取るメソッドです。@RequestMapping に consumes と produces という引数がありますが、これはSpring3.1から追加された引数です。consumes はこのメソッドが受け取るHTTPヘッダのContent-Type を制限するのに使用します。produces はこのメソッドが出力するレスポンスのメディアタイプ(HTTPヘッダのAccept)を指定するのに使用します。
ここではJSONで受け取り、JSONを出力するので共に「application/json」を指定します。
次に@ResponseBody ですがこのアノテーションを付与すると、フレームワーク側で戻り値を自動的に判断し、HTTPヘッダのメディアタイプのAcceptが「application/json」となります。*1
最後にメソッド引数に付与されている @RequestBody ですが、付与した引数がJavaBeanの場合、自動的にマッピングされます。なので、このメソッドの場合、受け取ったJSONデータは JsonCommand に自動でマッピングされます。*2

コマンド

受け取ったJSONデータをマッピングするコマンドクラスです。単純なJavaBeanです。

package jp.co.shantery.tutorial.web.json.command;

import java.io.Serializable;

import org.apache.commons.lang3.builder.ToStringBuilder;

/**
 * JSON形式で受け取った情報を保持するコマンドクラスです。
 * 
 * @author m-namiki
 * 
 */
public class JsonCommand implements Serializable {

	private static final long serialVersionUID = 1L;

	/** 名前です。 */
	private String name;

	/** 年齢です。 */
	private Integer age;

	public String getName() {
		return name;
	}

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

	public Integer getAge() {
		return age;
	}

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

	@Override
	public String toString() {
		return ToStringBuilder.reflectionToString(this);
	}
}

Dto

コントローラのメソッドの戻り値となるDtoクラスです。これも単純なJavaBeanですが、フレームワーク側で自動でJSON形式に変換されます。コマンドに部署名のプロパティを追加しました。

package jp.co.shantery.tutorial.dto;

import org.apache.commons.lang3.builder.ToStringBuilder;

/**
 * JSONのレスポンスを表現するDTOクラスです。
 * 
 * @author m-namiki
 * 
 */
public class JsonResponseDto {

	/** 名前です。 */
	private String name;

	/** 年齢です。 */
	private Integer age;

	/** 所属部署名です。 */
	private String departmentName;

	public String getName() {
		return name;
	}

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

	public Integer getAge() {
		return age;
	}

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

	public String getDepartmentName() {
		return departmentName;
	}

	public void setDepartmentName(String departmentName) {
		this.departmentName = departmentName;
	}

	@Override
	public String toString() {
		return ToStringBuilder.reflectionToString(this);
	}
}

JSP

最後にJSONデータの送信及び受け取りを行うJSPです。

<%@ page language="java" trimDirectiveWhitespaces="true"
	contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>

<script type="text/javascript">
	$(document).ready(function() {
		$('#jsonReq').click(function() {
			var form = $("#jsonCommand");
			var param = {};
			$(form.serializeArray()).each(function(i, v) {
				param[v.name] = v.value;
			});
			var data = $.toJSON(param);
			$.ajax({
				type: "POST",
				url: "json.html",
				contentType: "application/json",
				data: data,
				dataType: 'json',
				success: function(data) {
					$("#responseName").empty();
					$("#responseAge").empty();
					$("#departmentName").empty();

					$("<span/>").append('名前 = ' + data.name)
							.appendTo("#responseName");
					$("<span/>").append('年齢  = ' + data.age).appendTo("#responseAge");
					$("<span/>").append('所属部署 = ' + data.departmentName)
							.appendTo("#departmentName");
				},
				error: function(XMLHttpRequest, textStatus, errorThrown){
					alert("textStatus=" + textStatus);
					alert("errorThrown=" + errorThrown);
				}
			});
		});
	});
</script>

<form:form modelAttribute="jsonCommand" action="json.html" method="POST">
	<form:input path="name" class="input-large" placeholder="名前" />
	<form:errors path="name" class="text-error" />
	<br>
	<form:input path="age" class="input-large" placeholder="年齢" />
	<form:errors path="age" class="text-error" />
	<br>
	<input type="button" id="jsonReq" class="btn btn-primary" value="送信"/>
</form:form>
<div id="responseName"></div>
<div id="responseAge"></div>
<div id="departmentName"></div>

フォームの内容をJSON形式に変換するのにhttps://code.google.com/p/jquery-json/ を利用しました。
参考:http://d.hatena.ne.jp/hiro_nemu/20090826/1251284397

と、ここまでで問題なく動作すると思ったのですが、どうも思ったように動いてくれません。ブラウザからのJSONデータの送信は行えますが、406 Not Acceptable が発生してしまいます。
調べてみたところ、

Spring3.2からContentNegotiationManager が標準で登録され、その初期設定が拡張子によりメディアタイプを決定し、処理の優先度がAccept ヘッダーの内容よりも高いため

だそうです。ということは、コントローラでJSONレスポンスを返そうとしてるけど元々のURLがxxx.htmlの場合に拡張子とメディアタイプが合致しないから406を返却している、なのでしょうか。イマイチよく分かりません…。が、一応の回避策はあって、ContentNegotiationManagerの機能をOFFにすれば従来(Spring3.1)までの動きとなるようです。回避策は以下の通りです。

servlet-context.xml

<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" />
<bean id="contentNegotiationManager"
class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
	<property name="favorPathExtension" value="false" />
	<property name="favorParameter" value="false" />
	<property name="ignoreAcceptHeader" value="false" />
</bean>

こうすることにより、ブラウザからのJSONデータの送信及びサーバ側からのJSONデータの返却が正常に動作するようになりました。ただこれってかなり影響が大きそうな気がするんですけど、どうなんでしょう。ContentNegotiationManagerが標準登録されることで嬉しいことは何なのかというのが分かっていないのでなんとも言えませんが。

取り敢えず画面はこんな感じで動作します。

今回はここまで。

*1:確認:@RequestMappingのproducesは不要?

*2:確認:@RequestMappingのconsumesは不要?