javassist使用指南

Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口。Javaassist 就是一个用来 处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以去生成一个新的类对象,通过完全手动的方式。

1. 使用 Javassist 创建一个 class 文件

首先需要引入jar包:

<dependency>
 <groupId>org.javassist</groupId>
 <artifactId>javassist</artifactId>
 <version>3.25.0-GA</version>
</dependency>

编写创建对象的类:

package com.rickiyang.learn.javassist;

import javassist.*;

/**
 * @author rickiyang
 * @date 2019-08-06
 * @Desc
 */
public class CreatePerson {

 /**
 * 创建一个Person 对象
 *
 * @throws Exception
 */
 public static void createPseson() throws Exception {
 ClassPool pool = ClassPool.getDefault();

 // 1. 创建一个空类
 CtClass cc = pool.makeClass("com.rickiyang.learn.javassist.Person");

 // 2. 新增一个字段 private String name;
 // 字段名为name
 CtField param = new CtField(pool.get("java.lang.String"), "name", cc);
 // 访问级别是 private
 param.setModifiers(Modifier.PRIVATE);
 // 初始值是 "xiaoming"
 cc.addField(param, CtField.Initializer.constant("xiaoming"));

 // 3. 生成 getter、setter 方法
 cc.addMethod(CtNewMethod.setter("setName", param));
 cc.addMethod(CtNewMethod.getter("getName", param));

 // 4. 添加无参的构造函数
 CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
 cons.setBody("{name = \"xiaohong\";}");
 cc.addConstructor(cons);

 // 5. 添加有参的构造函数
 cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
 // $0=this / $1,$2,$3... 代表方法参数
 cons.setBody("{$0.name = $1;}");
 cc.addConstructor(cons);

 // 6. 创建一个名为printName方法,无参数,无返回值,输出name值
 CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
 ctMethod.setModifiers(Modifier.PUBLIC);
 ctMethod.setBody("{System.out.println(name);}");
 cc.addMethod(ctMethod);

 //这里会将这个创建的类对象编译为.class文件
 cc.writeFile("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");
 }

 public static void main(String[] args) {
 try {
  createPseson();
 } catch (Exception e) {
  e.printStackTrace();
 }
 }
}

执行上面的 main 函数之后,会在指定的目录内生成 Person.class 文件:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.rickiyang.learn.javassist;

public class Person {
 private String name = "xiaoming";

 public void setName(String var1) {
 this.name = var1;
 }

 public String getName() {
 return this.name;
 }

 public Person() {
 this.name = "xiaohong";
 }

 public Person(String var1) {
 this.name = var1;
 }

 public void printName() {
 System.out.println(this.name);
 }
}

跟咱们预想的一样。

在 Javassist 中,类 Javaassit.CtClass 表示 class 文件。一个 GtClass (编译时类)对象可以处理一个 class 文件,ClassPoolCtClass 对象的容器。它按需读取类文件来构造 CtClass 对象,并且保存 CtClass 对象以便以后使用。

需要注意的是 ClassPool 会在内存中维护所有被它创建过的 CtClass,当 CtClass 数量过多时,会占用大量的内存,API中给出的解决方案是 有意识的调用CtClassdetach()方法以释放内存。

ClassPool需要关注的方法:

  1. getDefault : 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool;
  2. appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
  3. toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
  4. get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑。

CtClass需要关注的方法:

  1. freeze : 冻结一个类,使其不可修改;
  2. isFrozen : 判断一个类是否已被冻结;
  3. prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
  4. defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
  5. detach : 将该class从ClassPool中删除;
  6. writeFile : 根据CtClass生成 .class 文件;
  7. toClass : 通过类加载器加载该CtClass。

上面我们创建一个新的方法使用了CtMethod类。CtMthod代表类中的某个方法,可以通过CtClass提供的API获取或者CtNewMethod新建,通过CtMethod对象可以实现对方法的修改。

CtMethod中的一些重要方法:

  1. insertBefore : 在方法的起始位置插入代码;
  2. insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
  3. insertAt : 在指定的位置插入代码;
  4. setBody : 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
  5. make : 创建一个新的方法。

