3

I am searching for a practical solution for the following problem:

  • An external library provides components as base classes.
  • Custom components are made by extending those base classes.
  • The base classes break when the implementations throw unhandled exceptions.
  • The base classes source code is not available. Only a binary jar.

What I am looking for is to have a generic AOP error handling advice. It would wrap the code of every method that is a direct override or implementation of a method from the external library. Basically doing a try/catch for error recovery.

I can search for them manually of course. However our code base is extensive. And so are the possibilities to override methods of the external library classes. Hence thinking about AOP to help me out.

Load time weaving is ruled out because it concerns a web application. It might not be possible to add a JVM agent with the application server. So it must be compile time weaving.

Does somebody know how to do this with AspectJ ?

Jan Goyvaerts
  • 2,913
  • 4
  • 35
  • 48

1 Answers1

5

Assuming that your external library classes/interfaces are in a certain package such as org.external.library or in any of its subpackages, you can express those classes/interfaces in AspectJ syntax as org.external.library..*+. The .. means "include subpackages", the * means "all classes", the + means "include subclasses". Now append .*(..) and you get all methods regardless of name and number of arguments for those classes.

Now for example the pointcut

execution(!static * org.external.library..*+.*(..))

intercepts all non-static methods in any library subclasses regardless of return type.

The bad news is that you cannot limit the pointcut to overriding methods because the @Override annotation has retention type "source", i.e. it is unavailable for aspect matching. But maybe you want to narrow down the pointcut to public methods only in order to exclude errors thrown by internal methods. It depends on your situation and how you want to deal with it. Assuming that also non-overriding methods can break base components, the pointcut is good, though.

Here is a self-consistent sample:

Library classes/interfaces:

package org.external.library;

public abstract class AbstractBase {
    public abstract String getText();
    public abstract int getNumber();
    public abstract void doSomething();
}
package org.external.library;

public interface Service {
    void start();
    void stop();
    boolean isRunning();
}

A class not extending any library class:

As you can see, this class throws runtime exceptions randomly in ~50% of all cases for demo purposes. We expect them not to be handled by our aspect.

package de.scrum_master.app;

import java.util.Random;

public class MyOwnClass {
    private static final Random RANDOM = new Random();

    public String getGreeting(String recipient) {
        if (RANDOM.nextBoolean())
            throw new RuntimeException("cannot get greeting for '" + recipient + "'");
        return "Hello " + recipient + "!";
    }
}

Classes extending/implementing library classes/interfaces:

As you can see, these classes also throw runtime exceptions randomly in ~50% of all cases for demo purposes. We expect them to be handled by our aspect.

The main method demonstrates the whole setup by calling each method a few times, not handling any errors in overridden library classes, but only in our own class.

package de.scrum_master.app;

import java.util.Random;

import org.external.library.Service;

public class FooService implements Service {
    private static final Random RANDOM = new Random();
    private boolean isRunning;

    @Override
    public void start() {
        if (RANDOM.nextBoolean())
            throw new RuntimeException("cannot start");
        isRunning = true;
    }

    @Override
    public void stop() {
        if (RANDOM.nextBoolean())
            throw new RuntimeException("cannot stop");
        isRunning = false;
    }

    @Override
    public boolean isRunning() {
        return isRunning;
    }

    public void pause() {
        if (RANDOM.nextBoolean())
            throw new RuntimeException("cannot pause");
        isRunning = false;
    }

    public void resume() {
        if (RANDOM.nextBoolean())
            throw new RuntimeException("cannot resume");
        isRunning = true;
    }
}
package de.scrum_master.app;

import java.util.Random;

import org.external.library.AbstractBase;

public class Application extends AbstractBase {
    private static final Random RANDOM = new Random();

    @Override
    public String getText() {
        if (RANDOM.nextBoolean())
            throw new RuntimeException("cannot get text");
        return "Hello world!";
    }

    @Override
    public int getNumber() {
        if (RANDOM.nextBoolean())
            throw new RuntimeException("cannot get number");
        return RANDOM.nextInt(10);
    }

    @Override
    public void doSomething() {
        if (RANDOM.nextBoolean())
            throw new RuntimeException("cannot do something");
    }

    public void doSomethingElse() {
        if (RANDOM.nextBoolean())
            throw new RuntimeException("cannot do something else");
    }

    public static void main(String[] args) {
        Application application = new Application();
        FooService fooService = new FooService();
        MyOwnClass myOwnClass = new MyOwnClass();
        for (int i = 0; i < 5; i++) {
            application.getText();
            application.getNumber();
            application.doSomething();
            application.doSomethingElse();
            fooService.start();
            fooService.pause();
            fooService.isRunning();
            fooService.resume();
            fooService.isRunning();
            fooService.stop();
            try {
                myOwnClass.getGreeting("world");
                myOwnClass.getGreeting("guys");
            }
            catch (Exception e) {
                System.out.println("Uncaught by aspect: " + e);
            }
        }
    }
}

