JVM生态系统已经成熟,并提供了大量库,因此您无需重新发明轮子。通过引入依赖可以使用这些库包。但是,有时,依赖关系引用的库包和当前用例会略有不一致。
在本文中,我们将研究一些替代方法,使它能现在就能在当前案例中工作:
反射
反射使您可以访问不希望被访问的状态,或调用不希望被调用的方法。
public class Private {
private String attribute = "My private attribute";
private String getAttribute() { return attribute; } }
public class ReflectionTest {
private Private priv;
@BeforeEach protected void setUp() { priv = new Private(); }
@Test public void should_access_private_members() throws Exception { var clazz = priv.getClass(); var field = clazz.getDeclaredField("attribute"); var method = clazz.getDeclaredMethod("getAttribute"); AccessibleObject.setAccessible(new AccessibleObject[]{field, method}, true); field.set(priv, "A private attribute whose value has been updated"); var value = method.invoke(priv); assertThat(value).isEqualTo("A private attribute whose value has been updated"); } }
|
但是,反思有一些局限性:
- “魔术”发生在AccessibleObject.setAccessible。可以在运行时使用适当配置的安全管理器来禁止这样做。我承认,在我的职业生涯中,我从未见过使用Security Manager。
- 模块系统限制了Reflection API的使用。例如,调用者和目标类都必须在同一模块中,目标成员必须是public,等等。请注意,许多库都不使用模块系统。
- 如果您直接使用具有私有成员的类,则反射是很好的。但是,如果您需要更改依赖类的行为,那就没有用:如果您的类使用A本身需要一个类的第三方类,B并且您需要更改B。
类路径阴影
类路径classpath是JVM将用来加载先前卸载的类的文件夹和JAR的有序列表。
启动应用程序的最简单命令如下:
java -cp=.:thirdparty.jar Main
不管出于什么原因,想象一下我们需要改变第三方包中类的行为。它的设计不允许这样做。
无论采用哪种设计,我们都可以通过以下方式对其进行破解:
- 获取类的源代码
- 根据我们的要求进行更改
- 编译它
- 将已编译的类放在包含原始类的JAR之前的classpath上
这种方法也有一些局限性:
- 您需要第三方的源代码,或者至少需要一种从编译后的代码中获取源代码的方法:反编译。
- 您需要能够从源代码进行编译。这意味着您需要重新创建的所有必需依赖项B。
面向方面的编程
在Java中,AspectJ是首选的AOP库。它依赖于以下核心概念:
- join point连接点定义了某些定义明确的点在该程序,执行例如,方法的执行
- 一个pointcut切入点可以在程序流中挑选出特定的连接点,例如,执行任何带有注释的方法@Loggable
- 一条advice汇集了一个切入点(以选择连接点)和一段代码(在每个连接点上运行)
这里有两个类:一个代表公共API,并将其实现委托给另一个。
public class Public {
private final Private priv;
public Public() { this.priv = new Private(); }
public String entryPoint() { return priv.implementation(); } }
final class Private {
final String implementation() { return "Private internal implementation"; } }
|
假设我们需要更改私有实现:public aspect Hack {
pointcut privateImplementation(): execution(String Private.implementation());
String around(): privateImplementation() { return "Hacked private implementation!"; } }
|
AspectJ提供了不同的实现:
- 编译时:在构建过程中更新字节码
- 编译后时间:在构建后立即更新字节码。它不仅允许更新项目类,还可以更新依赖的JAR。
- 加载时间:在加载类时在运行时更新字节码
您可以像这样在Maven中设置第一个选项:
<build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> </plugin> <plugin> <groupId>com.nickwongdev</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>1.12.6</version> <configuration> <complianceLevel>${java.version}</complianceLevel> <source>${java.version}</source> <target>${java.version}</target> <encoding>${project.encoding}</encoding> </configuration> <executions> <execution> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.5</version> </dependency> </dependencies>
|
但是,Codehaus的官方AspectJ Maven插件只能处理版本8(包括在内)的JDK,因为自2018年以来没有人进行过更新。有人在GitHub上分叉了可处理更高版本的代码。该fork可以处理JDK到版本13,以及AspectJ库到1.9.5。
Java代理
当您想破解时,AOP提供了一个高级抽象。但是,如果您想以一种细粒度的方式更改代码,则除了更改字节码本身之外别无其他方法。有趣的是,JVM为我们提供了一种在加载类时更改字节码的标准机制。
在您的职业生涯中,您可能已经遇到过该功能:它们被称为Java代理。当您启动JVM时,可以在命令行上静态设置Java代理,或者在以后动态地将Java代理动态附加到已经运行的JVM。
public class Agent {
public static void premain( String args, Instrumentation instrumentation){ var transformer = new HackTransformer(); //设置一个转换器,该转换器可以在JVM加载字节码之前对其进行更改 instrumentation.addTransformer(transformer); } }
|
Java代理在字节码级别工作。代理为您提供了字节数组,该字节数组根据JVM规范存储类的定义。必须更改字节数组中的字节并不有趣。好消息是其他人以前有此要求。因此,该生态系统提供了可使用的库,这些库提供了更高级别的抽象。
在以下代码段中,转换器使用Javassist:
public class HackTransformer implements ClassFileTransformer {
@Override public byte[] transform(ClassLoader loader, String name, Class<?> clazz, ProtectionDomain domain, byte[] bytes) { if ("ch/frankel/blog/agent/Private".equals(name)) { var pool = ClassPool.getDefault(); try { var cc = pool.get("ch.frankel.blog.agent.Private"); var method = cc.getDeclaredMethod("implementation"); method.setBody("{ return \"Agent-hacked implementation!\" }"); bytes = cc.toBytecode(); } catch (NotFoundException | CannotCompileException | IOException e) { e.printStackTrace(); } } return bytes; } }
|
结论
在这篇文章中,我们列出了四种用于破解第三方库行为的方法:反射,类路径阴影,面向方面的编程和Java代理。
有了这些,您应该能够解决遇到的任何问题。只要记住,库和JVM都是以这种方式设计的,这是有充分理由的:防止您犯错误。