Spring与AOP:
1.AOP底层实现的原理:利用JDK的动态代理来实现。
例子:见PreAOP项目
2.AOP介绍
AOP(Aspect Oriented Programming),面向切面编程,通过预编译方式和运行其动态代理实现程序功能同意维护的一种技术,AOP是OOP的补充,也是Spring框架的重要内容,是函数式编程的一种衍生类型,利用AOP可以对主业务逻辑的各个部分进行隔离,从而使得主业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发效率。
所谓面向切面编程,就是将交叉业务逻辑封装成切面,利用AOP容器的功能将切面织入到主业务逻辑代码中,所谓交叉业务逻辑,是指与主业务逻辑无关的,通用的代码,如安全检查,事务,日志等等。
若不使用AOP编程,交叉业务逻辑就会与主业务逻辑混合在一起,出现代码纠缠,会使主业务逻辑变得混杂不清。
3.AOP编程的术语
(1)切面(Aspect):切面泛指交叉业务逻辑,上面例子中的事务处理,日志处理都可以理解为切面。常用的切面有通知和顾问,切面实际上就是对主业务逻辑的一种增强。
(2)织入(Weaving):织入是指将切面代码插入到目标代码的过程。比如上面例子中的InvovationHandler的invoke()方法所完成的工作,就可以成为织入。
(3)连接点(JoinPoint):连接点是指可以被切面植入的方法,通常主业务逻辑接口中的方法均为连接点。如上面例子的IService接口中的doSome()和doOhter()方法均为连接点。
(4)切入点(Pointcut):切入点指切面具体织入的方法。比如上面例子中SomeServiceImpl类中,如果doSome()方法被增强,而doOther()方法不被增强 ,那么doSome()成为切入点,而doOther()仅仅成为连接点。注意被声明为final的方法是不能作为连接点或切入点的。因为final方法不能被修改。
(5)目标对象(target):目标对象指将要被增强的对象,即包含主业务逻辑的类的对象。如上例中的SomeServiceImpl类的对象即为目标对象。
(6)通知(Advice):通知是切面的一种实现,可以完成简单织入功能(织入功能就是在这里实现的)。通知定义了增强代码切入到目标代码的时间点,是在目标代码执行前执行,还是之后执行等等,通知类型不同,切入时间点不同。切入点定义了切入的位置,通知定义了切入的时间点。
(7)顾问(Advisor):顾问是切面的另一种实现,能够将通知以更复杂的方式织入到目标对象中,是将通知包装为更复杂切面的装配器。
一、Spring与AOP
1.Spring的AOP实现
2.AspectJ对AOP的实现
对于AOP的编程思想,很多框架都进行了实现,Spring就是其中之一,可以完成面向切面编程。然而,AspectJ也实现了AOP的功能,而且其实现方式更为简洁,使用更方便,而且还支持注解式开发,所以Spring又将AspectJ对AOP的实现引入到自己的框架中。在实际使用Spring的AOP编程时,一般都是采用AspectJ的实现方式。
AspectJ是一个面向切面编程的框架,它扩展了java语言,AspectJ定义了AOP的语法,它有一个专门的编译器来生成遵守java字节码编码规范的class文件。
(1)AspectJ的通知类型
I.前置通知:在主业务逻辑方法执行之前执行的通知
II.后置通知:在主业务逻辑方法执行之后执行的通知
III.环绕通知:可以在主业务逻辑方法执行之前和之后执行的通知
IV.异常通知:在主业务逻辑方法发生异常的时候执行的通知
V.最终通知:无论是否发生异常都会执行的通知
(2)AspectJ的切入点表达式:专门的表达式用于指定切入点。
语法:
execution(
[访问权限]
返回值类型
全类名
方法名(参数名)
[抛出异常类型]
)
(注意:表达式中[]的部分可以省略,各个部分之间用空格分开,可以使用如下符号:
符号 | 说明 |
* | 匹配0到多个任意字符 |
.. | 用在方法参数中,表示任意多个参数;用在包后面,表示当前包及其子包 |
+ | 用在类名后,表示当前类及其子类;用在接口后,表示当前接口及其实现类 |
举例:
表达式 | 说明 |
execution(public * *(..)) | 匹配任意的公共方法 |
execution(* com.edu.service.*.*(..)) | 匹配在service包下面的任意类的任意方法 |
execution(* com.edu.service..*.*(..)) | 匹配在service包及其子包下面的任意类的任意方法,注意:当".."出现在包中时,后面的类名必须为*,表示当前包及其子包中的所有类 |
execution(* add(Object+)) | 所有的add方法,且方法有一个参数,参数类型可以是Object类型或其子类型 |
(3)AspectJ的AOP编程,开发步骤:
I.导入额外的jar包:
AspectJ自身的jar包:
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.4.RELEASE.jar
Spring整合AspectJ的jar包:
spring-aspects-4.1.0.RELEASE.jar
II.引入aop约束
配置文件中使用的AOP约束中的标签,均是AspectJ框架使用的,而不是Spring框架本身在实现AOP时使用的。
III.AspectJ对于AOP的实现方式有两种:
A.注解方式开发
1). 定义主业务逻辑接口和实现类
2).定义切面(交叉业务逻辑),是一个POJO类(普通java类),其所包含的普通方法,将作为不同的通知方法。
通过使用@Aspect注解可以将这个POJO类注解为切面。
3).在POJO类的普通方法上添加通知注解:
a.@Before 前置通知:在目标方法执行之前执行,被注解为前置通知的方法,可以包含一个JoinPoint类型的参数,通过该参数,可以获得切入点表达式、方法签名、目标对象等等。该注解有一个属性value,用于指定切入点表达式。
@Aspect
public class MyAspect {
//普通方法,作为前置通知方法
@Before("execution(* com.edu.aop.service..*.doSome(..)")
public void before(JoinPoint jp) {
System.out.println("前置增强通知方法...");
System.out.println("前置通知:切入点表达式:"+jp);
System.out.println("前置通知:方法签名:"+jp.getSignature());
System.out.println("前置通知:目标对象:"+jp.getTarget());
}
b.@AfterReturning 后置通知:在目标方法执行之后执行,由于是在目标方法执行之后执行,所以可以获得目标方法的返回值,该注解的returning属性就是用于指定接收方法返回值的变量名,被注解的通知方法,除了可以包含JoinPoint参数,还可以包含一个Ojbect类型的返回值变量,变量名和returning属性指定的变量名同名。
//普通方法,作为后置通知方法
@AfterReturning(value="execution(* com.edu.aop.service..*.doOther(..)",returning = "result")
public void afterReturning(Object result) {
System.out.println("后置增强通知方法,返回值为:"+result);
}
c.@Around 环绕通知 :在目标方法执行之前之后执行,被注解为环绕通知的方法要有返回值,类型是Object类型,并且该通知方法居于一个ProceedingJoinPoint类型的参数,该类型参数有一个方法proceed(),用于执行目标方法。该方法的返回值就是目标方法的返回值。环绕通知实际上就是拦截了目标方法的执行。
@Around("execution(* com.edu.aop.service..*.doThird(..)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕通知前置增强....");
Object res = pjp.proceed(); //相当于执行了主业务逻辑中的目标方法
System.out.println("环绕通知后置增强....");
return res;
}
d.@AfterThorwing 异常通知:在目标方法抛出异常后执行,该注解有一个throwing属性,用于指定所发生的异常类对象,被注解的异常通过只的方法可以包含一个参数类型为Throwable的参数,参数名为throwing属性指定的对象名,表示发生的异常对象。
@AfterThrowing(value = "execution(* com.edu.aop.service..*.doForth(..)",throwing = "ex")
public void afterThrowing(Throwable ex) {
System.out.println("异常通知,异常信息:"+ex.getMessage());
}
e.@After 最终通知:无论目标方法是否发生异常,该通知都会执行
4)注册AspectJ的自动代理
在定义好切面后,需要通知Spring容器,让容器生成"目标类+切面"的代理对象,这个代理对象由容器自动生成。只需要在配置文件中注册一个基于aspectj的自动代理生成器,其就会自动扫描@Aspect注解,并按通知类型与切入点,将其织入,并生成代理。
<!-- 配置aspectj的自动代理 -->
<aop:aspectj-autoproxy/>
5)@Pointcut定义切入点
当较多通知方法使用的切入点表达式相同时,可以利用@Pointcut注解定义一个公用的切入点,然后在其他通知方法注解时使用:
@Pointcut("execution(* com.edu.aop.service..*.*(..)")
private void mypointcut(){
}
@AfterReturning(value="mypointcut()",returning = "result")
public void afterReturning(Object result) {
System.out.println("后置增强通知方法,返回值为:"+result);
}
例子:见AspectJAOP工程
B.XML方式开发
<!-- 注册切面MyAspect -->
<bean id="myAspect" class="com.edu.aop.utils.MyAspect"/>
<!-- 配置AOP -->
<aop:config>
<!-- 定义切入点:expression:指明切入点表达式,id:切入点的id号 -->
<aop:pointcut expression="execution(* com.edu.aop.service..*.doSome(..))" id="doSomePointCut"/>
<aop:pointcut expression="execution(* com.edu.aop.service..*.doOther(..))" id="doOtherPointCut"/>
<aop:pointcut expression="execution(* com.edu.aop.service..*.doThird(..))" id="doThirdPointCut"/>
<aop:pointcut expression="execution(* com.edu.aop.service..*.doForth(..))" id="doForthPointCut"/>
<!-- 定义通知:aop:aspect定义具体的织入规则,ref属性指定将哪个POJO类对象指定为切面 -->
<aop:aspect ref="myAspect">
<!-- 前置通知:method:指明使用切面中的那个方法作为前置通知方法,pointcut-ref指定使用哪一个切入点-->
<aop:before method="before" pointcut-ref="doSomePointCut"/>
<!-- 后置通知:returning:指定一个变量名,然后在切面中的后置通知方法参数给出同名的参数,即可获得目标方法的返回值 -->
<aop:after-returning method="afterReturning" returning="result" pointcut-ref="doOtherPointCut"/>
<!-- 环绕通知: -->
<aop:around method="around" pointcut-ref="doThirdPointCut"/>
<!-- 异常通知:throwing:指定一个对象名,然后可以在切面中的异常通知方法参数指定类型为Throwable,参数名和这个对象名相同即可获得目标方法抛出的异常对象 -->
<aop:after-throwing method="afterThrowing" throwing="ex" pointcut-ref="doForthPointCut"/>
</aop:aspect>
</aop:config>
例子:见AspectJAOPXML工程
二、Spring与DAO
Spring与DAO部分,是Spring两大核心技术IoC和AOP的典型应用。对于Spring的JDBC模板的使用,是应用了IoC,JDBC模板对象需要注入给Dao层的实现类,对于Spring的事务管理,则是AOP的应用,事务会作为切面织入到Service层的主业务逻辑方法中。
1.Spring与JDBC模板
例子:见SpringDao工程
2.Spring的事务管理
事务原本是数据库中概念,在Dao层,但一般情况下,需要将事务提升到业务层,即Service层。这样做是为了能够使用事务的特性来管理具体的业务。在Spring中通常有三种方式来实现对象事务的管理:
(1)使用Spring的事务代理工厂管理事务
(2)使用Spring的事务注解管理事务
(3)使用AspectJ的AOP配置管理事务
(1)Spring事务管理的API
I.事务管理器接口:事务管理器接口是PlatformTransactionManager,主要作用是完成事务提交,回滚,获取事务状态信息等等,有两个常用的实现类:
DataSourceTransactionManager:使用JDBC或Mybatis进行持久化数据时使用。
HibernateTransactionManager:使用Hibernate进行持久化数据时使用。
II.事务定义接口:事务定义接口TransactionDefinition中定义了事务描述相关的三类常量:事务隔离级别、事务传播行为、事务默认超时时间,及对它们的操作。
补充知识:事务并发产生的问题:
- 脏读:当一个事务正在访问数据,并且对数据进行了修改,到修改没有提交到数据库中,这时,另外一个事务也访问了这个数据,然后使用了这个数据,因为这个数据没有提交,那么另外一个事务读到的这个数据是脏数据。
- 不可重复度:事务1查询一条记录,事务2更新事务1查询的记录,事务2提交事务,事务1再次查询上次的记录,却得到不同的结果,称为不可重复读。
- 幻读:事务1对一个表中的数据进行了修改,这种修改涉及到表中的所有数据行,同时事务2也修改了这个表中的数据,这种修改是向表中插入了新数据,那么以后就会发生操作事务1的用户发现表中还有没有修改的数据行,就像发生了幻觉一样。
A.定义了五个事务隔离级别常量:这些常量均是以ISOLATION_开头,形如ISOLATION_XXX:
> DEFAULT:采用数据库默认的事务隔离级别,MySql默认为可重复读,Oracle默认为读已提交。
> READ_UNCOMMITTED:读未提交,未解决任何并发问题。
> READ_COMMITTED:读已提交,解决脏读,存在不可重复读和幻读。
> REPEATABLE_READ:可重复读,解决了脏读,不可重复读,存在幻读。
> SERIALIZABLE:串行化,不存在并发问题。
B.定义了七个事务传播行为常量
所谓事务传播行为是指处于不同事务中的方法在相互调用时,执行期间事务的维护情况。比如,A事务中的方法doSome()调用B事务中的方法doOther(),在调用执行期间事务的维护情况,就成为事务传播行为,事务传播行为是加在方法上的。
事务传播行为常量以PROPAGATION_开头,形式如:PROPAGATION_XXX:
> REQUIRED:若当前存在事务,就加入到当前事务中,若当前不存在事务,则创建一个新的事务,并在其中执行。这是Spring默认的事务传播行为。
> SUPPORTS:指定的方法支持当前事务,若当前没有事务,也可以以非事务方式运行。
> MANDATORY:指定的方法必须在当前事务中运行,若当前没有事务,则抛出异常。
> REQUIRES_NEW:总是新的事务,若当前存在事务,就将事务挂起,直到新事物执行完毕。
> NOT_SUPPORTED:指定的方法不能在事务中运行,若当前存在事务,则将该事务挂起。
> NEVER:指定的方法不能在事务中运行,若存在事务,则抛出异常。
> NESTED:指定的方法必须在事务中执行,如当前存在事务,则嵌套在事务内执行,若当前没有事务,则创建一个新的事务。
C.定义了默认事务超时时限
常量TIMEOUT_DEFAULT定义了事务底层默认超时时限,该值一般采用默认值即可。
例子:模拟购买股票:银行账户Account与股票账户Stock,从Account中扣除相应金额购买股票。
使用AspectJ的AOP配置管理事务:
<!-- 注册属性文件jdbc.properties -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 配置C3P0数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 数据库驱动 -->
<property name="driverClass" value="${jdbc.driver}"/>
<!-- 数据库连接字符串url -->
<property name="jdbcUrl" value="${jdbc.url}"/>
<!-- 用户名 -->
<property name="user" value="${jdbc.user}"/>
<!-- 密码 -->
<property name="password" value="${jdbc.pwd}"/>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--
配置事务通知:
-->
<tx:advice id="myAdvice" transaction-manager="transactionManager">
<!-- 设置通知相关属性,用于指定要将事务以什么方式织入给哪些方法 -->
<tx:attributes>
<!-- 事务通知被织入到哪些主业务逻辑方法中,以及事务相关属性的设置 -->
<!-- name指定所有以open开头的方法都会被织入事务,并且事务传播特性为REQUIRED,即必须在事务中运行 -->
<tx:method name="open*" propagation="REQUIRED"/>
<!-- 所有以find开头的方法被织入事务,传播特性为可以不在事务中运行,并且对数据库的操作为read-only为true,即只读 -->
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
<!--
buyStock方法被织入了事务,并且事务传播特性为必须在事务中运行,当该方法发生StockException异常时,
事务会回滚到上一个成功提交状态。
-->
<tx:method name="buyStock" propagation="REQUIRED" rollback-for="StockException"/>
<!-- 其他方法必须在事务中运行 -->
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!--
配置事务顾问:指定将之前配置好的事务通知,织入给谁
顾问=通知+切入点
-->
<aop:config>
<!-- 切入点 -->
<aop:pointcut expression="execution(* com.edu.spring.service..*.*(..))" id="mypointcut"/>
<!-- 顾问 -->
<aop:advisor advice-ref="myAdvice" pointcut-ref="mypointcut"/>
</aop:config>