Spring data JPA实践和原理浅析

作者:张葛  职位:后端工程师

一、前言

从我们进⼊编程的世界,成为程序员到现在为⽌,总有⼏个感觉神奇和激动的时刻,其中肯定包括你第⼀次程序连上数据库可以实现 CURD 功能的时候,就算那时的我们写着千遍⼀律 JDBC 模板代码也是乐此不疲。

时代在不断进步,技术也不断在发展,市面上已经有很多优秀的数据库持久化框架供我们使用,今天我将带大家来了解JPA的使用。

 

二、基本使用

在我们现在Spring Boot横行无忌的时代,在项目中引入JPA非常简单,我们以Maven以及常用的MySQL数据库为例

在pom.xml文件添加以下依赖

  1. <parent>

  2. <groupId>org.springframework.boot</groupId>

  3. <artifactId>spring-boot-starter-parent</artifactId>

  4. <version>2.5.6</version>

  5. <relativePath/>

  6. </parent>

  7.  

  8. <!-- jpa -->

  9. <dependency>

  10. <groupId>org.springframework.boot</groupId>

  11. <artifactId>spring-boot-starter-data-jpa</artifactId>

  12. </dependency>

  13. <!-- mysql -->

  14. <dependency>

  15. <groupId>mysql</groupId>

  16. <artifactId>mysql-connector-java</artifactId>

  17. </dependency>

在Spring Boot的YML文件中添加以下内容

  1. spring:
  2. datasource:
  3. driver-class-name:
  4. com.mysql.cj.jdbc.Driver
  5. url:jdbc:mysql://localhost:3306/sakila?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8 username: root
  6. password: root

定义Entity类,我测试过程使用的数据源为MySQL官方提供的样例数据库sakila,大家可以在https://dev.mysql.com/doc/index-other.html自行下载

  1. @Entity
  2. @Data // lombok注解
  3. public class Actor {

  4.  @Id
  5.  // Column注解不是必须的,如果满足字段驼峰形式
  6.  // 与数据库字段以下划线分隔形式对应即可
  7.  @Column(name = "actor_id", nullable = false)
  8.  private Integer actorId;
  9.  
  10.  @Column(name = "first_name", nullable = false, length = 45)
  11.  private String firstName;
  12.  private String lastName;
  13.  private Timestamp lastUpdate;
  14. }

定义Respository接口,一般我们通过继承JpaRepository接口即能满足我们一般的CURD操作,如果需要支持复杂逻辑查询

比如:动态SQL;联表查询,则需要继承 JpaSpecificationExecutor 接口,并配合Specification的接口方法。

  1. @Repository

  2. public interface ActorRepository extends JpaRepository<Actor, Integer> {

  3. }

在JpaRepository的中我们可以看到有findAll、getById、findById、save、saveAll、deleteById...这些默认定义,这是JPA为我们提供的常用的一些数据操作,我们可以直接使用,极大提高了日常的开发效率。

2.1通过方法名称直接生成查询

正是因为这一便利的让人着迷的特性,让我们乐于去使用JPA这一持久化框架,接下来让我们通过几个例子去欣赏它的迷人之处吧。

2.1.1一般写法

  1. /**

  2.  * 示例1

  3.  * SQL: SELECT * FROM actor WHERE first_name = ?

  4.  * 参数名的定义不影响程序的运行

  5.  */

  6. List<Actor> findByFirstName(String name);

  7.  

  8. /**

  9.  * 示例2

  10.  * SQL: SELECT * FROM actor WHERE first_name = ? AND last_name = ?

  11.  * 如果明确知道查询结果返回唯一一条记录时,建议使用单一实体类作为返回类型

  12.  */

  13. List<Actor> findByFirstNameAndLastName(String name1, String name2);

  14.  

  15. /**

  16.  * 示例3

  17.  * SQL: SELECT * FROM actor WHERE actor_id <= ?

  18.  */

  19. List<Actor>findByActorIdLessThanEqual(Integer id);

