java 创建1字节数组
Java代理是一个Java程序,它在另一个Java应用程序(“目标”应用程序)启动之前执行,从而为该代理提供了修改目标应用程序或其运行环境的机会。 在本文中,我们将从基础知识开始,逐步使用字节码操作工具Byte Buddy进行高级代理实现。
在最基本的用例中,Java代理设置应用程序属性或配置特定的环境状态,从而使该代理能够用作可重用和可插入的组件。 以下示例描述了这样的代理,该代理设置了对实际程序可用的系统属性:
public class Agent {
public static void premain(String arg) {
System.setProperty("my-property", “foo”);
}
}
如上面的代码所示,Java代理的定义与其他Java程序一样,只是premain
取代main方法作为入口点。 顾名思义,此方法在目标应用程序的main方法之前执行。 除了适用于任何其他Java程序的标准规则外,没有其他特定的编写代理规则。 最小的不同是,Java代理接收一个可选的参数,而不是零个或多个参数的数组。
要启动代理,必须将代理类和资源捆绑在一个jar中,然后在jar清单中将Agent-Class
属性设置为包含premain
方法的代理类的名称。 (代理必须始终捆绑为jar文件,不能以爆炸格式指定。)接下来,您必须通过在命令行上通过javaagent参数引用jar文件的位置来启动应用程序:
java -javaagent:myAgent.jar -jar myProgram.jar
您也可以在此位置路径前添加可选的代理参数。 以下命令启动一个Java程序,并将提供值myOptions作为参数的给定代理附加到premain
方法:
java -javaagent:myAgent.jar=myOptions -jar myProgram.jar
通过重复javaagent
命令可以附加多个代理。
但是,Java代理不仅具有改变应用程序环境状态的功能,还具有更多的功能。 可以授予Java代理访问Java工具API的权限,从而允许该代理修改目标应用程序的代码。 Java虚拟机的这一鲜为人知的功能提供了一个功能强大的工具,可促进面向方面编程的实现。
通过将类型为Instrumentation
的第二个参数添加到代理的premain
方法中,可以对Java程序进行此类修改。 Instrumentation参数可用于执行一系列任务,从确定对象的确切大小(以字节为单位)到通过注册ClassFileTransformers
实际修改类实现。 注册后,任何类加载器均会在加载类时调用ClassFileTransformer
。 调用时,类文件转换器有机会在加载表示的类之前进行转换,甚至完全替换任何类文件。 这样,可以在使用类之前增强或修改类的行为,如以下示例所示:
public class Agent {
public static void premain(String argument, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(
ClassLoader loader,
String className,
Class<?> classBeingRedefined, // null if class was not previously loaded
ProtectionDomain protectionDomain,
byte[] classFileBuffer) {
// return transformed class file.
}
});
}
}
在使用Instrumentation
实例注册上述ClassFileTransformer
之后,每次加载类时都会调用转换器。 为此,转换器接收类文件的二进制表示形式以及对试图加载该类的类加载器的引用。
Java代理也可以在Java应用程序的运行时期间注册。 在这种情况下,检测API允许重新定义已经加载的类,这一功能称为“ HotSwap” 。 不幸的是,重新定义加载的类仅限于替换方法体。 重新定义类时,不得添加或删除成员,也不得更改类型或签名。 首次加载类时,此限制不适用,在这种情况下, classBeingRedefined
参数设置为null。
Java字节码和类文件格式
类文件表示处于编译状态的Java类。 一个类文件包含最初被编码为Java源代码的程序指令的字节码表示。 Java字节码可以被视为Java虚拟机的语言。 实际上,JVM没有将Java作为编程语言的概念,而是专门处理字节码。 作为二进制表示的结果,字节码比程序的源代码占用更少的空间。 而且,将程序表示为字节码还可以使Java以外的其他语言(例如Scala或Clojure)的编译更加容易,以便在JVM上运行。 如果没有字节码作为中间语言,则必须在运行之前将任何程序转换为Java源代码。
但是,在代码操作的上下文中,这种抽象确实是有代价的。 在将ClassFileTransformer
应用于Java类时,即使假定转换后的代码最初是用Java编写的,也不能再将该类作为Java源代码进行处理。 更糟糕的是,在转换类时,用于自检类成员或批注的反射API也是禁止的,因为对API的访问将要求该类已经被加载,而这在转换过程之前不会发生完成。
幸运的是,Java字节码是一种相对简单的抽象,具有相对较少的操作,并且通常无需花费太多精力即可学习。 Java虚拟机通过将值作为堆栈机处理来执行程序。 字节码指令通常向虚拟机指示它应该从操作数堆栈中弹出值,执行某些操作并将结果推回堆栈中。
让我们考虑一个简单的示例:将数字1和2相加。 通过执行字节码指令iconst_1和iconst_2 ,这两个数字首先由JVM推入操作数堆栈。 iconsst_1是一个一字节的便利运算符,用于将数字1压入堆栈。 同样, iconst_2将第二个压入堆栈。 随后,执行iadd指令会将两个最新值从堆栈中弹出,并推回这些数字的总和。 在类文件中,每条指令不是通过其助记符名称存储的,而是存储为唯一标识特定指令的单个字节,因此称为字节码 。 下图显示了以上字节码指令及其对操作数堆栈的影响。
![](https://i-blog.csdnimg.cn/blog_migrate/004c23af13862d599c5ac46275be2987.png)
但幸运的是,对于使用源代码胜过字节码的人来说,Java社区创建了一些库来解析类文件,并将压缩后的字节码公开为命名指令流。 例如,流行的ASM库提供了一个简单的访问者API,该API将类文件分解为成员和方法指令,其操作方式类似于读取XML文件的SAX解析器。 使用ASM,可以如以下代码所示实现上述示例的字节码(其中visitIns
指令是ASM提供修改后的方法实现的方式):
MethodVisitor methodVisitor = ...
methodVisitor.visitIns(Opcodes.ICONST_1);
methodVisitor.visitIns(Opcodes.ICONST_2);
methodVisitor.visitIns(Opcodes.IADD);
应当注意,字节码规范仅用作隐喻,并且只要程序的结果保持正确,就允许Java虚拟机将程序转换为优化的机器代码。 由于字节码的简单性,可以很容易地替换或修改现有类中的指令。 因此,使用ASM并了解Java字节码的基本知识就足以通过注册使用此库处理其参数的ClassFileTransformer
来实现转换类的Java代理。
克服字节码隐喻
对于实际应用程序,解析原始类文件仍然意味着大量的手动工作。 Java程序员通常会对类的类型层次结构感兴趣。 例如,可能需要Java代理来修改实现给定接口的任何类。 要确定有关类的超级类型的信息,不再足以解析ClassFileTransformer
提供的类文件,该文件仅包含直接超级类型和接口的名称。 为了解决潜在的超级类型关系,仍然需要程序员为这些类型定位类文件。
另一个困难是,在项目中直接使用ASM要求团队中的任何开发人员了解Java字节码的基础知识。 实际上,这常常导致许多开发人员无法更改任何与字节码操作有关的代码。 在这种情况下,实施Java代理会对项目的长期可维护性构成威胁。
为了克服这些问题,希望使用比直接处理Java字节码更高级别的抽象来实现Java代理。 字节好友(Byte Buddy)是Apache 2.0许可的开源库,用于解决字节码操作和检测API的复杂性。 字节伙伴的声明目标是将显式字节码生成隐藏在类型安全的域特定语言后面。 使用字节伙伴,希望对熟悉Java编程语言的任何人来说字节码操作变得直观。
字节伙伴介绍
Byte Buddy并非专门用于生成Java代理。 它提供了用于生成任意Java类的API,并且在此类生成API的基础上,Byte Buddy还提供了用于生成Java代理的其他API。
为了轻松介绍Byte Buddy,下面的示例演示了一个简单类的生成,该类继承了Object并覆盖了toString方法以返回“ Hello World!”。 与原始ASM一样,“拦截”指令Byte Buddy使用截获的指令提供方法实现:
Class<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World!"))
.make()
.load(getClass().getClassLoader(),
ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
查看上面的代码,我们看到Byte Buddy分两步实现了一个方法。 首先,程序员需要指定一个ElementMatcher
,它负责标识一个或多个要实现的方法。 Byte Buddy提供了一组丰富的预定义拦截器,这些拦截器在ElementMatchers
类中公开。 在上述情况下, toString
方法通过其确切名称进行匹配,但是我们也可以在更复杂的代码结构(例如类型或注释)上进行匹配。
每当Byte Buddy生成类时,它都会分析所生成类型的类层次结构。 在上面的示例中,Byte Buddy确定所生成的类从其超类Object继承了一个名为toString的方法,其中指定的匹配器指示Byte Buddy通过后续的Implementation
实例(在本示例中为FixedValue
覆盖该方法。
创建子类时,字节好友总是通过覆盖生成的类中的方法来intercepts
匹配的方法。 但是,我们将在本文后面看到Byte Buddy也能够重新定义现有类而无需子类化。 在这种情况下,字节伙伴将现有方法替换为生成的代码,同时将原始代码复制到另一个合成方法中。
在上面的示例代码中,matched方法被返回固定值“ Hello World!”的实现所覆盖。 intercept
方法接受类型为Implementation的参数,并且Byte Buddy附带了几个预定义的实现,例如选定的FixedValue
类。 但是,如果需要,可以使用上面讨论的ASM API将方法实现为自定义字节码,并在其自身之上实现Byte Buddy。
在定义了一个类的属性之后,它是由make方法生成的。 在示例应用程序中,为生成的类提供一个随机名称,因为用户未指定名称。 最后,使用ClassLoadingStrategy
加载生成的ClassLoadingStrategy
。 使用上述默认的WRAPPER
策略,类将由新类加载器加载,该类加载器将环境的类加载器作为父类。
加载类后,可以使用Java Reflection API对其进行访问。 如果没有另外指定,则Byte Buddy会生成与超类相似的构造函数,以便为生成的类提供默认构造函数。 因此,可以验证生成的类已重写toString
方法,如以下代码所示:
assertThat(dynamicType.newInstance().toString(),
is("Hello World!"));
当然,这个生成的类没有太多实际用途。 对于实际应用程序,大多数方法的返回值都是在运行时计算的,并且取决于方法的参数和对象状态。
委托仪表
实现方法的一种更灵活的方法是使用Byte Buddy的MethodDelegation。 使用方法委托,可以生成重写的实现,该实现将调用给定类或实例的另一个方法。 这样,可以使用以下委托人重写前面的示例:
class ToStringInterceptor {
static String intercept() {
return “Hello World!”;
}
}
使用上述POJO拦截器,可以用MethodDelegation.to(ToStringInterceptor.class)替换以前的FixedValue实现:
Class<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.named("toString"))
.intercept(MethodDelegation.to(ToStringInterceptor.class))
.make()
.load(getClass().getClassLoader(),
ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
使用此委托者,Byte Buddy从提供给to方法的拦截目标中确定了最佳可调用方法。 对于ToStringInterceptor.class
,选择过程将简单地解析为该类型声明的唯一静态方法。 在这种情况下,仅考虑静态方法,因为将类指定为委托的目标。 相反,可以委托给一个类的实例 ,在这种情况下,Byte Buddy考虑所有虚拟方法。 如果在类或实例上有几种这样的方法可用,则Byte Buddy首先消除与特定仪器不兼容的所有方法。 然后,在其余方法中,库选择最佳匹配,通常是参数最多的方法。 通过调用滤镜方法将ElementMatcher
交给MethodDelegation
来缩小合格方法的范围,也可以显式选择目标方法。 例如,通过添加以下filter
,Byte Buddy仅将名为“ intercept”的方法视为委托目标:
MethodDelegation.to(ToStringInterceptor.class)
.filter(ElementMatchers.named(“intercept”))
截获后,被截获的方法仍会打印“ Hello World!”。 但是这次,结果是动态计算的,因此例如可以在每次从生成的类调用toString
触发的拦截器方法中设置一个断点。
指定拦截器方法的参数时,可以释放MethodDelegation
的全部功能。 通常会在参数上添加注释,以指示Byte Buddy在调用拦截器时注入值。 例如,使用@Origin
批注,Byte Buddy提供了检测方法的实例作为Java反射API提供的类的实例:
class ContextualToStringInterceptor {
static String intercept(@Origin Method m) {
return “Hello World from ” + m.getName() + “!”;
}
}
现在,在拦截toString
方法时,将对该调用进行检测以返回“来自toString的Hello world!”。
除了@Origin
批注外,Byte Buddy还提供了丰富的批注集 。 例如,在类型为Callable
的参数上使用@Super
批注,Byte Buddy创建并注入一个代理实例,该代理实例允许调用已检测方法的原始代码。 如果提供的注释对于特定用例不足或不切实际,则甚至可以注册注入用户指定值的自定义注释。
实施方法级安全性
如我们所见,可以使用MethodDelegation在运行时使用纯Java动态覆盖方法。 那是一个简单的例子,但是该技术可用于实现更多实际应用。 在本文的其余部分中,我们将开发一个示例,该示例使用代码生成来实施注释驱动的库以实施方法级安全性。 在我们的第一次迭代中,该库将生成子类以强制执行此安全性。 然后,我们将使用相同的方法来实现Java代理以执行相同的操作。
示例库使用以下注释允许用户指定一种方法被认为是安全的:
@interface Secured {
String user();
}
例如,考虑一个使用下面的Service
类执行敏感操作的应用程序,该操作仅在用户通过管理员身份验证后才能执行。 这是通过在执行此操作的方法上声明Secured批注来指定的。
class Service {
@Secured(user = “ADMIN”)
void doSensitiveAction() {
// run sensitive code...
}
}
当然可以将安全检查直接写入方法中。 实际上,硬编码横切关注点经常会导致难以维护的复制粘贴逻辑。 此外,一旦应用程序发现其他要求(例如日志记录,收集调用指标或结果缓存),直接添加此类代码就无法很好地扩展。 通过将这样的功能提取到代理中,一种方法可以完全代表其业务逻辑,从而使其更易于阅读,测试和维护代码库。
为了使建议的库保持简单,如果当前用户不是由注释的用户属性指定的用户,则注释的约定声明应抛出IllegalStateException
。 使用字节好友,可以通过一个简单的拦截器来实现此行为,如以下示例中的SecurityInterceptor
,该拦截器还可以通过其静态用户字段来跟踪当前登录的用户:
class SecurityInterceptor {
static String user = “ANONYMOUS”
static void intercept(@Origin Method method) {
if (!method.getAnnotation(Secured.class).user().equals(user)) {
throw new IllegalStateException(“Wrong user”);
}
}
}
正如我们在上面的代码中看到的那样,即使授予给定用户访问权限,拦截器也不会调用原始方法。 为了克服这个问题,可以将“字节好友”中的许多预定义方法实现方式进行链接。 使用MethodDelegation
类的andThen
方法,可以将上述安全检查放在对原始方法的简单调用之前,如下所示。 由于失败的安全检查将引发异常并阻止任何进一步的执行,因此如果用户未通过身份验证,则不会执行对原始方法的调用。
将这些部分放在一起,现在可以生成适当的Service
子类,其中所有带注释的方法都得到适当保护。 由于生成的类是Service的子类,因此生成的类可以用作Service
类型的所有变量的替代品,而无需进行类型强制转换,并且在调用doSensitiveAction
方法而没有适当的身份验证时将抛出异常:
new ByteBuddy()
.subclass(Service.class)
.method(ElementMatchers.isAnnotatedBy(Secured.class))
.intercept(MethodDelegation.to(SecurityInterceptor.class)
.andThen(SuperMethodCall.INSTANCE)))
.make()
.load(getClass().getClassLoader(),
ClassLoadingStrategy.Default.WRAPPER)
.getLoaded()
.newInstance()
.doSensitiveAction();
不幸的是,由于检测的子类仅在运行时创建,因此如果不使用Java反射就无法创建此类实例。 因此,应该由工厂创建一个已检测类的任何实例,该工厂封装了为进行检测而创建子类的复杂性。 结果,子类检测通常由已经需要由工厂创建实例的框架使用,例如,用于依赖注入的框架(例如Spring)或对象关系映射的框架(例如Hibernate)。 对于其他类型的应用程序,子类检测通常过于复杂而无法实现。
Java安全代理
使用Java代理,上述安全框架的另一种实现方式是修改诸如上述Service
类的原始字节码,而不是覆盖它。 这样,就不再需要创建托管实例。 简单地打电话
new Service().doSensitiveAction()
如果未对适当的用户进行身份验证,将已经引发了我们的异常。 为了支持这种修改类的方法,Byte Buddy提供了一个概念,称为“ 重新定义类” 。 当重新定义类的基础时,不会创建任何子类,而是将检测到的代码合并到检测到的类中以更改其行为。 使用这种方法,在对已检测类的任何方法的原始代码进行检测后仍可访问,因此SuperMethodCall
类的SuperMethodCall
工作原理与创建子类时完全相同。
由于在进行子类化或重新定义时具有相似的行为,因此,通过使用相同的DynamicType.Builder
接口描述类型,两种操作的API均以相同的方式执行。 可以通过ByteBuddy
类访问这两种形式的工具。 为了使Java代理的定义更加方便,但是Byte Buddy还提供了AgentBuilder
类,该类专门用于以简洁的方式解决常见的用例。 为了定义用于方法级安全性的Java代理,将以下类定义为代理的入口点就足够了:
class SecurityAgent {
public static void premain(String arg, Instrumentation inst) {
new AgentBuilder.Default()
.type(ElementMatchers.any())
.transform((builder, type) -> builder
.method(ElementMatchers.isAnnotatedBy(Secured.class)
.intercept(MethodDelegation.to(SecurityInterceptor.class)
.andThen(SuperMethodCall.INSTANCE))))
.installOn(inst);
}
}
如果此代理程序捆绑在jar文件中并在命令行上指定,则任何类型都将“转换”,或重新定义以保护指定Secured
批注的任何方法。 如果不激活Java代理,则该应用程序将在没有其他安全检查的情况下运行。 当然,这意味着在单元测试中可以调用带注释的方法的代码,而无需为模拟安全上下文而进行特定设置。 由于Java运行时会忽略在类路径上找不到的注释类型,因此甚至有可能在从应用程序中完全删除安全性库之后运行带注释的方法。
另一个优点是,Java代理易于堆叠。 如果在命令行上指定了多个Java代理,则每个代理都有机会按照它们在命令行上的排列顺序来修改类。 例如,这将允许安全性,日志记录和监视框架的组合,而无需这些应用程序之间的任何形式的集成层。 因此,使用Java代理来实现跨领域关注提供了编写更多模块化代码的机会,而无需将所有代码与用于管理实例的中央框架集成在一起。
Byte Buddy的源代码可在GitHub上免费获得 。 可以在 http://bytebuddy.net 上找到教程 。 Byte Buddy当前在版本0.7.4中可用,所有代码示例均基于该版本。 该库在2015年因其创新的方法及其对Java生态系统的贡献而获得了Oracle的Duke's Choice奖。
java 创建1字节数组