动态创建@EnableJpaRepositories配置类后Repository注入失败排查
背景
我正在为Spring Boot集成多数据库开发封装工具,流程如下:
- 从配置文件读取多数据库配置;
- 为每个数据库在运行时创建标注
@Configuration的配置类; - 在Spring Boot运行时注册该配置Bean。
动态生成的配置类模板
@Configuration @Component @EnableTransactionManagement @EnableJpaRepositories public class TESTDBJpaConfig { @Autowired private LoadTimeWeaver loadTimeWeaver; @Bean("testdbdatasource") public DataSource clusterDataSource() throws Exception { // 根据配置初始化数据源:例如testDataSOurce return testDataSOurce; } @Bean("testentitymanager") public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws Exception { LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean(); entityManagerFactory.setDataSource(clusterDataSource()); entityManagerFactory.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); String packageName = ""; // 从配置中获取包名 entityManagerFactory.setPackagesToScan(packageName); entityManagerFactory.setLoadTimeWeaver(loadTimeWeaver); return entityManagerFactory; } @Bean("testtxnmanager") public PlatformTransactionManager transactionManager() throws Exception { JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(entityManagerFactory().getObject()); return transactionManager; } }
动态注册与注解修改代码
@Component public class DatabasePostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { try { String generatedCode = "// 上述@Configuration类的示例代码字符串" String className = "com.example.database.TESTDBJpaConfig"; Class<?> generatedClass = InMemoryJavaCompiler.newInstance().compile(className, generatedCode); // 更新@EnableJpaRepositories注解为正确值 updateJpaAnnotation(generatedClass); // 在此注册Bean BeanDefinitionRegistry registry = ((BeanDefinitionRegistry )beanFactory); GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); beanDefinition.setBeanClass(generatedClass); beanDefinition.setLazyInit(false); beanDefinition.setAbstract(false); beanDefinition.setAutowireCandidate(true); registry.registerBeanDefinition(className,beanDefinition); beanDefinition.getBeanClass().newInstance(); } catch (Exception exp) { log.error("Unable to setup database"); } } private void updateJpaAnnotation(Class<?> generatedClass) throws Exception { Method method = Class.class.getDeclaredMethod("annotationData", null); method.setAccessible(true); Object annotationData = method.invoke(generatedClass, null); Field declaredAnnotations = annotationData.getClass().getDeclaredField("declaredAnnotations"); declaredAnnotations.setAccessible(true); Map<Class<? extends Annotation>, Annotation> annotations = (Map<Class<? extends Annotation>, Annotation>) declaredAnnotations.get(annotationData); Annotation oldJpaAnnotation = annotations.get(EnableJpaRepositories.class); Annotation newJpaAnnotation = new EnableJpaRepositories(){ @Override public Class<? extends Annotation> annotationType() { return oldJpaAnnotation.annotationType(); } @Override public String[] basePackages() { return // 从配置获取的基础包; } @Override public String entityManagerFactoryRef() { return "testentitymanager"; } @Override public String transactionManagerRef() { return "testtxnmanager"; } // 其余代码 }; annotations.put(EnableJpaRepositories.class, newJpaAnnotation); } }
问题描述
运行代码后配置Bean已注册,但@Autowired任何Repository类时报错:required a bean of type XYZ Repository not found。按理论,注册带有更新后basePackages属性的@EnableJpaRepositories配置Bean后应该能扫描到Repository,请问哪里出错了?
核心问题点
你的实现存在三个关键问题,导致Spring无法识别动态修改后的@EnableJpaRepositories注解,进而无法扫描Repository:
BeanFactoryPostProcessor执行时机过早
Spring对@EnableJpaRepositories的解析是在ConfigurationClassPostProcessor阶段完成的,而你的DatabasePostProcessor作为BeanFactoryPostProcessor,执行时机早于配置类解析流程。此时注册的配置Bean还没被Spring的配置类处理器扫描到,自然不会触发Repository扫描逻辑。动态修改注解的方式不被Spring认可
通过反射修改Class内部的annotationData属于绕过Spring注解元数据解析机制的操作,Spring解析注解时会直接读取类字节码中的原始注解信息,不会读取你通过反射修改后的内存中注解对象,这种修改对Spring完全无效。手动实例化Bean的操作多余且有问题
注册BeanDefinition后调用beanDefinition.getBeanClass().newInstance()是多余的,Spring会自行负责Bean实例化。手动实例化的对象不会被Spring容器管理,反而可能引发冲突。
正确的实现方案
方案1:生成代码时直接注入正确的注解属性
不要在生成类后修改注解,而是在动态生成代码字符串时,直接填充basePackages、entityManagerFactoryRef等属性:
// 生成代码时直接写入正确的注解属性 String basePackage = "com.example.repo.testdb"; String generatedCode = "@Configuration\n" + "@EnableTransactionManagement\n" + "@EnableJpaRepositories(basePackages = \"" + basePackage + "\", " + "entityManagerFactoryRef = \"testentitymanager\", " + "transactionManagerRef = \"testtxnmanager\")\n" + "public class TESTDBJpaConfig {\n" + // 类的其余代码... "}";
生成的类本身带有正确注解,Spring解析时能直接识别。
方案2:改用ImportBeanDefinitionRegistrar动态注册Repository扫描器
如果必须在运行时动态调整扫描参数,可以实现ImportBeanDefinitionRegistrar,直接注册Repository相关的BeanDefinition:
public class DynamicRepositoryRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { // 从配置获取数据库相关参数 String basePackage = "com.example.repo.testdb"; String emRef = "testentitymanager"; String txRef = "testtxnmanager"; // 创建Repository扫描器 RepositoryComponentProvider scanner = new RepositoryComponentProvider( false, new AnnotationConfigApplicationContext(registry)); scanner.addIncludeFilter(new AnnotationTypeFilter(Repository.class)); Set<BeanDefinition> repoDefinitions = scanner.findCandidateComponents(basePackage); // 为每个Repository注册FactoryBean for (BeanDefinition bd : repoDefinitions) { GenericBeanDefinition factoryBeanDef = new GenericBeanDefinition(); factoryBeanDef.setBeanClass(JpaRepositoryFactoryBean.class); factoryBeanDef.getConstructorArgumentValues().addGenericArgumentValue(bd.getBeanClassName()); factoryBeanDef.getPropertyValues().add("entityManagerFactoryBeanName", emRef); factoryBeanDef.getPropertyValues().add("transactionManagerBeanName", txRef); registry.registerBeanDefinition(bd.getBeanClassName(), factoryBeanDef); } } }
在主配置类上添加@Import(DynamicRepositoryRegistrar.class)即可生效。
方案3:使用ApplicationContextInitializer调整注册时机
如果坚持动态生成配置类,使用ApplicationContextInitializer在容器启动早期注册BeanDefinition,确保配置类能被ConfigurationClassPostProcessor扫描到:
public class DatabaseInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext applicationContext) { ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory(); BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; try { // 生成带有正确@EnableJpaRepositories属性的配置类代码 String basePackage = "com.example.repo.testdb"; String generatedCode = "@Configuration\n" + "@EnableTransactionManagement\n" + "@EnableJpaRepositories(basePackages = \"" + basePackage + "\", " + "entityManagerFactoryRef = \"testentitymanager\", " + "transactionManagerRef = \"testtxnmanager\")\n" + "public class TESTDBJpaConfig {\n" + // 类的其余代码... "}"; String className = "com.example.database.TESTDBJpaConfig"; Class<?> generatedClass = InMemoryJavaCompiler.newInstance().compile(className, generatedCode); GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); beanDefinition.setBeanClass(generatedClass); registry.registerBeanDefinition(className, beanDefinition); } catch (Exception e) { throw new RuntimeException("Failed to register dynamic database config", e); } } }
然后在META-INF/spring.factories中添加:
org.springframework.context.ApplicationContextInitializer=com.example.database.DatabaseInitializer
额外注意事项
- 动态生成的配置类中,
entityManagerFactory()方法里的setPackagesToScan也要填充正确的实体类包名,否则实体类无法被扫描到。 - 确保每个数据库的
entityManagerFactoryRef、transactionManagerRef、dataSource的Bean名称唯一,避免冲突。
内容的提问来源于stack exchange,提问作者Amit Singh

