spring-boot:apache commons-configuration2 异常:java.lang.IllegalArgumentException: name原因分析

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

java  -jar myrpc-service-0.0.0-SNAPSHOT-standalone.jar 

以下是错误输出


ooo. .oo.  .oo.   oooo    ooo oooo d8b oo.ooooo.   .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 ] (FluentPropertyBeanIntrospector.java:147) Error when creating PropertyDescriptor for public final void org.apache.commons.configuration2.AbstractConfiguration.setProperty(java.lang.String,java.lang.Object)! Ignoring this property.
 Exception in thread "main" java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48)
        at org.springframework.boot.loader.Launcher.launch(Launcher.java:87)
        at org.springframework.boot.loader.Launcher.launch(Launcher.java:50)
        at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:51)
Caused by: java.lang.ExceptionInInitializerError
        at myorg.myrpc.GlobalConfig.readConfig(GlobalConfig.java:101)
        at myorg.myrpc.GlobalConfig.(GlobalConfig.java:61)
        at myorg.service.myrpc.MyrpcServiceConfig.loadConfig(MyrpcServiceConfig.java:27)
        at net.gdface.cli.BaseAppConfig.parseCommandLine(BaseAppConfig.java:80)
        at myorg.service.myrpc.MyrpcServiceMain.main(MyrpcServiceMain.java:41)
        ... 8 more
Caused by: java.lang.IllegalArgumentException: name
        at sun.misc.URLClassPath$Loader.findResource(URLClassPath.java:658)
        at sun.misc.URLClassPath.findResource(URLClassPath.java:188)
        at java.net.URLClassLoader$2.run(URLClassLoader.java:569)
        at java.net.URLClassLoader$2.run(URLClassLoader.java:567)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findResource(URLClassLoader.java:566)
        at org.springframework.boot.loader.LaunchedURLClassLoader.findResource(LaunchedURLClassLoader.java:58)
        at java.lang.ClassLoader.getResource(ClassLoader.java:1096)
        at org.apache.commons.configuration2.io.FileLocatorUtils.locateFromClasspath(FileLocatorUtils.java:526)
        at org.apache.commons.configuration2.io.ClasspathLocationStrategy.locate(ClasspathLocationStrategy.java:47)
        at org.apache.commons.configuration2.io.CombinedLocationStrategy.locate(CombinedLocationStrategy.java:104)
        at org.apache.commons.configuration2.io.FileLocatorUtils.locate(FileLocatorUtils.java:326)
        at org.apache.commons.configuration2.io.FileLocatorUtils.fullyInitializedLocator(FileLocatorUtils.java:299)
        at org.apache.commons.configuration2.io.FileHandler.locate(FileHandler.java:676)
        at org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder.initFileHandler(FileBasedConfigurationBuilder.java:311)
        at org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder.initResultInstance(FileBasedConfigurationBuilder.java:291)
        at org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder.initResultInstance(FileBasedConfigurationBuilder.java:60)
        at org.apache.commons.configuration2.builder.BasicConfigurationBuilder.createResult(BasicConfigurationBuilder.java:421)
        at org.apache.commons.configuration2.builder.BasicConfigurationBuilder.getConfiguration(BasicConfigurationBuilder.java:285)
        at org.apache.commons.configuration2.builder.combined.CombinedConfigurationBuilder$ConfigurationSourceData.addChildConfiguration(CombinedConfigurationBuilder.java:1555)
        at org.apache.commons.configuration2.builder.combined.CombinedConfigurationBuilder$ConfigurationSourceData.createAndAddConfigurations(CombinedConfigurationBuilder.java:1429)
        at org.apache.commons.configuration2.builder.combined.CombinedConfigurationBuilder.initResultInstance(CombinedConfigurationBuilder.java:801)
        at org.apache.commons.configuration2.builder.combined.CombinedConfigurationBuilder.initResultInstance(CombinedConfigurationBuilder.java:239)
        at org.apache.commons.configuration2.builder.BasicConfigurationBuilder.createResult(BasicConfigurationBuilder.java:421)
        at org.apache.commons.configuration2.builder.BasicConfigurationBuilder.getConfiguration(BasicConfigurationBuilder.java:285)
        at org.apache.commons.configuration2.builder.fluent.Configurations.combined(Configurations.java:558)
        at myorg.myrpc.GlobalConfig.readConfig(GlobalConfig.java:94)
        ... 12 more

可以看出Caused by: java.lang.IllegalArgumentException: name是从org.apache.commons.configuration2这个第三方库抛出的。

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


<configuration>
	<override>
		
		<properties
			fileName="${sys:user.home}/${const:com.mycompany.hello_world.GlobalConfig.HOME_FOLDER}/${const:com.mycompany.hello_world.GlobalConfig.USER_PROPERTIES}"
			config-name="userConfig"
			config-forceCreate="true"
			config-optional="true" />
		<xml fileName="defaultConfig.xml" config-name="default config" />
	override>
configuration>

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

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

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

/**
 * 配置参数管理
 * @author unknow_author
 *
 */
public class GlobalConfig {
	private static final String ROOT_XML = "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/config.properties)不存在,上面的逻辑,在开发环境(IDE)下运行没有任何问题。
但运行sping-boot插件打成的 Fat-Jar,就会上面的异常。
通过反复测试比较,找到了原因,问题出在spring的org.springframework.boot.loader.LaunchedURLClassLoader,从上面的错误堆栈中能找到LaunchedURLClassLoader被调用的位置。
在上面的堆栈中同样找到apache commons-configuration2调用这个class loader的位置

at org.apache.commons.configuration2.io.FileLocatorUtils.locateFromClasspath(FileLocatorUtils.java: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 null 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方法一开始就通过Thread.currentThread().getContextClassLoader()获取了ClassLoader实例,然后通过调用ClassLoader.getResource(String name)方法获取指定的资源的URL。

java.lang.ClassLoader是个抽象类,根据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.
     *
     * 

The name of a resource is a '/'-separated path name that * identifies the resource. * *

This method will first search the parent class loader for the * resource; if the parent is null 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.

* * @apiNote When overriding this method it is recommended that an * implementation ensures that any delegation is consistent with the {@link * #getResources(java.lang.String) getResources(String)} method. * * @param name * The resource name * * @return A URL object for reading the resource, or * null 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 URL object for reading the resource, or * null if the resource could not be found * * @since 1.2 */ protected URL findResource(String name) { return null; }

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

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

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

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= "config.properties";
	private static final String ENCODING = "UTF-8";
	private static final String ROOT_XML = "root.xml";
	private static final URL ROOT_URL = GlobalConfig.class.getClassLoader().getResource(ROOT_XML);
	private static final String ATTR_DESCRIPTION ="description"; 
	/** 用户自定义文件位置 ${user.home}/{@value #HOME_FOLDER}/{@value #USER_PROPERTIES} */
	private static final File USER_CONFIG_FILE = Paths.get(System.getProperty("user.home"),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);
		}
	}
}

完整源码参见:
码云仓库:GlobalConfig.java

你可能感兴趣的:(java,java,configuration2,spring-boot,getResource,ClassLoader)