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

2012年1月9日月曜日

Spring3 + MyBatis3

MyBatis3がMyBatis2に比べて結構使いやすくなっています。
特に実装クラスを必要としないMapperの機能が便利です。
他には、XMLでognl式が使えたり、使いやすいタグが増えています。

Spring3 + MyBatis3の実装例で試してみます。

まず、データクラスを作成します。

User.java
  1. package com.brightgenerous.sample.application.bean;  
  2.   
  3. import java.io.Serializable;  
  4.   
  5. public class User implements Serializable {  
  6.   
  7.     private static final long serialVersionUID = -1056270813521861315L;  
  8.   
  9.     private Integer m_id;  
  10.   
  11.     private String m_name;  
  12.   
  13.     private String m_email;  
  14.   
  15.     public Integer getId() {  
  16.         return m_id;  
  17.     }  
  18.   
  19.     public void setId(Integer x_id) {  
  20.         m_id = x_id;  
  21.     }  
  22.   
  23.     public String getName() {  
  24.         return m_name;  
  25.     }  
  26.   
  27.     public void setName(String x_name) {  
  28.         m_name = x_name;  
  29.     }  
  30.   
  31.     public String getEmail() {  
  32.         return m_email;  
  33.     }  
  34.   
  35.     public void setEmail(String x_email) {  
  36.         m_email = x_email;  
  37.     }  
  38. }  

次に、Dao(Mapper)インターフェースを作成します。

UserDao.java
  1. package com.brightgenerous.sample.application.dao;  
  2.   
  3. import com.brightgenerous.sample.application.bean.User;  
  4.   
  5. public interface UserDao {  
  6.   
  7.     User select(User x_user);  
  8.   
  9.     int insert(User x_user);  
  10.   
  11.     int update(User x_user);  
  12.   
  13.     int delete(User x_user);  
  14. }  

そして、UserDao.javaと同じパッケージにUserDao.xmlを置きます。(拡張子以外のファイル名を同じにする)
それだけでインターフェースとXMLファイルが関連付けられます。

UserDao.xml
  1. <!--xml version="1.0" encoding="UTF-8" ?-->  
  2. <!--DOCTYPE mapper PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN" "http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd"-->  
  3.   
  4. <mapper namespace="com.brightgenerous.sample.application.dao.UserDao">  
  5.   
  6.  <cache flushinterval="3600000">  
  7.   
  8.  <resultmap type="User" id="User">  
  9.   <result column="id" property="id">  
  10.   <result column="name" property="name">  
  11.   <result column="email" property="email">  
  12.  </result></result></result></resultmap>  
  13.   
  14.  <select id="select" parametertype="User" resulttype="User" resultmap="User">  
  15.   select  
  16.    *  
  17.   from  
  18.    t_user  
  19.     
  20.      
  21.     and 1 = 0  
  22.     and id = #{id}  
  23.      
  24.     
  25.  </select>  
  26.   
  27.  <insert id="insert" parametertype="User">  
  28.   <selectkey keyproperty="id" resulttype="Integer" order="BEFORE">  
  29.    select coalesce((select max(id) from t_user),0) + 1 as id  
  30.   </selectkey>  
  31.   insert into  
  32.    t_user (  
  33.     id,  
  34.     name,  
  35.     email  
  36.    ) values (  
  37.     #{id},  
  38.     #{name},  
  39.     #{email}  
  40.    )  
  41.  </insert>  
  42.   
  43.  <update id="update" parametertype="User">  
  44.   update  
  45.    t_user  
  46.   set  
  47.    name = #{name},  
  48.    email = #{email}  
  49.   where  
  50.    id = #{id}  
  51.  </update>  
  52.   
  53.  <delete id="delete" parametertype="User">  
  54.   delete from  
  55.    t_user  
  56.   where  
  57.    id = #{id}  
  58.  </delete>  
  59.   
  60. </cache></mapper>  

最後に、トランザクション境界となるServiceクラスを作成します。

UserService.java
  1. package com.brightgenerous.sample.application.service;  
  2.   
  3. import javax.inject.Inject;  
  4.   
  5. import com.brightgenerous.sample.application.bean.User;  
  6. import com.brightgenerous.sample.application.dao.UserDao;  
  7.   
  8. public class UserService {  
  9.   
  10.     @Inject  
  11.     private UserDao m_userDao;  
  12.   
  13.     public User get(User x_user) {  
  14.         return m_userDao.select(x_user);  
  15.     }  
  16.   
  17.     public void update(User x_user) {  
  18.         m_userDao.update(x_user);  
  19.     }  
  20. }  

