You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 23:45:34