返回

避坑: Java Classpath JSON配置文件加载全攻略

java

避坑指南:Java 中加载 Classpath 内的 JSON 配置文件

写代码时,从 classpath 加载个 JSON 配置文件挺常见的,比如 configuration.json。通常我们期望程序能顺利读到它,然后按配置运行。但有时,就像下面这段代码遇到的情况,明明感觉文件就在那儿,程序偏偏加载不到,最后只能 fallback 到默认配置,让人有点摸不着头脑。

来看看这段有问题的代码:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

// 假设 Configuration 类已定义
// import your.app.Configuration; 

public class ConfigLoader {

    private static final Logger log = LoggerFactory.getLogger(ConfigLoader.class);
    private Configuration configuration;

    // 模拟获取默认配置的方法
    private Configuration getDefaultConfiguration() {
        log.info("Using default configuration object.");
        // 返回一个预设的默认配置实例
        return new Configuration(); 
    }

    public void setConfiguration() {
        try {
            ClassLoader cl = this.getClass().getClassLoader();
            ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(cl);
            // 注意这里使用了 classpath*: 试图查找所有路径下的文件
            Resource[] resources = resolver.getResources("classpath*:/configuration.json");

            if (resources.length == 0) {
                log.warn("Configuration file 'configuration.json' not found on classpath. Using default configuration.");
                configuration = getDefaultConfiguration();
                // 加上 return,避免继续执行下面的 for 循环
                return; 
            }

            boolean foundAndLoaded = false;
            for (Resource resource : resources) {
                // 检查资源是否存在且可读,是个好习惯
                if (resource.exists() && resource.isReadable()) { 
                    try {
                        log.info("Attempting to load configuration from: {}", resource.getURL());
                        // 使用 try-with-resources 确保 InputStream 被关闭
                        try (java.io.InputStream inputStream = resource.getInputStream()) {
                            configuration = new ObjectMapper().readValue(inputStream, Configuration.class);
                            if (configuration != null) {
                                log.debug("Configuration loaded successfully from {}", resource.getDescription());
                                foundAndLoaded = true;
                                // 找到并成功加载后,通常就可以退出了,除非你有特殊需求要合并多个配置
                                break; 
                            }
                        }
                    } catch (Exception parseException) {
                        // 捕获解析单个文件可能出现的异常,比如 JSON 格式错误
                        log.warn("Failed to parse configuration from {}: {}", resource.getDescription(), parseException.getMessage());
                        // 这里可以选择继续尝试下一个资源,或者记录错误后中断
                    }
                } else {
                     log.warn("Resource found but is not accessible: {}", resource.getDescription());
                }
            }
             
            // 如果循环结束仍未成功加载配置
            if (!foundAndLoaded) {
                 log.warn("Found resources for 'configuration.json', but failed to load/parse any. Using default configuration.");
                 configuration = getDefaultConfiguration();
            }

        } catch (Exception e) {
            // 捕获 getResources 可能抛出的 IOException 或其他意外错误
            log.warn("Error occurred while trying to load configuration from classpath. Using default configuration.", e);
            configuration = getDefaultConfiguration();
        }
    }

    // Getter for configuration
    public Configuration getConfiguration() {
        if (configuration == null) {
            // 如果 setConfiguration 还没被调用或失败了,确保返回默认配置
            log.warn("Configuration object is null, falling back to default.");
            configuration = getDefaultConfiguration();
        }
        return configuration;
    }
    
    // main 方法用于测试
    public static void main(String[] args) {
        // 这里你需要确保你的 src/main/resources (或者测试环境的对应目录)
        // 下确实有一个名为 configuration.json 的文件
        // 并且内容是合法的 JSON,能被 Configuration.class 解析
        
        ConfigLoader loader = new ConfigLoader();
        loader.setConfiguration();
        Configuration currentConfig = loader.getConfiguration();
        
        // 在这里可以打印配置项,验证是否加载成功
        // System.out.println("Loaded config value: " + currentConfig.getSomeProperty()); 
        log.info("Configuration loading process finished.");
    }
}

