Spring+HibernateEntityManager(@Required編)

DIし忘れに注意

実行時に、いきなり NullPointerException 喰らって、それが単なる bean への DI し忘れだとヘコみますよね。

そういうときには、@Required アノテーションによって、コンテキスト起動時に DI の抜けを確認することができます。

ぬるぽの例

前回の beans.xml の userService bean の設定から、あえて entityManager への DI を外してプログラムを動作させると、以下のようなエラーが発生します。

Exception in thread "main" java.lang.NullPointerException
	at domain.service.UserServiceImpl.registerUser(UserServiceImpl.java:22)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
	at java.lang.reflect.Method.invoke(Method.java:597)
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:310)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:182)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:149)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:106)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
	at $Proxy12.registerUser(Unknown Source)
	at application.Main.main(Main.java:37)

これは、実際に entityManager を使用したときに発生した例外なので、ApplicationContext を起動しただけでは発生しません

ということで、事前に抜けを検出できるようにすることにします。

bean の定義ファイルに

<context:annotation-config/>

を指定すると、コンテキスト設定に関するいくつかのアノテーションが使用可能になります。

その際、ネームスペース情報のため、beans 要素の属性に設定が必要になります。

※最後に beans.xml 全体のソースを載せておきます。

ネームスペースに関する設定を忘れると、以下のように、XML のパース時に怒られてしまうので、ご注意を、、、。

Exception in thread "main" org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException: Line 14 in XML document from class path resource [beans.xml] is invalid; nested exception is org.xml.sax.SAXParseException: The prefix "context" for element "context:annotation-config" is not bound.
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:404)
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:342)
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:310)
	at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:143)
	at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:178)
	at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:149)
	at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:212)
	at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:113)
	at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:80)
	at org.springframework.context.support.AbstractRefreshableApplicationContext.refreshBeanFactory(AbstractRefreshableApplicationContext.java:123)
	at org.springframework.context.support.AbstractApplicationContext.obtainFreshBeanFactory(AbstractApplicationContext.java:423)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:353)
	at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)
	at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)
	at application.Main.main(Main.java:27)
Caused by: org.xml.sax.SAXParseException: The prefix "context" for element "context:annotation-config" is not bound.
	at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException(ErrorHandlerWrapper.java:195)
	at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.fatalError(ErrorHandlerWrapper.java:174)
	at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:388)
	at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:318)
	at com.sun.org.apache.xerces.internal.impl.XMLNSDocumentScannerImpl.scanStartElement(XMLNSDocumentScannerImpl.java:310)
	at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl$FragmentContentDriver.next(XMLDocumentFragmentScannerImpl.java:2740)
	at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(XMLDocumentScannerImpl.java:647)
	at com.sun.org.apache.xerces.internal.impl.XMLNSDocumentScannerImpl.next(XMLNSDocumentScannerImpl.java:140)
	at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(XMLDocumentFragmentScannerImpl.java:508)
	at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:807)
	at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:737)
	at com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(XMLParser.java:107)
	at com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(DOMParser.java:225)
	at com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:283)
	at org.springframework.beans.factory.xml.DefaultDocumentLoader.loadDocument(DefaultDocumentLoader.java:75)
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:396)
	... 14 more

@Required アノテーション

DI を保証したい setter メソッドに、@Required アノテーションを追加します。

以下例

@Required
public void setEntityManager(EntityManager entityManager) {
    this.entityManager = entityManager;
}

BeanInitializationException の例

@Required アノテーションを指定する前は、DI されずに null となっていた変数にアクセスした結果 NullPointerException が発生していましたが、@Required アノテーションを設定した後は、ApplicationContext 作成時に、BeanInitializationException が発生するようになります。

Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userService' defined in class path resource [beans.xml]: Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanInitializationException: Property 'entityManager' is required for bean 'userService'
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:478)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory$1.run(AbstractAutowireCapableBeanFactory.java:409)
	at java.security.AccessController.doPrivileged(Native Method)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:380)
	at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:264)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:220)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:261)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:185)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:164)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:429)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:729)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:381)
	at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)
	at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)
	at application.Main.main(Main.java:27)
