spring data jpa 工作流程及原理
最近帮同事看jpa的查询问题,抽空整理了一下相关知识,本篇先介绍jpa的工作流程(原理),后续再写关于实战以及开发中常用到的各种查询。本文是基于debug源码结合自己的理解所写,可能有些细节被漏掉,也可能对某一块解释的不详细,具体的知识点建议结合本文,对源码进行阅读。也希望各位大佬留言斧正!
尝鲜
导入jpa相关jar(maven请自行搜索)
dependencies {
implementation('org.springframework.boot:spring-boot-starter-data-jpa')
runtimeOnly('mysql:mysql-connector-java')
testImplementation('org.springframework.boot:spring-boot-starter-test')
}
定义Entity和Repository以及Test测试类
#entity类
@Entity(name = "Customer")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private String address;
private String phone;
...setter getter 省略
}
#repository类
public interface CustomerRepository extends JpaRepository<Customer,Long> {
List<Customer> findCustomerByAddress(String address);
@Query("from Customer customer")
List<Customer> getCustomer();
}
#测试类
@RunWith(SpringRunner.class)
@SpringBootTest
public class JpaApplicationTests {
@Autowired
private CustomerRepository customerRepository;
@Test
public void contextLoads() {
customerRepository.findCustomerByAddress("aa");
customerRepository.findAll();
}
}
正餐
- springboot启动,初始化一些列bean
- test方法生效,进入自定义的customerRepository.findCustomerByAddress()方法
- aop拦截,注入代理执行代理方法
- 调用目标方法,返回结果
启动过程
由于引入了starter-data-jpa,自动配置,spring启动时会实例化一个Repositories,然后扫描包,找出所有继承Repository的接口(除@NoRepositoryBean注解的类),遍历装配。为每一个接口创建相关实例。
- SimpleJpaRespositry——用来进行默认的DAO操作,是所有Repository的默认实现
- JpaRepositoryFactoryBean——装配bean,装载了动态代理Proxy,会以对应的DAO的beanName为key注册到DefaultListableBeanFactory中,在需要被注入的时候从这个bean中取出对应的动态代理Proxy注入给DAO
- JdkDynamicAopProxy——动态代理对应的InvocationHandler,负责拦截DAO接口的所有的方法调用,然后做相应处理,比如findByUsername被调用的时候会先经过这个类的invoke方法
关于启动过程的初始化,始终没有整理太明白,奉上一张大佬的时序图,有兴趣的可以自己研究:(图片来源:(http://www.luckyzz.com/java/spring-data-jpa/)) proxy2 (1).png
debug源码
断点查看customerRepository,发现该接口被注入他的实现类作为代理对象,基于JDK的动态代理JdkDynamicAopProxy执行invoke方法,继续跟进,查看拦截链: chain经过这里系列的拦截器的方法后,查询的拦截器有图中的QueryExecutorMethodInterceptor和ImplementMethodInterceptor(均位于RepositoryFactorySupport内部),个人理解是repository默认的函数走后者,自定义查询走前者。
继续跟进,进入QueryExecutorMethodInterceptor的invoke方法,执行doInvoke,对方法进行判断,是否自定查询,这里我们是自定义的查询,继续看。
@Nullable
private Object doInvoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
Object[] arguments = invocation.getArguments();
if (hasQueryFor(method)) {
return queries.get(method).execute(arguments);
}
return invocation.proceed();
}
由于不会画时序图,根据自己的理解,简单瞎画了一张图,帮助理清楚思路。(画了之后确实清楚一些)希望有助于阅读理解。
jpa.png
这里前面说的初始化RepositoryFactorySupport时已经扫描所有repository,并未其选择了query策略,这里是PartTreeJpaQuery。
PartTreeJpaQuery
执行PartTreeJpaQuery.execute方法(该类继承了AbstracJpaQuery),
public Object execute(Object[] parameters) {
return doExecute(getExecution(), parameters);
}
private Object doExecute(JpaQueryExecution execution, Object[] values) {
Object result = execution.execute(this, values);
ParametersParameterAccessor accessor = new ParametersParameterAccessor(method.getParameters(), values);
ResultProcessor withDynamicProjection = method.getResultProcessor().withDynamicProjection(accessor);
return withDynamicProjection.processResult(result, new TupleConverter(withDynamicProjection.getReturnedType()));
}
protected JpaQueryExecution getExecution() {
if (method.isStreamQuery()) {
return new StreamExecution();
} else if (method.isProcedureQuery()) {
return new ProcedureExecution();
} else if (method.isCollectionQuery()) {
return new CollectionExecution();
} else if (method.isSliceQuery()) {
return new SlicedExecution(method.getParameters());
} else if (method.isPageQuery()) {
return new PagedExecution(method.getParameters());
} else if (method.isModifyingQuery()) {
return new ModifyingExecution(method, em);
} else {
return new SingleEntityExecution();
}
}
更加方法的返回值,入参,注解等等条件判断,得到一个query执行器(JpaQueryExecution)CollectionExecution,进入该execution查看其execute方法:
#JpaQueryExecution.java
public Object execute(AbstractJpaQuery query, Object[] values) {
Assert.notNull(query, "AbstractJpaQuery must not be null!");
Assert.notNull(values, "Values must not be null!");
Object result;
try {
result = doExecute(query, values);
} catch (NoResultException e) {
return null;
}
..........
}
static class CollectionExecution extends JpaQueryExecution {
@Override
protected Object doExecute(AbstractJpaQuery query, Object[] values) {
return query.createQuery(values).getResultList();
}
}
看到这是不是熟悉了,原来还是创建一个Query然后getResultList获取结果,使用jpa自定执行sql时,我们可以这么操作:EntityManager.createQuery("sql",ReturnClass),我们继续看看这个PartTreeJpaQuery是怎么创建Query的。
这里先解释一下为什么初始化时会为findCustomerByAddress这个方法注入PartTreeJpaQuery,查看AbstractJpaQuery抽象类,其有五个子类实现,
- SimpleJpaQuery 适用方法:使用@Query注解,但nativeQuery为隐式或显示声明为false,即使用JPQL语法解析。
- NativeJpaQuery 适用方法:使用@Query注解,且nativeQuery显示声明为true,即使用原生sql语法解析。
- NamedQuery 适用方法:在entity上声明@NamedQuery注解,并在repository中使用该方法。
- PartTreeJpaQuery 适用方法:未使用@Query注解的方法,即根据方法名反射entity字段获取sql。
- StoredProcedureJpaQuery 适用方法:使用@Procedure注解的存储过程方法。
#PartTreeJpaQuery.java
public Query createQuery(Object[] values) {
CriteriaQuery<?> criteriaQuery = cachedCriteriaQuery;
ParameterBinder parameterBinder = cachedParameterBinder;
ParametersParameterAccessor accessor = new ParametersParameterAccessor(parameters, values);
if (cachedCriteriaQuery == null || accessor.hasBindableNullValue()) {
JpaQueryCreator creator = createCreator(persistenceProvider, Optional.of(accessor));
criteriaQuery = creator.createQuery(getDynamicSort(values));
List<ParameterMetadata<?>> expressions = creator.getParameterExpressions();
parameterBinder = getBinder(expressions);
}
if (parameterBinder == null) {
throw new IllegalStateException("ParameterBinder is null!");
}
return restrictMaxResultsIfNecessary(invokeBinding(parameterBinder, createQuery(criteriaQuery), values));
}
private TypedQuery<?> createQuery(CriteriaQuery<?> criteriaQuery) {
if (this.cachedCriteriaQuery != null) {
synchronized (this.cachedCriteriaQuery) {
return getEntityManager().createQuery(criteriaQuery);
}
}
return getEntityManager().createQuery(criteriaQuery);
}
PartTreeJpaQuery源码如上,包含参数绑定,解析等等,利用JpaQueryCreator创建一个CriteriaQuery,丢进重载方法createQuery方法中,返回一个Query。注意查看PartTreeJpaQuery的构造函数,其中会为其定义一个PartTree对象,PartTree对象包含了sql关键字的正则解析。创建JpaQueryCreator时会传入该PartTree,CriteriaQuery就这么生成了。最后一步在重载方法里看到了entityManager.createQuery(),一切明了了。至于PartTree如何工作的,不再研究!
甜点
repository的默认方法,使用ImplementMethodInterceptor拦截器,会对每个默认方法单独实现,断点进走完拦截链后,直接进去SimpleJpaRepository的各自方法了,其中也是getQuery(),然后执行execute()方法获取的,这里不再分析。有兴趣的可以继续查看源码!
回味
导入jpa,自动配置扫描包内所有继承了Repository接口的接口,为每一个接口实例一个代理类(SimpleJpaRepository),并为每个方法确定一个query策略,已经其他所需的bean,使用自定的repository时,是使用的代理类,经过一些列的拦截器后,选取一个query执行器JpaQueryExecution,然后创建Query对象,拼接sql,组装参数等DB操作,最后返回Query.getResultList()。
基本流程就是这么多,当然对于如何初始化的,还没理清楚,即初始化时如何为每个方法分配具体的query策略,除了PartTreeJpaQuery外的其他query策略如何工作,PartTree如何解析sql等等没有深入研究。
ps:下一篇介绍开发中遇到的实际操作问题。
本文只梳理源码流程,并未深入研究,如有不正之处请联系作者进行修改,万分感谢!