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

Posted 10km

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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.<clinit>(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

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<override>
		<!-- 从系统 home 位置读取 -->
		<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.propertiesHOME文件夹下的配置文件,如果不存在则自动从Default Config复制数据创建一个
Default Configsrc/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 <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方法一开始就通过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.
     *
     * <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(java.lang.String) 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;
    

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

以上是关于spring-boot:apache commons-configuration2 异常:java.lang.IllegalArgumentException: name原因分析的主要内容,如果未能解决你的问题,请参考以下文章

commons-lang3

commons dbcp.jar有啥用

commons-beanutils的jar包导入了出错。Java 一直报下图错误

commons dbcp.jar有什么用

commons字符串工具类——commons-lang3

common-ui