Detached Service Methods with Spring AOP
In a typical spring based server application there is a service layer responsible for infrastructure tasks and communication with the clients. Service methods can be long running and execution would normally block the user-interface. Putting the client side end of the call in a thread solves the blocking but leaves the network connection open while the server side of the call is active. This can lead to all sorts of time-out issues and seems to be the wrong end to fix the problem .
Using spring AOP it is remarkably easy to declaratively enable service methods to execute detached from the regular call context.
The Goal
@DeferredExecution public myServiceMethod() throws DeferredExecutionException { }
This method would execute with the following sematics:
- The regular method call is wrapped in a thread
- The thread is executed
- If the thread does not finish within x seconds, raise a checked exception
- If it does finish in time, return the result of the method
This way clients of this method are forced to handle detached execution (i.e. show a progress dialog querying the server) and the client side of the call not langer lasts longer than a set time.
Implementation
Define a new Annotation
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DeferredExecution { }
Define a spring aspect (note that altough it uses AspectJ annotations, AspectJ is not required)
@Aspect public class DeferredExecutionInterceptor implements Ordered { private int order; @Pointcut("execution(* *.*(..)) && @annotation(DeferredExecution)") public void inServiceMethod() {} @Around("inServiceMethod()") public Object doDeferredExecution(ProceedingJoinPoint joinPoint) throws Throwable { JoinPointExecutionThread thread = new JoinPointExecutionThread(joinPoint); thread.start(); //wait for the thread to finish and //- either throw a DeferredExecutionException //- or return the result } @Override public int getOrder() { return order; } public void setOrder(int order) { this.order = order; } }
It is important to implement the Ordered interface since you most likely want to influence the order of the created AOP-Proxy. It should run after a security advisor and before any transaction interceptor.
The following spring config brings this aspect to life. The aspect is applied to all spring managed instances.
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"> <aop:aspectj-autoproxy /> <bean id="deferredExecutionInterceptor" class="DeferredExecutionInterceptor"> <property name="order" value="100" /> </bean> <bean id="testService" class="TestServiceImpl"> </bean> </beans>
There is one tiny bit that is optional: Nothing prevents a developer not to declare a throws clause on an annotated service method. Since client code must handle the case when execution is detached, this is crucial.
To enforce this at compile time we can use AspectJ’s declare error feature to generate a compile error when such an inconsistency is found.
Create a separate Aspect with
public aspect DeferredExecutionMethodCheckAspect { public pointcut deferredExecutionMethodWithoutThrows(): execution(@DeferredExecution public * *.* (..)) && !execution(@DeferredExecution public * *.* (..) throws DeferredExecutionException); declare error: deferredExecutionMethodWithoutThrows(): "Method must declare throws DeferredExecutionException for annotation to work"; }
When configuring compile time weaving in maven or AJDT it is crucial not to weave DeferredExecutionInterceptor, since this would make it impossible to set the order in which spring executes the AOP Proxies.
Remarks
This is of course only a rough sketch but it worked almost instantly. Note that there is no documentation concerning the thread safety of the joinPoint passed into the around advice. Since the joinPoint is only used within one thread it seems safe to to so.
In a real world scenario you would most likely not use threads directly but your choice for handling background jobs (for example backed by Quartz).
Note that there is no mechanism to handle cascaded service calls. You would most likely want only one background job created along the way. This could easily be solved using ThreadLocals…