2012年1月10日火曜日

Guice3 + MyBatis3

前回のSpring3 + MyBatis3のSpringをGuiceに置き換えてみます。

User.java、UserDao.java、UserDao.xml、UserService.javaは前回と同じ物をそのまま利用します。

Guice-MyBatisプラグインの標準のトランザクションインターセプターではreadOnlyの設定ができない(rollbackOnlyは可能)ため、 readOnlyに対応したトランザクションインターセプターを自作します。
ソースコードはGuice-MyBatisプラグインの標準のトランザクションインターセプターを参考にしています。

※PooledDataSourceProviderではなく、BasicDataSourceProviderを使用すればdefaultReadOnlyの設定が可能でした。
ただし、その場合には標準のトランザクションインターセプターではreadOnlyをfalseに設定しなおす事がないようで、更新系の処理ができません。

TransactionalMethodInterceptor.java
  1. package com.brightgenerous.extension.ibatis;  
  2.   
  3. import java.lang.reflect.Constructor;  
  4. import java.lang.reflect.Method;  
  5. import java.util.Arrays;  
  6.   
  7. import javax.inject.Inject;  
  8.   
  9. import com.brightgenerous.extension.logging.Log;  
  10. import com.brightgenerous.extension.logging.LogFactory;  
  11.   
  12. import org.aopalliance.intercept.MethodInterceptor;  
  13. import org.aopalliance.intercept.MethodInvocation;  
  14. import org.apache.ibatis.session.ExecutorType;  
  15. import org.apache.ibatis.session.SqlSessionManager;  
  16. import org.apache.ibatis.session.TransactionIsolationLevel;  
  17.   
  18. public final class TransactionalMethodInterceptor implements MethodInterceptor {  
  19.   
  20.     private static final Class<?>[] CAUSE_TYPES = new Class[] { Throwable.class };  
  21.   
  22.     private static final Class<?>[] MESSAGE_CAUSE_TYPES = new Class[] {  
  23.             String.class, Throwable.class };  
  24.   
  25.     private static final Log log = LogFactory  
  26.             .getLog(TransactionalMethodInterceptor.class);  
  27.   
  28.     @Inject  
  29.     private SqlSessionManager sqlSessionManager;  
  30.   
  31.     private final boolean readOnly;  
  32.   
  33.     private final ExecutorType executorType;  
  34.   
  35.     private final TransactionIsolationLevel isolationLevel;  
  36.   
  37.     private final boolean rollbackOnly;  
  38.   
  39.     private final boolean force;  
  40.   
  41.     private final Class<? extends Throwable> rethrowExceptionsAs;  
  42.   
  43.     private final String exceptionMessage;  
  44.   
  45.     public TransactionalMethodInterceptor() {  
  46.         this(false);  
  47.     }  
  48.   
  49.     public TransactionalMethodInterceptor(boolean readOnly) {  
  50.         this(readOnly, ExecutorType.SIMPLE, nullfalsefalse,  
  51.                 Exception.class"");  
  52.     }  
  53.   
  54.     public TransactionalMethodInterceptor(boolean readOnly,  
  55.             ExecutorType executorType,  
  56.             TransactionIsolationLevel isolationLevel, boolean rollbackOnly,  
  57.             boolean force, Class<? extends Throwable> rethrowExceptionsAs,  
  58.             String exceptionMessage) {  
  59.         this.readOnly = readOnly;  
  60.         this.executorType = executorType;  
  61.         this.isolationLevel = isolationLevel;  
  62.         this.rollbackOnly = rollbackOnly;  
  63.         this.force = force;  
  64.         this.rethrowExceptionsAs = rethrowExceptionsAs;  
  65.         this.exceptionMessage = exceptionMessage;  
  66.     }  
  67.   
  68.     public void setSqlSessionManager(SqlSessionManager sqlSessionManager) {  
  69.         this.sqlSessionManager = sqlSessionManager;  
  70.     }  
  71.   
  72.     @Override  
  73.     public Object invoke(MethodInvocation invocation) throws Throwable {  
  74.         Method interceptedMethod = invocation.getMethod();  
  75.   
  76.         String debugPrefix = null;  
  77.         if (log.isDebugEnabled()) {  
  78.             debugPrefix = String.format("[Intercepted method: %s]",  
  79.                     interceptedMethod.toGenericString());  
  80.         }  
  81.   
  82.         boolean isSessionInherited = sqlSessionManager  
  83.                 .isManagedSessionStarted();  
  84.   
  85.         if (isSessionInherited) {  
  86.             if (log.isDebugEnabled()) {  
  87.                 log.debug(String.format(  
  88.                         "%s - SqlSession already set for thread: %s",  
  89.                         debugPrefix, Thread.currentThread().getId()));  
  90.             }  
  91.         } else {  
  92.             if (log.isDebugEnabled()) {  
  93.                 log.debug(String  
  94.                         .format("%s - SqlSession not set for thread: %s, creating a new one",  
  95.                                 debugPrefix, Thread.currentThread().getId()));  
  96.             }  
  97.   
  98.             sqlSessionManager.startManagedSession(executorType, isolationLevel);  
  99.             sqlSessionManager.getConnection().setReadOnly(readOnly);  
  100.         }  
  101.   
  102.         Object object = null;  
  103.         try {  
  104.             object = invocation.proceed();  
  105.   
  106.             if (!isSessionInherited && !rollbackOnly) {  
  107.                 sqlSessionManager.commit(force);  
  108.             }  
  109.         } catch (Throwable t) {  
  110.             sqlSessionManager.rollback(force);  
  111.   
  112.             for (Class<?> exceptionClass : interceptedMethod  
  113.                     .getExceptionTypes()) {  
  114.                 if (exceptionClass.isAssignableFrom(t.getClass())) {  
  115.                     throw t;  
  116.                 }  
  117.             }  
  118.   
  119.             if (rethrowExceptionsAs.isAssignableFrom(t.getClass())) {  
  120.                 throw t;  
  121.             }  
  122.   
  123.             String errorMessage;  
  124.             Object[] initargs;  
  125.             Class<?>[] initargsType;  
  126.   
  127.             if (exceptionMessage.length() != 0) {  
  128.                 errorMessage = String.format(exceptionMessage,  
  129.                         invocation.getArguments());  
  130.                 initargs = new Object[] { errorMessage, t };  
  131.                 initargsType = MESSAGE_CAUSE_TYPES;  
  132.             } else {  
  133.                 initargs = new Object[] { t };  
  134.                 initargsType = CAUSE_TYPES;  
  135.             }  
  136.   
  137.             Constructor<? extends Throwable> exceptionConstructor = getMatchingConstructor(  
  138.                     rethrowExceptionsAs, initargsType);  
  139.             Throwable rethrowEx = null;  
  140.             if (exceptionConstructor != null) {  
  141.                 try {  
  142.                     rethrowEx = exceptionConstructor.newInstance(initargs);  
  143.                 } catch (Exception e) {  
  144.                     errorMessage = String  
  145.                             .format("Impossible to re-throw '%s', it needs the constructor with %s argument(s).",  
  146.                                     rethrowExceptionsAs.getName(),  
  147.                                     Arrays.toString(initargsType));  
  148.                     log.error(errorMessage, e);  
  149.                     rethrowEx = new RuntimeException(errorMessage, e);  
  150.                 }  
  151.             } else {  
  152.                 errorMessage = String  
  153.                         .format("Impossible to re-throw '%s', it needs the constructor with %s or %s argument(s).",  
  154.                                 rethrowExceptionsAs.getName(),  
  155.                                 Arrays.toString(CAUSE_TYPES),  
  156.                                 Arrays.toString(MESSAGE_CAUSE_TYPES));  
  157.                 log.error(errorMessage);  
  158.                 rethrowEx = new RuntimeException(errorMessage);  
  159.             }  
  160.   
  161.             throw rethrowEx;  
  162.         } finally {  
  163.             if (!isSessionInherited) {  
  164.                 if (rollbackOnly) {  
  165.                     if (log.isDebugEnabled()) {  
  166.                         log.debug(debugPrefix + " - SqlSession of thread: "  
  167.                                 + Thread.currentThread().getId()  
  168.                                 + " was in rollbackOnly mode, rolling it back");  
  169.                     }  
  170.   
  171.                     sqlSessionManager.rollback(true);  
  172.                 }  
  173.   
  174.                 if (log.isDebugEnabled()) {  
  175.                     log.debug(String  
  176.                             .format("%s - SqlSession of thread: %s terminated its life-cycle, closing it",  
  177.                                     debugPrefix, Thread.currentThread().getId()));  
  178.                 }  
  179.   
  180.                 sqlSessionManager.close();  
  181.             } else if (log.isDebugEnabled()) {  
  182.                 log.debug(String  
  183.                         .format("%s - SqlSession of thread: %s is inherited, skipped close operation",  
  184.                                 debugPrefix, Thread.currentThread().getId()));  
  185.             }  
  186.         }  
  187.   
  188.         return object;  
  189.     }  
  190.   
  191.     private static <E extends Throwable> Constructor<E> getMatchingConstructor(  
  192.             Class<E> type, Class<?>[] argumentsType) {  
  193.         Class<? super E> currentType = type;  
  194.         while (Object.class != currentType) {  
  195.             for (Constructor<?> constructor : currentType.getConstructors()) {  
  196.                 if (Arrays.equals(argumentsType,  
  197.                         constructor.getParameterTypes())) {  
  198.                     return (Constructor<E>) constructor;  
  199.                 }  
  200.             }  
  201.             currentType = currentType.getSuperclass();  
  202.         }  
  203.         return null;  
  204.     }  
  205. }  

