写点什么

Spring data JPA 实践和原理浅析

  • 2022 年 5 月 05 日
  • 本文字数:13000 字

    阅读完需:约 43 分钟

Spring data JPA实践和原理浅析

一、前言


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

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



二、基本使用


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

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

<parent>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-parent</artifactId>    <version>2.5.6</version>    <relativePath/></parent>
<!-- jpa --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId></dependency><!-- mysql --><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId></dependency>
复制代码

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

spring:  datasource:    driver-class-name: com.mysql.cj.jdbc.Driver    url: jdbc:mysql://localhost:3306/sakila?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8    username: root    password: root
复制代码

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

@Entity@Data // lombok注解public class Actor {
@Id // Column注解不是必须的,如果满足字段驼峰形式 // 与数据库字段以下划线分隔形式对应即可 @Column(name = "actor_id", nullable = false) private Integer actorId; @Column(name = "first_name", nullable = false, length = 45) private String firstName; private String lastName; private Timestamp lastUpdate;}
复制代码

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


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

@Repositorypublic interface ActorRepository extends JpaRepository<Actor, Integer> {}
复制代码

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


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

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

2.1.1 一般写法

/**  * 示例1 * SQL: SELECT * FROM actor WHERE first_name = ? * 参数名的定义不影响程序的运行 */List<Actor> findByFirstName(String name);
/** * 示例2 * SQL: SELECT * FROM actor WHERE first_name = ? AND last_name = ? * 如果明确知道查询结果返回唯一一条记录时,建议使用单一实体类作为返回类型 */List<Actor> findByFirstNameAndLastName(String name1, String name2);
/** * 示例3 * SQL: SELECT * FROM actor WHERE actor_id <= ? */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 * SQL: SELECT * FROM actor WHERE first_name = ? */@Query("FROM Actor WHERE firstName = ?1")List<Actor> findByFirstName(String name);
/** * 示例2 * SQL: SELECT * FROM actor WHERE first_name = ? AND last_name = ? */@Query("FROM Actor WHERE firstName = ?1 AND lastName = ?2")List<Actor> findByFirstNameAndLastName(String name1, String name2);
/** * 示例3 * SQL: SELECT * FROM actor WHERE actor_id <= ? */@Query("FROM Actor WHERE actorId <= ?1")List<Actor> findByActorIdLessThanEqual(Integer id);
/** * 示例4 * SQL: SELECT * FROM actor * 不能写"SELECT *" 要写"SELECT 别名" */@Query("SELECT a FROM Actor a")List<Actor> findAll()
复制代码

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


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


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

@Modifying@Query(value = "UPDATE Film SET description = :description WHERE filmId = :id")void update(@Param("id") Integer filmId, @Param("description") String description);
复制代码

2.2.2 使用原生 SQL

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


@Query("SELECT * FROM actor WHERE first_name = ?1", nativeQuery = true)List<Actor> findByFirstName(String name);
@Query("SELECT * FROM actor WHERE first_name = ?1 AND last_name = ?2", nativeQuery = true)List<Actor> findByFirstNameAndLastName(String name1, String name2);
@Query("SELECT * FROM actor WHERE actor_id <= ?", nativeQuery = true)List<Actor> findByActorIdLessThanEqual(Integer id);
@Query("SELECT first_name, last_name FROM actor", nativeQuery = true)List<Map<String, Object>> findAll();// 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,看着它我们既熟悉又陌生。我们通过它的源码来一探究竟。

package org.springframework.boot.autoconfigure;
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })public @interface SpringBootApplication {
@AliasFor(annotation = EnableAutoConfiguration.class) Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class) String[] excludeName() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages") String[] scanBasePackages() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses") Class<?>[] scanBasePackageClasses() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator") Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
@AliasFor(annotation = Configuration.class) boolean proxyBeanMethods() default true;
}
复制代码

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

package org.springframework.boot.autoconfigure;
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
复制代码

我们又看到了熟悉的身影,@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.2SimpleJpaRepository 类

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

@Repository@Transactional(readOnly = true)public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {}
复制代码

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


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


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


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

@Configuration(proxyBeanMethods = false)@ConditionalOnBean(DataSource.class)@ConditionalOnClass(JpaRepository.class)@ConditionalOnMissingBean({ JpaRepositoryFactoryBean.class, JpaRepositoryConfigExtension.class })@ConditionalOnProperty(prefix = "spring.data.jpa.repositories", name = "enabled", havingValue = "true",		matchIfMissing = true)@Import(JpaRepositoriesRegistrar.class)@AutoConfigureAfter({ HibernateJpaAutoConfiguration.class, TaskExecutionAutoConfiguration.class })public class JpaRepositoriesAutoConfiguration {}
复制代码

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

