Java 动态代理 Proxy源码详解

代理简介?

我们知道nginx可以实现正向,反向代理。比如我们想请求服务中一个tomcat,一般就是直接访问机器的ip,如果是代理的话,就是先访问中间代理层(nginx),然后nignx跳转到我们的tomcat机器。代理模式也是如此,也有一个Proxy层,通过Proxy层来真正访问我们的类接口。

为什么要有代理?

我们先看nginx实现的代理,他可以事先为我们做很多ip黑名单过滤,负载均衡,权限,甚至我们还可以到代理层改变我们http接口信息。java的Proxy也是如此,可以在访问真正类的时候做一些前置和后置的统一工作。

JDK的动态Proxy实现

比如有个相亲代理机构,java程序员们通过这个代理机构来找老婆,java程序员有个需求,对象必须是A照杯 的才行,这些就可以统一交给相亲代理机构来做,自己等结果就行了。

代码实现

public class Main {
    // 相亲代理机构
    static class FindWomanProxy implements InvocationHandler{
        // 被代理的对象
        private FindWoman woman;
        public FindWomanProxy(FindWoman woman) {
            this.woman = woman ;
        }
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            //在真实的对象执行之前我们可以添加自己的操作
            System.out.println("相亲代理正在筛选A照杯的女士");
            // 筛选 完成 交给 程序员去消费相亲
            return method.invoke(woman, args);
        }
        
    }
    // 相亲 接口
    static interface FindWoman {
        public void find();
    }
    // java 程序员相亲
    static class JavaFindWoman implements FindWoman{
        @Override
        public void find() {
            System.err.println("java程序员相亲 ");
            
        }
    }
    public static void main(String[] args) {
        // 构造相亲代理 , 把java程序员传进去 
        FindWomanProxy proxy = new FindWomanProxy(new JavaFindWoman());
        // 机构产出的相亲代理对象,并非传入的 JavaFindWoman 。
        FindWoman findWoman = (FindWoman)Proxy.newProxyInstance(Main.class.getClassLoader(), JavaFindWoman.class.getInterfaces(), proxy);
        findWoman.find();
    }
}

代码解释: java程序员 通过相亲代理机构(FindWomanProxy) 去完成相亲这件事(FindWoman)。代理机构 产出代理对象,然后调用代理对象的相亲方法,会执行筛选A照杯 ,然后java程序员真正进行相亲。

代理的好处

以后PHP程序员相亲,只要在相亲机构中传入PHP程序员就行了。

FindWomanProxy proxy = new FindWomanProxy(new PhpFindWoman());

而且我们要更改 相亲机构 的 筛选 “照杯” 算法也很简单,统一就改了。还有一个很重要的好处:

发现没有,我们JavaFindWoman类是不是 很干净, 没有丝毫的 筛选A照杯 算法 代码。也就是说 可以 实现无侵入式的代码扩展

我很好奇,那个 Proxy.newProxyInstance 方法 是是怎么动态生成代理对象的?生成的代理对象字节码又是什么样子? 于是我继续进行研究。

Proxy.newProxyInstance 实现原理

