Javassist之一秒理解java动态编程
作者:ShuSheng007 发布时间:2023-11-09 09:36:33
概述
什么是动态编程?动态编程解决什么问题?Java中如何使用?什么原理?如何改进?(需要我们一起探索,由于自己也是比较菜,一般深入不到这个程度)。
什么是动态编程
动态编程是相对于静态编程而言的,平时我们讨论比较多的就是静态编程语言,例如Java,与动态编程语言,例如JavaScript。那二者有什么明显的区别呢?简单的说就是在静态编程中,类型检查是在编译时完成的,而动态编程中类型检查是在运行时完成的。所谓动态编程就是绕过编译过程在运行时进行操作的技术,在Java中有如下几种方式:
反射
这个搞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文件保存到磁盘
//下面的代码为验证代码
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);
这段代码执行后会生成如下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
2
4
Javassit 还有许多功能,例如在方法中调用方法,异常捕捉,类型强制转换,注解相关操作等,而且其还提供了字节码层面的API(Bytecode level API)。
什么原理
反射:由于Java执行过程中是将类型载入虚拟机中的,在运行时我们就可以动态获取到所有类型的信息。只能获取却不能修类型信息。
动态编译与动态生成字节码:这两种方法比较相似,原理也都是利用了Java的设计原理,存在一个虚拟机执行字节码,这就使我们在此处有了改变字节码的操作空间。
总结
有关动态编程的知识在平时的应用层使用不是特别多,多是用在构建框架。例如Spring框架使用反射来构建,而用于AOP编程的 * 则多是采用生成字节码的方式,例如JBoss,Spring中的AOP部分。了解这部分知识可以在日后遇到相关问题时比别人多一条思考的思路也是好的,做一个思路开阔的Developer。
来源:https://blog.csdn.net/ShuSheng0007/article/details/81269295


猜你喜欢
- IntelliJ IDEA中实现跟eclipse一样的outline方法,查看文件内所有已经声明的方法。mac的可以在key map 里搜索
- 本文实例为大家分享了Unity实现物体左右移动效果的具体代码,供大家参考,具体内容如下效果如下代码:using UnityEngine;us
- 在许多APP中,有的搜索框是一直固定的,有的呢,附加了很多的效果,就比如京东好吧,谁让京东那么厉害呢,不说了,开始 * !原理:就是自定义sc
- Maven 多profile及指定编译要点项目A依赖项目B,项目A、B都有对应的多个profile,通过mvn –P参数指定profile,
- 实现需求:两个线程交替打印1,0,打印10轮java多线程口诀:高内聚,低耦合线程操作资源类判断干活通知防止虚假唤醒方式一:使用synchr
- Fragment的产生与介绍Android运行在各种各样的设备中,有小屏幕的手机,超大屏的平板甚至电视。针对屏幕尺寸的差距,很多情况下,都是
- 在C语言中,函数名可以当做函数指针传递给形参从而实现回调void f1() { printf("f1()\n"); }v
- 本文主要包括以下内容:使用Xfermode设置圆角图片使用BitmapShader设置圆角图片滑动旋转缩放的bimp图片图片颜色处理(滑动)
- 1、事务介绍事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元
- 阶乘(Factorial)是个很有意思的函数,但是不少人都比较怕它,我们来看看两个与阶乘相关的问题: 1、 给定一个整数N,那么N的阶乘N!
- 一、概述简单理解为 异步消息插队并优先执行。场景:排队买票先来了一个普通用户来排队,买完票走了。后面又来了一个VIP用户A来买票 就一直站在
- where 子句用于指定类型约束,这些约束可以作为泛型声明中定义的类型参数的变量。1.接口约束。例如,可以声明一个泛型类 MyGeneric
- 1.栈和队列的共同特点是(只允许在端点处插入和删除元素)4.栈通常采用的两种存储结构是(线性存储结构和链表存储结构)5.下列关于栈的叙述正确
- 本文为大家分享了C#实现窗体全屏的具体代码,供大家参考,具体内容如下方法一:不过此方法有时候会出现莫名的bug//程序启动路径,与生成程序的
- 背景今天学习Springboot,但是用的apache-maven 3.0 ,导入springboot1.5.19 ,Maven项目老是爆红
- 本文为大家分享了java实现百度云OCR识别的具体代码,高精度OCR识别身份证信息,供大家参考,具体内容如下1.通用OCR文字识别这种OCR
- 有时,通过Runtime.getRuntime().exec()执行命令的有效负载有时会失败。使用Web Shell,反序列化利用或通过其他
- 方法一(推荐):设置datetimepicker的属性ShowCheckBox为true在窗口初始化时候,添加代码this.datetime
- 偶然机会看到一种对象初始的方式:// 新建一个列表,并赋值 "Harry","Tony","
- 本文实例为大家分享了Java实现图形界面计算器的具体代码,供大家参考,具体内容如下 代码:import javax.swing.*