spring-boot:apache commons-configuration2 异常:: name原因分析

时间:2025-03-21 21:13:16

最近在设计一个spring-boot的服务,在开发环境(IDE)运行的时候,没有任何问题,
但如下在命令行运行使用spring-boot-maven-plugin插件打成Fat-Jar 服务jar包时出了问题

java  -jar myrpc-service-0.0. 

以下是错误输出


ooo. .oo.  .oo.   oooo    ooo oooo d8b .   .ooooo.
`888P"Y88bP"Y88b   `88.  .8'  `888""8P  888' `88b d88' `"Y8
 888   888   888    `88..8'    888      888   888 888
 888   888   888     `888'     888      888   888 888   .o8
o888o o888o o888o     .8'     d888b     888bod8P' `Y8bod8P'
                  .o..P'                888
                  `Y8P'                o888o

[main][INFO ] (:147) Error when creating PropertyDescriptor for public final void .(,)! Ignoring this property.
 Exception in thread "main" 
        at .invoke0(Native Method)
        at (:62)
        at (:43)
        at (:498)
        at (:48)
        at (:87)
        at (:50)
        at (:51)
Caused by: 
        at (:101)
        at .<clinit>(:61)
        at (:27)
        at (:80)
        at (:41)
        ... 8 more
Caused by: : name
        at $(:658)
        at (:188)
        at $(:569)
        at $(:567)
        at (Native Method)
        at (:566)
        at (:58)
        at (:1096)
        at .(:526)
        at .(:47)
        at .(:104)
        at .(:326)
        at .(:299)
        at .(:676)
        at .(:311)
        at .(:291)
        at .(:60)
        at .(:421)
        at .(:285)
        at .$(:1555)
        at .$(:1429)
        at .(:801)
        at .(:239)
        at .(:421)
        at .(:285)
        at .(:558)
        at (:94)
        ... 12 more

可以看出Caused by: : name是从.configuration2这个第三方库抛出的。

我的项目中的确使用了apache的commons-configuration2库来管理用户配置参数
以下xml是我的项目中定义的配置参数管理模型
src/main/resources/

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<override>
		<!-- 从系统 home 位置读取 -->
		<properties
			fileName="${sys:}/${const:.hello_world.GlobalConfig.HOME_FOLDER}/${const:.hello_world.GlobalConfig.USER_PROPERTIES}"
			config-name="userConfig"
			config-forceCreate="true"
			config-optional="true" />
		<xml fileName="" config-name="default config" />
	</override>
</configuration>

项目的配置参数由上面的xml文件定义的两个文件组成:

类型 位置 说明
User Config $HOME/.myrpc/ HOME文件夹下的配置文件,如果不存在则自动从Default Config复制数据创建一个
Default Config src/main/resources/ 项目内置的配置文件,用于保存参数的默认值

上面两个文件的优先级从上而下由高到低。如果两个文件都定义了相同的参数,则以优先级最高的为准
User Config定义为可选的(config-optional="true"),不存在也不影响
以下是根据定义的管理模型读取用户配置的readConfig方法的代码,readConfig方法返回一个CombinedConfiguration实例。

/**
 * 配置参数管理
 * @author unknow_author
 *
 */
public class GlobalConfig {
	private static final String ROOT_XML = "";
	private static final URL ROOT_URL = GlobalConfig.class.getClassLoader().getResource(ROOT_XML);
	private static CombinedConfiguration readConfig(){
		try{
			// 指定文件编码方式,否则properties文件读取中文会是乱码,要求文件编码是UTF-8
		    FileBasedConfigurationBuilder.setDefaultEncoding(PropertiesConfiguration.class, ENCODING);
		    // 使用默认表达式引擎
			DefaultExpressionEngine engine = new DefaultExpressionEngine(DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS);
			Configurations configs = new Configurations();
			CombinedConfiguration config = configs.combined(ROOT_URL);
			config.setExpressionEngine(engine);
			// 设置同步器
			config.setSynchronizer(new ReadWriteSynchronizer());
			config.setConversionHandler(ConversionHandlerWithURI.INSTANCE);
			return config;
		}catch(Exception e){
			throw new ExceptionInInitializerError(e);
		}
	}
}

如果User Config($HOME/.myrpc/)不存在,上面的逻辑,在开发环境(IDE)下运行没有任何问题。
但运行sping-boot插件打成的 Fat-Jar,就会上面的异常。
通过反复测试比较,找到了原因,问题出在spring的,从上面的错误堆栈中能找到LaunchedURLClassLoader被调用的位置。
在上面的堆栈中同样找到apache commons-configuration2调用这个class loader的位置

at .(:526)

下面是locateFromClasspath方法的实现代码

    /**
     * Tries to find a resource with the given name in the classpath.
     *
     * @param resourceName the name of the resource
     * @return the URL to the found resource or <b>null</b> if the resource
     *         cannot be found
     */
    static URL locateFromClasspath(String resourceName)
    {
        URL url = null;
        // attempt to load from the context classpath
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        if (loader != null)
        {
            url = loader.getResource(resourceName);

            if (url != null)
            {
                LOG.debug("Loading configuration from the context classpath (" + resourceName + ")");
            }
        }

        // attempt to load from the system classpath
        if (url == null)
        {
            url = ClassLoader.getSystemResource(resourceName);

            if (url != null)
            {
                LOG.debug("Loading configuration from the system classpath (" + resourceName + ")");
            }
        }
        return url;
    }

