Spring学习笔记02-AOP
文章目录
Spring AOP
代理模式
代理模式的原理
- 使用一个代理对象将目标对象包装起来. 然后使用代理对象"取代"目标对象. 所有对目标对象的操作都需要
- 通过代理对象. 代理对象也可以决定是否以及何时调用目标对象的方法
动态代理的缓存机制
根据题目百度
百度 :生成的代理类在哪里缓存
动态代理的方式
- 基于接口实现动态代理: JDK动态代理 (代理对象和 目标对象都实现相同的接口, 代理对象调用接口中的方法时 目标对象也有)
- 基于继承实现动态代理: Cglib javassist 等. (让代理对象继承目标对象)
具体看被代理对象有无实现接口
JDK动态代理
Proxy : 是所有动态代理类的父类,主要用于创建动态代理类 或者 代理类的对象.
Object proxy = newProxyInstance(ClassLoader, Interfaces ,InvocationHandler)
Class proxyClass = getProxyClass(ClassLoader,Interfaces );源码里Proxy类通过该方法获取
InvocationHandler: 代理处理工具.
invoke(proxy, Mehtod , args):代理对象 如何控制目标对象的方法调用并进行扩展
jdk动态代理执行流程
cglib动态代理
通过继承被代理类来代理方法的调用
Spring如何用aspectj切面创建代理类及只代理对应的切入的方法
- 在通过注解将bean交给spring管理时,spring就会通过调用getbean方法来反射调用构造函数将 bean交给spring管理,
- 在创建bean时如果有该bean的方法有切入点方法,那么直接创建该类的代理类
- 具体看实现接口还是继承类(有无实现接口),若配置了
< proxy-target-class="true">
配置了true都是(实现接口也是)cglib,默认false 实现了接口的是jdk动态代理,前面的继承(方法)控制,后面的if判断该方法是否是被切入的方法,来判断是否要执行具体的代理方法
保存生成的动态代理类
- jdk动态代理
Properties properties = System.getProperties(); properties.put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
- cglib动态代理
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\class");
Spring AOP使用
注意
-
如果目标类没有被切面类标注,AspectJ 不会为目标对象创建代理对象
如果目标类被切面类标注了,才会创建代理对象
spring aspectj 如何选择代理方式代理目标对象
-
目标对象有接口 基于实现,jdk动态代理
目标对象没有接口 基于继承,cglib代理
如果配置了
< proxy-target-class="true">
则基于实现接口的动态代理也是用cglib来生成代理对象
AOP测试环境搭建
-
创建项目导入 相关的jar包
springioc需要的5个jar包,spring支持aop和aspect的两个jar,导入aspectj的三个jar包 -
创建Springioc配置文件
并在配置中引入 aop名称空间 -
开启aop的自动代理功能:
开启后,以后获取容器中的bean时,AspectJ会自动创建对应的代理对象返回<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
-
需求
调用MathCalculator.java的方法时 希望使用动态代理模式在方法前后打印日志 - 方法执行之前需要打印日志(横切关注点),对应一个日志方法 - 日志方法封装的类(切面) 》 需要将切面类配置到IOC容器中交给容器管理 》在切面类上使用@Aspect 注解标注:表名是一个切面类 》在切面类的方法上指定当前o 方法的切入点 @Before注解,表名方法的切入点是 目标对象的方法执行之前 》 在before注解后还需要执行那些目标对象的哪个方法执行时需要执行当前方法 @Before(value="切入点表达式") 》 再从ioc容器中获取目标对象时就是代理对象 调用代理对象的方法会触发切面类的指定方法执行
-
如果需要打印详细的目标对象的方法信息,需要在切面类的 通知方法中 JoinPoint 使用参数接受方法信息
如果存在多个切面类的通知方法作用于同一个目标对象的方法,会有优先级的顺序, 我们可以通过@Order(int)指定执行的优先级,值越小优先级越高
AOP术语
横切关注点
从每个方法中抽取出来的同一类非核心业务。
切面(Aspect)
封装横切关注点信息的类,每个关注点体现为一个通知方法。
通知(Advice)
切面必须要完成的各个具体工作
目标(Target)
被通知的对象
代理(Proxy)
向目标对象应用通知之后创建的代理对象
连接点(Joinpoint)
横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置。例如:类某个方法调用前、调用后、方法捕获到异常后等。
在应用程序中可以使用横纵两个坐标来定位一个具体的连接点:
POintCut切入点
如果把连接点看作数据库中的记录,那么切入点就是查询条件——AOP可以通过切入点定位到特定的连接点。切点通过org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
aspectj切面类相关注解以及配置
切面类方法执行优先级
当有多个切面类的方法切入点切入同一个被切入方法时,可以配置优先级,如果不配置,优先级随机
@Order(10) 越小优先级越大
切面类注解以及application.xml配置
- 切面类需要注解@Aspect @Component 可选@Order(8)以及对应的通知
@After("pointcutExpression()")
和切入点表达式@Pointcut("execution(* *.*.*.MathCalculator.*(..))")
- 被切面类也需要@Component
- 配置
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
和<context:component-scan base-package="com.hey">
aspectj AOP切入注解语法
切入点(pointCut)表达式语法
权限 返回值类型 包名.类名.方法名(形参类型列表)
- |* 代表所有 : 除了参数之外的可以使用
- 不区分返回值类型:
public * com.hey.aop.MathCalculator.add(int, int)
- 所有权限和返回值类型:
* com.hey.aop.MathCalculator.add(..)
- 通过*代表一级任意包名:
* *.*.*.MathCalculator.add(..)
- 如果包名和类名以及方法名都全匹配:
* *(..)
- 不区分返回值类型:
- …代表任意的参数类型列表
- 匹配第一个参数类型是int的,其他参数无所谓:
* *(int,..)
- 匹配最后一个参数类型是double的,前面的参数格式类型都无所谓:
* *(..,double)
- 匹配第一个参数类型是int的,其他参数无所谓:
AspectJ的 通知注解
-
@Before 前置通知: 在方法即将执行时调用
-
@After 后置通知: 在方法执行后还没有返回结果之前
-
@AfterReturning 返回通知: 在方法执行之后正常返回结果时执行 获取方法和返回值信息
@AfterReturning(value="execution(* *.*.*.MathCalculator.*(..))",returning="a") public void afterReturing(JoinPoint point , Object a) { System.out.println(point.getSignature().getName()+"方法即将返回结果......... 结果是:" +a); }
a对应通知方法中的接受结果的形参名
-
@AfterThrowing 异常通知: 在方法执行时出现异常时执行 获取方法和异常信息
@AfterThrowing(value="pointcutExpression()" ,throwing="e") public void afterThrowing(JoinPoint point , Exception e) { System.out.println(point.getSignature().getName()+"方法出现异常......... 异常:"+e); }
-
@Around 环绕通知 :了解 ,相当于将之前的动态代理的InvocationHandler的invoke方法暴露给我们自己编写了
除了获取要执行的方法信息,还需要目标对象的要执行的方法对象
@Around(value="pointcutExpression()") public Object aroundMethod(ProceedingJoinPoint point) { Object result = null;//让目标对象的目标方法执行 try { //前置通知: //返回通知: result = point.proceed(); } catch (Throwable e) { //异常通知: e.printStackTrace(); } return result; }
提取切入点表达式
//使用方法绑定切入点表达式
@Pointcut("execution(* *.*.*.MathCalculator.*(..))")
public void pointcutExpression() {}
//其他类使用
@Before("com.hey.aop.LoggingAspect.pointcutExpression()")
或者@After("pointcutExpression()")//如果能够确定类,比如在同一个包下
public void beforeMethod(JoinPoint point) {}
JoinPoint连接点方法API作用
- point.getSignature()获取签名对象 被代理方法签名
- point.getArgs()获取被代理方法参数列表
- point.getThis(); 代理对象
- point.getTarget(); 目标对象
XML方式配置切面-较少使用
正常情况下,基于注解的声明要优先于基于XML的声明。通过AspectJ注解,切面可以与AspectJ兼容,而基于XML的配置则是Spring专有的。由于AspectJ得到越来越多的 AOP框架支持,所以以注解风格编写的切面将会有更多重用的机会。
配置语法
<!-- 1、配置所有的需要IOC容器管理的javabean
2、使用aop:config 标签配置切面类以及通知方法(一个aop:config就对应一个切面类的配置)
aop:aspect标签:代表 要当做切面类的类
-->
<bean class="com.hey.aop.MathCalculator"></bean>
<bean id="loggingAspect" class="com.hey.aop.LoggingAspect"></bean>
<!-- <aop:aspectj-autoproxy></aop:aspectj-autoproxy> -->
<aop:config>
<aop:pointcut expression="execution(* *.*.*.*.*(..))" id="pointcut"/>
<aop:aspect ref="loggingAspect">
<aop:before method="beforeMethod" pointcut-ref="pointcut" />
</aop:aspect>
</aop:config>
AOP应用-声明式事务
事务的实现方式
编程式事务
通过我们自己的代码控制事务
使用原生的JDBC API进行事务管理
- 获取数据库连接Connection对象
- 取消事务的自动提交
- 执行操作
- 正常完成操作时手动提交事务
- 执行失败时回滚事务
- 关闭相关资源
Spring的声明式事务:
底层使用AOP的方式提供了一个切面类,只需要将该切面类配置到IOC容器中管理即可
不需要我们编写代码,用过Spring事务注解标注即可
两者相比设计模式的优缺点
使用原生的JDBC API实现事务管理是所有事务管理方式的基石,同时也是最典型 的编程式事务管理。编程式事务管理需要将事务管理代码嵌入到业务方法中来控制事务 的提交和回滚。在使用编程的方式管理事务时,必须在每个事务操作中包含额外的事务 管理代码。相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块 都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余。
声明式事务将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
事务管理代码的固定模式作为一种横切关注点,可以通过AOP方法模块化,进而借助Spring AOP框架实现声明式事务管理。
Spring在不同的事务管理API之上定义了一个抽象层,通过配置的方式使其生效,从而让应用程序开发人员不必了解事务管理API的底层实现细节,就可以使用Spring的事务管理机制。
Spring既支持编程式事务管理,也支持声明式的事务管理。
事务管理器和事务注解驱动配置
<!--
1、 配置Spring事务管理器:必须指定要管理的数据库连接池
2、在需要使用事务的方法上 使用 @Transactional 注解标注当前方法需要使用事务
3、并且需要在IOC配置文件中开启支持事务注解
-->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="dataSourceTransactionManager">
<property name="dataSource" ref="druidDataSource"></property>
</bean>
通过druidDataSource属性对里面的事务(连接)进行管理和代理,在有@Transactional注解的方法上,dataSourceTransactionManager赋予该方法一个代理事务(一个连接),在单个jdbctemplate方法执行完不放回连接池,整个方法用一个事务进行控制回滚还是提交。
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
需要开启事务注解驱动,开启后注解才生效,且从配置的dataSourceTransactionManager拿取代理的事务(连接),作用域整个方法。
注解事务配置流程
注解属性
propagation 事务的传播行为
事务的传播行为:在不同类之间方法的调用
如果一个类的方法中调用了另外一个类的方法,两个方法都是用事务注解,默认会将事务传递给下一个类的方法使用
下一个方法拿到后直接使用
需求:
让事务方法中调用的另外一个事务方法 不使用当前的事务
- propagation 属性可以控制事务传播行为
- propagation=Propagation.REQUIRED 表示 使用前面的方法传过来的事务
- propagation=Propagation.REQUIRES_NEW 表示 将前面的方法传过来的事务挂起,自己新开事务管理自己方法代码的执行
isolation 事务的隔离级别
设置事务的隔离级别
- Isolation.READ_UNCOMMITTED 读未提交(安全级别最低) 当前事务的数据还未提交或回滚时其他事务也可以去取
如果数据只涉及到读取,可以使用该级别 - Isolation.READ_COMMITTED(oracle默认) 读已提交 orcal数据库的默认隔离级别 另外一个事务只能读取当前事务提交或回滚之后的数据
- 使用最多 同时考虑效率安全问题
- Isolation.REPEATABLE_READ 可重复读 mysql的默认隔离级别
- Isolation.SERIALIZABLE(oracle有) 串行化
- 效率低,数据安全,建议不要用
readOnly
默认false
true:代表本次事务操作只涉及到读取,
效率高了很多,以后会经过其他配置,有的事务考虑到安全问题,设置读已提交,可重复读,等安全级别高的,但是设置只读属性 可以打破这些隔离级别,可以进行相关操作,进行相关配置(意思安全性隔离级别降到最低?)
设置为true可以提高效率(只读)
timeout
设置事务的超时时间
- 默认 -1 永不超时
- 3 代表超时时间为3秒
- TransactionTimedOutException: 如果事务执行过程超过指定时间则抛出此 异常,并断开事务
- 事务如果在指定时间内还没操作完数据提交,则断开,应该是断开连接,如果不断开,一直保持连接,让数据库资源占用,那么数据库很有可能崩溃
注意如果事务没有进行任务数据库操作,如在外面开启事务,里面逻辑执行方法开启的传递事务是新的,那么超时后自然外面的连接就自然关闭了,如果事务确实传递,期间在对数据库进行操作才会报错(事务操作数据库超时) 如上,然后关闭,既然是声明式事务管理的事务,应该会回滚
@Transactional(isolation=Isolation.READ_COMMITTED,
readOnly=false ,
timeout=2)
public void createOrder(List<String> ids , String uId) {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//遍历图书的id集合,让用户一本本购买
for (String id : ids) {
service.buyBook(id, uId);
}
}
@Transactional(propagation=Propagation.REQUIRES_NEW,
noRollbackFor= { UserAccountException.class , NullPointerException.class}
)
public void buyBook(String bookId , String userId) {
//查询图书:判断图书库存是否足够,如果不够抛出异常,如果足够减库存加销量
Book book = bookDao.getBookByBookId(bookId);
if(book.getStock()<=0) {
throw new BookStockException(book.getTitle()+": 库存不足!!!!");
}
int i = bookDao.updateStockAndSalesByBookId(bookId);
//查询用户:判断用户账户余额是否足够,如果不够抛出异常,如果足够减账户余额
User user = userDao.getUserByUserId(userId);
//余额需要大于等于购买的图书的金额
if(user.getBalance()<book.getPrice()) {
throw new UserAccountException(user.getUsername()+": 账户余额不足!!!!");
}
i = userDao.updateUserAccountByUserId(userId , book.getPrice());
System.out.println(book+" , "+ user);
}
noRollbackFor
抛出什么异常不回滚
@Transactional(propagation=Propagation.REQUIRES_NEW,
)
@Transactional(isolation=Isolation.READ_COMMITTED,
readOnly=false ,
timeout=2,
noRollbackFor= { UserAccountException.class , NullPointerException.class})
public void createOrder(List<String> ids , String uId) {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//遍历图书的id集合,让用户一本本购买
for (String id : ids) {
service.buyBook(id, uId);
}
需要注意类似上面如果一个事务执行多次购买操作,抛出多个异常(每次购买都抛出账户余额不足),那第一次是不回滚,第二次就回滚了,因为是一个事务控制的,那么会有干扰,只监控第一次
noRollbackForStringName
通过字符串类名的数组来告诉注解哪些情况到那些异常不要回滚
基于xml实现的事务
<!--
了解:
基于XML实现的事务
-->
<!-- 指定要被(事务管理器)加事务的(方法) -->
<tx:advice id="txAdvice" transaction-manager="dataSourceTransactionManager">
<tx:attributes>
<!-- 指定要被事务控制的方法名:推荐使用 get* find* remove* delete* insert* save*-->
<tx:method name="buyBook" propagation="REQUIRES_NEW" timeout="3"/>
<tx:method name="createOrder" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- 将事务管理器和 对应包下的类的方法绑定: 去指定包下查找类 处理事务
aop:advisor advice-ref="txAdvice" 指定要使用的事务管理通知
pointcut="" 指定切入点表达式
-->
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.hey.service.*.*(..))"/>
</aop:config>