// 假设的 Configuration 类,你需要根据你的 JSON 结构定义它
class Configuration {
    // public String someProperty; // 示例属性
    // public int someValue;      // 示例属性
    
    // Getter 和 Setter ...
    
    @Override
    public String toString() {
       // 提供一个有意义的 toString 方法方便调试
       return "Configuration{/* your properties here */}";
    }
}

代码用了 Spring 的 PathMatchingResourcePatternResolver,目标是找到 classpath 下的 configuration.json。但结果是,resources 数组长度总是 0,日志打印 “Configuration could not be loaded”,最后用了 getDefaultConfiguration()

问题出在哪?

分析一下,为啥 PathMatchingResourcePatternResolver 找不到文件呢?原因可能五花八门:

  1. 文件压根没在 Classpath 上: 这是最常见的原因。你以为文件在 src/main/resources 下就万事大吉了?编译、打包(比如用 Maven 或 Gradle)时,这些资源文件需要被正确处理并复制到最终的 classpath 目录下(通常是 target/classes 或 JAR 包的根目录)。确认一下你的构建配置,以及最终构建产物里,configuration.json 是不是真的在你期望的位置。
  2. classpath*: vs classpath: 的误解:
    • classpath: 只搜索当前 ClassLoader 加载路径的根目录。如果你的项目是单模块,并且 configuration.json 就在 src/main/resources 下,那么用 classpath:configuration.json 通常就够了。
    • classpath*: 会搜索整个 classpath,包括所有 JAR 包里的根目录。原代码用了 classpath*:/configuration.json,它的意图是查找所有 classpath 根目录下的 configuration.json。如果文件只存在于项目本身的输出目录,而不在任何依赖的 JAR 包里,classpath: 可能更直接、高效。但如果你的配置文件确实可能来自不同的 JAR 包(比如模块化设计),classpath*: 就是必要的。但要小心,它可能找到多个同名文件。
  3. 路径名写错了,或者大小写问题: 检查文件名是不是 configuration.json,有没有拼写错误?路径分隔符在 classpath:classpath*: 后用 / 是标准的。另外,在 Linux/macOS 这类系统上,文件名大小写是敏感的,Configuration.jsonconfiguration.json 是两个不同的文件。Windows 则不敏感。最好统一用小写。
  4. ClassLoader 的问题: this.getClass().getClassLoader() 获取的是加载当前类的 ClassLoader。在复杂的应用环境(比如某些应用服务器或 OSGi 框架)中,可能存在多个 ClassLoader。有时需要用线程上下文 ClassLoader (Thread.currentThread().getContextClassLoader()) 可能更合适,它通常能接触到更广泛的 classpath 资源。
  5. 文件被过滤或排除: 检查你的构建工具配置(pom.xmlbuild.gradle),有没有无意中排除了 .json 文件,或者 configuration.json 这个特定文件。
  6. 代码逻辑的小瑕疵: 原代码的循环逻辑有点问题。for 循环里,每次找到一个文件就尝试解析并覆盖 configuration 变量。如果 classpath*: 真的找到了多个 configuration.json (比如来自不同依赖包),最终生效的是最后一个成功解析的文件。这可能不是你想要的行为。通常,我们要么取第一个找到的,要么需要合并策略。另外,原代码在 resources.length == 0 时才设置默认配置,但如果找到了文件,但在循环中解析失败,最终 configuration 变量可能还是 null,导致后续使用时出问题。需要在循环后增加检查。

可行的解决方案

针对上面的分析,我们有几种不同的方法来解决或改进这个问题。

方案一:返璞归真 - 使用 ClassLoader.getResourceAsStream

这是最基础、不依赖任何框架的标准 Java 方法,非常适合加载单个确定的 classpath 资源。

原理与作用:

ClassLoader.getResourceAsStream(String name) 直接在 ClassLoader 的搜索路径(通常是编译输出目录和依赖的 JAR 包)中查找指定名称的资源,并返回一个 InputStream。它简单直接,没有复杂的模式匹配。

操作步骤:

  1. 确保 configuration.json 文件位于 src/main/resources 目录下。
  2. 修改代码,使用 getResourceAsStream

代码示例:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.io.IOException;

