【问题标题】:Atomically maintaining service layer transactions and database logging with Spring framework使用 Spring 框架以原子方式维护服务层事务和数据库日志记录
【发布时间】:2015-11-15 13:49:29
【问题描述】:

我有一个使用 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 对象的一些属性。
  • 保存操作和日志创建操作应该是原子的。 IE。如果 foo 对象保存在对象中,我们应该在数据库中有相应的日志,指示用户和操作的其他属性。

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

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

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

我还阅读了有关使用 Spring AOP 和拦截器完成这项工作的信息,我对此几乎没有什么经验。但是他们正在使用服务类中已经存在的信息,我无法弄清楚如何将信息从HttpServletRequest 或授权User 传递给该拦截器。

我感谢任何可以满足此场景要求的指导或示例代码。

【问题讨论】:

    标签: java spring spring-mvc transactions transactional


    【解决方案1】:

    有多个步骤可以解决您的问题:

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

    1.传递 Log 对象

    您可以使用 ThreadLocal 来设置 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();
        }
    }
    

    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.aopalliance.intercept.MethodInterceptor 的实现,LogInteceptor(作为建议)。

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

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

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

    【讨论】:

    • 感谢您的回答。我认为它完美地解决了我的问题。我唯一担心的是文档建议使用 @AspectJ :“对于新应用程序,我们建议使用上一章中描述的 Spring 2.0 和更高版本的 AOP 支持”。因此,我倾向于将 MethodInterceptor 转换为类似于 Babl 答案的 Aspect。你对此有什么想法吗?
    • @nilgun:这个想法是为您提供解决问题的方法。 Spting AOP 和 Aspect 都可以满足您的目的。您可以参考这个 SO question 了解两者之间的区别 - stackoverflow.com/questions/1606559/spring-aop-vs-aspectj
    【解决方案2】:

    另一个基于 Spring AOP 的示例,但使用 java 配置,我讨厌 XML :) 基本上这个想法与 mohit 几乎相同,但没有 ThreadLocals、拦截器订单和 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 @AspectJ 而不是 Spring AOP API。但我不喜欢对 HttpServletRequest 的自动装配依赖,我认为应该在 Controller 中准备日志对象并通过 ThreadLocal 传递(与 mohit 的解决方案一样)。还应该删除对 Foo 类的依赖。这样,切面将更加可重用。
    • 如果您不想将转换逻辑放在控制器层,您可以实现一个将任何对象转换为 Log 的服务,将服务连接到您的方面并从 logIt 调用转换服务() 方法。
    • nilgun 基本上没有必要使用定制的 ThreadLocals,因为 Spring 会为您使用 RequestContextHolder。并且可以轻松删除对Foo 的依赖,但为此我们需要有关项目域模型的更多信息。 MounitReq 是的,这也是个好主意。
    • 我看到的问题是,如果您将 Web 请求和日志方面捆绑在一起,但后来有 Web 请求以外的服务客户端(例如队列消费者或 rpc 调用),您将不得不添加更多自动连接在这方面管理日志创建的依赖项。 (我什至不确定这些来源的信息是否可以自动连接到方面。)
    • 与@MounirReg 的建议一样,我可以创建一个ObjectToLogConverterFactory,但我不太明白如何让对象传递给这个工厂。是否通过自动装配依赖和更多逻辑来确定它是哪个自动装配属性的建议?相反,在我拥有信息的 ThreadLocal 中设置 Log 对象并从日志方面删除这些依赖项对我来说看起来更干净。
    【解决方案3】:

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

    【讨论】:

      猜你喜欢
      • 2020-11-17
      • 1970-01-01
      • 2015-11-27
      • 1970-01-01
      • 1970-01-01
      • 2020-11-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多