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
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と相性が良さそうです。

0 件のコメント:

コメントを投稿