遵照JPA的规范,通过定义类似以上接口方法的形式就可以零SQL实现我们需要的单表查询(不能实现DML操作)操作。JPA对此类查询方式有很丰富的支持,受限于篇幅,我们就不一一讲述了,详细的内容可以阅读官方文档

地址:

https://docs.spring.io/spring-data/jpa/docs/2.5.6/reference/html/#repository-query-keywords

 

Tips

1.在查询场景中,自定义的查询接口中,find关键词(也可以是search、query、get)后面必须跟随By关键词

2.Between适用于数值、日期字段,用于日期时,参数类型可以是java.util.Date或java.sql.Timestamp

  • List<Film> findByLengthBetween(Integer low, Integer up);
  • List<Film> findByLastUpdateBetween(Date startDate, Date endDate);

3.IsEmpty / IsNotEmpty只能用于集合类型的字段

4.Before或者After可用于日期、数值类型的字段

  • List<Film> findByLengthBefore(Integer length)

5.涉及到删除和修改时,需要在方法定义上加上@Modifying

 

2.2基于@Query 注解的操作

2.2.1使用JPQL

JPQL是通过Hibernate的HQL演变过来的,它和HQL语法及其相似。

  1. /**
  2.  * 示例1
  3.  * SQL: SELECT * FROM actor WHERE first_name = ?
  4.  */
  5. @Query("FROM Actor WHERE firstName = ?1")
  6. List<Actor> findByFirstName(String name);
  7.  
  8. /**
  9.  * 示例2
  10.  * SQL: SELECT * FROM actor WHERE first_name = ? AND last_name = ?
  11.  */
  12. @Query("FROM Actor WHERE firstName = ?1 AND lastName = ?2")
  13. List<Actor> findByFirstNameAndLastName(String name1, String name2);
  14.  
  15. /**
  16.  * 示例3
  17.  * SQL: SELECT * FROM actor WHERE actor_id <= ?
  18.  */
  19. @Query("FROM Actor WHERE actorId <= ?1")
  20. List<Actor> findByActorIdLessThanEqual(Integer id);
  21.  
  22. /**
  23.  * 示例4
  24.  * SQL: SELECT * FROM actor
  25.  * 不能写"SELECT *" 要写"SELECT 别名"
  26.  */
  27. @Query("SELECT a FROM Actor a")
  28. List<Actor> findAll()

通过以上例子我们发现,JPQL与SQL的区别是,SQL是面向对象关系数据库,它操作的是数据表和数据列,而JPQL操作的对象是实体对象和实体属性,区分大小写,出现的SQL关键字还是原有的意思,不区分大小写。JPQL也可以支持复杂的联表查询。下面是JPQL的基本格式:

👉SELECT 实体别名.属性名, 实体别名.属性名 FROM 实体名 AS 实体别名 WHERE 实体别名.实体属性 op 比较值

看完查询的写法,我们来看看DML操作的写法

  1. @Modifying

  2. @Query(value = "UPDATE Film SET description = :description WHERE filmId = :id")

  3. void update(@Param("id") Integer filmId, @Param("description") String description);

 

2.2.2使用原生SQL

在Query注解中,有一个属性字段nativeQuery,默认情况下为false,即为JPQL模式,如果我们设置为true,则我们可以value属性中定义原生SQL语句实现数据库操作。

  1. @Query("SELECT * FROM actor WHERE first_name = ?1", nativeQuery = true)

  2. List<Actor> findByFirstName(String name);

  3.  
  4. @Query("SELECT * FROM actor WHERE first_name = ?1 AND last_name = ?2", nativeQuery = true)

  5. List<Actor> findByFirstNameAndLastName(String name1, String name2);

  6.  
  7. @Query("SELECT * FROM actor WHERE actor_id <= ?", nativeQuery = true)

  8. List<Actor> findByActorIdLessThanEqual(Integer id);

  9.  

  10. @Query("SELECT first_name, last_name FROM actor", nativeQuery = true)

  11. List<Map<String, Object>> findAll();

  12. // List<String[]> findAll();

 

通过上述示例,我们总结几点编写原生SQL需要注意的地方

1.如果接口方法返回的是实体对象,如List<Actor>、Actor这样的,则SELECT部分不能指定字段名,必须为*

2.如果只想查询指定列的数据,方法定义时的返回类型可以是Map<String, Object>、String[],返回数据如果是多条,则用集合类嵌套

3.关于JPA接口方法的返回类型我们可以参考官方文档,地址为:

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repository-query-return-types

 

三、JPA的原理浅析

通过前面的学习,我们已经对JPA有了比较清晰的了解,不再是门外汉了,当然我们还是会有很多的疑问:

○为什么我们只是加入了一个Maven的依赖,就能直接使用JPA?

○为什么我们只是定义了一个Repository的接口就能直接使用它来实现数据库操作逻辑?

○为什么我们只是按照JPA的规范,定义了一个接口方法就能在使用时生成想要的SQL?

在接下来的学习中一一解开。

 

3.1JPA的自动配置

虽然这一节是要讲JPA的自动配置,其实本质要说的是Spring Boot自动加载配置的原理。在我们使用Spring Boot创建一个项目时,必不可少的一个注解就是@SpringBootApplication,看着它我们既熟悉又陌生。我们通过它的源码来一探究竟。

  1. package org.springframework.boot.autoconfigure;

  2.  
  3. @Target(ElementType.TYPE)

  4. @Retention(RetentionPolicy.RUNTIME)

  5. @Documented

  6. @Inherited

  7. @SpringBootConfiguration

  8. @EnableAutoConfiguration

  9. @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),

  10.   @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

  11. public @interface SpringBootApplication {


  12.  @AliasFor(annotation = EnableAutoConfiguration.class)

  13.  Class<?>[] exclude() default {};

  14.  
  15.  @AliasFor(annotation = EnableAutoConfiguration.class)

  16.  String[] excludeName() default {};

  17.  
  18.  @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")

  19.  String[] scanBasePackages() default {};

  20.  
  21.  @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")

  22.  Class<?>[] scanBasePackageClasses() default {};

  23.  
  24.  @AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")

  25.  Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

  26.  
  27.  @AliasFor(annotation = Configuration.class)

  28.  boolean proxyBeanMethods() default true;

  29.  
  30. }

 

通过源码我们发现其实SpringBootApplication也被一些注解所修饰,其中就有EnableAutoConfiguration注解,一看名字我们就知道它就是我们今天要找的正主。同样的,我们也不会放过它的源码的。

 

  1. package org.springframework.boot.autoconfigure;

  2.  

  3. @Target(ElementType.TYPE)

  4. @Retention(RetentionPolicy.RUNTIME)

  5. @Documented

  6. @Inherited

  7. @AutoConfigurationPackage

  8. @Import(AutoConfigurationImportSelector.class)

  9. public @interface EnableAutoConfiguration {

  10.  

  11.  String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";


  12.  Class<?>[] exclude() default {};


  13.  String[] excludeName() default {};

  14.  

  15. }


我们又看到了熟悉的身影,@Import注解,它意味着在我们的IOC容器中引入一个AutoConfigurationImportSelector对象。

通过源码我们又发现AutoConfigurationImportSelector最终实现了ImportSelector接口。因此在程序的启动过程中,会执行selectImports方法,在当前的实现中,这个方法的主要逻辑是去读取一个 spring.factories下key为EnableAutoConfiguration对应的全限定名的值。

spring.factories里面配置的那些类,主要作用是告诉 Spring Boot这个stareter所需要加载哪些xxxAutoConfiguration类,也就是你真正的要自动注册的那些bean或功能。

