转载

spring data 运行时添加JPA Repository

在特定的业务场景中,需要提供一个类似自定义实体的动态对象,并根据此对象生成相应的CRUD Repository。在这种场景中,与正常的domain对象不同,这个对象是在运行时,才定义出来,并产生相应的domain class和相应的repository class类。在业务体系中,需要将在运行时定义的对象重新加载至运行时中,并与普通的domain一样的操作。

如 普通的 AbcRepository ,可以使用如下的代码进行操作

@Autowired
private AbcRepository abcRepository;

abcRepository.findAll()

而新产生的repository,则可以通过如下的手法进行操作

val repository = (CrudRepository)applicationContext.getBean("newRepository");
repository.findAll();

本文即描述,通过 spring cloud 中自带的 RepositoryConfigurationDelegate, 使用新的自定义ClassLoader, 如何在运行时注册一个新的repository.

前提条件:

类名: com.iflym.repository.NewRepository, 此类通过字节码在运行时生成。实际定义为 NewRepository extends JpaRepository

存放位置: d:/tmp

扩展类加载器

为了数据隔离以及自定义实现,相应新产生的类都通过一个统一的扩展类加载器来完成,即将生成类统一放在一个指定的临时目录(也可以放在其它地方,取决于具体实现). 整个资源加载均通过此扩展ClassLoader来完成,后续在加载resource以及spring查找类时均使用此类来处理. 常规的扩展classLoader均通过继承自 URLClassLoader来处理.

简单定义如下

public class NewClassLoader extends URLClassLoader {
    public NewClassLoader() {
        super(new URL[]{new File("d:/temp").toURI().toURL())}, parentClassLoader);
    }
}

扩展资源加载器

spring 体系中, classLoader用于加载类,而 resourceLoader 则用于加载资源。在注册过程中,spring 容器会尝试使用 resourceLoader加载类信息,以获取特定的信息,如 annotationMetadata 信息。因此这里,通过扩展类classLoader,同样定义 扩展resourceLoader

这里直接使用 new DefaultResourceLoader(NewClassLoader.CLASS_LOADER) 来完成.

JPA Repository标准扫描及定义机制

spring cloud中 jpa repository的扫描是通过 @EntityScan 和 @EnableJpaRepositories 来定义,同时 @EnableJpaRepositories 还会定义 用于构建 repository 的 facotryBeanClass, 以及定义出来的repository实现默认由哪个实现类来处理。在注册新的repository时,同样需要 由 @EnableJpaRepositories 标注的接口来描述这些信息,由 spring data 标注的 RepositoryConfigurationSource 需要由 相应的接口来查找meta 注解信息。

简单定义如下

@EnableJpaRepositories(repositoryFactoryBeanClass = NewJpaRepositoryFactoryBean.class,
        repositoryBaseClass = SimpleJpaRepository.class)
public class NewRepositoryConfiguration {
}

因为,由标注上并没有定义需要扫描的类信息,因此 spring data 中 AnnotationRepositoryConfigurationSource 可能并不能简单由 注解上拿到需要扫描的类。并且这里也并不适合扫描到并不该扫描的类,以避免重复注册bean的Exception产生。这里通过重写 getCandidates 方法来处理此问题。即通过定义一个新的 AnnotationRepositoryConfigurationSource,以产生自实现时所需要的 beanDefinition。

简单参考如下(以下代码有删减)

public class NewAnnotationRepositoryConfigurationSource extends AnnotationRepositoryConfigurationSource {
    private String className;

    public Streamable<BeanDefinition> getCandidates(@Nonnull ResourceLoader loader) {
        val classResourceName = className.replace('.', '/') + ".class";
        val is = loader.getResource(classResourceName).getInputStream();
        val classReader = new ClassReader(is);

        val annotationMetadata = new AnnotationMetadataReadingVisitor(NewClassLoader.CLASS_LOADER);
        classReader.accept(annotationMetadata, ClassReader.SKIP_DEBUG);
        val definition = new AnnotatedGenericBeanDefinition(annotationMetadata);

        return Streamable.of(definition);
    });
}

以上代码(主要为demo实现),仅返回所需要的定义信息. 并且为完整的包括注解信息的beanDefinition, 这里并不能简单地使用 RootBeanDefinition, 因为相应的类还没有加载,并且也没有相应 annotationMetadata 信息.

JPA Repository 类 构建扩展

通过jpa 扫描出来的类, 仅仅是接口信息,spring data 需要通过此进一步构建出 实现类的beanDefinition. 需要通过 BeanDefinitionBuilder 进一步处理,如注入transactionManager, entityManager 等。在spring data 中,此过程是通过 JpaRepositoryConfigExtension 来完成. 一般情况下, spring data 均能正常地注入相应的信息, 并通过spring 容器 autowire正常处理。

这里的问题在于,在构建 factoryBean JpaRepositoryFactoryBean时,其构造器需要 传递 repositoryInterface 对于默认的spring 容器处理时,这里传递的为接口的字符串形式,在构建对象时再进行从字符串到类的转换。转换时需要进行class.forName加载类,对于这里的扩展类,则直接会class not found Exception. 因此,这里进行调整,将相应的参数直接则默认的字符串调用为已经转换好的class形式。

参考代码如下:

public class NewJpaRepositoryConfigExtension extends JpaRepositoryConfigExtension {
    @Override
    public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) {
        super.postProcess(builder, source);

        //替代原参数值从 string 到 class,因为构建时无法class.forname, classLoader不一样
        val argumentValue = builder.getRawBeanDefinition().getConstructorArgumentValues().getIndexedArgumentValue(0, String.class);
        String v = (String) Objects.requireNonNull(argumentValue.getValue());
        val clazz = ClassUtils.forName(v, NewClassLoader.CLASS_LOADER);

        builder.getRawBeanDefinition().getConstructorArgumentValues().addIndexedArgumentValue(0, clazz);
    }
}

注册新的repository

准备好上述工作之后,即直接使用 spring data 中的处理类,直接进行注册即可

RepositoryConfigurationDelegate delegate = new RepositoryConfigurationDelegate(configurationSource, resourceLoader, environment);
delegate.registerRepositoriesIn(registry, extension);

上述的registry和enviroment均为当前运行时spring 容器,而其它参数均根据扩展classLoader和要处理的类进行调整. 其中configurationSource进行 beanDefinition 查找, resourceLoader 进行类资源查找, extension 进行 beanDefinitonBuilder 扩展.

至此,整个类即完成相应的注册工作。在业务代码中,即可以通过 application.getBean(“new”) 拿到已经成功处理好的类,并且与正常bean 一样使用(包括事务等功能均工作正常).

总结

本文总共涉及到的扩展类和原始类如下:

RepositoryConfiguration 标记注解 EnableJpaRepositories

NewAnnotationRepositoryConfigurationSource 对应类 AnnotationRepositoryConfigurationSource

NewJpaRepositoryFactoryBean 对应类 JpaRepositoryFactoryBean

NewClassLoader 对应类 URLClassLoader

NewJpaRepositoryConfigExtension 对应类 JpaRepositoryConfigExtension

原文  https://www.iflym.com/index.php/code/201912140001.html
正文到此结束
Loading...