实现事务代码重用
思路 :
1) 提供了一个代理类(Proxy)
调用通知类的invoke方法
获取方法对象和方法实际参数
与目标要实现相同的接口(目的是让使用者察觉不出是代理替换了原来的目标)
2) 提供了一个通知类(Advice)
实现了重复代码(事务的重复代码)
反射调用了目标对象的方法
把重复代码和目标方法联系在了一起
jdk提供了一个通用的接口 InvocationHandler (通知接口)
动态代理
自己实现的代理类称之为‘静态代理’
jdk提供了动态代理技术(程序运行期间,由jdk生成这个代理类和它的实例对象)
正常使用类:*.java -> javac -> *.class -> java -> 加载该class到虚拟机
动态代理 直接生成了*.class字节码, 加载该class到虚拟机
Proxy.newProxyInstance(类加载器, 要实现的接口数组, InvocationHandler);
jdk的动态代理, 只能针对接口实现代理, jdk动态代理在高版本中性能优于cglib
cglib动态代理, 既可以针对接口实现代理,也可以生成子类充当目标父类的代理
SpringAOP
AOP:面向切面编程(aspect oriented programming),在程序开发中主
要用来解决一些系统层面上的问题,比如日志,事务,权限等待
技术概念上理解:面向切面编程就是把重复的逻辑抽取为一个通知方法,
然后通过切点匹配来决定哪些目标要应用这些通知。
其中利用了代理的技术,在代理中检查切点是否匹配,
调用通知(多个),最后调用目标
一:AOP的基本概念
(1)Aspect(切面):通常是一个类,里面可以定义切入点和通知
(2)JointPoint(连接点):程序执行过程中明确的点,一般是方法的调用
(3)Advice(通知):AOP在特定的切入点上执行的增强处理,有before,after,afterReturning,afterThrowing,around
(4)Pointcut(切入点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式(定义匹配规则)
(5)AOP代理:AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类
二:基于注解的AOP配置方式
1. pom.xml 新增依赖
<!-- spring aop -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.17.RELEASE</version>
</dependency>
<!-- 第三方 aop依赖 aspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.9</version>
</dependency>
2. spring的配置文件加入aop相关标签
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 目标类 -->
<bean id="userService" class="spring.service.UserServiceTarget">
</bean>
<!-- 通知类 -->
<bean id="transactionAdvice" class="spring.advice.TransactionAdvice">
</bean>
<!-- 让相关注解生效, 还能够帮我们生成底层的代理对象 -->
<aop:aspectj-autoproxy/>
</beans>
3. 通知类型介绍
环绕通知: @Around(切点表达式) 加在通知方法上(功能最全的), 通知方法环绕着目标方法
前置通知: @Before 通知代码只会出现在目标方法之前
正常结束通知: @AfterReturning在目标方法正常完成后做增强,除了指定切入点表达式后,还
可以指定一个返回值形参名returning,代表目标方法的返回值
异常通知: @AfterThrowing 主要用来处理程序中未处理的异常,除了指定切入点表达式后,还
可以指定一个throwing的返回值形参名,可以通过该形参名来访问目标方法中所抛出
的异常对象
结束通知: @After 在目标方法结束后(正常或异常),总会调用的通知
4. 切点表达式
* within(包名.类名) 这个类中所有方法执行时,都会应用通知方法
* execution(访问修饰符 返回值类型 包名.类名.方法名( 参数类型... ))
注意`*` 可以匹配任意类型, 可以出现在方法返回值,方法名,类名当中
注意`..` 可以匹配方法参数,表示参数的个数和类型都是任意的
* @annotation(包名.注解名)
它是根据方法上的注解进行匹配, 如果有注解则匹配
@Aspect
public class Advice {
@Before("within(spring.service.UserServiceTarget)")
public void before() {
System.out.println("==== 在目标方法之前被调用");
}
@AfterReturning("within(spring.service.UserServiceTarget)")
public void afterReturning() {
System.out.println("======== 方法正常结束");
}
@AfterThrowing(pointcut = "within(spring.service.UserServiceTarget)", throwing = "e")
public void exception(Exception e) { // 类似于catch ,可以获取异常信息
System.out.println("=====出现了异常" + e.getMessage());
}
@After("within(spring.service.UserServiceTarget)")
public void after() {
System.out.println("==========总会被调用");
}
}
注意:Arround环绕通知需要引入ProceedingJoinPoint
且Around不要和AfterThrowing同时使用,会冲突
/ 切面
@Aspect
public class TransactionAdvice {
// @Around("within(spring.service.UserServiceTarget)")
// @Around("execution(public * spring.service.UserServiceTarget.biz1(..) )")
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
// 通过切点表达式,把通知和目标结合在一起
// 作用当UserServiceTarget类中任意一个方法执行时(biz1,biz2) 就会和下面的transaction方法结合在一起了
// 通知类型:环绕通知,决定了这个通知方法会和哪些目标方法结合
// 这个方法称为通知方法,它其中包含了一些重复逻辑,另外它负责调用目标方法
public Object transaction(ProceedingJoinPoint pjp) { // pjp 内部去调用目标的方法
System.out.println("开始事务");
Object r = null;
try {
// 调用目标
r = pjp.proceed(); // 目标 biz1, biz2...
System.out.println("提交事务");
} catch (Throwable e) {
System.out.println("回滚事务");
}
return r;
}
}
5. 调用流程
容器启动时:
1)spring会检查容器,看是否需要为这些bean创建代理对象
2)检查所有的切点表达式,如果匹配到了目标,就为该目标创建代理对象
获取对象时(getBean, 依赖注入):
会检查这个类型是否有代理对象,如果有,优先返回代理对象
调用方法时:
调用了代理对象的方法,代理对象会首先经过通知类(多个)
检查切点,如果切点匹配,再进行下面的通知调用
根据不同的通知类型进行调用:
例如前置通知先调通知,再调目标
如果是环绕通知,先调用通知,通知内部调用目标