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



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

@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对象应包含来自授权User对象的userId
  • Log对象应该包含HttpServletRequest对象的一些属性
  • 保存操作和日志创建操作应该是原子操作。也就是说,如果一个foo对象保存在对象中,我们应该在数据库中有一个相应的日志,指示用户和操作的其他属性

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

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

我可以将事务注释移到控制器上,这在我读过的许多地方都是不建议的。

我还读过关于使用springAOP和拦截器完成这项工作的文章,对此我几乎没有经验。但他们使用的是服务类中已经存在的信息,我不知道如何将信息从HttpServletRequest或授权的User传递给拦截器。

我感谢任何方向或示例代码来满足这种情况下的需求。

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

  1. 将Log对象不引人注目地传递给服务类
  2. 创建基于AOP的拦截器,开始将Log实例插入DB
  3. 维护AOP拦截器(事务拦截器和日志拦截器)的顺序,以便首先调用事务拦截器。这将确保用户插入和日志插入发生在单个事务中

1.通过日志对象

您可以使用ThreadLocal来设置日志实例。

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.日志拦截器了解spring AOP的工作原理(http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop-api.html)

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

@Log
public Foo saveFoo(Foo foo) {}

b) 创建org.opalliance.intercept.MethodInterceptor.的实现LogIntelceptor(充当建议)

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) 连线切入点&顾问。

<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.拦截器(事务和日志)的排序

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

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

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

  1. @Loggable注释,将方法标记为创建日志的一次
  2. TransactionTemplate,我们将使用它以编程方式控制事务
  3. 简单的Aspect,它将把每件事都放在它的位置

因此,首先让我们创建注释

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

您的控制器保持不变。

如果在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中。

最新更新