注意到在上面代码中的:setBody()的时候我们使用了一些符号:

// $0=this / $1,$2,$3... 代表方法参数
cons.setBody("{$0.name = $1;}");

具体还有很多的符号可以使用,但是不同符号在不同的场景下会有不同的含义,所以在这里就不在赘述,可以看javassist 的说明文档。http://www.javassist.org/tutorial/tutorial2.html

2. 调用生成的类对象

(1). 通过反射的方式调用

上面的案例是创建一个类对象然后输出该对象编译完之后的 .class 文件。那如果我们想调用生成的类对象中的属性或者方法应该怎么去做呢?javassist也提供了相应的api,生成类对象的代码还是和第一段一样,将最后写入文件的代码替换为如下:

// 这里不写入文件,直接实例化
Object person = cc.toClass().newInstance();
// 设置值
Method setName = person.getClass().getMethod("setName", String.class);
setName.invoke(person, "cunhua");
// 输出值
Method execute = person.getClass().getMethod("printName");
execute.invoke(person);

然后执行main方法就可以看到调用了 printName方法。

(2). 通过读取 .class 文件的方式调用

ClassPool pool = ClassPool.getDefault();
// 设置类路径
pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");
CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person");
Object person = ctClass.toClass().newInstance();
// ...... 下面和通过反射的方式一样去使用

(3). 通过接口的方式

上面两种其实都是通过反射的方式去调用,问题在于我们的工程中其实并没有这个类对象,所以反射的方式比较麻烦,并且开销也很大。那么如果你的类对象可以抽象为一些方法得合集,就可以考虑为该类生成一个接口类。这样在newInstance()的时候我们就可以强转为接口,可以将反射的那一套省略掉了。

还拿上面的Person类来说,新建一个PersonI接口类:

package com.rickiyang.learn.javassist;

/**
 * @author rickiyang
 * @date 2019-08-07
 * @Desc
 */
public interface PersonI {

 void setName(String name);

 String getName();

 void printName();

}

实现部分的代码如下:

ClassPool pool = ClassPool.getDefault();
pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");

// 获取接口
CtClass codeClassI = pool.get("com.rickiyang.learn.javassist.PersonI");
// 获取上面生成的类
CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person");
// 使代码生成的类,实现 PersonI 接口
ctClass.setInterfaces(new CtClass[]{codeClassI});

// 以下通过接口直接调用 强转
PersonI person = (PersonI)ctClass.toClass().newInstance();
System.out.println(person.getName());
person.setName("xiaolv");
person.printName();

使用起来很轻松。

3. 修改现有的类对象

前面说到新增一个类对象。这个使用场景目前还没有遇到过,一般会遇到的使用场景应该是修改已有的类。比如常见的日志切面,权限切面。我们利用javassist来实现这个功能。

有如下类对象:

package com.rickiyang.learn.javassist;

/**
 * @author rickiyang
 * @date 2019-08-07
 * @Desc
 */
public class PersonService {

 public void getPerson(){
 System.out.println("get Person");
 }

 public void personFly(){
 System.out.println("oh my god,I can fly");
 }
}

然后对他进行修改:

package com.rickiyang.learn.javassist;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;

import java.lang.reflect.Method;

/**
 * @author rickiyang
 * @date 2019-08-07
 * @Desc
 */
public class UpdatePerson {

 public static void update() throws Exception {
 ClassPool pool = ClassPool.getDefault();
 CtClass cc = pool.get("com.rickiyang.learn.javassist.PersonService");

 CtMethod personFly = cc.getDeclaredMethod("personFly");
 personFly.insertBefore("System.out.println(\"起飞之前准备降落伞\");");
 personFly.insertAfter("System.out.println(\"成功落地。。。。\");");

 //新增一个方法
 CtMethod ctMethod = new CtMethod(CtClass.voidType, "joinFriend", new CtClass[]{}, cc);
 ctMethod.setModifiers(Modifier.PUBLIC);
 ctMethod.setBody("{System.out.println(\"i want to be your friend\");}");
 cc.addMethod(ctMethod);

 Object person = cc.toClass().newInstance();
 // 调用 personFly 方法
 Method personFlyMethod = person.getClass().getMethod("personFly");
 personFlyMethod.invoke(person);
 //调用 joinFriend 方法
 Method execute = person.getClass().getMethod("joinFriend");
 execute.invoke(person);
 }

