自定义 Repository 实现

Spring Data 提供了多种选项,只需少量编码即可创建查询方法。但当这些选项无法满足您的需求时,您也可以为 repository 方法提供自己的自定义实现。本节将介绍如何做到这一点。

自定义单个 Repository

要为 repository 丰富自定义功能,您必须首先定义一个 fragment 接口以及该自定义功能的实现,如下所示

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

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

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

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

位于与 repository 接口同一包中、名称匹配 repository 接口名 后面跟 实现后缀 的类型,被视为自定义实现,并将被当作自定义实现处理。遵循该名称的类可能导致非预期行为。

我们认为单自定义实现命名模式已被弃用,不推荐使用此模式。请迁移到基于 fragment 的编程模型。

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

然后您可以让您的 repository 接口继承 fragment 接口,如下所示

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

  // Declare query methods here
}

通过让您的 repository 接口继承 fragment 接口,可以将 CRUD 功能和自定义功能结合起来,并使其可供客户端使用。

Spring Data repository 通过使用构成 repository 组合的 fragment 来实现。Fragment 包括基础 repository、功能切面(例如 Querydsl)以及自定义接口及其实现。每当您向 repository 接口添加一个接口时,您就通过添加一个 fragment 来增强组合。基础 repository 和 repository 切面的实现由各个 Spring Data 模块提供。

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

带有实现的 Fragment
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 和切面方法,并在两个 fragment 提供相同方法签名时解决歧义。Repository fragment 不仅限于在单个 repository 接口中使用。多个 repository 可以使用同一个 fragment 接口,从而允许您在不同的 repository 中重用自定义功能。

以下示例显示了一个 repository fragment 及其实现

覆盖 save(…) 的 Fragment
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 fragment 的 repository

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

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

配置

repository 基础设施会尝试通过扫描找到 repository 的包下的类来自动检测自定义实现 fragment。这些类需要遵循追加后缀(默认为 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,这与 fragment 接口(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 实现定义的名称相匹配,并将使用它而不是第一个实现。

手动装配

如果您的自定义实现仅使用基于注解的配置和自动装配,那么前面展示的方法效果很好,因为它被视为任何其他 Spring bean。如果您的实现 fragment 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 注册 Fragment

如同在配置一节中提到的,基础设施仅在 repository 基础包中自动检测 fragment。因此,位于其他位置或希望由外部归档贡献的 fragment,如果它们不共享同一个命名空间,将不会被找到。在 spring.factories 中注册 fragment 可以让您规避此限制,具体说明见下文。

假设您想为您的组织提供一些可跨多个 repository 使用的自定义搜索功能,利用文本搜索索引。

首先,您需要的是 fragment 接口。注意泛型参数 <T>,它用于使 fragment 与 repository 领域类型对齐。

Fragment 接口
public interface SearchExtension<T> {

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

假设实际的全文搜索通过一个在上下文中注册为 BeanSearchService 提供,因此您可以在我们的 SearchExtension 实现中使用它。您执行搜索所需的一切是集合(或索引)名称以及一个将搜索结果转换为实际领域对象的对象映射器,如下所示。

Fragment 实现
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 标记接口添加到 fragment 实现中将触发基础设施,并为使用该 fragment 的 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 {

    // ...
}

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

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 上启用该标志。

完成 fragment 声明和实现后,您可以将扩展注册到 META-INF/spring.factories 文件中,并在需要时打包。

META-INF/spring.factories 中注册 fragment
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
  }
}
该类需要有一个超类的构造函数,特定 store 的 repository 工厂实现会使用它。如果 repository 基础类有多个构造函数,请覆盖接受一个 EntityInformation 以及一个特定 store 的基础设施对象(例如 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" />