关于IOC与AOP的一些理解

1,237 阅读7分钟
原文链接: mrdear.cn


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


IOC#

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

处理循环依赖#

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

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方法或者反射直接注入到对应的成员变量上的依赖注入方式,如下所示:

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.到此循环依赖解决.

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

核心思想#

IOC的核心思想在于资源统一管理,你所持有的资源全部放入到IOC容器中,而你也只需要依赖IOC容器,该容器会自动为你装配所需要的具体依赖.

AOP#

先聊一聊动态代理的本质,动态代理得益于Java的类加载机制,内存中生成字节码,然后使用类加载器进行加载,之后实例化出来就是可以用的实例.下面对常见代理方式分析:

JDK的动态代理方式#

JDK的动态代理是基于ProxyInvocationHandler实现的,其中Proxy是拦截发生的地方,而InvocationHandler是发生调用的地方,创建动态代理方式如下:

Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{interfaceClass}, new InvokerInvocationHandler(interfaceClass));

JDK的动态代理只能应用于接口,本质原因是其动态生成一个extends Proxy implements yourInterface的代理类,如下所示,由于Java是单继承的存在,因此针对非接口的类是无法动态代理.
其代理方法也很简单,直接将所有操作都转向到对应的InvocationHandler,然后用户的InvocationHandler就可以收到相关调用信息,然后做出相关的AOP动作.

public final class $proxy4 extends Proxy implements IUserService {
    private static Method m1;
    private static Method m4;
    private static Method m2;
    private static Method m3;
    private static Method m0;
  
    public $proxy4(InvocationHandler var1) throws  {
      super(var1);
    }
  
    public final User findById(Long var1) throws  {
      try {
        return (User)super.h.invoke(this, m3, new Object[]{var1});
      } catch (RuntimeException | Error var3) {
        throw var3;
      } catch (Throwable var4) {
        throw new UndeclaredThrowableException(var4);
      }
    }
  ......
    public final int hashCode() throws  {
      try {
        return (Integer)super.h.invoke(this, m0, (Object[])null);
      } catch (RuntimeException | Error var2) {
        throw var2;
      } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
      }
    }
  .....
  }

Cglib的动态代理#

cglib的动态代理相比JDK方式其更加灵活,支持非final修饰的类,其使用的策略是继承,然后覆盖上层方法,并且自己生成一个转向上层的方法,在覆盖的方法中传入转向上层的方法.说的有点抽象,看下面的hashCode实现: 其中public final int hashCode()属于覆盖的方法,final int CGLIB$hashCode$2()是转向上层的方法,然后再MethodInterceptor调用时将CGLIB$hashCode$2作为Method参数传入,这样保证了调用时可以转向父类已有的方法.

public class IUserService?EnhancerByCGLIB?4ed3797 implements IUserService, Factory {
    ....
  private static final Callback[] CGLIB$STATIC_CALLBACKS;
  private MethodInterceptor CGLIB$CALLBACK_0;
  private static final Method CGLIB$hashCode$2$Method;
  private static final MethodProxy CGLIB$hashCode$2$Proxy;
  private static final Method CGLIB$findById$4$Method;
  private static final MethodProxy CGLIB$findById$4$Proxy;

  final int CGLIB$hashCode$2() {
    return super.hashCode();
  }

  public final int hashCode() {
    MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
    if (this.CGLIB$CALLBACK_0 == null) {
      CGLIB$BIND_CALLBACKS(this);
      var10000 = this.CGLIB$CALLBACK_0;
    }
    if (var10000 != null) {
      Object var1 = var10000.intercept(this, CGLIB$hashCode$2$Method, CGLIB$emptyArgs, CGLIB$hashCode$2$Proxy);
      return var1 == null ? 0 : ((Number)var1).intValue();
    } else {
      return super.hashCode();
    }
  }

  final User CGLIB$findById$4(Long var1) {
    return super.findById(var1);
  }

  public final User findById(Long var1) {
    MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
    if (this.CGLIB$CALLBACK_0 == null) {
      CGLIB$BIND_CALLBACKS(this);
      var10000 = this.CGLIB$CALLBACK_0;
    }
    return var10000 != null ? (User)var10000.intercept(this, CGLIB$findById$4$Method, new Object[]{var1}, CGLIB$findById$4$Proxy) : super.findById(var1);
  }
}

AOP的实现#

AOP是基于动态代理实现了一种无侵入式的代码扩展方式,与动态代理不同的是AOP的前提是已经存在了目标类的实例,因此在AOP要做的就是在目标类执行目标方法前后织入相应的操作,对于AOP的实现有两个很重要的接口:

  • MethodInvocation: AOP需要增强的那个方法的封装,其中包括被AOP的目标target,这个是为了解决嵌套问题所必须持有的对象.
  • MethodInterceptor: AOP的拦截,AOP相关操作一般在其内部完成.
    两者混合使用可以构造出如下结构:
    MethodInvocation是对HelloService.sayHello();的封装,而MethodInterceptor持有了MethodInvocation,在调用其之前进行了增强处理,这就是AOP的实质.

处理this#

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

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代理了.还有一点要理解,造成这个的原因是AOP要代理的那个类是实实在在存在的类,动态代理只是起到了方法调用的转发作用.

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

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;
  }
}

AOP嵌套问题#

动态代理之后会产生一个代理类,那么把这个类当成target,也就是AOP后要转向的真实类操作,封装后然后接着AOP,就实现了嵌套.本质上是一样的道理,既然都是实实在在的类,那么就可以一直嵌套下去,这样的嵌套一般会形成一个功能链,Mybatis的Plugin就是利用这种形式来实现的.
代码上的实现就是在MethodInvocation对象中存储着要转向的Object target,如果这个target是代理类,那么这个传递转向会向责任链一样一直传下去,直到遇到最初被AOP的真实类.

public class ReflectiveMethodInvocation implements MethodInvocation {

	private Object target;

	private Method method;

	private Object[] args;
      .....
	@Override
	public Object proceed() throws Throwable {
		return method.invoke(target, args);
	}
      ....
}

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