12.6. JPA

Spring Framework

12.6. JPA

位于 org.springframework.orm.jpa 包下的Spring JPA相关类提供了与Hibernate和JDO类似的针对 Java Persistence API 的简单支持。 Spring提供了一些实现类来支持额外的特性。

12.6.1. 在Spring环境中建立JPA

Spring JPA 提供了两种方法创建JPA EntityManagerFactory

12.6.1.1.  LocalEntityManagerFactoryBean

LocalEntityManagerFactoryBean 负责创建一个适合于当前环境的 EntityManager 来使用JPA进行数据访问。 factory bean将使用JPA PersistenceProvider 类的自动检测机制,而在绝大多数情况下,仅仅需要一个persistence unit名称配置:

<beans>
...
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
 <property name="persistenceUnitName" value="myPersistenceUnit"/>
</bean>
...
</beans>

切换到一个JNDI的 EntityManagerFactory (举例来说,在JTA环境中),只需要简单更改XML配置:

<beans>
...
<jndi:lookup id="entityManagerFactory" jndi-name="jpa/myPersistenceUnit"/>
...
</beans>

12.6.1.2.  LocalContainerEntityManagerFactoryBean

LocalContainerEntityManagerFactoryBean 提供了对JPA EntityManagerFactory 的完整控制,并且非常适合那种有简单用户定制需要的环境。 LocalContainerEntityManagerFactoryBean 将基于 'persistence.xml' 文件、提供的 dataSourceLookup 策略和 loadTimeWeaver 创建一个 PersistenceUnitInfo 类。 这就是为何它能够在JNDI之外的用户定义的数据源之上工作,并且能够控制织入流程。

<beans>
 ...
 <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
  <property name="dataSource" ref="someDataSource"/>
  <property name="loadTimeWeaver">
    <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
  </property>
 </bean>
</beans>

LoadTimeWeaver 接口是由Spring提供的允许JPA ClassTransformer 实例能够插入到特定的应用环境中的支持类(web容器/应用服务器)。 通过一个JDK 5.0的 代理 挂钩,在典型情况下不是非常有效率。 代理通常在 整个虚拟机 环境下工作,并且监控 每一个 被加载的类 - 有时这在生产环境下是不提倡的。

Spring提供了许多在不同环境中的 LoadTimeWeaver 实现类,允许 ClassTransformer 实例能够仅仅在 每个classloader 而不是每个虚拟机上被应用。

12.6.1.2.1. 在Tomcat上创建

Jakarta Tomcat 默认的类装载器并不支持类的切换,但是它允许使用用户自定义的类装载器。 Spring提供了 TomcatInstrumentableClassLoader 类(位于 org.springframework.instrument.classloading.tomcat 包中), 这个类继承自Tomcat的类装载器(WebappClassLoader)并且允许JPA ClassTransformer 的实例来“增强”所有由它加载的类。 简单来说,JPA转化器(JPA transformer)仅仅在特定的web应用中才能被使用。(使用 TomcatInstrumentableClassLoader 的那些应用)。

为了使用用户自定义的类装载器:

  1. spring-tomcat-weaver.jar 复制到 $CATALINA_HOME/server/lib 下(其中$CATALINA_HOME 表示Tomcat的安装路径)。

  2. 通过修改web application context使Tomcat使用用户自定义的类装载器(而不是默认的类装载器):

    <Context path="/myWebApp" docBase="/my/webApp/location" ...>
       <Loader loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader"/>
    ...
    </Context>

    Tomcat 5.0.x 和 5.5.x 系列支持多个上下文路径(context locations): 服务器配置文件($CATALINA_HOME/conf/server.xml), 默认的上下文配置($CATALINA_HOME/conf/context.xml)会影响所有被部署的web应用、 每个单独部署在Server上的web应用的配置($CATALINA_HOME/conf/[enginename]/[hostname]/my-webapp-context.xml) 或者跟随着web应用的配置(your-webapp.war/META-INF/context.xml)。从效率的角度说, 我们推荐在web-app的内部配置的方式,因为仅仅使用JPA的应用会使用用户自定义的类装载器。 更多具体有关可用的上下文路径的内容请参见Tomcat 5.x的 文档

    注意,目前(Tomcat 5.5.17)有一个XML配置解析的bug造成 server.xml 中无法使用Loader标签,无论是否指定了classloader,也不管这个classloader是官方的还是自定义的。

    在Tomcat 4.x中,你可以使用相同的context.xml并将它放到 $CATALINA_HOME/webapps 下,或者修改 $CATALINA_HOME/conf/server.xml 来使用用户自定义的类装载器。 更多信息请参看Tomcat 4.x的文档

  3. 在配置 LocalContainerEntityManagerFactoryBean 时,使用合适的LoadTimeWeaver

    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
      ...
      <property name="loadTimeWeaver">
        <bean class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
      </property>
      ...
    </bean>

