Spring有两个核心概念,一个是IOC/DI,另一个是AOP
AOP:面向切面编程(Aspect Oriented Programing)
OOP:面向对象编程(Object Oriented Programing)
AOP功能:在不惊动原始设计的基础上增强代码,符合Spring的无入侵式/无侵入式思想
的案例中BookServiceImpl中有save , update , delete和select方法,这些方法我们给起了一 个名字叫连接点
四个方法中,update和delete只有打印没有计算万次执行消耗时间, 但是在运行的时候已经有该功能,那也就是说update和delete方法都已经被增强,所以对于需要增 强的方法我们给起了一个名字叫切入点
通知是要增强的内容,会有多个,切入点是需要被增强的方法,也会有多个,那哪个切入点需要添 加哪个通知,就需要提前将它们之间的关系描述清楚,那么对于通知和切入点之间的关系描述,我们 给起了个名字叫切面
通知是一个方法,方法不能独立存在需要被写在一个类中,这个类我们也给起了个名字叫通知类
总结
在SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法
一个具体的方法:如com.itheima.dao包下的BookDao接口中的无形参无返回值的save方 法 匹配多个方法:
所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意 方法,所有带有一个参数的方法
AOP实例
使用注解开发
1.导入坐标
在pom.xml文件中导入相关坐标
2.制作连接点(原始操作,dao接口,实现类)
添加bookdao和bookdaoimpl类
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println(System.currentTimeMillis());
System.out.println("book dao save ...");
}
public void update(){
System.out.println("book dao update ...");
}
}
创建Spring配置类
编写APP运行类
3.制作共性功能(通知类和通知)
通知就是将共性功能抽取出来后形成的方法,共性功能指的就是当前系统时间的打印。
@Component //通知类必须配置成Spring管理的bean
@Aspect //设置当前类为切面类类
public class MyAdvice {
//设置切入点,要求配置在方法上方
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
//设置在切入点pt()的前面运行当前操作(前置通知)
// @Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
4.定义切入点
通过@Pointcut定义切入点,且依赖一个不具有实际意义的方法(pt())进行,配置在pt()方法上方,
5.绑定切入点与通知关系(制作切面)
通过@Before绑定切入点和通知的关系,设置在切入点的前面运行操作(前置)
6.开启注解格式AOP功能
@EnableAspJAutoproxy
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}
AOP工作流程
1.容器启动
2.读取所有切面配置中的切点
3.初始化bean
此时要被实例化的bean对象类中的方法和切入点进行匹配
匹配成功就,创建原始对象的代理对象,匹配成功说明需要对其进行增强
匹配失败,创建原始对象
对哪个类做增强,这个类对应的对象就叫做目标对象
因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强
4.获取bean执行方法
获取的bean是原始对象时,调用方法并执行,完成操作
获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
结论:容器中存入的是目标对象的代理对象
AOP的两个核心概念
目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终 工作的
代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实 现
上面这两个概念比较抽象,简单来说, 目标对象就是要增强的类[如:BookServiceImpl类]对应的对象,也叫原始对象,不能说它不能运 行,只能说它在运行的过程中对于要增强的内容是缺失的。 SpringAOP是在不改变原有设计(代码)的前提下对其进行增强的,它的底层采用的是代理模式实现 的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知 [如:MyAdvice中的method方法]内容加进去,就实现了增强,这就是我们所说的代理(Proxy)。
AOP切入点表达式
1.语法格式
切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常 名)
//通过以下的例子来理解格式
execution(public User com.itheima.service.UserService.findById(int))
2.通配符
execution(void com.itheima.dao.BookDao.update())
//匹配接口,能匹配到
execution(void com.itheima.dao.impl.BookDaoImpl.update())
//匹配实现类,能匹配到
execution(* com.itheima.dao.impl.BookDaoImpl.update())
//返回值任意,能匹配到
execution(* com.itheima.dao.impl.BookDaoImpl.update(*))
//返回值任意,但是update方法必须要有一个参数,无法匹配,要想匹配需要在update接口和实现类添加参数
execution(void com.*.*.*.*.update())
//返回值为void,com包下的任意包三层包下的任意类的update方法,匹配到的是实现类,能匹配
execution(void com.*.*.*.update())
//返回值为void,com包下的任意两层包下的任意类的update方法,匹配到的是接口,能匹配
execution(void *..update())
//返回值为void,方法名是update的任意包下的任意类,能匹配
execution(* *..*(..))
//匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广
execution(* *..u*(..))
//匹配项目中任意包任意类下只要以u开头的方法,update方法能满足,能匹配
execution(* *..*e(..))
//匹配项目中任意包任意类下只要以e结尾的方法,update和save方法能满足,能匹配
execution(void com..*())
//回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法
execution(* com.itheima.*.*Service.find*(..))
//将项目中所有业务层方法的以find开头的方法匹配
execution(* com.itheima.*.*Service.save*(..))
//将项目中所有业务层方法的以save开头的方法匹配
3.书写技巧
AOP通知类型
前置通知
后置通知
环绕通知(重点)
返回后通知(了解)
抛出异常后通知(了解)
前置通知:@Before
@Pointcut定义任意一个切入点,在update()方法上,此时update()方法就是切入点,切点依赖pt()方法
@Before表示前置通知,@Before("pt()")表示切入点和通知已经绑定,通知在切入点之前运行通知
后置通知@After
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@After("pt()") //表示在切入点之后运行该通知
public void after() {
System.out.println("after advice ...");
}
环绕通知@Around()
使用环绕通知,需要对原始操作进行增强,因为环绕通知需要在原始方法的前后进行增强
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Around("pt()")
public void around(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("around before advice ...");
//表示对原始操作的调用
pjp.proceed();
System.out.println("around after advice ...");
}
原始方法中存在返回值的处理时,例如
public int select(){
System.out.println("book dao select is running");
return 100;
}
@Pointcut("execution(int com.itheima.dao.BookDao.select())")
private void pt2(){}
@Around("pt2()")
public void around(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("around before advice ...");
//表示对原始操作的调用
pjp.proceed();
System.out.println("around after advice ...");
}
int num = bookDao.select();
System.out.println(num);
运行出现错误,空的返回不匹配原始方法的int返回
void就是返回Null 原始方法就是BookDao下的select方法
所以如果我们使用环绕通知的话,要根据原始方法的返回值来设置环绕通知的返回值,具体解决方案 为:
@Around("pt2()")
public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
//表示对原始操作的调用
Object ret = pjp.proceed();
System.out.println("around after advice ...");
return ret;
}
object类型比int类型更通用
返回后通知@AfterReturning
异常后通知@AfterThrowing
1.环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法 调用前后同时添加通知
2. 通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
3. 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为 Object类型
4. 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成 Object
5. 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常