自定义 Repository 实现

Spring Data 提供了多种创建查询方法的方式,代码量很少。但是当这些方式不能满足你的需求时,你也可以为 repository 方法提供自己的自定义实现。本节将介绍如何实现。

定制单个 Repository

为了使用自定义功能丰富 repository,你必须首先定义一个片段接口和自定义功能的实现,如下所示

自定义 repository 功能的接口
interface CustomizedUserRepository {
  void someCustomMethod(User user);
}
自定义 repository 功能的实现
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  @Override
  public void someCustomMethod(User user) {
    // Your custom implementation
  }
}

与片段接口对应的类名最重要的部分是 Impl 后缀。你可以通过设置 @Enable<StoreModule>Repositories(repositoryImplementationPostfix = …) 来自定义特定于存储的后缀。

从历史上看,Spring Data 自定义 repository 实现的发现遵循一个 命名模式,该模式从 repository 派生自定义实现类名,实际上只允许一个自定义实现。

位于与 repository 接口相同包中、匹配 "repository 接口名" 后面跟 "实现后缀" 的类型被认为是自定义实现,并将被视为自定义实现。遵循该名称的类可能导致意外行为。

我们认为单自定义实现命名已被弃用,并建议不要使用此模式。请转而迁移到基于片段的编程模型。

实现本身不依赖于 Spring Data,并且可以是一个普通的 Spring Bean。因此,你可以使用标准的依赖注入行为来注入对其他 Bean(例如 JdbcTemplate)的引用,参与 AOP 等等。

然后,你可以让你的 repository 接口扩展片段接口,如下所示

对你的 repository 接口的更改
interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {

  // Declare query methods here
}

将你的 repository 接口扩展片段接口,结合了 CRUD 和自定义功能,并使其可供客户端使用。

Spring Data repository 通过使用构成 repository 组合的片段来实现。片段包括基本 repository、功能方面(例如 Querydsl)以及自定义接口及其实现。每次向 repository 接口添加接口时,都会通过添加片段来增强组合。基本 repository 和 repository 方面实现由每个 Spring Data 模块提供。

以下示例展示了自定义接口及其实现

包含其实现的片段
interface HumanRepository {
  void someHumanMethod(User user);
}

class HumanRepositoryImpl implements HumanRepository {

  @Override
  public void someHumanMethod(User user) {
    // Your custom implementation
  }
}

interface ContactRepository {

  void someContactMethod(User user);

  User anotherContactMethod(User user);
}

class ContactRepositoryImpl implements ContactRepository {

  @Override
  public void someContactMethod(User user) {
    // Your custom implementation
  }

  @Override
  public User anotherContactMethod(User user) {
    // Your custom implementation
  }
}

以下示例展示了一个扩展 CrudRepository 的自定义 repository 的接口

对你的 repository 接口的更改
interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {

  // Declare query methods here
}

Repository 可以由多个自定义实现组成,这些实现按照声明顺序导入。自定义实现的优先级高于基本实现和 repository 方面。这种排序允许你覆盖基本 repository 和方面的方法,并在两个片段贡献相同方法签名时解决歧义。Repository 片段不仅限于在单个 repository 接口中使用。多个 repository 可以使用同一个片段接口,从而允许你在不同的 repository 之间重用定制。

以下示例展示了一个 repository 片段及其实现

覆盖 save(…) 方法的片段
interface CustomizedSave<T> {
  <S extends T> S save(S entity);
}

class CustomizedSaveImpl<T> implements CustomizedSave<T> {

  @Override
  public <S extends T> S save(S entity) {
    // Your custom implementation
  }
}

以下示例展示了一个使用前面 repository 片段的 repository

定制的 repository 接口
interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}

interface PersonRepository extends CrudRepository<Person, Long>, CustomizedSave<Person> {
}

配置

Repository 基础设施尝试通过扫描发现 repository 的包下的类来自动检测自定义实现片段。这些类需要遵循附加 Impl 后缀(默认值)的命名约定。

以下示例展示了一个使用默认后缀的 repository 和一个为后缀设置自定义值的 repository

示例 1. 配置示例
  • Java

  • XML

@EnableJpaRepositories(repositoryImplementationPostfix = "MyPostfix")
class Configuration { … }
<repositories base-package="com.acme.repository" />

<repositories base-package="com.acme.repository" repository-impl-postfix="MyPostfix" />

前面示例中的第一个配置尝试查找名为 com.acme.repository.CustomizedUserRepositoryImpl 的类作为自定义 repository 实现。第二个示例尝试查找 com.acme.repository.CustomizedUserRepositoryMyPostfix

歧义解决

如果在不同的包中找到多个类名匹配的实现,Spring Data 会使用 bean 名称来确定使用哪个实现。

鉴于前面所示的 CustomizedUserRepository 的以下两个自定义实现,将使用第一个实现。其 bean 名称是 customizedUserRepositoryImpl,这与片段接口(CustomizedUserRepository)加上后缀 Impl 的名称匹配。

示例 2. 歧义实现的解决
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  // Your custom implementation
}
@Component("specialCustomImpl")
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  // Your custom implementation
}

如果你使用 @Component("specialCustom") 注解 UserRepository 接口,则 bean 名称加上 Impl 将匹配在 com.acme.impl.two 中为 repository 实现定义的 bean 名称,并会使用它而不是第一个实现。

手动装配

如果你的自定义实现只使用基于注解的配置和自动装配,前面所示的方法效果很好,因为它被视为任何其他 Spring bean。如果你的实现片段 bean 需要特殊的装配,你可以声明该 bean 并根据前面章节中描述的约定命名它。然后,基础设施会按名称引用手动定义的 bean 定义,而不是自己创建一个。以下示例展示了如何手动装配自定义实现