Error handling aspect:

package de.scrum_master.aspect;

public aspect ErrorHandler {
    Object around() : execution(!static * org.external.library..*+.*(..)) {
        try {
            return proceed();
        }
        catch (Exception e) {
            System.out.println(thisJoinPoint + " -> " + e);
            return null;
        }
    }
}

Console output:

The output looks a bit different each time you run the application due to the randomisation in the code:

execution(int de.scrum_master.app.Application.getNumber()) -> java.lang.RuntimeException: cannot get number
execution(void de.scrum_master.app.Application.doSomething()) -> java.lang.RuntimeException: cannot do something
execution(void de.scrum_master.app.Application.doSomethingElse()) -> java.lang.RuntimeException: cannot do something else
execution(void de.scrum_master.app.FooService.start()) -> java.lang.RuntimeException: cannot start
execution(void de.scrum_master.app.FooService.pause()) -> java.lang.RuntimeException: cannot pause
execution(void de.scrum_master.app.FooService.resume()) -> java.lang.RuntimeException: cannot resume
execution(void de.scrum_master.app.FooService.stop()) -> java.lang.RuntimeException: cannot stop
Uncaught by aspect: java.lang.RuntimeException: cannot get greeting for 'world'
execution(String de.scrum_master.app.Application.getText()) -> java.lang.RuntimeException: cannot get text
execution(int de.scrum_master.app.Application.getNumber()) -> java.lang.RuntimeException: cannot get number
execution(void de.scrum_master.app.Application.doSomething()) -> java.lang.RuntimeException: cannot do something
execution(void de.scrum_master.app.FooService.start()) -> java.lang.RuntimeException: cannot start
execution(void de.scrum_master.app.FooService.pause()) -> java.lang.RuntimeException: cannot pause
execution(void de.scrum_master.app.FooService.stop()) -> java.lang.RuntimeException: cannot stop
Uncaught by aspect: java.lang.RuntimeException: cannot get greeting for 'guys'
execution(void de.scrum_master.app.Application.doSomething()) -> java.lang.RuntimeException: cannot do something
execution(void de.scrum_master.app.FooService.start()) -> java.lang.RuntimeException: cannot start
execution(void de.scrum_master.app.FooService.pause()) -> java.lang.RuntimeException: cannot pause
Uncaught by aspect: java.lang.RuntimeException: cannot get greeting for 'guys'
execution(void de.scrum_master.app.Application.doSomethingElse()) -> java.lang.RuntimeException: cannot do something else
execution(void de.scrum_master.app.FooService.resume()) -> java.lang.RuntimeException: cannot resume
execution(void de.scrum_master.app.FooService.stop()) -> java.lang.RuntimeException: cannot stop
Uncaught by aspect: java.lang.RuntimeException: cannot get greeting for 'world'
execution(String de.scrum_master.app.Application.getText()) -> java.lang.RuntimeException: cannot get text
execution(int de.scrum_master.app.Application.getNumber()) -> java.lang.RuntimeException: cannot get number
execution(void de.scrum_master.app.Application.doSomething()) -> java.lang.RuntimeException: cannot do something
execution(void de.scrum_master.app.Application.doSomethingElse()) -> java.lang.RuntimeException: cannot do something else
execution(void de.scrum_master.app.FooService.start()) -> java.lang.RuntimeException: cannot start
execution(void de.scrum_master.app.FooService.pause()) -> java.lang.RuntimeException: cannot pause
execution(void de.scrum_master.app.FooService.stop()) -> java.lang.RuntimeException: cannot stop
kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • I'll have a look to it asap - I am a bit swamped by work for the moment. :-) But know I am very grateful for the effort made to answer me. I'll come back as soon as I'm done with this. – Jan Goyvaerts Jan 20 '15 at 12:02
  • OK ! Back again ! (Was sidetracked into a verrrrry important subject.) I wonder whether there is a possibility in AspectJ to intercept calls from (sub)package of foo to the (sub)package bar ? Because typically that's where the extension code is added. – Jan Goyvaerts Feb 23 '15 at 13:14
  • I was also unavailable for a few weeks. I would appreciate some feedback concerning my rather elaborate and extensive answer rather than feedback-less follow-up questions. Have you even tried my solution? It basically does what you requested. If you have more AspectJ questions, feel free to ask in a new StackOverflow question so as not to mix unrelated topics. – kriegaex Mar 15 '15 at 12:00