【问题标题】:Add code to package private library method添加代码打包私有库方法
【发布时间】:2013-11-23 10:30:29
【问题描述】:

我有一个带有包私有方法的库类。通过子类直接覆盖此方法是没有选择的。当从库内部调用此包私有方法时,无论多么丑陋,是否有任何方法可以执行自己的代码,例如使用 AspectJ?

这是该类的简化示例(packagePrivateMethod() 实际上不是直接调用,而是从本机代码调用):

public LibClass {

  public LibClass() {
    ...
    packagePrivateMethod();
    ...
  }

  void packagePrivateMethod() {
    // <-- here I want to execute additional code
    ...
  }
}

【问题讨论】:

  • 你试过反射吗?您也许可以在运行时获取方法定义并更改其可访问性 - 没有比这更丑陋的了!
  • 这并没有解决问题,因为我不想自己启动这个方法,而是在库调用这个方法时添加更多代码。
  • 你有没有想过用不同的方法来解决这个问题?你能详细说明一下场景吗?我相信应该有一种解决方法,而不必弄乱当前形式的库包。

标签: java aspectj


【解决方案1】:

您可以使用相当重量级的方法。

  1. 编写一个小型 Java 代理 SO post about that topic
  2. 使用提供的Instrumentation interface拦截类加载
  3. 使用字节码修改库(例如 ASMJava Assist(仅限 Java 6 !))来检测字节码(例如,将方法调用替换为您真正想做的任何事情。

这会起作用,因为您可以修改所有内容的字节码,但它需要您在执行之前修改该字节码。

当然,您也可以通过修改类文件,将现有的字节码替换为您在上面第 3 步中创建的字节码来静态地做到这一点。

如果您不想/不能静态替换类的字节码,则必须在运行时修改字节码。因为使用 Java 代理是个好主意。

由于到目前为止这一切都相当抽象,我添加了一个示例,它将拦截库类的加载,在包私有方法中注入方法调用。当 main 方法执行时,您可以从输出中看到,注入的方法是在库类的代码之前直接调用的。如果您将return; 添加为注入代码,您还可以一起阻止该方法的执行。

以下是使用 Java 6 和 JavaAssist 解决的问题的示例代码。如果你想沿着这条路走并使用像 Java 7 这样的更新,你只需要用 ASM 替换字节码操作。这有点可读性差,但也不完全是火箭科学。

主类:

package com.aop.example;

public class Main {

  public static void main(String[] args) {
    System.out.println("Main starts!");
    LibClass libClass = new LibClass();
    System.out.println("Main finished!");
  }
}

你的 LibClass:

package com.aop.example;

public class LibClass {

  public LibClass() {
    packagePrivateMethod();
  }

  void packagePrivateMethod() {
    // <-- here I want to execute additional code
    System.out.println("In packagePrivateMethod");
  }
}

代理:

package com.aop.agent;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;
import javassist.NotFoundException;

public class Agent {

  public static void premain(String agentArgs, Instrumentation instr) {
    System.out.println("Agent starts!");
    instr.addTransformer(new ClassFileTransformer() {

      @Override
      public byte[] transform(ClassLoader classLoader, String className, Class<?> arg2, ProtectionDomain arg3,
          byte[] bytes)
          throws IllegalClassFormatException {
        System.out.println("Before loading class " + className);

        final String TARGET_CLASS = "com/aop/example/LibClass";

        if (!className.equals(TARGET_CLASS)) {
          return null;
        }

        LoaderClassPath path = new LoaderClassPath(classLoader);
        ClassPool pool = new ClassPool();
        pool.appendSystemPath();
        pool.appendClassPath(path);

        try {
          CtClass targetClass = pool.get(TARGET_CLASS.replace('/', '.'));
          System.out.println("Enhancing class " + targetClass.getName());
          CtMethod[] methods = targetClass.getDeclaredMethods();
          for (CtMethod method : methods) {
            if (!method.getName().contains("packagePrivateMethod")) {
              continue;
            }
            System.out.println("Enhancing method " + method.getSignature());
            String myMethodInvocation = "com.aop.agent.Agent.myMethodInvocation();";
            method.insertBefore(myMethodInvocation);
          }
          System.out.println("Enhanced bytecode");

          return targetClass.toBytecode();
        }
        catch (CannotCompileException e) {
          e.printStackTrace();
          throw new RuntimeException(e);
        }
        catch (IOException e) {
          e.printStackTrace();
          throw new RuntimeException(e);
        }
        catch (NotFoundException e) {
          e.printStackTrace();
          throw new RuntimeException(e);
        }
      }

    });
  }

  public static void myMethodInvocation() {
    System.out.println("<<<My injected code>>>!");
  }
}

用于运行示例的命令(您必须将代理放入一个 jar 中,其清单具有属性 Premain-Class: com.aop.agent.Agent

%JAVA_HOME%\bin\java -cp .;..\javassist-3.12.1.GA.jar -javaagent:..\..\agent.jar com.aop.example.Main

本示例运行如下命令的输出:

Agent starts!
Before loading class com/aop/example/Main
Main starts!
Before loading class com/aop/example/LibClass
Enhancing class com.aop.example.LibClass
Enhancing method ()V
Enhanced bytecode
<<<My injected code>>>!
In packagePrivateMethod
Main finished!
Before loading class java/lang/Shutdown
Before loading class java/lang/Shutdown$Lock

【讨论】:

  • 您也可以将 Javassist 与 Java 7 和 8 一起使用。此外,我的代理出现了错误,直到我在清单中添加了一个额外的参数,正如我在这个问题的答案中提到的那样:stackoverflow.com/questions/10423319/…
【解决方案2】:

您可以通过Mockito 或类似的模拟库来模拟包私有方法。示例:

// declared in a package
public class Foo {
    String foo(){
        return "hey!";
    }
}

@Test
public void testFoo() throws Exception {
    Foo foo = Mockito.spy(new Foo());

    Assert.assertEquals("hey!", foo.foo());

    Mockito.when(foo.foo()).thenReturn("bar!");

    Assert.assertEquals("bar!", foo.foo());

}

【讨论】:

  • 如果在类的构造函数中调用包私有方法(在您的示例中:如果Foo 调用foo()),这是否也有效?
  • 不,不会。如果可能,我会建议重新考虑设计。
  • 这是一个无法轻易修改的第三方库。
【解决方案3】:

您可以将 Spring 添加到您的项目中吗? 可以使用ProxyFactory - 请参阅another SO post

使用 ProxyFactory,您可以为一个类实例 add an advice 并将方法执行委托给另一个类(它执行 packagePrivateMethod() 和/或用您想要的代码替换它)。

由于该库不是 spring 管理的,您可能必须使用 spring 的加载时编织:ltw xml & examples

【讨论】:

  • 不幸的是,我无法更改库内部结构(“将方法执行委托给另一个类”)。否则这将是微不足道的。
  • 你误会了。您拦截原始方法调用并将其委托给另一个类。
【解决方案4】:

使用装饰器模式。它专为这种情况而设计。如果您需要更多详细信息,请回复我,否则请查看this

或者您也可以使用反射或字节码操作机制在运行时动态创建您的类型。

【讨论】:

    【解决方案5】:

    另一种思路:在同一个包中新建一个同名的类。

    假设你想在下面的项目中替换LibraryClass

    Project structure:
      - library.jar (contains com.example.LibraryClass)
      - src
        - com
          - mycompany
            - MyClass.java
    

    只需创建同名的包和文件。

    Project structure:
      - library.jar (contains com.example.LibraryClass)
      - src
        - com
          - mycompany
            - MyClass.java
          - example
            - LibraryClass.java  <- create this package and file
    

    这依赖于类加载器获取您的文件而不是库的文件,但如果您只是想让 hack 用于测试,那么值得一试。我不确定类加载器如何决定加载哪个文件,因此这可能不适用于所有环境。

    如果您没有LibraryClass 的源代码,只需复制反编译的代码,然后进行更改即可。

    对于我需要这种能力的项目,它只是一些测试原型代码......我不需要任何生产质量或在所有环境中工作。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-10-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-10-17
      相关资源
      最近更新 更多