// ... Configuration class definition ...

public class ConfigLoaderSimplified {

    private static final Logger log = LoggerFactory.getLogger(ConfigLoaderSimplified.class);
    private Configuration configuration;

    private Configuration getDefaultConfiguration() {
        log.info("Using default configuration object.");
        return new Configuration();
    }

    public void loadConfiguration() {
        // 获取当前线程的上下文 ClassLoader,通常更健壮
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        // 如果获取不到,回退到加载本类的 ClassLoader
        if (classLoader == null) {
            classLoader = ConfigLoaderSimplified.class.getClassLoader();
        }
        
        // 注意:路径不要以 / 开头,相对于 classpath 根目录
        String resourcePath = "configuration.json"; 

        // 使用 try-with-resources 确保 InputStream 自动关闭
        try (InputStream inputStream = classLoader.getResourceAsStream(resourcePath)) {
            if (inputStream == null) {
                log.warn("Resource '{}' not found on classpath. Using default configuration.", resourcePath);
                configuration = getDefaultConfiguration();
            } else {
                log.info("Found resource '{}', attempting to load.", resourcePath);
                ObjectMapper objectMapper = new ObjectMapper();
                configuration = objectMapper.readValue(inputStream, Configuration.class);
                log.debug("Configuration loaded successfully from '{}'.", resourcePath);
            }
        } catch (IOException e) {
            log.warn("Failed to load or parse configuration from '{}'. Using default configuration.", resourcePath, e);
            configuration = getDefaultConfiguration();
        } catch (Exception e) { // 捕获可能的 JSON 解析异常或其他运行时异常
             log.error("Unexpected error during configuration loading. Using default configuration.", e);
             configuration = getDefaultConfiguration();
        }
        
        // 确保 configuration 不为 null
        if (configuration == null) {
             log.warn("Configuration loading resulted in null. Falling back to default.");
             configuration = getDefaultConfiguration();
        }
    }
    
    public Configuration getConfiguration() {
        if (configuration == null) {
             loadConfiguration(); // 或者在构造函数、首次访问时加载
        }
        return configuration;
    }

    // main 方法用于测试
    public static void main(String[] args) {
        ConfigLoaderSimplified loader = new ConfigLoaderSimplified();
        loader.loadConfiguration(); // 主动加载配置
        Configuration config = loader.getConfiguration();
        log.info("Configuration loading process finished.");
        // 打印配置进行验证
        // System.out.println(config); 
    }
}

安全建议:

  • 务必使用 try-with-resources 语句来管理 InputStream,确保它在任何情况下(包括异常)都能被关闭,避免资源泄露。
  • 充分处理 getResourceAsStream 返回 null 的情况(表示文件未找到)。
  • 捕获并处理 ObjectMapper.readValue 可能抛出的 IOException (如文件读取错误) 或 Jackson 库自身的 JSON 解析异常(如 JsonParseException, JsonMappingException)。

进阶使用技巧:

  • 路径细节: ClassLoader.getResourceAsStream("path/to/resource.txt") 的路径是相对于 classpath 根目录的。不要 在路径前面加 /。如果用 YourClass.class.getResourceAsStream("/path/to/resource.txt"),路径前面加 / 表示从 classpath 根目录查找;不加 / 则表示相对于该类的包路径查找。一般用 ClassLoader 的方式更不容易混淆。
  • 字符编码: 如果你的 JSON 文件不是 UTF-8 编码(虽然强烈推荐使用 UTF-8),你可能需要用 InputStreamReader 包装 InputStream 并指定正确的字符集:new ObjectMapper().readValue(new InputStreamReader(inputStream, StandardCharsets.YOUR_CHARSET), Configuration.class);

方案二:拥抱 Spring - 使用 ResourceLoader

如果你的项目已经使用了 Spring Framework,那么利用 Spring 提供的 ResourceLoader 是一个更优雅、更统一的方式。

原理与作用:

Spring 的 Resource 接口是对各种底层资源(文件系统、classpath、URL 等)的统一抽象。ResourceLoader (通常是 ApplicationContext 本身) 可以根据资源路径前缀(如 classpath:, file:, http:)自动选择合适的 Resource 实现来加载资源。

