Spring In Action 学习总结
摘要:本文是笔者在学习了《Spring In Action》后的读书笔记,主要是对该书前7章的学习和心得总结。
一、简化Java开发
1. Spring采用的4个关键策略
- 基于POJO的轻量级和最小侵入性编程
- 通过依赖注入和面向接口实现松耦合
- 基于切面和管理进行声明式编程
- 通过切面和模板减少样板式代码
2. 关于命名空间
<aop:config> <aop:aspectref="customLogManager">
上述配置是spring的支持的AOP的配置,其中aop被称为命名空间,每一种独立的模块,都会有独立的命名空间,如context,tx等,这些命名空间,需要在application.xml的头部引入对应的描述文件,如下:
<beansxmlns=http://www.springframework.org/schema/beans
xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xmlns:p=http://www.springframework.org/schema/p
xmlns:context=http://www.springframework.org/schema/context xmlns:aop=http://www.springframework.org/schema/aop
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
二、 依赖注入IoC
依赖注入最大的优势是可以实现松耦合和面向接口编程,代码实现中以接口的形式编码,具体的实现类通过依赖关系自动注入,当后续实现类改动时,可以保证使用它的类改动最小。
1. 依赖注入和面向接口编程---松耦合
Bean的定义使用的是类的包路径,而bean注入到某个属性中时,建议属性的类型是类实现的接口,也就是说使用bean时,以接口的方式使用,而注入时负责指明注入的是这个接口的哪个实现类,由此做到松耦合,如下:
publicinterface IssueDaoIntf {
public List<IssueDts> getByNumber(StringissueNumber);
publicclass IssueDao implements IssueDaoIntf
{
public List<IssueDts> getByNumber(StringissueNumber)
{
Query q = getSession().createQuery("...");
q.setParameter("issueNumber",issueNumber);
return (List<IssueDts>) q.list();
}
publicclass IssueService {
private IssueDaoIntf issueDao;
//getter/setter
public List<IssueDts> getByNumber(StringissueNumber)
{
getPrintLog();
return issueDao.getByNumber(issueNumber);
}
<bean id="issueDao"class="com.dts.dao.IssueDao">
......
</bean>
<bean id="issueService"class="com.dts.service.IssueService">
<propertyname="issueDao" ref="issueDao"></property>
</bean>
按照上述实现,当com.dts.dao.IssueDao的实现需要替换时,只需要让新的类实现IssueDaoIntf接口即可,IssueService不需要做任何修改,这样就做到了松耦合。
2. 使用配置文件作为注入属性的值
<context:property-placeholder location="classpath:DTSConfig.properties"/>
上述配置可以指明哪个文件中的配置可以被拿来作为属性值获取,使用方式:value="${dts.jdbc.driverClass}"文件内容如下:dts.jdbc.driverClass=com.microsoft.sqlserver.jdbc.SQLServerDriver
PS:使用context标签,需要引入对应的命名空间,参见1.2
三、AOP
1. 概念与由来
- Spring支持面向切面编程,但他不是第一个支持面向切面编程的,最早支持的且支持的最好的是AspectJ,支持它比较复杂。
- 面向切面编程是把一些各个bean均会涉及到的基础功能抽象为切面,提供以切面为单位的编程,也就是说你的编程是针对某个切面的,而这个切面必然是涉及到具备类似特点的一系列bean。
- 面向切面编程采用的是织入的思想,其实现的技术手动是通过动态代理,也就是说原来由一个bean调用另一个bean的某个方法,变为一个bean调用另一个bean的动态代理类的对应方法,再由这个对应方法在合适的时机去调用被代理的bean的真实方法,在这过程中,代理类可以做一些额外的事情,这些事情的实现代码就被称为织入的代码
2. 术语介绍
在AOP中,最难理解的,莫过于切点、切面、通知、连接点这几个概念。现逐个介绍,以如下的例子(以切面的方式完成日志的持久化)为例:
<beanid="customLogManager" class="com.dts.log.CustomLogManager">
<property name="dtsLogService"ref="dtsLogService"></property>
</bean>
<aop:config>
<aop:aspectref="customLogManager">
<aop:pointcutid="getAllDtsPoint" expression="execution(* com.dts.service.IssueService.get*(..))"/>
<aop:beforepointcut-ref="getAllDtsPoint" method="markLogInfoBefore"/>
<aop:afterpointcut-ref="getAllDtsPoint" method="markLogInfoAfter"/>
</aop:aspect>
<aop:aspectref="customLogManager">
<aop:pointcut id="saveDtsPoint"expression="execution(* com.dts.service.IssueService.save*(..))"/>
<aop:aroundpointcut-ref="saveDtsPoint" method="markLogByAround" />
</aop:aspect>
</aop:config>
上述配置是一个完整的AOP配置,分析如下:定义一个bean(customLogManager),在后面的切面中使用。每个切面最终都要完成通知对应的实际行为,比如把本次日志写入数据库,这些实际行为就需要切面关联的这个bean来完成,所以customLogManager中必定需要有markLogInfoBefore、markLogInfoAfter和markLogByAround。此外,这是一个纯粹的POJO,它依然可以像普通的bean一样,通过spring注入需要的实例变量,如dtsLogService
- <aop:aspectref="customLogManager">定义了一个切面,切面其实是一组配置的总称,即根据切点定义的规则,配上前后置通知,得到一个切面,也可以认为是根据上述条件"切"出来的一个结果面,是一系列连接点的集合。切点(<aop:pointcut id="。。。"expression="execution(* com.dts.service.IssueService.save*(..))"),其实是给出了一个"切"的规则,即满足怎么样的条件的方法,会被切到,从而算作是这个切面的。其中expression就给出了表达式,这种称为AspectJ切点表达式。
- 通知(<aop:beforepointcut-ref="getAllDtsPoint" method="markLogInfoBefore"),是为某个切点定义了具体的行为,即被"切"到的方法,需要做些什么额外的事情。通知包括前置、后置、和环绕,而这些通知最终都需要有实际的作为,这就通过method来指明,它指明了该通知由切面关联的bean的哪个方法来处理。
- 连接点:按照上述4个规定,程序在真正运行时,就会执行"切"这个动作,比如执行save方法时,会先调用markLogInfoBefore方法,当AOP调起该方法时,就形成了一个连接点,即切面通过"切"的动作把真正的业务切开时的切口,这个连接点通常会携带被代理类的一些信息,如当前正在执行哪个方法,参数是什么等。一般是作markLogInfoBefore的入参。
3. 实战
在上述aop配置下,完成访问的拦截,并将拦截的日志持久化。
publicclass CustomLogManager {
private DtsLogService dtsLogService;
private void storeLogToDB(String type, String
executeClassInfo,String logClassInfo, String logMessage,
StringlogMethodName)
{
LogEntityBean logBean = new LogEntityBean();
//set
getDtsLogService().saveOrUpdateLog(logBean);
}
private void storeLogToDB(String type,JoinPoint point, String logMessage)
{
//从连接点获取被代理方法的信息
Object target = point.getTarget();
Class clazz = target.getClass();
String targetClassName = clazz.getName();
String methodName =point.getSignature().getName();
//日志持久化
storeLogToDB(type, this.getClass().getName(),targetClassName, logMessage, methodName);
}
/**
* 前置通知,用于拦截所有的DTS的get*方法
* */
public void markLogInfoBefore(JoinPoint point)
{
System.out.println("markLogInfoBeforeexecute!!!!");
String logMessage = "被日志切面拦截---前置通知";
storeLogToDB("info",point, logMessage);
以下方法,是环绕通知的处理方法:
/**
* 环绕通知,可以实现通知前和通知后的参数共享,避免使用切面的成员
变量带来的不安全
* */
public void markLogByAround(ProceedingJoinPointproceedJointPoint)
{
String logMessage = "被日志切面拦截---环绕通知---执行前";
storeLogToDB("info",proceedJointPoint, logMessage);
try {
IssueDts resultDts = (IssueDts) proceedJointPoint.proceed();
logMessage = "被日志切面拦截---环绕通知---执行后:"+resultDts.getId()+""+resultDts.getNumber();
storeLogToDB("info",proceedJointPoint, logMessage);
} catch (Throwable e) {
// TODO Auto-generated catch block
e.printStackTrace();
logMessage = "被日志切面拦截---环绕通知---执行后异常";
storeLogToDB("error",proceedJointPoint, logMessage);
}
}
解读:
1、 黄色部分,区分于连接点JointPoint,此处是ProceedingJoinPoint
2、 绿色部分,通过proceed(),以代理的方式完成对被拦截方法的调用,并允许接收其返回值,返回值类型为Object
3、 粉色部分,分别是完成前置和后置的处理,环绕在真实功能执行(proceed())的前后
4. 注意点
1) 通过Restlet接收页面端请求的Resource,好像无法做AOP
2) 切面表达式的语法需要仔细了解其匹配范围,切勿出现循环拦截的情况
3) 一个切面可以有多个切点,可以指向同一个处理类
4) 除了通过aop: aspect的形式配置切面,还可以通过aop:advisor的形式配置,具体区别参见事务章节
5) 一个方法,可以同时配置多个切面或者advisor进行拦截,它会依次执行拦截的处理函数,顺序由spring决定。
6) 任一切面,对异常的处理,建议都使用aop:after-throwing来捕获,而不是自己写try,因为自己写try捕获会吞没异常,导致被代理的方法的调用者无法捕获到异常,从而无法判断方法执行的结果,如果一定要自己捕获,建议在catch中重新抛出该异常
四、Spring支持数据库
Spring对数据库的支持,主要体现在2个方面,第一个方面是对JDBC异常的细化,第二个方面是对各个ORM框架的支持:Spring将JDBC的DataAccessException细化成将近20个异常,形成自己的异常体系,方便用户判断出错的原因,本文用到的是DataIntegrityViolationException异常。
五、事务
1. 什么叫事务
全有或者全无的操作,被称为事务,一旦其中一个步骤失败,全部回滚。事务通常用在数据库操作中
2. 事务的4个特性:ACID
- 原子性(Atomic):整个事务的操作被看成是一个原子操作,需要确保整个事务的所有操作全部发生或者全部不发生
- 一致性(Consistent):一旦事务完成,系统都要确保现实数据和业务一致。
- 隔离性(Isolated):多个用户同时操作数据库,各自的事务是互相隔离的,不会干扰和纠缠,通过锁定数据库表或行来实现
- 持久性(Durable):一旦事务完成,其结果一定被持久化
3. 事务的不同实现平台
Hibernate、JDBC、Java持久化API(JPA)等
4. Spring提供事务管理
Spring并不提供事务的能力,而是负责事务的管理。事务的能力是由其对应的实现平台来提供的。比如hibernate会提供事务的能力,而spring则通过HibernateTransactionManager来管理hibernate提供的事务,使用户能方便的编码使用或者配置何时、何地、以何种特定的配置使用事务。
Spring通过TransactionTemplate来适配所有类型的事务管理器,该类的execute方法具备事务管理的能力,用户指明被执行的回调函数,回调函数通常实现将哪些数据调用save方法。而execute方法的事务管理能力就体现在"允许用户简单的指明业务持久化相关的回调函数,而不需要知道出错时该调用哪个方法进行回滚,回滚的操作有事务管理器来完成"。
5. 编码型事务
5.4中提到的就是事务管理的编码使用方式。对于需要精确控制事务边界的时候,可以使用编码式事务,否则,建议使用申明式事务。通常情况下,不会需要如此精确的事务边界,因为有事务总比没事务好,无非只是性能上有点小影响,比如后面会提到的隔离级别。
6. 申明式事务的介绍
申明式事务,即通过配置来指明哪些方法需要事务,并且使用何种程度的事务。申明式事务的实现原理是Spring AOP,你可以将spring事务管理想象成"将方法包装上事务边界后形成的切面",也就是说事务其实是通过切面来实现的,只不过普通的切面可以指明通知处理方法,而事务的切面则是把通知的具体处理方法交给了事务管理模块来指定,所以,申明式事务其实是spring AOP的一个优秀实践。
这里将的比较粗糙,什么叫"何种程度的事务"?准确的将就是事务的传播行为是什么,隔离级别是什么,是否只读,超时时间是多少,回滚规则怎么样等等,详见下面5个小点
1) 5.6.1 传播行为
传播行为是指某个事务方法的调用者和被调用方法本身之间的事务边界,通俗的讲就是调用者和被调用者各自事务该如何配合使用,是嵌套使用,还是使用其中一个,还是额外启动一个事务等。这些行为类似于EJB中的CMT支持的规则。
传播行为共有7个,分别是PROPAGATION_MANDATORY、PROPAGATION_NESTED、PROPAGATION_NEVER、PROPAGATION_NOT_SUPPORTED、
PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_SUPPORTS。 例如,如果一个方法声明了PROPAGATION_REQUIRES_NEW行为,这意味着:该方法在被调用开始执行时,会启动一个新事务并在自身方法返回时结束该事务,与父事务没有任何关系。7个规则的具体描述,请自行查阅。
2) 5.6.2 隔离级别
隔离级别定义了一个事务可能受其他并发事务影响的程度。有如下5种级别:ISOLATION_DEFAULT、ISOLATION_READ_UNCOMMITED、ISOLATION_READ_COMMITED、ISOLATION_REPEATABLE_READ、ISOLATION_SERIALIZABLE,具体含义,请自行查阅。
在理想情况下,事务直接应该是完全隔离的,但考虑到并发性能的问题,增加了上述5种隔离级别,以供用户自行选择,牺牲部分准确性换来性能提升。
3) 5.6.3 事务超时
当事务运行时间特别长的情况下,由于会有可能锁定数据库的表或者记录,所以会造成性能降低,所以允许用户为特定的事务指定超时时间,在超时时自动回滚并结束事务。
4) 5.6.4 是否只读
如果实际操作都是制度,可以选择配置"是否只读"为是,数据库可利用该配置,进行特殊的优化
5) 5.6.5 回滚规则
默认情况下,事务遇到运行期异常时,会回滚,而遇到检查型异常时,不会回滚。但允许用户认为指定需要回滚的异常,这就是回滚规则
7. 申明式事务的使用样例
申明式事务是基于AOP的优秀实践,事务的捕获其实是通过切面完成的,所以,通俗的讲,申明式事务的配置是"一个切面+事务的配置"。
首先,定义一个切面,指定哪种类型的方法使用哪个事务通知
<aop:config>
<aop:advisoradvice-ref="batchSaveAdvice" pointcut="execution(* com.dts.service.IssueService.batch*(..))"/>
</aop:config>
这里的pointcut是通过表达式来指定某一系列符合要求的方法,batchSaveAdvice是指这些方法对应的事务通知,事务通知负责配置事务的一些属性,如传播行为、隔离级别等,配置方式如下:
<tx:adviceid="batchSaveAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="batchSave*"isolation="DEFAULT" propagation="REQUIRED"timeout="-1" read-only="false" />
<tx:methodname="batchGet*" isolation="DEFAULT" propagation="SUPPORTS"read-only="true" />
</tx:attributes>
</tx:advice>
一个事务通知需要指定事务管理对应的bean,不同的ORM平台有对应的事务管理模块,此处使用hibernate作为ORM,代码如下
<beanid="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<propertyname="sessionFactory" ref="sessionFactory" />
</bean>
一个事务通知,还包括一些属性配置,如指明batchSave*的隔离级别为DEFAULT等,这里需要用户根据自己的需要进行配置,同时也是体现经验的地方。