通过使用这种技术,JPA应用依赖于特定的“仪器”,从而无需任何代理可以在Tomcat中运行。 当主应用程序依赖于不同JPA实现时这点显得尤为重要,因为JPA转换器只在classloader级别运行并各自独立

12.6.1.2.2. OC4J setup (10.1.3.1+)

因为Oracle的 OC4J classloader支持本地字节码转换,从JDK代理切换到LoadTimeWeaver 可以通过应用程序的Spring配置实现:

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
  ...
  <property name="loadTimeWeaver">
    <bean class="org.springframework.instrument.classloading.oc4j.OC4JLoadTimeWeaver"/>
  </property>
  ...
</bean>
12.6.1.2.3. 通用LoadTimeWeaver

在那些需要类探测器但现有LoadTimeWeaver又不支持它的时候,JDK代理是唯一可行的方法。 在这种情况下,Spring提供了InstrumentationLoadTimeWeaver, 它需要一个Spring特有的(但是很通用的)VM代理(spring-agent.jar):

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
  <property name="loadTimeWeaver">
    <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
  </property>
</bean>

请注意在启动虚拟机时要启动Spring代理,方法是提供JVM选项:

-javaagent:/path/to/spring-agent.jar

12.6.1.3. 处理多个持久化单元

对于那些依靠多个持久化单元位置(例如存放在classpath中的多个jar中), Spring提供了作为中央仓库的PersistenceUnitManager, 避免了持久化单元查找过程(的潜在开销)。默认实现允许指定多个位置 (默认情况下classpath会搜索META-INF/persistence.xml文件), 它们会被解析然后通过持久化单元名称调用:

<bean id="persistenceUnitManager" class="org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager">
  <property name="persistenceXmlLocation">
    <list>
     <value>org/springframework/orm/jpa/domain/persistence-multi.xml</value>
     <value>classpath:/my/package/**/custom-persistence.xml</value>
     <value>classpath*:META-INF/persistence.xml</value>
    </list>
  </property>
  <property>
   <map>
    <entry key="localDataSource" value-ref="local-db"/>
    <entry key="remoteDataSource" value-ref="remote-db"/>
   </map>
  </property>
  <!-- if no datasource is specified, use this one -->
  <property name="defaultDataSource" ref="remoteDataSource"/>
  
</bean>

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
 <property name="persistenceUnitManager" ref="persistenceUnitManager"/>
 ...
</bean>

要注意的是默认实现允许在将持久化单元信息传入JPA provider前用 PersistenceUnitPostProcessor(它允许选择持久化单元)修改它们, 传入的过程可以是通过属性声明式地传入(影响其中所有的单元)或编程式地传入。 要是没有指定persistenceUnitManager,LocalContainerEntityManagerFactoryBean 会创建一个并在内部使用它。

12.6.2.  JpaTemplateJpaDaoSupport

每个基于JPA的DAO将通过IoC接收一个 EntityManagerFactory 实例。 这样的DAO可以通过 EntityManagerFactory 来操作原生JPA的API进行数据访问,也可以直接使用Spring的 JpaTemplate

<beans>
...
  <bean id="myProductDao" class="product.ProductDaoImpl">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
  </bean>
...
</beans>
public class JpaProductDao implements ProductDao {

    private EntityManagerFactory entityManagerFactory;

    public void setEntityManagerFactory(EntityManagerFactory emf) {
        this.entityManagerFactoryeManagerFactory = emf;
    }

