IK.AM

@making's tech note


JSFとJAX-RS 2.0実現するTSA(Thin-Server Architecture)アプリケーション

🗃 {Programming/Java/JavaEE7}
🗓 Updated at 2013-05-19T09:33:39Z  🗓 Created at 2013-05-19T09:33:39Z   🌎 English Page

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

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

Single Page Web Applications: Javascript End-to-end
Michael Mikowski Josh Powell
Manning Pubns Co

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

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

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

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

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

はじめてのNode.js -サーバーサイドJavaScriptでWebアプリを開発する-
松島 浩道
ソフトバンククリエイティブ
売り上げランキング: 11,816
Backbone.jsガイドブック
Backbone.jsガイドブック
posted with amazlet at 13.05.19
高橋 侑久
ラトルズ
売り上げランキング: 5,946

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でインジェクションした。@RestQualifierをつけるとREST実装に、@InMemoryQualifierをつけるとスタブ処理実装になる。 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ビギナーで、以下の本を参照しながら↑を作った

Java EE 6 Pocket Guide
Java EE 6 Pocket Guide
posted with amazlet at 13.05.19
Arun Gupta
Oreilly & Associates Inc
売り上げランキング: 36,258
↑はArunさんにもらった
Core JavaServer Faces (Core Series)
David Horstmann, Cay S. Geary
Prentice Hall
売り上げランキング: 35,572

✒️️ Edit  ⏰ History  🗑 Delete