前回の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
-
package com.brightgenerous.extension.ibatis; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.Arrays; import javax.inject.Inject; import com.brightgenerous.extension.logging.Log; import com.brightgenerous.extension.logging.LogFactory; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.apache.ibatis.session.ExecutorType; import org.apache.ibatis.session.SqlSessionManager; import org.apache.ibatis.session.TransactionIsolationLevel; public final class TransactionalMethodInterceptor implements MethodInterceptor { private static final Class<?>[] CAUSE_TYPES = new Class[] { Throwable.class }; private static final Class<?>[] MESSAGE_CAUSE_TYPES = new Class[] { String.class, Throwable.class }; private static final Log log = LogFactory .getLog(TransactionalMethodInterceptor.class); @Inject private SqlSessionManager sqlSessionManager; private final boolean readOnly; private final ExecutorType executorType; private final TransactionIsolationLevel isolationLevel; private final boolean rollbackOnly; private final boolean force; private final Class<? extends Throwable> rethrowExceptionsAs; private final String exceptionMessage; public TransactionalMethodInterceptor() { this(false); } public TransactionalMethodInterceptor(boolean readOnly) { this(readOnly, ExecutorType.SIMPLE, null, false, false, Exception.class, ""); } public TransactionalMethodInterceptor(boolean readOnly, ExecutorType executorType, TransactionIsolationLevel isolationLevel, boolean rollbackOnly, boolean force, Class<? extends Throwable> rethrowExceptionsAs, String exceptionMessage) { this.readOnly = readOnly; this.executorType = executorType; this.isolationLevel = isolationLevel; this.rollbackOnly = rollbackOnly; this.force = force; this.rethrowExceptionsAs = rethrowExceptionsAs; this.exceptionMessage = exceptionMessage; } public void setSqlSessionManager(SqlSessionManager sqlSessionManager) { this.sqlSessionManager = sqlSessionManager; } @Override public Object invoke(MethodInvocation invocation) throws Throwable { Method interceptedMethod = invocation.getMethod(); String debugPrefix = null; if (log.isDebugEnabled()) { debugPrefix = String.format("[Intercepted method: %s]", interceptedMethod.toGenericString()); } boolean isSessionInherited = sqlSessionManager .isManagedSessionStarted(); if (isSessionInherited) { if (log.isDebugEnabled()) { log.debug(String.format( "%s - SqlSession already set for thread: %s", debugPrefix, Thread.currentThread().getId())); } } else { if (log.isDebugEnabled()) { log.debug(String .format("%s - SqlSession not set for thread: %s, creating a new one", debugPrefix, Thread.currentThread().getId())); } sqlSessionManager.startManagedSession(executorType, isolationLevel); sqlSessionManager.getConnection().setReadOnly(readOnly); } Object object = null; try { object = invocation.proceed(); if (!isSessionInherited && !rollbackOnly) { sqlSessionManager.commit(force); } } catch (Throwable t) { sqlSessionManager.rollback(force); for (Class<?> exceptionClass : interceptedMethod .getExceptionTypes()) { if (exceptionClass.isAssignableFrom(t.getClass())) { throw t; } } if (rethrowExceptionsAs.isAssignableFrom(t.getClass())) { throw t; } String errorMessage; Object[] initargs; Class<?>[] initargsType; if (exceptionMessage.length() != 0) { errorMessage = String.format(exceptionMessage, invocation.getArguments()); initargs = new Object[] { errorMessage, t }; initargsType = MESSAGE_CAUSE_TYPES; } else { initargs = new Object[] { t }; initargsType = CAUSE_TYPES; } Constructor<? extends Throwable> exceptionConstructor = getMatchingConstructor( rethrowExceptionsAs, initargsType); Throwable rethrowEx = null; if (exceptionConstructor != null) { try { rethrowEx = exceptionConstructor.newInstance(initargs); } catch (Exception e) { errorMessage = String .format("Impossible to re-throw '%s', it needs the constructor with %s argument(s).", rethrowExceptionsAs.getName(), Arrays.toString(initargsType)); log.error(errorMessage, e); rethrowEx = new RuntimeException(errorMessage, e); } } else { errorMessage = String .format("Impossible to re-throw '%s', it needs the constructor with %s or %s argument(s).", rethrowExceptionsAs.getName(), Arrays.toString(CAUSE_TYPES), Arrays.toString(MESSAGE_CAUSE_TYPES)); log.error(errorMessage); rethrowEx = new RuntimeException(errorMessage); } throw rethrowEx; } finally { if (!isSessionInherited) { if (rollbackOnly) { if (log.isDebugEnabled()) { log.debug(debugPrefix + " - SqlSession of thread: " + Thread.currentThread().getId() + " was in rollbackOnly mode, rolling it back"); } sqlSessionManager.rollback(true); } if (log.isDebugEnabled()) { log.debug(String .format("%s - SqlSession of thread: %s terminated its life-cycle, closing it", debugPrefix, Thread.currentThread().getId())); } sqlSessionManager.close(); } else if (log.isDebugEnabled()) { log.debug(String .format("%s - SqlSession of thread: %s is inherited, skipped close operation", debugPrefix, Thread.currentThread().getId())); } } return object; } private static <E extends Throwable> Constructor<E> getMatchingConstructor( Class<E> type, Class<?>[] argumentsType) { Class<? super E> currentType = type; while (Object.class != currentType) { for (Constructor<?> constructor : currentType.getConstructors()) { if (Arrays.equals(argumentsType, constructor.getParameterTypes())) { return (Constructor<E>) constructor; } } currentType = currentType.getSuperclass(); } return null; } }
Guiceでは、Spring3 + MyBatis3で使用したapplicationContext.xml、sqlMapConfig.xml、database.propertiesが不要になります。
かわりに、Guiceの場合はソースコードに設定を記述します。
以下が実行するテストコードです。beforeメソッド内の処理が設定になります。
- UserServiceTestGuice.java
-
package com.brightgenerous.sample.application.service; import java.lang.reflect.Method; import java.util.Properties; import java.util.Set; import com.brightgenerous.extension.ibatis.TransactionalMethodInterceptor; import com.brightgenerous.sample.application.bean.User; import com.brightgenerous.sample.application.dao.UserDao; import org.aopalliance.intercept.MethodInterceptor; import org.apache.ibatis.io.ResolverUtil; import org.apache.ibatis.session.AutoMappingBehavior; import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; import org.junit.Before; import org.junit.Test; import org.mybatis.guice.MyBatisModule; import org.mybatis.guice.datasource.builtin.PooledDataSourceProvider; import org.mybatis.guice.transactional.Transactional; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Scopes; import com.google.inject.matcher.AbstractMatcher; import com.google.inject.name.Names; public class UserServiceTestGuice { private Injector injector; @Before public void before() { injector = Guice.createInjector(new MyBatisModule() { @Override protected void initialize() { MethodInterceptor updateInterceptor = new TransactionalMethodInterceptor(); binder().requestInjection(updateInterceptor); binder().bindInterceptor(new AbstractMatcher<Class<?>>() { @Override public boolean matches(Class<?> x_arg0) { return x_arg0.getName().matches("^.*Service$"); } }, new AbstractMatcher<Method>() { @Override public boolean matches(Method x_arg0) { return (x_arg0.getAnnotation(Transactional.class) == null) && x_arg0.getName().matches( "^(regist|update|delete).*$"); } }, updateInterceptor); MethodInterceptor readInterceptor = new TransactionalMethodInterceptor( true); binder().requestInjection(readInterceptor); binder().bindInterceptor(new AbstractMatcher<Class<?>>() { @Override public boolean matches(Class<?> x_arg0) { return x_arg0.getName().matches("^.*Service$"); } }, new AbstractMatcher<Method>() { @Override public boolean matches(Method x_arg0) { return (x_arg0.getAnnotation(Transactional.class) == null) && !x_arg0.getName().matches( "^(regist|update|delete).*$"); } }, readInterceptor); environmentId("test"); bindDataSourceProviderType(PooledDataSourceProvider.class); bindTransactionFactoryType(JdbcTransactionFactory.class); Properties properties = new Properties(); properties.setProperty("JDBC.url", "jdbc:postgresql://127.0.0.1:5432/scheme"); properties.setProperty("JDBC.driver", "org.postgresql.Driver"); properties.setProperty("JDBC.username", "username"); properties.setProperty("JDBC.password", "password"); properties.setProperty("JDBC.autoCommit", "false"); Names.bindProperties(binder(), properties); useCacheEnabled(true); lazyLoadingEnabled(true); aggressiveLazyLoading(true); autoMappingBehavior(AutoMappingBehavior.PARTIAL); useColumnLabel(true); useGeneratedKeys(false); failFast(true); multipleResultSetsEnabled(true); addSimpleAliases("com.brightgenerous.sample.application.bean"); addMapperClasses(new ResolverUtil<Object>().find( new ResolverUtil.Test() { @Override public boolean matches(Class<?> x_arg0) { return x_arg0.getName().matches("^.*Dao$"); } }, "com.brightgenerous.sample.application.dao") .getClasses()); } }, new AbstractModule() { @Override protected void configure() { Set<Class<? extends Object>> services = new ResolverUtil<Object>() .find(new ResolverUtil.Test() { @Override public boolean matches(Class<?> x_arg0) { return x_arg0.getName().matches("^.*Service$"); } }, "com.brightgenerous.sample.application.service") .getClasses(); for (Class<? extends Object> service : services) { bind(service).in(Scopes.SINGLETON); } } }); } @Test public void test01() { UserService service = injector.getInstance(UserService.class); User user = new User(); user.setId(Integer.valueOf(1)); user = service.get(user); System.out.println(user.getName()); user.setName(user.getName() + "-"); service.update(user); System.out.println(service.get(user).getName()); System.out.println(service.equals(injector .getInstance(UserService.class))); System.out.println(injector.getInstance(UserDao.class).equals( injector.getInstance(UserDao.class))); } }
使ってみると、Guiceもかなり便利でした。
今回は、あえてSpringの場合と同じ設定にするためにトランザクションインターセプターを自作しましたが、
標準のトランザクションインターセプターとTransactionalアノテーションを使う方法が一般的だと思います。
設定をソースコードに記述するフレームワークとして、Wicketと相性が良さそうです。