I'm attempting to inject a context for tracing into newly created Callable objects using @Around advice:
@Aspect
@Configurable
public class TracingAspect {
@Around("execution(java.util.concurrent.Callable+.new(..))")
public Callable wrapExecutor(ProceedingJoinPoint pjp) throws Throwable {
Context context = Context.current();
return context.wrap((Callable) pjp.proceed());
}
}
When the weaver encounters a prospective joinpoint such as the anonymous Callable implementation in the following example:
public class Foo {
private ExecutorService threadpool = Executors.newFixedThreadPool(10);
public Future<String> doStuffAsync() throws InterruptedException {
return threadpool.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println(42);
return 42;
}
});
}
}
I get the following error message from the Aspectj weaver:
error at foo/bar/tracing/aspect/TracingAspect.java::0 incompatible return type applying to constructor-execution(void foo.bar.Foo$1.<init>(foo.bar.Foo))
What am I doing wrong here?
Firstly, you cannot replace a returned object from within the constructor execution()
, because a constructor is not a normal method which could e.g. have a supertype or interface as return type. A constructor always returns exactly the object of the defined type, nothing else. This is not even an AspectJ limitation but a JVM limitation. With a byte code engineering library such as ASM you would have the same limitation.
So the best you can do in AspectJ is to replace the returned object in the constructor call()
, but the object must also match the expected type. Unfortunately, OpenTelemetry returns a lambda instance which cannot be cast to the exact anonymous Callable
subtype you have in your code. This means, there is nothing you can do here with that kind of code structure.
As a workaround, you can intercept calls to methods taking Callable
instances, such as ExecutorService.submit(Callable)
. You just need to make sure to capture all relevant ones. For example:
package de.scrum_master.app;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Callable;
public class Application {
private ExecutorService threadpool = Executors.newFixedThreadPool(10);
public Future<Integer> doStuffAsync() throws InterruptedException {
return threadpool.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 42;
}
});
}
public Future<Integer> doStuffLambdaAsync() throws InterruptedException {
return threadpool.submit(() -> 77);
}
public static void main(String[] args) throws Exception {
Application app = new Application();
System.out.println("Future value = " + app.doStuffAsync().get());
System.out.println("Future value (lambda) = " + app.doStuffLambdaAsync().get());
}
}
package de.scrum_master.aspect;
import java.util.concurrent.Callable;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import io.opentelemetry.context.Context;
@Aspect
public class TracingAspect {
@Around("call(* java.util.concurrent.ExecutorService.submit(*)) && args(callable)")
public Object wrapExecutor(ProceedingJoinPoint pjp, Callable<?> callable) throws Throwable {
System.out.println(pjp);
Context context = Context.current();
return pjp.proceed(new Object[] { context.wrap(callable) });
}
}
call(Future java.util.concurrent.ExecutorService.submit(Callable))
Future value = 42
call(Future java.util.concurrent.ExecutorService.submit(Callable))
Future value (lambda) = 77
Of course you could also filter the intercepted calls for callables with certain properties, if you are able to determine those with any accuracy, e.g. from which classes in which packages the calls were made etc.