去年のJavaOneでTSA(Thin-Server Architecture)というアークテクチャが発表された。
多分Single Page Application(SPA)とほぼ同じアーキテクチャのことを指していると思う。こっちの方がメジャーかな。本も出てる。
Manning Pubns Co
要はサーバー側のビュー処理をJSONを返すだけにして、画面の見栄え、コントロールはクライアント側でやり、サーバークライアントはJSONでやり取りするだけの疎な関係にしましょうというアーキテクチャ。
個人的にこのアーキテクチャは好きで、サーバーサイドを作ってしまえばいろんなタイプのクライアントアプリケーションを作成できるし言語によらない。 Javaで実装してもいいし、JavaScriptだけで実装してもいいし、iPhoneアプリから呼び出しても良い。
まさにいまのマルチクライアント時代に合うアーキテクチャだと思う。
JavaEEでこのアーキテクチャを実装する場合、サーバーはJAX-RSまたはWebSocketsになるだろう。で、クライアントは? 大抵のサンプルはクライアントは
(個人的にはクライアントをがりがりJavaScriptで書くならサーバーもJavaScriptにしてBackbone.js + Node.jsというスタックにしたい。最近は↓の本 でBackbone.jsとNode.jsを勉強中。これはこれで良いと思う。)
ソフトバンククリエイティブ
売り上げランキング: 11,816
JAX-RSも良い技術だが、Javaで作るならクライアント側もJavaで書きたい。というかJSFを使いたい。 TSAとは別にJSFのコンポーネント(PrimeFacesなど)を使いたいという要求はあるはず。 だがJSFにはRESTクライアントの機能はないからJAX-RSとの親和性があまり高くない。 やるならJSFのManagedBeanからEJB叩く代わりにRESTクライアントを叩く感じになる。
ちょうどJAX-RS 2.0からClient APIが登場した。 Java Day Tokyo 2013に行ってJavaEE7使いたいなぁと思うようになったこともあり、GlassFish4とNetBeans7.3.1RCをダウンロードしてTSAなTodoアプリケーションを実装してみた。
JSFのAjax機能を使ってManagedBean経由でRESTを叩き、画面の一部だけ更新する。 なんちゃってTSAモデルである。HTTPを2回叩くのが微妙だが、疎結合だから仕方ない。
ここでManagedBeanで直接REST Clientを叩くのではなく、CRUD処理隠蔽し、CDIでインジェクションした。@Rest
QualifierをつけるとREST実装に、@InMemory
Qualifierをつけるとスタブ処理実装になる。
JSF側から@InMemory
をつけることで密結合にすることもできる。またREST実装はJavaFXなど他のクライアントで再利用可能だ。DI厨が考えそうなスタイルである。
JAX-RS実装はこんな感じ。シンプルなCRUD処理。
package resource;
import domain.Todo;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.PathParam;
import javax.ws.rs.Consumes;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.GET;
import javax.ws.rs.Produces;
import javax.inject.Inject;
import javax.ws.rs.DELETE;
import javax.ws.rs.POST;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import repository.InMemory;
import repository.TodoRepository;
import sequencer.Sequencer;
/**
* REST Web Service
*
* @author maki
*/
@Path("todo")
@ApplicationScoped
public class TodoResource {
private static final Logger LOGGER = Logger.getLogger(TodoResource.class.getName());
@Inject
protected Sequencer sequencer;
@Inject
@InMemory
protected TodoRepository todoRepository;
public TodoResource() {
}
@PostConstruct
public void postConstruct() {
for (Todo todo : Arrays.asList(
new Todo(sequencer.getNext(), "aaaa"),
new Todo(sequencer.getNext(), "bbbb"),
new Todo(sequencer.getNext(), "cccc"),
new Todo(sequencer.getNext(), "dddd"))) {
todoRepository.create(todo);
}
}
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Todo getTodo(@PathParam("id") String id) {
LOGGER.log(Level.INFO, "GET Todo {0}", id);
return todoRepository.findOne(id);
}
@PUT
@Path("{id}")
@Consumes(MediaType.APPLICATION_JSON)
public Todo putTodo(@PathParam("id") String id, Todo content) {
content.setId(id);
LOGGER.log(Level.INFO, "PUT Todo {0}", id);
return todoRepository.update(content);
}
@DELETE
@Path("{id}")
@Consumes(MediaType.APPLICATION_JSON)
public void deleteTodo(@PathParam("id") String id) {
LOGGER.log(Level.INFO, "DELETE Todo {0}", id);
todoRepository.delete(id);
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public Collection<Todo> getTodos() {
LOGGER.log(Level.INFO, "GET Todos");
return todoRepository.findAll();
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response postTodos(Todo content, @Context UriInfo uriInfo) {
LOGGER.log(Level.INFO, "POST Todos");
String id = sequencer.getNext();
content.setId(id);
todoRepository.create(content);
URI newUri = uriInfo.getRequestUriBuilder().path(id).build();
return Response.created(newUri).entity(content).build();
}
}
InMemoryなレポジトリはMapに突っ込んでいるだけである。
Faceletsは
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<h:head>
<title>Facelet Title</title>
<script language="javascript" type="text/javascript">
function showIndicator(data) {
var elm = document.getElementById('indicator');
if (data.status === 'begin') {
elm.style.display = 'inline';
} else if (data.status === 'success') {
elm.style.display = 'none';
}
}
</script>
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/foundation/4.1.2/css/normalize.min.css"/>
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/foundation/4.1.2/css/foundation.min.css"/>
</h:head>
<h:body>
<div class="row">
<div class="large12 columns">
<h1>Todo</h1>
<div style="height: 10px;">
<span style="display: none;" class="alert alert-box" id="indicator">loading...</span>
</div>
<h:form id="form">
<h:outputLabel>Title: </h:outputLabel>
<h:inputText id="title" value="#{todoManagedBean.todo.title}"></h:inputText>
<h:commandButton value="create" action="#{todoManagedBean.create}" styleClass="button small">
<f:ajax execute="@form" render=":list @form" onevent="showIndicator" />
</h:commandButton>
</h:form>
</div>
</div>
<div class="row">
<div class="large12 columns">
<h:form>
<h:commandButton action="#{todoManagedBean.reload}" id="reload" value="reload" styleClass="button secondary small">
<f:ajax render=":list" onevent="showIndicator" />
</h:commandButton>
</h:form>
<h:form id="list">
<h:dataTable border="1" value="#{todoManagedBean.todos}" var="todo">
<h:column>
<f:facet name="header">ID</f:facet>
<h:outputText value="#{todo.id}" />
</h:column>
<h:column>
<f:facet name="header">Title</f:facet>
<h:outputText value="#{todo.title}" style="text-decoration: line-through;" rendered="#{todo.finished}" />
<h:outputText value="#{todo.title}" rendered="#{!todo.finished}" />
</h:column>
<h:column>
<f:facet name="header">Finished</f:facet>
<h:selectBooleanCheckbox value="#{todo.finished}">
<f:ajax event="change" onevent="showIndicator"
listener="#{todoManagedBean.update(todo)}"
render="@form">
</f:ajax>
</h:selectBooleanCheckbox>
</h:column>
<h:column>
<f:facet name="header">Delete</f:facet>
<h:commandButton action="#{todoManagedBean.delete(todo.id)}" value="Delete" styleClass="button alert tiny">
<f:ajax execute="@form" render="@form" onevent="showIndicator" />
</h:commandButton>
</h:column>
</h:dataTable>
</h:form>
</div>
</div>
</h:body>
</html>
ManagedBeanは
package faces;
import domain.Todo;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import repository.Rest;
import repository.TodoRepository;
/**
*
* @author maki
*/
@ManagedBean(name = "todoManagedBean")
@ViewScoped
public class TodoManagedBean {
private static final Logger LOGGER = Logger.getLogger(TodoManagedBean.class.getName());
protected Collection<Todo> todos;
protected Todo todo = new Todo();
@Inject
@Rest
protected TodoRepository todoRepository;
/**
* Creates a new instance of TodoManagedBean
*/
public TodoManagedBean() {
}
@PostConstruct
public void postContruct() {
LOGGER.log(Level.INFO, "construct");
findAll();
}
public void reload() {
findAll();
}
public void findAll() {
this.todos = todoRepository.findAll();
}
public void create() {
LOGGER.log(Level.INFO, "create {0}", this.todo);
todoRepository.create(this.todo);
findAll();
this.todo = new Todo();
}
public void delete(String id) {
LOGGER.log(Level.INFO, "delete {0}", id);
todoRepository.delete(id);
findAll();
}
public void update(Todo todo) {
LOGGER.log(Level.INFO, "update {0}", todo);
todoRepository.update(todo);
}
public Todo getTodo() {
return todo;
}
public Collection<Todo> getTodos() {
return todos;
}
}
これで
(画面でイベント発火)-[ajax]->(ManagedBeanがTodoレポジトリを叩く)->(RESTクライアントがREST APIを叩く)-[HTTP]->(JAX-RSがTodoレポジトリを叩く)->(メモリ操作)->(JAX-RSがレスポンスを返す)-[HTTP]->(RESTクライアントがレスポンスをJavaBeanにマッピング)->(ManagedBean更新)->(画面の一部更新)
という処理の流れになる。
RESTレポジトリは
package repository;
import domain.Todo;
import java.util.Collection;
import javax.annotation.PreDestroy;
import javax.inject.Singleton;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
/**
*
* @author maki
*/
@Rest
@Singleton
public class TodoRestRepository implements TodoRepository {
private static final String TODO_RESOURCE_PATH = "http://localhost:8080/todo-tsa/todo";
private static GenericType<Collection<Todo>> TODO_COLLECTION_TYPE = new GenericType<Collection<Todo>>() {
};
private final Client client; // Thread Safe?
public TodoRestRepository() {
this.client = ClientBuilder.newClient();
}
@PreDestroy
public void preDestroy() {
this.client.close();
}
@Override
public Todo findOne(String id) {
return this.client.target(TODO_RESOURCE_PATH).path(id).request(MediaType.APPLICATION_JSON_TYPE)
.get(Todo.class);
}
@Override
public Collection<Todo> findAll() {
return this.client.target(TODO_RESOURCE_PATH).request(MediaType.APPLICATION_JSON_TYPE)
.get(TODO_COLLECTION_TYPE);
}
@Override
public Todo create(Todo todo) {
return this.client.target(TODO_RESOURCE_PATH).request()
.post(Entity.entity(todo, MediaType.APPLICATION_JSON_TYPE)).readEntity(Todo.class);
}
@Override
public Todo update(Todo todo) {
return this.client.target(TODO_RESOURCE_PATH).path(todo.getId()).request()
.put(Entity.entity(todo, MediaType.APPLICATION_JSON_TYPE)).readEntity(Todo.class);
}
@Override
public void delete(String id) {
this.client.target(TODO_RESOURCE_PATH).path(id).request().delete();
}
}
という実装。Mappingが非常に簡単だ。
ソースコードはGithubに。
このサンプルだとJavaScriptを書かずにTSAアプリを作ることが出来た。 Backbone.jsを使うとJavaScript力もかなり必要になるし、結構面倒くさい。 JSF+JAX-RSはManagedBeanを挟む必要があって面倒臭いかなと思ったが、意外とそうでもなかった。
JavaScriptMVCフレームワークの代替として意外といけるかもしれない。
ちなみにJSFビギナーで、以下の本を参照しながら↑を作った
翔泳社
売り上げランキング: 83,284
Oreilly & Associates Inc
売り上げランキング: 36,258
Prentice Hall
売り上げランキング: 35,572