class JpaRepositoriesRegistrar extends AbstractRepositoryConfigurationSourceSupport {}
public abstract class AbstractRepositoryConfigurationSourceSupport implements ImportBeanDefinitionRegistrar, BeanFactoryAware, ResourceLoaderAware, EnvironmentAware {
private ResourceLoader resourceLoader;
private BeanFactory beanFactory;
private Environment environment;
@Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) { RepositoryConfigurationDelegate delegate = new RepositoryConfigurationDelegate( getConfigurationSource(registry, importBeanNameGenerator), this.resourceLoader, this.environment); delegate.registerRepositoriesIn(registry, getRepositoryConfigurationExtension()); }
@Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { registerBeanDefinitions(importingClassMetadata, registry, null); } // 其他源码 ...}
复制代码

其实分析了这么久,如果我们经常去看看 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 中。

@Nonnullpublic T getObject() {	return this.repository.get();}
复制代码

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

@Overridepublic void afterPropertiesSet() {
Assert.state(entityManager != null, "EntityManager must not be null!");
super.afterPropertiesSet();}
复制代码

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


// 以下代码实现在org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport中public void afterPropertiesSet() {
this.factory = createRepositoryFactory(); this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey); this.factory.setNamedQueries(namedQueries); this.factory.setEvaluationContextProvider( evaluationContextProvider.orElseGet(() -> QueryMethodEvaluationContextProvider.DEFAULT)); this.factory.setBeanClassLoader(classLoader); this.factory.setBeanFactory(beanFactory);
if (publisher != null) { this.factory.addRepositoryProxyPostProcessor(new EventPublishingRepositoryProxyPostProcessor(publisher)); }
repositoryBaseClass.ifPresent(this.factory::setRepositoryBaseClass);
this.repositoryFactoryCustomizers.forEach(customizer -> customizer.customize(this.factory));
RepositoryFragments customImplementationFragment = customImplementation // .map(RepositoryFragments::just) // .orElseGet(RepositoryFragments::empty);
RepositoryFragments repositoryFragmentsToUse = this.repositoryFragments // .orElseGet(RepositoryFragments::empty) // .append(customImplementationFragment);
this.repositoryMetadata = this.factory.getRepositoryMetadata(repositoryInterface);
this.repository = Lazy.of(() -> this.factory.getRepository(repositoryInterface, repositoryFragmentsToUse));
// Make sure the aggregate root type is present in the MappingContext (e.g. for auditing) this.mappingContext.ifPresent(it -> it.getPersistentEntity(repositoryMetadata.getDomainType()));
if (!lazyInit) { this.repository.get(); }}
复制代码

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

this.repository = Lazy.of(() -> this.factory.getRepository(repositoryInterface, repositoryFragmentsToUse));
复制代码

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

this.factory = createRepositoryFactory();
复制代码

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

protected final RepositoryFactorySupport createRepositoryFactory() {
RepositoryFactorySupport factory = doCreateRepositoryFactory();
RepositoryProxyPostProcessor exceptionPostProcessor = this.exceptionPostProcessor;
if (exceptionPostProcessor != null) { factory.addRepositoryProxyPostProcessor(exceptionPostProcessor); }
RepositoryProxyPostProcessor txPostProcessor = this.txPostProcessor;
if (txPostProcessor != null) { factory.addRepositoryProxyPostProcessor(txPostProcessor); }
return factory;}
复制代码

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

protected RepositoryFactorySupport doCreateRepositoryFactory() {
Assert.state(entityManager != null, "EntityManager must not be null!");
return createRepositoryFactory(entityManager);}
/** * Returns a {@link RepositoryFactorySupport}. */protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager); jpaRepositoryFactory.setEntityPathResolver(entityPathResolver); jpaRepositoryFactory.setEscapeCharacter(escapeCharacter);
if (queryMethodFactory != null) { jpaRepositoryFactory.setQueryMethodFactory(queryMethodFactory); }
return jpaRepositoryFactory;}
复制代码

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


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

public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fragments) {
... RepositoryInformation information = getRepositoryInformation(metadata, composition);
... Object target = getTargetRepository(information);
... ProxyFactory result = new ProxyFactory(); result.setTarget(target); result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class);
... T repository = (T) result.getProxy(classLoader);
...
return repository;}
复制代码

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

private RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata,        RepositoryComposition composition) {
RepositoryInformationCacheKey cacheKey = new RepositoryInformationCacheKey(metadata, composition);
return repositoryInformationCache.computeIfAbsent(cacheKey, key -> {
Class<?> baseClass = repositoryBaseClass.orElse(getRepositoryBaseClass(metadata));
return new DefaultRepositoryInformation(metadata, baseClass, composition); });}
复制代码

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

protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {    return SimpleJpaRepository.class;}
复制代码

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

@EnableJpaRepositories(basePackages = "ai.advance.jpademo.repository")@SpringBootApplicationpublic class JpaDemoApplication {
public static void main(String[] args) { SpringApplication.run(JpaDemoApplication.class, args); }
}
复制代码

3.3 自定义查询

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

public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fragments) {        ...            ProxyFactory result = new ProxyFactory();        ...            Optional<QueryLookupStrategy> queryLookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey,            evaluationContextProvider);    result.addAdvice(new QueryExecutorMethodInterceptor(information, projectionFactory, queryLookupStrategy,            namedQueries, queryPostProcessors, methodInvocationListeners));
result.addAdvice( new ImplementationMethodExecutionInterceptor(information, compositionToUse, methodInvocationListeners));
T repository = (T) result.getProxy(classLoader); ... return repository;}
复制代码

从上面的源码我们可以看到 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 方法中。

private Object doInvoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
if (hasQueryFor(method)) {
RepositoryMethodInvoker invocationMetadata = invocationMetadataCache.get(method);
if (invocationMetadata == null) { invocationMetadata = RepositoryMethodInvoker.forRepositoryQuery(method, queries.get(method)); invocationMetadataCache.put(method, invocationMetadata); }
return invocationMetadata.invoke(repositoryInformation.getRepositoryInterface(), invocationMulticaster, invocation.getArguments()); }
return invocation.proceed();}
复制代码

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


3.3.2ImplementationMethodExecutionInterceptor

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

public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod(); Object[] arguments = invocation.getArguments();
try { return composition.invoke(invocationMulticaster, method, arguments); } catch (Exception e) { org.springframework.data.repository.util.ClassUtils.unwrapReflectionException(e); }
throw new IllegalStateException("Should not occur!");}
复制代码

其实这里面的逻辑属于中规中矩的代理类的拦截调用。我们拿 findById 这个方法作为例子,打上断点看看


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


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


四、总结

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


关于领创集团(Advance Intelligence Group)

领创集团成立于 2016 年,致力于通过科技创新的本地化应用,改造和重塑金融和零售行业,以多元化的业务布局打造一个服务于消费者、企业和商户的生态圈。集团旗下包含企业业务和消费者业务两大板块,企业业务包含 ADVANCE.AI 和 Ginee,分别为银行、金融、金融科技、零售和电商行业客户提供基于 AI 技术的数字身份验证、风险管理产品和全渠道电商服务解决方案;消费者业务 Atome Financial 包括亚洲领先的先享后付平台 Atome 和数字金融服务。2021 年 9 月,领创集团宣布完成超 4 亿美元 D 轮融资,融资完成后领创集团估值已超 20 亿美元,成为新加坡最大的独立科技创业公司之一。


往期回顾 BREAK AWAY

如何解决海量数据更新场景下的 Mysql 死锁问题

企业级 APIs 安全实践指南 (建议初中级工程师收藏)

Cypress UI 自动化测试框架

serverless让我们的运维更轻松


▼ 如果觉得这篇内容对你有所帮助,有所启发,欢迎点赞收藏:

1、点赞、关注领创集团,获取最新技术分享和公司动态。

2、关注我们的公众号 & 知乎号「领创集团 Advance Group」或访问官方网站,了解更多企业动态。

发布于: 刚刚阅读数: 3
用户头像

智慧领创美好生活 2021.08.12 加入

AI技术驱动的科技集团,致力于以技术赋能为核心,通过科技创新的本地化应用,改造和重塑金融和零售行业,以多元化的业务布局打造一个服务于消费者、企业和商户的生态圈,带来个性化、陪伴式的产品服务和优质体验。

评论

发布
暂无评论
Spring data JPA实践和原理浅析_工作原理_领创集团Advance Intelligence Group_InfoQ写作社区