Spring+HibernateEntityManager(コンポーネント自動検出 without アノテーション編)

前回のおさらい

前回は、@Component アノテーション@Scope アノテーション、context:component-scan 要素を用いて、Entity のパッケージ以下の各クラスを bean として自動登録しました。その実際の記述は以下のようになります。

  • @Component("user")
  • @Scope("prototype")

不満点

@Component に value を渡すことについて


やっぱイヤだなぁ。


@Component に名前を渡すということは、コンポーネント名をドメインモデルの実装内で指定することになります。


しかし、ドメインモデル内部にあるエンティティという視点からは、同一アプリケーション内で共存するコンポーネント名を知る術はありません。


なぜなら、ドメインモデルをここねこねしてアプリケーションを構築するのは、アプリケーション層の責務だからです。


そして、アプリケーション層がドメインモデルをこねこねした際に選ばれた複数のコンポーネントが、同じコンポーネント名を主張することは十分に有り得ます。


コンポーネント名が衝突しないことをドメインモデルの視点のみで事前に保証することは原理的に不可能なのです。


まぁ、現実的には、FQCNコンポーネント名にすれば何とかなると思うけど、そんなことしたら負けだと思っているw(実装の都合で仕様や設計を捻じ曲げ、後にカオスが訪れる。そんな例をイヤってほど見てきてるからなぁ o...rz)

@Component アノテーションそのものも冗長かも。


おいら的なアプリケーション設計上、エンティティはすべてコンポーネントとして検出してほしいので、エンティティ実装側にいちいち @Component アノテーションを書かなくても、自動検出してほしいかも。

@Scope アノテーションつけなくても、慮ってスコープを prototype にしてほしいなぁ。


おいらのアプリケーション設計上、エンティティのスコープが prototype 以外になることはありえないので、いちいち指定するのは面倒です。


※関係ないですが、おいら的には「慮(おもんぱか)る」って言葉好きです。alcで調べると、「take thought for 〜」とかなるっぽい。日本語の「慮る」ほうが意味深長っぽい。日本万歳。


※※おいらは右寄りの人ではありません。外資系で正社員もしてました。一応念のため (^^;

課題

ということで、今回の課題。

@Component アノテーションの排除

現在、@Component アノテーションは、エンティティが bean として検出・登録されるためのマーカーとしての役割をしています。ということで、beans.xml 側の設定で @Entity アノテーションをそのマーカーとして扱うようにします。

変更前
<context:component-scan base-package="domain.entity" />
変更後
<context:component-scan base-package="domain.entity">
  <context:include-filter type="annotation" expression="javax.persistence.Entity"/>
</context:component-scan>


これで第一関門クリアです。


では、@Component アノテーションを削除してしまいましょう


...


と思いきや、このまま @Component アノテーションを削除してしまうと、domain.entity.UserImpl の bean 名は userImpl になってしまいます。


どうしたものか、、、。

自動検出・登録されるエンティティの bean 名を命名規則により決定する。


おいら的な設計では今のところ、エンティティの bean 名は、エンティティの実装クラスの NQCN(Non-Qualified Class Name) から Impl を抜いた文字列としています。ということで、アプリケーション層側の責任において、この命名規則を適用してしまおうと思います。


ここで重要なポイントは、@Component アノテーションで名前を定義する場合は上記の命名規則が変わるとドメイン層の実装にも影響してしまいますが、アプリケーション層側で bean 名を指定する場合にはアプリケーション層の実装に影響するだけです。アプリケーション層側の設計を変更するに当たり、アプリケーション層の実装を変更することは、理にかなっています。


さて、ここで問題となるのは、どうやってアプリケーション層でエンティティ名を設定するかですが、そのために context:component-scan 要素には name-generator 属性が用意されています。

name-generator 属性と BeanNameGenerator インタフェース


context:component-scan 要素の name-generator 属性には、bean 名を作成するためのクラスを指定することができます。ここで指定されるクラスは、BeanNameGenerator インタフェースを実装している必要があります。

EntityBeanNameGenerator クラスを作成する。


ということで、BeanNameGenerator インタフェースを実装する EntityBeanNameGenerator クラスを作成してみようと思います。


仕様は、エンティティクラスの NQDN(Non-Qualified Domain Name) に JavaBeans のプロパティのようなルールを適用し、最後の "Impl" という文字列を削除したものを bean 名として返すということにします。NQDN が "Impl" 以外で終了した場合は、IllegalStateException を throw するようにします。


EntityBeanNameGenerator.java

package infrastracture.org.springframework.beans.factory.support;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.util.ClassUtils;

/**
 * BeanNameGenerator only for Entity class.
 * <p>
 * The name of entity class must be followed by "Impl" for using this class.
 * </p>
 * 
 * @author beyondseeker@users.sourceforge.jp
 * @version $Id$
 */
public class EntityBeanNameGenerator implements BeanNameGenerator {
    
    /**
     * Suffix of entity class name.
     */
    private static final String IMPL_SUFFIX = "Impl";
    
    /**
     * Generate bean name for entity.
     * 
     * @throws IllegalStateException
     *             when class name retrieved from BeanDefinition is not followed
     *             by "Impl"
     * @see BeanNameGenerator#generateBeanName(BeanDefinition,
     *      BeanDefinitionRegistry)
     */
    @Override
    public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
        try {
            String propertyName = ClassUtils.getShortNameAsProperty(Class.forName(definition.getBeanClassName()));
            int pos = propertyName.indexOf(IMPL_SUFFIX);
            if (pos == -1) throw new IllegalStateException("Entity class name must be followed by 'Impl'");
            return propertyName.substring(0, pos);
            
        } catch (ClassNotFoundException e) {
            // No chance to reach here.
            throw new IllegalStateException("Class " + definition.getBeanClassName() + " not found.");
        }
    }
    
}
beans.xml の変更点


