认识切面
Spring AOP是Spring最核心的功能之一,AOP中最关键的词也就是Aspect——切面。我刚开始接触这个词的时候Java编程水平还不高(当然,现在也不高0。0),只觉面向对象的意义还没有弄清楚,又出来一个面向切面,直呼难哉。
如今再次学习到此处,发现所谓切面只是场景,本质还是对象之间的操作,只不过是用动态代理的方式,对原有方法的特定位置(如方法执行前,执行后,异常后等)进行增加一些操作,而增加的这些操作大多是重复的,所以为了减少冗余的代码,我们可以利用Spring的AOP把业务功能的一些重复代码进行抽取,放入切面,再对原有业务的执行进行一种监控。
我们在编写单元测试的时候,如果是DAO的一些操作,一般都是拿到数据源,进行操作,提交事务,释放资源。
//伪代码
public class UnitTest{
@Test
public void testAdd(){
//获取连接、数据源
try{
//数据库操作
//...
//事务提交
}catch{
//回滚事务
}finally{
//释放资源
}
}
@Test
public void testAdd(){
//重复的代码又来一遍
}
@Test
public void testUpdate(){
//重复的代码又来一遍
}
}
真正有意义的代码有可能只是一次很简单的增、删、改操作,但还是不得不按照这样的流程编码,有经验的码农就会这样写:
//伪代码
public class UnitTest{
//把数据源提取到成员变量
@Before
public void SetUp(){
//获取连接、数据源
}
@Test
public void testAdd(){
try{
//数据库操作
//...
//事务提交
}catch{
//回滚事务
}
}
@Test
public void testAdd(){
//重复的代码又来一遍
}
@Test
public void testUpdate(){
//重复的代码又来一遍
}
@After
public void close(){
//释放资源。
}
}
我们发现,这样编写单元测试,我们的每一个测试单元都可以省只做一次头(获取连接)和尾(关闭资源)的操作。但是每次还是需要写事务提交,事务回滚,那咋办呢?这个时候,AOP应运而生了…
切面怎么用?
知道了场景,我们来说一说切面怎么用,也就是它在代码中的体现是什么样的。
还是先从伪代码说起吧,首先声明一个切面,其实就是一个类:
public class 切面{
//定义前置操作,我被管理的方法执行之前要先干什么?,其实就是在这再定义一个方法,一般叫before
//方法正常运行结束后,执行什么操作?再定义一个方法,一般叫afterReturning
//方法出现了指定异常,执行什么操作?还是先定义一个方法,一般叫afterThrowing
//方法无论是异常还是正常,我都要关闭资源啊,再来个方法,叫after
//之前的方法内容过时了,我想改动一下,但没办法修改之前的方法了,怎么办?我在这里再写一个方法,把之前的方法包裹起来,控制原有方法的执行,在原有方法的基础上进行操作就行了,这个方法比较特殊,他有参数,也有原方法执行的代码,我们暂且叫他around。
}
现在切面有了,比如我的切面方法这样定义:
- before:获取连接
- afterReturing: 提交事务
- afterThrowing:回滚事务
- after:关闭连接
- around:你先去凉快会儿。一会再说你。
这个时候要写dao的类了,其实一般我们都会用切面去切service层,我一开始举例用了dao,就暂且继续用dao吧。
public class 被切的类{
//数据库增操作 addXXX方法
//数据库删操作 deleteXXX方法
//数据库改操作 updateXXX方法
}
现在问题来了,怎么把这两个关联起来?怎么让切面切进去?
一般有两种方式,XML和注解都可以。这里用伪xml举例。
我是前边一堆的约束文件...
把定义的切面类也放到容器中,这时他有了一个可爱的id。
告诉spring这里有个切面,他的id是XXX。
告诉spring这个切面的before方法,作用到哪些类的哪些方法上。spring就会自动在那些方法执行前,执行before了。
同理,指定afterReturning的作用载体
...
这样一个切面就配置好了。
PS:在指定方法载体的时候,可以是项目中的任意方法,一般是整个service层,就好像service层被这个切面监视了,无论你要干嘛,都要经过切面,看上去这个切面的类把整个service层的类横切了一刀,被切的类都会受到这个切面的影响。当然,要监视哪些方法,哪些方法不需要增强,也是可以通过切面过滤的。
这样,一个切面就写完了,当你再次执行dao的测试的时候,你只需要写业务代码就行了。当然,这里又涉及到切面中的事务所用的数据源要与dao用的数据源是同一个,这个问题的解决可以用threadlocal,也可以进行传参,不过这个问题不在本篇文章讨论范围,大家知道有这回事需要注意就行了。
然后接下来开始用真代码写一个模拟的例子。为了简便,就是模拟真实操作改成控制台输出,代表方法执行了即可。
终于开始切面编程
简单搭建一下环境:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!--spring myaop-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
建一个切面类,就叫 MyAspect.java
package pers.buyusan.myaop;
/**
* @author buyusan
* @Description
* @date 2019-07-16 22:14
**/
public class MyAspect {
//模拟拿到连接
public void beforeDao(){
System.out.println("资源的获取,拿到连接...");
}
//模拟事务提交
public void commitDao(){
System.out.println("提交事务...");
}
//事务回滚
public void rollbackDao(){
System.out.println("事务回滚了..");
}
//关闭资源
public void closeDao(){
System.out.println("关闭资源...");
}
}
创建一个被监视的类:MyService.java
package pers.buyusan.service;
/**
* @author buyusan
* @Description
* @date 2019-07-16 22:18
**/
public class MyService{
public void addData(){
System.out.println("添加数据...");
}
public void updateData(){
System.out.println("更新数据...");
}
public void deleteData(){
System.out.println("删除数据...");
}
}
配置Spring文件:ApplicationContext.xml
<bean id="myAspect" class="pers.buyusan.myaop.MyAspect"/>
<bean id="myService" class="pers.buyusan.service.MyService"/>
<aop:config>
<aop:aspect ref="myAspect">
<aop:before method="beforeDao"
pointcut="execution(* pers.buyusan.service.MyService.*(..))"/>
<aop:after-returning method="commitDao"
pointcut="execution(* pers.buyusan.service.MyService.*(..))"/>
<aop:after-throwing method="rollbackDao"
pointcut="execution(* pers.buyusan.service.MyService.*(..))"/>
<aop:after method="closeDao"
pointcut="execution(void *.buyusan.service.MyService.*(..))"/>
</aop:aspect>
</aop:config>
来个测试类:
public class TestAop {
@Test
public void testAdd(){
//加载容器
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
MyService myService = ac.getBean("myService", MyService.class);
//随便测一个方法
myService.updateData();
}
}
结果:
如果异常了呢?手动添加一个异常:
结果:
提高一下逼格
程序已经写完了,我们可以看到切面编程并不难理解,但以上是用我自己的理解的语言去描述的,官方的定义直接拿过来是很难接受和理解的,现在我们在理解的情况下,去和官方定义的术语进行匹配。
- Aspect:一般翻译叫做切面,这个没什么争议,我们的MyAspect类就是一个切面,如果想让Spring容器认识这个切面,需要在容器中进行配置。
- join point:正常翻译叫做连接点,就是需要被监视的点就叫连接点。连接点可以包括多个方法。
- point cut:也叫切入点,或者切点,就是具体到被拦截的方法,我们例子中的XXXData方法就是切点,意为spring通过这个方法切进来进行干涉,spring中切点就是连接点。因为切点指方法,连接点更加广义,但是spring中只支持方法作为连接点。
- Advice:通知,也叫增强,具体指的就是拦截到方法后做的具体的操作,通知共五种:
- before:作用在被增强方法之前。一般翻译成:前置通知。
- afterReturning:作用在被增强方法正常结束之后。有人翻译成返回通知,有的人翻译成后置通知。
- afterThrowing:作用在被增强方法出现异常后。有人翻译成异常通知、或者方法异常通知。
- after:作用在方法结束后,不管是否正常结束。有人翻译成后置通知,有人翻译成最终通知。
- around:作用在方法之上,对被增强的方法再次进行包裹,替代被增强的方法。环绕通知。
- Weaving:织入,代表被增强方法被监视到后进入切面的流程中的过程。
- Introduction:叫引介或者引入,是一种特殊的通知,可以动态增加方法或者field(其实我也不太懂)
- Proxy:代理,切点织入切面后,底层生成的是一个代理方法,其实真正执行的就是这个代理。
总结和注意
- 对于通知和一些术语的翻译,各个版本参差不齐,最直接的方式是直接用英语原文,以免产生歧义。
- 环绕通知没有进行演示,有兴趣可自行加入代码查看效果。
- 切面的存在是需要Spring支持的,如果连接点不在容器中,是无法进入切面的。
- 连接点所在的类在底层会被动态代理生成新的代理对象,Spring根据有无接口来决定使用jdk代理还是cglib代理。
- xml方式,after通知会在最后执行,而注解方式下,after会先执行,然后再执行afterReturning或者afterThrowing,所以我认为把after叫做最终通知不合理,因为它没有在最后执行。叫做后置通知与before对应更加亲民一些。
- before可以对方法的参数进行先一步的处理。
- afterReturning可以处理方法的返回值,所以我认为它被称作返回通知更合理一些。