5 jun 2008

Howto: Setup Spring for Hibernate Annotations

Today, we'll explore how to setup the Spring ApplicationContext to use annotation-driven Hibernate mappings.
Hibernate mappings should be specified in any of the following:

  • Hibernate XML mapping files

  • XDoclet

  • Hibernate Annotations

Hibernate Annotations is my preferred way to map my entity classes, since they don't require any external file (thus keeping mapping info in your Java files), is fully integrated with all Hibernate mapping capabilities and Hibernate documentation encourages us to use this kind of configuration because it's more efficient.
Annotation driven mapping in Hibernate uses the standard JPA API annotations and introduce some specific extensions to deal with some Hibernate features. You can find a full reference in the official documentation.
The Spring (version 2.5) applicationContext.xml file used to configure an annotation-driven SessionFactory should look like this:
...
  <bean id="sessionFactory"
        class="org.springframework.orm.hibernate3.AnnotationSessionFactoryBean">
    <property name="dataSource">
      <ref bean="dataSource" />
    </property>
    <property name="annotatedClasses">
      <util:list>
        <value>foo.bar.model.MyEntity</value>
        <value>foo.bar.model.AnotherEntity</value>
        <value>...</value>
      </util:list>
    </property>
    <property name="hibernateProperties">
      <util:properties location="hibernate.properties Location">
    </property>
  </bean>
...

Unfortunately, Spring's AnnotationSessionFactoryBean doesn't accept wildcards in the annotatedClasses property (since it's implemented as Class[] instead of Resource[]) and the annotatedPackages property is not intended to specify the package containing our annotated entities but to 'add package level annotations at the class leve' (I don't really understand this ;-) ).
But don't worry, we can extend AnnotationSessionFactoryBean to achieve what we want like this:
public class ExtendedAnnotationSessionFactoryBean extends
 AnnotationSessionFactoryBean {

  private String[] basePackages;
  private ClassLoader beanClassLoader;

  public void afterPropertiesSet() throws Exception {
    Collection<Class<?>> entities = new ArrayList<Class<?>>();
    ClassPathScanningCandidateComponentProvider scanner = this.createScanner();
    for (String basePackage : this.basePackages) {
      this.findEntities(scanner, entities, basePackage);
    }
    this.setAnnotatedClasses(entities.toArray(new Class<?>[entities.size()]));
    super.afterPropertiesSet();
  }

  private ClassPathScanningCandidateComponentProvider createScanner() {
    ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
    scanner.addIncludeFilter(new AnnotationTypeFilter(Entity.class));
    return scanner;
  }

  private void findEntities(ClassPathScanningCandidateComponentProvider scanner,
                            Collection<Class<?>> entities, String basePackage) {
    Set<BeanDefinition> annotatedClasses = scanner.findCandidateComponents(basePackage);
    for (BeanDefinition bd : annotatedClasses) {
      String className = bd.getBeanClassName();
      Class<?> type = ClassUtils.resolveClassName(className, this.beanClassLoader);
      entities.add(type);
    }
  }
  
  public void setBasePackage(String basePackage) {
    this.basePackages = new String[] { basePackage };
  }

  public void setBasePackages(String[] basePackages) {
    this.basePackages = basePackages;
  }

  public void setBeanClassLoader(ClassLoader beanClassLoader) {
    this.beanClassLoader = beanClassLoader;
  }

}

We don't need to inject the beanClassLoader property if we don't want to, thus using the default ClassLoader. This happens because we're using ClassUtils.resolveClassName() to get the Class objects, and the Javadoc on this method stands that "the class loader to use may be null, which indicates the default ClassLoader". With that in mind, we can use the following applicationContext setup:
...
  <bean id="sessionFactory"
        class="foo.bar.MyAnnotationSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="basePackages">
      <util:list>
        <value>foo.bar.model</value>
      </util:list>
    </property>
    <property name="hibernateProperties">
      <util:properties location="hibernate.properties Location">
    </property>
  </bean>
...

That's all, with that, Hibernate will inspect and map all the classes in package foo.bar.model having the required annotations (@Entiy or @MappedSuperclass).

1 comentario:

Anónimo dijo...

Nice guide)
One thing to mention is that @MappedSuperclass is not required to be mapped. Thus you may only scan for @Entity.