而在SpringBoot中的META-INF/spring.factories(完整路径:spring-boot/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories)中关于EnableAutoConfiguration的这段配置如下 :

可以发现有JpaRepositoriesAutoConfiguration和HibernateJpaAutoConfiguration帮我们配置了JPA 相关的配置。至此,我们的第一个疑惑可以解开了。

 

3.2 SimpleJpaRepository类

接下来我们要来解决第二个疑惑,其实本节的标题已经告诉了我们答案。先上源码:

  1. @Repository

  2. @Transactional(readOnly = true)

  3. public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {

  4. }

 

我们不用看具体实现的方法逻辑了,单从SimpleJpaRepository的声明来看,它是一个类,不是抽象类是一个实现了JpaRepositoryImplementation接口的类

实际上JpaRepositoryImplementation接口也继承了JpaRepository和JpaSpecificationExecutor接口。

到此,我们可以大胆的猜测,为什么我们定义的repository接口只是继承了JpaRepository和JpaSpecificationExecutor接口就能使用一系列的接口调用,其实是SimpleJpaRepository类帮我们做了要做的事情。

那到底是不是,如果是的,那JPA是怎么做到了的呢?我们继续一步一步分析。首先我们得有一个分析的起点,上一节我们讲了JPA的自动配置,那我们是不是需要看看它到底配置了什么,看看能不能找到我们想要的。我们直接看JpaRepositoriesAutoConfiguration,因为它的名字里带Repositories。

 

  1. @Configuration(proxyBeanMethods = false)

  2. @ConditionalOnBean(DataSource.class)

  3. @ConditionalOnClass(JpaRepository.class)

  4. @ConditionalOnMissingBean({ JpaRepositoryFactoryBean.class, JpaRepositoryConfigExtension.class })

  5. @ConditionalOnProperty(prefix = "spring.data.jpa.repositories", name = "enabled", havingValue = "true",

  6.   matchIfMissing = true)

  7. @Import(JpaRepositoriesRegistrar.class)

  8. @AutoConfigureAfter({ HibernateJpaAutoConfiguration.class, TaskExecutionAutoConfiguration.class })

  9. public class JpaRepositoriesAutoConfiguration {

  10. }

我们好像运气不错,果然发现有一个Import注解引入了一个JpaRepositoriesRegistrar类,从名字看,JPA repository注册器,好像越来越接近了,直接上源码:

  1. class JpaRepositoriesRegistrar extends AbstractRepositoryConfigurationSourceSupport {

  2. }

  3.  
  4. public abstract class AbstractRepositoryConfigurationSourceSupport

  5.   implements ImportBeanDefinitionRegistrar, BeanFactoryAware, ResourceLoaderAware, EnvironmentAware {

  6.  
  7.  private ResourceLoader resourceLoader;

  8.  
  9.  private BeanFactory beanFactory;

  10.  
  11.  private Environment environment;

  12.  
  13.  @Override

  14.   public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,

  15.  BeanNameGenerator importBeanNameGenerator) {

  16.   RepositoryConfigurationDelegate delegate = new RepositoryConfigurationDelegate(

  17.   getConfigurationSource(registry, importBeanNameGenerator), this.resourceLoader, this.environment);

  18.   delegate.registerRepositoriesIn(registry, getRepositoryConfigurationExtension());

  19. }


  20. @Override

  21. public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

  22. registerBeanDefinitions(importingClassMetadata, registry, null);

  23. }


  24.  // 其他源码

  25.   ...

  26. }

其实分析了这么久,如果我们经常去看看Spring系的一些源码,会发现很多老朋友。我们看到其中一个关键接口ImportBeanDefinitionRegistrar,它的关键方法就是registerBeanDefinitions了,就是按照我们的实现逻辑把一些我们需要的bean注册到IOC容器中。分析到这,我们通过测试代码调试来看看吧。