Caused by: org.springframework.beans.factory.BeanInitializationException: Property 'entityManager' is required for bean 'userService'
	at org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor.postProcessPropertyValues(RequiredAnnotationBeanPostProcessor.java:121)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:996)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:470)
	... 14 more

こうすることにより、DI し忘れを早期に発見することができるようになります。

設計的な視点(おいらの意見)

おいら的には、ドメインモデルに含まれるクラスの実装ソースに @Required を指定するという方法は、悪くないアイデアだと思います。

理由は、ドメインモデル中の各モデルは、アプリケーション層により、アプリケーションとして組み立てられることを前提に設計されているからです。そのため、ドメインモデルとしての責務を果たすために必要なリソースを、ドメインモデル側からアプリケーション層に要求するというのは自然なのかなと思います。

変更したソース

application/src/main/resources/beans.xml

contextネームスペース用の設定と、 要素を設定しました。

<?xml version="1.0" encoding="UTF-8"?>
<beans
  xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:aop="http://www.springframework.org/schema/aop"
  xmlns:tx="http://www.springframework.org/schema/tx"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
">

  <!-- for context configuration by annotations -->
  <context:annotation-config/>

  <!-- for @Transactional annotations -->
  <tx:annotation-driven transaction-manager="txManager"/>

  <!-- Actual DataSource -->
  <bean id="rawDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/test" />
    <property name="username" value="root" />
    <property name="password" value="" />
  </bean>

  <!-- LazyConnectionDataSourceProxy -->
  <bean id="dataSource" class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy">
    <property name="targetDataSource" ref="rawDataSource" />
  </bean>

  <!-- Transaction Manager -->
  <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
  </bean>

  <!-- JpaVendorAdapter  -->
  <bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />

  <!-- EntityManagerFactory -->
  <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="persistenceXmlLocation" value="persistence.xml" />
    <property name="jpaVendorAdapter" ref="jpaVendorAdapter" />
    <property name="dataSource" ref="dataSource" />
    <property name="jpaProperties">
      <props>
        <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</prop>
        <prop key="hibernate.hbm2ddl.auto">create-drop</prop>
      </props>
    </property>
  </bean>

  <!-- EntityManager -->
  <bean id="entityManager" class="org.springframework.orm.jpa.support.SharedEntityManagerBean">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
  </bean>

  <!-- Entity -->
  <bean id="user" class="domain.entity.UserImpl" scope="prototype" />

  <!-- Service -->
  <bean id="userService" class="domain.service.UserServiceImpl">
    <lookup-method name="newUser" bean="user"/>
    <property name="entityManager" ref="entityManager"/>
  </bean>

</beans>
infrastracture-impl/src/main/java/infrastracture/ServiceBase.java

@Required アノテーションを追加しました。

package infrastracture;

import javax.persistence.EntityManager;

import org.springframework.beans.factory.annotation.Required;

/**
 * Service のベースクラスです。現段階では、このサンプルは DAO 等の仕組みを持たず、Service 実装内で直接 EntityManager
 * を使用するため、 EntityManager を DI されるようになっています。
 * 
 * @author beyondseeker
 * @version $Id$
 */
public class ServiceBase {
    
    /**
     * EntityManager
     */
    private EntityManager entityManager;
    
    /**
     * EntityManager をセットします。
     * 
     * @param entityManager
     *            EntityManager
     */
    @Required
    public void setEntityManager(EntityManager entityManager) {
        this.entityManager = entityManager;
    }
    
    /**
     * EntityManager を返します。
     * 
     * @return EntityManager
     */
    public EntityManager getEntityManager() {
        return entityManager;
    }
    
}

メモ

メモ