Spring框架
两大核心:IOC AOP
此篇将要讲解第二个核心AOP
第三天:
发现问题:
设想转账的场景,一方账户钱-,另一方账户钱+,-的和+的值理应相当。这是正常情况。但如果在转出和转入语句之间加一句会让运行时报错的语句,此时数据库里的值:转账方钱-,收款方钱没变。
原因分析:
QueryRunner多例,即在每次执行操作(CRUD)时都会从数据源中获取一个连接。在上上图transfer方法中和数据库交互了4次,也即建立了4次连接。前三次都成功提交了,然后发生了异常导致第四次不能提交,故造成了数据库数据没有满足事务一致性的原因。
解决思路:
为了让一整件事中的所有连接一起成功一起失败,需要进行捆绑,由同一个Connection控制。
解决方案:
使用ThreadLocal对象把Connection和当前线程绑定,使得一个线程中只有一个能控制事务的对象。创建以下两个工具类:
负责绑定线程和连接的工具类:ConnectionUtils.java
package com.itheima.utils;
import javax.sql.DataSource;
import java.sql.Connection;
/**
* 连接的工具类,用于从数据源中获取连接,并且实现和线程的绑定
*/
public class ConnectionUtils {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
private DataSource dataSource;//在这里不new 等待spring注入
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 获取当前线程上的连接
* @return
*/
public Connection getThreadConnection(){
try {
//1.先从ThreadLocal上获取
Connection conn = tl.get();
//2.判断当前线程中有无连接
if(conn == null){
//3.若无,从数据源中获取一个连接,并且存入ThreadLocal中
conn = dataSource.getConnection();
tl.set(conn);
}
return conn;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 把连接和线程解绑
*/
public void removeConnection(){
tl.remove();
}
}
负责事务管理的工具类:TransactionManager.java
package com.itheima;
import com.itheima.utils.ConnectionUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import java.sql.SQLException;
/**
* 和事务管理相关的工具类,包含:开启事务、提交事务、回滚事务、释放连接
*/
public class TransactionManager {
private ConnectionUtils connectionUtils;//等待spring注入
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
/**
* 开启事务--前置
*/
public void beginTransaction(){
try {
connectionUtils.getThreadConnection().setAutoCommit(false);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
/**
* 提交事务--后置
*/
public void commitTransaction(){
try {
connectionUtils.getThreadConnection().commit();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
/**
* 回滚事务--异常
*/
public void rollbackTransaction(){
try {
connectionUtils.getThreadConnection().rollback();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
/**
* 释放连接--最终
*/
public void release(){
try {
connectionUtils.getThreadConnection().close();//还回连接池中
connectionUtils.removeConnection();//把连接和线程解绑
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//环绕通知
public Object aroundTxManager(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
Object[] args = pjp.getArgs();//得到方法执行所需要的参数
System.out.println("环绕通知-前置");
beginTransaction();
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("环绕通知-后置");
commitTransaction();
return rtValue;
} catch (Throwable throwable) {
System.out.println("环绕通知-异常");
rollbackTransaction();
throw new RuntimeException(throwable);
} finally {
System.out.println("环绕通知-最终");
release();
}
}
}
真正的事务控制应该写在业务层。
业务层 AccountServiceImpl.java 中:
1.注入事务管理工具类对象
2.在每个CRUD操作的方法中都要加上事务控制:开启事务-执行操作-提交事务-(返回结果)-(回滚操作)-释放连接
@Autowired
private TransactionManager txManager;
public List<Account> findAllAccount() {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
List<Account> accounts = accountDao.findAllAccount();
//3.提交事务
txManager.commitTransaction();
//4.返回结果
return accounts;
} catch (Exception e) {
//5.回滚操作
txManager.rollbackTransaction();
throw new RuntimeException(e);
} finally {
//6.释放连接
txManager.release();
}
}
此时运行,如果在钱入账语句前放一句会报异常的语句,则双方的账号前都不变,即不转出也不转入,事务一致性达到。
问题再发现:
但是每个方法中都有同样的1、3、5、6步骤的代码使得整个业务层太冗余了。而动态代理能解决这种同样的反复出现的代码。
【引出动态代理】
动态代理:生产商一开始负责生产以及销售和售后,直接和顾客交接忙不过来,后来有了经销商专门负责销售和售后,成为了顾客和生产商中间的桥梁。经销商就是代理。
动态代理:
* 特点:字节码随用随创建,随用随加载
* 作用:在不修改源码的基础上对方法增强
* 分类:基于接口的动态代理
* 基于子类的动态代理
* 基于接口的动态代理:
* 涉及的类:Proxy
* 提供者:JDK官方
* 如何创建代理对象:
* 使用Proxy类中的newProxyInstance方法
* 创建代理对象的要求:
* 被代理类最少实现一个接口,若没有则不能使用
* newProxyInstance方法的参数:
* ClassLoader:类加载器,用于加载代理对象字节码,和被代理对象使用相同的类加载器
* 固定写法,代理谁就是谁.getClass().getClassLoader()
* Class[]:字节码数组,用于让代理对象和被代理对象有相同的方法(即实现同一个接口)
* 固定写法,代理谁就是谁.getClass().getInterfaces()
* InvocationHandler:用于提供增强的代码,让我们写如何代理。
* 我们一般都是写一个该接口的实现类,通常情况下是匿名内部类,但不是必须的。
* 此接口的实现类是谁用谁写。
* 基于子类的动态代理:
* 涉及的类:Enhancer
* 提供者:第三方cglib库
* 如何创建代理对象:
* 使用Enhancer类中的create方法
* 创建代理对象的要求:
* 被代理类最不能是最终类
* newProxyInstance方法的参数:
* Class:字节码,用于指定被代理对象的字节码
* 固定写法,代理谁就是谁.getClass().getClassLoader()
* Callback:用于提供增强的代码,让我们写如何代理。
* 我们一般都是写一个该接口的实现类,通常情况下是匿名内部类,但不是必须的。
* 此接口的实现类是谁用谁写。
* 我们一般写的都是该接口的子接口实现类:MethodIntercepter
单独开一个例子讲解动态代理的两种类型:
现有Producer生产者 & Client消费者两个类,生产者负责saleProduct销售和afterSale售后功能,其中销售是通过经销商的,所以要分给经销商百分之20的利润。
1.基于接口的动态代理
前提:被代理类Product类实现了一个接口,以下是Client类中代码,用proxy增强。
IProducer proxyProducer = (IProducer)Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 作用:执行被代码对象的任何接口方法都会经过该方法。
* @param proxy 代理对象的引用
* @param method 当前执行的方法
* @param args 当前执行方法所需的参数
* @return 和被代理对象方法有相同的返回值
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())){
//消费者付的钱中:80%的钱归producer,20%归代理商(经销商)
returnValue = method.invoke(producer,money*0.8f);
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f);
2.基于子类的动态代理
前提:被代理类最不能是最终类
Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行被代理对象的任何方法都会经过该方法
*
* @param o
* @param method
* @param objects
* 以上三个参数和基于接口的动态代理中invoke方法的参数是一样的(proxy,method,args)
* @param methodProxy 当前执行方法的代理对象
* @return
* @throws Throwable
*/
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)objects[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())){
//消费者付的钱中:80%的钱归producer,20%归代理商(经销商)
returnValue = method.invoke(producer,money*0.8f);
}
return returnValue;
}
});
cglibProducer.saleProduct(1200f);
在项目中动态代理的优势:1.连接池,对connection的close()方法增强,加回到池里。2.request方法增强。
回到刚刚Account的项目中
刚刚提到每个CRUD方法都要写开启、提交、回滚、释放事务的代码过于冗余。可以用动态代理对每个方法增强。
解决:创建一个工厂类BeanFactory.java, 用于创建业务层的代理对象的工厂。这样一来,IAccountService类中的所有方法只需要完成业务逻辑,事务控制交给此工厂类。
好处:
1.达到分离的效果
2.提高代码可重用性,提高开发效率
3.便于维护
工厂类BeanFactory:
package com.itheima.factory;
import com.itheima.TransactionManager;
import com.itheima.service.IAccountService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 用于创建Service的代理对象的工厂
*/
public class BeanFactory {
private IAccountService accountService;//等待注入
public final void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}
private TransactionManager txManager;//等待spring注入
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
/**
* 获取Service代理对象
*
* @return
*/
public IAccountService getAccountService() {
return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
accountService.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 添加事务的支持
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rtValue = null;
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
rtValue = method.invoke(accountService, args);
//3.提交事务
txManager.commitTransaction();
//4.返回结果
return rtValue;
} catch (Exception e) {
//5.回滚事务
txManager.rollbackTransaction();
throw new RuntimeException(e);
} finally {
//6.释放连接
txManager.release();
}
}
});
}
}
AOP(Aspect Oriented Programming):面向切面编程
作用:在程序运行期间不修改源码,对已有方法增强。
实现:动态代理技术【上述】
Spring中的AOP是通过配置的方式【注解 & xml】实现上述的工厂代理类
AOP术语:
- JoinPoint(连接点)-- 业务层中所有的方法。
- Pointcut(切入点)-- 业务层中需要被增强的方法,有些不需要被增强的方法直接pass。【所有的切入点都是连接点】
- Advice(通知/增强)-- 拦截到连接点后要做的事务支持的事情。其中:开启事务->前置通知、提交事务->后置通知、回滚操作->异常通知、释放连接->最终通知,环绕通知:从前置到最终的整个过程。
- Target(目标对象)-- 被代理对象
- Weaving(织入)-- 加入事务支持的过程
- Proxy(代理)-- 一个类被AOP织入增强后,就产生一个结果代理类
- Aspect(切面)-- 切入点和通知的结合(配置对应关系)
Spring基于xml的AOP配置:
Spring中基于xml的aop配置步骤
1.把通知Bean也交给Spring来管理
2.使用aop:config标签表明开始aop配置
3.使用aop:aspect标签表明配置切面
id属性:是给切面提供一个唯一标识
ref属性:是指定通知类bean的id
4.在aop:aspect标签的内部使用对应的标签来配置通知的类型
aop:before:表示配置前置通知
method属性:用于指定logger类中哪个方法是前置通知
pointcut:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
切入点表达式的写法:
关键字:execution
表达式:访问修饰符 返回值 包名.包名.包名.....类名.方法名(参数)
标准写法:public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
【访问修饰符可省略】
返回值可以使用通配符表示任何返回值:
* com.itheima.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符 表示任意包 但是有几级包就需要几个*. :
* *.*.*.*.AccountServiceImpl.saveAccount()
包名可以使用..表示当前包及其子包:
* *..AccountServiceImpl.saveAccount()
类名和方法名都可以使用*来实现通配:
* *..*.*()
参数列表:
1.可以直接写数据类型:
基本类型直接写名称 int
引用类型写包名.类名的方式 java.lang.Spring
2.可以使用通配符表示任意类型,但是必须有参数
3.可以使用..表示有无参数均可,有参数可以是任意类型
全通配写法:(但是可能配多了 有些满足全通配写法的切入点表达式的方法不需要被增强也被增强了)
* *..*.*(..)
实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法: * com.itheima.service.impl.*.*(..)
在bean.xml配置文件中,
-
导入依赖:xmlns:aop
-
配置业务层对象
-
配置通知类
-
配置AOP:
配置切入点表达式(所有切面可用)
配置切面:关联事务控制对象
配置通知类型:关联通知方法与切入点方法(切入点表达式)
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置Spring的IOC,把Service对象配置进来-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
<!--1.配置Logger类-->
<bean id="logger" class="com.itheima.service.utils.Logger"></bean>
<!--2.配置aop-->
<aop:config>
<!--配置切入点表达式,id属性用于指定表达式的唯一标识,下面都可用id来指代对应的表达式,使得更加简洁。
此标签写在aop:aspect标签内部只能适用于当前切面使用
还可以写在aop:aspect外面,此时变成了所有切面可用,但是一定要放在所有aop:aspect之前写(以此种情况为例)-->
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知的类型 + 建立通知方法和切入点方法的关联-->
<!-- <aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>-->
<!-- <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>-->
<!-- <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>-->
<!-- <aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>-->
<!--配置环绕通知 详细注释请看logger类中-->
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
</beans>
通知类Logger.java
package com.itheima.service.utils;
import org.aspectj.lang.ProceedingJoinPoint;
public class Logger {
/**
* 前置通知
*/
public void beforePrintLog(){
System.out.println("前置通知->Logger类中的beforePrintLog方法开始记录日志了。");
}
/**
* 后置通知
*/
public void afterReturningPrintLog(){
System.out.println("后置通知->Logger类中的afterReturningPrintLog方法开始记录日志了。");
}
/**
* 异常通知
*/
public void afterThrowingPrintLog(){
System.out.println("异常通知->Logger类中的afterThrowingPrintLog方法开始记录日志了。");
}
/**
* 最终通知
*/
public void afterPrintLog(){
System.out.println("最终通知->Logger类中的afterPrintLog方法开始记录日志了。");
}
/**
* 环绕通知
* 问题:当配置了环绕通知之后,切入点方法没有执行,而通知方法执行了
* 分析:通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点调用,而此处代码中没有
* 解决:Spring框架为我们提供了一个接口:ProceedingJoinPoint,该接口有一个方法:Proceed(),该方法相当于明确调用切入点方法
* 该接口可以作为环绕通知的方法参数,在程序执行是spring框架会我们提供该接口的实现类供我们使用
* Spring中的环绕通知:
* 是框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
*
* 既可以配置通知,也可以在代码中写环绕通知。
*/
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
Object[] args = pjp.getArgs();//得到方法执行所需要的参数
System.out.println("环绕通知-前置->Logger类中的aroundPrintLog方法开始记录日志了。");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("环绕通知-后置->Logger类中的aroundPrintLog方法开始记录日志了。");
return rtValue;
} catch (Throwable throwable) {
System.out.println("环绕通知-异常->Logger类中的aroundPrintLog方法开始记录日志了。");
throw new RuntimeException(throwable);
} finally {
System.out.println("环绕通知-最终->Logger类中的aroundPrintLog方法开始记录日志了。");
}
}
}
Spring基于注解的AOP配置:
bean.xml:
只剩下以下两个,可以都用注解代替:(此时需要写在主配置类里)
@ComponentScan
@EnableTransactionManagement
但不想建配置类了,懒。
- 加入依赖:xmlns:context
- 扫包
- 配置spring开启注解的AOP支持
<?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"
xmlns:aop="http://www.springframework.org/schema/aop"
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 http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置Spring创建容器时要扫描的包-->
<context:component-scan base-package="com.itheima"></context:component-scan>
<!--配置spring开启注解的AOP支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
Logger类:
切记:@Before(“pt1()”)中的括号不能省!
package com.itheima.service.utils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;
@Component("logger")
@Aspect//表示当前类是切面类
public class Logger {
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void pt1(){}
// @Before("pt1()")
public void beforePrintLog(){
System.out.println("前置通知->Logger类中的beforePrintLog方法开始记录日志了。");
}
// @AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置通知->Logger类中的afterReturningPrintLog方法开始记录日志了。");
}
// @AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("异常通知->Logger类中的afterThrowingPrintLog方法开始记录日志了。");
}
// @After("pt1()")
public void afterPrintLog(){
System.out.println("最终通知->Logger类中的afterPrintLog方法开始记录日志了。");
}
@Around(("pt1()"))
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
Object[] args = pjp.getArgs();//得到方法执行所需要的参数
System.out.println("环绕通知-前置->Logger类中的aroundPrintLog方法开始记录日志了。");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("环绕通知-后置->Logger类中的aroundPrintLog方法开始记录日志了。");
return rtValue;
} catch (Throwable throwable) {
System.out.println("环绕通知-异常->Logger类中的aroundPrintLog方法开始记录日志了。");
throw new RuntimeException(throwable);
} finally {
System.out.println("环绕通知-最终->Logger类中的aroundPrintLog方法开始记录日志了。");
}
}
}