我们可以看到debug窗口中,在测试类中注入的CustomerRepository接口的实际对象是JdkDynamicAopProxy类型,这是一个jdk动态代理类,这确实比较符合我们对Spring IOC容器对接口类注入处理的认识。我们看一下它代理的目标类,发现是SimpleJpaRepository类,如此就证实了我们之前的猜测。第一次调试到此结束。

我们再回过头来看我们之前找到的AbstractRepositoryConfigurationSourceSupport类中registerBeanDefinitions方法,这个是在项目启动过程中执行的,我们加上断点进行第二次调试。

registerBeanDefinitions方法中主要的逻辑在delegate.registerRepositoriesIn中实现,我们直接在里面标记上断点。

当执行到上面断点后,我们看到configurations对象中已经有了我们自己定义的repository接口的信息了,接下来我们将进入for循环分别处理我们定义的接口了,我们进到下一个断点继续看。

在当前断点,我们可以看到,beanName是符合我们正常创建IOC容器中bean的命名规则的,但是JPA为我们创建的BeanDefinition对象是org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean类型,它是一个FactoryBean。我们继续看187、189行的代码

JPA将我们的BeanDefinition对象设置了一个name为factoryBeanObjectType,value为当前被处理的repository接口的全限定类名(比如ai.advance.jpademo.repository.FilmRepository)的attribute

最后将BeanDefinition对象注册到了我们的IOC容器中。因此当for循环执行完后,如果我们使用repository接口对应的beanName从IOC容器中获取一个实例,最开始我们拿到的是一个JpaRepositoryFactoryBean类型的Bean,当然我们最后真正拿到的bean对象是由getObject方法返回的对象。我们先来看一下它的继承关系:

JpaRepositoryFactoryBean既是一个工厂,又是一个bean。其作用类似于@Bean注解,但比其能实现更多复杂的功能,可以对对象增强。重要方法是getObject(),返回一个bean,因此我们去看一下getObject方法的实现,实际它的getObject方法的实现在其父类RepositoryFactoryBeanSupport中。

  1. @Nonnull

  2. public T getObject() {

  3. return this.repository.get();

  4. }

好像没什么东西,有点懵,那我们先忽略它,我们再一想,JpaRepositoryFactoryBean对象也是一个Bean,那有没有一点和bean实例化过程相关的代码,然后我们找到了这块代码:

  1. @Override

  2. public void afterPropertiesSet() {


  3. Assert.state(entityManager != null, "EntityManager must not be null!");

  4.  

  5. super.afterPropertiesSet();

  6. }

而确实JpaRepositoryFactoryBean的父类RepositoryFactoryBeanSupport也实现了InitializingBean接口,它自身也是调用了父类的afterPropertiesSet方法。

  1. // 以下代码实现在org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport中

  2. public void afterPropertiesSet() {


  3.   this.factory = createRepositoryFactory();

  4.  this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey);

  5.   this.factory.setNamedQueries(namedQueries);

  6.   this.factory.setEvaluationContextProvider(

  7.   evaluationContextProvider.orElseGet(() -> QueryMethodEvaluationContextProvider.DEFAULT));

  8.  this.factory.setBeanClassLoader(classLoader);

  9.  this.factory.setBeanFactory(beanFactory);

  10.  
  11.  if (publisher != null) {

  12.   this.factory.addRepositoryProxyPostProcessor(new EventPublishingRepositoryProxyPostProcessor(publisher));

  13. }


  14. repositoryBaseClass.ifPresent(this.factory::setRepositoryBaseClass);

  15.  
  16.  this.repositoryFactoryCustomizers.forEach(customizer -> customizer.customize(this.factory));

  17.  
  18.  RepositoryFragments customImplementationFragment = customImplementation //

  19.   .map(RepositoryFragments::just) //

  20.   .orElseGet(RepositoryFragments::empty);

  21.  
  22.  RepositoryFragments repositoryFragmentsToUse = this.repositoryFragments //

  23.   .orElseGet(RepositoryFragments::empty) //

  24.  .append(customImplementationFragment);

  25.  
  26.  this.repositoryMetadata = this.factory.getRepositoryMetadata(repositoryInterface);

  27.  
  28. this.repository = Lazy.of(() -> this.factory.getRepository(repositoryInterface, repositoryFragmentsToUse));

  29.  
  30.  // Make sure the aggregate root type is present in the MappingContext (e.g. for auditing)

  31.   this.mappingContext.ifPresent(it -> it.getPersistentEntity(repositoryMetadata.getDomainType()));

  32.  
  33. if (!lazyInit) {

  34. this.repository.get();

  35.   }

  36. }

逻辑有点多,我们先看到这一行代码:

  1. this.repository = Lazy.of(() -> this.factory.getRepository(repositoryInterface, repositoryFragmentsToUse));

先提一句Lazy类继承了java.util.function.Supplier接口。这个this.repository我们是不是在getObject方法有看到,我们还发现它的get方法返回的结果是由this.factory.getRepository()得到的,那这个this.factory又是谁呢,这时我们就要返回来看afterPropertiesSet方法的第一行代码:

  1. this.factory = createRepositoryFactory();

直接看createRepositoryFactory方法,它是在JpaRepositoryFactoryBean的直接父类TransactionalRepositoryFactoryBeanSupport中实现的。

  1. protected finalRepositoryFactorySupport createRepositoryFactory() {

  2.  

  3.  RepositoryFactorySupport factory = doCreateRepositoryFactory();

  4.  

  5.  RepositoryProxyPostProcessor exceptionPostProcessor = this.exceptionPostProcessor;

  6.  

  7.  if (exceptionPostProcessor != null) {

  8.  factory.addRepositoryProxyPostProcessor(exceptionPostProcessor);

  9.   }


  10.  RepositoryProxyPostProcessor txPostProcessor = this.txPostProcessor;

  11.  

  12.  if (txPostProcessor != null) {

  13.  factory.addRepositoryProxyPostProcessor(txPostProcessor);

  14.   }


  15.  return factory;

  16. }

我们直接看关键代码 -- doCreateRepositoryFactory方法,这个是由JpaRepositoryFactoryBean类自己实现的。

  1. protected RepositoryFactorySupport doCreateRepositoryFactory() {

  2.  
  3. Assert.state(entityManager != null, "EntityManager must not be null!");

  4.  
  5. return createRepositoryFactory(entityManager);

  6. }

  7.  
  8. /**

  9.  * Returns a {@link RepositoryFactorySupport}.

  10.  */

  11. protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {

  12.  
  13.  JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager);

  14.  jpaRepositoryFactory.setEntityPathResolver(entityPathResolver);

  15.  jpaRepositoryFactory.setEscapeCharacter(escapeCharacter);

  16.  
  17.  if (queryMethodFactory != null) {

  18.  jpaRepositoryFactory.setQueryMethodFactory(queryMethodFactory);

  19.  }


  20.  return jpaRepositoryFactory;

  21. }

我们直接看到createRepositoryFactory方法,它最后返回是一个JpaRepositoryFactory类型的对象,其他的我们就先不关心了,直接看JpaRepositoryFactory类。我们先来一张类关系图:

我们直奔我们的主题-- getRepository方法,它是在JpaRepositoryFactory的父类RepositoryFactorySupport中实现的。

  1. public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fragments) {

  2.  
  3. ...

  4.  
  5. RepositoryInformation information = getRepositoryInformation(metadata, composition);

  6.  
  7. ...

  8.  
  9. Object target = getTargetRepository(information);

  10.  
  11. ...

  12.  
  13. ProxyFactory result = new ProxyFactory();

  14. result.setTarget(target);

  15. result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class);

  16.  
  17. ...

  18.  
  19. T repository = (T) result.getProxy(classLoader);

  20.  
  21. ...


  22. return repository;

  23. }

