AOP的概念与spring中的AOP
环境:
Idea:2019.3.1
系统:windows10 家庭版
Jdk: 8
spring:5.0.3 release
spring文档
项目代码
一、AOP的概念
1.什么是AOP
AOP:全称是Aspect Oriented Programming,即面向切面编程
简单地说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理技术,在不需要修改源码的基础上,对我们的已有方法进行增强
2.AOP的作用及优势
作用:
在程序运行期间,不修复源码的已有方法进行增强
优势:
减少重复代码
提高开发效率
维护方便
3.AOP的实现方式
动态代理技术实现
二、spring中的AOP
1.AOP相关术语
Joinpoint(连接点):
所谓连接点是指那些被拦截到的点,在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。即业务层中被动态代理的类中所有的方法
Pointout(切入点):
所谓切入点是指我们要对哪些Joinpoint进行拦截的定义,即业务层中被动态代理的类中被增强了的方法
Advice(通知/增强):
所谓通知是指拦截到Joinpoint之后所要做的事情就是通知
通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知
Introduction(引介):
所谓引介是指一种特殊的通知,在不修改类代码的前提下,Introduction可以在运行期为类动态地添加一些方法成Field
Target(目标对象):
代理的目标对象
Weaving(织入):
是指把增强应用到目标对象来创建新的代理对象的过程
spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入
Proxy(代理):
一个类被AOP织入增强后,就产生了一个结果代理类,Target是被代理对象,而Proxy则是代理对象
Aspect(切面):
是切入点和通知(引介)的结合,很抽象,就是在织入的过程中,哪个service,哪些增强方法,这些方法何时执行,这就叫做切面
2.学习spring中的AOP要明确的事
a、开发阶段(我们做的)
编写核心业务代码(开发主线):大部分程序员来做,要求熟悉业务需求。
把公用代码抽取出来,制作成通知(开发阶段最后再做):AOP编程人员来做
在配置文件中,声明切入点与通知间的关系,即切面:AOP编程人员来做
b、运行阶段(spring框架完成的)
spring框架监控切入点方法的执行,一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行
3.关于代理的选择
在spring中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式
三、spring中基于xml的AOP
1.导入坐标
除了之前的,我们再导入一个aspectj的jar包
idea中,再pom.xml文件中,直接alt+insert,然后选择Dependency,在出来的框里面搜索需要的包就能找到,还能选择版本
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
2.新建项目springAOP
最终项目结构如下
业务层的AccountServiceImpl中的方法为我们所要增强的方法,即它是我们要用的切入点
AccountServiceImpl类
import com.cbw.service.IAccountService;
/**
* 账户的业务层实现类
*/
public class AccountServiceImpl implements IAccountService {
/**
* 保存账户方法
*/
public void saveAccount() {
System.out.println("执行了保存");
}
/**
* 更新账户方法
* @param i
*/
public void updateAccount(int i) {
System.out.println("执行了更新" + i);
}
/**
* 删除账户方法
* @return
*/
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
这3个方法都有各自的特点
saveAccount()方法: 无返回值,无参数
updateAccount()方法: 无返回值,有参数
deleteAccount()方法: 有返回值,无参数
这样设置是为了在后面知识学习中有个对比
Logger类
它是用来提供切面中作为通知的方法的类,这里我们用打印日志作为前置通知,来增强service中的方法
/**
* 用于记录日志的工具类,它里面提供了公共的代码
*/
public class Logger {
/**
* 用于打印日志,计划让其在切入点方法执行之前执行(切入点方法就是业务层方法)
*/
public void printLog(){
System.out.println("Logger类中的printLog方法开始执行");
}
}
AopTest测试类
import com.cbw.service.IAccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* 测试aop的配置是否成功
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class AopTest {
@Autowired
IAccountService accountService;
/**
* 测试存储账户方法
*/
@Test
public void testSaveAccount(){
accountService.saveAccount();
}
/**
* 测试更新账户方法
*/
@Test
public void testUpdateAccount(){
accountService.updateAccount(1);
}
/**
* 测试删除账户方法
*/
@Test
public void testdeleteAccount(){
accountService.deleteAccount();
}
}
3.applicationContext.xml中配置IOC和AOP
1.首先导入相关依赖
这个可以在文章开头的官方文档里,进Core technologies即core核心组件的文档中搜索xmlns:aop就能快速定位到
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
</bean>
2.配置spring的ioc并且将bean对象加入容器
<!--把service对象配置进去-->
<bean id="accountService" class="com.cbw.service.impl.AccountServiceImpl"></bean>
<!--配置Logger类,因为Logger类要在切入点方法之前执行,也就可以当作是前置通知-->
<bean id="Logger" class="com.cbw.utils.Logger"></bean>
3.配置springAOP
·使用<aop:config></aop:config>标签表明开始AOP的配置
·使用<aop:aspect></aop:aspect>标签表明开始配置切面,在<aop:config></aop:config>标签内部使用
属性:
id:是给切面提供一个唯一标识
ref属性:是指定通知类bean的id,即作为通知的方法所在的类的bean的id
·在<aop:aspect></aop:aspect>标签内部使用不同的通知类型的标签来配置通知,这里我们先学前置通知,所用标签<aop:before></aop:before>
属性:
method:用于指定Logger类(即通知类)中哪个方法是前置通知
pointcut:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
切入点表达式的写法:
关键字:execution(表达式)
表达式:访问控制符 返回值 包名.包名.包名...类名.方法名(参数列表)
标准的实例:
增强com.cbw.service.impl包下的AccountServiceImpl类中的pulic void saveAccount()方法
表达式:
· public void com.cbw.service.impl.AccountServiceImpl.saveAccount()
注意:切入点表达式可以用通配符表示,最简洁的即全通配符
* *..*.*(..)
由标准表达式到全通配符变换过程如下
1.标准
public void com.cbw.service.impl.AccountServiceImpl.saveAccount()
2.访问修饰符可以省略
void com.cbw.service.impl.AccountServiceImpl.saveAccount()
3.返回值可以使用通配符*,表示任意返回值
* com.cbw.service.impl.AccountServiceImpl.saveAccount()
4.包名可以使用通配符,表示任意包,但是有几级包,就需要写几个
* *.*.*.*.AccountServiceImpl.saveAccount()
5.包名可以使用..表示当前包及其子包
* *..AccountServiceImpl.saveAccount()
6.类名和方法名都可以使用通配符*来实现统配
* *..*.*()
7.参数列表:(这里就体现了设置3个不同方法的用处)
基本类型直接写名称 如int
* *..*.*(int)
引用类型写包名.类名 如java.lang.String
* *..*.*(java.lang.String)
可以使用通配符*表示任意类型,但是一定得有参数
* *..*.*(*)
可以使用..表示有无参数均可,有参数则表示任意参数
* *..*.*(..) <------------------------------------这个即全通配符写法
8.注意,我们实际开发中不能这么写,因为这么写,所有的方法都会被增强,因为它全是*,所有方法都满足条件
写法规则:切到业务层实现类下的所有方法
即:* com.cbw.service.impl.*.*(..)
配置好的是这样的
<aop:config>
<aop:aspect id="logAdvice" ref="Logger">
<aop:before method="printLog" pointcut="execution(* com.cbw.service.impl.AccountServiceImpl.*.*(..))"></aop:before>
</aop:aspect>
</aop:config>
还有一个小知识点,切入点表达式的解析由aspectjweaver负责,下面这个东西
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
4.完整的文件内容
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
//配置所需bean,并加入IOC核心容器
<bean id="accountService" class="com.cbw.service.impl.AccountServiceImpl"></bean>
<bean id="Logger" class="com.cbw.utils.Logger"></bean>
//配置springAOP
<aop:config>
<aop:aspect id="logAdvice" ref="Logger">
<aop:before method="printLog" pointcut="execution(* com.cbw.service.impl.*.*(..))"></aop:before>
</aop:aspect>
</aop:config>
</beans>
5.测试类中的测试结果
testSaveAccount()方法
testUpdateAccount()方法
testDeleteAccount()方法
总结:方法增强成功了,也就是springAOP配置成功了
6.问题
从上面一节应该可以发现一个问题,它的前置通知为什么输出了两次?我最初想的是可能是它的构造方法也给增强了
我就在AccountServiceImpl类中添加了无参构造方法,并加入一句话
然后我去执行testSaveAccount()方法
这里打印了构造方法中的语句,说明执行了构造方法,但是也不能说明构造方法被增强了,因为前置通知在构造方法的后面,按理说应该是不对的,然后我就想,会不会和bean的作用范围有关,毕竟bean的scope属性默认是sington,而sington类型的bean是在容器创建的时候就一起初始化了,而prototype类型的bean是在调用的时候才初始化的,抱着这个心态,我把accountService的scope设置成了prototype
再次执行testSaveAccount()方法
他顺序竟然对了,所以我暂时搞不懂,等查阅了相关资料再把这个问题弄明白