使用Spring框架原子地维护服务层事务和数据库日志记录。
使用Spring框架原子地维护服务层事务和数据库日志记录。
我有一个使用Spring和Hibernate实现的Web应用程序。应用程序中的典型控制器方法如下所示:\n
@RequestMapping(method = RequestMethod.POST) public @ResponseBody Foo saveFoo(@RequestBody Foo foo, HttpServletRequest request) throws Exception { // 授权 User user = getAuthorizationService().authorizeUserFromRequest(request); // 服务调用 return fooService.saveFoo(foo); }
\n而典型的服务类如下所示:\n
@Service @Transactional public class FooService implements IFooService { @Autowired private IFooDao fooDao; @Override public Foo saveFoo(Foo foo) { // ... } }
\n现在,我想在每次保存Foo
对象时创建一个Log
对象并将其插入到数据库中。以下是我的要求:\n
- \n
Log
对象应包含来自授权的User
对象的userId
。Log
对象应包含HttpServletRequest
对象的一些属性。- 保存操作和日志创建操作应该是原子的。即,如果在对象中保存了一个foo对象,我们应该在数据库中有一条相应的日志,指示操作的用户和其他属性。
\n
\n
\n
\n由于事务管理是在服务层处理的,所以在控制器中创建日志并保存它违反了原子性要求。\n我可以将Log
对象传递给FooService
,但这似乎违反了关注点分离原则,因为日志记录是一个横切关注点。\n我可以将事务注解移到控制器上,但这在我阅读的许多地方并不被建议。\n我还了解到可以使用Spring AOP和拦截器来完成这个任务,但我对此几乎没有经验。但他们使用的是已经存在于服务类中的信息,我无法弄清楚如何将HttpServletRequest
或授权的User
的信息传递给拦截器。\n我感激任何关于如何满足此场景中要求的方向或示例代码。
原因:在使用Spring框架中的LocalSessionFactoryBean
或其子类时,需要原子地维护服务层事务和数据库日志记录。需要通过entityInterceptor
属性来实现。
解决方法:创建一个实现org.hibernate.Interceptor
接口的类,例如LogInterceptor
,在其中实现保存逻辑。将该类配置为Spring上下文中sessionFactory
的entityInterceptor
属性的引用。同时,在web.xml
中添加RequestContextListener
监听器,或者在Java代码中添加监听器,以实现请求范围的Bean。可以将日志数据的获取与拦截器分离,使用不同的请求范围组件,或者使用过滤器将数据存储在ThreadLocal
中。
原因:
- 需要在服务层中实现事务的管理和数据库的日志记录。
- 需要通过注解标记需要创建日志的方法。
- 需要控制事务的执行。
解决方法:
1. 创建一个注解`@Loggable`,用来标记需要创建日志的方法。
2. 使用`TransactionTemplate`来编程式地控制事务。
3. 创建一个`Aspect`,通过切面将所有内容放到正确的位置。
4. 配置`TransactionTemplate`和`EnableAspectJAutoProxy`。
具体实现代码如下:
// 创建注解 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Loggable {} // 配置TransactionTemplate public class ApplicationContext { // ... TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager){ TransactionTemplate template = new TransactionTemplate(); template.setTransactionManager(transactionManager); return template; } } // 创建Aspect public class LogAspect { private HttpServletRequest request; private TransactionTemplate template; private LogService logService; @Before("execution(* *(..)) && @annotation(loggable)") public void logIt(JoinPoint joinPoint, Loggable loggable) { template.execute(status -> { try { Object result = joinPoint.proceed(); Log log = new Log(); log.setObject(result); if (request != null) { log.setSomeRequestData(request.getAttribute("name")); } logService.saveLog(log); } catch (Throwable ex) { throw new RuntimeException(); } return null; }); } } // 在FooService中调用saveFoo方法 public class FooService { public Foo saveFoo(Foo foo) {} } // 控制器保持不变
感谢你的解决方案。我认为它很好,我喜欢你使用Spring而不是Spring AOP API。但是我不喜欢对HttpServletRequest的依赖,我认为日志对象应该在控制器中准备好并通过ThreadLocal传递(与mohit的解决方案类似)。另外,应该删除对Foo类的依赖,这样切面将更具可重用性。
如果你不想在控制器层面上进行转换逻辑,你可以实现一个将任何对象转换为Log的服务,并将其在切面中注入并从logIt()方法中调用该转换服务。
nilgun,实际上,没有必要使用自定义的ThreadLocal,因为Spring已经为你使用RequestContextHolder来做到了。而且可以很容易地删除对Foo的依赖,但是为此我们需要获取项目的领域模型的更多信息。MounitReq,是的,这也是一个不错的主意。
我看到的问题是,如果将Web请求和日志切面绑定在一起,但以后还有其他不是来自Web请求的服务客户端(例如队列消费者或RPC调用),你将不得不添加更多的自动注入依赖项来管理该切面中的日志创建(我甚至不确定这些来源的信息是否可以自动注入到切面中)。
按照's的建议,我可以创建一个ObjectToLogConverterFactory,但我不太明白如何获取要传递给该工厂的对象。建议是通过自动注入的依赖项和一些更多的逻辑来确定它是哪个自动注入属性吗?相比之下,在我拥有信息并将其从日志切面中删除这些依赖项的情况下,将Log对象设置在ThreadLocal中更加清晰。
如果要记录服务的输出,可以使用pjp.proceed()返回的值。这个指令触发了目标服务的实际执行。如果要记录输入,可以通过pjp.getArgs()返回的对象来传递。如果要记录调用的方法名或类名,可以通过pjp.getSignature()获取。
我可以记录它们。但是我应该能够记录HTTP头信息,如果服务是从控制器调用的,或者队列的名称,如果服务是从队列消费者调用的。我希望这些信息和服务更新到数据库的日志是原子的。
Atomically maintaining service layer transactions and database logging with Spring framework
在Spring框架中原子地维护服务层事务和数据库日志的问题和解决方法
问题的出现原因:
- 需要将日志对象非侵入地传递给服务类。
- 需要创建基于AOP的拦截器来开始向数据库插入日志实例。
- 需要维护AOP拦截器的顺序(事务拦截器和日志拦截器),以便事务拦截器首先被调用。这将确保用户插入和日志插入在一个事务中发生。
解决方法:
1. 通过使用ThreadLocal来设置日志实例。
public class LogThreadLocal{ private static ThreadLocalt = 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. 日志拦截器
- 创建一个方法级别的注解(作为切入点),用于在需要记录日志的服务方法上使用。
public Foo saveFoo(Foo foo) {}
- 创建一个实现 org.aopalliance.intercept.MethodInterceptor 接口的 LogInterceptor(作为建议)。
public class LogInterceptor implements MethodInterceptor, Ordered{ 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。在Spring配置中,确保事务拦截器具有较低的顺序值。
- 这样,首先调用事务拦截器创建事务,然后调用 LogInterceptor。该拦截器首先进行调用(保存 foo),然后保存日志(从线程本地获取)。
感谢您的答案。我认为它完美地解决了我的问题。我唯一剩下的担忧是文档建议使用:"对于新的应用程序,我们建议使用在前一章中描述的Spring 2.0及更高版本的AOP支持"。因此,我倾向于将 MethodInterceptor 转换为类似Babl答案的 Aspect。您对此有什么想法吗?
- 这个解决方案是为了给出解决问题的方法。Spring AOP和Aspect都可以满足您的目的。您可以参考这个SO问题,了解两者之间的区别- stackoverflow.com/questions/1606559