あとは設定ファイルを書くだけです。
注入先フィールドのInjectアノテーションはjavax.inject.Injectです。
実は、上記のすべてのクラスでMyBatisやSpringに関するインポートがないのです。

applicationContext.xml
  1. <!--xml version="1.0" encoding="UTF-8" ?-->  
  2. <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemalocation="  
  3.    http://www.springframework.org/schema/beans  
  4.    http://www.springframework.org/schema/beans/spring-beans-3.1.xsd  
  5.    http://www.springframework.org/schema/context  
  6.    http://www.springframework.org/schema/context/spring-context-3.1.xsd  
  7.    http://www.springframework.org/schema/jee  
  8.    http://www.springframework.org/schema/jee/spring-jee-3.1.xsd">  
  9.   
  10.  <!-- DATABASE CONFIG -->  
  11.  <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">  
  12.   <property name="locations" value="classpath:database.properties">  
  13.  </property></bean>  
  14.   
  15.  <!-- DATA SOURCE -->  
  16.  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">  
  17.   <property name="driverClassName" value="${jdbc.driverClassName}">  
  18.   <property name="url" value="${jdbc.url}">  
  19.   <property name="username" value="${jdbc.username}">  
  20.   <property name="password" value="${jdbc.password}">  
  21.   <property name="defaultAutoCommit" value="true">  
  22.   <property name="removeAbandoned" value="true">  
  23.  </property></property></property></property></property></property></bean>  
  24.   
  25.  <!-- TRANSACTION -->  
  26.  <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
  27.   <property name="dataSource" ref="dataSource">  
  28.  </property></bean>  
  29.  <bean id="transactionAttributeSource" class="org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource">  
  30.   <property name="properties">  
  31.    <props>  
  32.     <prop key="regist*">PROPAGATION_REQUIRED</prop>  
  33.     <prop key="update*">PROPAGATION_REQUIRED</prop>  
  34.     <prop key="delete*">PROPAGATION_REQUIRED</prop>  
  35.     <prop key="*">PROPAGATION_REQUIRED,readOnly</prop>  
  36.    </props>  
  37.   </property>  
  38.  </bean>  
  39.  <bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">  
  40.   <property name="transactionManager" ref="transactionManager">  
  41.   <property name="transactionAttributeSource" ref="transactionAttributeSource">  
  42.  </property></property></bean>  
  43.  <bean id="beanNameAutoProxy" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">  
  44.   <property name="interceptorNames" value="transactionInterceptor">  
  45.   <property name="beanNames" value="*Service">  
  46.  </property></property></bean>  
  47.   
  48.  <!-- MYBATIS3 SPRING3 -->  
  49.  <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">  
  50.   <property name="configLocation" value="classpath:sqlMapConfig.xml">  
  51.   <property name="dataSource" ref="dataSource">  
  52.  </property></property></bean>  
  53.   
  54.  <!-- PACKAGE IMPLEMENTS -->  
  55.  <context:component-scan base-package="com.brightgenerous.sample.application.service" name-generator="com.brightgenerous.extension.spring3.FqcnBeanNameGenerator" use-default-filters="false">  
  56.   <context:include-filter type="regex" expression=".*Service">  
  57.  </context:include-filter></context:component-scan>  
  58.  <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">  
  59.   <property name="basePackage" value="com.brightgenerous.sample.application.dao">  
  60.  </property></bean>  
  61.   
  62. </beans>  

applicationContext.xmlには com.brightgenerous.extension.spring3.FqcnBeanNameGeneratorというクラスがありますが、
これは、ただFQCNを返すだけのSpringのBeanNameGenerator実装です。
database.propertiesファイルは特にここで書くほどでもないので省略します。

sqlMapConfig.xml
  1. <!--xml version="1.0" encoding="UTF-8" ?-->  
  2. <!--DOCTYPE configuration PUBLIC "-//ibatis.apache.org//DTD Config 3.0//EN" "http://ibatis.apache.org/dtd/ibatis-3-config.dtd"-->  
  3.   
  4. <configuration>  
  5.   
  6.  <settings>  
  7.   <setting name="cacheEnabled" value="true">  
  8.   <setting name="lazyLoadingEnabled" value="true">  
  9.   <setting name="multipleResultSetsEnabled" value="true">  
  10.   <setting name="aggressiveLazyLoading" value="true">  
  11.   <setting name="useColumnLabel" value="true">  
  12.   <setting name="useGeneratedKeys" value="false">  
  13.   <setting name="autoMappingBehavior" value="PARTIAL">  
  14.   <setting name="defaultExecutorType" value="REUSE">  
  15.   <setting name="safeRowBoundsEnabled" value="true">  
  16.   <setting name="mapUnderscoreToCamelCase" value="false">  
  17.  </setting></setting></setting></setting></setting></setting></setting></setting></setting></setting></settings>  
  18.   
  19.  <typealiases>  
  20.   <package name="com.brightgenerous.sample.application.bean">  
  21.  </package></typealiases>  
  22.   
  23. </configuration>  

