寻求Kotlin多平台、与实现无关的依赖注入方案
适配Spring Boot与Ktor的Kotlin多平台库DI方案
核心思路是基于Jakarta Inject API做抽象层,同时为Spring Context和Koin分别实现适配桥接,让库本身完全不绑定任何DI框架,用户可以根据自身项目选择对应适配模块即可。
1. 库核心代码:用Jakarta Inject定义无绑定的配置与Bean
库的公共多平台模块只依赖Jakarta Inject的基础注解(@Inject、@Singleton),同时自定义一个标记配置类的注解(类似Spring的@Configuration),统一Bean的声明方式:
// 多平台公共模块代码 import jakarta.inject.Inject import jakarta.inject.Singleton // 自定义配置类标记注解 @Target(AnnotationTarget.CLASS) annotation class LibraryModule // 库的配置类,类似Spring @Configuration @LibraryModule class DataModule { // 声明单例Bean,对应Spring @Bean + @Singleton @Singleton fun userRepository(): UserRepository { return JdbcUserRepository() } // 依赖注入其他Bean,用@Inject标记参数 @Singleton fun userService(@Inject repo: UserRepository): UserService { return DefaultUserService(repo) } } // 业务Bean,用Jakarta注解声明依赖 interface UserRepository class JdbcUserRepository : UserRepository interface UserService class DefaultUserService @Inject constructor(val repo: UserRepository) : UserService
2. Spring Boot适配:桥接Jakarta注解到Spring Context
Spring本身原生支持Jakarta Inject的@Inject和@Singleton,我们只需要实现一个ImportBeanDefinitionRegistrar,扫描库中带@LibraryModule的配置类,自动将其中的Bean方法注册到Spring上下文:
// Spring适配模块(仅JVM) import org.springframework.context.annotation.ImportBeanDefinitionRegistrar import org.springframework.core.type.AnnotationMetadata import org.springframework.beans.factory.support.BeanDefinitionRegistry import org.springframework.beans.factory.support.GenericBeanDefinition import org.springframework.core.io.support.PathMatchingResourcePatternResolver import org.springframework.core.type.classreading.CachingMetadataReaderFactory class LibrarySpringRegistrar : ImportBeanDefinitionRegistrar { override fun registerBeanDefinitions(metadata: AnnotationMetadata, registry: BeanDefinitionRegistry) { // 扫描classpath中所有带@LibraryModule的类 val resolver = PathMatchingResourcePatternResolver() val readerFactory = CachingMetadataReaderFactory(resolver) val resources = resolver.getResources("classpath*:**/*.class") resources.forEach { resource -> val reader = readerFactory.getMetadataReader(resource) val classMetadata = reader.classMetadata if (classMetadata.hasAnnotation(LibraryModule::class.qualifiedName)) { val configClass = Class.forName(classMetadata.className) // 注册配置类本身到Spring上下文 registry.registerBeanDefinition(configClass.simpleName, GenericBeanDefinition().apply { beanClass = configClass }) // 解析配置类中的Bean方法 configClass.declaredMethods.filter { it.isAnnotationPresent(Singleton::class.java) }.forEach { method -> val beanDef = GenericBeanDefinition().apply { beanClass = method.returnType factoryBeanName = configClass.simpleName factoryMethodName = method.name scope = "singleton" // 自动注入方法参数中的依赖 autowireMode = org.springframework.beans.factory.config.AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE } registry.registerBeanDefinition(method.returnType.simpleName, beanDef) } } } } } // Spring Boot应用中引入库的方式 @SpringBootApplication @Import(LibrarySpringRegistrar::class) class SpringApp
3. Ktor(Koin)适配:桥接Jakarta注解到Koin模块
Koin支持多平台,我们可以写一个Koin模块生成器,扫描库的@LibraryModule配置类,将其中的Bean声明转换成Koin的single/factory定义,同时处理@Inject的依赖注入:
// Koin适配模块(多平台) import org.koin.core.module.Module import org.koin.dsl.module import jakarta.inject.Singleton import jakarta.inject.Inject fun libraryKoinModule(): Module = module { // JVM可用反射扫描,JS/Native建议手动指定配置类(受反射限制) val configClasses = listOf(DataModule::class.java) configClasses.forEach { configClass -> // 注册配置类实例 single { configClass.getDeclaredConstructor().newInstance() } // 解析配置类中的Bean方法 configClass.declaredMethods.filter { it.isAnnotationPresent(Singleton::class.java) }.forEach { method -> val beanProvider = { val configInstance = get(configClass) // 解析方法参数,自动注入Koin中的依赖 val args = method.parameters.map { param -> if (param.isAnnotationPresent(Inject::class.java)) { get(param.type.kotlin) } else null }.filterNotNull() method.invoke(configInstance, *args.toTypedArray()) } if (method.isAnnotationPresent(Singleton::class.java)) { single(method.returnType.kotlin) { beanProvider() } } else { factory(method.returnType.kotlin) { beanProvider() } } } } // 直接处理带@Singleton的业务Bean single<UserRepository> { JdbcUserRepository() } single<UserService> { DefaultUserService(get()) } } // Ktor应用中引入库的方式 fun Application.module() { install(Koin) { modules(libraryKoinModule()) } }
关键注意事项
- 多平台类扫描兼容:JVM可以用反射扫描classpath,JS/Native由于反射能力有限,建议通过编译时注解处理器生成配置类清单,或者让用户手动传入配置类列表。
- 编译时优化:如果担心运行时扫描的性能开销,可以用KAPT写一个注解处理器,在编译时为Spring生成对应的
@Configuration类,为Koin生成对应的Module代码,运行时直接加载生成的代码即可。 - 依赖隔离:库的核心模块只依赖Jakarta Inject API,Spring和Koin的适配代码分别放在独立的子模块中,用户按需引入即可,完全避免DI框架绑定。
内容的提问来源于stack exchange,提问作者meijuh
相关产品推荐
相关产品推荐

