使用 Spring 框架以原子方式维护服务层事务和数据库日志记录
Atomically maintaining service layer transactions and database logging with Spring framework
我有一个使用 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);
}
典型的服务 class 如下所示:
@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 和拦截器完成工作的信息,对此我经验很少。但是他们使用的信息已经存在于服务 class 中,我不知道如何将信息从 HttpServletRequest
或授权的 User
传递给拦截器。
我很感激任何指导或示例代码来满足这种情况下的要求。
要解决您的问题需要执行多个步骤:
- 以非侵入方式将日志对象传递给服务 类。
- 创建基于 AOP 的拦截器以开始将日志实例插入到数据库中。
- 维护 AOP 拦截器(事务拦截器和日志拦截器)的顺序,以便首先调用事务拦截器。这将确保用户插入和日志插入发生在单个事务中。
1.传递日志对象
您可以使用 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。拦截器的顺序(事务和日志)
确保您实现了 LogInterceptor 的 org.springframework.core.Ordered 接口和 getOrder() 方法的 return Integer.MAX_VALUE。在您的 spring 配置中,确保您的交易拦截器具有较低的订单价值。
因此,首先调用您的事务拦截器并创建一个事务。然后,您的 LogInterceptor 被调用。此拦截器首先进行调用(保存 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
.
另一个基于 Spring AOP 的示例,但使用 java 配置,我讨厌 XMLs :) 基本上这个想法几乎与 mohit
相同,但没有ThreadLocals、拦截器命令和 XML 配置:)
所以你需要:
@Loggable
注释将方法标记为创建日志的一次。
TransactionTemplate
我们将使用它以编程方式控制交易。
- 简单
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 和 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);
}
典型的服务 class 如下所示:
@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 和拦截器完成工作的信息,对此我经验很少。但是他们使用的信息已经存在于服务 class 中,我不知道如何将信息从 HttpServletRequest
或授权的 User
传递给拦截器。
我很感激任何指导或示例代码来满足这种情况下的要求。
要解决您的问题需要执行多个步骤:
- 以非侵入方式将日志对象传递给服务 类。
- 创建基于 AOP 的拦截器以开始将日志实例插入到数据库中。
- 维护 AOP 拦截器(事务拦截器和日志拦截器)的顺序,以便首先调用事务拦截器。这将确保用户插入和日志插入发生在单个事务中。
1.传递日志对象
您可以使用 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。拦截器的顺序(事务和日志)
确保您实现了 LogInterceptor 的 org.springframework.core.Ordered 接口和 getOrder() 方法的 return Integer.MAX_VALUE。在您的 spring 配置中,确保您的交易拦截器具有较低的订单价值。
因此,首先调用您的事务拦截器并创建一个事务。然后,您的 LogInterceptor 被调用。此拦截器首先进行调用(保存 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
.
另一个基于 Spring AOP 的示例,但使用 java 配置,我讨厌 XMLs :) 基本上这个想法几乎与 mohit
相同,但没有ThreadLocals、拦截器命令和 XML 配置:)
所以你需要:
@Loggable
注释将方法标记为创建日志的一次。TransactionTemplate
我们将使用它以编程方式控制交易。- 简单
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) {}
您的控制器保持不变。