変更前

<context:component-scan base-package="domain.entity" />


変更後

<context:component-scan
    base-package="domain.entity"
    name-generator="infrastracture.org.springframework.beans.factory.support.EntityBeanNameGenerator"
>
  <context:include-filter type="annotation" expression="javax.persistence.Entity"/>
</context:component-scan>
UserImpl.java の変更

以下を削除

@Component("user")


これで、第二関門突破です。 ヽ( ・∀・)ノ ワーイ

@Scope アノテーションの排除


さて、最後の関門です。

  • エンティティのスコープを prototype に保ったまま、@Scope アノテーションを排除する。
scope-resolver 属性と ScopeMetadataResolver インタフェース


context:component-scan 要素の scope-resolver 属性には、bean のスコープを指定するためのクラスを指定することができます。ここで指定されるクラスは、ScopeMetadataResolver インタフェースを実装している必要があります。

PrototypeScopeMetadataResolver クラスを作成する。


ということで、ScopeMetadataResolver インタフェースを実装する PrototypeScopeMetadataResolver クラスを作成してみようと思います。


仕様は、「常に "prototype" を返す」ですw


PrototypeScopeMetadataResolver.java

package infrastracture.org.springframework.context.annotation;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ScopeMetadata;
import org.springframework.context.annotation.ScopeMetadataResolver;

/**
 * ScopeMetadataResolver which always returns prototype scope.
 *
 * @author beyondseeker@users.sourceforge.jp
 * @version $Id$
 */
public class PrototypeScopeMetadataResolver implements ScopeMetadataResolver {
    
    /**
     * Always returns prototype scope.
     * @see org.springframework.context.annotation.ScopeMetadataResolver#resolveScopeMetadata(org.springframework.beans.factory.config.BeanDefinition)
     */
    @Override
    public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) {
        ScopeMetadata scopeMetadata = new ScopeMetadata();
        scopeMetadata.setScopeName(BeanDefinition.SCOPE_PROTOTYPE);
        return scopeMetadata;
    }
    
}


ScopeMetadataResolver は設定項目が scopeProxyMode と scopeName しかないので、汎用的な FactoryBean を提供するのが正解かもしれない。(けど、本筋とずれるので今はやらないw)

beans.xml の変更点


変更前

<context:component-scan
    base-package="domain.entity"
    name-generator="infrastracture.org.springframework.beans.factory.support.EntityBeanNameGenerator"
>
  <context:include-filter type="annotation" expression="javax.persistence.Entity"/>
</context:component-scan>


変更後

<context:component-scan
    base-package="domain.entity"
    name-generator="infrastracture.org.springframework.beans.factory.support.EntityBeanNameGenerator"
    scope-resolver="infrastracture.org.springframework.context.annotation.PrototypeScopeMetadataResolver"
>
  <context:include-filter type="annotation" expression="javax.persistence.Entity"/>
</context:component-scan>
UserImpl.java の変更点

以下を削除

@Scope("prototype")

結果

最初の方で上げた課題を一通り解決しましたー ヽ( ・∀・)ノ ワーイ


課題一覧


タイトルが、「Spring+HibernateEntityManager(コンポーネント自動検出 without アノテーション編)」といいつつ、@Entity アノテーションつかってんじゃねーかとお思いのアナタ。


慮ってくださいw


実際は、@Entity アノテーションを使わなくても、assignable や regexp 等の方法でもできましたが、今回は @Entity を利用するのが一番自然だと判断した次第です。


重要なのは、ドメインモデルの実装から SpringFramework に依存したアノテーションが排除されたことにあります。


また、今後エンティティの数がいくら増えても、そのための設定が一切必要なくなるというメリットは非常に大きいと思います。おいら自身、今までの案件で、数十から数百のエンティティ定義を xml ファイルに書いてきましたから、、、。