美文网首页springbootJava后端Web基础
spring data jpa 工作流程及原理

spring data jpa 工作流程及原理

作者: 打不开的回忆 | 来源:发表于2018-10-25 17:44 被阅读289次

    最近帮同事看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注解的类),遍历装配。为每一个接口创建相关实例。

    1. SimpleJpaRespositry——用来进行默认的DAO操作,是所有Repository的默认实现
    2. JpaRepositoryFactoryBean——装配bean,装载了动态代理Proxy,会以对应的DAO的beanName为key注册到DefaultListableBeanFactory中,在需要被注入的时候从这个bean中取出对应的动态代理Proxy注入给DAO
    3. 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:下一篇介绍开发中遇到的实际操作问题。
    本文只梳理源码流程,并未深入研究,如有不正之处请联系作者进行修改,万分感谢!

    微信: 728961272908841360.jpg

    相关文章

      网友评论

      本文标题:spring data jpa 工作流程及原理

      本文链接:https://www.haomeiwen.com/subject/thqctqtx.html