本文介绍Spring Security和Spring Data Jpa集成实现数据库操作审计功能的原理。
本篇中使用的示例同如何对数据库操作进行审计?
Repository
首先Spring Data Jpa中的Repository
资源是接口类型的。接口类型的文件在Java中主要用于定义规范。一般的接口中是不包含具体的实现代码和逻辑的。
如果要实现接口中定义的功能,一般需要:
-
实现该接口,在具体实现类中增加相关的处理逻辑;
-
使用动态代理机制,比如Java Dynamic或CGLIB。动态代理是基于Java规范,在运行时创建相关的Class对象并实例化。真正的处理逻辑都是在代理类中,因此在源代码中是找不到具体的实现逻辑的。
Spring Data Jpa中对Repository
的处理使用的就是动态代理技术,底层是Java Dynamic。
下图展示的是一个名为CommentRepository
对象在内存中的样子:
从图中可以看出,Spring Data Jpa是基于AOP框架开发的。那为何我们又说Spring Data Jpa是基于动态代理技术实现的框架呢?因为这两个本来就是一回事,AOP底层用的还是动态代理技术……
AOP的整个框架的核心是下图中的这个列表。正常运行时,会依次执行Pointcut
判定成功的Interceptor
类。Spring Data中的事务也是通过AOP实现的,实现的方式就是图中下标为3的那个Interceptor
,名字叫:TransactionInterceptor
。
在所有AOP切面执行完成后,如果没有被强制返回(比如某一个切面方法抛出异常了),则最终会调用target对象的对应方法。
因此在JdkDynamicAopProxy
这个类中,包含了一个名为targetSource的属性,如下图所示:
这个targetSource对一个类型是SimpleJpaRepository
的对象进行了封装。别看这个类的名字叫SimpleJpaRepository
,其实它里面包含的方法多着呢。Spring Data Jpa中默认提供的大部分数据访问方法的逻辑,都在它里面:
Entity
上面说了在Spring Data Jpa中,所有接口类型的Repository
对象,最后都通过AOP/动态代理技术,生成了一个SimpleJpaRepository
对象的代理类。那SimpleJpaRepository
对象是不是内部保存了JdbcTemplate
,并创建sql语句访问数据库呢?
其实不是,SimpleJpaRepository
本身不处理和SQL相关的任何操作。在SimpleJpaRepository
通过使用EntityManager
对象对数据进行持久化。
比如save()
方法:
@Transactional
@Override
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
这个内部属性em,就是一个类型为EntityManager
的实例。
在Spring Data Jpa中,正常使用的EntityManager
的实现是SessionImpl
,继承关系简化后如下图所示:
EntityManager
用于管理所有和数据库交互的操作。结合上面说的Repository
内部结构能看出,对Repository
的操作是具有事务属性的。
因此,从事务隔离性角度来考虑,EntityManager
应该是线程独享的。在Spring Web应用中,用户的每个HTTP请求都会单独创建一个线程,这意味着EntityManager
需要为每一个用户单独创建一个实例。
怎么实现呢?听起来好像特别适合使用ThreadLocal
来管理了。当在Spring Web应用中使用Spring Data Jpa时,框架会在每次请求到来时,创建EntityManager
实例,并保存在ThreadLocal
中。后续该线程的所有Repository
对数据库的访问,都共用这个EntityManager
对象了。
给张图说明下。下面这张图展示的就是线程的所有ThreadLocal
对象了。展开的那个元素的referent叫Transactional resources。它是一个HashMap
对象,其中有个元素的value是EntityManagerHolder
类型的,它的内部保存了一个SessionImpl
实例。
SessionImpl
是EntityManger
的具体实现类,所以当前这个线程中所有和数据库进行交互的操作,都会使用这个SessionImpl
对象。
另外需要注意的是,这个SessionImpl
也是一个代理类(多看看就习惯了,Spring Data框架是AOP的天下,切面满天飞……)。
我们将SessionImpl
展开,可以看到代理类底层的target对象。这个target对象就是真正的SessionImpl
类的实例了:
在上图中,我们圈出了一个名为fastSessionServices的属性,这个属性展开的内容如下:
我们展开下eventListenerGroup_PERSIST,里面的结构如下图所示:
可以看到,其内部属性上保存了一个名为preCreates的HashMap
对象。这个对象的key值是我们定义的Entity
实体类:
package org.example.entity;
...
@Entity
@Data
@EntityListeners(AuditingEntityListener.class)
public class Comment {
@Id @GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
...
@CreatedBy private String creator;
@CreatedDate private Date createdDate;
@LastModifiedBy private String modifier;
@LastModifiedDate private Date ModifiedDate;
}
value是AuditingEntityListener
的touchForCreate
方法,这个方法就是为了补足创建人、创建时间、修改人、修改时间四个属性的。
@PrePersist
public void touchForCreate(Object target) {
Assert.notNull(target, "Entity must not be null!");
if (handler != null) {
AuditingHandler object = handler.getObject();
if (object != null) {
object.markCreated(target);
}
}
}
嗯……可以畅想一下:
用户在页面上提交创建评论的请求,请求到达服务器端。在服务器端处理后,最终调用SimpleJpaRepository
方法,触发EntityManager
(即SessionImpl
)的persist
方法持久化到数据库中。
SessionImpl
内部保存了一个映射表。该映射表要求:如果保存的实体类是Comment
类型的,在persist
方法执行前,则需要先执行AuditingEntityListener
的touchForCreate
的方法。
这个touchForCreate
方法,完成了将创建人、创建时间、修改人、修改时间四个字段补全的操作。
跑一下试试
首先在浏览器上输入,提交创建一条评论的请求。请求到达服务器端,可以Comment
对象和审计相关的四个属性都是空的。
按照我们的分析,直接在SimpleJpaRepository
的save
方法上增加断点,如下图所示,四个属性依旧是空的:
现在应该需要进入到SessionImpl
中了,我们增加相关位置断点:
成功接住,依旧为空。下面程序应该要进入AuditingEntityListener
的touchForCreate
方法,开始补充这四个属性:
可以看出,AuditingEntityListener
会调用一个名为jpaAuditingHandler的bean对象的markCreated
方法。这个jpaAuditingHandler在Spring Data Jpa中就一个实现类AuditingHandler
。
在AuditingHandler
的markCreated
方法中增加断点,继续执行程序:
这个AuditingHandler
对象,使用IoC容器的自动装配功能,将我们在Application
中定义的SpringSecurityAuditorAware
对象注入。后面的逻辑就不用跟了,闭着眼睛都能自己补全……
到此,和数据库审计相关的四个属性就补充进来了:
之后就是保存入库,没有啥特别需要说明的。
缺了的部分
本篇初略介绍了应用跑起来时,整个数据流是怎么处理的。还缺了一部分:整个框架在系统启动时是如何构建的,关键对象的属性又是进行初始化的?这个后续再补充吧。