面向切面编程
基于OOP(面向对象编程)基础之上的新的编程思想,指在程序运行期间将某段代码动态的切入到指定方法的指定位置进行运行的这种编程方式
抛砖引玉,先看这样一个案例,一步一步强化它,让它更便捷
需求:一个简单的计算接口,有一个实现类,实现了加减乘除四种运算功能,传入两个参数计算对应结果,但是要求有日志进行记录
环境准备:
public interface calculate { //一个calcu接口,内置四个方法
int add(int a,int b);
int subtract(int a,int b);
int multiply (int a,int b);
int divide(int a,int b);
}
<?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:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="impl"/> //开启注解
<context:annotation-config/>
</beans>
- 原始版本
public class calculateImpl implements calculate { //实习类
public int add(int a, int b) {
System.out.println("调用了add方法,传入的参数为[" + a + "," + "]");
int result = a + b;
System.out.println("结果为[" + result + "]"); //手动日志记录
return result;
}
public int subtract(int a, int b) {
System.out.println("调用了subtract方法,传入的参数为[" + a + "," + "]");
int result = a - b;
System.out.println("结果为[" + result + "]");
return result;
}
public int multiply(int a, int b) {
System.out.println("调用了multiply方法,传入的参数为[" + a + "," + "]");
int result = a * b;
System.out.println("结果为[" + result + "]");
return result;
}
public int divide(int a, int b) {
System.out.println("调用了divide方法,传入的参数为[" + a + "," + "]");
int result = a / b;
System.out.println("结果为[" + result + "]");
return result;
}
}
测试结果
缺点:耦合度极高,若方法错了或样式换了,需要手动全部修改,多了之后会变得非常繁琐,难以维护。
- 抽取日志工具类
既然一条一条的写,十分复杂,且代码重复,则可以将日志功能的代码,抽取出来变成一个工具类
public class logUtil {
public static void showInfo(int a,int b) {
System.out.println("调用了xxx方法,传入的参数为[" + a + "," + b + "]");
}
}
这样在需要修改方法或样式时,修改这一个就行了,但是依然麻烦,不同的方法需要不同的实现,日志只是我们想要的辅助功能,而不是核心功能,费大功夫去实现工具类达到一个辅助效果,显然是舍本逐末了,且要多次调用且耦合度依然很高
- 动态代理
public class calculateProxy {
public static calculate getProxy(final calculate cal) {
calculate proxy = (calculate) Proxy.newProxyInstance(cal.getClass().getClassLoader(),
cal.getClass().getInterfaces(),
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//在此处增强代理对象
Object invoke = null;
try {
logUtil.start(method,args);
invoke = method.invoke(cal, args);
logUtil.resu(method);
}catch (Exception e){
logUtil.exce(method,e);
}finally {
logUtil.end(method);
}
return invoke; //此处return的值,是代理对象执行方法的结果,不做修改则执行结果与原对象一致
//return 8;若做了修改,例如此处,代理对象执行方法的结果就 恒为8
}
});
return proxy;
}
}
这样耦合度就低了,一切增强由动态代理实现,修改也只需要修改动态代理的实现方式即可,也可以将sout语句抽取到工具类中,提高易读性
public class logUtil {
public static void start(Method method, Object[] args) {
System.out.println("调用了" + method.getName() + "方法传入的参数为" + Arrays.toString(args));
}
public static void resu(Method method) {
System.out.println(method.getName() + "方法正常执行结束");
}
public static void exce(Method method, Exception e) {
System.out.println(method.getName() + "方法,方法执行出错了,错误为" + e.getCause());
}
public static void end(Method method) {
System.out.println(method.getName() + "方法执行结束");
}
}
动态代理最大的缺点,jdk实现动态代理,需要目标对象至少实现一个接口,因为代理对象需要通过实现目标对象的接口与目标对象产生联系
AOP
spring知晓了动态代理的缺点,于是开发出了基于动态代理的AOP,AOP底层就是动态代理,弥补了动态代理的缺点,同时简化了实现
-
专业术语
以刚才的例子为例,
calculate,引用雷神的图
- 横切关注点&通知方法&切面类
在calculate中,实现了加减乘除四个方法,在动态代理中,在每一个方法的开始、正常执行出结果、错误执行出异常、结束四个地方插入了logUtil工具类的方法来执行日志操作,这些操作是四个方法都有的,而这四个位置就称作横切关注点,这些操作就称作通知方法,而集成这些方法的logUtil工具类,就称作切面类 - 连接点&切入点
方法执行会存在开始、正常执行出结果、错误执行出异常、结束四个状态,而AOP可以在这些状态上添加额外的操作,将方法看做纵向执行,AOP看做横向插入,则他们在每个状态上的交点称作连接点,其中被AOP真正执行了操作的连接点称作切入点 - 切入点表达式
连接点有很多,在众多连接点中,使用切入点表达式选出我们感兴趣的地方作为切入点
注解实现AOP步骤
- 在maven导入依赖jar包
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
-
写配置
1、配置xml
xml
2、配置切面类
切面类
切面类属性详解:
1、@Aspect: 该注解可以让ioc容器识别该类为切面类
五个通知方法注解:
1、@Before(前置通知):该注解可以让通知方法在指定方法执行前执行
value:切面表达式
2、@AfterReturning(返回通知):该注解可以让通知方法在指定方法正常执行并返回结果后执行
value:切面表达式
returning:指定通知方法形参列表中的一个值为方法的返回值
3、@AfterThrowing(异常通知):该注解可以让通知方法在指定方法执行异常时执行
value:切面表达式
throwing:指定通知方法形参列表中的一个异常对象为方法的异常对象
4、@After(后置通知):该注解可以让通知方法在指定方法执行结束后执行
value:切面表达式
5、@Around(环绕通知):使用该注解修饰的方法,内部就是一个动态代理的实现,可以实现之前的四种通知,非常强大
@Around
我们知道之前的四种通知,其实执行顺序是有误的,但是使用环绕通知,内部定义的顺序是不会有误的,因为他内部就是一个动态代理类,当环绕通知和其他通知共存时,会优先执行环绕通知,因为他掌握了目标方法对象本身,何时执行由他决定,因此若程序有异常,环绕通知抓取后,其他通知就无法获取了,所以在环绕通知抓取异常后,一定要主动抛出异常
切面表达式:
1、默认格式:
execution(权限修饰符 返回值类型 方法全类名(形参列表))
2、通配符
* :
用在返回值上表示任意返回值
用在方法全类名第一位,表示所有方法都切,放在其余位置表示任意一层的目录名
用在形参列表上表示任意类型形参
.. :
用在方法全类名中表示任意多层的目录名,不能用在第一位
JoinPoint对象:包含了方法执行时的所有信息,参数、方法全类名……
补充:
1、通知方法的要求不严格,唯一的要求是通知方法的每一个形参,spring必须知道
2、当引入了cglib的包之后,spring的AOP会被强化,spring默认的AOP基于jdk的动态代理实现,所以需要目标类至少实现一个接口,而添加cglib之后,不实现接口也可以实现动态代理,实现AOP
3、@AfterThrowing指定异常可以指定指定异常,例如只指定空指针异常,所以尽量往大了写(Exception),同理@AfterReturning的返回值类型也可以指定,但是建议往大了写(Object)
4、切入点表达式也可以实现重用,当多个通知方法的切入点表达式重复时,会有很多重复的代码以及工作量,这时候可以使用@Pointcut来抽取重复的切面表达式
5、普通通知之所以称为普通通知,就是因为他们只能在方法执行到某个状态是时做出相应反应,而不能影响方法本身,而环绕通知称为高级通知就是因为他内部是一个动态代理,可以影响方法本身的执行
6、当有多个切面类对一个目标对象生效时,可以使用@Order(x)注解传入int数据来决定多个切面的执行顺序,数值越小优先级越高,若不指定,IOC容器默认以切面类首字母排序来执行,注意环绕通知内有一个目标对象,且可以影响到本切面类的其他通知执行,但是有多个切面类时,一个切面类内的环绕通知无法影响其他切面类
可见,多个切面类内部的通知方法执行顺序,和栈是一样的,先进后出,后进先出
-
测试
测试结果
XML配置AOP
使用xml配置AOP和注解实现上是一致的,只要将注解的一个个注解换成xml中的标签即可,并不复杂
配置xml
xml
可见几乎所有注解在xml中都能找到对应的标签,所以一通百通
切面类中的JoinPoint等对象参数,无需在xml中配置,而result、exception等对象参数则需要在xml中告诉IOC容器,xml中只是生命,具体实现依旧在类中进行
切面类
牢记:重要的切面类使用xml配置,更加清晰,更加稳定,更加便于维护,更加容易区分,且外部导入的切面类,只能使用xml进行配置
网友评论