Spring+HibernateEntityManager(コンポーネント自動検出 without アノテーション編)
前回のおさらい
前回は、@Component アノテーション、@Scope アノテーション、context:component-scan 要素を用いて、Entity のパッケージ以下の各クラスを bean として自動登録しました。その実際の記述は以下のようになります。
- @Component("user")
- @Scope("prototype")
不満点
@Component に value を渡すことについて
やっぱイヤだなぁ。
@Component に名前を渡すということは、コンポーネント名をドメインモデルの実装内で指定することになります。
しかし、ドメインモデル内部にあるエンティティという視点からは、同一アプリケーション内で共存するコンポーネント名を知る術はありません。
なぜなら、ドメインモデルをここねこねしてアプリケーションを構築するのは、アプリケーション層の責務だからです。
そして、アプリケーション層がドメインモデルをこねこねした際に選ばれた複数のコンポーネントが、同じコンポーネント名を主張することは十分に有り得ます。
コンポーネント名が衝突しないことをドメインモデルの視点のみで事前に保証することは原理的に不可能なのです。
まぁ、現実的には、FQCN をコンポーネント名にすれば何とかなると思うけど、そんなことしたら負けだと思っているw(実装の都合で仕様や設計を捻じ曲げ、後にカオスが訪れる。そんな例をイヤってほど見てきてるからなぁ o...rz)
課題
ということで、今回の課題。
@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>
@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>
結果
最初の方で上げた課題を一通り解決しましたー ヽ( ・∀・)ノ ワーイ
課題一覧
- @Component アノテーションを排除する。
- コンポーネント名の指定をアプリケーション層に移行する。
- エンティティのスコープを prototype に保ったまま、@Scope アノテーションを排除する。
タイトルが、「Spring+HibernateEntityManager(コンポーネント自動検出 without アノテーション編)」といいつつ、@Entity アノテーションつかってんじゃねーかとお思いのアナタ。
慮ってくださいw
実際は、@Entity アノテーションを使わなくても、assignable や regexp 等の方法でもできましたが、今回は @Entity を利用するのが一番自然だと判断した次第です。
重要なのは、ドメインモデルの実装から SpringFramework に依存したアノテーションが排除されたことにあります。
また、今後エンティティの数がいくら増えても、そのための設定が一切必要なくなるというメリットは非常に大きいと思います。おいら自身、今までの案件で、数十から数百のエンティティ定義を xml ファイルに書いてきましたから、、、。
links
第1回 Spring+HibernateEntityManager(HibernateEntityManager単体編)
第2回 Spring+HibernateEntityManager(とりあえずSpring編)
第3回 Spring+HibernateEntityManager(宣言的トランザクション編)
第4回 Spring+HibernateEntityManager(Spring+DDDっぽく編)
第5回 Spring+HibernateEntityManager(Spring+DDDっぽく編 その2)
第6回 Spring+HibernateEntityManager(Spring+DDDっぽく編 その3)
第7回 Spring+HibernateEntityManager(Spring+DDDっぽく編 その4)
第8回 Spring+HibernateEntityManager(@Transactionalアノテーション編)
第9回 Spring+HibernateEntityManager(@Required編)
第10回 Spring+HibernateEntityManager(XMLからの外部リソース参照編)
第11回 Spring+HibernateEntityManager(AspectJ AOP with Load-Time-Weaver編)
第12回 Spring+HibernateEntityManager(DBCPのvalidationQuery編)
第13回 Spring+HibernateEntityManager(@Resource編)
第14回 Spring+HibernateEntityManager(コンポーネント自動検出 with アノテーション編)
第15回 Spring+HibernateEntityManager(コンポーネント自動検出 without アノテーション編)