如何动态修改类的父类/方法调用?

2017-08-08 16:48:39 +08:00
 gam2046

需求

现在有一些老旧项目需要迁移,针对其中某些方法需要做重定向,例如:

由于这些老旧项目通过动态加载的方式运行,那么能否有方法,在运行期间(不是编译期间)达到这样的目的:

尝试

尝试过采用 Spring AOP,第一个需求由于无法预知旧项目的Class对象类型,无法拦截。 即项目中可能通过各种Class对象,例如:

Class.class.getResourceAsStream(...);
String.class.getResourceAsStream(...);
"".getClass().getResourceAsStream(...);
this.getClass().getResourceAsStream(...);

至于第二个需求点,表示完全没有思路,目前在翻看 javassist 文档,不知道是否有解决方案。

环境限定

JDK 1.7

2769 次点击
所在节点    Java
6 条回复
3pmtea
2017-08-08 18:06:25 +08:00
自定义 class loader ?
alexwu
2017-08-08 18:33:13 +08:00
用 Java agent + asm/bcel/javassist 应该可以实现吧
breadenglish
2017-08-08 18:42:04 +08:00
为什么要在运行期间,而不是编译期间解决这个问题?
gam2046
2017-08-08 19:08:21 +08:00
@breadenglish 算是历史遗留原因,因为旧项目在不同环境下需要 hack 的方法不同。可以理解为新环境下是提供一种模拟环境,供老旧项目运行。

目前做过这样的尝试,通过 javassist 生成不检查类名安全检查以及 hack 过的 Class 字节码的 ClassLoader 对象,然后试图通过反射修改当前类加载器的父类加载器,以此绕过双亲委派机制,结果并不成功,程序运行直接输出 Exception in thread "main" 且没有退出,仿佛一种假死状态。代码如下:
```
public class JavassistHelp {
static ClassLoader hackClassLoader = new ClassLoader() {
private byte[] classBytecode = null;

private byte[] classBytecode() {
if (this.classBytecode == null) {
ClassPool classPool = ClassPool.getDefault();

try {
CtClass ordClass = classPool.get("java.lang.Class");
ordClass.defrost();

CtMethod ordMethod = ordClass.getDeclaredMethod("getResourceAsStream");
ordMethod.insertBefore("return new java.io.ByteArrayInputStream(new byte[]{1, 3, 5, 7, 9});");

this.classBytecode = ordClass.toBytecode();
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

}
return classBytecode;
}

@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {

if ("java.lang.Class".equals(name)) {
System.err.println("loadClass hack");
return this.defineClass(name, this.classBytecode(), 0, this.classBytecode().length);
}

String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}

byte[] b = new byte[is.available()];
is.read(b);

return defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
};

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// 通过反射修改父类加载器
Field parent = ClassLoader.class.getDeclaredField("parent");
parent.setAccessible(true);

Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(parent, parent.getModifiers() & ~Modifier.FINAL);
parent.set(JavassistHelp.class.getClassLoader(), JavassistHelp.hackClassLoader);

System.err.println("load resource is " + String.class.getResourceAsStream("111"));
}
}
```
SoloCompany
2017-08-09 00:23:35 +08:00
第一个问题,你需要直接置换掉 bootstrapclasspath
根据 jvm 的 spec,默认的 classloader 实现总是会优先使用 parent loader 以防止系统类被轻易改写

当然如果你的应用是比如类似 webapp 这类明确说明不符合以上规范的,很简单,把 WebappClassLoader 也置换掉

第二个问题,你可以先使用编译手段把 OldParent 改成自己想要的样子,然后把 patch 过的 class 和你的老程序 jar 放一起,确保在路径上能够优先加载即可
erhu
2017-08-09 06:50:51 +08:00
为什么用 ordMethod.insertBefore(),直接用 replace() 啊

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/381415

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX