Spring boot: Apache commons-configuration2 exception: Java Lang.illegalargumentexception: name cause analysis

Posted by Desbrina on Wed, 15 Dec 2021 03:12:05 +0100

Recently, I was designing a spring boot service. There was no problem when the development environment (IDE) was running, However, there is a problem when using the spring boot Maven plugin plug-in to generate the fat jar service jar package on the command line

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

The following is the error output

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

It can be seen that caused by: Java Lang.illegalargumentexception: name is from org.org apache. commons. Configuration2 is thrown by this third-party library.

My project does use apache's commons-configuration2 library to manage user configuration parameters The following xml is the configuration parameter management model defined in my project src/main/resources/root.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<override>
		<!-- Slave system home Position reading -->
		<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>

The configuration parameters of the project are composed of two files defined in the above xml file:

type

position

explain

User Config

$HOME/.myrpc/config.properties

The configuration file in the HOME folder. If it does not exist, it will automatically copy the data from Default Config to create one

Default Config

src/main/resources/defaultConfig.xml

The built-in configuration file of the project is used to save the default values of parameters

The priority of the above two files is from top to bottom, from high to low. If both files define the same parameters, the one with the highest priority shall prevail User Config is defined as optional (config optional = "true"), and does not exist or affect The following is based on root The management model defined in XML reads the code of the readConfig method configured by the user, and the readConfig method returns a CombinedConfiguration instance.

/**
 * Configuration parameter management
 * @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{
			// Specify the file encoding method, otherwise the Chinese of the properties file will be garbled, and the file encoding is required to be UTF-8
		    FileBasedConfigurationBuilder.setDefaultEncoding(PropertiesConfiguration.class, ENCODING);
		    // Use default expression engine
			DefaultExpressionEngine engine = new DefaultExpressionEngine(DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS);
			Configurations configs = new Configurations();
			CombinedConfiguration config = configs.combined(ROOT_URL);
			config.setExpressionEngine(engine);
			// Set synchronizer
			config.setSynchronizer(new ReadWriteSynchronizer());
			config.setConversionHandler(ConversionHandlerWithURI.INSTANCE);
			return config;
		}catch(Exception e){
			throw new ExceptionInInitializerError(e);
		}
	}
}

If User Config($HOME/.myrpc/config.properties) does not exist, there is no problem with the above logic running in the development environment (IDE). However, if you run the fat jar typed by the spin boot plug-in, the above exception will occur. Through repeated testing and comparison, the reason is found. The problem lies in spring's org springframework. boot. loader. LaunchedURLClassLoader. You can find the location where LaunchedURLClassLoader is called from the above error stack. In the above stack, you can also find the location where apache commons-configuration2 calls this class loader

at org.apache.commons.configuration2.io.FileLocatorUtils.locateFromClasspath(FileLocatorUtils.java:526)

The following is the implementation code of the locateFromClasspath method

    /**
     * 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;
    }

The locateFromClasspath method starts with thread currentThread(). Getcontextclassloader () gets the ClassLoader instance, and then calls ClassLoader The getresource (string name) method gets the URL of the specified resource.

java.lang.ClassLoader is an abstract class. According to the description of getResource(String name) method in Java source code, null is returned when the specified resource cannot be found The getResource(String name) method will call the findResource(String name) method. The same is true for the official description of findResource(String name). If a resource is not found, null will be returned and no exception should be thrown.

    /**
     * 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. The LaunchedURLClassLoader class overrides classloader findResource(String name). The findresource implemented by LaunchedURLClassLoader does not return null but throws an IllegalArgumentException when the parameter is "/ home/gyd/.hello_world/config.properties".

This is the reason for the problem. Strictly speaking, this is a spring boot bug because it is not implemented according to the Java standard interface, and Commons configuration2 is implemented in strict accordance with the Java standard. However, this problem can also be avoided by adding the logic to catch exceptions when calling getResource.

Unfortunately, the latest versions of spring boot and Commons configuration 2 have not improved this problem Therefore, to avoid this problem, if config. Config is found before the service is started If properties does not exist, create an empty file to avoid this problem.

public class GlobalConfig {
	/** Must be public static final,{@code #ROOT_XML} will reference  */
	public static final String HOME_FOLDER = ".myrpc";
	/** Must be public static final,{@code #ROOT_XML} will reference  */
	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 defined file location ${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();
	/** Whether the user-defined file has a flag  */
	private static volatile boolean userPropertiesExists = USER_CONFIG_FILE.isFile();
	/** Global configuration parameter object (immutable, modification invalid) */
	private static final CombinedConfiguration CONFIG =readConfig();
	/** User defined configuration object (mutable), based on which all parameter modifications are made */
	private static final PropertiesConfiguration USER_CONFIG = createUserConfig();
	private GlobalConfig() {
	}
	/**
	 * If $home / ${home_folder} / $user_ If properties does not exist, create an empty file and the corresponding folder
	 * @throws IOException Failed to create file
	 */
	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{
			/** Ensure that the user configuration file exists when reading the configuration file, otherwise an exception will be thrown in the case of spring boot packaging */
			createEmptyUserPropertiesIfAbsent();
			// Specify the file encoding method, otherwise the Chinese of the properties file will be garbled, and the file encoding is required to be UTF-8
		    FileBasedConfigurationBuilder.setDefaultEncoding(PropertiesConfiguration.class, ENCODING);
		    // Use default expression engine
			DefaultExpressionEngine engine = new DefaultExpressionEngine(DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS);
			Configurations configs = new Configurations();
			CombinedConfiguration config = configs.combined(ROOT_URL);
			config.setExpressionEngine(engine);
			// Set synchronizer
			config.setSynchronizer(new ReadWriteSynchronizer());
			config.setConversionHandler(ConversionHandlerWithURI.INSTANCE);
			return config;
		}catch(Exception e){
			throw new ExceptionInInitializerError(e);
		}
	}
}

For complete source code, see: Code cloud warehouse: GlobalConfig.java