使用Byte Buddy生成Java字节码

2024-01-26 05:58

本文主要是介绍使用Byte Buddy生成Java字节码,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

上周我们探索了下[url=http://it.deepinmind.com/jvm/2014/07/03/how-to-make-java-more-dynamic-with-runtime-code-generation.html]Java的强类型及静态类型系统[/url]。我承认这样的类型让我们的代码表达性更强,但是同时也限制了第三方库提供面向POJO的API的能力。

我们明白了Java的反射的确是一种和用户代码交互的不错方式,但是这样损失了类型安全的好处。为了实现类型安全,_或许更好的方式就是在运行时通过代码生成来创建指定用户类的子类_?对于这些类,我们会重写它们的方法以实现我们的框架逻辑,而不需要用户依赖于框架的类型。

我建议你读下这篇文章的第一部分,如果你还没读过的话。我希望它能让你明白代码生成想要解决的是哪类问题,以及什么时候用反射的话会更合适一些 。

[b]可以开始了吗[/b]

我们都知道如何去定义一个Java类(我希望如此),因此这里也没有什么新的东西可讲了。然而一般我们都是通过某种JVM语言来表述这些类的定义,然后才请求编译器去把我们的东西翻译成字节码。

Java字节码是一个二进制格式,但是,不止如此,Java编程语言和它对应的字节码非常相似。这也不奇怪,因为Java语言某种程度上也驱动着字节码指令集的演进。如果你想了解更多关于字节码的知识,可以参考下RebelLabs上的这篇_[url=http://zeroturnaround.com/rebellabs/rebel-labs-report-mastering-java-bytecode-at-the-core-of-the-jvm/]掌握JVM字节码[/url]_的不错的分享,它会告诉你里面的很多技巧。

虽然直接操作字节码可以让你完全掌控类的生成过程,不过它的确很难搞定。字节码并没有给你提供像自动装箱或者高层面的控制流程之类的便利性。同样的,生成字节码需要你做许多繁琐的工作比如计算操作数栈的大小以及计算栈帧。如果可以跳过这些,只关注于你要生成的运行时代码不是更好吗?

当然如此。也正由于这个原因,即便是Java类库也自带了一些有限的支持。这些类又被称为JDK代理,它可以在运行时生成实现了某组接口的某个类的定义。

调用里面任何一个接口的方法都会被重定向到一个调用handler里面,你得去实现并提供这个handler。说起JDK代理,它依赖于接口的实现。你不能用这些Java代理来扩展现有的类。

对于我们上篇博客中的那个安全框架而言,这意味着我们只能确保接口方法的安全,而不能是别的类型的方法。这个限制真是弱爆了。正是因为这个,在JDK代理出现了之后,很快cglib就作为一个独立的库发布了。

cglib的工作原理与JDK代理类似,但是它可以动态生成子类而不止是实现接口,因此它可以代理任何方法。cglib至今仍非常流行,像Spring这样的框架也依赖于它来实现某些功能。

然而,尽管它非常流行,cglib这些年的开发已经不那么活跃了,甚至bug修复也只是偶尔才发布一次。由于CGLIB不再支持Java 8的默认方法等新特性,这个问题就变得相当严重了。

由于cglib这些未解决的问题,这些年越来越多的工程开始选择移除这个库。至少在Hiberate将javassist作为首选之后,这个_Java辅助_(javassist: java assistant)库明显成为了主流的选择。

javassist有一个和cglib功能非常类似的一个代码库。除此之外,它还提供了一个运行时的Java编译器,这使得你可以通过提供文本的Java代码来进行任何Java类的定义及重定义。

这听起来非常酷,在一个字符串中写代码就可以快速地解决问题,尤其是你需要将一个已编译的功能和一个动态功能进行混入的时候。下图中的代码是一个使用javassist的真实例子。

[img]http://zeroturnaround.com/wp-content/uploads/2014/07/javassist-horrors-640x130.png[/img]

更重要的是,尽管Java源码是表达Java类的一个不错的方式,不幸的是,尽管Red Hat已经开始赞助这个库了,但迄今为止javassist仍然只能算是一个个人的项目。随着Java语言的不断改进,javassist的编译器慢慢也开始落后于JDK的版本了。

因此,尽管可以用javassist来写Java代码,你还是得考虑一下字节码。而且这也是必须的,比如说,进行基础类型的显式装箱。猜猜为什么?这是因为由于Java 8新的语言特性的引入,两个编译器间的差距变得越来越大了。

[b]少抱怨一点好吧?[/b]

你很容易就能指出别人写的库中存在的问题,但是我们可以改善这种情况吗?cglib和javassist都 是本世纪初期的时候建立的,它们提供的API是围绕着Java在这段时期提供的语言特性来设计 的。

这些库的介入之后一个显著的创新就是注解。这多少有点可笑,代码生成主要就是用于实现基于注解的API,但是居然没有一个库是内建了这个功能的。由于这个原因,我接受了这个挑战,自己写了另一个库来实现这个功能。

这个库叫做[url=http://bytebuddy.net/]byte buddy[/url],它使用注解和一个领域特定的语言来实现它的目标。你可以像下面这段代码那样创建一个运行时类:




new ByteBuddy()
.subclass(Object.class)
.method(named(“toString”))
.intercept(MethodDelegation.to(ToStringInterception.class))
.make()



如果你能读懂这段代码的含义,那么这个库就已经达到我的预期了。还有一个比较神秘的一点或许就是方法拦截了,它接受一个方法代理作为参数。当接受到这样一个方法代理的时候,Byte Buddy会将方法调用重定向到指定类中匹配给定名字的一个静态方法里。目标类和方法可以实现如下:




class ToStringInterception {
public static String intercept(@Context Class<?> type) {
return type.getSimpleName();
}
}



有了上述的这个拦截器,这个动态类上任何toString方法的调用都可以委托给这个静态方法。通过使用了@Context注解,这个方法接受一个类引用作为它的唯一参数。因此,运行时toString的返回值就是这个被拦截类的名字。

这只是Byte Buddy API的一只皮毛功夫,这里我们并不想深入这个库的细节。如果你感兴趣的话,可以参数下它的[url=http://bytebuddy.net/javadoc/0.2.1/index.html]官网文档[/url],上面有详细的说明。


不管怎么说,为了演示下这个库非常适用于日常的编程工作,我们来用Byte Buddy实现我们在文章前面所提到的那个安全库。正如你所看到的,这个实现只是简单的将登录用户存储在了一个静态字段中,它只需几行代码就能完成:




class ByteBuddySecurityLibrary implements SecurityLibrary {

public static String currentUser = “admin”;

@Override
public Class<? extends T> secure(Class type) {
return new ByteBuddy()
.method(isAnnotatedBy(Secured.class))
.intercept(MethodDelegation.to(ByteBuddySecurityLibrary.class))
.make()
.load(type.getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
.getLoaded();
}

@RuntimeType
public static Object intercept(@SuperCall Callable<?> superMethod,
@Origin Method method) throws Exception {
if (!method.getAnnotation(Secured.class).requiredUser().equals(currentUser)) {
throw new IllegalStateException(method + " requires appropriate login”);
}
return superMethod.call();
}
}



和第一个例子类似,我们使用Byte Buddy来拦截方法以便将它们的调用委派给框架方法。这次我们是拦截了被Secured所注解的方法。

对这些方法而言,我们将注解的值和当前登录用户进行比较,如果用户不是注解所需的用户的话,就抛出异常。否则,我们调用原始的那个方法。

在Java中,在实例外部调用父方法一般是不可能的。为了解决这个问题,Byte Buddy自动创建了一个代理类,它的定义有点类似一个非静态的内部类。这么做的话,即便从拦截方法的外部也可以调用super方法了。


未完待续。


原创文章转载请注明出处:[url=http://it.deepinmind.com/jvm/2014/07/10/how-my-new-friend-byte-buddy-enables-annotation-driven-java-runtime-code-generation.html]http://it.deepinmind.com[/url]

[url=http://zeroturnaround.com/rebellabs/how-my-new-friend-byte-buddy-enables-annotation-driven-java-runtime-code-generation/]英文原文链接[/url]

这篇关于使用Byte Buddy生成Java字节码的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/645837

相关文章

Spring Boot整合Redis注解实现增删改查功能(Redis注解使用)

《SpringBoot整合Redis注解实现增删改查功能(Redis注解使用)》文章介绍了如何使用SpringBoot整合Redis注解实现增删改查功能,包括配置、实体类、Repository、Se... 目录配置Redis连接定义实体类创建Repository接口增删改查操作示例插入数据查询数据删除数据更

Java Lettuce 客户端入门到生产的实现步骤

《JavaLettuce客户端入门到生产的实现步骤》本文主要介绍了JavaLettuce客户端入门到生产的实现步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要... 目录1 安装依赖MavenGradle2 最小化连接示例3 核心特性速览4 生产环境配置建议5 常见问题

使用python生成固定格式序号的方法详解

《使用python生成固定格式序号的方法详解》这篇文章主要为大家详细介绍了如何使用python生成固定格式序号,文中的示例代码讲解详细,具有一定的借鉴价值,有需要的小伙伴可以参考一下... 目录生成结果验证完整生成代码扩展说明1. 保存到文本文件2. 转换为jsON格式3. 处理特殊序号格式(如带圈数字)4

Java使用Swing生成一个最大公约数计算器

《Java使用Swing生成一个最大公约数计算器》这篇文章主要为大家详细介绍了Java使用Swing生成一个最大公约数计算器的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下... 目录第一步:利用欧几里得算法计算最大公约数欧几里得算法的证明情形 1:b=0情形 2:b>0完成相关代码第二步:加

Java 的ArrayList集合底层实现与最佳实践

《Java的ArrayList集合底层实现与最佳实践》本文主要介绍了Java的ArrayList集合类的核心概念、底层实现、关键成员变量、初始化机制、容量演变、扩容机制、性能分析、核心方法源码解析、... 目录1. 核心概念与底层实现1.1 ArrayList 的本质1.1.1 底层数据结构JDK 1.7

Java Map排序如何按照值按照键排序

《JavaMap排序如何按照值按照键排序》该文章主要介绍Java中三种Map(HashMap、LinkedHashMap、TreeMap)的默认排序行为及实现按键排序和按值排序的方法,每种方法结合实... 目录一、先理清 3 种 Map 的默认排序行为二、按「键」排序的实现方式1. 方式 1:用 TreeM

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

Linux join命令的使用及说明

《Linuxjoin命令的使用及说明》`join`命令用于在Linux中按字段将两个文件进行连接,类似于SQL的JOIN,它需要两个文件按用于匹配的字段排序,并且第一个文件的换行符必须是LF,`jo... 目录一. 基本语法二. 数据准备三. 指定文件的连接key四.-a输出指定文件的所有行五.-o指定输出

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

Linux jq命令的使用解读

《Linuxjq命令的使用解读》jq是一个强大的命令行工具,用于处理JSON数据,它可以用来查看、过滤、修改、格式化JSON数据,通过使用各种选项和过滤器,可以实现复杂的JSON处理任务... 目录一. 简介二. 选项2.1.2.2-c2.3-r2.4-R三. 字段提取3.1 普通字段3.2 数组字段四.