2012年1月5日木曜日

Servlet3.0 と Guice

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

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

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

AbstractService.java
  1. package com.brightgenerous.sample;  
  2.   
  3. import java.lang.annotation.ElementType;  
  4. import java.lang.annotation.Retention;  
  5. import java.lang.annotation.RetentionPolicy;  
  6. import java.lang.annotation.Target;  
  7. import java.sql.Connection;  
  8.   
  9. public abstract class AbstractService {  
  10.   
  11.     @Target({ ElementType.METHOD })  
  12.     @Retention(RetentionPolicy.RUNTIME)  
  13.     public static @interface Transactional {  
  14.     }  
  15.   
  16.     private final ThreadLocal<Holder<Connection>> thlocal = new ThreadLocal<Holder<Connection>>() {  
  17.   
  18.         @Override  
  19.         protected Holder<Connection> initialValue() {  
  20.             return new Holder<Connection>();  
  21.         }  
  22.     };  
  23.   
  24.     protected Connection getConnection() {  
  25.         return thlocal.get().m_object;  
  26.     }  
  27.   
  28.     private void setConnection(Connection x_connection) {  
  29.         thlocal.get().m_object = x_connection;  
  30.     }  
  31.   
  32.     static class Holder<T> {  
  33.   
  34.         T m_object;  
  35.     }  
  36. }  

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

UserService.java
  1. package com.brightgenerous.sample.service;  
  2.   
  3. import java.sql.SQLException;  
  4. import java.util.ArrayList;  
  5. import java.util.List;  
  6. import java.util.Map;  
  7.   
  8.   
  9. import org.apache.commons.dbutils.QueryRunner;  
  10. import org.apache.commons.dbutils.handlers.MapListHandler;  
  11. import org.apache.commons.dbutils.handlers.ScalarHandler;  
  12.   
  13. import com.brightgenerous.sample.AbstractService;  
  14.   
  15. public interface UserService {  
  16.   
  17.     List<Map<String, Object>> findAll();  
  18.   
  19.     void add(Map<String, Object> x_data);  
  20.   
  21.     class UserServiceImpl extends AbstractService implements UserService {  
  22.   
  23.         @Transactional  
  24.         @Override  
  25.         public List<Map<String, Object>> findAll() {  
  26.             StringBuilder sql = new StringBuilder();  
  27.             sql.append("SELECT id, name, email FROM t_user ORDER BY id");  
  28.             try {  
  29.                 return new QueryRunner().query(getConnection(), sql.toString(),  
  30.                         new MapListHandler());  
  31.             } catch (SQLException e) {  
  32.                 throw new RuntimeException(e);  
  33.             }  
  34.         }  
  35.   
  36.         @Transactional  
  37.         @Override  
  38.         public void add(Map<String, Object> x_data) {  
  39.             x_data.put("id", nextId());  
  40.             StringBuilder sql = new StringBuilder();  
  41.             sql.append("INSERT INTO t_user(id, name, email) VALUES (?, ?, ?)");  
  42.             List<Object> params = new ArrayList<Object>();  
  43.             params.add(x_data.get("id"));  
  44.             params.add(x_data.get("name"));  
  45.             params.add(x_data.get("email"));  
  46.             try {  
  47.                 new QueryRunner().update(getConnection(), sql.toString(),  
  48.                         params.toArray());  
  49.             } catch (SQLException e) {  
  50.                 throw new RuntimeException(e);  
  51.             }  
  52.         }  
  53.   
  54.         private Number nextId() {  
  55.             StringBuilder sql = new StringBuilder();  
  56.             sql.append("SELECT COALESCE((SELECT MAX(id) FROM t_user), 0) + 1");  
  57.             try {  
  58.                 return (Number) new QueryRunner().query(getConnection(),  
  59.                         sql.toString(), new ScalarHandler());  
  60.             } catch (SQLException e) {  
  61.                 throw new RuntimeException(e);  
  62.             }  
  63.         }  
  64.     }  
  65. }  

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

