简体   繁体   中英

Atomically maintaining service layer transactions and database logging with Spring framework

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:

  • The Log object should contain userId from the authorised User object.
  • The Log object should contain some properties from the HttpServletRequest object.
  • The save operation and log creation operation should be atomic. Ie if a foo object is saved in the object we should have a corresponding log in the database indicating the user and other properties of the operation.

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 non-obtrusively to service classes.
  2. Create AOP based interceptors to start inserting Log instances to DB.
  3. Maintaining the order to AOP interceptors (Transaction interceptor and Log interceptor) such that transaction interceptor is invoked first. This will ensure that user insert and log insert happens in a single transaction.

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).

One more example based Spring AOP but using java configuration, I hate XMLs :) Basically the idea is almost the same as mohit has but without ThreadLocals, Interceptor Orders and XML configs:) So you will need :

  1. @Loggable annotation to mark methods as the once which create the logs.
  2. TransactionTemplate which we will use to programmatically control the transactions.
  3. Simple Aspect which will put every thing in its place.

So at first lets create the annotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Loggable {}

If you are missing the TransactionTemplate configuration or EnableAspectJAutoProxy just add following to your Java Config.

@EnableAspectJAutoProxy
@Configuration
public class ApplicationContext {
    .....
    @Bean
    TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager){
        TransactionTemplate template = new TransactionTemplate();
        template.setTransactionManager(transactionManager);
        return template;
    }
}

And next we will need an Aspect which will do all the magic :)

@Component
@Aspect
public class LogAspect {

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private TransactionTemplate template;

    @Autowired
    private LogService logService;

    @Around("execution(* *(..)) && @annotation(loggable)")
    public void logIt(ProceedingJoinPoint pjp, Loggable loggable) {
        template.execute(s->{
            try{
                Foo foo = (Foo) pjp.proceed();
                Log log = new Log();
                log.setFoo(foo);
                // check may be this is a internal call, not from web
                if(request != null){
                    log.setSomeRequestData(request.getAttribute("name"));
                }
                logService.saveLog(log);
            } catch (Throwable ex) {
                // lets rollback everything
                throw new RuntimeException();
            }
            return null;
        });
    }
}

And finally in your FooService

@Loggable
public Foo saveFoo(Foo foo) {}

Your controller remains the same.

If you use LocalSessionFactoryBean or it's subclass (for instance AnnotationSessionFactoryBean ) with inside your Spring context, then the best option would be using entityInterceptor property. You have to pass instance of orh.hibernate.Interceptor interface. For instance:

// java file
public class LogInterceptor extends ScopedBeanInterceptor {

    // you may use your authorization service to retrieve current user
    @Autowired
    private AutorizationService authorizationService

    // or get the user from request
    @Autowired
    private HttpServletRequest request;

    @Override
    public boolean onSave(final Object entity, final Serializable id, final Object[] state, final String[] propertyNames, final Type[] types) {
        // get data from request
        // your save logic here
        return true;
    }
}

// in spring context    
<bean id="sessionFactory"
    class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean" destroy-method="destroy">
    <property name="dataSource" ref="dataSource"/>
    <property name="hibernateProperties">
        ....
    </property>
        ....
    <property name="entityInterceptor" ref="logInterceptor"/>
</bean>

Add the following to your web.xml (or add listener in java code, depending on what you use).

<listener>
    <listener-class>
        org.springframework.web.context.request.RequestContextListener
    </listener-class>
</listener>

Add request scope bean, so it'll be request aware.

<bean id="logInterceptor" class="LogInterceptor" scope="request">
    <aop:scoped-proxy proxy-target-class="false" />
</bean>

You can separate log data fetch from interceptor, so there will be a different request scoped component, or also you can use filters to store data in ThreadLocal .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM