作者:小傅哥

博客:https://bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!

一、前言

到本章为止已经写了四篇关于字节码编程的内容,涉及了大部分的API方法。整体来说对 Javassist 已经有一个基本的使用认知。那么在 Javassist 中不仅提供了高级 API 用于创建和修改类、方法,还提供了低级 API 控制字节码指令的方式进行操作类、方法。

有了这样的 javassist API 在一些特殊场景下就可以使用字节码指令控制方法。

接下来我们通过字节码指令模拟一段含有自定义注解的方法修改和生成。在修改的过程中会将原有方法计算息费的返回值替换成 0,最后我们使用这样的技术去生成一段计算息费的方法。通过这样的练习学会字节码操作。

二、开发环境

  1. JDK 1.8.0
  2. javassist 3.12.1.GA
  3. 本章涉及源码在:itstack-demo-bytecode-1-05,可以关注公众号bugstack虫洞栈,回复源码下载获取。你会获得一个下载链接列表,打开后里面的第17个「因为我有好多开源代码」,记得给个Star

三、案例目标

  1. 使用指令码修改原有方法返回值
  2. 使用指令码生成一样的方法

测试方法

@RpcGatewayClazz(clazzDesc = "用户信息查询服务", alias = "api", timeOut = 500)
public class ApiTest { @RpcGatewayMethod(methodDesc = "查询息费", methodName = "interestFee")
public double queryInterestFee(String uId){
return BigDecimal.TEN.doubleValue(); // 模拟息费计算返回
} }
  • 这里使用的注解是测试中自定义的,模拟一个相当于网关接口的暴漏。

四、技术实现

1. 读取类自定义注解

ClassPool pool = ClassPool.getDefault();
// 类、注解
CtClass ctClass = pool.get(ApiTest.class.getName());
// 通过集合获取自定义注解
Object[] clazzAnnotations = ctClass.getAnnotations();
RpcGatewayClazz rpcGatewayClazz = (RpcGatewayClazz) clazzAnnotations[0];
System.out.println("RpcGatewayClazz.clazzDesc:" + rpcGatewayClazz.clazzDesc());
System.out.println("RpcGatewayClazz.alias:" + rpcGatewayClazz.alias());
System.out.println("RpcGatewayClazz.timeOut:" + rpcGatewayClazz.timeOut());
  • ctClass.getAnnotations(),可以获取所有的注解,进行操作

输出结果:

RpcGatewayClazz.clazzDesc:用户信息查询服务
RpcGatewayClazz.alias:api
RpcGatewayClazz.timeOut:500

2. 读取方法的自定义注解

CtMethod ctMethod = ctClass.getDeclaredMethod("queryInterestFee");
RpcGatewayMethod rpcGatewayMethod = (RpcGatewayMethod) ctMethod.getAnnotation(RpcGatewayMethod.class);
System.out.println("RpcGatewayMethod.methodName:" + rpcGatewayMethod.methodName());
System.out.println("RpcGatewayMethod.methodDesc:" + rpcGatewayMethod.methodDesc());
  • 在读取方法自定义注解时,通过的是注解的 class 获取的,这样按照名称可以只获取最需要的注解名称。

输出结果:

RpcGatewayMethod.methodName:interestFee
RpcGatewayMethod.methodDesc:查询息费

3. 读取方法指令码

MethodInfo methodInfo = ctMethod.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
CodeIterator iterator = codeAttribute.iterator();
while (iterator.hasNext()) {
int idx = iterator.next();
int code = iterator.byteAt(idx);
System.out.println("指令码:" + idx + " > " + Mnemonic.OPCODE[code]);
}
  • 这里的指令码就是一个方法编译后在 JVM 执行的操作流程。

输出结果:

指令码:0 > getstatic
指令码:3 > invokevirtual
指令码:6 > dreturn

4. 通过指令修改方法

ConstPool cp = methodInfo.getConstPool();
Bytecode bytecode = new Bytecode(cp);
bytecode.addDconst(0);
bytecode.addReturn(CtClass.doubleType);
methodInfo.setCodeAttribute(bytecode.toCodeAttribute());
  • addDconst,将 double 型0推送至栈顶
  • addReturn,返回 double 类型的结果

此时的方法的返回值已经被修改,下面的是新的 class 类;

@RpcGatewayClazz(
clazzDesc = "用户信息查询服务",
alias = "api",
timeOut = 500L
)
public class ApiTest {
public ApiTest() {
} @RpcGatewayMethod(
methodDesc = "查询息费",
methodName = "interestFee"
)
public double queryInterestFee(String var1) {
return 0.0D;
}
}
  • 可以看到查询息费的返回结果已经是 0.0D。如果你的程序被这样操作,那么还是很危险的。所以有时候会进行一些混淆编译,降低破解风险。

5. 使用指令码生成方法

5.1 创建基础方法信息

