概述

代理分两种技术,一种是jdk代理(机制就是反射,只对接口操作),一种就是字节码操作技术。前者不能算技术,后者算是新的技术。未来将有大的动作或者较为广泛的应用和变革,它可以实现代码自我的编码(人工智能,代码智能)。

什么是动态编程?动态编程解决什么问题?Java中如何使用?什么原理?如何改进?(需要我们一起探索,由于自己也是比较菜,一般深入不到这个程度)。

什么是动态编程

动态编程是相对于静态编程而言的,平时我们讨论比较多的就是静态编程语言,例如Java,与动态编程语言,例如JavaScript。那二者有什么明显的区别呢?简单的说就是在静态编程中,类型检查是在编译时完成的,而动态编程中类型检查是在运行时完成的。所谓动态编程就是绕过编译过程在运行时进行操作的技术,在Java中有如下几种方式:

先看看jvm class技术:

字节码改写:

一 、jdk 动态代理:

1.定义业务逻辑

public interface Service {  
    //目标方法 
    public abstract void add();  

 
public class UserServiceImpl implements Service {  
    public void add() {  
        System.out.println("This is add service");  
    }  
}
2.利用java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口定义代理类的实现。
class MyInvocatioHandler implements InvocationHandler {
    private Object target;
 
    public MyInvocatioHandler(Object target) {
        this.target = target;
    }
 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("-----before-----");
        Object result = method.invoke(target, args);
        System.out.println("-----end-----");
        return result;
    }
    // 生成代理对象
    public Object getProxy() {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        Class<?>[] interfaces = target.getClass().getInterfaces();
        return Proxy.newProxyInstance(loader, interfaces, this);
    }
}

3.使用动态代理
public class ProxyTest {
    public static void main(String[] args) {
        Service service = new UserServiceImpl();
        MyInvocatioHandler handler = new MyInvocatioHandler(service);
        Service serviceProxy = (Service)handler.getProxy();
        serviceProxy.add();
    }
}
执行结果:

-----before-----
This is add service
-----end-----

代理对象的生成过程由Proxy类的newProxyInstance方法实现,分为3个步骤:

1、ProxyGenerator.generateProxyClass方法负责生成代理类的字节码,生成逻辑比较复杂,了解原理继续分析源码 sun.misc.ProxyGenerator;

byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
2、native方法Proxy.defineClass0负责字节码加载的实现,并返回对应的Class对象。

Class clazz = defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);
3、利用clazz.newInstance反射机制生成代理类的对象;

使用 反编译工具 jad jad com.sun.proxy.$Proxy.1 看看代理类如何实现,反编译出来的java代码如下:

public final class $proxy1 extends Proxy implements Service {
 
    public $proxy1(InvocationHandler invocationhandler) {
        super(invocationhandler);
    }
 
