简体   繁体   English

如何使用 java 代理和 ASM 监控抽象 class 中方法的调用?

[英]How to monitor the invocation of methods in abstract class using java agent and ASM?

What I want to do is to monitor the invocation of JUnit 4 test methods.我要做的是监控JUnit 4种测试方法的调用。 The reason I must do this by myself is: I need to record the executed classes during the execution of each test method.我必须自己这样做的原因是:我需要记录每个测试方法执行过程中执行的类。 So I need to insert some instructions to the test method so that I know when the test start/end and those recorded classes are executed by which test entity.所以我需要在测试方法中插入一些指令,以便我知道测试何时开始/结束以及那些记录的类是由哪个测试实体执行的。 So I need to filter the test methods on my own.所以我需要自己过滤测试方法。

Actually, the reason why I am doing this is not relevant to the question , as I did not mention "JUnit", "test" in the title.实际上,我这样做的原因与问题无关,因为我没有在标题中提到“JUnit”、“test”。 The problem can still be a problem in other similar cases.在其他类似情况下,该问题仍然可能是一个问题。

The case I have is like this:我的情况是这样的:

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

I have also modified Surefire's member argLine so that my agent will be attached (premain mode) when Surefire launch a new JVM process to execute tests.我还修改了 Surefire 的成员argLine ,以便在 Surefire 启动新的 JVM 进程以执行测试时附加我的代理(预主模式)。

In my agent class:在我的代理 class 中:

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

My transformer class:我的变压器 class:

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();
    }
}

In my ClassVisitor adapter class:在我的 ClassVisitor 适配器 class 中:

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;
    }
}

In my MethodVisitor adapter class:在我的 MethodVisitor 适配器 class 中:

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

Sadly, I found that the abstract class will not get into the transform method, thus I can not instrument the t8 method.可悲的是,我发现抽象 class 不会进入transform方法,因此我无法检测t8方法。 TestC should be executed as a test class, but I can never monitor the invocation of TestC.t8 . TestC应该作为测试 class 执行,但我永远无法监视TestC.t8的调用。

There are several opportunities to inject logging into the test via the JUnit API.有几个机会通过 JUnit API 将日志记录注入测试。 There is no need for instrumentation.不需要仪器。

For a very simple setup:对于一个非常简单的设置:

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());
    }
}

which will print这将打印

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

You can also implement your own rule.您也可以实施自己的规则。 Eg ad-hoc:例如特设:

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());
            }
        };
    }
}

Or as a reusable rule that can be inserted via a single-liner into a test class或者作为可以通过单线插入测试 class 的可重用规则

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);
                }
            }
        };
    }
}

used as simple as使用起来很简单

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

A different approach is implementing a custom test runner.另一种方法是实现自定义测试运行器。 This allows to apply a behavior to an entire test suite, as test suites are implemented via runners as well.这允许将行为应用于整个测试套件,因为测试套件也是通过运行器实现的。

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());
        };
    };
}

This may get applied to an entire test suite, ie still inheriting test methods from BaseTest , you may use这可能会应用于整个测试套件,即仍然从BaseTest继承测试方法,您可以使用

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

public class TestB extends BaseTest {}

public class TestC extends BaseTest {}

which will print这将打印

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

These are only pointers, to suggest studying the API which allows even more.这些只是建议,建议研究 API,它允许更多。 Another point to consider, is that depending on the method you're using for launching the tests (you mentioned a maven plugin), there might be support for adding a global RunListener right there, without the need to alter the test classes.要考虑的另一点是,根据您用于启动测试的方法(您提到了 maven 插件),可能支持在此处添加全局RunListener ,而无需更改测试类。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM