使用Spring框架以原子方式维护服务层事务和数据库日志记录

时间:2015-11-15 13:49:29

标签: java spring spring-mvc transactions transactional

我有一个使用Spring和Hibernate实现的Web应用程序。应用程序中的典型控制器方法如下所示:

SELECT substring(firstname,1) AS first_letter
AND substring(lastname,1) AS first_letter
FROM kids
ORDER BY firstname

典型的服务类如下所示:

@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);
}

现在,我想创建一个@Service @Transactional public class FooService implements IFooService { @Autowired private IFooDao fooDao; @Override public Foo saveFoo(Foo foo) { // ... } } 对象,并在每次保存Log对象时将其插入数据库。这些是我的要求:

  • Foo对象应包含授权Log对象中的userId
  • User对象应包含Log对象的一些属性。
  • 保存操作和日志创建操作应该是原子操作。即如果foo对象保存在对象中,我们应该在数据库中有相应的日志,指示用户和操作的其他属性。

由于事务管理是在服务层中处理的,因此创建日志并将其保存在控制器中会违反原子性要求。

我可以将HttpServletRequest对象传递给Log,但这似乎违反了关注点分离原则,因为记录是一个跨领域的问题。

我可以将事务注释移动到控制器上,这在我读过的很多地方都没有建议。

我还读到了使用Spring AOP和拦截器来完成这项工作,我很少有经验。但是他们正在使用服务类中已经存在的信息,我无法弄清楚如何将信息从FooService或授权HttpServletRequest传递给那些拦截器。

我很欣赏任何方向或示例代码,以满足此方案中的要求。

3 个答案:

答案 0 :(得分:2)

要解决您的问题,需要执行多个步骤:

  1. 将非对象的Log对象传递给服务类。
  2. 创建基于AOP的拦截器以开始将日志实例插入数据库。
  3. 维护AOP拦截器(事务拦截器和Log拦截器)的顺序,以便首先调用事务拦截器。这将确保用户插入和日志插入在单个事务中发生。
  4. <强> 1。传递日志对象

    您可以使用ThreadLocal设置Log实例。

    var bluetoothle = {isInitialized:function(){setTimeout(function(){func({isInitialized:false});},20);}};
    // to execute:
    bluetoothle.isInitialized(/*callbackfunction*/);
    

    <强> 2。日志拦截器 了解spring AOP的工作原理(http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop-api.html

    a)创建注释(充当切入点),@ Log为方法级别。此注释将放在要进行日志记录的服务方法上。

    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();
        }
    }
    

    b)创建org.aopalliance.intercept.MethodInterceptor的实现,LogInteceptor(充当建议)。

    @Log
    public Foo saveFoo(Foo foo) {}
    

    c)切入点切口&amp;顾问。

    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;
        }
    }
    

    第3。拦截器的排序(交易和日志)

    确保为LogInterceptor实现org.springframework.core.Ordered接口,并从getOrder()方法返回Integer.MAX_VALUE。在弹簧配置中,确保您的事务拦截器具有较低的订单值。

    因此,首先调用您的事务拦截器并创建一个事务。然后,调用您的LogInterceptor。此拦截器首先进行调用(保存foo),然后保存日志(从本地线程中提取)。

答案 1 :(得分:2)

另一个基于Spring AOP但使用java配置的例子,我讨厌XML :)基本上这个想法与mohit几乎相同但没有ThreadLocals,Interceptor Orders和XML configs :) 所以你需要:

  1. @Loggable注释将方法标记为创建日志的一次。
  2. TransactionTemplate我们将用它来以编程方式控制交易。
  3. 简单Aspect,它将把所有东西都放在原处。
  4. 首先让我们创建注释

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

    如果您缺少TransactionTemplate配置或EnableAspectJAutoProxy只需将以下内容添加到Java配置中。

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

    接下来我们需要一个Aspect来做所有的魔术:)

    @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;
            });
        }
    }
    

    最后在你的FooService

    @Loggable
    public Foo saveFoo(Foo foo) {}
    

    您的控制器保持不变。

答案 2 :(得分:0)

如果在Spring上下文中使用LocalSessionFactoryBean或它的子类(例如AnnotationSessionFactoryBean),那么最好的选择是使用entityInterceptor属性。你必须传递orh.hibernate.Interceptor接口的实例。例如:

// 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>

将以下内容添加到web.xml(或在java代码中添加侦听器,具体取决于您使用的内容)。

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

添加请求范围bean,因此它将被请求识别。

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

您可以将日志数据提取与拦截器分开,因此将有一个不同的请求范围组件,或者您也可以使用过滤器在ThreadLocal中存储数据。