在特定的业务场景中,需要提供一个类似自定义实体的动态对象,并根据此对象生成相应的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) 来完成.
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 扫描出来的类, 仅仅是接口信息,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); } }
准备好上述工作之后,即直接使用 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