前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >spring-boot:apache commons-configuration2 异常:java.lang.IllegalArgumentException: name原因分析

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

作者头像
10km
发布2021-12-15 09:25:06
1.3K0
发布2021-12-15 09:25:06
举报
文章被收录于专栏:10km的专栏10km的专栏

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

代码语言:javascript
复制
java  -jar myrpc-service-0.0.0-SNAPSHOT-standalone.jar 

以下是错误输出

代码语言:javascript
复制
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

代码语言:javascript
复制
<?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.properties

HOME文件夹下的配置文件,如果不存在则自动从Default Config复制数据创建一个

Default Config

src/main/resources/defaultConfig.xml

项目内置的配置文件,用于保存参数的默认值

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

代码语言:javascript
复制
/**
 * 配置参数管理
 * @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的位置

代码语言:javascript
复制
at org.apache.commons.configuration2.io.FileLocatorUtils.locateFromClasspath(FileLocatorUtils.java:526)

下面是locateFromClasspath方法的实现代码

代码语言:javascript
复制
    /**
     * 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,不应该抛出异常。

代码语言:javascript
复制
    /**
     * 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不存在就创建一个空文件,以避免这个问题。

代码语言:javascript
复制
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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-12-13 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档