示例 3. 手动装配自定义实现
  • Java

  • XML

class MyClass {
  MyClass(@Qualifier("userRepositoryImpl") UserRepository userRepository) {
    …
  }
}
<repositories base-package="com.acme.repository" />

<beans:bean id="userRepositoryImpl" class="…">
  <!-- further configuration -->
</beans:bean>

使用 spring.factories 注册片段

配置 一节所述,基础设施只会在 repository 基包内自动检测片段。因此,位于其他位置或希望由外部归档文件贡献的片段如果它们不共享公共命名空间,将不会被找到。在 spring.factories 中注册片段可以让你规避此限制,如下一节所述。

想象一下,你想利用文本搜索索引为你的组织提供一些可在多个 repository 中使用的自定义搜索功能。

首先,你需要片段接口。注意通用 <T> 参数,用于使片段与 repository 领域类型对齐。

片段接口
public interface SearchExtension<T> {

    List<T> search(String text, Limit limit);
}

假设实际的全文搜索可以通过一个注册为上下文中的 BeanSearchService 来获得,这样你就可以在我们的 SearchExtension 实现中消费它。运行搜索所需的一切就是集合(或索引)名称以及一个将搜索结果转换为实际领域对象的对象映射器,如下面概述所示。

片段实现
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Limit;
import org.springframework.data.repository.core.RepositoryMethodContext;

class DefaultSearchExtension<T> implements SearchExtension<T> {

    private final SearchService service;

    DefaultSearchExtension(SearchService service) {
        this.service = service;
    }

    @Override
    public List<T> search(String text, Limit limit) {
        return search(RepositoryMethodContext.getContext(), text, limit);
    }

    List<T> search(RepositoryMethodContext metadata, String text, Limit limit) {

        Class<T> domainType = metadata.getRepository().getDomainType();

        String indexName = domainType.getSimpleName().toLowerCase();
        List<String> jsonResult = service.search(indexName, text, 0, limit.max());

        return jsonResult.stream().map(…).collect(toList());
    }
}

在上面的示例中,RepositoryMethodContext.getContext() 用于检索实际方法调用的元数据。RepositoryMethodContext 公开附加到 repository 的信息,例如领域类型。在这种情况下,我们使用 repository 领域类型来标识要搜索的索引名称。

公开调用元数据开销较大,因此默认是禁用的。要访问 RepositoryMethodContext.getContext(),你需要通知负责创建实际 repository 的 repository 工厂,使其公开方法元数据。

公开 Repository 元数据
  • 标记接口

  • Bean 后置处理器

RepositoryMetadataAccess 标记接口添加到片段实现将触发基础设施,并为使用该片段的那些 repository 启用元数据公开。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Limit;
import org.springframework.data.repository.core.support.RepositoryMetadataAccess;
import org.springframework.data.repository.core.RepositoryMethodContext;

class DefaultSearchExtension<T> implements SearchExtension<T>, RepositoryMetadataAccess {

    // ...
}

exposeMetadata 标志可以直接通过 BeanPostProcessor 在 repository 工厂 bean 上设置。

import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.lang.Nullable;

@Configuration
class MyConfiguration {

    @Bean
    static BeanPostProcessor exposeMethodMetadata() {

        return new BeanPostProcessor() {

            @Override
            public Object postProcessBeforeInitialization(Object bean, String beanName) {

                if(bean instanceof RepositoryFactoryBeanSupport<?,?,?> factoryBean) {
                    factoryBean.setExposeMetadata(true);
                }
                return bean;
            }
        };
    }
}

请不要直接复制/粘贴上述内容,而应考虑你的实际用例,因为上述做法只是简单地在每个 repository 上启用该标志,这可能需要更细粒度的方法。

拥有了片段声明和实现后,你可以在 META-INF/spring.factories 文件中注册扩展,并在需要时打包。

在 META-INF/spring.factories 中注册片段
com.acme.search.SearchExtension=com.acme.search.DefaultSearchExtension

现在你可以开始使用你的扩展了;只需将接口添加到你的 repository 中。

使用方法
import com.acme.search.SearchExtension;
import org.springframework.data.repository.CrudRepository;

interface MovieRepository extends CrudRepository<Movie, String>, SearchExtension<Movie> {

}

定制基础 Repository

前一节中描述的方法需要定制每个 repository 接口,以便定制基础 repository 行为并影响所有 repository。为了改变所有 repository 的行为,你可以创建一个实现,扩展特定持久化技术的基础 repository 类。该类随后充当 repository 代理的自定义基础类,如以下示例所示

自定义 repository 基础类
class MyRepositoryImpl<T, ID>
  extends SimpleJpaRepository<T, ID> {

  private final EntityManager entityManager;

  MyRepositoryImpl(JpaEntityInformation entityInformation,
                          EntityManager entityManager) {
    super(entityInformation, entityManager);

    // Keep the EntityManager around to used from the newly introduced methods.
    this.entityManager = entityManager;
  }

  @Override
  @Transactional
  public <S extends T> S save(S entity) {
    // implementation goes here
  }
}
该类需要有一个超类的构造函数,供特定于存储的 repository 工厂实现使用。如果 repository 基础类有多个构造函数,请覆盖接受一个 EntityInformation 以及一个特定于存储的基础设施对象(例如 EntityManager 或模板类)的那个构造函数。

最后一步是让 Spring Data 基础设施知晓定制的 repository 基础类。在配置中,你可以通过使用 repositoryBaseClass 来实现,如下例所示

示例 4. 配置自定义 repository 基础类
  • Java

  • XML

@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
<repositories base-package="com.acme.repository"
     base-class="….MyRepositoryImpl" />