ClassPool pool = ClassPool.getDefault();
// 创建类信息
CtClass ctClass = pool.makeClass("org.itstack.demo.javassist.HelloWorld");
// 添加方法
CtMethod mainMethod = new CtMethod(CtClass.doubleType, "queryInterestFee", new CtClass[]{pool.get(String.class.getName())}, ctClass);
mainMethod.setModifiers(Modifier.PUBLIC);
MethodInfo methodInfo = mainMethod.getMethodInfo();
ConstPool cp = methodInfo.getConstPool();
  • 创建类和方法的信息在我们几个章节中也经常使用,主要是创建方法的时候需要传递;返回类型、方法名称、入参类型,以及最终标记方法的可访问量。

5.2 创建类使用注解

// 类添加注解
AnnotationsAttribute clazzAnnotationsAttribute = new AnnotationsAttribute(cp, AnnotationsAttribute.visibleTag);
Annotation clazzAnnotation = new Annotation("org/itstack/demo/javassist/RpcGatewayClazz", cp);
clazzAnnotation.addMemberValue("clazzDesc", new StringMemberValue("用户信息查询服务", cp));
clazzAnnotation.addMemberValue("alias", new StringMemberValue("api", cp));
clazzAnnotation.addMemberValue("timeOut", new LongMemberValue(500L, cp));
clazzAnnotationsAttribute.setAnnotation(clazzAnnotation);
ctClass.getClassFile().addAttribute(clazzAnnotationsAttribute);
  • AnnotationsAttribute,创建自定义注解标签
  • Annotation,创建实际需要的自定义注解,这里需要传递自定义注解的类路径
  • addMemberValue,用于添加自定义注解中的值。需要注意不同类型的值 XxxMemberValue 前缀不一样;StringMemberValueLongMemberValue
  • setAnnotation,最终设置自定义注解。如果不设置,是不能生效的。

5.3 创建方法注解

// 方法添加注解
AnnotationsAttribute methodAnnotationsAttribute = new AnnotationsAttribute(cp, AnnotationsAttribute.visibleTag);
Annotation methodAnnotation = new Annotation("org/itstack/demo/javassist/RpcGatewayMethod", cp);
methodAnnotation.addMemberValue("methodName", new StringMemberValue("查询息费", cp));
methodAnnotation.addMemberValue("methodDesc", new StringMemberValue("interestFee", cp));
methodAnnotationsAttribute.setAnnotation(methodAnnotation);
methodInfo.addAttribute(methodAnnotationsAttribute);
  • 设置类的注解与设置方法的注解,前面的内容都是一样的。唯独需要注意的是方法的注解,需要设置到方法的;addAttribute 上。

5.4 字节码编写方法快

// 指令控制
Bytecode bytecode = new Bytecode(cp);
bytecode.addGetstatic("java/math/BigDecimal", "TEN", "Ljava/math/BigDecimal;");
bytecode.addInvokevirtual("java/math/BigDecimal", "doubleValue", "()D");
bytecode.addReturn(CtClass.doubleType);
methodInfo.setCodeAttribute(bytecode.toCodeAttribute());
  • Javassist 中的指令码通过,Bytecode 的方式进行添加。基本所有的指令你都可以在这里使用,它有非常强大的 API
  • addGetstatic,获取指定类的静态域, 并将其压入栈顶
  • addInvokevirtual,调用实例方法
  • addReturn,从当前方法返回double
  • 最终讲字节码添加到方法中,也就是会变成方法体。

5.5 添加方法信息并输出

// 添加方法
ctClass.addMethod(mainMethod); // 输出类信息到文件夹下
ctClass.writeFile();
  • 这部分内容就比较简单了,也是我们做 Javassist 字节码开发常用的内容。添加方法和输出字节码编程后的类信息。

5.6 最终创建的类方法

@RpcGatewayClazz(
clazzDesc = "用户信息查询服务",
alias = "api",
timeOut = 500L
)
public class HelloWorld {
@RpcGatewayMethod(
methodName = "查询息费",
methodDesc = "interestFee"
)
public double queryInterestFee(String var1) {
return BigDecimal.TEN.doubleValue();
} public HelloWorld() {
}
}

五、总结

  • 本章节我们看到字节码编程不只可以像以前使用强大的api去直接编写代码,还可以向方法中添加指令,控制方法。这样就可以非常方便的处理一些特殊场景。例如 TryCatch 中的开始位置。
  • 关于 javassist 字节码编程本身常用的方法基本已经覆盖完成,后续会集合 JavaAgent 做一些案例汇总,将知识点与实际场景进行串联。
  • 学习终究还是要成体系的系统化深入学习,只言片语有的内容不能很好的形成一个技术栈的闭环,也不利于在项目中实战。

