---
title: JSFとJAX-RS 2.0実現するTSA(Thin-Server Architecture)アプリケーション
tags: []
categories: ["Programming", "Java", "JavaEE7"]
date: 2013-05-19T09:33:39Z
updated: 2013-05-19T09:33:39Z
---

去年のJavaOneで[TSA(Thin-Server Architecture)というアークテクチャが発表された][1]。

多分Single Page Application(SPA)とほぼ同じアーキテクチャのことを指していると思う。こっちの方がメジャーかな。本も出てる。

<div class="amazlet-box" style="margin-bottom:0px;"><div class="amazlet-image" style="float:left;margin:0px 12px 1px 0px;"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/1617290750/ikam-22/ref=nosim/" name="amazletlink" target="_blank"><img src="http://ecx.images-amazon.com/images/I/51lvY0%2Bj2vL._SL160_.jpg" alt="Single Page Web Applications: Javascript End-to-end" style="border: none;" /></a></div><div class="amazlet-info" style="line-height:120%; margin-bottom: 10px"><div class="amazlet-name" style="margin-bottom:10px;line-height:120%"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/1617290750/ikam-22/ref=nosim/" name="amazletlink" target="_blank">Single Page Web Applications: Javascript End-to-end</a><div class="amazlet-powered-date" style="font-size:80%;margin-top:5px;line-height:120%">posted with <a href="http://www.amazlet.com/" title="amazlet" target="_blank">amazlet</a> at 13.05.19</div></div><div class="amazlet-detail">Michael Mikowski Josh Powell <br />Manning Pubns Co <br /></div><div class="amazlet-sub-info" style="float: left;"><div class="amazlet-link" style="margin-top: 5px"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/1617290750/ikam-22/ref=nosim/" name="amazletlink" target="_blank">Amazon.co.jpで詳細を見る</a></div></div></div><div class="amazlet-footer" style="clear: left"></div></div>

要はサーバー側のビュー処理をJSONを返すだけにして、画面の見栄え、コントロールはクライアント側でやり、サーバークライアントはJSONでやり取りするだけの疎な関係にしましょうというアーキテクチャ。

個人的にこのアーキテクチャは好きで、サーバーサイドを作ってしまえばいろんなタイプのクライアントアプリケーションを作成できるし言語によらない。
Javaで実装してもいいし、JavaScriptだけで実装してもいいし、iPhoneアプリから呼び出しても良い。

まさにいまのマルチクライアント時代に合うアーキテクチャだと思う。

JavaEEでこのアーキテクチャを実装する場合、サーバーはJAX-RSまたはWebSocketsになるだろう。で、クライアントは？
大抵のサンプルはクライアントは

(個人的にはクライアントをがりがりJavaScriptで書くならサーバーもJavaScriptにしてBackbone.js + Node.jsというスタックにしたい。最近は↓の本 でBackbone.jsとNode.jsを勉強中。これはこれで良いと思う。)

<div class="amazlet-box" style="margin-bottom:0px;"><div class="amazlet-image" style="float:left;margin:0px 12px 1px 0px;"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4797370904/ikam-22/ref=nosim/" name="amazletlink" target="_blank"><img src="http://ecx.images-amazon.com/images/I/510pDzY1u9L._SL160_.jpg" alt="はじめてのNode.js -サーバーサイドJavaScriptでWebアプリを開発する-" style="border: none;" /></a></div><div class="amazlet-info" style="line-height:120%; margin-bottom: 10px"><div class="amazlet-name" style="margin-bottom:10px;line-height:120%"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4797370904/ikam-22/ref=nosim/" name="amazletlink" target="_blank">はじめてのNode.js -サーバーサイドJavaScriptでWebアプリを開発する-</a><div class="amazlet-powered-date" style="font-size:80%;margin-top:5px;line-height:120%">posted with <a href="http://www.amazlet.com/" title="amazlet" target="_blank">amazlet</a> at 13.05.19</div></div><div class="amazlet-detail">松島 浩道 <br />ソフトバンククリエイティブ <br />売り上げランキング: 11,816<br /></div><div class="amazlet-sub-info" style="float: left;"><div class="amazlet-link" style="margin-top: 5px"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4797370904/ikam-22/ref=nosim/" name="amazletlink" target="_blank">Amazon.co.jpで詳細を見る</a></div></div></div><div class="amazlet-footer" style="clear: left"></div></div>

