文章目录
一、代理模式
二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标
方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑
的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调
用和打扰,同时让附加功能能够集中在一起也有利于统一维护
二、AOP概念及相关术语
2.1、概述
AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面
向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况
下给程序动态统一添加额外功能的一种技术
2.2相关术语
①横切关注点
从每个方法中抽取出来的同一类非核心业务。
②通知
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
③切面
封装通知方法的类。
④目标
被代理的目标对象。
⑤代理
向目标对象应用通知之后创建的代理对象。
⑥连接点
把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉
点就是连接点。
⑦切入点
定位连接点的方式。
2.3 作用
简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,
提高内聚性。
代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就
被切面给增强了。
三、基于注解的AOP
3.1、技术说明
动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因
为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最
终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。
3.2、准备工作
①添加依赖
在IOC所需依赖基础上再加入下面依赖即可:
<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
②准备被代理的目标资源
这一步就是创建接口以及实现类
3.3、创建切面类并配置
在切面中需要通过指定的注解将方法标识为通知方法
3.3.1、各种通知
@Before:前置通知,在目标对象方法执行之前执行
@After 后置通知,在目标对象方法的finally字句中执行
@AfterReturning返回通知 在目标对象方法返回值之后执行
@AfterThrowing 异常通知 在目标对象方法的catch子句中执行
1、前置通知
若想将方法标识为前置通知方法只需加上@Before便可
可以通过参数joinPoint获取对应方法的签名信息
签名信息
访问修饰符 参数名 返回值类型 方法名都是签名信息
@Before("pointcut()")
public void beforeAdviceMethod(JoinPoint joinPoint)
{
//获取连接点所对应方法的签名信息
Signature signature=joinPoint.getSignature();
//获取连接点所对应方法的参数
Object[] args= joinPoint.getArgs();
System.out.println("方法"+signature.getName()+"参数"+Arrays.toString(args));
}
2、后置通知
若想将方法标识为前置通知方法只需加上@After便可
@After("pointcut()")
public void afterAdviceMethod(JoinPoint joinPoint)
{ //获取连接点所对应方法的签名信息
Signature signature=joinPoint.getSignature();
System.out.println("方法"+signature.getName()+"执行完毕");
}
3、返回通知
若想将方法标识为前置通知方法只需加@上AfterReturning便可
在返回通知中若要获取目标对象的返回值 只需通过returning 就可以将通知方法的某个参数指定为接收目标独享方法的返回值的参数
简单来说就是Object x和returning = "x"他们的x相同就可以通过x得到返回值
@AfterReturning(value = "pointcut()",returning = "result")
public void afterReturningAdviceMethod(JoinPoint joinPoint ,Object result)
{
Signature signature=joinPoint.getSignature();
System.out.println("方法"+signature.getName()+"返回通知"+"结果:"+result);
}
4、异常通知
@AfterThrowing(value = "pointcut()",throwing ="ex" )
public void afterThrowingAdviceMethod(JoinPoint joinPoint,Throwable ex)
{
Signature signature=joinPoint.getSignature();
System.out.println("方法"+signature.getName()+"异常通知"+ex);
}
5、环绕通知
环绕通知的方法的返回值一定要和目标对象返回值一致
@Around("pointcut()")
public Object aroundAdviceMethod(ProceedingJoinPoint joinPoint)
{
Object result=null;
//表示目标对象方法的执行
try{
System.out.println("环绕通知--》前置通知");
result= joinPoint.proceed();
System.out.println("环绕通知--》返回通知");
}catch (Throwable throwable){
throwable.printStackTrace();
System.out.println("环绕通知--》前异常通知");
}finally {
System.out.println("环绕通知--》后置通知");
}
return result;
}
6、切入点表达式
例如
execution(public int com.atguigu.spring.aop.annotation.Calculator.add(int,int))
execution(星com.atguigu.spring.aop.annotation.CalculatorPureImpl.星(…))
第一个星表示任意的访问修饰符和返回值类型
第二个星表示类中任意的方法
。。表示任意的参数列表
类的地方也可以使用*,表示包下所有的类
切入点表达式的重用
@Pointcut标签 声明一个公共的切入点表达式
后面的@Before等可以直接使用@Before(“pointcut()”)的方式来写入表达式
3.3.3、切面的优先级
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
优先级高的切面:外面
优先级低的切面:里面
使用@Order注解可以控制切面的优先级:
@Order(较小的数):优先级高
@Order(较大的数):优先级低
3.3.4、aop的注意事项:
aop的注意事项:
切面类和目标类都要交给IOC容器管理
切面类必须通过@Aspect注解标识为一个切面
在Spring的配置文件中设置 <aop:aspectj-autoproxy/>开启基于注解的AOP
<context:component-scan base-package="com.atguigu.spring.aop.annotation"></context:component-scan>
开启基于注解的Aop
<aop:aspectj-autoproxy/>
四、声明式事务概念
编程式事务
事务功能的相关操作全部通过自己编写代码来实现
编程式的实现方式存在缺陷:
细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用
声明式事务
既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。
封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。
好处1:提高开发效率
好处2:消除了冗余的代码
好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化
所以,我们可以总结下面两个概念:
编程式:自己写代码实现功能
声明式:通过配置让框架实现功能
4.1基于注解的声明式事务
准备工作
①加入依赖
<!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.1</version>
</dependency>
<!-- Spring 持久化层支持jar包 -->
<!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个
jar包 -->
<!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.1</version>
</dependency>
<!-- Spring 测试相关 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.1</version>
</dependency>
<!-- junit测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<!-- 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.31</version>
</dependency>
</dependencies>
②创建jdbc.properties
jdbc.user=root
jdbc.password=atguigu
jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC
jdbc.driver=com.mysql.cj.jdbc.Driver
③配置Spring的配置文件
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="druidDataSource"></property>
</bean>
<!--配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="druidDataSource"></property>
</bean>
<!--开启事务注解驱动
将使用@Transactional注解所表示的方法或类中所有的方法使用事务进行管理
transaction-manager属性设置事务管理器的id
-->
<tx:annotation-driven transaction-manager="transactionManager"/>
④创建表
略
⑤创建组件
略
测试无事务情况
①创建测试类
②模拟场景
用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额
假设用户id为1的用户,购买id为1的图书
用户余额为50,而图书价格为80
购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段
此时执行sql语句会抛出SQLException
③观察结果
因为没有添加事务,图书的库存更新了,但是用户的余额没有更新
显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败
加入事务
1.在Spring的配置文件中添加配置:
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--
开启事务的注解驱动
通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务
-->
<!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就
是这个默认值,则可以省略这个属性 -->
<tx:annotation-driven transaction-manager="transactionManager" />
2.添加事务注解
因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层处理
在BookServiceImpl的buybook()添加注解@Transactional
③观察结果
由于使用了Spring的声明式事务,更新库存和更新余额都没有执行
@Transactional注解标识的位置
@Transactional标识在方法上,只会影响该方法
@Transactional标识的类上,会影响类中所有的方法
事务属性:只读
①介绍
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这
样数据库就能够针对查询操作来进行优化。
②使用方式
@Transactional(readOnly = true)
public void buyBook(Integer bookId, Integer userId) {
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
//System.out.println(1/0);
}
③注意
对增删改操作设置只读会抛出下面异常:
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification
are not allowed
事务属性:超时
①介绍
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间
占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。
此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常
程序可以执行。概括来说就是一句话:超时回滚,释放资源。
②使用方式
@Transactional(timeout = 3)
public void buyBook(Integer bookId, Integer userId) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
//System.out.println(1/0);
}
③观察结果
执行过程中抛出异常:
org.springframework.transaction.TransactionTimedOutException: Transaction timed out:
deadline was Fri Jun 04 16:25:39 CST 2022
事务属性:回滚策略
①介绍
声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
可以通过@Transactional中相关属性设置回滚策略
rollbackFor属性:需要设置一个Class类型的对象
rollbackForClassName属性:需要设置一个字符串类型的全类名
noRollbackFor属性:需要设置一个Class类型的对象
rollbackFor属性:需要设置一个字符串类型的全类名
②使用方式
@Transactional(noRollbackFor = ArithmeticException.class)
//@Transactional(noRollbackForClassName = “java.lang.ArithmeticException”)
public void buyBook(Integer bookId, Integer userId) {
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
System.out.println(1/0);
}
③观察结果
虽然购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当
出现ArithmeticException不发生回滚,因此购买图书的操作正常执行