frankfan的胡思乱想

学海无涯,回头是岸

Java反射与动态代理

在实际工作中,有一种常用的设计模式「代理模式」,用来减少对类封装的修改。在面向对象的设计中,一个基本原则是「尽可能少或者不的改动原有的封装设计,而是在其基础上扩展或修改功能」,要达成这样的目的,可以是通过类的继承来实现,不过大量的类继承会给项目维护带来灾难(也就是说我们不应该依赖大量的继承机制来实现功能修改或者扩展的目标)

在这样的背景下,「代理模式」应运而生。

代理模式的设计非常简单,给每一个「原生类」配一个「代理类」,通过代理类来执行原生类的相关方法,在代理类中可以在原生方法的基础上增加或者修改实现。

//Subject.java
public interface Subject{
  public void doWork();
}
//Subject1.java
public class Subject1 implements Subject{
  
  @Override
  public void doWork(){
    //The subject1 work logic
  }
}
//这是一个「原生类」,我们可以直接调用这个类的doWork方法。但这里在实际业务中可能会面临1个问题。业务需求可能会变动,在某个场景下,业务要求Subject1对象调用doWork方法之前(或者之后)需要执行某个业务逻辑L(比如写入日志,比如埋点等等),此时面临的问题在于需要修改Subject1类的doWork方法,这破坏了类的封装性,甚至Subject1这个类属于第三方包,我们无权修改,这就导致了通过直接修改doWork方法的逻辑来企图响应业务逻辑调整的手段失效。

针对以上问题,我们可以使用一个中间层(「代理类」)来包裹住Subject1类,外界不直接与Subject1类通信,而是转而与Subject的中间层类通信,这个所谓的中间层就是所谓的Proxy(代理类)

//SubjectProxy.java
class SubjectProxy implements Subject{
  private Subject sub;
  Public SubjectProxy(Subject obj){
    sub = obj;
  }
  
  @Override
  Public void doWork(){
    //before doWork
    sub.doWork();
    //after doWork
  } 
}

class TestProxy{
  
  public static void main(String[] args){
    
     Subject1 subj1 = new Subject1();
		 SubjectProxy proxy = new SubjectProxy(subj1);  
		 proxy.doWork();
  }
}

//这样,我们通过new一个新的SubjectProxy对象,「包裹」Subject1对象,调用doWork方法时就能在Subject1类的doWork方法正式被调用前,或者调用后做一些逻辑执行,(也可以修改Subject1类的doWork方法返回值后再返回),这就是代理者模式

上面的代理模式似乎很好的解决了我们遇到的问题,但另一个随之而来的问题是「这太™麻烦了!」,这样的设计似乎每一个不同的interface实现类就需要创建一个新的与之对应的代理类,这使得组织结构复杂度直线上升,维护是个大问题!

那么,有没有一种方式,既能完成代理模式的功能,又不用反复的实现代理类呢?

这个问题的答案就是「动态代理

在代码编写动态代理类之前,我们首先讲解一下其基本逻辑:我们上面的代理实现核心问题在于SubjectProxy所代理的对象是通过外部手动传入的,并且在传入时就确定了被代理者,而如果我们需要一个代理类能代理不同的类,就需要事先在代理类内部确定好哪些类是需要被代理的,这显然不够灵活,而因此这也被称之为静态代理。

而所谓动态代理则意味着,我们实现好代理类后,我们并不关心被代理者是哪些,代理者内部通过动态生成「中间代理类」的方式,使用反射的机制来完成目标。

//ProxyHandler.java
class ProxyHandler implements InvocationHandler{

    private Object target;//被代理对象,只要是java类都行
    public Object bind(Object obj){
        this.target = obj;//传入被代理者
      	//我们在bind方法中传入被代理者的的类加载器,以及这个类所实现的接口(这用来在动态生成的代理类中再次实现这些接口方法)
        Object retObj = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),this);
        return retObj;//通过Proxy.newProxyInstance生成一个中间的代理实例(如果打印obj与retObj的值回发现其内存标号是一致的,但本质上是两个不同的值)
    }

  	//经过绑定后,生成的retObj的方法被调用时,invoke函数都会被调用,因为在动态生成的ProxyInvocation类中,动态生成的接口方法的实现中,本质都是在调用invoke方法,在本类(Proxyhandler)中实现了这个invoke方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			
        Object ret = null;
        System.out.println("before function called");
        ret = method.invoke(this.target,args);//在这里执行外界的真正逻辑,但是在执行这个方法之前和之后,我们都能处理自己的逻辑,ProxyHandle类只需实现一次,就能适配各种Java对象,因为它代理的是Object类型
        System.out.println("after function called");
        return ret;
    }
}
//TestProxyHandler.java
class TestProxyHandelr implements InvocationHandler{
  public static void main(String[] args){
    
    Subject1 s1 = new Subject1;
		ProxyHandler proxy = new ProxyHandler();
    Subject sProxy = (Subject)proxy.bind(s1);//不关心传入的被代理对象是何种类型
    
    sproxy.doWork();//proxy.bind方法返回的表面上是传入的s1对象,实际是动态生成的一个中间代理类,在ProxyHandler类的bind实现中,Proxy.newProxyInstance方法中,我们传入了被代理类的加载器以及被代理类所实现的接口方法,因此新生成的代理类中会动态的生成「代理.class」,在「代理.class」中会生成这些接口中发方法,因此,sproxy.doWork()本质就是在调用动态生成类里的doWork方法,而改方法被改写,在该方法中直接调用invoke方法,而ProxyHandler作为InvocationHandler接口的实现这,以及实现了invoke方法,所以执行sproxy.doWork()方法时,ProxyHandler类的invoke方法会被调用,这就是所谓的代理(或者hook)
  }
}
image.png

以上就是动态代理的基本实现和机制。

接着我们讨论一下这套机制可行的机理是什么。

在代理类ProxyHandler的方法bind中,调用了Proxy.newProxyInstance,往这个方法中传入了接口interfaces,那么,有了方法以及方法名后,怎么调用某个对象的某个方法呢?

这就涉及到Java中的「反射」的概念。

在Java中,通过反射(Reflection)可以在运行时获取某个实例(instance)属于哪个类(class),某个实例有哪些字段(field),有哪些方法(method),当然,拿到这些字段后,还可以直接给这些字段赋值。也能根据字符串(类名)动态的生成某个对象,然后根据字符串(方法名)获取到该类的某个方法,并调用。

可见Java中的反射(Reflection)功能很强大(不过效率也很低),而动态代理的实现则需要依靠反射的功能。

//Myflection.java
public class Myflection{
  public void sayHello(){ /**/}
  public void saySomething(String content){/**/}
  
  public static void main(String[] args){
    
    //根据名字生成实例对象
    Myflection mf = (Myflection)Class.forName("Myflection").getConstructor().newInstance();
    //根据名字获取方法
    Method method = Class.forName("Myflection").getMethod("sayHello",new Class[]{});
    method.invoke(n,null);//调用该方法

    //根据名字获取方法
    Method method2 = Class.forName("Myflection").getMethod("saySomething",new Class[]{String.class});
    method2.invoke(n,"content");//调用该方法
  }
}
//此外,通过实例对象也能获取到Class,拿到Class后,可以获取到该类的各种字段(公开或者私有),以及各种方法(公开或私有),在此不再赘述。

posted on 2021-12-28 00:08  shadow_fan  阅读(498)  评论(0)    收藏  举报

导航