<div class="amazlet-box" style="margin-bottom:0px;"><div class="amazlet-image" style="float:left;margin:0px 12px 1px 0px;"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4899773501/ikam-22/ref=nosim/" name="amazletlink" target="_blank"><img src="http://ecx.images-amazon.com/images/I/31tI0WaZukL._SL160_.jpg" alt="Backbone.jsガイドブック" style="border: none;" /></a></div><div class="amazlet-info" style="line-height:120%; margin-bottom: 10px"><div class="amazlet-name" style="margin-bottom:10px;line-height:120%"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4899773501/ikam-22/ref=nosim/" name="amazletlink" target="_blank">Backbone.jsガイドブック</a><div class="amazlet-powered-date" style="font-size:80%;margin-top:5px;line-height:120%">posted with <a href="http://www.amazlet.com/" title="amazlet" target="_blank">amazlet</a> at 13.05.19</div></div><div class="amazlet-detail">高橋 侑久 <br />ラトルズ <br />売り上げランキング: 5,946<br /></div><div class="amazlet-sub-info" style="float: left;"><div class="amazlet-link" style="margin-top: 5px"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4899773501/ikam-22/ref=nosim/" name="amazletlink" target="_blank">Amazon.co.jpで詳細を見る</a></div></div></div><div class="amazlet-footer" style="clear: left"></div></div>

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アプリケーションを実装してみた。

スクリーンショット
<a href='/api/v1/files/00098/todo.png'><img src='/api/v1/files/00098/todo.png' /></a>


アーキテクチャはこんな感じ。
<a href='/api/v1/files/00096/tsa-jsf-jaxrs.png'><img src='/api/v1/files/00096/tsa-jsf-jaxrs.png' /></a>

JSFのAjax機能を使ってManagedBean経由でRESTを叩き、画面の一部だけ更新する。
なんちゃってTSAモデルである。HTTPを２回叩くのが微妙だが、疎結合だから仕方ない。

ここでManagedBeanで直接REST Clientを叩くのではなく、CRUD処理隠蔽し、CDIでインジェクションした。`@Rest`QualifierをつけるとREST実装に、`@InMemory`Qualifierをつけるとスタブ処理実装になる。
JSF側から`@InMemory`をつけることで密結合にすることもできる。またREST実装はJavaFXなど他のクライアントで再利用可能だ。DI厨が考えそうなスタイルである。

<a href='/api/v1/files/00097/cdi.png'><img src='/api/v1/files/00097/cdi.png' /></a>

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][2]に。

このサンプルだとJavaScriptを書かずにTSAアプリを作ることが出来た。
Backbone.jsを使うとJavaScript力もかなり必要になるし、結構面倒くさい。
JSF+JAX-RSはManagedBeanを挟む必要があって面倒臭いかなと思ったが、意外とそうでもなかった。

JavaScriptMVCフレームワークの代替として意外といけるかもしれない。

ちなみにJSFビギナーで、以下の本を参照しながら↑を作った