locateFromClasspath方法一开始就通过().getContextClassLoader()获取了ClassLoader实例,然后通过调用(String name)方法获取指定的资源的URL。

是个抽象类,根据Java源码中对getResource(String name)方法的说明,当找不到指定的资源时,返回null.getResource(String name)方法会调用findResource(String name)方法,findResource(String name)官方说明也是一样,找不到资源返回null,不应该抛出异常。

    /**
     * Finds the resource with the given name.  A resource is some data
     * (images, audio, text, etc) that can be accessed by class code in a way
     * that is independent of the location of the code.
     *
     * <p> The name of a resource is a '<tt>/</tt>'-separated path name that
     * identifies the resource.
     *
     * <p> This method will first search the parent class loader for the
     * resource; if the parent is <tt>null</tt> the path of the class loader
     * built-in to the virtual machine is searched.  That failing, this method
     * will invoke {@link #findResource(String)} to find the resource.  </p>
     *
     * @apiNote When overriding this method it is recommended that an
     * implementation ensures that any delegation is consistent with the {@link
     * #getResources() getResources(String)} method.
     *
     * @param  name
     *         The resource name
     *
     * @return  A <tt>URL</tt> object for reading the resource, or
     *          <tt>null</tt> if the resource could not be found or the invoker
     *          doesn't have adequate  privileges to get the resource.
     *
     * @since  1.1
     */
    public URL getResource(String name) {
        URL url;
        if (parent != null) {
            url = parent.getResource(name);
        } else {
            url = getBootstrapResource(name);
        }
        if (url == null) {
            url = findResource(name);
        }
        return url;
    }
   /**
     * Finds the resource with the given name. Class loader implementations
     * should override this method to specify where to find resources.
     *
     * @param  name
     *         The resource name
     *
     * @return  A <tt>URL</tt> object for reading the resource, or
     *          <tt>null</tt> if the resource could not be found
     *
     * @since  1.2
     */
    protected URL findResource(String name) {
        return null;
    }

类重写了(String name)。而LaunchedURLClassLoader实现的findResource在参数为"/home/gyd/.hello_world/"这种明显找不到的资源名时,没有返回null而是抛出了IllegalArgumentException异常。

这就是问题的原因所在。严格来说,这算是spring-boot的bug,因为它没按照Java标准接口实现,commons-configuration2是严格按照Java标准来实现的。但是但凡在调用getResource的时候增加捕获异常的逻辑,也会避免这个问题。

遗憾的是查看了spring-boot和commons-configuration2目前的最新版本都没有改进此问题
所以要避免此问题就是在服务启动前如果发现不存在就创建一个空文件,以避免这个问题。

public class GlobalConfig {
	/** 必须为public static final,{@code #ROOT_XML}会引用  */
	public static final String HOME_FOLDER = ".myrpc";
	/** 必须为public static final,{@code #ROOT_XML}会引用  */
	public static final String USER_PROPERTIES= "";
	private static final String ENCODING = "UTF-8";
	private static final String ROOT_XML = "";
	private static final URL ROOT_URL = GlobalConfig.class.getClassLoader().getResource(ROOT_XML);
	private static final String ATTR_DESCRIPTION ="description"; 
	/** 用户自定义文件位置 ${}/{@value #HOME_FOLDER}/{@value #USER_PROPERTIES} */
	private static final File USER_CONFIG_FILE = Paths.get(System.getProperty(""),HOME_FOLDER,USER_PROPERTIES).toFile();
	/** 用户自定义文件是否存在标志  */
	private static volatile boolean userPropertiesExists = USER_CONFIG_FILE.isFile();
	/** 全局配置参数对象(immutable,修改无效) */
	private static final CombinedConfiguration CONFIG =readConfig();
	/** 用户定义配置对象(mutable),所有对参数的修改都基于此对象 */
	private static final PropertiesConfiguration USER_CONFIG = createUserConfig();
	private GlobalConfig() {
	}
	/**
	 * 如果$HOME/${HOME_FOLDER}/$USER_PROPERTIES不存在,则创建空文件和对应的文件夹
	 * @throws IOException 创建文件失败
	 */
	private static void createEmptyUserPropertiesIfAbsent() throws IOException {
		// double check
		if(!userPropertiesExists){
			synchronized (USER_CONFIG_FILE) {
				if(!userPropertiesExists){	
					File parent = USER_CONFIG_FILE.getParentFile();
					if(!parent.exists()){
						parent.mkdirs();
					}
					USER_CONFIG_FILE.createNewFile();
					userPropertiesExists = true;
				}
			}
		}
	}
	private static CombinedConfiguration readConfig(){
		try{
			/** 确保在读取配置文件时用户配置文件存在,否则spring-boot打包的情况下会抛出异常 */
			createEmptyUserPropertiesIfAbsent();
			// 指定文件编码方式,否则properties文件读取中文会是乱码,要求文件编码是UTF-8
		    FileBasedConfigurationBuilder.setDefaultEncoding(PropertiesConfiguration.class, ENCODING);
		    // 使用默认表达式引擎
			DefaultExpressionEngine engine = new DefaultExpressionEngine(DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS);
			Configurations configs = new Configurations();
			CombinedConfiguration config = configs.combined(ROOT_URL);
			config.setExpressionEngine(engine);
			// 设置同步器
			config.setSynchronizer(new ReadWriteSynchronizer());
			config.setConversionHandler(ConversionHandlerWithURI.INSTANCE);
			return config;
		}catch(Exception e){
			throw new ExceptionInInitializerError(e);
		}
	}
}

完整源码参见:
码云仓库: