如何在Kotlin中枚举包及JAR内的Flag接口实现类?
实现跨模块加载Flag接口所有实现类的方案
原代码通过文件系统遍历指定包目录的方式,只能加载当前模块编译目录下的类,无法处理依赖模块(如ModuleA作为jar包被ModuleB引用)的类,也不能同时扫描多模块的包路径。以下是适配需求的解决方案:
核心思路
- 改用类路径扫描方式,覆盖当前类加载器能访问的所有资源(包括目录、jar包)
- 支持传入多个包前缀,精准控制扫描范围
- 用可靠的类型判断逻辑检测类是否实现Flag接口
- 测试场景通过传入不同包列表区分扫描范围:ModuleA测试仅传ModuleA的包,ModuleB测试同时传两个模块的包
具体实现代码
private val availableFlags = mutableMapOf<String, String>() private fun scanClasspathForFlags(vararg basePackages: String) { val flagInterface = Flag::class.java val classLoader = flagInterface.classLoader basePackages.forEach { basePackage -> val path = basePackage.replace('.', '/') val resources = classLoader.getResources(path) ?: return@forEach while (resources.hasMoreElements()) { val resource = resources.nextElement() when (resource.protocol) { "file" -> scanDirectoryForFlags(File(resource.file), basePackage, flagInterface) "jar" -> scanJarForFlags(resource, basePackage, flagInterface) } } } } // 处理目录形式的类路径资源 private fun scanDirectoryForFlags( directory: File, basePackage: String, flagInterface: Class<Flag> ) { if (!directory.exists()) return directory.walk() .filter { it.isFile && it.name.endsWith(".class") && !it.name.contains('$') } .forEach { file -> val relativePath = file.canonicalPath.removePrefix(directory.canonicalPath).drop(1) val className = "${basePackage}.${relativePath.replace('/', '.').dropLast(6)}" try { val clazz = Class.forName(className, false, flagInterface.classLoader) if (flagInterface.isAssignableFrom(clazz) && clazz != flagInterface) { availableFlags[clazz.simpleName] = className } } catch (e: ClassNotFoundException) { // 跳过无法加载的类 } catch (e: NoClassDefFoundError) { // 跳过依赖缺失的类 } } } // 处理jar包形式的类路径资源 private fun scanJarForFlags( resource: URL, basePackage: String, flagInterface: Class<Flag> ) { val jarPath = resource.file.substringAfter("file:").substringBefore("!") JarFile(jarPath).use { jar -> jar.entries().asSequence() .filter { entry -> !entry.isDirectory && entry.name.startsWith("${basePackage.replace('.', '/')}/") && entry.name.endsWith(".class") && !entry.name.contains('$') } .forEach { entry -> val className = entry.name.dropLast(6).replace('/', '.') try { val clazz = Class.forName(className, false, flagInterface.classLoader) if (flagInterface.isAssignableFrom(clazz) && clazz != flagInterface) { availableFlags[clazz.simpleName] = className } } catch (e: ClassNotFoundException) { // 跳过无法加载的类 } catch (e: NoClassDefFoundError) { // 跳过依赖缺失的类 } } } }
使用方式
- ModuleA核心逻辑/测试:调用
scanClasspathForFlags("com.company.projectName.moduleA"),仅扫描ModuleA内的Flag实现类 - ModuleB测试:调用
scanClasspathForFlags("com.company.projectName.moduleA", "com.company.projectName.moduleB"),同时扫描两个模块的实现类
关键优化点
- 用
flagInterface.isAssignableFrom(clazz)替代原代码的接口名称判断,避免同名接口导致的误判 - 类加载时传入
false参数(不初始化类),提升扫描性能 - 捕获类加载异常,防止扫描过程因依赖问题中断
- 支持目录和jar包两种类路径形式,覆盖跨模块依赖的场景
内容的提问来源于stack exchange,提问作者vgaltes
相关产品推荐
相关产品推荐

