Spring精华笔记3(动态代理、理解事务、AOP)
1.理解事务:
事务概念:* 如果一个包含多个步骤的业务操作,被事务管理,那么这些操作要么同时成功,要么同时失败。
关于事务的解决方案:
方案一:
如果我们单纯只想每一次进行一次增删改查的话,获取连接在用完之后会自动提交。
但是如果我们在业务层要进行组合操作,比如我们要进行转账操作:涉及到持久层的操作有(查询、更新)
eg:A给B转账
具体操作逻辑:
(1)查询操作:查A账户金额
(2)查询操作:查B账户金额
(3)操作账户:查到A账户后按要求给A账户减钱。对A账户的实体类进行操作。
(4)操作账户:查到B账户后按要求给B账户加钱。对B账户的实体类进行操作。
(5)更新操作:A账户更新操作
(6)更新操作:B账户更新操作
整个过程涉及到与是持久层交互的地方有:查询两次(A\B),更新两次(A/B)
每一次操作相当于都是获取一个新的连接,连接与连接之间并不相关。
问题是:
一旦在转账过程中发生某种异常中断,上一个连接已经转钱过来了,下一个连接转钱失败。
这样就会造成钱的总数不对的情况。有悖我们常理。
所以我们就需要事务管理。也即是,我们需要用一个连接处理这一连串的事情。而不是四个连接。
于是就想到使用ThreadLocal,它可以把连接和当前线程进行绑定,保证一个线程里只有一个控制事务的对象。
获取连接就从ThreadLocal来。
获取连接的过程:
详解:把连接装进ThreadLocal中,如果ThreadLocal里面有,就直接从ThreadLocal里面拿;如果ThreadLocal里面没有,就从数据源里获取连接,再把连接放进ThreadLocal中
/**
* 连接的工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定
*/
public class ConnectionUtils {
//一个装着连接的线程池
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
private DataSource dataSource;
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);
}
//4.返回当前线程上的连接
return conn;
}catch (Exception e){
throw new RuntimeException(e);
}
}
/**
* 把连接和线程解绑
*/
public void removeConnection(){
tl.remove();
}
}
再写一个事务管理的类,控制事务的开始、回滚、提交和释放。
接着在业务层,对每一个业务,都引入完整的事务管理,就拿我们的转账操作来说。
一开始开始业务、最后完成提交业务、中间有任何异常都回滚业务,最后释放。
保证了整个过程要成功一起成功,要失败一起失败。这样就不会有一方钱已经转出而因为中断操作,另一方收不到钱的bug。
但是这样做的一个弊端是:
业务层的代码非常的冗余,有着大量的重复代码(比如事务开始、事务提交、事务回滚、事务释放),并且这一部分的代码极度依赖于方法,也即是事务管理类那里方法名字一改,这边立刻就不能执行,耦合性太高。
方案二:
动态代理
1.动态代理:
特点:字节码随用随创建,随用随加载
作用:不需修改源码的基础上对方法进行加强
分类:基于接口的动态代理;基于子类的动态代理;
(1)基于接口的动态代理
①涉及的类:Proxy
提供者:JDK官方
②如何创建代理对象:使用Proxy中的newProxyInstance方法
③创建代理对象的要求:被代理类最少实现一个接口,如果没有则不能使用
④newProxyInstance方法参数:
-
ClassLoader:类加载器(它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器,固定写法)
-
class[]:字节码数组(它是用于让代理对象和被代理对象有相同方法。固定写法)(代理对象和被代理对象要实现相同接口)
-
InvocationHandler:用于提供增强的代码。它是让我们写如何代理。我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类,但不要是必须的。此接口的实现类是谁用谁写。
final Producer producer = new Producer();//匿名内部类访问外部成员变量,外部成员变量要求是最终的,要加final
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),//基于接口
producer.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 作用:执行被代理对象的任何接口方法都会经过该方法,该方法有拦截功能
* 方法参数的含义
* @param proxy 代理对象的引用
* @param method 当前执行的方法
* @param args 当前执行方法所需的参数
* @return 和被代理对象方法有相同的返回值
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//这个method就是下面 传进来的saleproduct,这个args就是下面传进来的参数,参数在这只有一个,也即从此处传进来拦截的方法和参数
//提供增强的代码
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())) {
// 方法执行
returnValue = method.invoke(producer, money*0.8f);//匿名内部类访问外部成员变量,外部成员变量要求是最终的,要加final method.invoke(producer, money*0.8f);,参数1,谁的方法,肯定是被代理对象的方法,后面第二个参数——方法 的参数。
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f);
}
}
基于接口的动态代理过程分析:
基于接口的动态代理是由jdkProxy类提供的,实现动态代理直接用这个类的newProxyInstance方法,方法中传入需要的参数,并用匿名内部类的方式完成加强。前两个参数是固定的,最后一个要自己写加强方法。这里要注意它的机制是:执行被代理对象的任何接口方法(每个被代理对象都一定要有一个接口,这是这种情况使用的前提)都会被动态代理的增强方法拦截下来,执行动态代理的方法。
问题:若没有接口就会报错。
执行被代理对象的任何接口方法都会经过该方法,该方法有拦截功能
(2)基于子类的动态代理
* 涉及的类:Enhancer
* 提供者:第三方cglib库
* 如何创建代理对象:
* 使用Enhancer类中的create方法
* 创建代理对象的要求:
* 被代理类不能是最终类
* create方法的参数:
* Class:字节码
* 它是用于指定被代理对象的字节码。
* Callback:用于提供增强的代码
* 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
* 此接口的实现类都是谁用谁写。
* 我们一般写的都是该接口的子接口实现类:MethodInterceptor
Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行被代理对象的任何方法都会经过该方法
* @param proxy
* @param method
* @param args
* 以上三个参数和基于接口的动态代理中invoke方法的参数是一样的
* @param methodProxy :当前执行方法的代理对象
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
cglibProducer.saleProduct(12000f);
}
}
2.AOP
一些概念:
a、开发阶段(我们做的)
编写核心业务代码(开发主线):大部分程序员来做,要求熟悉业务需求。
把公用代码抽取出来,制作成通知。(开发阶段最后再做): AOP 编程人员来做。
在配置文件中,声明切入点与通知间的关系,即切面。: AOP 编程人员来做。
b、运行阶段( Spring 框架完成的)
Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对
象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
<!-- 配置srping的Ioc,把service对象配置进来-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
<!--spring中基于XML的AOP配置步骤
1、把通知Bean也交给spring来管理
2、使用aop:config标签表明开始AOP的配置
3、使用aop:aspect标签表明配置切面
id属性:是给切面提供一个唯一标识
ref属性:是指定通知类bean的Id。
4、在aop:aspect标签的内部使用对应标签来配置通知的类型
我们现在示例是让printLog方法在切入点方法执行之前之前:所以是前置通知
aop:before:表示配置前置通知
method属性:用于指定Logger类中哪个方法是前置通知
pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
切入点表达式的写法:
关键字:execution(表达式)
表达式:
访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
标准的表达式写法:
public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
访问修饰符可以省略
void com.itheima.service.impl.AccountServiceImpl.saveAccount()
返回值可以使用通配符,表示任意返回值
* com.itheima.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*.
* *.*.*.*.AccountServiceImpl.saveAccount())
包名可以使用..表示当前包及其子包
* *..AccountServiceImpl.saveAccount()
类名和方法名都可以使用*来实现通配
* *..*.*()
参数列表:
可以直接写数据类型:
基本类型直接写名称 int
引用类型写包名.类名的方式 java.lang.String
可以使用通配符表示任意类型,但是必须有参数
可以使用..表示有无参数均可,有参数可以是任意类型
全通配写法:
* *..*.*(..)
实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法
* com.itheima.service.impl.*.*(..)
-->
<!-- 配置Logger类 -->
<bean id="logger" class="com.itheima.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:before>
</aop:aspect>
</aop:config>
</beans>
<!--配置AOP-->
<aop:config>
<!-- 配置切入点表达式 id属性用于指定表达式的唯一标识。expression属性用于指定表达式内容
此标签写在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="aroundPringLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
</beans>
/**
* 环绕通知
* 问题:
* 当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。
* 分析:
* 通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码中没有。
* 解决:
* Spring框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
* 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。
*
* spring中的环绕通知:
* 它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
*/
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");
return rtValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");
}
}
}