自定义 Repository 实现

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

定制单个 Repository

要通过自定义功能丰富一个 repository,您必须首先定义一个片段接口(fragment interface)和该自定义功能的实现,如下所示

自定义 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 repositories 是通过使用片段(fragments)来实现的,这些片段构成了 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
}

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

以下示例展示了一个 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

@EnableMongoRepositories(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. 歧义实现的解决
package com.acme.impl.one;

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  // Your custom implementation
}
package com.acme.impl.two;

@Component("specialCustomImpl")
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  // Your custom implementation
}

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

手动装配

如果您的自定义实现仅使用基于注解的配置和自动装配,则前面展示的方法运行良好,因为它被视为任何其他 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 中注册片段可以绕过此限制,如下一节所述。

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

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

片段接口
package com.acme.search;

public interface SearchExtension<T> {

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

假设实际的全文搜索通过一个注册为上下文中的 BeanSearchService 可用,这样您就可以在我们的 SearchExtension 实现中调用它。运行搜索所需的一切只是集合(或索引)名称和一个对象映射器,它将搜索结果转换为实际的域对象,如下所示。

片段实现
package com.acme.search;

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 标记接口添加到片段实现将触发基础设施并为那些使用该片段的 repositories 启用元数据暴露。

package com.acme.search;

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

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

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

现在您就可以使用您的扩展了;只需将接口添加到您的 repository 中即可。

使用它
package io.my.movies;

import com.acme.search.SearchExtension;
import org.springframework.data.repository.CrudRepository;

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

}

定制基础 Repository

当您想要定制基础 repository 行为以影响所有 repositories 时,上一节中描述的方法要求定制每个 repository 接口。为了改变所有 repositories 的行为,您可以创建一个扩展特定于持久化技术的 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
@EnableMongoRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
<repositories base-package="com.acme.repository"
     base-class="….MyRepositoryImpl" />