AbstractHttpServlet.java
  1. package com.brightgenerous.sample.servlet;  
  2.   
  3. import java.io.IOException;  
  4.   
  5. import javax.servlet.ServletException;  
  6. import javax.servlet.http.HttpServlet;  
  7. import javax.servlet.http.HttpServletRequest;  
  8. import javax.servlet.http.HttpServletResponse;  
  9.   
  10. abstract class AbstractHttpServlet extends HttpServlet {  
  11.   
  12.     private static final long serialVersionUID = 2788992165026478005L;  
  13.   
  14.     @Override  
  15.     protected void doGet(HttpServletRequest x_request,  
  16.             HttpServletResponse x_response) throws ServletException,  
  17.             IOException {  
  18.   
  19.         doService(x_request, x_response);  
  20.     }  
  21.   
  22.     @Override  
  23.     protected void doPost(HttpServletRequest x_request,  
  24.             HttpServletResponse x_response) throws ServletException,  
  25.             IOException {  
  26.   
  27.         doService(x_request, x_response);  
  28.     }  
  29.   
  30.     abstract protected void doService(HttpServletRequest x_request,  
  31.             HttpServletResponse x_response) throws ServletException,  
  32.             IOException;  
  33. }  

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

UserAddServlet.java
  1. package com.brightgenerous.sample.servlet;  
  2.   
  3. import java.io.IOException;  
  4. import java.util.HashMap;  
  5. import java.util.List;  
  6. import java.util.Map;  
  7.   
  8. import javax.servlet.Servlet;  
  9. import javax.servlet.ServletException;  
  10. import javax.servlet.ServletOutputStream;  
  11. import javax.servlet.http.HttpServletRequest;  
  12. import javax.servlet.http.HttpServletResponse;  
  13.   
  14.   
  15. import com.brightgenerous.sample.service.UserService;  
  16. import com.google.inject.Inject;  
  17.   
  18. public interface UserAddServlet extends Servlet {  
  19.   
  20.     class UserAddServletImpl extends AbstractHttpServlet implements  
  21.             UserAddServlet {  
  22.   
  23.         private static final long serialVersionUID = -4751580520972958252L;  
  24.   
  25.         @Inject  
  26.         private UserService service;  
  27.   
  28.         @Override  
  29.         protected void doService(HttpServletRequest x_request,  
  30.                 HttpServletResponse x_response) throws ServletException,  
  31.                 IOException {  
  32.   
  33.             List<Map<String, Object>> datas = service.findAll();  
  34.             Map<String, Object> data = new HashMap<String, Object>();  
  35.             data.put("name", x_request.getParameter("name"));  
  36.             data.put("email", x_request.getParameter("email"));  
  37.   
  38.             service.add(data);  
  39.   
  40.             StringBuilder sb = new StringBuilder();  
  41.             sb.append("<html><head><title>servlet3 + guice</title></head><body>");  
  42.             sb.append("<a href=\"" + UserFormServlet.class.getSimpleName()  
  43.                     + "\">back</a>");  
  44.             sb.append("<table border=\"1\">");  
  45.             sb.append("<tr><th>name</th><th>e-mail</th></tr>");  
  46.             // ignore SQL injection...  
  47.             for (Map<String, Object> d : datas) {  
  48.                 sb.append("<tr><td>" + d.get("name") + "</td>");  
  49.                 sb.append("<td>" + d.get("email") + "</td></tr>");  
  50.             }  
  51.             sb.append("<tr><th>name</th><th>e-mail</th></tr>");  
  52.             sb.append("<tr><td>" + data.get("name") + "</td>");  
  53.             sb.append("<td>" + data.get("email") + "</td></tr>");  
  54.   
  55.             sb.append("</table>");  
  56.             sb.append("</body></html>");  
  57.   
  58.             ServletOutputStream out = x_response.getOutputStream();  
  59.             out.print(sb.toString());  
  60.             out.flush();  
  61.         }  
  62.     }  
  63. }  

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

