【问题标题】:How to monitor the invocation of methods in abstract class using java agent and ASM?java - 如何使用java代理和ASM监控抽象类中方法的调用?
【发布时间】:2020-07-08 12:43:01
【问题描述】:

我想做的是监控 JUnit 4 测试方法的调用。我必须自己这样做的原因是:我需要记录每个测试方法执行过程中执行的类。所以我需要在测试方法中插入一些指令,以便我知道测试何时开始/结束以及那些记录的类是由哪个测试实体执行的。所以我需要自己过滤测试方法。

其实我这样做的原因与问题无关,因为我没有在标题中提到“JUnit”、“test”。在其他类似情况下,问题仍然可能是问题。

我的情况是这样的:

public abstract class BaseTest {
    @Test
    public void t8() {
        assert new C().m() == 1;
    }
}
public class TestC  extends BaseTest{
    // empty
}

我还修改了 Surefire 的成员 argLine,以便在 Surefire 启动新的 JVM 进程执行测试时附加我的代理(预主模式)。

在我的代理类中:

    public static void premain(String args, Instrumentation inst){
        isPreMain = true;
        agentArgs = args;
        log("args: " + args);
        parseArgs(args);
        inst.addTransformer(new TestTransformer(), true);
    }

我的变压器类:

public class TestTransformer implements ClassFileTransformer {
    public byte[] transform(ClassLoader loader, String className,
                            Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) throws IllegalClassFormatException {

        log("TestTransformer: transform: " + className);
        ...
        ClassReader cr = new ClassReader(classfileBuffer);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        RecordClassAdapter mca = new RecordClassAdapter(cw, className);
        cr.accept(mca, 0);
        return cw.toByteArray();
    }
}

在我的 ClassVisitor 适配器类中:

class RecordClassAdapter extends ClassVisitor {
    ...
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        mv = new RecordMethodAdapter (...);
        return mv;
    }
}

在我的 MethodVisitor 适配器类中:

class RecordMethodAdapter extends MethodVisitor {
    public void visitCode() {
        mv.visitCode();
        if (isTestMethod){
            mv.visitLdcInsn(methodName);
            mv.visitMethodInsn(INVOKESTATIC, MyClass, "entityStarted",
                    "(Ljava/lang/String;)V", false);
        }
    }
}

遗憾的是,我发现抽象类不会进入transform 方法,因此我无法检测t8 方法。 TestC 应该作为测试类执行,但我永远无法监控 TestC.t8 的调用。

【问题讨论】:

  • 如何注册/调用转换? (请用相关代码修改问题)
  • 为什么? JUnit 4 已经提供了监控测试方法调用所需的一切。事实上,您可以使用不止一个内置功能来实现此类监控。
  • 即使 JUnit 没有这样的选项,您为什么要为此开发自定义代理而不是使用 AspectJ?或者,如果 AspectJ 对您来说太容易并且您想编写更多代码,也许是 ByteBuddy?对于这样一个简单的高级任务,ASM 的级别差不多。这只是一个用于学习目的的游乐场项目吗?
  • @apangin 很抱歉我忘了显示这些代码。现在我已经编辑了这个问题。我只是使用addTransformer方法注册。但是在日志中找不到BaseTest ,说明没有进入transform方法。
  • @kriegaex 实际上它是一个严肃的工具。我使用 ASM 是因为我想做的不仅仅是监控测试执行。其实每个测试方法的执行过程中我都需要记录执行的类,所以我只是顺便用ASM来监控测试实体的开始/结束。

标签: java jvm instrumentation java-bytecode-asm javaagents


【解决方案1】:

有多种机会可以通过 JUnit API 将日志记录注入测试。不需要仪器。

对于一个非常简单的设置:

public class BaseTest {
    @Test
    public void t8() {
        System.out.println("Running  test "+getClass().getName()+".t8() [BaseTest.t8()]");
    }
  
    @Test
    public void anotherMethod() {
        System.out.println("Running  test "
            +getClass().getName()+".anotherMethod() [BaseTest.anotherMethod()]");
    }
}

public class TestC extends BaseTest {
    @Rule
    public TestName name = new TestName();
  
    @Before
    public void logStart() throws Exception {
       System.out.println("Starting test "+getClass().getName()+'.'+name.getMethodName());
    }
  
    @After
    public void logEnd() throws Exception {
       System.out.println("Finished test "+getClass().getName()+'.'+name.getMethodName());
    }
}

将打印出来

Starting test class TestC.t8
Running  test TestC.t8() [BaseTest.t8()]
Finished test class TestC.t8
Starting test class TestC.anotherMethod
Running  test TestC.anotherMethod() [BaseTest.anotherMethod()]
Finished test class TestC.anotherMethod

