场景模拟
- 声明接口Calculator,包含加减乘除的抽象方法
- 创建实现类
- 问题
- 现在有代码缺陷
针对带日志功能的实现类,我们发现有如下缺陷:- 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
- 附加功能分散在各个业务功能方法中,不利于统一维护
- 解决思路
- 解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。
- 困难
- 解决问题的困难:要抽取的代码在方法内部;靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术
- 现在有代码缺陷
代理模式
-
概念
- 介绍
二十三中设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护
- 生活中的代理
- 广告商找大明星拍广告需要经过经纪人
- 合作伙伴找大老板谈合作要约见面时间需要经过秘书
- 房产中介是买卖双方的代理
- 相关术语
- 代理:将费核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法
- 目标:被代理“套用”了非核心逻辑代码的类、对象、方法
- 介绍
-
静态代理
- 创建静态代理类
public class CalculatorStaticProxy implements Calculator{
// 代理类的目标类
private CalculatorImpl target;
public CalculatorStaticProxy(CalculatorImpl target) {
this.target = target;
}
public int add(int i, int j) {
System.out.println("日志:方法:add,参数:"+i+","+j);
int result= target.add(i,j);
System.out.println("日志:方法:add,结果:"+result);
return result;
}
public int sub(int i, int j) {
System.out.println("日志:方法:sub,参数:"+i+","+j);
int result=target.sub(i,j);
System.out.println("日志:方法:sub,结果:"+result);
return result;
}
public int mul(int i, int j) {
System.out.println("日志:方法:mul,参数:"+i+","+j);
int result=target.mul(i,j);
System.out.println("日志:方法:mul,结果:"+result);
return result;
}
public int div(int i, int j) {
System.out.println("日志:方法:div,参数:"+i+","+j);
int result=target.div(i,j);
System.out.println("日志:方法:div,结果:"+result);
return result;
}}
静态代理类是一对一的代理模式,需要在代理类中创建目标对象的变量,然后并为目标对象赋值。然后在代理类中完成对目标方法的增强功能,并调用目标对象的方法,这样看似麻烦,但是可以将目标对象核心方法和附加方法分开来,实现解耦,修改的时候更加清晰。
静态代理的缺点
静态代理确实实现了解耦,但是由于代码都是写死的,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多静态代理类,那就产生了大量重复的的代码,日志功能还是分散的,没有统一管理。提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。
动态代理
- 动态代理有两种:
- jdk动态代理,要求必须有接口,最终生成的代理类和目标类实现相同的接口,在com.sun.proxy包下,类名为$proxy+数字
- cglib动态代理,最终生成的代理类会继承目标类,并且和目标类在相同的包下
- 演示一下jdk动态代理
- 创建一个接口文件
- 创建一个实现类实现接口文件,重写方法,实现核心功能
- 创建一个类其中写获取动态代理类的逻辑
- 在测试类中进行测试,成功
- 创建一个接口文件
AOP概念及相关术语
- 概述
AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加和额外功能的一种技术 - 相关术语
- 横切关注点
在每个方法中抽取出来的同一类非核心业务,在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方向的增强,比如上面演示中的计算器类中的日志功能,就是我们从目标对象中抽取出来的非核心业务。这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个很切关注点。 - 通知
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法- 前置通知:在被代理的目标方法前执行
- 返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
- 异常通知:在被代理的目标方法异常结束后执行(死于非命)
- 后置通知:再被代理的目标方法最终结束后(finally)执行(盖棺定论)
- 环绕通知:使用try。。。catch。。。finally结构围绕整个被代理的目标对象,包括上面四种通知对应的所有位置
- 切面
封装通知方法的类 - 目标
被代理的目标对象 - 代理
向目标对象应用通知之后创建的代理对象 - 连接点
这也是一个纯逻辑概念,不是语法定义的。把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点,可以理解为我们抽取横切关注点的位置
- 切入点
定义连接点的方式,每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事务(从逻辑上来说)。如果把连接点看作数据库中的记录,那么切入点就是查询记录的sql语句。Spring的AOP技术可以通过切入点定位到特定的连接点。切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件 - AOP的作用
- 简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性
- 代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑方法就被切面给增强了
- 横切关注点
基于注解的AOP
- 动态代理:jdk原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)
- cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口
- Aspectj:本质上静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的,weaver就是织入器,Spring只是借用了Aspectj中的注解
测试基于注解的AOP
- 创建一个新的子项目,spring-aop
- 添加必要的依赖,spring的aop依赖于ioc,所以spring中最核心的部分是ioc
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
</dependencies>
-
将上面的计算器接口类和实现类复制到这个项目中
-
创建切面类
-
创建一个spring的配置文件,aop是依赖于ioc的,切面类和目标类都需要交给ioc容器管理
-
将切面类标识为切面组件
-
在spring配置文件中开启aop注解支持
-
在切面类中写通知方法,类似before前置通知
-
发现before注解报错,是因为必须需要表达式去设置在哪个连接点进行注入
-
在测试类中进行测试,我们这里测试获取CalculatorImpl对象
原因是因为我们为目标对象创建了代理对象以后就只能通过代理对象去访问,在这里体现的更为明显,在ioc中连我们的目标对象都无法获取了,这里我们需要使用代理对象来调用方法,但是我们不知道代理类具体是什么,因为是spring为我们生成的,但是代理类与目标类实现的是相同的接口,我们可以通过向上转型来创建接口对象,调用对应的方法。因为代理对象和目标对象都实现了这个接口,但是目标对象在上面测试的时候无法获取到,所以只能获取到代理对象,然后执行对应的方法,发现通知输出成功,切面起了作用
AOP切入点表达式语法和重用以及获取连接点信息
获取连接点信息
当我们写一个新的通知方法的时候还需要写切入点表达式,会有些麻烦,我们可以使用切入点表达式重用去解决这个问题,在切面类中使用@Pointcut注解标识一个方法,在注解的括号中写切入点表达式,这个方法的名称就可以作为该表达式的名称作为@Before或者其他通知类型注解的value值,标识切入点表达式,这样可以做到切入点表达式的重用。
基于注解的AOP之个各种通知的使用
- @Before:前置通知,在目标对象方法执行之前执行
- @After:后置通知,在目标对象方法的finally子句中执行的,一般用于关闭资源
- @AfterReturning:返回通知,在目标对象方法返回值之后执行,可以获取到目标方法的返回值。若要在返回通知中获取目标对象的返回值,只需要在注解中使用returning属性设置接受返回值的名称,并在方法的参数中添加一个Object对象来接收返回值即可
- @AfterThrowing 异常通知,在目标方法的catch中执行,当有异常的时候就会执行,可以获取到出现的异常,设置这个注解的throwing属性作为异常对象的属性值,在方法的参数中写一个异常对象,名字与throwing属性中设置的属性名一致即可
- @Around,环绕通知就相当于另外四种加在一起了
各种通知的执行顺序
- Spring版本5.3.x以前
- 前置通知
- 目标操作
- 后置通知
- 返回通知或异常通知
- Spring版本5.3.x以后
- 前置通知
- 目标操作
- 返回通知或异常通知
- 后置通知
切面的优先级
当我们有多个切面的时候可以设置切面的优先级,两个切面中都创建了前置通知的情况下,在执行目标方法之前,会按照优先级去执行前置通知
- 创建一个新的切面类ValidateAspect,用于验证计算之前的两个参数是否合法
- 使用@Order注解即可设置切面的优先级,value是整数,数越小表示优先级越高,默认的优先级是integer的最大值
- 设置优先级之前
- 在ValidateAspect使用@Order设置一个优先级,可以不用在LoggerAspect中设置优先级,因为没一个切面类都有默认的优先级
- 设置默认优先级之后
基于xml的AOP(了解)
- 创建接口
- 创建实现类
- 创建两个切面类,把切面注解全部去除掉
- 创建一个spring的配置文件
- 在测试文件中进行测试