2012年1月5日木曜日

Servlet3.0 と Guice

最近、Servlet3.0のアノテーションによるゼロコンフィグを試していましたところ、 ふと、これってGuiceと相性いいんんじゃね?と思い至りました。
サクっと書いてみたServlet3.0 + Guice + JDBCってのを紹介します。

Servlet3.0からは、アノテーションでServletを登録できる他に、ServletContext#addServlet()を呼び出しての登録も可能になっています。
つまり、Guiceを利用して生成したServletインスタンスをマッピングさせることが可能だということです。
これをやってみようかと思います。

まず、トランザクション担当のクラスの親となるクラスを作成します。

AbstractService.java
package com.brightgenerous.sample;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.sql.Connection;

public abstract class AbstractService {

    @Target({ ElementType.METHOD })
    @Retention(RetentionPolicy.RUNTIME)
    public static @interface Transactional {
    }

    private final ThreadLocal<Holder<Connection>> thlocal = new ThreadLocal<Holder<Connection>>() {

        @Override
        protected Holder<Connection> initialValue() {
            return new Holder<Connection>();
        }
    };

    protected Connection getConnection() {
        return thlocal.get().m_object;
    }

    private void setConnection(Connection x_connection) {
        thlocal.get().m_object = x_connection;
    }

    static class Holder<T> {

        T m_object;
    }
}

次に、そのトランザクションの実装クラスを作成します。
インターフェースと実装クラスは1つずつなので、まとめて書いてしまいます。
便利なのでapache-commons-dbutilsを使用しています。

UserService.java
package com.brightgenerous.sample.service;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;


import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.MapListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;

import com.brightgenerous.sample.AbstractService;

public interface UserService {

    List<Map<String, Object>> findAll();

    void add(Map<String, Object> x_data);

    class UserServiceImpl extends AbstractService implements UserService {

