关于IOC与AOP的一些理解

最近在复习一些旧的知识,随着工作经验的增加,看待问题的眼光也在发生变化,重新谈谈对IOC与AOP新的理解.


IOC#

IOC叫做控制反转或者说是依赖注入,IOC提出的目的是解决项目中复杂的依赖关系,使用非硬编码的方式来扩展这些关系,简单来说就是为了解耦,在你需要的地方IOC容器会自动帮你注入对应的服务实例.
对于IOC来说一直存在循环依赖的难题,当A依赖B,B依赖C,C依赖A,彼此的依赖关系构成的是一个环形时,就是循环依赖,解决这种环形的依赖才是IOC的最关键的本质.(系统中出现循环依赖的话一不小心就掉进了死递归,因此尽可能避免循环依赖设计)

处理循环依赖#

构造注入
构造注入时利用构造函数在实例化类的时候注入需要参数的一种方式.对于构造注入的循环依赖如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A {
private B b;
// A的创建依赖B
public A(B b) {
this.b = b;
}
}

class B {
private A a;
// B的创建依赖A
public B(A a) {
this.a = a;
}
}

那么结果自然是死锁,A需要B才能实例化,B需要A才能实例化,系统中有没有两个类的实例,互相僵持就是死锁,无法解决循环依赖问题.

属性注入
属性注入是在类实例化之后,通过set方法或者反射直接注入到对应的成员变量上的依赖注入方式,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A {
private B b;

public A() {
}
// 实例化之后set方法注入B
public A setB(B b) {
this.b = b;
return this;
}
}

class B {
private A a;

public B() {
}
// 实例化之后set方法注入A
public B setA(A a) {
this.a = a;
return this;
}
}

与构造函数最大的不同点是去除了类的实例化对外部的强依赖关系,转而用代码逻辑保证这个强依赖的逻辑,比如属性注入失败直接抛异常让系统停止.那么此时的循环依赖解决办法就很简单了.

  • 做法1: 系统初始化时不考虑依赖关系把所有的Bean都实例化出来,然后依次执行属性注入,因为每个Bean都有实例,所以循环依赖不存在死锁.
  • 做法2: 按需实例化,实例化A,然后执行A的属性注入,发现依赖B,接着去实例化B,执行B的属性注入,此时A已经存在,那么B可以注入A,回到A的属性注入,拿到了B的实例,注入B.到此循环依赖解决.

回归本质,概括一下就是转变强依赖到弱依赖,把实例化与属性注入两个步骤分开来解决循环依赖的死锁.

AOP#

AOP是用动态代理实现了一种无侵入式的代码扩展方式.对于AOP来说有两个很重要的接口:

  • MethodInvocation: AOP需要增强的那个方法的封装
  • MethodInterceptor: AOP的所增强的操作.
    两者混合使用可以构造出如下结构:
    MethodInvocation是对HelloService.sayHello();的封装,而MethodInterceptor持有了MethodInvocation,在调用其之前进行了增强处理,这就是AOP的实质.

那么AOP之后再AOP怎么实现呢?
动态代理之后会产生一个代理类,对于上图就是HelloServiceProxy,那么把这个类当成HelloService,然后接着AOP,就实现了嵌套.本质上是一样的道理,既然都是实实在在的类,那么就可以一直嵌套下去,这样的嵌套一般会形成一个功能链,Mybatis的Plugin就是利用这种形式来实现的.

处理this#

假设HelloService被AOP增强,那么调用sayHello()时执行this.sayWorld()这行代码会走AOP处理吗?

1
2
3
4
5
6
7
8
9
10
11
12
public class HelloService {

public void sayHello() {
System.out.println("hello");
// 这里调用了本类的方法
this.sayWorld();
}

public void sayWorld() {
System.out.println("world");
}
}

答案当然是不会,由上图可以得知: 无论AOP怎么增强最终调用sayHello()这个方法的实例一定是HelloService,那么这里的this也一定是HelloService,既然这样肯定不会走AOP代理了.

解决办法也很简单,就是获取到代理类,然后再执行这个方法,对于Spring,可以从ApplicationContext中获取到当前的HelloService实例,这里获取到的自然是代理类,然后利用该实例调用sayWorld()就会走AOP代理了,大概形式如下,当然可以更好地封装下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class HelloService implements ApplicationContextAware {
private ApplicationContext applicationContext;

public void sayHello() {
System.out.println("hello");
// 这里拿到代理类后再执行
applicationContext.getBean("helloService", HelloService.class)
.sayWorld();
}

public void sayWorld() {
System.out.println("world");
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

Java学习记录--CAS操作分析
Spring MVC--所存在的疑惑