0

I’m using Javassist (3.25.0-GA) and Java 8 with a custom Agent to transform bytecode and add print statements to existing catch{} clauses. This works for simple cases, but has a problem with the compiled bytecode of the try-with-resources syntax.

Here is a basic example of what I'm trying to do and the results when it works correctly on standard try/catch blocks:

    // before byte code manipulation
    public void methodWithCatchClause() {
        try {
            throwsAnException();
        } catch (Exception ex) {
            handleException(ex);
        }
    }
    // after byte code manipulation
    public void methodWithCatchClause() {
        try {
            throwsAnException();
        } catch (Exception ex) {
            System.out.println("CATCH CLAUSE!"); // added by Javassist
            handleException(ex);
        }
    }

The logic I'm using to transform the bytecode is inspired by another SO post [0]:

    // from https://stackoverflow.com/questions/51738034/javassist-insert-a-method-at-the-beginning-of-catch-block
    ControlFlow cf = new ControlFlow(ctMethod); // ctMethod == methodWithCatchClause()
    for (ControlFlow.Block block : cf.basicBlocks()) {

        ControlFlow.Catcher catchers[] = block.catchers();
        for (int i = 0; i < catchers.length; i++) {
            ControlFlow.Catcher catcher = catchers[i];

            ControlFlow.Block catcherBlock = catcher.block();

            int position = catcherBlock.position();
            int lineNumber = ctMethod.getMethodInfo().getLineNumber(position);

            ctMethod.insertAt(lineNumber + 1, "System.out.println(\"CATCH CLAUSE!\");");
        }
    }

But this code breaks in conjunction with the try-with-resources syntax. As a concrete example this code:

    public void tryWithResources() {
        try (TestAutoClosable test = new TestAutoClosable()) {
            test.doStuff();
        } catch (Exception ex) {
            handleException(ex);
        }
    }

Turns into this after code generation:

   public void tryWithResources() {
        try {
            TestAutoClosable test = new TestAutoClosable();
            Throwable var2 = null;

            try {
                System.out.println("CATCH CLAUSE!");
                test.doStuff();
            } catch (Throwable var12) {
                var2 = var12;
                throw var12;
            } finally {
                if (test != null) {
                    if (var2 != null) {
                        try {
                            test.close();
                        } catch (Throwable var11) {
                            var2.addSuppressed(var11);
                        }
                    } else {
                        test.close();
                    }
                }

            }
        } catch (Exception var14) {
            System.out.println("CATCH CLAUSE!");
            System.out.println("CATCH CLAUSE!");
            System.out.println("CATCH CLAUSE!");
            // this goes on for 15 more entries...
            this.handleException(var14);
        }

    }

This of course is causing "CATCH CLAUSE!" to be printed multiple times in odd places. It might be helpful to mention that empty catch clauses, regardless of try/catch syntax, break in a similar fashion (maybe the underlying cause is related?).

I would expect something closer to this as the end result:

    public void tryWithResources() {
        try {
            TestAutoClosable test = new TestAutoClosable();
            Throwable var2 = null;

            try {
                test.noop();
            } catch (Throwable var12) {
                System.out.println("CATCH CLAUSE!");
                var2 = var12;
                throw var12;
            } finally {
                if (test != null) {
                    if (var2 != null) {
                        try {
                            test.close();
                        } catch (Throwable var11) {
                            var2.addSuppressed(var11);
                        }
                    } else {
                        test.close();
                    }
                }

            }
        } catch (Exception var14) {
            this.handleException(var14);
        }

    }

I'm trying to figure out if I have a simple error in my code or if my approach is entirely wrong. I would appreciate any help with the matter. Thanks in advance.

[0] Javassist: insert a method at the beginning of catch block

user1661467
  • 139
  • 2
  • 11
  • Have you looked at https://stackoverflow.com/questions/17354150/8-branches-for-try-with-resources-jacoco-coverage-possible? When compiling try with resources, the generated bytecode contains many hidden exception handlers. – Antimony Oct 01 '19 at 03:08
  • It looks like the same ControlFlow.Block is used by multiple Catchers. You want to avoid adding your print statement to a block if you've already done that before. Hopefully ASM uses the same instance of the ControlFlow.Block class; if that's the case you can keep a HashSet of Blocks that you've already added your print statement to, and check in that Set before adding your print statement. – Erwin Bolwidt Oct 01 '19 at 06:08
  • The bytecode catch constructs are not exact matches of the Java language constructs. This does not only affect how `finally` or try-with-resource get compiled, but also `synchronized` blocks. In the case of try-with-resource, the code rarely maps to ordinary language constructs. As discussed in [try with resources introduce unreachable bytecode](https://stackoverflow.com/a/25746587/2711488) a lot of odd things will go on, especially with older compilers. And the result will depend on the compiler used for the original code, ecj or javac and which version… – Holger Oct 01 '19 at 13:03
  • @Antimony - My hope was that regardless of the bytecode generated from the try-with-resources syntax, I'd be able to hook into any `catch` clauses, regardless of their origin since this agent will run on systems without source code to reference. Ideally it would be very "dumb" and just seek `catch` clauses with Javassist, but maybe I was being a bit too eager with that goal. – user1661467 Oct 01 '19 at 21:57
  • @ErwinBolwidt - I actually started testing out an idea like that and while it did reduce the repeated entries by checking a `HashSet` against blocks, some of the bytecode transformations were still in the wrong place (like completely outside of the `catch` clause). It feels a bit closer to what I need, but I'm concerned my approach isn't feasible at all in the end. Thanks for your help. – user1661467 Oct 01 '19 at 21:59
  • @Holger - Are you thinking that this is just something that isn't really solvable given the nuances with bytecode, at least with Javassist and my overall approach? Maybe something lower-level is needed with straight bytecode scanning and manipulation? – user1661467 Oct 01 '19 at 22:02
  • The limitations of the bytecode do not change when using a tool at a lower lower level. Still, I prefer doing bytecode manipulations at a lower level only, as it provides a better mental model of what’s going on and what is possible. – Holger Oct 02 '19 at 09:10

0 Answers0