操作步骤:

  1. 确保你的类是 Spring 管理的 Bean(例如,使用 @Component, @Service 等注解)。
  2. 注入 ResourceLoader
  3. 使用 resourceLoader.getResource() 获取 Resource 对象。
  4. Resource 对象获取 InputStream 并解析。

代码示例:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct; // JSR-250 标准注解
import java.io.InputStream;
import java.io.IOException;

// ... Configuration class definition ...

@Component // 让 Spring 管理这个 Bean
public class ConfigLoaderSpring {

    private static final Logger log = LoggerFactory.getLogger(ConfigLoaderSpring.class);
    
    @Autowired // 注入 ResourceLoader
    private ResourceLoader resourceLoader; 
    
    @Autowired // 假设 ObjectMapper 也是一个 Bean,或者直接 new
    private ObjectMapper objectMapper;

    private Configuration configuration;

    private Configuration getDefaultConfiguration() {
        log.info("Using default configuration object.");
        return new Configuration();
    }

    // 使用 @PostConstruct,在 Bean 初始化后自动加载配置
    @PostConstruct 
    public void initializeConfiguration() {
        // 使用 classpath: 前缀明确指定从 classpath 加载
        Resource resource = resourceLoader.getResource("classpath:configuration.json");

        if (!resource.exists() || !resource.isReadable()) {
            log.warn("Configuration file 'classpath:configuration.json' not found or not readable. Using default configuration.");
            configuration = getDefaultConfiguration();
            return; // 明确返回
        }

        try (InputStream inputStream = resource.getInputStream()) {
            configuration = objectMapper.readValue(inputStream, Configuration.class);
            log.info("Configuration loaded successfully from {}", resource.getDescription());
        } catch (IOException e) {
            log.warn("Failed to load or parse configuration from {}. Using default configuration.", resource.getDescription(), e);
            configuration = getDefaultConfiguration();
        } catch (Exception e) { // 捕获可能的 JSON 解析异常等
             log.error("Unexpected error during configuration loading. Using default configuration.", e);
             configuration = getDefaultConfiguration();
        }
        
        // 确保 configuration 不为 null
        if (configuration == null) {
             log.warn("Configuration loading resulted in null after attempting load. Falling back to default.");
             configuration = getDefaultConfiguration();
        }
    }

    public Configuration getConfiguration() {
        // 由于 @PostConstruct 在依赖注入完成后执行,理论上 configuration 已经被初始化了
        // 但作为防御性编程,检查一下总没错
        if (configuration == null) {
             log.error("Configuration accessed before initialization or initialization failed. Returning default.");
             // 考虑是否应该抛出异常,或者返回一个不可变的默认实例
             return getDefaultConfiguration(); 
        }
        return configuration;
    }
    
     // 用于演示如何在 Spring 环境外(例如单元测试)使用它
     // 但通常你会从 Spring Context 获取这个 Bean 实例
    public static void main(String[] args) {
        // 这里模拟 Spring 环境的启动过程,实际应用中由 Spring 容器负责
        // 这个 main 方法仅为示意,直接运行可能因为缺少 Spring Context 而失败
        // 你需要配置一个简单的 Spring Boot 应用或 ApplicationContext 来测试
        log.error("This main method is for demonstration purposes only and requires a Spring context to run properly.");
        // ConfigLoaderSpring loader = new ConfigLoaderSpring(); // 错误!不能直接 new
        // ApplicationContext context = ...; // 需要启动 Spring 容器
        // ConfigLoaderSpring loader = context.getBean(ConfigLoaderSpring.class);
        // Configuration config = loader.getConfiguration();
        // System.out.println(config);
    }
}

安全建议:

  • 同样,必须用 try-with-resources 管理 InputStream
  • 检查 resource.exists()resource.isReadable() 是个好习惯,可以在尝试读取前就发现问题。
  • 妥善处理所有可能的异常。