您也可以实施自己的规则。例如。临时:

public class TestB extends BaseTest {
    @Rule
    public TestRule notify = TestB::decorateTest;
  
    static Statement decorateTest(Statement st, Description d) {
        return new Statement() {
            @Override public void evaluate() throws Throwable {
              System.out.println("Starting test "+d.getClassName()+"."+d.getMethodName());
              st.evaluate();
              System.out.println("Finished test "+d.getClassName()+"."+d.getMethodName());
            }
        };
    }
}

或者作为可以通过单行插入到测试类中的可重用规则

public class LoggingRule implements TestRule {
    public static final LoggingRule INSTANCE = new LoggingRule();
  
    private LoggingRule() {}
  
    @Override
    public Statement apply(Statement base, Description description) {
        Logger log = Logger.getLogger(description.getClassName());
        log.setLevel(Level.FINEST);
        Logger.getLogger("").getHandlers()[0].setLevel(Level.FINEST);
        String clName = description.getClassName(), mName = description.getMethodName();
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                log.entering(clName, mName);
                String result = "SUCCESS";
                try {
                    base.evaluate();
                }
                catch(Throwable t) {
                    result = "FAIL";
                    log.throwing(clName, mName, t);
                }
                finally {
                    log.exiting(clName, mName, result);
                }
            }
        };
    }
}

使用起来很简单

public class TestB extends BaseTest {
    @Rule
    public LoggingRule log = LoggingRule.INSTANCE;
}

另一种方法是实现自定义测试运行器。这允许将行为应用于整个测试套件,因为测试套件也是通过运行器实现的。

public class LoggingSuiteRunner extends Suite {
    public LoggingSuiteRunner(Class<?> klass, RunnerBuilder builder)
                                                            throws InitializationError {
        super(klass, builder);
    }

    @Override
    public void run(RunNotifier notifier) {
        notifier.addListener(LOG_LISTENER);
        try {
            super.run(notifier);
        } finally {
            notifier.removeListener(LOG_LISTENER);
        }
    }

    static final RunListener LOG_LISTENER = new RunListener() {
        public void testStarted(Description d) {
            System.out.println("Starting test "+d.getClassName()+"."+d.getMethodName());
        }
        public void testFinished(Description d) {
            System.out.println("Finished test "+d.getClassName()+"."+d.getMethodName());
        }
        public void testFailure(Failure f) {
            Description d = f.getDescription();
            System.out.println("Failed test "+d.getClassName()+"."+d.getMethodName()
                              +": "+f.getMessage());
        };
    };
}

这可能会应用于整个测试套件,即仍然从 BaseTest 继承测试方法,您可以使用

@RunWith(LoggingSuiteRunner.class)
@SuiteClasses({ TestB.class, TestC.class })
public class TestA {}

public class TestB extends BaseTest {}

public class TestC extends BaseTest {}

将打印出来

Starting test TestB.t8
Running  test TestB.t8() [BaseTest.t8()]
Finished test TestB.t8
Starting test TestB.anotherMethod
Running  test TestB.anotherMethod() [BaseTest.anotherMethod()]
Finished test TestB.anotherMethod
Starting test TestC.t8
Running  test TestC.t8() [BaseTest.t8()]
Finished test TestC.t8
Starting test TestC.anotherMethod
Running  test TestC.anotherMethod() [BaseTest.anotherMethod()]
Finished test TestC.anotherMethod

这些只是提示,建议研究允许更多功能的 API。要考虑的另一点是,根据您用于启动测试的方法(您提到了一个 maven 插件),可能支持在此处添加全局 RunListener,而无需更改测试类。

【讨论】:

  • 你忘了包含TestName类吗?
  • @Eugene 不,是org.junit.rules.TestName
  • @Holger 感谢您的详细回答!我也想知道,在我的情况下,为什么抽象类不能进入transform 方法,有没有办法在抽象类中检测方法?也许我应该为此提出一个新问题,但我想也许你对此有所了解,因为你已经帮助了我不止一次:)
  • 据我所知,您正在premain 中安装加载时间转换器,因此我们应该能够排除该类已经加载的可能性。 transform 中应该有一个名称测试,为您不感兴趣的类返回 null,以避免在加载代理所需的类时出现循环依赖。如果单元测试实际运行,我们也可以排除该类永远不会被加载。我想,您确实已经检查过代理是否已安装?然后,没有可识别的原因。一定是缺少一些信息……
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-03
  • 1970-01-01
  • 2021-03-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多