実行方法は以下のようになります。

UserServiceTestSpring
  1. package com.brightgenerous.sample.application.service;  
  2.   
  3. import com.brightgenerous.sample.application.bean.User;  
  4. import com.brightgenerous.sample.application.dao.UserDao;  
  5.   
  6. import org.junit.Before;  
  7. import org.junit.Test;  
  8. import org.springframework.context.ApplicationContext;  
  9. import org.springframework.context.support.ClassPathXmlApplicationContext;  
  10.   
  11. public class UserServiceTestSpring {  
  12.   
  13.     private ApplicationContext context;  
  14.   
  15.     @Before  
  16.     public void before() {  
  17.         context = new ClassPathXmlApplicationContext("applicationContext.xml");  
  18.     }  
  19.   
  20.     @Test  
  21.     public void test01() {  
  22.         UserService service = context.getBean(UserService.class);  
  23.         User user = new User();  
  24.         user.setId(Integer.valueOf(1));  
  25.         user = service.get(user);  
  26.         user.setName(user.getName() + "-");  
  27.         service.update(user);  
  28.     }  
  29. }  

これ以上ソースコードの記述量を減らせないんじゃないのかと思えるほど、簡単になりました。
設定ファイルは一度書いてしまえば、以降は編集しないので、 実際に作成するファイルはデータクラス、Mapperインターフェース、MapperのXMLファイル、サービスクラスの4つです。

次回は、SpringをGuiceに置き換えて同じ機能を実現してみようと思います。
具体的には、Springでの設定ファイルにあたる部分をソースで書くだけで、Mapperやサービスクラスには手を加えません。

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の選択が最適でしょう。

2011年8月9日火曜日

Androidアプリを登録しました

昨日、個人で使うために作ったアプリを公開しました。

どんなアプリかというと、最近使用したアプリのショートカットを表示するウィジェットです。

Android端末のユーザの大半は、その機能はHomeボタン長押しで使えるとご認識しているでしょう。
しかしながら、実は、私の使用しているAndroid端末(IS05)では、Home長押しは「起動中のアプリ」を表示します。
さらには、「最近起動したアプリ」を表示する機能が見当たりません(案外、調べればありそうですが・・・)。

と、いうような経緯があり、それならばウィジェットでその機能を提供し、ワンタッチで起動できるアプリとして作成してみました。

つまるところ、ただの俺得アプリに過ぎませんが、興味がありましたら、使ってみてください。
Android Market - 最近アプリ

2011年7月23日土曜日

JavaSE7のtryは便利

7月28日に正式リリースされるJavaSE7では、いくつか便利な構文が増えます。
今回は、その中でも特に有用であろうtryの新しい構文について調べてみました。

ファイルのコピーを例にして、tryの構文の拡張を調べてみます
JavaSE6以前のソースコードをJavaSE7のコードに置き換えます。

