Spring Aspects Ordering

A lot of features of Spring framework rely on aspects to add solutions to cross-cutting concerns. These aspects are usually enabled through annotations. This gives a nice declarative way of handling cross-cutting concerns without impacting your actual code.

A problem with this, however, arises when you need multiple aspects on the same method, since annotations don’t have any order. Consider this example below. We only want the Timed annotation to apply when we get a cache miss.

@Service
public class SomeRemoteClient {
    @Cacheable("nameOfCache")
    @Timed("timers")
    public String remoteCall() {
        // do some expensive work ...
        return "Hello World!";
    }
}

As per the documentation, the only way to control the invocation order of the aspects is to either have the annotation implement Ordered or the aspect implement Ordered. However, since both the Timed and Cacheable annotations and their corresponding aspects are coming from libraries, you cannot have them implement Ordered.

There are a few solutions to this, unfortunately all of them are quite “hacky”.

Solution 1 - Split your classes

Split your method call into separate classes and let the call chain define the order. It needs to be different classes as Spring will only proxy calls in between bean methods, but not method calls within the same bean.

This has the drawback of being verbose, and it gives you very convoluted class names. If you need more aspects than two, it becomes even more annoying. I believe this is the least hacky solution. It is however the most verbose one.

@Service
public class SomeRemoteClient {
    private final TimedRemoteClient timedRemoteClient;

    public SomeRemoteClient(TimedRemoteClient timedRemoteClient) {
        this.timedRemoteClient = timedRemoteClient;
    }

    @Cacheable("nameOfCache")
    public String remoteCall() {
        return timedRemoteClient.remoteCall();
    }
}

@Service
class TimedRemoteClient {
    @Timed("timers")
    String remoteCall() {
        // do some expensive work ...
        return "Hello World!";
    }
}

Solution 2 - Autowire a “self-reference” and split your method

This solution is a “hack” solution to make “solution 1” work with method calls within the same class. By autowireing a “self reference” you can perform bean method calls within the same class and Spring will correctly proxy those calls.

It is slightly less verbose than “solution 1” but instead slightly more “hacky”.

@Service
public class RemoteClient {
    @Lazy
    @Autowired
    private RemoteClient selfReference;

    @Cacheable("nameOfCache")
    public long remoteCall() {
        return selfReference.timedRemoteCall();
    }

    // Note that this method cannot be private as the
    // Timed aspect doesn't pick up private methods
    @Timed("timers")
    long timedRemoteCall() {
        // do some expensive work ...
        return "Hello World!";
    }
}

Solution 3 - Write code instead of using annotations

Some of the solutions to cross-cutting concerns are supposed to be triggered from annotations. Others also have a way of triggering from regular code. For example, @Transactional has TransactionTemplate and @Retryable has RetryTemplate.

For the ones that don’t have ready-made code solutions, you can write your own little adapter to trigger them from code. For example, @Timed doesn’t have a corresponding code solution that does the same thing. But it’s pretty easy to write one yourself.

@Service
public class TimerTemplate {
    private final MeterRegistry meterRegistry;

    public TimerTemplate(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    public <T> T withTimer(String className, 
                           String methodName, 
                           Supplier<T> operation) {
        Tags  tags = Tags.of("class", className, "method", methodName);
        Timer timer = meterRegistry.timer("timers", tags);
        return timer.record(operation);
    }
}

@Service
public class RemoteClient {
    private final TimerTemplate timerTemplate;

    public RemoteClient(TimerTemplate timerTemplate) {
        this.timerTemplate = timerTemplate;
    }

    // Please note you can still have one aspect triggered
    // by annotation, this will be the outermost aspect
    @Cacheable("nameOfCache") 
    public long remoteCall() {
        return timerTemplate.withTimer(getClass().getName(), "remoteCall", () -> {
            // do some expensive work ...
            return "Hello World!";
        });
    }
}
Written on July 7, 2022

Tags: Spring, Java