使用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

相关文章

redis在spring boot中异常退出的问题解决方案

《redis在springboot中异常退出的问题解决方案》:本文主要介绍redis在springboot中异常退出的问题解决方案,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴... 目录问题:解决 问题根源️ 解决方案1. 异步处理 + 提前ACK(关键步骤)2. 调整Redis消费者组

一文教你Java如何快速构建项目骨架

《一文教你Java如何快速构建项目骨架》在Java项目开发过程中,构建项目骨架是一项繁琐但又基础重要的工作,Java领域有许多代码生成工具可以帮助我们快速完成这一任务,下面就跟随小编一起来了解下... 目录一、代码生成工具概述常用 Java 代码生成工具简介代码生成工具的优势二、使用 MyBATis Gen

C#使用MQTTnet实现服务端与客户端的通讯的示例

《C#使用MQTTnet实现服务端与客户端的通讯的示例》本文主要介绍了C#使用MQTTnet实现服务端与客户端的通讯的示例,包括协议特性、连接管理、QoS机制和安全策略,具有一定的参考价值,感兴趣的可... 目录一、MQTT 协议简介二、MQTT 协议核心特性三、MQTTNET 库的核心功能四、服务端(BR

springboot项目redis缓存异常实战案例详解(提供解决方案)

《springboot项目redis缓存异常实战案例详解(提供解决方案)》redis基本上是高并发场景上会用到的一个高性能的key-value数据库,属于nosql类型,一般用作于缓存,一般是结合数据... 目录缓存异常实践案例缓存穿透问题缓存击穿问题(其中也解决了穿透问题)完整代码缓存异常实践案例Red

使用@Cacheable注解Redis时Redis宕机或其他原因连不上继续调用原方法的解决方案

《使用@Cacheable注解Redis时Redis宕机或其他原因连不上继续调用原方法的解决方案》在SpringBoot应用中,我们经常使用​​@Cacheable​​注解来缓存数据,以提高应用的性能... 目录@Cacheable注解Redis时,Redis宕机或其他原因连不上,继续调用原方法的解决方案1

SpringCloud整合MQ实现消息总线服务方式

《SpringCloud整合MQ实现消息总线服务方式》:本文主要介绍SpringCloud整合MQ实现消息总线服务方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录一、背景介绍二、方案实践三、升级版总结一、背景介绍每当修改配置文件内容,如果需要客户端也同步更新,

java中XML的使用全过程

《java中XML的使用全过程》:本文主要介绍java中XML的使用全过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录什么是XML特点XML作用XML的编写语法基本语法特殊字符编写约束XML的书写格式DTD文档schema文档解析XML的方法​​DOM解析XM

Java 的 Condition 接口与等待通知机制详解

《Java的Condition接口与等待通知机制详解》在Java并发编程里,实现线程间的协作与同步是极为关键的任务,本文将深入探究Condition接口及其背后的等待通知机制,感兴趣的朋友一起看... 目录一、引言二、Condition 接口概述2.1 基本概念2.2 与 Object 类等待通知方法的区别

SpringBoot项目中Redis存储Session对象序列化处理

《SpringBoot项目中Redis存储Session对象序列化处理》在SpringBoot项目中使用Redis存储Session时,对象的序列化和反序列化是关键步骤,下面我们就来讲讲如何在Spri... 目录一、为什么需要序列化处理二、Spring Boot 集成 Redis 存储 Session2.1

使用Java实现Navicat密码的加密与解密的代码解析

《使用Java实现Navicat密码的加密与解密的代码解析》:本文主要介绍使用Java实现Navicat密码的加密与解密,通过本文,我们了解了如何利用Java语言实现对Navicat保存的数据库密... 目录一、背景介绍二、环境准备三、代码解析四、核心代码展示五、总结在日常开发过程中,我们有时需要处理各种软