实现代码有点多,这里仅展示我们关注的关键代码,去掉其他多余的代码之后,有没有发现上面的逻辑很熟悉,其实就是构建一个代理类实例的过程,因此也解释了为什么我们在第一次调试看到实际注入的是一个JdkDynamicAopProxy类型的实体。那我们得好好看看代理对象的目标对象是怎么得到的,请看getTargetRepository方法,发现他需要一个RepositoryInformation类型的传参,我继续往上找,看到了getRepositoryInformation方法。

  1. private RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata,

  2.   RepositoryComposition composition) {

  3.  
  4.  RepositoryInformationCacheKey cacheKey = new RepositoryInformationCacheKey(metadata, composition);

  5.  
  6. return repositoryInformationCache.computeIfAbsent(cacheKey, key -> {

  7.  

  8. Class<?> baseClass = repositoryBaseClass.orElse(getRepositoryBaseClass(metadata));

  9.  
  10. return new DefaultRepositoryInformation(metadata, baseClass, composition);

  11.   });

  12. }

其中,可以看到一个getRepositoryBaseClass方法

  1. protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {

  2. return SimpleJpaRepository.class;

  3. }

好了,什么也不用说了,它直接给我们返回了SimpleJpaRepository的Class对象。其他的逻辑我们也不看了,虽然还有一些包装和判断的过程,但是我们今天的目的已经达到了,第二个疑惑也算比较好的解答了。最后顺便提一句,如果你的项目是手动使用了@EnableJpaRepositories注解,可能你的调试过程开局会有点不一样,但是后续的逻辑是相同的,自己可以去试试。代码如下:

  1. @EnableJpaRepositories(basePackages = "ai.advance.jpademo.repository")

  2. @SpringBootApplication

  3. public class JpaDemoApplication {


  4. public static void main(String[] args) {

  5. SpringApplication.run(JpaDemoApplication.class, args);

  6. }

  7.  

  8. }

3.3自定义查询

我们还有一个疑问,为什么我们自己定义的那些不属于SimpleJpaRepository类的方法也能被调用,并且被正确生成SQL?我们又要回到上节分析的getRepository方法,同样的我们去掉不需要关心的代码。

  1. public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fragments) {

  2.  
  3. ...

  4.  
  5. ProxyFactory result = new ProxyFactory();

  6.  
  7. ...

  8.  
  9. Optional<QueryLookupStrategy> queryLookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey,

  10. evaluationContextProvider);

  11. result.addAdvice(new QueryExecutorMethodInterceptor(information, projectionFactory, queryLookupStrategy,

  12. namedQueries, queryPostProcessors, methodInvocationListeners));

  13.  
  14. result.addAdvice(

  15. new ImplementationMethodExecutionInterceptor(information, compositionToUse, methodInvocationListeners));

  16.  
  17. T repository = (T) result.getProxy(classLoader);

  18.  
  19. ...

  20.  
  21. return repository;

  22. }

从上面的源码我们可以看到ProxyFactory对象在最后有两次addAdvice方法的调用,目的是为了增加QueryExecutorMethodInterceptor

和ImplementationMethodExecutionInterceptor两个拦截器,它们都实现了MethodInterceptor接口,其中ImplementationMethodExecutionInterceptor为RepositoryFactorySupport的静态内部类。

3.3.1QueryExecutorMethodInterceptor

QueryExecutorMethodInterceptor这个拦截器是用来拦截处理我们在repository接口中自定义的方法的。在QueryExecutorMethodInterceptor的成员变量中有一个定义为Map<Method, RepositoryQuery>类型的queries变量,这个变量主要保存了自定义方法对象与一个RepositoryQuery对象的映射关系。

RepositoryQuery的直接抽象子类是AbstractJpaQuery,可以看到,一个RepositoryQuery实例持有一个JpaQueryMethod实例,JpaQueryMethod又持有一个Method实例,所以RepositoryQuery实例的用途很明显,一个RepositoryQuery代表了Repository接口中的一个方法,根据方法头上注解不同的形态,将每个Repository接口中的方法分别映射成相对应的RepositoryQuery实例。我们通过类关系图来熟悉一下RepositoryQuery具体的实现类有哪些。