进阶使用技巧:

  • classpath: vs classpath*:: 对于 ResourceLoadergetResource("classpath:myconfig.json") 只会返回第一个找到的匹配资源。如果需要查找所有匹配的资源(类似原代码的意图),需要使用 ResourcePatternResolverApplicationContext 通常也是一个 ResourcePatternResolver),并调用 getResources("classpath*:myconfig.json")。这时就要处理返回的 Resource[] 数组了,逻辑可以参考下面的方案三改进版。
  • 注入 Resource: Spring 支持直接注入 Resource 对象,如果你用 @Value("classpath:configuration.json") 注解一个 Resource 类型的字段,Spring 会自动帮你加载好。这更简洁:
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.core.io.Resource;
    
    @Component
    public class ConfigLoaderValue {
        private static final Logger log = LoggerFactory.getLogger(ConfigLoaderValue.class);
    
        @Value("classpath:configuration.json")
        private Resource configFile;
    
        @Autowired
        private ObjectMapper objectMapper;
    
        private Configuration configuration;
    
        @PostConstruct
        public void init() {
             // ... (加载逻辑类似方案二,使用 this.configFile) ...
        }
        // ... (getConfiguration, getDefaultConfiguration) ...
    }
    

方案三:对症下药 - 调优 PathMatchingResourcePatternResolver

如果你确实需要 classpath*: 的行为(比如,配置文件可能分散在不同的 JAR 包里),那么需要正确地使用 PathMatchingResourcePatternResolver 并完善逻辑。

原理与作用:

PathMatchingResourcePatternResolver 专门用于解析带通配符或 classpath*: 前缀的资源路径,能找出所有匹配的资源。原代码的问题可能在于文件实际位置、路径模式,或者对找到的多个资源处理不当。

操作步骤:

  1. 再次确认 configuration.json 是否真的在 classpath 的某个地方(项目输出目录或某个依赖 JAR 的根目录)。
  2. 考虑是否真的需要 classpath*:。如果只需要项目本身的配置文件,classpath: 可能更合适、更简单。
  3. 改进处理找到的多个资源的逻辑。是只取第一个?还是合并?还是报错?

代码示例(改进原代码):

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import java.io.InputStream;
import java.io.IOException;

// ... Configuration class definition ...

public class ConfigLoaderAdvanced {

    private static final Logger log = LoggerFactory.getLogger(ConfigLoaderAdvanced.class);
    private Configuration configuration;

    private Configuration getDefaultConfiguration() {
        log.info("Using default configuration object.");
        return new Configuration();
    }

    public void loadConfigurationUsingResolver() {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        if (cl == null) {
            cl = ConfigLoaderAdvanced.class.getClassLoader();
        }
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(cl);
        String pattern = "classpath*:configuration.json"; // 或者 "classpath:configuration.json" 如果你确定只需要当前 classpath

        try {
            Resource[] resources = resolver.getResources(pattern);

            if (resources.length == 0) {
                log.warn("No resources found matching pattern '{}'. Using default configuration.", pattern);
                configuration = getDefaultConfiguration();
                return;
            }

            log.info("Found {} resources matching pattern '{}'. Attempting to load the first valid one.", resources.length, pattern);

            Configuration loadedConfig = null;
            for (Resource resource : resources) {
                if (resource.exists() && resource.isReadable()) {
                    log.debug("Attempting to load from potential source: {}", resource.getDescription());
                    try (InputStream inputStream = resource.getInputStream()) {
                        ObjectMapper objectMapper = new ObjectMapper(); // 或者注入/复用 ObjectMapper 实例
                        loadedConfig = objectMapper.readValue(inputStream, Configuration.class);
                        // 只要成功加载并解析了一个,就停止(取第一个策略)
                        log.info("Configuration successfully loaded from {}.", resource.getDescription());
                        break; 
                    } catch (IOException e) {
                        log.warn("Failed to read or parse configuration from {}. Trying next resource if available.", resource.getDescription(), e);
                        // 这里可以选择继续尝试下一个资源
                    } catch (Exception e) { // 捕获 Jackson 解析异常等
                         log.warn("Error parsing configuration from {}: {}. Trying next resource if available.", resource.getDescription(), e.getMessage());
                         // 同样选择继续
                    }
                } else {
                     log.warn("Resource {} found but is not accessible (exists: {}, readable: {}). Skipping.", resource.getDescription(), resource.exists(), resource.isReadable());
                }
            } // end for loop

            // 检查循环结束后是否成功加载了配置
            if (loadedConfig != null) {
                configuration = loadedConfig;
            } else {
                log.warn("Though resources were found for pattern '{}', none could be successfully loaded/parsed. Using default configuration.", pattern);
                configuration = getDefaultConfiguration();
            }

        } catch (IOException e) {
            // 捕获 getResources 可能抛出的 IOException
            log.error("Error resolving resources for pattern '{}'. Using default configuration.", pattern, e);
            configuration = getDefaultConfiguration();
        } catch (Exception e) { // 捕获其他可能的意外错误
            log.error("Unexpected error during configuration resolution or loading. Using default configuration.", e);
            configuration = getDefaultConfiguration();
        }
        
        // 最后再检查一次,防御性编程
         if (configuration == null) {
             log.warn("Configuration is unexpectedly null after load process. Falling back to default.");
             configuration = getDefaultConfiguration();
        }
    }

