1. Spring AOP
AOP通常叫面向切面编程(Aspect-oriented Programming,简称AOP),它是一种编程范式,通过预编译的方式和运行期动态代理实现程序功能的统一维护的一种技术。
通常用来对隔离不同业务逻辑,比如常见的事务管理、日志管理等。同时实现AOP的方式也有两种:cglib 以及 jdk两种方式来实现。
1.1 AOP相关概念
JoinPoint:程序在执行流程中经过的一个个时间点,这个时间点可以是方法调用时,或者是执行方法中异常抛出时,也可以是属性被修改时等时机,在这些时间点上你的切面代码是可以(注意是可以但未必)被注入的
Pointcut:JoinPoints 只是切面代码可以被织入的地方,但我并不想对所有的 JoinPoint 进行织入,这就需要某些条件来筛选出那些需要被织入的 JoinPoint,Pointcut 就是通过一组规则(使用 AspectJ pointcut expression language 来描述) 来定位到匹配的 joinpoint
Advice:代码织入(也叫增强),Pointcut 通过其规则指定了哪些 joinpoint 可以被织入,而 Advice 则指定了这些 joinpoint 被织入(或者增强)的具体时机与逻辑,是切面代码真正被执行的地方,主要有五个织入时机。
- Before Advice:在JoinPoint执行前织入
- After Advice:在JoinPoint执行后织入(不管是否抛出异常都会织入)
- After returning advice:在JoinPoint执行正常退出后织入(抛出异常则不会织入)
- After throwing advice:在方法执行过程中抛出异常后织入
- Around Advice:这是所有 Advice 中最强大的,它在 JoinPoints 前后都可织入切面代码,也可以选择是否执行原有正常的逻辑,如果不执行原有流程,它甚至可以用自己的返回值代替原有的返回值,甚至抛出异常。
在这些 advice 里我们就可以写入切面代码了。综上所述,切面(Aspect)我们可以认为就是 pointcut 和 advice,pointcut 指定了哪些 joinpoint 可以被织入,而 advice 则指定了在这些 joinpoint 上的代码织入时机与逻辑。
AOP示例:
public interface TestService {
// 吃萝卜
void eatCarrot();
// 吃蘑菇
void eatMushroom();
// 吃白菜
void eatCabbage();
}
@Component
public class TestServiceImpl implements TestService {
@Override
public void eatCarrot() {
System.out.println("吃萝卜");
}
@Override
public void eatMushroom() {
System.out.println("吃蘑菇");
}
@Override
public void eatCabbage() {
System.out.println("吃白菜");
}
}
@Aspect
@Component
public class TestAdvice {
// 1. 定义 PointCut
@Pointcut("execution(* com.example.demo.api.TestServiceImpl.eatCarrot())")
private void eatCarrot(){}
// 2. 定义应用于 JoinPoint 中所有满足 PointCut 条件的 advice, 这里我们使用 around advice,在其中织入增强逻辑
@Around("eatCarrot()")
public void handlerRpcResult(ProceedingJoinPoint point) throws Throwable {
System.out.println("吃萝卜前洗手");
// 原来的 TestServiceImpl.eatCarrot 逻辑,可视情况决定是否执行
point.proceed();
System.out.println("吃萝后买单");
}
}
1.2 实现原理
1.2.1 静态代理
public interface Subject {
public void request();
}
public class RealSubject implements Subject {
@Override
public void request() {
// 卖房
System.out.println("卖房");
}
}
public class Proxy implements Subject {
private RealSubject realSubject;
public Proxy(RealSubject subject) {
this.realSubject = subject;
}
@Override
public void request() {
// 执行代理逻辑
System.out.println("卖房前");
// 执行目标对象方法
realSubject.request();
// 执行代理逻辑
System.out.println("卖房后");
}
public static void main(String[] args) {
// 被代理对象
RealSubject subject = new RealSubject();
// 代理
Proxy proxy = new Proxy(subject);
// 代理请求
proxy.request();
}
}
静态代理劣势:
- 代理类只代理一个委托类(其实可以代理多个,但不符合单一职责原则),也就意味着如果要代理多个委托类,就要写多个代理(别忘了静态代理在编译前必须确定)
- 第一点还不是致命的,再考虑这样一种场景:如果每个委托类的每个方法都要被织入同样的逻辑,比如说我要计算前文提到的每个委托类每个方法的耗时,就要在方法开始前,开始后分别织入计算时间的代码,那就算用代理类,它的方法也有无数这种重复的计算时间的代码
1.2.2 动态代理
为了解决静态代理带来的问题,需要使用动态代理。1.2.2.1 JDK动态代理
```java // 委托类 public class RealSubject implements Subject { @Override public void request() { // 卖房 System.out.println(“卖房”); } }
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy;
public class ProxyFactory {
private Object target;// 维护一个目标对象
public ProxyFactory(Object target) { this.target = target; }
// 为目标对象生成代理对象 public Object getProxyInstance() { return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("计算开始时间");
// 执行目标对象方法
method.invoke(target, args);
System.out.println("计算结束时间");
return null;
}
});
}
public static void main(String[] args) { RealSubject realSubject = new RealSubject(); System.out.println(realSubject.getClass()); Subject subject = (Subject) new ProxyFactory(realSubject).getProxyInstance(); System.out.println(subject.getClass()); subject.request(); } }
> 打印结果如下:
> ```shell
> 原始类:class com.example.demo.proxy.staticproxy.RealSubject
> 代理类:class com.sun.proxy.$Proxy0
> 计算开始时间
> 卖房
> 计算结束时间
```java
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h);
- loader: 代理类的ClassLoader,最终读取动态生成的字节码,并转成 java.lang.Class 类的一个实例(即类),通过此实例的 newInstance() 方法就可以创建出代理的对象
- interfaces: 委托类实现的接口,JDK 动态代理要实现所有的委托类的接口
- InvocationHandler: 委托对象所有接口方法调用都会转发到 InvocationHandler.invoke(),在 invoke() 方法里我们可以加入任何需要增强的逻辑 主要是根据委托类的接口等通过反射生成的
动态代理优点:
由于动态代理是程序运行后才生成的,哪个委托类需要被代理到,只要生成动态代理即可,避免了静态代理那样的硬编码,另外所有委托类实现接口的方法都会在 Proxy 的 InvocationHandler.invoke() 中执行,这样如果要统计所有方法执行时间这样相同的逻辑,可以统一在 InvocationHandler 里写, 也就避免了静态代理那样需要在所有的方法中插入同样代码的问题,代码的可维护性极大的提高了。
缺点:
注意第二个参数 Interfaces 是委托类的接口,是必传的, JDK 动态代理是通过与委托类实现同样的接口,然后在实现的接口方法里进行增强来实现的,这就意味着如果要用 JDK 代理,委托类必须实现接口,这样的实现方式看起来有点蠢,更好的方式是什么呢,直接继承自委托类不就行了,这样委托类的逻辑不需要做任何改动,CGlib 就是这么做的
1.2.2.2 CGLIB动态代理
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("目标类增强前!!!");
//注意这里的方法调用,不是用反射哦!!!
Object object = proxy.invokeSuper(obj, args);
System.out.println("目标类增强后!!!");
return object;
}
}
public class CGlibProxy {
public static void main(String[] args) {
//创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数
Enhancer enhancer = new Enhancer();
//设置目标类的字节码文件
enhancer.setSuperclass(RealSubject.class);
//设置回调函数
enhancer.setCallback(new MyMethodInterceptor());
//这里的creat方法就是正式创建代理类
RealSubject proxyDog = (RealSubject) enhancer.create();
//调用代理类的eat方法
proxyDog.request();
}
}
代理类:class com.example.demo.proxy.staticproxy.RealSubject$$EnhancerByCGLIB$$889898c5
目标类增强前!!!
卖房
目标类增强后!!!
public class RealSubject {
@Override
public void request() {
// 卖房
System.out.println("卖房");
}
}
/** 生成的动态代理类(简化版)**/
public class RealSubject$$EnhancerByCGLIB$$889898c5 extends RealSubject {
@Override
public void request() {
System.out.println("增强前");
super.request();
System.out.println("增强后");
}
}
2.Spring IOC
3. Spring事务
3.1 事务的特性
ACID
- 原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
- 一致性(Consistency):一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。
- 隔离性(Isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。
3.2 Spring事务的配置方式
编程式事务
声明式事务
3.3 事务的传播机制
事务的传播性一般用在事务嵌套的场景,比如一个事务方法里面调用了另外一个事务方法,那么两个方法是各自作为独立的方法提交还是内层的事务合并到外层的事务一起提交,这就是需要事务传播机制的配置来确定怎么样执行。
常用的事务传播机制如下:PROPAGATION_REQUIRED
Spring默认的传播机制,能满足绝大部分业务需求,如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行
- PROPAGATION_REQUES_NEW
该事务传播机制是每次都会新开启一个事务,同时把外层事务挂起,当前事务执行完毕,恢复上层事务的执行。如果外层没有事务,执行当前新开启的事务即可
- PROPAGATION_SUPPORT
如果外层有事务,则加入外层事务,如果外层没有事务,则直接使用非事务方式执行。完全依赖外层的事务
- PROPAGATION_NOT_SUPPORT
该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码
- PROPAGATION_NEVER
该传播机制不支持外层事务,即如果外层有事务就抛出异常
- PROPAGATION_MANDATORY
与NEVER相反,如果外层没有事务,则抛出异常
- PROPAGATION_NESTED
该传播机制的特点是可以保存状态保存点,当前事务回滚到某一个点,从而避免所有的嵌套事务都回滚,即各自回滚各自的,如果子事务没有把异常吃掉,基本还是会引起全部回滚的。
传播规则回答了这样一个问题:一个新的事务应该被启动还是被挂起,或者是一个方法是否应该在事务性上下文中运行。
3.4 事务的隔离级别
并发事务可能会引起的问题
- 脏读
- 不可重复读
- 幻读 | 隔离级别 | 含义 | | —- | —- | | ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 | | ISOLATION_READ_UNCOMMITTED | 允许读取尚未提交的更改。可能导致脏读、幻读或不可重复读。 | | ISOLATION_READ_COMMITTED | (Oracle 默认级别)允许从已经提交的并发事务读取。可防止脏读,但幻读和不可重复读仍可能会发生。 | | ISOLATION_REPEATABLE_READ | (MYSQL默认级别)对相同字段的多次读取的结果是一致的,除非数据被当前事务本身改变。可防止脏读和不可重复读,但幻读仍可能发生。 | | ISOLATION_SERIALIZABLE | 完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的。 |
3.5 只读
如果一个事务只对数据库执行读操作,那么该数据库就可能利用那个事务的只读特性,采取某些优化措施。通过把一个事务声明为只读,可以给后端数据库一个机会来应用那些它认为合适的优化措施。由于只读的优化措施是在一个事务启动时由后端数据库实施的, 因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、 ROPAGATION_NESTED)的方法来说,将事务声明为只读才有意义。
3.6 事务超时
为了使一个应用程序很好地执行,它的事务不能运行太长时间。因此,声明式事务的下一个特性就是它的超时。
假设事务的运行时间变得格外的长,由于事务可能涉及对数据库的锁定,所以长时间运行的事务会不必要地占用数据库资源。这时就可以声明一个事务在特定秒数后自动回滚,不必等它自己结束。
由于超时时钟在一个事务启动的时候开始的,因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、ROPAGATION_NESTED)的方法来说,声明事务超时才有意义。
3.7 回滚规则
在默认设置下,事务只在出现运行时异常(runtime exception)时回滚,而在出现受检查异常(checked exception)时不回滚(这一行为和EJB中的回滚行为是一致的)。
不过,可以声明在出现特定受检查异常时像运行时异常一样回滚。同样,也可以声明一个事务在出现特定的异常时不回滚,即使特定的异常是运行时异常。
3.7 实现原理
注:只要是以代理方式实现的声明式事务,无论是JDK动态代理,还是CGLIB直接写字节码生成代理,都只有public方法上的事务注解才起作用。而且必须在代理类外部调用才行,如果直接在目标类里面调用,事务照样不起作用。