通过调式jdk源码,发现了内部用了缓存来缓存生成的class,不是每一次都生成,最终生成class的代码在apply里面(缓存部分的我就不讲了)

 private static final class ProxyClassFactory
        implements BiFunction<ClassLoader, Class<?>[], Class<?>>
    {
        // 生成的class 前缀
        private static final String proxyClassNamePrefix = "$Proxy";
        // 名字的自增 标识
        private static final AtomicLong nextUniqueNumber = new AtomicLong();
        @Override
        public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
            Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
            for (Class<?> intf : interfaces) {
                Class<?> interfaceClass = null;
                try {
                   // 加载传入的接口 也就是我们上面的相亲接口  FindWoman.class 
                    interfaceClass = Class.forName(intf.getName(), false, loader);
                } catch (ClassNotFoundException e) {
                }
                if (interfaceClass != intf) {
                    throw new IllegalArgumentException(
                        intf + " is not visible from class loader");
                }
                if (!interfaceClass.isInterface()) {
                    // 不是接口抛出异常。 所以JDK只能代理接口
                    throw new IllegalArgumentException(
                        interfaceClass.getName() + " is not an interface");
                }
                if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                    throw new IllegalArgumentException(
                        "repeated interface: " + interfaceClass.getName());
                }
            }
            ///这段代码 就是为了产生 proxyPkg  包名
            String proxyPkg = null;     // package to define proxy class in
            int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
            for (Class<?> intf : interfaces) {
                int flags = intf.getModifiers();
                if (!Modifier.isPublic(flags)) {
                    accessFlags = Modifier.FINAL;
                    String name = intf.getName();
                    int n = name.lastIndexOf('.');
                    String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                    if (proxyPkg == null) {
                        proxyPkg = pkg;
                    } else if (!pkg.equals(proxyPkg)) {
                        throw new IllegalArgumentException(
                            "non-public interfaces from different packages");
                    }
                }
            }
            if (proxyPkg == null) {
                proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
            }
            long num = nextUniqueNumber.getAndIncrement();
            // proxyPkg 为 传入接口的 所在包名称
            //proxyClassNamePrefix  :  固定值  $Proxy
            // num : 自增
           // 生成的代理类名称 包名.$Proxy0
            String proxyName = proxyPkg + proxyClassNamePrefix + num;

            /*
             * sun.misc.ProxyGenerator 工具 生成类 的字节流
             */
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);
            try {
               // 将class字节流 加载到jvm , 从而生成class
                return defineClass0(loader, proxyName,
                                    proxyClassFile, 0, proxyClassFile.length);
            } catch (ClassFormatError e) {
                throw new IllegalArgumentException(e.toString());
            }
        }

        private static native Class<?> defineClass0(ClassLoader loader, String name,
                                                byte[] b, int off, int len);
    }

从上面看出 其实也是借助了 sun.misc.ProxyGenerator 工具 生成class字节流,然后通过native方法 defineClass0 加载到jvm生成class的。而且上面判断了只能代理接口。

上面拿到class 之后 ,然后通过反射,就构造出了代理对象了

 // cl  为上面产生的 class 
final Constructor<?> cons = cl.getConstructor(constructorParams);
 // 反射 构造实例
return cons.newInstance(new Object[]{h});

现在我觉得我最关注的就是这个class 里面内容到底是什么? jdk提供一个参数让我们把class的文件download出来

 public static void main(String[] args) throws IOException {
    	 //生成$Proxy0的class文件  
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");  
        // 构造相亲代理 , 把java程序员传进去 
        FindWomanProxy proxy = new FindWomanProxy(new JavaFindWoman());
        FindWoman findWoman = (FindWoman)Proxy.newProxyInstance(Main.class.getClassLoader(), JavaFindWoman.class.getInterfaces(), proxy);
        findWoman.find();
    }

sun.misc.ProxyGenerator.saveGeneratedFiles 设置为true后,会生成class文件
在这里插入图片描述
我们通过反编译工具 jd-gui 就可以看到 class内容

import debug_jdk8.;
import debug_jdk8.Main;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

final class Proxy0 extends Proxy implements Main.FindWoman {
  private static Method m1;
  
  private static Method m3;
  
  private static Method m2;
  
  private static Method m0;
  
  public Proxy0(InvocationHandler paramInvocationHandler) {
    super(paramInvocationHandler);
  }
  