<div class="amazlet-box" style="margin-bottom:0px;"><div class="amazlet-image" style="float:left;margin:0px 12px 1px 0px;"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4798124605/ikam-22/ref=nosim/" name="amazletlink" target="_blank"><img src="http://ecx.images-amazon.com/images/I/51ITQohknTL._SL160_.jpg" alt="Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava (Programmer’s SELECTION)" style="border: none;" /></a></div><div class="amazlet-info" style="line-height:120%; margin-bottom: 10px"><div class="amazlet-name" style="margin-bottom:10px;line-height:120%"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4798124605/ikam-22/ref=nosim/" name="amazletlink" target="_blank">Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava (Programmer’s SELECTION)</a><div class="amazlet-powered-date" style="font-size:80%;margin-top:5px;line-height:120%">posted with <a href="http://www.amazlet.com/" title="amazlet" target="_blank">amazlet</a> at 13.05.19</div></div><div class="amazlet-detail">Antonio Goncalves <br />翔泳社 <br />売り上げランキング: 83,284<br /></div><div class="amazlet-sub-info" style="float: left;"><div class="amazlet-link" style="margin-top: 5px"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4798124605/ikam-22/ref=nosim/" name="amazletlink" target="_blank">Amazon.co.jpで詳細を見る</a></div></div></div><div class="amazlet-footer" style="clear: left"></div></div>

<div class="amazlet-box" style="margin-bottom:0px;"><div class="amazlet-image" style="float:left;margin:0px 12px 1px 0px;"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/144933668X/ikam-22/ref=nosim/" name="amazletlink" target="_blank"><img src="http://ecx.images-amazon.com/images/I/517HEZFEGKL._SL160_.jpg" alt="Java EE 6 Pocket Guide" style="border: none;" /></a></div><div class="amazlet-info" style="line-height:120%; margin-bottom: 10px"><div class="amazlet-name" style="margin-bottom:10px;line-height:120%"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/144933668X/ikam-22/ref=nosim/" name="amazletlink" target="_blank">Java EE 6 Pocket Guide</a><div class="amazlet-powered-date" style="font-size:80%;margin-top:5px;line-height:120%">posted with <a href="http://www.amazlet.com/" title="amazlet" target="_blank">amazlet</a> at 13.05.19</div></div><div class="amazlet-detail">Arun Gupta <br />Oreilly & Associates Inc <br />売り上げランキング: 36,258<br /></div><div class="amazlet-sub-info" style="float: left;"><div class="amazlet-link" style="margin-top: 5px"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/144933668X/ikam-22/ref=nosim/" name="amazletlink" target="_blank">Amazon.co.jpで詳細を見る</a></div></div></div><div class="amazlet-footer" style="clear: left"></div></div>
↑はArunさんにもらった

<div class="amazlet-box" style="margin-bottom:0px;"><div class="amazlet-image" style="float:left;margin:0px 12px 1px 0px;"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/0137012896/ikam-22/ref=nosim/" name="amazletlink" target="_blank"><img src="http://ecx.images-amazon.com/images/I/51YYb-66DoL._SL160_.jpg" alt="Core JavaServer Faces (Core Series)" style="border: none;" /></a></div><div class="amazlet-info" style="line-height:120%; margin-bottom: 10px"><div class="amazlet-name" style="margin-bottom:10px;line-height:120%"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/0137012896/ikam-22/ref=nosim/" name="amazletlink" target="_blank">Core JavaServer Faces (Core Series)</a><div class="amazlet-powered-date" style="font-size:80%;margin-top:5px;line-height:120%">posted with <a href="http://www.amazlet.com/" title="amazlet" target="_blank">amazlet</a> at 13.05.19</div></div><div class="amazlet-detail">David Horstmann, Cay S. Geary <br />Prentice Hall <br />売り上げランキング: 35,572<br /></div><div class="amazlet-sub-info" style="float: left;"><div class="amazlet-link" style="margin-top: 5px"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/0137012896/ikam-22/ref=nosim/" name="amazletlink" target="_blank">Amazon.co.jpで詳細を見る</a></div></div></div><div class="amazlet-footer" style="clear: left"></div></div>


  [1]: http://www.myexpospace.com/JavaOne2012/SessionFiles/CON7042_PDF_7042_0001.pdf
  [2]: https://github.com/making/todo-tsa
