Auditing 及其事件详解
Auditing 翻译过来是审计和审核,Spring 的优秀之处在于帮我们想到了很多繁琐事情的解决方案,我们在实际的业务系统中,针对一张表的操作大部分是需要记录谁什么时间创建的,谁什么时间修改的,并且能让我们方便的记录操作日志。Spring Data JPA 为我们提供了审计功能的架构实现,提供了四个注解专门解决这件事情:
- @CreatedBy 哪个用户创建的。
- @CreatedDate 创建的时间。
- @LastModifiedBy 修改实体的用户。
- @LastModifiedDate 最后一次修改时间。
Auditing 如何配置
我们以一个快速的例子,看看它是怎么配置生效的。
(1)先新建一个 @Entity:UserCustomerEntity 里面的写法如下。
@Entity
@Table(name = "user_customer", schema = "test", catalog = "")
@EntityListeners(AuditingEntityListener.class)
public class UserCustomerEntity {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@CreatedDate
@Column(name = "create_time", nullable = true)
private Date createTime;
@CreatedBy
@Column(name = "create_user_id", nullable = true)
private Integer createUserId;
@LastModifiedBy
@Column(name = "last_modified_user_id", nullable = true)
private Integer lastModifiedUserId;
@LastModifiedDate
@Column(name = "last_modified_time", nullable = true)
private Date lastModifiedTime;
@Column(name = "customer_name", nullable = true, length = 50)
private String customerName;
@Column(name = "customer_email", nullable = true, length = 50)
private String customerEmail;
......
}
@Entity 实体中我们需要做两点:
- 相应的字段添加 @CreatedBy、@CreatedDate、@LastModifiedBy and @LastModifiedDate注解。
- 增加 @EntityListeners(AuditingEntityListener.class)。
(2)实现 AuditorAware 接口告诉 JPA 当前的用户是谁。
实现 AuditorAware 接口,实现 getCurrentAuditor 方法,返回一个 Integer 的 user ID。以下代码介绍了两种做法:
public class MyAuditorAware implements AuditorAware<Integer> {
/**
* Returns the current auditor of the application.
* @return the current auditor
*/
@Override
public Integer getCurrentAuditor() {
// 第一种方式:如果我们集成了spring的Security,我们直接通过如下方法即可获得当前请求的用户ID.
// Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
// if (authentication == null || !authentication.isAuthenticated()) {
// return null;
// }
// return ((LoginUserInfo) authentication.getPrincipal()).getUser().getId();
//第二种方式通过request里面取或者session里面取
ServletRequestAttributes servletRequestAttributes =
(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
return (Integer) servletRequestAttributes.getRequest().getSession().getAttribute("userId");
}
}
而 AuditorAware 的源码如下:
public interface AuditorAware<T> {
T getCurrentAuditor();
}
通过实现 AuditorAware 接口的 getCurrentAuditor() 方法告诉 JPA 当前的用户是谁,里面实现方法千差万别,作者举例了两种最常见的:
- 通过 Security 取。
- 通过 Request 取。
(3)通过 @EnableJpaAuditing 注解开启 JPA 的 Auditing 功能。
并且告诉应用 AuditorAware 的实现类是谁,也就是我们通过 @Bean 注解把上面的实现类放到 Spring 的 Bean 管理里面,当然了也可以上面的类加上 @Component。具体配置方式如下:
@SpringBootApplication
@EnableJpaAuditing
public class QuickStartApplication {
public static void main(String[] args) {
SpringApplication.run(QuickStartApplication.class, args);
}
@Bean
public AuditorAware<Integer> auditorProvider() {
return new MyAuditorAwareImpl();
}
}
验证结果如下。
通过以上的三步,我们已经完成了 auting 的配置,通过 userCustomerRepository.save(new UserCustomerEntity("1","Jack")); 的执行,我们看数据库里面的 4 个字段已经给填上去了。
@MappedSuperclass
实际工作中我们还会对上面的实体部分进行改进,引入 @MappedSuperclass 注解,我们将 @Id、@CreatedBy、@CreatedDate、@LastModifiedBy and @LastModifiedDate 抽象到一个公用的基类里面,方便公用和形成每个表的字段约束。可以将其放到我们公司的框架代码上,对表设计形成统一的强约束。
步骤如下:
(1)改进后我们新增一个 AbstractAuditable 的抽象类:
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AbstractAuditable {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@CreatedDate
@Column(name = "create_time", nullable = true)
private Date createTime;
@CreatedBy
@Column(name = "create_user_id", nullable = true)
private Integer createUserId;
@LastModifiedBy
@Column(name = "last_modified_user_id", nullable = true)
private Integer lastModifiedUserId;
@LastModifiedDate
@Column(name = "last_modified_time", nullable = true)
private Date lastModifiedTime;
......
}
(2)而我们每个需要 Auditing 的实体只需要继承 AbstractAuditable 即可。
内容如下:
@Entity
@Table(name = "user_customer", schema = "test", catalog = "")
public class UserCustomerEntity extends AbstractAuditable {
@Column(name = "customer_name", nullable = true, length = 50)
private String customerName;
@Column(name = "customer_email", nullable = true, length = 50)
private String customerEmail;
......}
Auditing 原理解析
(1)我们先看一下关键的几个源码的关系图:
(2)AuditingEntityListener 的源码如下:
@Configurable
public class AuditingEntityListener {
private ObjectFactory<AuditingHandler> handler;
public void setAuditingHandler(ObjectFactory<AuditingHandler> auditingHandler) {
Assert.notNull(auditingHandler, "AuditingHandler must not be null!");
this.handler = auditingHandler;
}
//在新增之前通过handler来往我们的@Entity里面的auditor的那些字段塞值。
@PrePersist
public void touchForCreate(Object target) {
if (handler != null) {
handler.getObject().markCreated(target);
}
}
//在更新之前通过handler来往我们的@Entity里面的auditor的那些字段塞值。
@PreUpdate
public void touchForUpdate(Object target) {
if (handler != null) {
handler.getObject().markModified(target);
}
}
}
(3)通过调用关系图和 AuditingEntityListener,我们其实可以发现以下两点情况:
- AuditingEntityListener 通过委托设计模式,委托 AuditingHandler 进行处理,而我们看 AuditingHandler 的源码会发现,里面就是根据 ID 和 Version(后面介绍)来判断我们的对象是新增还是更新,从而来更改时间字段和 User 字段。而 User 字段是通过 AuditorAware 的实现类来取的,并且 AuditorAware 没有默认实现类,只有我们自己的实现类,也就是 AuditorAware 的实现类必须我们自己来定义,否则启动会报错。
- AuditingEntityListener 的代码如此简单,我们能不能自定义呢?答案是肯定的,通过 @PrePersist、@PreUpdate 查看源码得出,Java Persistence API 底层又帮我们提供的 Callbacks,而这些回调方法,用于侦听保存、查询(抓取)、更新和删除数据库中的数据。注解方式如下:
Type |
描述 |
@PrePersist |
新增之前 |
@PreRemove |
删除之前 |
@PostPersist |
新增之后 |
@PostRemove |
删除之后 |
@PreUpdate |
更新之前 |
@PostUpdate |
更新之后 |
@PostLoad |
加载后 |
注意:这个方法都是同步机制,一但报错将会影响所有底层代码执行。在实际工作中实现这些方法的时候,方法体里面开启异步线程,或者消息队列,来异步处理日志,或者更繁重的工作。
Listener 事件的扩展
自定义 EntityListener
随着 DDD 的设计模式逐渐被大家认可和热捧,JPA 通过这种 Listener 这种机制可以很好的实现事件分离、状体分离。假如,订单的状态变化可能对我们来说比较重要,我们需要定一个类去监听订单状态变更,通知相应的逻辑代码各自去干各自的活。
(1)新增一个 OrderStatusAuditListener 类,在相应的操作上添加 Callbacks 注解。
public class OrderStatusAuditListener {
@PostPersist
private void postPersist(OrderEntiy entity) {
//当更新的时候做一些逻辑判断,及其事件通知。
}
@PostRemove
private void PostRemove(OrderEntiy entity) {
//当删除的时候做一些逻辑判断。
}
@PostUpdate
private void PostUpdate(OrderEntiy entity) {
//当更新的时候
// entity.getOrderStatus(),做一些逻辑判断
}
}
(2)我们的订单实体变化如下:
@Entity
@Table("orders")
@EntityListeners({AuditingEntityListener.class, OrderStatusAuditListener.class})
public class OrderEntity extends AbstractAuditable{
@Enumerated(EnumType.STRING)
@Column("order_status")
private OrderStatusEnum orderStatus;
......
}
即可完成自定义 EntityListener。
实际工作记录操作日志的实例
public class ActionsLogsAuditListener {
private static final Logger logger = LoggerFactory.getLogger(ActionsLogsAuditListener.class);
@PostLoad
private void postLoad(Object entity) {
this.notice(entity, OperateType.load);
}
@PostPersist
private void postPersist(Object entity) {
this.notice(entity, OperateType.create);
}
@PostRemove
private void PostRemove(Object entity) {
this.notice(entity, OperateType.remove);
}
@PostUpdate
private void PostUpdate(Object entity) {
this.notice(entity, OperateType.update);
}
private void notice(Object entity, OperateType type) {
logger.info("{} 执行了 {} 操作", entity, type.getDescription());
//我们通过active mq 异步发出消息处理事件
ActiveMqEventManager.notice(new ActiveMqEvent(type, entity));
}
enum OperateType {
create("创建"), remove("删除"),update("修改"),load("查询");
private final String description;
OperateType(String description) {
this.description=description;
}
public String getDescription() {
return description;
}
}
}
我们通过自定义的 ActionsLogsAuditListener 来监听我们要处理日志的实体,然后将事件变更,通过消息队列进行异步处理,这样就可以完全解耦了。当然了,这里我们解耦的方式也可以通过 Spring 的事件机制进行解决。通过工作中的此示例,来帮助大家更好的理解 Audit 的机制,顺便说一下处理操作的日志的正确思路,记录当前真实发生的数据和状态,及其时间即可,具体变化了什么那是在业务展示层面上要做的事情,这里没有必要做比对的事情,记住这一点之后就会让你的日志处理实现机制豁然明朗,变得容易许多。