Context

It is a well known best practice to execute threads with the help of ExecutorService.
This way, one can create a pool of reusable threads to execute the work that needs to be done in separate threads.
For example, if we want to create an executor that executes only one thread at a time, we can use the following snippet

private ExecutorService executor = Executors.newSingleThreadExecutor()

This will create a pool of only thread, meaning that only one thread will be executed at a time no matter how many we submit through it with the help of submit() method.
The submit method can receive either a Runnable or a Callable as argument.

Here is a good explanation of the differences between Runnable and Callable in java: https://www.baeldung.com/java-runnable-callable
In short, if you want to return a value (for example, an Integer) from your thread, you can use Callable for that purpose.

Problem

Now that we set the stage, let’s say that you want to intercept the moments where the Executor will start and stop a given thread (for example, for logging purposes).
More precisely, if you submit 10 threads, they will be executed only when the size of the pool allows, not all at once.

If you are using Runnable, then (as google will immediately tell you) this is simply a matter of implementing a custom ThreadPoolExecutor which will Override the methods: afterExecute and beforeExecute

public class JobThreadPoolExecutor extends ThreadPoolExecutor{

@Override
protected void beforeExecute(Runnable r, Throwable t){
//DO stuff here
}

@Override
protected void afterExecute(Runnable r, Throwable t){
//DO stuff here
}
}

As you can see, the method receives a Runnable.
But if you want to work with Callable objects, then you will need to do a bit more work.

If you dig into the implementation of the ThreadPoolExecutor, you will see that it will take your Callback and tranform it to a FutureTask, which will be then eligible for interception in our above described methods (because it’s a subclass of RunnableFuture). The method responsible for this is: newTaskFor. We have to Override this one as well and return a custom object which I will shortly describe.

@Override
protected <T> RunnableFuture<T> newTaskFor( Callable<T> callable ){
return new JobFutureTask<T>(callable);
}

JobFutureTask is a new class that we create and which extends from FutureTask . FutureTask receives a callable in the constructor. Thus, we can create a new Callable field and save it here.

public class JobFutureTask<T> etends FutureTask<T>{
private Callable<T> callable;

public JobFutureTask(Callable<T> callable){
super(callable);
this.callable = callable;
}

public Callable<T> getCallable(){
return this.callable;
}
}

Now, we can intercept JobFutureTask in afterExecute or beforeExecute and retrieve the Callable object from it.

@Override
protected void beforeExecute(Thread t, Runnable r){
super.beforeExecute(t, r);
//Check types here
//Cast Runnable to JobFutureTask
JobFutureTask<Integer> jobFutureTask = JobFutureTask.class.cast(r);
//Retrieve our Callable object
Callable<Integer> callable = jobFutureTask.getCallable();
//Do stuff with our callable object
}