Guiceでは、Spring3 + MyBatis3で使用したapplicationContext.xml、sqlMapConfig.xml、database.propertiesが不要になります。
かわりに、Guiceの場合はソースコードに設定を記述します。
以下が実行するテストコードです。beforeメソッド内の処理が設定になります。

UserServiceTestGuice.java
  1. package com.brightgenerous.sample.application.service;  
  2.   
  3. import java.lang.reflect.Method;  
  4. import java.util.Properties;  
  5. import java.util.Set;  
  6.   
  7. import com.brightgenerous.extension.ibatis.TransactionalMethodInterceptor;  
  8. import com.brightgenerous.sample.application.bean.User;  
  9. import com.brightgenerous.sample.application.dao.UserDao;  
  10.   
  11. import org.aopalliance.intercept.MethodInterceptor;  
  12. import org.apache.ibatis.io.ResolverUtil;  
  13. import org.apache.ibatis.session.AutoMappingBehavior;  
  14. import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;  
  15. import org.junit.Before;  
  16. import org.junit.Test;  
  17. import org.mybatis.guice.MyBatisModule;  
  18. import org.mybatis.guice.datasource.builtin.PooledDataSourceProvider;  
  19. import org.mybatis.guice.transactional.Transactional;  
  20.   
  21. import com.google.inject.AbstractModule;  
  22. import com.google.inject.Guice;  
  23. import com.google.inject.Injector;  
  24. import com.google.inject.Scopes;  
  25. import com.google.inject.matcher.AbstractMatcher;  
  26. import com.google.inject.name.Names;  
  27.   
  28. public class UserServiceTestGuice {  
  29.   
  30.     private Injector injector;  
  31.   
  32.     @Before  
  33.     public void before() {  
  34.   
  35.         injector = Guice.createInjector(new MyBatisModule() {  
  36.   
  37.             @Override  
  38.             protected void initialize() {  
  39.   
  40.                 MethodInterceptor updateInterceptor = new TransactionalMethodInterceptor();  
  41.                 binder().requestInjection(updateInterceptor);  
  42.                 binder().bindInterceptor(new AbstractMatcher<Class<?>>() {  
  43.   
  44.                     @Override  
  45.                     public boolean matches(Class<?> x_arg0) {  
  46.                         return x_arg0.getName().matches("^.*Service$");  
  47.                     }  
  48.                 }, new AbstractMatcher<Method>() {  
  49.   
  50.                     @Override  
  51.                     public boolean matches(Method x_arg0) {  
  52.                         return (x_arg0.getAnnotation(Transactional.class) == null)  
  53.                                 && x_arg0.getName().matches(  
  54.                                         "^(regist|update|delete).*$");  
  55.                     }  
  56.                 }, updateInterceptor);  
  57.                 MethodInterceptor readInterceptor = new TransactionalMethodInterceptor(  
  58.                         true);  
  59.                 binder().requestInjection(readInterceptor);  
  60.                 binder().bindInterceptor(new AbstractMatcher<Class<?>>() {  
  61.   
  62.                     @Override  
  63.                     public boolean matches(Class<?> x_arg0) {  
  64.                         return x_arg0.getName().matches("^.*Service$");  
  65.                     }  
  66.                 }, new AbstractMatcher<Method>() {  
  67.   
  68.                     @Override  
  69.                     public boolean matches(Method x_arg0) {  
  70.                         return (x_arg0.getAnnotation(Transactional.class) == null)  
  71.                                 && !x_arg0.getName().matches(  
  72.                                         "^(regist|update|delete).*$");  
  73.                     }  
  74.                 }, readInterceptor);  
  75.   
  76.                 environmentId("test");  
  77.   
  78.                 bindDataSourceProviderType(PooledDataSourceProvider.class);  
  79.                 bindTransactionFactoryType(JdbcTransactionFactory.class);  
  80.   
  81.                 Properties properties = new Properties();  
  82.                 properties.setProperty("JDBC.url",  
  83.                         "jdbc:postgresql://127.0.0.1:5432/scheme");  
  84.                 properties.setProperty("JDBC.driver""org.postgresql.Driver");  
  85.                 properties.setProperty("JDBC.username""username");  
  86.                 properties.setProperty("JDBC.password""password");  
  87.                 properties.setProperty("JDBC.autoCommit""false");  
  88.                 Names.bindProperties(binder(), properties);  
  89.   
  90.                 useCacheEnabled(true);  
  91.                 lazyLoadingEnabled(true);  
  92.                 aggressiveLazyLoading(true);  
  93.                 autoMappingBehavior(AutoMappingBehavior.PARTIAL);  
  94.                 useColumnLabel(true);  
  95.                 useGeneratedKeys(false);  
  96.                 failFast(true);  
  97.                 multipleResultSetsEnabled(true);  
  98.   
  99.                 addSimpleAliases("com.brightgenerous.sample.application.bean");  
  100.   
  101.                 addMapperClasses(new ResolverUtil<Object>().find(  
  102.                         new ResolverUtil.Test() {  
  103.   
  104.                             @Override  
  105.                             public boolean matches(Class<?> x_arg0) {  
  106.                                 return x_arg0.getName().matches("^.*Dao$");  
  107.                             }  
  108.                         }, "com.brightgenerous.sample.application.dao")  
  109.                         .getClasses());  
  110.             }  
  111.         },  
  112.   
  113.         new AbstractModule() {  
  114.   
  115.             @Override  
  116.             protected void configure() {  
  117.                 Set<Class<? extends Object>> services = new ResolverUtil<Object>()  
  118.                         .find(new ResolverUtil.Test() {  
  119.   
  120.                             @Override  
  121.                             public boolean matches(Class<?> x_arg0) {  
  122.                                 return x_arg0.getName().matches("^.*Service$");  
  123.                             }  
  124.                         }, "com.brightgenerous.sample.application.service")  
  125.                         .getClasses();  
  126.                 for (Class<? extends Object> service : services) {  
  127.                     bind(service).in(Scopes.SINGLETON);  
  128.                 }  
  129.             }  
  130.         });  
  131.     }  
  132.   
  133.     @Test  
  134.     public void test01() {  
  135.         UserService service = injector.getInstance(UserService.class);  
  136.         User user = new User();  
  137.         user.setId(Integer.valueOf(1));  
  138.         user = service.get(user);  
  139.         System.out.println(user.getName());  
  140.         user.setName(user.getName() + "-");  
  141.         service.update(user);  
  142.         System.out.println(service.get(user).getName());  
  143.         System.out.println(service.equals(injector  
  144.                 .getInstance(UserService.class)));  
  145.         System.out.println(injector.getInstance(UserDao.class).equals(  
  146.                 injector.getInstance(UserDao.class)));  
  147.     }  
  148. }  

使ってみると、Guiceもかなり便利でした。
今回は、あえてSpringの場合と同じ設定にするためにトランザクションインターセプターを自作しましたが、 標準のトランザクションインターセプターとTransactionalアノテーションを使う方法が一般的だと思います。

設定をソースコードに記述するフレームワークとして、Wicketと相性が良さそうです。

0 件のコメント:

コメントを投稿