        @Transactional
        @Override
        public List<Map<String, Object>> findAll() {
            StringBuilder sql = new StringBuilder();
            sql.append("SELECT id, name, email FROM t_user ORDER BY id");
            try {
                return new QueryRunner().query(getConnection(), sql.toString(),
                        new MapListHandler());
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }

        @Transactional
        @Override
        public void add(Map<String, Object> x_data) {
            x_data.put("id", nextId());
            StringBuilder sql = new StringBuilder();
            sql.append("INSERT INTO t_user(id, name, email) VALUES (?, ?, ?)");
            List<Object> params = new ArrayList<Object>();
            params.add(x_data.get("id"));
            params.add(x_data.get("name"));
            params.add(x_data.get("email"));
            try {
                new QueryRunner().update(getConnection(), sql.toString(),
                        params.toArray());
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }

        private Number nextId() {
            StringBuilder sql = new StringBuilder();
            sql.append("SELECT COALESCE((SELECT MAX(id) FROM t_user), 0) + 1");
            try {
                return (Number) new QueryRunner().query(getConnection(),
                        sql.toString(), new ScalarHandler());
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

いったん、Servletの抽象クラスを作成しておきます。

AbstractHttpServlet.java
package com.brightgenerous.sample.servlet;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

abstract class AbstractHttpServlet extends HttpServlet {

    private static final long serialVersionUID = 2788992165026478005L;

    @Override
    protected void doGet(HttpServletRequest x_request,
            HttpServletResponse x_response) throws ServletException,
            IOException {

        doService(x_request, x_response);
    }

    @Override
    protected void doPost(HttpServletRequest x_request,
            HttpServletResponse x_response) throws ServletException,
            IOException {

        doService(x_request, x_response);
    }

    abstract protected void doService(HttpServletRequest x_request,
            HttpServletResponse x_response) throws ServletException,
            IOException;
}

そして、Servletの実装クラスを作成します。
ここでも、インターフェースと実装クラスは1つずつなので、まとめて書いてしまいます。

UserAddServlet.java
package com.brightgenerous.sample.servlet;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


import com.brightgenerous.sample.service.UserService;
import com.google.inject.Inject;

public interface UserAddServlet extends Servlet {

    class UserAddServletImpl extends AbstractHttpServlet implements
            UserAddServlet {

        private static final long serialVersionUID = -4751580520972958252L;

        @Inject
        private UserService service;

        @Override
        protected void doService(HttpServletRequest x_request,
                HttpServletResponse x_response) throws ServletException,
                IOException {

            List<Map<String, Object>> datas = service.findAll();
            Map<String, Object> data = new HashMap<String, Object>();
            data.put("name", x_request.getParameter("name"));
            data.put("email", x_request.getParameter("email"));

            service.add(data);

            StringBuilder sb = new StringBuilder();
            sb.append("<html><head><title>servlet3 + guice</title></head><body>");
            sb.append("<a href=\"" + UserFormServlet.class.getSimpleName()
                    + "\">back</a>");
            sb.append("<table border=\"1\">");
            sb.append("<tr><th>name</th><th>e-mail</th></tr>");
            // ignore SQL injection...
            for (Map<String, Object> d : datas) {
                sb.append("<tr><td>" + d.get("name") + "</td>");
                sb.append("<td>" + d.get("email") + "</td></tr>");
            }
            sb.append("<tr><th>name</th><th>e-mail</th></tr>");
            sb.append("<tr><td>" + data.get("name") + "</td>");
            sb.append("<td>" + data.get("email") + "</td></tr>");

            sb.append("</table>");
            sb.append("</body></html>");

            ServletOutputStream out = x_response.getOutputStream();
            out.print(sb.toString());
            out.flush();
        }
    }
}

最後に、ServletContextListener実装クラスを作成します。
このクラスにweb.xmlに対応する処理を記述します。

InitialContextListener.java
package com.brightgenerous.sample;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;


import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import com.brightgenerous.sample.AbstractService.Transactional;
import com.brightgenerous.sample.service.UserService;
import com.brightgenerous.sample.service.UserService.UserServiceImpl;
import com.brightgenerous.sample.servlet.UserAddServlet;
import com.brightgenerous.sample.servlet.UserFormServlet;
import com.brightgenerous.sample.servlet.UserAddServlet.UserAddServletImpl;
import com.brightgenerous.sample.servlet.UserFormServlet.UserFormServletImpl;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.matcher.Matchers;

@WebListener
public class InitialContextListener implements ServletContextListener {

    @Override
    public void contextDestroyed(ServletContextEvent x_sce) {
    }

    @Override
    public void contextInitialized(ServletContextEvent x_sce) {

        Injector injector = Guice.createInjector(new AbstractModule() {

            @Override
            protected void configure() {
                bind(UserAddServlet.class).to(UserAddServletImpl.class);
                bind(UserService.class).to(UserServiceImpl.class);
                bindInterceptor(Matchers.subclassesOf(AbstractService.class),
                        Matchers.annotatedWith(Transactional.class),
                        new MethodInterceptorImpl());
            }
        });

        ServletContext context = x_sce.getServletContext();
        context.addServlet(UserAddServlet.class.getSimpleName(),
                injector.getInstance(UserAddServlet.class)).addMapping(
                "/" + UserAddServlet.class.getSimpleName());
    }

    static class MethodInterceptorImpl implements MethodInterceptor {

        private static final String URL = "jdbc:postgresql://127.0.0.1:5432/db";

        private static final String USERNAME = "username";

        private static final String PASSWORD = "password";

        private static final Method getter;

        private static final Method setter;

        static {
            try {
                Class.forName("org.postgresql.Driver");
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
            try {
                getter = AbstractService.class
                        .getDeclaredMethod("getConnection");
                if (!getter.isAccessible()) {
                    getter.setAccessible(true);
                }
                setter = AbstractService.class.getDeclaredMethod(
                        "setConnection", Connection.class);
                if (!setter.isAccessible()) {
                    setter.setAccessible(true);
                }
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public Object invoke(MethodInvocation x_arg0) throws Throwable {
            Object object = x_arg0.getThis();
            if (getter.invoke(object) != null) {
                return x_arg0.proceed();
            }

            Connection connection = DriverManager.getConnection(URL, USERNAME,
                    PASSWORD);
            if (connection.getAutoCommit()) {
                connection.setAutoCommit(false);
            }

            setter.invoke(object, connection);

            Object result = null;
            try {

                result = x_arg0.proceed();
                connection.commit();

            } catch (Throwable e) {
                try {
                    connection.rollback();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
                throw e;
            } finally {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
                try {

                    setter.invoke(object, (Connection) null);

                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (IllegalArgumentException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
            return result;
        }
    }
}

起動すると、以下の順に実行されます
1.WebListenerアノテーションによってInitialContextListenerをServletContextListenerとして登録
2.contextInitializedが実行される
2-1.GuiceでAbstractServiceのサブクラスのTransactionalアノテーションが付いているメソッドに対してMethodInterceptorImplを設定しておく
2-2.GuiceがUserAddServletのインスタンスを生成する際にUserServiceを実装したクラスのインスタンスを注入
2-3.UserAddServletのインスタンスをServletとして登録

http://[SERVER_NAME]:[PORT_NUMBER]/[CONTEXT_ROOT]/UserAddServlet?name=brigen_name&email=brigen_mail
にRequestを送ると、サーブレットが実行されます。

かなり簡単にServletが書けました。
実際に活用することを考えたらトランザクションの実装はもっと細かくすべきでしょうが。

もちろん、GlassFishなどのJavaEE環境ではServlet3.0 + EJB Lite + JPAの選択が最適でしょう。

0 件のコメント:

コメントを投稿