下面我们看看JPA在哪些情况下创建对应的那个RepositoryQuery对象

1.SimpleJpaQuery

方法头上@Query注解的nativeQuery属性缺省值为false,也就是使用JPQL,此时会创建SimpleJpaQuery实例,并通过两个StringQuery类实例分别持有query JPQL语句和根据query JPQL计算拼接出来的countQuery JPQL语句

2.NativeJpaQuery

方法头上@Query注解的nativeQuery属性如果显式的设置为nativeQuery=true,也就是使用原生SQL的时候

3.PartTreeJpaQuery

方法头上未进行@Query注解,将使用spring-data-jpa独创的方法名识别的方式进行sql语句拼接

4.NamedQuery

使用javax.persistence.NamedQuery注解访问数据库的形式的时候

5.StoredProcedureJpaQuery

在Repository接口的方法头上使用org.springframework.data.jpa.repository.query.Procedure注解,也就是调用存储过程的方式访问数据库的时候

所以QueryExecutorMethodInterceptor最终的目的就是根据当前需要调用的自定义的Repository的方法找到对应的RepositoryQuery对象,并构建调用信息并使用invoke方法触发调用,主要逻辑在QueryExecutorMethodInterceptor类的doInvoke方法中。

  1. private Object doInvoke(MethodInvocation invocation) throws Throwable {

  2.  
  3. Method method = invocation.getMethod();

  4.  
  5. if (hasQueryFor(method)) {

  6.  
  7. RepositoryMethodInvoker invocationMetadata = invocationMetadataCache.get(method);

  8.  
  9. if (invocationMetadata == null) { invocationMetadata = RepositoryMethodInvoker.forRepositoryQuery(method, queries.get(method));

  10. invocationMetadataCache.put(method,

  11. invocationMetadata);

  12. }

  13.  
  14. return invocationMetadata.invoke(repositoryInformation.getRepositoryInterface(), invocationMulticaster,

  15. invocation.getArguments());

  16. }


  17. return invocation.proceed();

  18. }

其实最终调用的是RepositoryMethodInvoker类中的doInvoke方法,我们打上断点来看一下

 

3.3.2ImplementationMethodExecutionInterceptor

ImplementationMethodExecutionInterceptor这个拦截器是来处理SimpleJpaRepository类本身实现的方法调用的。它是RepositoryFactorySupport类的静态内部类,只要没有被QueryExecutorMethodInterceptor拦截器处理的方法调用都会由它来处理,最终也是调用invoke方法。

  1. public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable {
  2.  
  3. Method method = invocation.getMethod();
  4. Object[] arguments = invocation.getArguments();
  5.  
  6. try {
  7. return composition.invoke(invocationMulticaster, method, arguments);
  8. } catch (Exception e) {
  9. org.springframework.data.repository.util.ClassUtils.unwrapReflectionException(e);
  10. }

  11. throw new IllegalStateException("Should not occur!");
  12. }
其实这里面的逻辑属于中规中矩的代理类的拦截调用。我们拿findById这个方法作为例子,打上断点看看

从上面信息可以看出来ImplementationMethodExecutionInterceptor类的invoke方法调用的是RepositoryComposition类的invoke方法,如果继续深入,其实最终也是调用的RepositoryMethodInvoker类中的doInvoke方法

至此,我们开头的几大疑惑基本都得到了解释。

 

四、总结

我也是尽量把⾃⼰知道的知识写明⽩,奈何本⼈⽔平有限,接触JPA也不久,如果存在纰漏欢迎指正。关于JPA更深层次的东⻄我也还在学习中,希望之后有机会再分享。

 

Back to Blog
Contact Icon
Message Icon
Back To Top