最近在设计一个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