これまで(JavaSE6以前)のコード
  1. FileInputStream fis = null;  
  2. FileOutputStream fos = null;  
  3. try {  
  4.  fis = new FileInputStream("D:\\test.txt");  
  5.  fos = new FileOutputStream("D:\\test2.txt");  
  6.  byte[] bytes = new byte[1024];  
  7.  int size = -1;  
  8.  while ((size = fis.read(bytes)) != -1) {  
  9.   fos.write(bytes, 0, size);  
  10.  }  
  11. catch (FileNotFoundException e) {  
  12.  e.printStackTrace();  
  13. catch (IOException e) {   
  14.  e.printStackTrace();  
  15. finally {  
  16.  if (fos != null) {  
  17.   try {  
  18.    fos.close();  
  19.   } catch (IOException ex) {  
  20.   }  
  21.  }  
  22.  if (fis != null) {  
  23.   try {  
  24.    fis.close();  
  25.   } catch (IOException ex) {  
  26.   }  
  27.  }  
  28. }  

これから、上記のソースコードをJavaSE7風に書き換えていきます。

例外のマルチキャッチ
catchの部分をひとつにまとめることができます
  1. FileInputStream fis = null;  
  2. FileOutputStream fos = null;  
  3. try {  
  4.  fis = new FileInputStream("D:\\test.txt");  
  5.  fos = new FileOutputStream("D:\\test2.txt");  
  6.  byte[] bytes = new byte[1024];  
  7.  int size = -1;  
  8.  while ((size = fis.read(bytes)) != -1) {  
  9.   fos.write(bytes, 0, size);  
  10.  }  
  11. catch (FileNotFoundException | IOException e) {  
  12.  e.printStackTrace();  
  13. finally {  
  14.  if (fos != null) {  
  15.   try {  
  16.    fos.close();  
  17.   } catch (IOException ex) {  
  18.   }  
  19.  }  
  20.  if (fis != null) {  
  21.   try {  
  22.    fis.close();  
  23.   } catch (IOException ex) {  
  24.   }  
  25.  }  
  26. }  
これで、例外処理時に同じコードを書かずに済みます。
また、「|」で複数の例外を指定した場合は、左の例外から評価されるようです。
これまでの構文のcatchで上に書いていた例外は、その順で左から書けばよいでしょう。つまり、Throwableを書く場合は、一番右です。
AutoCloseableインターフェース
JavaSE7から、AutoCloseableというインターフェースが追加されます。
このインターフェースには、closeというメソッドがあります(ご存知のとおり、JavaSE6以前にもcloseメソッドはありますが、それをオーバーライドしている感じです)。このメソッドが「try-finallyの処理で勝手に呼ばれる」ようです。 AutoCloseableを利用するためのコードは下記の通りです。
  1. try (FileInputStream fis = new FileInputStream("D:\\test.txt"); FileOutputStream fos = new FileOutputStream("D:\\test2.txt")) {  
  2.  byte[] bytes = new byte[1024];  
  3.  int size = -1;  
  4.  while ((size = fis.read(bytes)) != -1) {  
  5.   fos.write(bytes, 0, size);  
  6.  }  
  7. catch (FileNotFoundException | IOException e) {  
  8.  e.printStackTrace();  
  9. }  
tryの()の中ではセミコロンで区切ると、複数のAutoCloseableを並べて記述できるようです。
Stream関係のインターフェースはAutoCloseableのサブインターフェースになっているようです。
インターフェースを実装する際に、AutoCloseableを意識する必要はあまりないと思われます。ъ(゚Д゚)グッジョブ!!(JavaSE6以前のソースを書き換える場合は別)
※public void close() throws IOException => public void close() throws Exception というふうに変わります。

なにはともあれ、コード量がかなり減りました。ワーイヽ(゚∀゚)メ(゚∀゚)メ(゚∀゚)ノワーイ
AutoCloseableを検証
実際に自分の眼で見て確認しました。
closeの順番と、実際に例外が起こった場合について。
  1. public class Test {  
  2.  public static void main(String[] args) {  
  3.   new Test().start();  
  4.  }  
  5.   
  6.  void start() {  
  7.   try (AutoCloseableImpl1 ac1 = new AutoCloseableImpl1(); AutoCloseableImpl2 ac2 = new AutoCloseableImpl2()) {  
  8.      
  9.   }  
  10.  }  
  11. }  
  12.   
  13. class AutoCloseableImpl1 implements AutoCloseable {  
  14.   
  15.  AutoCloseableImpl1() {  
  16.   System.out.println(getClass().getSimpleName());  
  17.  }  
  18.   
  19.  @Override  
  20.  public void close() {  
  21.   System.out.println(getClass().getSimpleName() + "#close()");  
  22.  }  
  23. }  
  24.   
  25. class AutoCloseableImpl2 implements AutoCloseable {  
  26.   
  27.  AutoCloseableImpl2() {  
  28.   System.out.println(getClass().getSimpleName());  
  29.  }  
  30.   
  31.  @Override  
  32.  public void close() {  
  33.   System.out.println(getClass().getSimpleName() + "#close()");  
  34.  }  
  35. }  
上記のプログラムを実行します。
実行結果は

AutoCloseableImpl1
AutoCloseableImpl2
AutoCloseableImpl2#close()
AutoCloseableImpl1#close()

closeが呼ばれており、closeする順番もオッケー。
ちなみに、AutoCloseableImpl2()コンストラクタで例外を投げた場合は、以下

AutoCloseableImpl1
AutoCloseableImpl1#close()
Exception in thread "main" java.lang.NullPointerException
・・・

期待通りです。

余談ですが、catchもfinallyもないtry構文が書けるようになりました。(内部的にはfinallyが存在しますが)

実は、JavaSE7にはjava.nio.file.Filesというクラスが追加されており、ファイルをコピーするだけなら、このクラスを使ったほうが良いです。
まぁ、今回はあくまで勉強のためですから・・・

今回の検証でもちいたIDEはNetBeansです。
Eclipse3.7でも検証は可能でしたが、不都合な点がありましたので、NetBeansを使用しました。
現在のEclipse3.7とJDK1.7の詳細については こちら

2011年7月17日日曜日

Eclipse3.7 で JavaSE7

Eclipse3.7 で JavaSE7

JavaSE7の正式リリースは2011年7月28日で、NetBeansは既にJavaSE7に対応しています。
EclipseはまだJavaSE7に対応していないのですが、pluginをインストールすれば使えるようになります。

各ツールのサイト
Java™ Platform, Standard Edition 7 Developer Preview Release
Eclipse Classic 3.7
JDT/Eclipse Java 7 Support(BETA)
JDK準備
JDK7 Preview Releaseをダウンロードして、インストールします。
開発ツールだけインストールすればOKです。
Eclipse準備
Eclipseは3.7の「Classic」を使います。
「IDE for Java Developers」や、「IDE for Java EE Developers」では、pluginのインストールができませんでした。
Install New SoftWare -> Work with:
「http://build.eclipse.org/eclipse/java7patch/」を入力して、pluginをインストールします。
Eclipse設定
Preferences -> Java -> Installed JREs
インストールしたJDK1.7を追加し、使用するようにします。
あとは、Java Compilerの設定で、Compiler compliance level を 1.7 にすれば準備完了です。

実は、上記で設定した開発環境には問題があり、メソッドや変数の候補を出せなくなります。
普段から、何も考えずにとりあえず Ctrl + Space 押して幸せになっている私にとっては致命的です。
Eclipseで正式にサポートされるまではJavaSE7は我慢するか、NetBeansを使いましょう。

2011年5月31日火曜日

アルゴリズムを考える

アルゴリズムというものについて考えてみる

Twitterでこんなツイートを見つけました。

自然数nに対して操作F「nが偶数ならばnを2で割る。nが奇数ならばnを3倍して1を加える。」を10回行う。10回目で初めて1となる自然数を全て求めよ。(07京都高校生数学コンテスト)

プログラマとしては、さっとプログラムを書いて結果を出してみたくなるのではないでしょうか。

この解を出すアルゴリズムは大きく分けて2通りあります。
その1.1から1024(2^10)までの全ての数についてチェックして探す
その2.結果の「1」から逆に遡って探す

上記その1のアルゴリズムは2重ループによる処理で、その2は再帰による処理になります。
おそらく、この問題で求められている解き方は2の方法でしょう。

書いてみた
  1. import java.util.ArrayList;  
  2. import java.util.Collection;  
  3.   
  4.   
  5. public class Main {  
  6.   
  7.  /* 
  8.   * こいつを呼ぶ 
  9.   */  
  10.  public static void main(String[] x_args) {  
  11.   new Main().start();  
  12.  }  
  13.   
  14.  public void start() {  
  15.   exec(new F10To1ImplVer1());  
  16.   System.out.println();  
  17.   exec(new F10To1ImplVer2());  
  18.  }  
  19.   
  20.  void exec(F10To1 x_f) {  
  21.   {  
  22.    System.out.println(x_f.getClass().getSimpleName() + ":start.");  
  23.    long time_s = System.currentTimeMillis();  
  24.    int count = 10;  
  25.    int end = (int) Math.pow(2, count);  
  26.    Collection<Integer> values = x_f.getF10To1(end, count);  
  27.    for (Integer value : values) {  
  28.     System.out.print(value + "\t");  
  29.    }  
  30.    long time_e = System.currentTimeMillis();  
  31.    System.out.println();  
  32.    System.out.println(":end.");  
  33.    System.out.println("time => " + (time_e - time_s) + "ms");  
  34.   }  
  35.  }  
  36.    
  37. }  
  38.   
  39. interface F10To1 {  
  40.   
  41.  /* 
  42.   * 戻り値Collection内の要素の順番は保証しない 
  43.   */  
  44.  Collection<Integer> getF10To1(int x_end, int x_count);  
  45. }  
  46.   
  47. /** 
  48.  * 全ての自然数についてチェックするパターン 
  49.  */  
  50. class F10To1ImplVer1 implements F10To1 {  
  51.   
  52.  public Collection<Integer> getF10To1(int x_end, int x_count) {  
  53.   Collection<Integer> result = new ArrayList<Integer>();  
  54.   // 1...最後までのループ  
  55.   for (int i = 1 ; i <= x_end ; i++) {  
  56.    if (isF10To1(i, x_count)) {  
  57.     result.add(Integer.valueOf(i));  
  58.    }  
  59.   }  
  60.   return result;  
  61.  }  
  62.   
  63.  private boolean isF10To1(int x_value, int x_count) {  
  64.   int tmp = x_value;  
  65.   // 9回処理する  
  66.   for (int i = 0 ; i < x_count - 1 ; i++) {  
  67.    tmp = f(tmp);  
  68.    if (tmp == 1) {  
  69.     // 1になったら対象外  
  70.     return false;  
  71.    }  
  72.   }  
  73.   // 10回目  
  74.   tmp = f(tmp);  
  75.   return tmp == 1;  
  76.  }  
  77.   
  78.  /* 
  79.   * 1回分の処理 
  80.   */  
  81.  private int f(int x_value) {  
  82.   if (x_value % 2 == 0) {  
  83.    return x_value / 2;  
  84.   }  
  85.   return x_value * 3 + 1;  
  86.  }  
  87. }  
  88.   
  89. /** 
  90.  * 逆から辿るパターン 
  91.  */  
  92. class F10To1ImplVer2 implements F10To1 {  
  93.   
  94.  public Collection<Integer> getF10To1(int x_end, int x_count) {  
  95.   int tmp = 1;  
  96.   // 再帰処理でチェックします  
  97.   Collection<Integer> result = new ArrayList<Integer>();  
  98.   revF(result, tmp, x_count);  
  99.   return result;  
  100.  }  
  101.   
  102.  private void revF(Collection<Integer> x_values, int x_value, int count) {  
  103.   boolean can3 = canRevF_3(x_value);  
  104.   int v2 = revF_2(x_value);  
  105.   int v3 = revF_3(x_value);  
  106.   count--;  
  107.   if (count < 1) {  
  108.    if (can3) {  
  109.     x_values.add(Integer.valueOf(v3));  
  110.    }  
  111.    x_values.add(Integer.valueOf(v2));  
  112.    return;  
  113.   }  
  114.   if (can3 && (1 < v3)) {  
  115.    revF(x_values, v3, count);  
  116.   }  
  117.   if (v2 != 1) {  
  118.    revF(x_values, v2, count);  
  119.   }  
  120.  }  
  121.   
  122.  /* 
  123.   * 引数が2で割った結果だとした場合の元の数を返す 
  124.   */  
  125.  private int revF_2(int x_value) {  
  126.   return x_value * 2;  
  127.  }  
  128.   
  129.  /* 
  130.   * 引数が3倍して1を足した結果だとした場合に元の数が存在するか判断する 
  131.   */  
  132.  private boolean canRevF_3(int x_value) {  
  133.   if (x_value < 4) {  
  134.    return false;  
  135.   }  
  136.   if ((x_value - 1) % 3 != 0) {  
  137.    return false;  
  138.   }  
  139.   return (x_value - 1) / 3 % 2 == 1;  
  140.  }  
  141.   
  142.  /* 
  143.   * 引数が3倍して1を足した結果だとした場合の元の数を返す 
  144.   */  
  145.  private int revF_3(int x_value) {  
  146.   return (x_value - 1) / 3;  
  147.  }  
  148. }  
もしかしたら上記の実装に間違いがあるかもしれません。その場合は、ご指摘いただけるとありがたいです。
F10To1ImplVer1が総当り(その1)で、F10To1ImplVer2が再帰処理を行う(その2)クラスです。
10回程度のループではそれほど差が出ません(マシンにもよります)が、20回程に回数を増やすと極端に処理時間に差が出ます。

上記の2通りのアルゴリズムで、私が先に書いたのは総当り(その1)の方です。
その後、少し考えてその2の方法を書きました。

普段から簡単にプログラムを書こうとしているせいか、「本来はどういうアルゴリズムであるべきか」をあまり考えなくなってしまったようです。

私は細かい実装はあまり気にしないのですが、気にせずともより良いアルゴリズムで書けるのが理想的ですね。