上一篇文章中,我们对于代理模式有了基础了解:https://blog.csdn.net/szy2333/article/details/89338412
从没有使用代理模式到静态代理再到动态代理,我们对于代码已经有了大大的简化,但这些都是在可以不需要spring的情况下进行的。但其实,如果使用spring对其进行管理,代码还会进一步简化,并且,可插拔性也会更高
什么是SpringAOP?
首先我们需要先了解一些名词
代理:proxy 代理就是目标对象的加强。Spring中AOP既可以是JDK动态代理(基于接口),也可以是CGLIB代理(基于子类)
目标:target 目标对象,即被代理的对象,一般是方法的调用
通知:advice 包含了可重用的代码(例如事务代码),并调用目标
切点:pointcut 把通知和目标结合,是带有通知的连接点,它定义的是一种匹配规则,在程序中主要体现为书写切点表达式
切面:ascept 通常是一个类,里面定义切点和通知,即 切面 = 通知 + 切点
而AOP就是 ascept oriented programming ---> 面向切面编程 ---> 将重复代码取出来变成通知类并与目标类结合(和代理差不多)
接下来,我们用代码演示AOP的过程,依然是使用对于事务代码块简化的例子
首先,我们需要加入相关的依赖,之前文章中添加过的不再赘述,这次需要加入两个关于aop的相关的依赖
org.springframework
spring-aop
4.3.18.RELEASE
org.aspectj
aspectjweaver
1.8.9
org.aspectj
aspectjrt
1.8.9
UserService类
package spring.service;
public interface UserService {
public void biz1();
public void biz1(int i);
public void biz1(int a, String b);
public void biz2();
public void abc();
}
UserServiceTarget类
package spring.service;
public class UserServiceTarget implements UserService {
@Override
public void biz1() {
System.out.println("sql1");
System.out.println("sql2");
}
@Override
public void biz1(int i) {
System.out.println("i = " +i);
}
@Override
public void biz1(int a, String b) {
System.out.println(b + " ---> " + a);
}
@Override
public void biz2() {
System.out.println("sql3");
System.out.println("sql4");
System.out.println("sql5");
}
@Override
public void abc() {
System.out.println("sql6");
System.out.println("sql7");
}
}
通知类
package spring.service;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class TransactionAdvice {
//@Around是环绕通知,within决定了这个通知方法会和哪些目标方法相结合
@Around("within(spring.service.UserServiceTarget)")
//该方法称为通用方法,里面包含了重用逻辑,以及负责调用的方法,参数必须是ProceedingJoinPoint
public Object tansaction(ProceedingJoinPoint pjp) {
System.out.println("开始事务");
Object obj = null;
try {
//调用目标,使用proceed方法
obj = pjp.proceed();
System.out.println("提交事务");
} catch (Throwable e) {
System.out.println("回滚事务");
}
return obj;
}
}
最后,我们还需要利用spring中的控制反转,让容器来管理方法调用
执行结果
到这里,我们使用AOP就完成了最基础的内容啦,我们发现,步骤和原理其实和代理是一样的,只不过又将一部分内容交给spring并让其实现了控制反转。那么既然是代理,getBean的时候,返回的是接口或者目标类型还是其它什么类型呢?接下来,我们对于getBean返回的结果进行一个验证
验证代码
@Test
public void test1() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
//尽量使用接口类型,可扩展性强
UserService service = context.getBean(UserService.class);
service.biz1();
System.out.println(service.getClass());
}
验证结果
通过调用.getClass()方法,我们明确的看到它返回的不是target类型,而是一个代理类型,这个代理类就是spring容器生成的
其底层产生代理的技术就是:动态代理技术
首先spring容器返回一个代理对象,代理对象调用biz1() 的时候,就会在内部去调用通知TransactionAdvice对象,然后间接执行里面的Transaction方法,然后该方法先进行判断,看方法和目标是否符合,符合了才会去执行(即调用的biz1()确实是和@Around注解里面的东西是相符合的)
接下来,我们对于上面的几个名词进行一下深入的了解
在@Around注解括号中的东西,是具体定位到方法的连接点,比上面代码中的“within(spring.service.UserServiceTarget) ”
有这几种类型
1.within(包名.类名) 这个类中的所有方法执行时,都会应用通知方法
这种方式就是上面所演示的方式,但是这种匹配规则范围太广,有的时候,我们希望进行更细致的匹配,而不是对于类中所有的方法都进行匹配,这样这个就不适用了
2.execution(访问修饰符 返回值类型 包名.类名 方法名(参数类型))
这种方式就很好的解决了within中存在的问题,能够根据需要的方法时才启用事务
注意:" * " 可以匹配任意类型,“ . . ” 可以匹配任意类型的参数
我们用代码进行一下测试,在执行上述中biz1(int a, String b)时,才需要启用事务
通知类代码
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class TransactionAdvice {
//@Around是环绕通知,within决定了这个通知方法会和哪些目标方法相结合
@Around("execution(public void UserServiceTarget.biz1(int, String))")
//该方法称为通用方法,里面包含了重用逻辑,以及负责调用的方法,参数必须是ProceedingJoinPoint
public Object tansaction(ProceedingJoinPoint pjp) {
System.out.println("开始事务");
Object obj = null;
try {
//调用目标,使用proceed方法
obj = pjp.proceed();
System.out.println("提交事务");
} catch (Throwable e) {
System.out.println("回滚事务");
}
return obj;
}
}
测试代码
@Test
public void test1() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
//尽量使用接口类型,可扩展性强
UserService service = context.getBean(UserService.class);
service.biz1(98, "成绩:"); //测试1
service.biz1(); //测试2
System.out.println(service.getClass());
}
测试结果
当进行测试1时,执行结果如左图,进行测试2时,结果如右图
3.@annotation(包名.注解名)
它是根据方法上的注解进行匹配,如果有注解,则匹配,是最灵活的!
代码测试:
在UserServiceTarget类中的biz2()方法上面加上注解
package spring.service;
import org.springframework.transaction.annotation.Transactional;
public class UserServiceTarget implements UserService {
@Transactional
@Override
public void biz2() {
System.out.println("sql3");
System.out.println("sql4");
System.out.println("sql5");
}
@Override
public void abc() {
System.out.println("sql6");
System.out.println("sql7");
}
}
通知类---也需要改变注解
package spring.service;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class TransactionAdvice {
//@Around是环绕通知,within决定了这个通知方法会和哪些目标方法相结合
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
//该方法称为通用方法,里面包含了重用逻辑,以及负责调用的方法,参数必须是ProceedingJoinPoint
public Object tansaction(ProceedingJoinPoint pjp) {
System.out.println("开始事务");
Object obj = null;
try {
//调用目标,使用proceed方法
obj = pjp.proceed();
System.out.println("提交事务");
} catch (Throwable e) {
System.out.println("回滚事务");
}
return obj;
}
}
测试代码
@Test
public void test1() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
//尽量使用接口类型,可扩展性强
UserService service = context.getBean(UserService.class);
service.biz2(); //测试1
service.abc(); //测试2
System.out.println(service.getClass());
}
测试结果
环绕通知:@Around(切点表达式)加在通知方法上(是功能最全的) 前面的例子使用的是这种通知
前置通知:@Before 通知代码只会出现在目标方法之前
正常结束通知:@AfterRunning 在目标方法正常结束后,调用通知方法
异常通知:@AfterThrowing 在目标方法出现异常后,调用通知方法
结束通知:@After 在目标方法结束后(正常或异常),总会调用的通知
以前置通知为例,进行代码测试示范
在通知类前面加注解
@Aspect
public class Advice2 {
@Before("within(spring.service.UserServiceTarget)")
public void advice() {
System.out.println("通知类---在目标方法之前调用");
}
}
测试代码
@Test
public void test2() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
UserService bean = context.getBean(UserService.class);
bean.biz1();
}
测试结果
前面所有的实现方式,我们都先要目标类实现一个父类接口,那么如果不实现接口,而是直接使用目标类,spring支持这种方式吗?接下来,我们测验是否能使用
首先,创建一个不用实现父类接口的新目标类型OrderService
package spring.service;
public class OrderService {
public void ord1() {
System.out.println("订单交易完成");
}
public void ord2() {
System.out.println("订单已删除");
}
}
接下来,创建新的通知类Advice3,使用@Before的方式
package spring.service;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class Advice3 {
@Before("within(spring.service.OrderService)")
public void transaction() {
System.out.println("前置通知...");
}
}
然后,将这两个类添加到spring.xml中,让spring容器对其进行管理
测试文件
@Test
public void test3() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
OrderService service = context.getBean(OrderService.class);
service.ord1();
}
从结果我们可以看出,即使没有实现父类接口,却仍然可以使用AOP实现代理的目的,那么是之前讲的东西都不成立吗?
想要验证,我们首先看一下,通过getBean得到的service是什么类型
测试代码
@Test
public void test3() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
OrderService service = context.getBean(OrderService.class);
service.ord1();
System.out.println("service的真正类型是 : " + service.getClass());
}
测试结果
通过测验结果我可以看到,它的真正类型和之前测试得到的代理类型不一样,而是包名.类名$$EnhancerBySpringCGLIB$$字符串
答案是:其实在spring中除了之前的jdk代理和动态代理除外,还有其他代理类型,比如CGLIB代理,这种代理类型不需要目标实现特殊的接口,而我们这次就是使用的这种代理类型来实现的
那么没有实现接口,我们为什么能把得到的service当做OrderService来用呢?两者之间有什么关系?
其实就是底层生成了一个子类代理对象,然后把它当做父类变量来使用,这对于程序员来说,是一个非常简洁的方式,因为不需要再刻意去实现一个父类接口了
jdk的动态代理 只能针对接口实现代理
cglib动态代理 既可以针对接口实现代理,也可以生成子类充当父类的代理
javasist动态代理 是struct2,hibernate框架中常用的代理
到那时值得注意的是:在spring中,并不是cglib代理的适用性更加广泛,就会直接使用这个代理,而是优先使用jdk动态代理,因为在jdk高版本中,jdk动态代理技术相比于cglib的性能更优(虽然cglib也不差),所以spring会优先考虑jdk动态代理,如果没有实现父类接口,才会退而求其次去使用cglib动态代理