    public Collection loadProductsByCategory(final String category) throws DataAccessException {
        JpaTemplate jpaTemplate = new JpaTemplate(this.entityManagerFactory);

        return (Collection) jpaTemplate.execute(new JpaCallback() {

            public Object doInJpa(EntityManager em) throws PersistenceException {

                Query query = em.createQuery("from Product as p where p.category = :category");
                query.setParameter("category", category);
                List result = query.getResultList(); 
                // do some further processing with the result list
                return result;
            }
        });
    }
}

JpaCallback 实现允许所有类型的JPA数据访问。 JpaTemplate 将确保 EntityManager 正确的打开和关闭,并且能够自动地参与到事务中去。 除此之外,JpaTemplate 能够恰当地处理异常,确保资源的及时清理以及必要时的事务回滚。 Template实例不仅是线程安全的,而且它是可重用的,因而它能够作为实例变量被一个类持有。 注意 JpaTemplate 提供了简单的诸如find、load、merge等操作的快捷函数来替代默认的回调实现。

不仅如此,Spring还提供了一个方便的 JpaDaoSupport 基类,提供了 get/setEntityManagerFactory 方法以及 getJpaTemplate() 方法供子类调用:

public class ProductDaoImpl extends JpaDaoSupport implements ProductDao {

    public Collection loadProductsByCategory(String category) throws DataAccessException {
        Map<String, String> params = new HashMap<String, String>();
        params.put("category", category);
        return getJpaTemplate().findByNamedParams("from Product as p where p.category = :category", params);
    }
}

除了直接使用Spring的 JpaTemplate,你也可以使用原生JPA的API来实现基于Spring的DAO,此时你需要自行明确地处理EntityManager。 正如在相应的Hibernate章节描述的一样,这种做法的主要优点在于你的数据访问代码可以在整个过程中抛出checked exceptions。JpaDaoSupport 为这种情况提供了多种函数支持,包括获取和释放一个具备事务管理功能的 EntityManager 和相关的异常转化。

12.6.3. 基于原生的JPA实现DAO

注意

EntityManagerFactory实例是线程安全的, 但EntityManager实例不是。被注入的JPA EntityManager的行为和从应用服务器JNDI环境中获取的没有区别, 和JPA规范定义的一样。如果存在一个被注入的JPA EntityManager, 它会代理对当前事务EntityManager的所有调用; 否则每个操作都会创建一个EntityManager,确保线程安全。

你完全可以使用原生的JPA的API进行编程而无需对Spring产生任何依赖,这可以通过一个被注入的 EntityManagerFactoryEntityManager 来完成。 注意Spring能够识别字段或者方法级别的 @PersistenceUnit@PersistenceContext 标注,如果 PersistenceAnnotationBeanPostProcessor 功能被激活的话。 一个相应的DAO实现类看上去就像这样:

public class ProductDaoImpl implements ProductDao {

    @PersistenceUnit
    private EntityManagerFactory entityManagerFactory;

    public Collection loadProductsByCategory(String category) {
        EntityManager em = this.entityManagerFactory.getEntityManager();
        try {
             Query query = em.createQuery("from Product as p where p.category = ?1");
             query.setParameter(1, category);
             return query.getResultList();

        }
        finally {
            if (em != null) {
                em.close();
            }
        }
    }
}

上述的DAO不对Spring产生任何依赖,而它就如同使用Spring的 JpaTemplate 那样,非常适合在Spring的application context中进行配置。 此外,这样的DAO可以利用标注来要求 EntityManagerFactory 的注入:

<beans>
  ...
  <!-- JPA annotations bean post processor -->
  <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>

  <bean id="myProductDao" class="product.ProductDaoImpl"/>

</beans>

这类DAO的主要问题在于每次总是从工厂中获取一个新的 EntityManager 实例。 这一点可以通过对 EntityManager 而不是factory进行注入来解决:

public class ProductDaoImpl implements ProductDao {

    private EntityManager em;

    @PersistenceContext
    public void setEntityManager(EntityManager em) {
        this.em = em;
    }

    public Collection loadProductsByCategory(String category) {
       Query query = em.createQuery("from Product as p where p.category = :category");
       query.setParameter("category", category);
       return query.getResultList(); 
    }
}

被注入的 EntityManager 是受到Spring管理的(能够感知当前的事务)。 非常值得注意的是,即使这种新的实现更倾向于方法级别的注入(使用 EntityManager 而不是 EntityManagerFactory), 对于注解的使用,application context的XML配置无需任何改变。