字节码编程,Javassist篇五《使用Bytecode指令码生成含有自定义注解的类和方法》的更多相关文章

  1. 字节码编程,Javassist篇三《使用Javassist在运行时重新加载类「替换原方法输出不一样的结果」》

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 通过前面两篇 javassist 的基本内容,大体介绍了:类池(ClassPool) ...

  2. 字节码编程,Javassist篇二《定义属性以及创建方法时多种入参和出参类型的使用》

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 在上一篇 Helloworld 中,我们初步尝试使用了 Javassist字节编程的 ...

  3. 字节码编程,Javassist篇一《基于javassist的第一个案例helloworld》

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 目录 @ 目录 目录 一.前言 二.开发环境 三.案例目标 四.技术实现 五.测试结果 1. ...

  4. 字节码编程,Byte-buddy篇一《基于Byte Buddy语法创建的第一个HelloWorld》

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 相对于小傅哥之前编写的字节码编程: ASM.Javassist 系列,Byte Bu ...

  5. 字节码操作JAVAssist

    字节码操作Javassist 字节码:字节码是设计被用来将代码高效的传送给多种软件平台.硬件平台,字节码的设计也实现了Java的平台无关性,字节码比机器码更抽象,它通常被认为是包含了一个可执行文件的二 ...

  6. [19/04/20-星期六] Java的动态性_字节码操作(Javassist类库(jar包),assist:帮助、援助)

    一.概念 [基本] /** * */ package cn.sxt.jvm; import javassist.ClassPool; import javassist.CtClass; import ...

  7. 动态字节码技术Javassist

    字节码技术可以动态改变某个类的结构(添加/删除/修改  新的属性/方法) 关于字节码的框架有javassist,asm,bcel等 引入依赖 <dependency> <groupI ...

  8. Javassist字节码增强示例

    概述 Javassist是一款字节码编辑工具,可以直接编辑和生成Java生成的字节码,以达到对.class文件进行动态修改的效果.熟练使用这套工具,可以让Java编程更接近与动态语言编程. 下面一个方 ...

  9. 5.Dubbo原理解析-代理之Javassist字节码技术生成代理 (转)

    转载自  斩秋的专栏  http://blog.csdn.net/quhongwei_zhanqiu/article/details/41597219 JavassistProxyFactory:利用 ...

  10. Dubbo原理实现之使用Javassist字节码结束构建代理对象

    JavassistProxyFactory利用自己吗技术构建代理对象的实现如下: public <T> T getProxy(Invoker<T> invoker, Class ...

随机推荐

  1. spring管理实务有几种方式

    一:事务认识 大家所了解的事务Transaction,它是一些列严密操作动作,要么都操作完成,要么都回滚撤销.Spring事务管理基于底层数据库本身的事务处理机制.数据库事务的基础,是掌握Spring ...

  2. 打造绿色城市,数字孪生天然气站 3D 可视化

    前言 天然气作为常见的燃料,与我们的生活息息相关,而对于这种燃料的存储与传输,对于天然气供应站有着严格的要求.燃气企业一般都会将点供站设计为无人值守站,而往往在运营管理过程中,对点供站的安全管理尤为重 ...

  3. 我让 ChatGPT 化身为全知全能的文档小助理,啥姿势她都会......

    ChatGPT 虽然只是一个对话型人工智能,但已经震惊了全世界,有人甚至认为人工智能的奇点已经到来.未来一定会有很多人失业,从工业革命开始,每出现一次重大的技术变革,就必然会有一批人失业,我们要直面现 ...

  4. freeswitch的mod_xml_cdr模块

    概述 freeswitch是一款简单好用的VOIP开源软交换平台. 在语音呼叫的过程中,话单是重要的计价和结算依据,话单的产生需要稳定可靠,可回溯. fs中的mod_xml_cdr模块提供了基本话单功 ...

  5. C# ,使用“^”运算符对数字加密

    原理:"异或"运算符"^", 用于比较两个二进制数的响应位.计算过程如果两个二进制数的相应位都为1或两个二进制数的相应位都为0,则返回0:如果两个二进制数的相应 ...

  6. 关闭 cockpit 登陆提示

    sudo rm /etc/issue.d/cockpit.issue sudo rm /etc/motd.d/cockpit

  7. Docker-02应用部署案例

    1.Docker部署mysql 拉取mysql镜像 # 查询mysql镜像 docker search mysql # 拉取镜像命令 docker pull centos/mysql-57-cento ...

  8. Go-类型推导

  9. [转帖]十分钟掌握 Vim 编辑器核心功能

    https://juejin.cn/post/6929248764746006535 前言 相信不论是前端还是后台多多少少都需要上到服务器上做一些操作,改改配置文件等,大多数 Linux 服务器默认都 ...

  10. [转帖]Kafka生产者——重要参数配置

    https://www.cnblogs.com/luckyhui28/p/12001798.html 目录 acks max.request.size retries和retry.backoff.ms ...