  public final boolean equals(Object paramObject) {
    try {
      return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }
  
  public final void find() {
    try {
      this.h.invoke(this, m3, null);
      return;
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }
  
  public final String toString() {
    try {
      return (String)this.h.invoke(this, m2, null);
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }
  
  public final int hashCode() {
    try {
      return ((Integer)this.h.invoke(this, m0, null)).intValue();
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }
  
  static {
    try {
      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
      m3 = Class.forName("debug_jdk8.Main$FindWoman").getMethod("find", new Class[0]);
      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      return;
    } catch (NoSuchMethodException noSuchMethodException) {
      throw new NoSuchMethodError(noSuchMethodException.getMessage());
    } catch (ClassNotFoundException classNotFoundException) {
      throw new NoClassDefFoundError(classNotFoundException.getMessage());
    } 
  }
}

内部确实很用心, 把toString,hashCode,equals方法都代理了。也继承了 Proxy类,实现了我们的接口。Proxy类:

public class Proxy implements java.io.Serializable {
    protected InvocationHandler h;
    protected Proxy(InvocationHandler h) {
        this.h = h;
    }
    ...
}

我们就关注下我们代理 的 find 方法吧

 public final void find() {
    try {
     // h : InvocationHandler 也就是我们的相亲代理机构 FindWomanProxy 
     // m3 
      this.h.invoke(this, m3, null);
      return;
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }
 static {
    try {
       // 代理的接口的 find 方法 Method对象
      m3 = Class.forName("debug_jdk8.Main$FindWoman").getMethod("find", new Class[0]);
      return;
    } catch (NoSuchMethodException noSuchMethodException) {
      throw new NoSuchMethodError(noSuchMethodException.getMessage());
    } catch (ClassNotFoundException classNotFoundException) {
      throw new NoClassDefFoundError(classNotFoundException.getMessage());
    } 

也就是回调了我们的代理机构的 InvocationHandler 方法了

在这里插入图片描述
这样当调用代理对象的find 方法时 , 也就回调到上面这个红色的方法了。到此,面纱已经解开。我们在来自己实现一个简易的JDK动态Proxy。

自己实现JDK 动态Proxy

我们不用 sun.misc.ProxyGenerator 来生class了,我们用 Javassist 技术动态生成class。

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。它可以用来检查、”动态”修改以及创建 Java类。其功能与jdk自带的反射功能类似,但比反射功能更强大。

算了,下面还有很多内容要讲,我另起炉灶吧,要想自己实现一个JDK的动态代理,请您移步到这。

自己实现Java 动态代理 Proxy

强烈推荐一套Java进阶博客,都是干货,走向架构师不是梦!

Java进阶全套博客

基本原理: 代理服务器打开一个端口接收浏览器发来的访问某个站点的请求,从请求的字符串中解析出用户想访问哪个网页,让后通过URL对象建立输入流读取相应的网页内容,最后按照web服务器的工作方式将网页内容发送给用户浏览器 源程序: import java.net.*; import java.io.*; public class MyProxyServer { public static void main(String args[]) { try { ServerSocket ss=new ServerSocket(8080); System.out.println("proxy server OK"); while (true) { Socket s=ss.accept(); process p=new process(s); Thread t=new Thread(p); t.start(); } } catch (Exception e) { System.out.println(e); } } }; class process implements Runnable { Socket s; public process(Socket s1) { s=s1; } public void run() { String content=" "; try { PrintStream out=new PrintStream(s.getOutputStream()); BufferedReader in=new BufferedReader(new InputStreamReader(s.getInputStream())); String info=in.readLine(); System.out.println("now got "+info); int sp1=info.indexOf(' '); int sp2=info.indexOf(' ',sp1+1); String gotourl=info.substring(sp1,sp2); System.out.println("now connecting "+gotourl); URL con=new URL(gotourl); InputStream gotoin=con.openStream(); int n=gotoin.available(); byte buf[]=new byte[1024]; out.println("HTTP/1.0 200 OK"); out.println("MIME_version:1.0"); out.println("Content_Type:text/html"); out.println("Content_Length:"+n); out.println(" "); while ((n=gotoin.read(buf))>=0) { out.write(buf,0,n); } out.close(); s.close(); } catch (IOException e) { System.out.println("Exception:"+e); } } };
首先声明,这段源代码不是我编写的,让我们感谢这位名叫Carl Harris的大虾,是他编写了这段代码并将其散播到网上供大家学习讨论。这段代码虽然只是描述了最简单的proxy操作,但它的确是经典,它不仅清晰地描述了客户机/服务器系统的概念,而且几乎包括了Linux网络编程的方方面面,非常适合Linux网络编程的初学者学习。   这段Proxy程序的用法是这样的,我们可以使用这个proxy登录其它主机的服务端口。假如编译后生成了名为Proxy的可执行文件,那么命令及其参数的描述为:    ./Proxy   其中参数proxy_port是指由我们指定的代理服务器端口。参数remote_host是指我们希望连接的远程主机的主机名,IP地址也同样有效。这个主机名在网络上应该是唯一的,如果您不确定的话,可以在远程主机上使用uname -n命令查看一下。参数service_port是远程主机可提供的服务名,也可直接键入服务对应的端口号。这个命令的相应操作是将代理服务器的proxy_port端口绑定到remote_host的service_port端口。然后我们就可以通过代理服务器的proxy_port端口访问remote_host了。例如一台计算机,网络主机名是legends,IP地址为10.10.8.221,如果在我的计算机上执行:    [root@lee /root]#./proxy 8000 legends telnet   那么我们就可以通过下面这条命令访问legends的telnet端口。 ----------------------------------------------------------------- [root@lee /root]#telnet legends 8000 Trying 10.10.8.221... Connected to legends(10.10.8.221). Escape character is '^]' Red Hat Linux release 6.2(Zoot) Kernel 2.2.14-5.0 on an i686 Login: -----------------------------------------------------------------
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值