这种DAO风格的主要优点在于它仅仅依赖JPA,而无需依赖任何的Spring的类。除此之外,JPA的标注是被识别的,注入能够被Spring容器自动应用。 从无入侵性的角度来说,这一点非常有吸引力,它对于JPA开发者来说也更自然。

12.6.4. 异常转化

DAO不仅会抛出普通的 PersistenceException 异常,(这是一个无需声明和捕获的unchecked exception),还会抛出诸如 IllegalArgumentExceptionIllegalStateException 之类的异常。 这意味着,DAO的调用者只能以普通的错误来处理这些异常,除非完全依赖JPA自身的异常体系。因而,除非你将DAO的调用者绑定到具体的实现策略上去,否则你将无法捕获特定的异常原因(诸如乐观锁异常)。 这种折中平衡或许可以被接受,如果你的应用完全基于JPA或者无需进行特殊的异常处理。不过,Spring提供了一个允许你进行透明的异常转化的解决方案:通过使用 @Repository 注解:

@Repository
public class ProductDaoImpl implements ProductDao {
 ...
}
<beans>
  ...
  <!-- Exception translation bean post processor -->
  <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>

  <bean id="myProductDao" class="product.ProductDaoImpl"/>

</beans>

后置处理器将自动的寻找所有的异常转化器(PersistenceExceptionTranslator 这个接口的实现类)并通知所有打上 @Repository 注解的bean,从而能够使得被找到的异常转化器能够在抛出异常时做相应的异常转化工作。

总结来说:DAO能够基于普通的Java持久层API和注解来实现,但同样也能享受到由Spring管理事务、IoC和透明的异常转化(转化成为Spring的异常体系)等好处。

12.6.5. 事务管理

将事务管理纳入到Service操作的执行中,你可以使用Spring通用的声明式的事务管理功能,参加下面的例子:

<?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"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
  ...

  <bean id="myTxManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="myEmf"/>
  </bean>

  <bean id="myProductService" class="product.ProductServiceImpl">
    <property name="productDao" ref="myProductDao"/>
  </bean>

  <aop:config>
    <aop:pointcut id="productServiceMethods" expression="execution(* product.ProductService.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="productServiceMethods"/>
  </aop:config>

  <tx:advice id="txAdvice" transaction-manager="myTxManager">
    <tx:attributes>
      <tx:method name="increasePrice*" propagation="REQUIRED"/>
      <tx:method name="someOtherBusinessMethod" propagation="REQUIRES_NEW"/>
      <tx:method name="*" propagation="SUPPORTS" read-only="true"/>
    </tx:attributes>
  </tx:advice>

</beans>

Spring JPA允许将一个JPA事务暴露给访问相同的JDBC DataSource 的JDBC访问代码,前提条件是,被注册的 JpaDialect 能够支持获取底层的JDBC Connection。 Spring提供了针对Toplink和Hibernate的JPA实现的Dialect。具体参见下一节有关 JpaDialect 的机制。

12.6.6. JpaDialect

作为一个高级特性,JpaTemplateJpaTransactionManagerAbstractEntityManagerFactoryBean 的子类支持用户自定义的 JpaDialect 作为"jpaDialect"的bean属性进行注入。 在这种情况下,DAO将不再接收 EntityManagerFactory 的引用作为参数,而是接收一个完整的 JpaTemplate(也就是将它注入到 JpaDaoSupport 的"jpaTemplate"属性中去 一个 JpaDialect 实现能够激活一些由Spring支持的高级特性,这通常由特定的实现供应商指定: 一个 JpaDialect 实现能够激活一些由Spring支持的高级特性,这通常由特定的实现供应商指定:

  • 使用特定的事务语义(例如用户自定义的事务隔离级别和事务超时)

  • 获取具备事务功能的Connection对象(暴露给基于JDBC的DAO)

  • PersistenceExceptions 到Spring的 DataAccessExceptions 高级转化

这对于特殊的事务语义和异常的高级转化这样的高级特性比较有价值。注意默认的实现(使用DefaultJpaDialect)并不提供任何特殊的功能。如果你需要上述的特殊功能,你必须指定合适的Dialect。

更多有关它的操作以及它如何在Spring的JPA支持中使用的详细信息请参看 JpaDialect 的Javadoc。