I have a web application implemented using Spring and Hibernate. A typical controller method in the application looks like the following:
@RequestMapping(method = RequestMethod.POST)
public @ResponseBody
Foo saveFoo(@RequestBody Foo foo, HttpServletRequest request) throws Exception {
// authorize
User user = getAuthorizationService().authorizeUserFromRequest(request);
// service call
return fooService.saveFoo(foo);
}
And a typical service class looks like the following:
@Service
@Transactional
public class FooService implements IFooService {
@Autowired
private IFooDao fooDao;
@Override
public Foo saveFoo(Foo foo) {
// ...
}
}
Now, I want to create a Log
object and insert it to database every time a Foo
object is saved. These are my requirements:
Log
object should contain userId
from the authorised User
object. Log
object should contain some properties from the HttpServletRequest
object.Since transaction management is handled in the service layer, creating the log and saving it in the controller violates the atomicity requirement.
I could pass the Log
object to the FooService
but that seems to be violation of separation of concerns principle since logging is a cross cutting concern.
I could move the transactional annotation to the controller which is not suggested in many of the places I have read.
I have also read about accomplishing the job using spring AOP and interceptors about which I have very little experience. But they were using information already present in the service class and I could not figure out how to pass the information from HttpServletRequest
or authorised User
to that interceptors.
I appreciate any direction or sample code to fulfill the requirements in this scenario.
There are multiple steps which are to be implemented to solve your problem:
1. Passing Log object
You can use ThreadLocal to set the Log instance.
public class LogThreadLocal{
private static ThreadLocal<Log> t = new ThreadLocal();
public static void set(Log log){}
public static Log get(){}
public static void clear(){}
}
Controller:saveFoo(){
try{
Log l = //create log from user and http request.
LogThreadLocal.set(l);
fooService.saveFoo(foo);
} finally {
LogThreadLocal.clear();
}
}
2. Log Interceptor See how spring AOP works (http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop-api.html)
a) Create an annotation (acts as pointcut), @Log for method level. This annotation will be put on the service methods for which logging is to be done.
@Log
public Foo saveFoo(Foo foo) {}
b) Create an implementation, LogInteceptor (acts as the advice) of org.aopalliance.intercept.MethodInterceptor.
public class LogInterceptor implements MethodInterceptor, Ordered{
@Transactional
public final Object invoke(MethodInvocation invocation) throws Throwable {
Object r = invocation.proceed();
Log l = LogThreadLocal.get();
logService.save(l);
return r;
}
}
c) Wire the pointcut & advisor.
<bean id="logAdvice" class="com.LogInterceptor" />
<bean id="logAnnotation" class="org.springframework.aop.support.annotation.AnnotationMatchingPointcut">
<constructor-arg type="java.lang.Class" value="" />
<constructor-arg type="java.lang.Class" value="com.Log" />
</bean>
<bean id="logAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="advice" ref="logAdvice" />
<property name="pointcut" ref="logAnnotation" />
</bean>
3. Ordering of interceptors (transaction and log)
Make sure you implement org.springframework.core.Ordered interface to LogInterceptor and return Integer.MAX_VALUE from getOrder() method. In your spring configuration, make sure your transaction interceptor has lower order value.
So, first your transaction interceptor is called and creates a transaction. Then, your LogInterceptor is called. This interceptor first proceed the invocation (saving foo) and then save log (extracting from thread local).