第一件事:
- 在Java 中,一旦创建了一个数组,它的长度就是固定的。数组无法调整大小。
- 您可以将数组的元素复制到具有不同大小的新数组中。最简单的方法是使用
Arrays.copyOf() 方法之一。
- 如果您需要可变大小的集合,最好使用
ArrayList 而不是数组。
话虽如此,在某些情况下,您可能别无选择,只能更改在代码之外某处创建的数组的大小。1 唯一的方法是操作创建数组的代码的生成字节码。
概念验证
以下是一个小型概念验证项目,它使用 Java instrumentation 动态更改数组的大小2。示例项目是一个maven项目,结构如下:
.
├─ pom.xml
└─ src
└─ main
└─ java
└─ com
└─ stackoverflow
└─ agent
├─ Agent.java
└─ test
└─ Main.java
Main.java
此文件包含我们将要操作其字节码的目标类:
package com.stackoverflow.agent.test;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
String[] array = {"Zero"};
fun(array);
System.out.println(Arrays.toString(array));
}
public static void fun(String[] array) {
array[1] = "One";
array[2] = "Two";
array[3] = "Three";
array[4] = "Four";
}
}
在main 方法中,我们创建一个大小为1 的String 数组。在fun 方法中,在数组边界之外分配了4 个附加值。按原样运行此代码显然会导致错误。
Agent.java
此文件包含将执行字节码操作的类:
package com.stackoverflow.agent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class Agent {
public static void premain(String args, Instrumentation instrumentation) {
instrumentation.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader l, String name, Class<?> c,
ProtectionDomain d, byte[] b) {
if (name.equals("com/stackoverflow/agent/test/Main")) {
byte iconst1 = (byte) 0x04;
byte iconst5 = (byte) 0x08;
byte anewarray = (byte) 0xbd;
for (int i = 0; i <= b.length - 1; i++) {
if (b[i] == iconst1 && b[i + 1] == anewarray) {
b[i] = iconst5;
}
}
return b;
}
return null;
}
});
}
}
在字节码级别,Main 类中String 数组的创建包含两个命令:
-
iconst_1,它将值为 1 的 int 常量压入堆栈 (0x04)。
-
anewarray,它会弹出堆栈的值并创建一个相同大小的引用数组3 (0xbd)。
上面的代码在 Main 类中查找该命令组合,如果找到,则将 const_1 命令替换为 const_5 命令 (0x08),从而有效地将数组的维度更改为 5。4
pom.xml
maven POM文件用于构建应用JAR,配置主类和Java代理类。5
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.stackoverflow</groupId>
<artifactId>agent</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.1</version>
<configuration>
<archive>
<manifestEntries>
<Main-Class>com.stackoverflow.agent.test.Main</Main-Class>
<Premain-Class>com.stackoverflow.agent.Agent</Premain-Class>
<Agent-Class>com.stackoverflow.agent.Agent</Agent-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
构建和执行
可以使用标准的mvn clean package 命令构建示例项目。
在不引用代理代码的情况下执行将产生预期的错误:
$> java -jar target/agent.jar
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 1
at com.stackoverflow.agent.test.Main.fun(Main.java:15)
at com.stackoverflow.agent.test.Main.main(Main.java:9)
使用代理代码执行时会产生:
$> java -javaagent:target/agent.jar -jar target/agent.jar
[Zero, One, Two, Three, Four]
这表明使用字节码操作成功更改了数组的大小。
1 问题here和here中出现了这种情况,后者促使我写下这个答案。
2从技术上讲,示例项目不会调整数组的大小。它只是使用与代码中指定的大小不同的大小创建它。实际上,在保持其引用并复制其元素的同时调整现有数组的大小会稍微复杂一些。
3 对于原始数组,相应的字节码操作将是 newarray (0xbc ) 代替。
4 如前所述,这只是一个概念证明(而且是一个非常老套的证明)。代替随机替换字节,更健壮的实现可以使用像ASM 这样的字节码操作库在任何newarray 或anewarray 命令之前插入pop 命令,然后是sipush 命令。可以在this answer 的 cmets 中找到有关该解决方案的更多提示。
5在实际场景中,代理代码显然位于单独的项目中。