InitialContextListener.java
  1. package com.brightgenerous.sample;  
  2.   
  3. import java.lang.reflect.InvocationTargetException;  
  4. import java.lang.reflect.Method;  
  5. import java.sql.Connection;  
  6. import java.sql.DriverManager;  
  7. import java.sql.SQLException;  
  8.   
  9. import javax.servlet.ServletContext;  
  10. import javax.servlet.ServletContextEvent;  
  11. import javax.servlet.ServletContextListener;  
  12. import javax.servlet.annotation.WebListener;  
  13.   
  14.   
  15. import org.aopalliance.intercept.MethodInterceptor;  
  16. import org.aopalliance.intercept.MethodInvocation;  
  17.   
  18. import com.brightgenerous.sample.AbstractService.Transactional;  
  19. import com.brightgenerous.sample.service.UserService;  
  20. import com.brightgenerous.sample.service.UserService.UserServiceImpl;  
  21. import com.brightgenerous.sample.servlet.UserAddServlet;  
  22. import com.brightgenerous.sample.servlet.UserFormServlet;  
  23. import com.brightgenerous.sample.servlet.UserAddServlet.UserAddServletImpl;  
  24. import com.brightgenerous.sample.servlet.UserFormServlet.UserFormServletImpl;  
  25. import com.google.inject.AbstractModule;  
  26. import com.google.inject.Guice;  
  27. import com.google.inject.Injector;  
  28. import com.google.inject.matcher.Matchers;  
  29.   
  30. @WebListener  
  31. public class InitialContextListener implements ServletContextListener {  
  32.   
  33.     @Override  
  34.     public void contextDestroyed(ServletContextEvent x_sce) {  
  35.     }  
  36.   
  37.     @Override  
  38.     public void contextInitialized(ServletContextEvent x_sce) {  
  39.   
  40.         Injector injector = Guice.createInjector(new AbstractModule() {  
  41.   
  42.             @Override  
  43.             protected void configure() {  
  44.                 bind(UserAddServlet.class).to(UserAddServletImpl.class);  
  45.                 bind(UserService.class).to(UserServiceImpl.class);  
  46.                 bindInterceptor(Matchers.subclassesOf(AbstractService.class),  
  47.                         Matchers.annotatedWith(Transactional.class),  
  48.                         new MethodInterceptorImpl());  
  49.             }  
  50.         });  
  51.   
  52.         ServletContext context = x_sce.getServletContext();  
  53.         context.addServlet(UserAddServlet.class.getSimpleName(),  
  54.                 injector.getInstance(UserAddServlet.class)).addMapping(  
  55.                 "/" + UserAddServlet.class.getSimpleName());  
  56.     }  
  57.   
  58.     static class MethodInterceptorImpl implements MethodInterceptor {  
  59.   
  60.         private static final String URL = "jdbc:postgresql://127.0.0.1:5432/db";  
  61.   
  62.         private static final String USERNAME = "username";  
  63.   
  64.         private static final String PASSWORD = "password";  
  65.   
  66.         private static final Method getter;  
  67.   
  68.         private static final Method setter;  
  69.   
  70.         static {  
  71.             try {  
  72.                 Class.forName("org.postgresql.Driver");  
  73.             } catch (ClassNotFoundException e) {  
  74.                 throw new RuntimeException(e);  
  75.             }  
  76.             try {  
  77.                 getter = AbstractService.class  
  78.                         .getDeclaredMethod("getConnection");  
  79.                 if (!getter.isAccessible()) {  
  80.                     getter.setAccessible(true);  
  81.                 }  
  82.                 setter = AbstractService.class.getDeclaredMethod(  
  83.                         "setConnection", Connection.class);  
  84.                 if (!setter.isAccessible()) {  
  85.                     setter.setAccessible(true);  
  86.                 }  
  87.             } catch (NoSuchMethodException e) {  
  88.                 throw new RuntimeException(e);  
  89.             }  
  90.         }  
  91.   
  92.         @Override  
  93.         public Object invoke(MethodInvocation x_arg0) throws Throwable {  
  94.             Object object = x_arg0.getThis();  
  95.             if (getter.invoke(object) != null) {  
  96.                 return x_arg0.proceed();  
  97.             }  
  98.   
  99.             Connection connection = DriverManager.getConnection(URL, USERNAME,  
  100.                     PASSWORD);  
  101.             if (connection.getAutoCommit()) {  
  102.                 connection.setAutoCommit(false);  
  103.             }  
  104.   
  105.             setter.invoke(object, connection);  
  106.   
  107.             Object result = null;  
  108.             try {  
  109.   
  110.                 result = x_arg0.proceed();  
  111.                 connection.commit();  
  112.   
  113.             } catch (Throwable e) {  
  114.                 try {  
  115.                     connection.rollback();  
  116.                 } catch (SQLException ex) {  
  117.                     ex.printStackTrace();  
  118.                 }  
  119.                 throw e;  
  120.             } finally {  
  121.                 try {  
  122.                     connection.close();  
  123.                 } catch (SQLException e) {  
  124.                     e.printStackTrace();  
  125.                 }  
  126.                 try {  
  127.   
  128.                     setter.invoke(object, (Connection) null);  
  129.   
  130.                 } catch (IllegalAccessException e) {  
  131.                     e.printStackTrace();  
  132.                 } catch (IllegalArgumentException e) {  
  133.                     e.printStackTrace();  
  134.                 } catch (InvocationTargetException e) {  
  135.                     e.printStackTrace();  
  136.                 }  
  137.             }  
  138.             return result;  
  139.         }  
  140.     }  
  141. }  

起動すると、以下の順に実行されます
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 件のコメント:

コメントを投稿