 public static void main(String[] args) {
 try {
  update();
 } catch (Exception e) {
  e.printStackTrace();
 }
 }
}

personFly方法前后加上了打印日志。然后新增了一个方法joinFriend。执行main函数可以发现已经添加上了。

另外需要注意的是:上面的insertBefore() setBody()中的语句,如果你是单行语句可以直接用双引号,但是有多行语句的情况下,你需要将多行语句用{}括起来。javassist只接受单个语句或用大括号括起来的语句块。

以上就是javassist使用指南的详细内容,更多关于javassist使用的资料请关注我们其它相关文章!

(0)

相关推荐

  • Eclipse下Javassist正确使用方法代码解析

    这两天看到Hibernate的代理部分,第一反应是底层使用了反射,针对用户实体生成了代理类,后来反应过来了,反射没有任何可以产生新类的能力,也就顺理成章地找到了Javassist(下载地址). 在网上搜索到的大部分教程,都是针对Javassist的API进行一番讲解,但是最后,往往没有一个加载过程,而笔者模仿这些教程进行类的加载时,加载到的结果都是原来的类,并没有产生字节码被修改的内容. 在经过一番探索后,笔者发现,网上的大部分教程中的最后一步,保存字节码,使用的均是writeFile的无参数重

  • Javassist如何操作Java 字节码

    一.开篇 说起 AOP 小伙伴们肯定很熟悉,无论是 JDK 动态代理或者是 CGLIB 等,其底层都是通过操作 Java 字节码来实现代理.常用的一些操作字节码的技术有 ASM.AspectJ.Javassist 等. ASM 其设计和实现是尽可能小而且快,更专注于性能.它在指令的层面来操作,所以使用它需要对 JVM 的指令有所了解,门槛较高,CGLIB 就使用了 ASM 技术. AspectJ 扩展了 Java 语言,定义了一系列 AOP 语法,在 JVM 中运行需要使用特定的编译器生成遵守

  • Javassist之一秒理解java动态编程

    概述 什么是动态编程?动态编程解决什么问题?Java中如何使用?什么原理?如何改进?(需要我们一起探索,由于自己也是比较菜,一般深入不到这个程度). 什么是动态编程 动态编程是相对于静态编程而言的,平时我们讨论比较多的就是静态编程语言,例如Java,与动态编程语言,例如JavaScript.那二者有什么明显的区别呢?简单的说就是在静态编程中,类型检查是在编译时完成的,而动态编程中类型检查是在运行时完成的.所谓动态编程就是绕过编译过程在运行时进行操作的技术,在Java中有如下几种方式: 反射 这个

  • 基于javassist进行动态编程过程解析

    今天在研究dubbo时,发现一个新的知识点,可以使用javassist包进行动态编程,hibernate也使用该包进行编程.晚上百度了很多资料,将它的特性以代码的形式展现出来. package com.zhi.demo; import java.lang.reflect.Field; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import javassist.CtFi

  • javassist使用指南

    Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口.Javaassist 就是一个用来 处理 Java 字节码的类库.它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解.同时也可以去生成一个新的类对象,通过完全手动的方式. 1. 使用 Javassist 创建一个 class 文件 首先需要引入jar包: <dependency> <groupId>org.javassi

  • 浅谈vue中使用图片懒加载vue-lazyload插件详细指南

    在vue中使用图片懒加载详细指南,分享给大家.具体如下: 说明 当网络请求比较慢的时候,提前给这张图片添加一个像素比较低的占位图片,不至于堆叠在一块,或显示大片空白,让用户体验更好一点. 使用方式 使用vue的 vue-lazyload 插件 插件地址:https://www.npmjs.com/package/vue-lazyload 案例 demo: 懒加载案例demo Installation 安装方式 npm $ npm i vue-lazyload -D CDN CDN: https:

  • AngularJS 中的指令实践开发指南(一)

    指令(Directives)是所有AngularJS应用最重要的部分.尽管AngularJS已经提供了非常丰富的指令,但还是经常需要创建应用特定的指令.这篇教程会为你讲述如何自定义指令,以及介绍如何在实际项目中使用.在这篇文章的最后(第二部分),我会指导你如何使用Angular指令来创建一个简单的记事本应用. 概述 一个指令用来引入新的HTML语法.指令是DOM元素上的标记,使元素拥有特定的行为.举例来说,静态的HTML不知道如何来创建和展现一个日期选择器控件.让HTML能识别这个语法,我们需要

  • Swift、Objective-C、Cocoa混合编程设置指南

    Swift 被设计用来无缝兼容 Cocoa 和 Objective-C .在 Swift 中,你可以使用 Objective-C 的 API(包括系统框架和你自定义的代码),你也可以在 Objective-C中 使用 Swift 的 API.这种兼容性使 Swift 变成了一个简单.方便并且强大的工具集成到你的 Cocoa 应用开发工作流程中. 这篇指南包括了三个有关兼容性的重要方面方便你更好地利用来开发 Cocoa 应用: 互用性 使你将 Swift 和 Objective-C 相接合,允许在

  • 气象 XML 数据源应用程序开发指南-内容目录

    从今天开始解读由 weather.com® 提供的气象数据源(XML)以及对这些数据的应用.下面是所要讲的内容目录. 1. 内容目录 1.内容目录 2. 简介 2.1. 关于气象 XML 数据源 2.2. 获得气象 XML 数据源帐号 2.3. 应用程序类型  2.3.1. Web应用程序  2.3.2. 桌面应用程序   2.3.3. 其他应用程序3. 操作检查列表 4. 搜索 4.1. 请求   4.1.1. URLs   4.1.2. 查询参数4.2. 响应  4.2.1. DTD  4.

  • XML指南——XML元素

    XML元素是可以扩展的,它们之间有关联. XML元素有简单的命名规则. XML元素是可以扩展的 XML文档可以被扩展一边携带更多的信息. 请看下面的XML便条例子: <note> <to>Lin</to> <from>Ordm</from> <body>Don't forget me this weekend!</body> </note> 让我们来设想一个能够读取此XML文档的并能解读其中XML元素(<

  • Prototype最新版(1.5 rc2)使用指南(1)

    在写这个指南之前,先介绍一下Prototype主要是干吗的,如果你比较关注ajax/javascipt方面的应用,你应该早就听说过这个javascript framework. Prototype是一个基础的javascript应用框架,先引用一段官方网站的介绍 Prototype is a JavaScript framework that aims to ease development of dynamic web applications. Featuring a unique, eas

  • Prototype使用指南之base.js

    base.js中包含下面的内容  类的创建与继承: Class.create(): 创建一个类,例如 person=Class.create() Object.extend(destination, source): 把source中方法属性copy到destination(使用for propertyin source),需要注意的是,javascript中除了基本类型(Number, Boolean)外都是引用类型,所以这种copy一般只是copy引用而已,destination和sourc

  • 比较不错的函数式JavaScript编程指南教程

    你是否知道JavaScript其实也是一个函数式编程语言呢?本指南将教你如何利用JavaScript的函数式特性. 要求:你应当已经对JavaScript和DOM有了一个基本的了解. 写这篇指南的目的是因为关于JavaScript编程的资料太多了但是极少的资料提到了JavaScript的函数式特性.在本指南中,我只会讲解这些基本知识而不会深入其它的函数式语言或这是Lambda算子. 你可以点击所有的例子然后你所看到的代码就会被执行,这样就可以令指南变得具有交互性.你也可以使用这个沙箱来尝试. 第

  • angularJS 中$scope方法使用指南

    复制代码 代码如下: <!doctype html> <html> <head> <meta charset="utf-8"> <title>无标题文档</title> </head> <script src="http://localhost:81/js/jquery.js"> </script> <script src="http://lo

随机推荐