    public final boolean equals(Object obj) {
        try {
            return ((Boolean)super.h.invoke(this, m1, new Object[] {
                obj
            })).booleanValue();
        }
        catch(Error _ex) { }
        catch(Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
 
    public final String toString() {
        try {
            return (String)super.h.invoke(this, m2, null);
        }
        catch(Error _ex) { }
        catch(Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
 
    public final void add() {
        try {
            super.h.invoke(this, m3, null);
            return;
        }
        catch(Error _ex) { }
        catch(Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
 
    public final int hashCode() {
        try {
            return ((Integer)super.h.invoke(this, m0, null)).intValue();
        }
        catch(Error _ex) { }
        catch(Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
 
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;
 
    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] {
                Class.forName("java.lang.Object")
            });
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m3 = Class.forName("zzzzzz.Service").getMethod("add", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
        }
        catch(NoSuchMethodException nosuchmethodexception) {
            throw new NoSuchMethodError(nosuchmethodexception.getMessage());
        }
        catch(ClassNotFoundException classnotfoundexception) {
            throw new NoClassDefFoundError(classnotfoundexception.getMessage());
        }
    }
}
从上述代码可以发现:

1、生成的$proxy1继承自Proxy类,并实现了Service接口。

2、执行代理对象的方法,其实就是执行InvocationHandle对象的invoke方法,传入的参数分别是当前代理对象,当前执行的方法和参数。

jdk动态代理使用的局限性

通过反射类Proxy和InvocationHandler回调接口实现的jdk动态代理,要求委托类必须实现一个接口,但事实上并不是所有类都有接口,对于没有实现接口的类,便无法使用该方方式实现动态代理。

二、字节码修改技术

了解该技术必然先了解Java class文件格式 《读《深入jvm原理》之class文件》

目前字节码修改技术有ASM,javassist等。cglib就是基于封装的Asm. Spring 就是使用cglib代理库。

ASM 是一个 Java 字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
不过ASM在创建class字节码的过程中,操纵的级别是底层JVM的汇编指令级别,这要求ASM使用者要对class组织结构和JVM汇编指令有一定的了解。

Java字节码生成开源框架介绍--Javassist:

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。

动态编译

动态编译是从Java 6开始支持的,主要是通过一个JavaCompiler接口来完成的。通过这种方式我们可以直接编译一个已经存在的java文件,也可以在内存中动态生成Java代码,动态编译执行。

调用JavaScript引擎
Java 6加入了对Script(JSR223)的支持。这是一个脚本框架,提供了让脚本语言来访问Java内部的方法。你可以在运行的时候找到脚本引擎,然后调用这个引擎去执行脚本。这个脚本API允许你为脚本语言提供Java支持。

动态生成字节码
这种技术通过操作Java字节码的方式在JVM中生成新类或者对已经加载的类动态添加元素。

动态编程解决什么问题
在静态语言中引入动态特性,主要是为了解决一些使用场景的痛点。其实完全使用静态编程也办的到,只是付出的代价比较高,没有动态编程来的优雅。例如依赖注入框架Spring使用了反射,而Dagger2 却使用了代码生成的方式(APT)。

例如 
1: 在那些依赖关系需要动态确认的场景: 
2: 需要在运行时动态插入代码的场景,比如动态代理的实现。 
3: 通过配置文件来实现相关功能的场景

Java中如何使用

此处我们主要说一下通过动态生成字节码的方式,其他方式可以自行查找资料。

操作java字节码的工具有两个比较流行,一个是ASM,一个是Javassit 。

ASM :直接操作字节码指令,执行效率高,要是使用者掌握Java类字节码文件格式及指令,对使用者的要求比较高。

Javassit 提供了更高级的API,执行效率相对较差,但无需掌握字节码指令的知识,对使用者要求较低。

应用层面来讲一般使用建议优先选择Javassit,如果后续发现Javassit 成为了整个应用的效率瓶颈的话可以再考虑ASM.当然如果开发的是一个基础类库,或者基础平台,还是直接使用ASM吧,相信从事这方面工作的开发者能力应该比较高。

上一张国外博客的图,展示处理Java字节码的工具的关系。

接下来介绍如何使用Javassit来操作字节码

Javassit使用方法

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。

Javassist中最为重要的是ClassPool,CtClass ,CtMethod 以及 CtField这几个类。

ClassPool:一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。

CtClass:表示一个类,这些CtClass对象可以从ClassPool获得。

CtMethods:表示类中的方法。

CtFields :表示类中的字段。

动态生成一个类
下面的代码会生成一个实现了Cloneable接口的类GenerateClass

public void DynGenerateClass() {
     ClassPool pool = ClassPool.getDefault();
     CtClass ct = pool.makeClass("top.ss007.GenerateClass");//创建类
     ct.setInterfaces(new CtClass[]{pool.makeInterface("java.lang.Cloneable")});//让类实现Cloneable接口
     try {
         CtField f= new CtField(CtClass.intType,"id",ct);//获得一个类型为int,名称为id的字段
         f.setModifiers(AccessFlag.PUBLIC);//将字段设置为public
         ct.addField(f);//将字段设置到类上
         //添加构造函数
         CtConstructor constructor=CtNewConstructor.make("public GeneratedClass(int pId){this.id=pId;}",ct);
         ct.addConstructor(constructor);
         //添加方法
         CtMethod helloM=CtNewMethod.make("public void hello(String des){ System.out.println(des);}",ct);
         ct.addMethod(helloM);

ct.writeFile();//将生成的.class文件保存到磁盘,从项目路径开始算起,如果为"com.test",那么在项目com.test包下会生成该类的class 文件

//下面的代码为验证代码
         Field[] fields = ct.toClass().getFields();
         System.out.println("属性名称:" + fields[0].getName() + "  属性类型:" + fields[0].getType());
     } catch (CannotCompileException e) {
         e.printStackTrace();
     } catch (IOException e) {
         e.printStackTrace();
     } catch (NotFoundException e) {
         e.printStackTrace();
     }
 }

上面的代码就会动态生成一个.class文件,我们使用反编译工具,例如Bytecode Viewer,查看生成的字节码文件GenerateClass.class,如下图所示。

动态添加构造函数及方法

有很多种方法添加构造函数,我们使用CtNewConstructor.make,他是一个的静态方法,其中有一个重载版本比较方便,如下所示。第一个参数是source text 类型的方法体,第二个为类对象。

CtConstructor constructor=CtNewConstructor.make("public GeneratedClass(int pId){this.id=pId;}",ct);
 ct.addConstructor(constructor);     
这段代码执行后会生成如下java代码,代码片段是使用反编译工具JD-GUI产生的,可以看到构造函数的参数名被修改成了paramInt。

public GeneratedClass(int paramInt)
  {
    this.id = paramInt;
  }

同样有很多种方法添加函数,我们使用CtNewMethod.make这个比较简单的形式

CtMethod helloM=CtNewMethod.make("public void hello(String des){ System.out.println(des);}",ct);
ct.addMethod(helloM);
1
这段代码执行后会生成如下java代码:

public void hello(String paramString)
  {
    System.out.println(paramString);
  }

动态修改方法体

动态的修改一个方法的内容才是我们关注的重点,例如在AOP编程方面,我们就会用到这种技术,动态的在一个方法中插入代码。 
例如我们有下面这样一个类

public class Point {
    private int x;
    private int y;

public Point(){}
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

public void move(int dx, int dy) {
        this.x += dx;
        this.y += dy;
    }
}

我们要动态的在内存中在move()方法体的前后插入一些代码

public void modifyMethod()
    {
        ClassPool pool=ClassPool.getDefault();
        try {
            CtClass ct=pool.getCtClass("top.ss007.Point");
            CtMethod m=ct.getDeclaredMethod("move");
            m.insertBefore("{ System.out.print(\"dx:\"+$1); System.out.println(\"dy:\"+$2);}");
            m.insertAfter("{System.out.println(this.x); System.out.println(this.y);}");

ct.writeFile();
            //通过反射调用方法,查看结果
            Class pc=ct.toClass();
            Method move= pc.getMethod("move",new Class[]{int.class,int.class});
            Constructor<?> con=pc.getConstructor(new Class[]{int.class,int.class});
            move.invoke(con.newInstance(1,2),1,2);
        }
        ...
    }

使用反编译工具查看修改后的move方法结果:

public void move(int dx, int dy) {
    System.out.print("dx:" + dx);System.out.println("dy:" + dy);
    this.x += dx;
    this.y += dy;
    Object localObject = null;//方法返回值
    System.out.println(this.x);System.out.println(this.y);
  }

可以看到,在生成的字节码文件中确实增加了相应的代码。 
函数输出结果为:

dx:1dy:2

Javassit 还有许多功能,例如在方法中调用方法,异常捕捉,类型强制转换,注解相关操作等,而且其还提供了字节码层面的API(Bytecode level API)。

什么原理
反射:由于Java执行过程中是将类型载入虚拟机中的,在运行时我们就可以动态获取到所有类型的信息。只能获取却不能修类型信息。 
动态编译与动态生成字节码:这两种方法比较相似,原理也都是利用了Java的设计原理,存在一个虚拟机执行字节码,这就使我们在此处有了改变字节码的操作空间。

总结
有关动态编程的知识在平时的应用层使用不是特别多,多是用在构建框架。例如Spring框架使用反射来构建,而用于AOP编程的动态代理则多是采用生成字节码的方式,例如JBoss,Spring中的AOP部分。了解这部分知识可以在日后遇到相关问题时比别人多一条思考的思路也是好的,做一个思路开阔的Developer。

java 动态代理 和动态编程的更多相关文章

  1. java静态代理与动态代理简单分析

    原创作品,可以转载,但是请标注出处地址http://www.cnblogs.com/V1haoge/p/5860749.html 1.动态代理(Dynamic Proxy) 代理分为静态代理和动态代理 ...

  2. java静态代理和动态代理(一)

    代理Proxy: Proxy代理模式是一种结构型设计模式,主要解决的问题是:在直接访问对象时带来的问题. 代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问.代理类负责为 ...

  3. java:java静态代理与动态代理简单分析

    java静态代理与动态代理简单分析 转载自:http://www.cnblogs.com/V1haoge/p/5860749.html 1.动态代理(Dynamic Proxy) 代理分为静态代理和动 ...

  4. JAVA静态代理和动态代理理解

    代理 代理是英文 Proxy 翻译过来的.我们在生活中见到过的代理,大概最常见的就是朋友圈中卖面膜的同学了. 她们从厂家拿货,然后在朋友圈中宣传,然后卖给熟人. 按理说,顾客可以直接从厂家购买产品,但 ...

  5. 深入浅出java静态代理和动态代理

    首先介绍一下.什么是代理: 代理模式,是经常使用的设计模式. 特征是.代理类与托付类有同样的接口,代理类主要负责为托付类预处理消息.过滤消息.把消息转发给托付类.以及事后处理消息. 代理类和托付类,存 ...

  6. 技术的正宗与野路子 c#, AOP动态代理实现动态权限控制(一) 探索基于.NET下实现一句话木马之asmx篇 asp.net core 系列 9 环境(Development、Staging 、Production)

    黄衫女子的武功似乎与周芷若乃是一路,飘忽灵动,变幻无方,但举手抬足之间却是正而不邪,如说周芷若形似鬼魅,那黄衫女子便是态拟神仙. 这段描写出自<倚天屠龙记>第三十八回. “九阴神抓”本是& ...

  7. java静态代理与动态代理

    原文链接:http://www.orlion.ga/207/ 一.代理模式 代理模式是经常用到的设计模式,代理模式是给指定对象提供代理对象.由代理对象来控制具体对象的引用. 代理模式涉及到的角色: 抽 ...

  8. Java静态代理与动态代理模式的实现

    前言:    在现实生活中,考虑以下的场景:小王打算要去租房,他相中了一个房子,准备去找房东洽谈相关事宜.但是房东他很忙,平时上班没时间,总找不到时间去找他,他也没办法.后来,房东想了一个办法,他找到 ...

  9. JAVA设计模式——代理(动态代理)

    传送门:JAVA设计模式——代理(静态代理) 序言: 在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring ...

随机推荐

  1. linux下配置LAMP开发环境,以及经常使用小细节

    本来安装没什么可说到.可是在linux其中easy会出现各种各样到问题. 我安装以后导致各种问题 比方php无法正常解析,数据库无法关闭,Apache无法开启等等........ 所以搞得我比較郁闷, ...

  2. Wps 2013 拼音标注两种方式分析

    Wps 2013 拼音标注两种方式分析 太阳火神的漂亮人生 (http://blog.csdn.net/opengl_es) 本文遵循"署名-非商业用途-保持一致"创作公用协议 转 ...

  3. [swift实战入门]手把手教你编写2048(一)

    苹果设备越来越普及,拿着个手机就想捣鼓点啥,于是乎就有了这个系列,会一步一步教大家学习swift编程,学会自己做一个自己的app,github地址:https://github.com/scarlet ...

  4. JDK和TOMCAT的安装与配置环境变量

    一.JDK该怎么安装与配置环境变量 步骤1.安装JDK选择安装目录,安装jdk1.8.0_77过程中会出现安装提示. 步骤2.(1)安装jdk随意选择目录 只需把默认安装目录\java之前的目录修改即 ...

  5. Java中的字节输入出流和字符输入输出流

    Java中的字节输入出流和字符输入输出流 以下哪个流类属于面向字符的输入流( ) A BufferedWriter B FileInputStream C ObjectInputStream D In ...

  6. neo4j 张一鸣 8

    头条关注 粉丝关系 张一鸣 8

  7. ip(点分十进制 <==> 二进制整数)之间的转换

    linux的套接字部分比较容易混乱,在这里稍微总结一下. 地址转换函数在地址的文本表达式和它们存放在套接字地址结构中的二进制值进行转换. 地址转换函数有四个:其中inet_addr 和 inet_nt ...

  8. POJ 2636:Electrical Outlets

    Electrical Outlets Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 9597   Accepted: 718 ...

  9. luogu3941入阵曲

    https://www.zybuluo.com/ysner/note/1301562 题面 统计在给出的\(n*m\)矩阵中,有多少个不同的子矩形中的数字之和是\(k\)的倍数? 解析 切不掉这道题是 ...

  10. Commons IO 2.5-IOUtils

    转自:http://blog.csdn.net/zhaoyanjun6/article/details/55051917 福利另外我已经把Commons IO 2.5的源码发布到Jcenter,大家就 ...