对于J2EE 应用程序而言,事务的处理一般有两种模式:
1. 依赖特定事务资源的事务处理
这是应用开发中最常见的模式,即通过特定资源提供的事务机制进行事务管理。
如通过JDBC、JTA 的rollback、commit方法;Hibernate Transaction 的rollback、commit方法等。这种方法大家已经相当熟悉。
2. 依赖容器的参数化事务管理
通过容器提供的集约式参数化事务机制,实现事务的外部管理,如EJB 中的事务管理模式。
如,下面的EJB事务定义中,将SessionBean MySession的doService方
法定义为Required。也就是说,当MySession.doServer 方法被某个线程调用时,容器将此线程纳入事务管理容器,方法调用过程中如果发生异常,当前事务将被容器自动回滚,如果方法正常结束,则容器将自动提交当前事务。
<container-transaction >
<method >
<ejb-name>MySession</ejb-name>
<method-intf>Remote</method-intf>
<method-name>doService</method-name>
<method-params>
<method-param>java.lang.String</method-param>
</method-params>
</method>
<trans-attribute>Required</trans-attribute>
</container-transaction>
容器管理的参数化事务为程序开发提供了相当的灵活性,同时因为将事务委托给容器进行管理,应用逻辑中无需再编写事务代码,大大节省了代码量(特别是针对需要同时操作多个事务资源的应用),从而提高了生产率。然而,使用EJB 事务管理的代价相当高昂,撇开EJB 容器不菲的价格,EJB的学习成本,部署、迁移、维护难度,以及容器本身带来的性能开销(这往往意味着需要更高的硬件配置)都给我们带来了相当的困惑。此时事务管理所带来的优势往往还不能抵消上面这些负面影响。
Spring事务管理能给我们带来什么?
下面这段xml配置片断展示了Spring中的事务设定方式:
<beans>
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>org.gjt.mm.mysql.Driver</value>
</property>
<property name="url">
<value>jdbc:mysql://localhost/sample</value>
</property>
<property name="username">
<value>user</value>
</property>
<property name="password">
<value>mypass</value>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTr
ansactionManager">
<property name="dataSource">
<ref local="dataSource" />
</property>
</bean>
<bean id="userDAO" class="net.xiaxin.dao.UserDAO">
<property name="dataSource">
<ref local="dataSource" />
</property>
</bean>
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
<bean id="userDAOProxy"
class="org.springframework.transaction.interceptor.Tran
sactionProxyFactoryBean">
<property name="transactionManager">
<ref bean="transactionManager" />
</property>
<property name="target">
<ref local="userDAO" />
</property>
<property name="transactionAttributes">
<props>
<prop key="insert*">PROPAGATION_REQUIRED</prop>
<prop key="get*">
PROPAGATION_REQUIRED,readOnly
</prop>
</props>
</property>
</bean>
</beans>
配置中包含了dataSource,transactionManager 等资源定义。这些资源都为一个名为userDAOProxy 的TransactionProxyFactoryBean 服务, 而userDAOProxy 则对包含实际数据逻辑的userDAO进行了事务性封装。
可以看到,在userDAOProxy 的"transactionAttributes"属性中,我们定义了针对userDAO 的事务策略,即将所有名称以insert 开始的方法(如UserDAO.insertUser方法)纳入事务管理范围。如果此方法中抛出异常,则Spring
将当前事务回滚,如果方法正常结束,则提交事务。而对所有名称以get 开始的方法(如UserDAO.getUser 方法)则以只读的事务处理机制进行处理。(设为只读型事务,可以使持久层尝试对数据操作进行优化,如对
于只读事务Hibernate将不执行flush操作,而某些数据库连接池和JDBC 驱动也对只读型操作进行了特别化。)
结合上面这段申明带来的感性认知,看看Spring 的事务管理机制与EJB 中事务管理有何不同,或者有何优势。这里自然有许多方面可以比较,不过,笔者认为其中最为关键的两点是:
1. Spring可以将任意Java Class 纳入事务管理
这里的UserDAO只是我们编写的一个普通Java Class,其中包含了一些基本的数据应用逻辑。通过Spring,我们即可简单的实现事务的可配置化。也就是说,我们可以随意为某个类的某个方法指定事务管理机制。与之对比,如果使用EJB容器提供的事务管理功能,我们不得不按照EJB规范编将UserDAO 进行改造,将其转换为一个标准的EJB。
2. Spring事务管理并不依赖特定的事务资源。
EJB 容器必须依赖于JTA 提供事务支持。而Spring 的事务管理则支持JDBC、JTA 等多种事务资源。这为我们提供了更多的选择,从而也使得我们的系统部署更加灵活。
对Spring事务管理机制进行简单分析之后,我们将结合持久层封装的具体事务应用机制,对Spring中的事务管理进行更具实效的探讨。
持久层封装
JDBC
Spring对JDBC进行了良好的封装,通过提供相应的模板和辅助类,在相当程度上降低了JDBC操作的复杂性。并且得益于Spring良好的隔离设计,JDBC封装类库可以脱离Spring Context独立使用,也就是说,即使系统并没有采用Spring作为结构性框架,我们也可以单独使用Spring的JDBC部分(spring-dao.jar)来改善我们的代码。作为对比,首先让我们来看一段传统的JDBC代码:
Connection conn =null;
Statement stmt = null;
try {
conn = dataSource.getConnection();
stmt = con.createStatement();
stmt.executeUpdate("UPDATE user SET age = 18 WHERE id = 'erica'");
} finally {
if (stmt != null) {
try {
stmt.close();
} catch (SQLException ex) {
logger.warn("Exception in closing JDBC Statement", ex);
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException ex) {
logger.warn("Exception in closing JDBC Connection", ex);
}
}
}
类似上面的代码非常常见。为了执行一个SQL语句,我们必须编写22行代码,而其中21行与应用逻辑并无关联,并且,这样的代码还会在系统其他地方(也许是每个需要数据库访问的地方)重复出现。
于是,大家开始寻找一些设计模式以改进如此的设计,Template模式的应用是其中一种典型的改进方案。Spring的JDBC封装,很大一部分就是借助Template模式实现,它提供了一个优秀的JDBC模板库,借助这个工具,我们可以简单有效的对传统的JDBC编码方式加以改进。下面是借助Spring JDBC Template修改过的代码,这段代码完成了与上面代码相同的功能。
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.update("UPDATE user SET age = 10 WHERE id = 'erica'");
可以看到,两行代码完成了上面需要19行代码实现的功能。所有冗余的代码都通过合理的抽象汇集到了JdbcTemplate中。无需感叹,借助Template模式,我们大致也能实现这样一个模板,不过,Spring的设计
者已经提前完成了这一步骤。org.springframework.jdbc.core.JdbcTemplate中包含了这个模板实现的代码,经过Spring设计小组精心设计,这个实现可以算的上是模板应用的典范。特别是回调(CallBack)的使用,使得整个模板结构清晰高效。值得一读。
Tips:实际开发中,可以将代码中硬编码的SQL语句作为Bean的一个String类型属性,借助DI机制在配置文件中定义,从而实现SQL的参数化配置。
再对上面的例子进行一些改进,通过PrepareStatement执行update操作以避免SQL
Injection 漏洞 9:
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate
.update(
"UPDATE user SET age = ? WHERE id = ?",
new PreparedStatementSetter() {
public void setValues(PreparedStatementSetter ps)
throws SQLException {
ps.setInt(1, 18);
ps.setString(2, "erica");
}
}
);
可以看到,上面引用了update方法的另一个版本,传入的参数有两个,第一个用于创建
PreparedStatement的SQL。第二个参数是为PreparedStatement设定参数的PreparedStatementSetter。
第二个参数的使用方法比较独到,我们动态新建了一个PreparedStatementSetter类,并实现了这个抽象类的setValues方法。之后将这个类的引用作为参数传递给update。update接受参数之后,即可调用第二个参数提供的方法完成PreparedStatement的初始化。
Spring JDBC Template中大量使用了这样的Callback机制,这带来了极强的灵活性和扩展性。
上面演示了update方法的使用(同样的操作适用于update、insert、delete)。下面是一个查询的示例。
final List userList = new ArrayList();
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate
.query(
"SELECT name, sex, address FROM user WHERE age > 18",
9 SQL Injection: SQL语句中直接引入参数值而导致的系统漏洞,具体请参见以下论文:
http://www.governmentsecurity.org/articles/SQLInjectionModesofAttackDefenceandWhyItMatters.php
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
new RowCallbackHandler() {
public void processRow(ResultSet rs) throws SQLException {
User user = new User();
user.setId(rs.getString("name"));
user.setSex(rs.getString("sex"));
user.setAddress(rs.getString("address"));
userList.add(product);
}
}
);
这里传入query方法的有两个参数,第一个是Select查询语句,第二个是一个RowCallbackHandler实例,我们通过RowCallbackHandler对Select语句得到的每行记录进行解析,并为其创建一个User数据对象。实现了手动的OR映射。此外,我们还可以通过JdbcTemplate.call方法调用存储过程。query、update方法还有其他很多不同参数版本的实现,具体调用方法请参见SpringJavaDoc。
JdbcTemplate与事务
上例中的JdbcTemplate操作采用的是JDBC默认的AutoCommit模式,也就是说我们还无法保证数据操作的原子性(要么全部生效,要么全部无效),如:
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.update("UPDATE user SET age = 10 WHERE id = 'erica'");
jdbcTemplate.update("UPDATE user SET age = age+1 WHERE id = 'erica'");
由于采用了AutoCommit模式,第一个update操作完成之后被自动提交,数据库中”erica”对应的记录已经被更新,如果第二个操作失败,我们无法使得整个事务回滚到最初状态。对于这个例子也许无关紧要,但是对于一个金融帐务系统而言,这样的问题将导致致命错误。
为了实现数据操作的原子性,我们需要在程序中引入事务逻辑,在JdbcTemplate中引入事务机制,在Spring中有两种方式:
1. 代码控制的事务管理
2. 参数化配置的事务管理
下面就这两种方式进行介绍。
u 代码控制的事务管理
首先,进行以下配置,假设配置文件为(Application-Context.xml):
<beans>
<bean id="dataSource"
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>net.sourceforge.jtds.jdbc.Driver</value>
</property>
<property name="url">
<value>jdbc:jtds:sqlserver://127.0.0.1:1433/Sample</value>
</property>
<property name="username">
<value>test</value>
</property>
<property name="password">
<value>changeit</value>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransac
tionManager">
<property name="dataSource">
<ref local="dataSource" />
</property>
</bean>
<bean id="userDAO" class="net.xiaxin.dao.UserDAO">
<property name="dataSource">
<ref local="dataSource" />
</property>
<property name="transactionManager">
<ref local="transactionManager" />
</property>
</bean>
</beans>
配置中包含了三个节点:
Ø dataSource
这里我们采用了apache dhcp组件提供的DataSource实现,并为其配置了JDBC驱动、数据库URL、用户名和密码等参数。
Ø transactionManager
针对JDBC DataSource类型的数据源,我们选用了DataSourceTransactionManager作为事务管理组件。
如果需要使用基于容器的数据源(JNDI),我们可以采用如下配置:
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
<bean id="dataSource"
class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName">
<value>jdbc/sample</value>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.transaction.jta.JtaTrans
actionManager"
/>
Ø userDAO
申明了一个UserDAO Bean,并为其指定了dataSource和transactionManger资源。
UserDAO对应的代码如下:
public class UserDAO {
private DataSource dataSource;
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
private PlatformTransactionManager transactionManager;
public PlatformTransactionManager getTransactionManager() {
return transactionManager;
}
public void setTransactionManager(PlatformTransactionManager
transactionManager) {
this.transactionManager = transactionManager;
}
public DataSource executeTestSource() {
return dataSource;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void insertUser() {
TransactionTemplate tt =
new TransactionTemplate(getTransactionManager());
tt.execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
JdbcTemplate jt = new JdbcTemplate(executeTestSource());
jt.update(
"insert into users (username) values ('xiaxin');");
jt.update(
"insert into users (id,username) values(2,
'erica');");
return null;
}
});
}
}
可以看到,在insertUser方法中,我们引入了一个新的模板类:
org.springframework.transaction.support.TransactionTemplate。
TransactionTemplate封装了事务管理的功能,包括异常时的事务回滚,以及操作成功后的事务提交。和JdbcTemplate一样,它使得我们无需在琐碎的try/catch/finally代码中徘徊。
在doInTransaction中进行的操作,如果抛出未捕获异常将被自动回滚,如果成功执行, 则将被自动提交。
这里我们故意制造了一些异常来观察数据库操作是否回滚(通过在第二条语句中更新自增ID字段故意触发一个异常):
编写一个简单的TestCase来观察实际效果:
InputStream is = new FileInputStream("Application-Context.xml");
XmlBeanFactory factory = new XmlBeanFactory(is);
UserDAO userDAO = (UserDAO) factory.getBean("userDAO");
userDAO.insertUser();
相信大家多少觉得上面的代码有点凌乱,Callback类的编写似乎也有悖于日常的编程习惯(虽然笔者觉得这一方法比较有趣,因为它巧妙的解决了笔者在早期自行开发数据访问模板中曾经遇到的问题)。如何进一步避免上面这些问题?Spring 的容器事务管理机制在这里即体现出其强大的能量。u 参数化配置的事务管理在上面的Application-Context.xml增加一个事务代理(UserDAOProxy)配置,同时,由于事务由容器管理,UserDAO不再需要TransactionManager设定,将其移除:
<bean id="UserDAOProxy"
class="org.springframework.transaction.interceptor.Transac
tionProxyFactoryBean">
<property name="transactionManager">
<ref bean="transactionManager" />
</property>
<property name="target">
<ref local="userDAO" />
</property>
<property name="transactionAttributes">
<props>
<prop key="insert*">PROPAGATION_REQUIRED</prop>
<prop key="*">PROPAGATION_REQUIRED,readOnly</prop>
</props>
</property>
</bean>
<bean id="userDAO" class="net.xiaxin.dao.UserDAO">
<property name="dataSource">
<ref local="dataSource" />
</property>
</bean>
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
上面的配置中,UserDAOProxy节点配置了一个针对userDAO bean的事务代理(由target属性指定)。通过transactionAttributes属性,我们指定了事务的管理策略,即对所有以insert开头的方法进行事务管理,如果被管理方法抛出异常,则自动回滚方法中的事务,如果成功执行,则在方法完成之后进行事务提交。另一方面对于其他方法(通过通配符*表示),则进行只读事务管理,以获得更好的性能。与之对应UserDAO.insertUser的代码修改如下:
public void insertUser(RegisterInfo regInfo) {
JdbcTemplate jt = new JdbcTemplate(executeTestSource());
jt.update("insert into users (username) values ('xiaxin');");
jt.update("insert into users (id,username) values (2,'erica');");
}
测试代码修改如下:
InputStream is = new FileInputStream("Application-Context.xml");
XmlBeanFactory factory = new XmlBeanFactory(is);
//注意这里须通过代理Bean"userDAOProxy"获得引用,而不是直接getBean(“userDAO”)
//此外这里还存在一个有关强制转型的潜在问题,请参见Hibernate in Spring一节后
//关于强制转型的补充描述。
UserDAO userDAO = (UserDAO) factory.getBean("userDAOProxy");
userDAO.insertUser();
可以看到,insertUser变得非常简洁。数据逻辑清晰可见,对比前面代码控制的事务管理,以及传统的JDBC操作,相信大家会有一些霍然开朗的感觉。细心的读者会说,这只不过将代码转移到了配置文件,并没有减少太多的工作量。这点区别也许并不重要,从应用维护的角度而言,配置化的事务管理显然更具优势。何况,实际开发中,如果前期设计细致,方法的事务特性确定之后一般不会发生大的变动,之后频繁的维护过程中,我们只需面对代码中的数据逻辑即可。上面我们结合JdbcTemplate介绍了Spring中的模板操作以及事务理机制。Spring作为一个开放式的应用开发平台。同时也针对其他组件提供了良好的支持。在持久层,Spring提供面向了Hibernate、ibatis和JDO的模板实现,同样,这些实现也为我们的开发提供了强有力的支持。
下面我们就hibernate、ibatis这两种主流持久层框架在Spring中的使用进行介绍。至于JDO,由于实际开发中使用并不广泛(实际上笔者觉得JDO前景堪忧),这里也就不重点介绍,有兴趣的读者可参见Spring-Reference中的相关章节。
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
Hibernate in Spring
Hibernate在开源的持久层框架中无疑是近期最为鲜亮的角色,其作者甚至被邀请加入
新版EJB设计工作之中,足见Hibernate设计的精彩贴切。关于Hibernate的使用,在笔者
的另外一篇文档中进行了探讨:
《Hibernate开发指南》 http://www.xiaxin.net/Hibernate_DEV_GUIDE.rar。
下面主要就Hibernate在Spring中的应用加以介绍,关于Hibernate本身就不多加描
述。
另外考虑到Spring对容器事务的良好支持,笔者建议在基于Spring Framework的应
用开发中,尽量使用容器管理事务,以获得数据逻辑代码的最佳可读性。下面的介绍中,将
略过代码控制的事务管理部分,而将重点放在参数化的容器事务管理应用。代码级事务管理
实现原理与上面JdbcTemplate中基本一致,感兴趣的读者可以参见Spring-Reference中
的相关内容。
出于简洁,我们还是沿用上面的示例。首先,针对Hibernate,我们需要进行如下配置:
Hibernate-Context.xml:
<beans>
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>net.sourceforge.jtds.jdbc.Driver</value>
</property>
<property name="url">
<value>jdbc:jtds:sqlserver://127.0.0.1:1433/Sample</value>
</property>
<property name="username">
<value>test</value>
</property>
<property name="password">
<value>changeit</value>
</property>
</bean>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate.LocalSessionFactoryBean"
>
<property name="dataSource">
<ref local="dataSource" />
</property>
<property name="mappingResources">
<list>
<value>net/xiaxin/dao/entity/User.hbm.xml</value>
</list>
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">
net.sf.hibernate.dialect.SQLServerDialect
</prop>
<prop key="hibernate.show_sql">
true
</prop>
</props>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate.HibernateTransactionMana
ger">
<property name="sessionFactory">
<ref local="sessionFactory" />
</property>
</bean>
<bean id="userDAO" class="net.xiaxin.dao.UserDAO">
<property name="sessionFactory">
<ref local="sessionFactory" />
</property>
</bean>
<bean id="userDAOProxy"
class="org.springframework.transaction.interceptor.TransactionPro
xyFactoryBean">
<property name="transactionManager">
<ref bean="transactionManager" />
</property>
<property name="target">
<ref local="userDAO" />
</property>
<property name="transactionAttributes">
<props>
<prop key="insert*">PROPAGATION_REQUIRED</prop>
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
</props>
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
</property>
</bean>
</beans>
与上面JDBC中的配置相对比,区别主要在于:
1. SessionFactory的引入
Hibernate中通过SessionFactory创建和维护Session。Spring对SessionFactory的配置也进行了整合,无需再通过Hibernate.cfg.xml对SessionFactory进行设定。
SessionFactory节点的mappingResources属性包含了映射文件的路径,list节点下可配置多个映射文件。
hibernateProperties节点则容纳了所有的属性配置。
可以对应传统的Hibernate.cfg.xml文件结构对这里的SessionFactory配置进行解读。
2. 采用面向Hibernate的TransactionManager实现:
org.springframework.orm.hibernate.HibernateTransactionManag
er
可以看到,对于事务管理配置,基本与上一章节中相同。对应刚才的Users表,建立如下映射类:
User.java:
/**
* @hibernate.class table="users"
*/
public class User {
public Integer id;
public String username;
public String password;
/**
* @hibernate.id
* column="id"
* type="java.lang.Integer"
* generator-class="native"
*/
public Integer getId() {
return id;
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
}
public void setId(Integer id) {
this.id = id;
}
/**
* @hibernate.property column="password" length="50"
*/
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
/**
* @hibernate.property column="username" length="50"
*/
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
上面的代码中,通过xdoclet指定了类/表;属性/字段的映射关系,通过xdoclet anttask 我们可以根据代码生成对应的user.hbm.xml文件。具体细节请参见《hibernate开发指南》一文。
下面是生成的user.hbm.xml:
<hibernate-mapping>
<class
name="net.xiaxin.dao.entity.User"
table="users"
dynamic-update="false"
dynamic-insert="false"
>
<id
name="id"
column="id"
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
type="java.lang.Integer"
>
<generator class="native">
</generator>
</id>
<property
name="password"
type="java.lang.String"
update="true"
insert="true"
access="property"
column="password"
length="50"
/>
<property
name="username"
type="java.lang.String"
update="true"
insert="true"
access="property"
column="username"
length="50"
/>
</class>
</hibernate-mapping>
UserDAO.java:
public class UserDAO extends HibernateDaoSupport implements IUserDAO
{
public void insertUser(User user) {
getHibernateTemplate().saveOrUpdate(user);
}
}
看到这段代码想必会有点诧异,似乎太简单了一点……,不过这已经足够。短短一行代码我们已经实现了与上一章中示例相同的功能,这也正体现了Spring+Hibernate的威力所在。
上面的UserDAO实现了自定义的IUserDAO接口(这里的IUserDAO接口仅包含insertUser方法的定义,不过除此之外,它还有另一层含义,见下面的代码测试部分),
并扩展了抽象类:
HibernateDaoSupport
HibernateSupport实现了HibernateTemplate和SessionFactory实例的关联。与JdbcTemplate类似,HibernateTemplate对Hibernate Session操作进行了封装,而HibernateTemplate.execute方法则是一封装机制的核心,感兴趣的读者可以
研究一下其实现机制。借助HibernateTemplate我们可以脱离每次数据操作必须首先获得Session实例、启动事务、提交/回滚事务以及烦杂的try/catch/finally的繁琐操作。从而获得以上代码中精干集中的逻辑呈现效果。
对比下面这段实现了同样功能的Hibernate原生代码,想必更有体会:
Session session
try {
Configuration config = new Configuration().configure();
SessionFactory sessionFactory =
config.buildSessionFactory();
session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
User user = new User();
user.setName("erica");
user.setPassword("mypass");
session.save(user);
tx.commit();
} catch (HibernateException e) {
e.printStackTrace();
tx.rollback();
}finally{
session.close();
}
测试代码:
InputStream is = new FileInputStream("Hibernate-Context.xml");
XmlBeanFactory factory = new XmlBeanFactory(is);
IUserDAO userDAO = (IUserDAO)factory.getBean("userDAOProxy");
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
User user = new User();
user.setUsername("erica");
user.setPassword("mypass");
userDAO.insertUser(user);
这段代码似乎并没有什么特殊,但有一个细微之处:
IUserDAO userDAO = (IUserDAO)factory.getBean("userDAOProxy");
这里并没有直接用UserDAO对获得的Bean实例进行强制转型。这与上面JdbcTemplate的测试代码不同。并非完全出自设计上的考虑,这里情况有些特殊,我们可以尝试一下用UserDAO类对bean实例进行强制转型,不过将得到一个ClassCastException,程序异常中止。
为什么会出现这样的问题?是不是只有在使用Hibernate才会出现这样的问题?事实并非如此,如果对上面基于JdbcTempate的UserDAO进行改造,使之实现IUserDAO接口,同样的问题也将会出现。IUserDAO接口本身非常简单(仅包含一个insertUser方法的定义),显然也不是导致异常的原因所在。原因在于Spring的AOP实现机制,前面曾经提及,Spring中的事务管理实际上是基于动态AOP机制实现,为了实现动态AOP,Spring在默认情况下会使用Java DynamicProxy,但是,Dynamic Proxy要求其代理的对象必须实现一个接口,该接口定义了准备进行代理的方法。而对于没有实现任何接口的Java Class,需要采用其他方式,Spring通过CGLib10实现这一功能。
当UserDAO没有实现任何接口时(如JdbcTemplate示例中)。Spring通过CGLib对UserDAO进行代理,此时getBean返回的是一个继承自UserDAO类的子类实例,可以通过UserDAO对其强制转型。而当UserDAO实现了IUserDAO接口之后,Spring将通过JavaDynamic Proxy机制实现代理功能,此时返回的Bean,是通过java.lang.reflect.Proxy.newProxyInstance方法创建的IUserDAO接口的一个代理实现,这个实例实现了IUserDAO接口,但与UserDAO类已经没有继承关系,因此无法通过UserDAO强制转型。由于此问题牵涉到较为底层的代理机制实现原理,下面的AOP章节中我们再进行详细探讨。
实际开发中,应该面向接口编程,通过接口来调用Bean提供的服务。
10 CGLib可以在运行期对Class行为进行修改。由于其功能强大,性能出众,常常被作为Java Dynamic Proxy
之外的动态Proxy模式的实现基础。在Spring、Hibernate中都用到了CGLib类库。
1. 依赖特定事务资源的事务处理
这是应用开发中最常见的模式,即通过特定资源提供的事务机制进行事务管理。
如通过JDBC、JTA 的rollback、commit方法;Hibernate Transaction 的rollback、commit方法等。这种方法大家已经相当熟悉。
2. 依赖容器的参数化事务管理
通过容器提供的集约式参数化事务机制,实现事务的外部管理,如EJB 中的事务管理模式。
如,下面的EJB事务定义中,将SessionBean MySession的doService方
法定义为Required。也就是说,当MySession.doServer 方法被某个线程调用时,容器将此线程纳入事务管理容器,方法调用过程中如果发生异常,当前事务将被容器自动回滚,如果方法正常结束,则容器将自动提交当前事务。
<container-transaction >
<method >
<ejb-name>MySession</ejb-name>
<method-intf>Remote</method-intf>
<method-name>doService</method-name>
<method-params>
<method-param>java.lang.String</method-param>
</method-params>
</method>
<trans-attribute>Required</trans-attribute>
</container-transaction>
容器管理的参数化事务为程序开发提供了相当的灵活性,同时因为将事务委托给容器进行管理,应用逻辑中无需再编写事务代码,大大节省了代码量(特别是针对需要同时操作多个事务资源的应用),从而提高了生产率。然而,使用EJB 事务管理的代价相当高昂,撇开EJB 容器不菲的价格,EJB的学习成本,部署、迁移、维护难度,以及容器本身带来的性能开销(这往往意味着需要更高的硬件配置)都给我们带来了相当的困惑。此时事务管理所带来的优势往往还不能抵消上面这些负面影响。
Spring事务管理能给我们带来什么?
下面这段xml配置片断展示了Spring中的事务设定方式:
<beans>
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>org.gjt.mm.mysql.Driver</value>
</property>
<property name="url">
<value>jdbc:mysql://localhost/sample</value>
</property>
<property name="username">
<value>user</value>
</property>
<property name="password">
<value>mypass</value>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTr
ansactionManager">
<property name="dataSource">
<ref local="dataSource" />
</property>
</bean>
<bean id="userDAO" class="net.xiaxin.dao.UserDAO">
<property name="dataSource">
<ref local="dataSource" />
</property>
</bean>
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
<bean id="userDAOProxy"
class="org.springframework.transaction.interceptor.Tran
sactionProxyFactoryBean">
<property name="transactionManager">
<ref bean="transactionManager" />
</property>
<property name="target">
<ref local="userDAO" />
</property>
<property name="transactionAttributes">
<props>
<prop key="insert*">PROPAGATION_REQUIRED</prop>
<prop key="get*">
PROPAGATION_REQUIRED,readOnly
</prop>
</props>
</property>
</bean>
</beans>
配置中包含了dataSource,transactionManager 等资源定义。这些资源都为一个名为userDAOProxy 的TransactionProxyFactoryBean 服务, 而userDAOProxy 则对包含实际数据逻辑的userDAO进行了事务性封装。
可以看到,在userDAOProxy 的"transactionAttributes"属性中,我们定义了针对userDAO 的事务策略,即将所有名称以insert 开始的方法(如UserDAO.insertUser方法)纳入事务管理范围。如果此方法中抛出异常,则Spring
将当前事务回滚,如果方法正常结束,则提交事务。而对所有名称以get 开始的方法(如UserDAO.getUser 方法)则以只读的事务处理机制进行处理。(设为只读型事务,可以使持久层尝试对数据操作进行优化,如对
于只读事务Hibernate将不执行flush操作,而某些数据库连接池和JDBC 驱动也对只读型操作进行了特别化。)
结合上面这段申明带来的感性认知,看看Spring 的事务管理机制与EJB 中事务管理有何不同,或者有何优势。这里自然有许多方面可以比较,不过,笔者认为其中最为关键的两点是:
1. Spring可以将任意Java Class 纳入事务管理
这里的UserDAO只是我们编写的一个普通Java Class,其中包含了一些基本的数据应用逻辑。通过Spring,我们即可简单的实现事务的可配置化。也就是说,我们可以随意为某个类的某个方法指定事务管理机制。与之对比,如果使用EJB容器提供的事务管理功能,我们不得不按照EJB规范编将UserDAO 进行改造,将其转换为一个标准的EJB。
2. Spring事务管理并不依赖特定的事务资源。
EJB 容器必须依赖于JTA 提供事务支持。而Spring 的事务管理则支持JDBC、JTA 等多种事务资源。这为我们提供了更多的选择,从而也使得我们的系统部署更加灵活。
对Spring事务管理机制进行简单分析之后,我们将结合持久层封装的具体事务应用机制,对Spring中的事务管理进行更具实效的探讨。
持久层封装
JDBC
Spring对JDBC进行了良好的封装,通过提供相应的模板和辅助类,在相当程度上降低了JDBC操作的复杂性。并且得益于Spring良好的隔离设计,JDBC封装类库可以脱离Spring Context独立使用,也就是说,即使系统并没有采用Spring作为结构性框架,我们也可以单独使用Spring的JDBC部分(spring-dao.jar)来改善我们的代码。作为对比,首先让我们来看一段传统的JDBC代码:
Connection conn =null;
Statement stmt = null;
try {
conn = dataSource.getConnection();
stmt = con.createStatement();
stmt.executeUpdate("UPDATE user SET age = 18 WHERE id = 'erica'");
} finally {
if (stmt != null) {
try {
stmt.close();
} catch (SQLException ex) {
logger.warn("Exception in closing JDBC Statement", ex);
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException ex) {
logger.warn("Exception in closing JDBC Connection", ex);
}
}
}
类似上面的代码非常常见。为了执行一个SQL语句,我们必须编写22行代码,而其中21行与应用逻辑并无关联,并且,这样的代码还会在系统其他地方(也许是每个需要数据库访问的地方)重复出现。
于是,大家开始寻找一些设计模式以改进如此的设计,Template模式的应用是其中一种典型的改进方案。Spring的JDBC封装,很大一部分就是借助Template模式实现,它提供了一个优秀的JDBC模板库,借助这个工具,我们可以简单有效的对传统的JDBC编码方式加以改进。下面是借助Spring JDBC Template修改过的代码,这段代码完成了与上面代码相同的功能。
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.update("UPDATE user SET age = 10 WHERE id = 'erica'");
可以看到,两行代码完成了上面需要19行代码实现的功能。所有冗余的代码都通过合理的抽象汇集到了JdbcTemplate中。无需感叹,借助Template模式,我们大致也能实现这样一个模板,不过,Spring的设计
者已经提前完成了这一步骤。org.springframework.jdbc.core.JdbcTemplate中包含了这个模板实现的代码,经过Spring设计小组精心设计,这个实现可以算的上是模板应用的典范。特别是回调(CallBack)的使用,使得整个模板结构清晰高效。值得一读。
Tips:实际开发中,可以将代码中硬编码的SQL语句作为Bean的一个String类型属性,借助DI机制在配置文件中定义,从而实现SQL的参数化配置。
再对上面的例子进行一些改进,通过PrepareStatement执行update操作以避免SQL
Injection 漏洞 9:
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate
.update(
"UPDATE user SET age = ? WHERE id = ?",
new PreparedStatementSetter() {
public void setValues(PreparedStatementSetter ps)
throws SQLException {
ps.setInt(1, 18);
ps.setString(2, "erica");
}
}
);
可以看到,上面引用了update方法的另一个版本,传入的参数有两个,第一个用于创建
PreparedStatement的SQL。第二个参数是为PreparedStatement设定参数的PreparedStatementSetter。
第二个参数的使用方法比较独到,我们动态新建了一个PreparedStatementSetter类,并实现了这个抽象类的setValues方法。之后将这个类的引用作为参数传递给update。update接受参数之后,即可调用第二个参数提供的方法完成PreparedStatement的初始化。
Spring JDBC Template中大量使用了这样的Callback机制,这带来了极强的灵活性和扩展性。
上面演示了update方法的使用(同样的操作适用于update、insert、delete)。下面是一个查询的示例。
final List userList = new ArrayList();
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate
.query(
"SELECT name, sex, address FROM user WHERE age > 18",
9 SQL Injection: SQL语句中直接引入参数值而导致的系统漏洞,具体请参见以下论文:
http://www.governmentsecurity.org/articles/SQLInjectionModesofAttackDefenceandWhyItMatters.php
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
new RowCallbackHandler() {
public void processRow(ResultSet rs) throws SQLException {
User user = new User();
user.setId(rs.getString("name"));
user.setSex(rs.getString("sex"));
user.setAddress(rs.getString("address"));
userList.add(product);
}
}
);
这里传入query方法的有两个参数,第一个是Select查询语句,第二个是一个RowCallbackHandler实例,我们通过RowCallbackHandler对Select语句得到的每行记录进行解析,并为其创建一个User数据对象。实现了手动的OR映射。此外,我们还可以通过JdbcTemplate.call方法调用存储过程。query、update方法还有其他很多不同参数版本的实现,具体调用方法请参见SpringJavaDoc。
JdbcTemplate与事务
上例中的JdbcTemplate操作采用的是JDBC默认的AutoCommit模式,也就是说我们还无法保证数据操作的原子性(要么全部生效,要么全部无效),如:
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.update("UPDATE user SET age = 10 WHERE id = 'erica'");
jdbcTemplate.update("UPDATE user SET age = age+1 WHERE id = 'erica'");
由于采用了AutoCommit模式,第一个update操作完成之后被自动提交,数据库中”erica”对应的记录已经被更新,如果第二个操作失败,我们无法使得整个事务回滚到最初状态。对于这个例子也许无关紧要,但是对于一个金融帐务系统而言,这样的问题将导致致命错误。
为了实现数据操作的原子性,我们需要在程序中引入事务逻辑,在JdbcTemplate中引入事务机制,在Spring中有两种方式:
1. 代码控制的事务管理
2. 参数化配置的事务管理
下面就这两种方式进行介绍。
u 代码控制的事务管理
首先,进行以下配置,假设配置文件为(Application-Context.xml):
<beans>
<bean id="dataSource"
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>net.sourceforge.jtds.jdbc.Driver</value>
</property>
<property name="url">
<value>jdbc:jtds:sqlserver://127.0.0.1:1433/Sample</value>
</property>
<property name="username">
<value>test</value>
</property>
<property name="password">
<value>changeit</value>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransac
tionManager">
<property name="dataSource">
<ref local="dataSource" />
</property>
</bean>
<bean id="userDAO" class="net.xiaxin.dao.UserDAO">
<property name="dataSource">
<ref local="dataSource" />
</property>
<property name="transactionManager">
<ref local="transactionManager" />
</property>
</bean>
</beans>
配置中包含了三个节点:
Ø dataSource
这里我们采用了apache dhcp组件提供的DataSource实现,并为其配置了JDBC驱动、数据库URL、用户名和密码等参数。
Ø transactionManager
针对JDBC DataSource类型的数据源,我们选用了DataSourceTransactionManager作为事务管理组件。
如果需要使用基于容器的数据源(JNDI),我们可以采用如下配置:
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
<bean id="dataSource"
class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName">
<value>jdbc/sample</value>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.transaction.jta.JtaTrans
actionManager"
/>
Ø userDAO
申明了一个UserDAO Bean,并为其指定了dataSource和transactionManger资源。
UserDAO对应的代码如下:
public class UserDAO {
private DataSource dataSource;
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
private PlatformTransactionManager transactionManager;
public PlatformTransactionManager getTransactionManager() {
return transactionManager;
}
public void setTransactionManager(PlatformTransactionManager
transactionManager) {
this.transactionManager = transactionManager;
}
public DataSource executeTestSource() {
return dataSource;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void insertUser() {
TransactionTemplate tt =
new TransactionTemplate(getTransactionManager());
tt.execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
JdbcTemplate jt = new JdbcTemplate(executeTestSource());
jt.update(
"insert into users (username) values ('xiaxin');");
jt.update(
"insert into users (id,username) values(2,
'erica');");
return null;
}
});
}
}
可以看到,在insertUser方法中,我们引入了一个新的模板类:
org.springframework.transaction.support.TransactionTemplate。
TransactionTemplate封装了事务管理的功能,包括异常时的事务回滚,以及操作成功后的事务提交。和JdbcTemplate一样,它使得我们无需在琐碎的try/catch/finally代码中徘徊。
在doInTransaction中进行的操作,如果抛出未捕获异常将被自动回滚,如果成功执行, 则将被自动提交。
这里我们故意制造了一些异常来观察数据库操作是否回滚(通过在第二条语句中更新自增ID字段故意触发一个异常):
编写一个简单的TestCase来观察实际效果:
InputStream is = new FileInputStream("Application-Context.xml");
XmlBeanFactory factory = new XmlBeanFactory(is);
UserDAO userDAO = (UserDAO) factory.getBean("userDAO");
userDAO.insertUser();
相信大家多少觉得上面的代码有点凌乱,Callback类的编写似乎也有悖于日常的编程习惯(虽然笔者觉得这一方法比较有趣,因为它巧妙的解决了笔者在早期自行开发数据访问模板中曾经遇到的问题)。如何进一步避免上面这些问题?Spring 的容器事务管理机制在这里即体现出其强大的能量。u 参数化配置的事务管理在上面的Application-Context.xml增加一个事务代理(UserDAOProxy)配置,同时,由于事务由容器管理,UserDAO不再需要TransactionManager设定,将其移除:
<bean id="UserDAOProxy"
class="org.springframework.transaction.interceptor.Transac
tionProxyFactoryBean">
<property name="transactionManager">
<ref bean="transactionManager" />
</property>
<property name="target">
<ref local="userDAO" />
</property>
<property name="transactionAttributes">
<props>
<prop key="insert*">PROPAGATION_REQUIRED</prop>
<prop key="*">PROPAGATION_REQUIRED,readOnly</prop>
</props>
</property>
</bean>
<bean id="userDAO" class="net.xiaxin.dao.UserDAO">
<property name="dataSource">
<ref local="dataSource" />
</property>
</bean>
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
上面的配置中,UserDAOProxy节点配置了一个针对userDAO bean的事务代理(由target属性指定)。通过transactionAttributes属性,我们指定了事务的管理策略,即对所有以insert开头的方法进行事务管理,如果被管理方法抛出异常,则自动回滚方法中的事务,如果成功执行,则在方法完成之后进行事务提交。另一方面对于其他方法(通过通配符*表示),则进行只读事务管理,以获得更好的性能。与之对应UserDAO.insertUser的代码修改如下:
public void insertUser(RegisterInfo regInfo) {
JdbcTemplate jt = new JdbcTemplate(executeTestSource());
jt.update("insert into users (username) values ('xiaxin');");
jt.update("insert into users (id,username) values (2,'erica');");
}
测试代码修改如下:
InputStream is = new FileInputStream("Application-Context.xml");
XmlBeanFactory factory = new XmlBeanFactory(is);
//注意这里须通过代理Bean"userDAOProxy"获得引用,而不是直接getBean(“userDAO”)
//此外这里还存在一个有关强制转型的潜在问题,请参见Hibernate in Spring一节后
//关于强制转型的补充描述。
UserDAO userDAO = (UserDAO) factory.getBean("userDAOProxy");
userDAO.insertUser();
可以看到,insertUser变得非常简洁。数据逻辑清晰可见,对比前面代码控制的事务管理,以及传统的JDBC操作,相信大家会有一些霍然开朗的感觉。细心的读者会说,这只不过将代码转移到了配置文件,并没有减少太多的工作量。这点区别也许并不重要,从应用维护的角度而言,配置化的事务管理显然更具优势。何况,实际开发中,如果前期设计细致,方法的事务特性确定之后一般不会发生大的变动,之后频繁的维护过程中,我们只需面对代码中的数据逻辑即可。上面我们结合JdbcTemplate介绍了Spring中的模板操作以及事务理机制。Spring作为一个开放式的应用开发平台。同时也针对其他组件提供了良好的支持。在持久层,Spring提供面向了Hibernate、ibatis和JDO的模板实现,同样,这些实现也为我们的开发提供了强有力的支持。
下面我们就hibernate、ibatis这两种主流持久层框架在Spring中的使用进行介绍。至于JDO,由于实际开发中使用并不广泛(实际上笔者觉得JDO前景堪忧),这里也就不重点介绍,有兴趣的读者可参见Spring-Reference中的相关章节。
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
Hibernate in Spring
Hibernate在开源的持久层框架中无疑是近期最为鲜亮的角色,其作者甚至被邀请加入
新版EJB设计工作之中,足见Hibernate设计的精彩贴切。关于Hibernate的使用,在笔者
的另外一篇文档中进行了探讨:
《Hibernate开发指南》 http://www.xiaxin.net/Hibernate_DEV_GUIDE.rar。
下面主要就Hibernate在Spring中的应用加以介绍,关于Hibernate本身就不多加描
述。
另外考虑到Spring对容器事务的良好支持,笔者建议在基于Spring Framework的应
用开发中,尽量使用容器管理事务,以获得数据逻辑代码的最佳可读性。下面的介绍中,将
略过代码控制的事务管理部分,而将重点放在参数化的容器事务管理应用。代码级事务管理
实现原理与上面JdbcTemplate中基本一致,感兴趣的读者可以参见Spring-Reference中
的相关内容。
出于简洁,我们还是沿用上面的示例。首先,针对Hibernate,我们需要进行如下配置:
Hibernate-Context.xml:
<beans>
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>net.sourceforge.jtds.jdbc.Driver</value>
</property>
<property name="url">
<value>jdbc:jtds:sqlserver://127.0.0.1:1433/Sample</value>
</property>
<property name="username">
<value>test</value>
</property>
<property name="password">
<value>changeit</value>
</property>
</bean>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate.LocalSessionFactoryBean"
>
<property name="dataSource">
<ref local="dataSource" />
</property>
<property name="mappingResources">
<list>
<value>net/xiaxin/dao/entity/User.hbm.xml</value>
</list>
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">
net.sf.hibernate.dialect.SQLServerDialect
</prop>
<prop key="hibernate.show_sql">
true
</prop>
</props>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate.HibernateTransactionMana
ger">
<property name="sessionFactory">
<ref local="sessionFactory" />
</property>
</bean>
<bean id="userDAO" class="net.xiaxin.dao.UserDAO">
<property name="sessionFactory">
<ref local="sessionFactory" />
</property>
</bean>
<bean id="userDAOProxy"
class="org.springframework.transaction.interceptor.TransactionPro
xyFactoryBean">
<property name="transactionManager">
<ref bean="transactionManager" />
</property>
<property name="target">
<ref local="userDAO" />
</property>
<property name="transactionAttributes">
<props>
<prop key="insert*">PROPAGATION_REQUIRED</prop>
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
</props>
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
</property>
</bean>
</beans>
与上面JDBC中的配置相对比,区别主要在于:
1. SessionFactory的引入
Hibernate中通过SessionFactory创建和维护Session。Spring对SessionFactory的配置也进行了整合,无需再通过Hibernate.cfg.xml对SessionFactory进行设定。
SessionFactory节点的mappingResources属性包含了映射文件的路径,list节点下可配置多个映射文件。
hibernateProperties节点则容纳了所有的属性配置。
可以对应传统的Hibernate.cfg.xml文件结构对这里的SessionFactory配置进行解读。
2. 采用面向Hibernate的TransactionManager实现:
org.springframework.orm.hibernate.HibernateTransactionManag
er
可以看到,对于事务管理配置,基本与上一章节中相同。对应刚才的Users表,建立如下映射类:
User.java:
/**
* @hibernate.class table="users"
*/
public class User {
public Integer id;
public String username;
public String password;
/**
* @hibernate.id
* column="id"
* type="java.lang.Integer"
* generator-class="native"
*/
public Integer getId() {
return id;
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
}
public void setId(Integer id) {
this.id = id;
}
/**
* @hibernate.property column="password" length="50"
*/
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
/**
* @hibernate.property column="username" length="50"
*/
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
上面的代码中,通过xdoclet指定了类/表;属性/字段的映射关系,通过xdoclet anttask 我们可以根据代码生成对应的user.hbm.xml文件。具体细节请参见《hibernate开发指南》一文。
下面是生成的user.hbm.xml:
<hibernate-mapping>
<class
name="net.xiaxin.dao.entity.User"
table="users"
dynamic-update="false"
dynamic-insert="false"
>
<id
name="id"
column="id"
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
type="java.lang.Integer"
>
<generator class="native">
</generator>
</id>
<property
name="password"
type="java.lang.String"
update="true"
insert="true"
access="property"
column="password"
length="50"
/>
<property
name="username"
type="java.lang.String"
update="true"
insert="true"
access="property"
column="username"
length="50"
/>
</class>
</hibernate-mapping>
UserDAO.java:
public class UserDAO extends HibernateDaoSupport implements IUserDAO
{
public void insertUser(User user) {
getHibernateTemplate().saveOrUpdate(user);
}
}
看到这段代码想必会有点诧异,似乎太简单了一点……,不过这已经足够。短短一行代码我们已经实现了与上一章中示例相同的功能,这也正体现了Spring+Hibernate的威力所在。
上面的UserDAO实现了自定义的IUserDAO接口(这里的IUserDAO接口仅包含insertUser方法的定义,不过除此之外,它还有另一层含义,见下面的代码测试部分),
并扩展了抽象类:
HibernateDaoSupport
HibernateSupport实现了HibernateTemplate和SessionFactory实例的关联。与JdbcTemplate类似,HibernateTemplate对Hibernate Session操作进行了封装,而HibernateTemplate.execute方法则是一封装机制的核心,感兴趣的读者可以
研究一下其实现机制。借助HibernateTemplate我们可以脱离每次数据操作必须首先获得Session实例、启动事务、提交/回滚事务以及烦杂的try/catch/finally的繁琐操作。从而获得以上代码中精干集中的逻辑呈现效果。
对比下面这段实现了同样功能的Hibernate原生代码,想必更有体会:
Session session
try {
Configuration config = new Configuration().configure();
SessionFactory sessionFactory =
config.buildSessionFactory();
session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
User user = new User();
user.setName("erica");
user.setPassword("mypass");
session.save(user);
tx.commit();
} catch (HibernateException e) {
e.printStackTrace();
tx.rollback();
}finally{
session.close();
}
测试代码:
InputStream is = new FileInputStream("Hibernate-Context.xml");
XmlBeanFactory factory = new XmlBeanFactory(is);
IUserDAO userDAO = (IUserDAO)factory.getBean("userDAOProxy");
SpringFrameWork Developer’s Guide Version 0.6
October 8, 2004 So many open source projects. Why not Open your Documents?
User user = new User();
user.setUsername("erica");
user.setPassword("mypass");
userDAO.insertUser(user);
这段代码似乎并没有什么特殊,但有一个细微之处:
IUserDAO userDAO = (IUserDAO)factory.getBean("userDAOProxy");
这里并没有直接用UserDAO对获得的Bean实例进行强制转型。这与上面JdbcTemplate的测试代码不同。并非完全出自设计上的考虑,这里情况有些特殊,我们可以尝试一下用UserDAO类对bean实例进行强制转型,不过将得到一个ClassCastException,程序异常中止。
为什么会出现这样的问题?是不是只有在使用Hibernate才会出现这样的问题?事实并非如此,如果对上面基于JdbcTempate的UserDAO进行改造,使之实现IUserDAO接口,同样的问题也将会出现。IUserDAO接口本身非常简单(仅包含一个insertUser方法的定义),显然也不是导致异常的原因所在。原因在于Spring的AOP实现机制,前面曾经提及,Spring中的事务管理实际上是基于动态AOP机制实现,为了实现动态AOP,Spring在默认情况下会使用Java DynamicProxy,但是,Dynamic Proxy要求其代理的对象必须实现一个接口,该接口定义了准备进行代理的方法。而对于没有实现任何接口的Java Class,需要采用其他方式,Spring通过CGLib10实现这一功能。
当UserDAO没有实现任何接口时(如JdbcTemplate示例中)。Spring通过CGLib对UserDAO进行代理,此时getBean返回的是一个继承自UserDAO类的子类实例,可以通过UserDAO对其强制转型。而当UserDAO实现了IUserDAO接口之后,Spring将通过JavaDynamic Proxy机制实现代理功能,此时返回的Bean,是通过java.lang.reflect.Proxy.newProxyInstance方法创建的IUserDAO接口的一个代理实现,这个实例实现了IUserDAO接口,但与UserDAO类已经没有继承关系,因此无法通过UserDAO强制转型。由于此问题牵涉到较为底层的代理机制实现原理,下面的AOP章节中我们再进行详细探讨。
实际开发中,应该面向接口编程,通过接口来调用Bean提供的服务。
10 CGLib可以在运行期对Class行为进行修改。由于其功能强大,性能出众,常常被作为Java Dynamic Proxy
之外的动态Proxy模式的实现基础。在Spring、Hibernate中都用到了CGLib类库。