    public Configuration getConfiguration() {
         if (configuration == null) {
              loadConfigurationUsingResolver(); // 触发加载
         }
         return configuration;
     }

    // main 方法用于测试
    public static void main(String[] args) {
        // 确保 classpath 下有 configuration.json 文件
        ConfigLoaderAdvanced loader = new ConfigLoaderAdvanced();
        loader.loadConfigurationUsingResolver();
        Configuration config = loader.getConfiguration();
        log.info("Configuration loading process finished.");
        // System.out.println(config);
    }
}

安全建议:

  • 处理 getResources 可能抛出的 IOException
  • 循环内部对每个 ResourcegetInputStream 调用也要用 try-with-resources
  • 对每个资源的解析都要单独进行异常处理,避免一个损坏的配置文件影响到加载其他可能有效的配置文件(如果你采取了某种合并策略的话)。
  • 明确你处理多个匹配资源的策略:取第一个?最后一个?合并?报错?

进阶使用技巧:

  • 通配符: PathMatchingResourcePatternResolver 支持 Ant 风格的路径通配符,比如 classpath*:/config/**/*.json 可以加载 config 目录下(及其子孙目录)的所有 .json 文件。
  • 合并配置: 如果你确实需要从多个找到的 configuration.json 文件中合并配置,你需要定义一套合并规则。比如,后找到的配置覆盖先找到的配置项,或者对于列表类型的配置进行追加等。这通常需要更复杂的逻辑,可能需要借助专门的配置库(如 Spring Cloud Config Client,Typesafe Config 等,它们内置了强大的配置加载和合并功能)。

通用排错技巧

无论用哪种方法,遇到问题时,试试这些:

  1. 确认文件位置: 解压你的 JAR 包,或者查看 target/classes (或类似) 目录,用肉眼确认 configuration.json 文件是不是真的在你认为它应该在的地方。路径、文件名、扩展名都看仔细。
  2. 检查构建过程: 看看 Maven 或 Gradle 的输出日志,确认 process-resources 任务有没有正确执行,有没有相关警告或错误。
  3. 注意大小写: 特别是在 Linux 或 macOS 上开发/部署时,保证代码里引用的文件名大小写和实际文件名完全一致。
  4. 打个断点: 在调用资源加载的代码行(如 getResourceAsStream, getResource, getResources)打个断点,检查传入的路径字符串是否正确,看看 ClassLoader 对象是什么,它的内部状态(比如 URLClassLoader 的 URLs)是否包含了你期望的路径。检查 resource.getURL()resource.getDescription() 的输出,看它实际找到了什么路径。
  5. 简化环境: 如果在复杂环境(如 Web 服务器)中遇到问题,尝试写一个简单的 main 方法独立运行加载代码,排除环境干扰。

加载 classpath 资源有时确实会遇到点小麻烦,但通常只要理清文件位置、路径写法、ClassLoader 和使用的工具这几个环节,问题基本都能解决。希望上面的分析和方案能帮你搞定 JSON 配置文件的加载问题。