工具类 God从开始的一个类,变成了使用策略模式的几个类;从项目中的一部分,变成了一个单独jar;
打包的yqj2065.jar在NetBeans环境中运行良好,有一天我在BlueJ中使用该yqj2065.jar,总是抛出NullPointerException。
我知道是路径问题,但是为什么NetBeans环境可以但是BlueJ中不行?虽然我一般用NetBeans,这个问题总在心里放着,真TM不舒服,而且一下两下还搞不定它。
近期有时间研究一下这个问题,发现是BlueJ的ClassLoader问题。ClassLoader问题其实在《编程导论(Java)·7.1类载入》中提到过,但是我已经忘记了。,或者说,当时我就没有太在意这个问题。p220写到:
注意:在BlueJ和控制台中运行,结果不同。在BlueJ中输出:java.net.URLClassLoader,而控制台中为sun.misc.Launcher$AppClassLoader。开发环境通常提供自己的装载器(注:选择某种装载器使用策略),而在控制台中myLoader 与ClassLoader loader = ClassLoader.getSystemClassLoader()的loader指向同一个对象。(这点差异不影响后面的介绍)
下面介绍一下我的探索过程,虽然走了一些弯路,但是走些弯路,可以看到一些也有意义的东西。
1.选择ClassLoader
第一阶段,在NetBeans和BlueJ项目中测试和选择ClassLoader。目标是访问 默认文件"my.properties"。try (InputStream is = this.getClass().getClassLoader().getResourceAsStream(path))
是关键所在,因此将该语句分开
public void testPropertiesLocator() {
ClassLoader[] cls = new ClassLoader[]{
ClassLoader.getSystemClassLoader(),
this.getClass().getClassLoader(),
Main.class.getClassLoader(),
Thread.currentThread().getContextClassLoader()};
for(ClassLoader x : cls ){
URL resource = x.getResource("my.properties");
pln(resource);
}
String cwd = System.getProperty("user.dir");
pln(cwd);
}
NetBeans输出:
file:/D:/yqj2065/yqj2065/build/classes/my.properties
file:/D:/yqj2065/yqj2065/build/classes/my.properties
file:/D:/yqj2065/yqj2065/build/classes/my.properties
D:\yqj2065\yqj2065
BlueJ项目输出:
null
file:/D:/path/my.properties
file:/D:/path/my.properties
file:/D:/path/my.properties
D:\path
基于classpath的相对路径,在BlueJ中需要排除ClassLoader.getSystemClassLoader()。
剩下的2个(2和3其实是一个东西),在BlueJ项目中都可以工作,
public static void testGetValue() {
String str = new PropertiesLocator6().getValue("2065") ;
pln(str);
}
但是打包后,仅仅测试getValue("2065"),发现this.getClass().getClassLoader()不行了,它不能够正确访问测试项目的my.properties(它能够访问jar中的资源),
在测试项目中调用jar中testPropertiesLocator()和直接执行testPropertiesLocator()代码,可以明显发现,
public class Test{
public static void testGetValue() {
String str = new PropertiesLocator6().getValue("2065") ;
pln(str);
new Main().testPropertiesLocator();
pln("-------------------------------" );
ClassLoader[] cls = new ClassLoader[]{
Test.class.getClassLoader(),
Thread.currentThread().getContextClassLoader()};
for(ClassLoader x : cls ){
URL resource = x.getResource("my.properties");
pln(resource);
}
}
}
输出:
a.DDD
null
null
null
file:/D:/test/my.properties
-------------------------------
file:/D:/test/my.properties
file:/D:/test/my.properties
当我以为问题搞定了,测试create,又出现NullPointerException。既然已经将类全名找到——a.DDD,用它创建对象还有问题?
这时,我才想起来翻书,我意识到是ClassLoader问题。
2.WhoLoadMe
类装载器系统采用托付模型,每个类装载器都有一个父加载器(与类层次无关)。细节请看《编程导论(Java)·7.1类载入》和随书代码。
在NetBeans使用yqj2065.jar的某个项目中,随便找一个类如Client,打印类装载器
pln(Client.class.getClassLoader());
pln(ClassLoader.getSystemClassLoader());
pln(Thread.currentThread().getContextClassLoader());
输出:
sun.misc.Launcher$AppClassLoader@55f96302
sun.misc.Launcher$AppClassLoader@55f96302
sun.misc.Launcher$AppClassLoader@55f96302
在BlueJ使用yqj2065.jar的项目中,随便找一个类如Test,打印类装载器
public static void testWhoLoadMe(){
pln(Test.class.getClassLoader() );
pln( God.class.getClassLoader() );
pln( Locator.class.getClassLoader() );
pln(Thread.currentThread().getContextClassLoader());
}
输出:
java.net.URLClassLoader@50ce9f3e
sun.misc.Launcher$AppClassLoader@14dad5dc
sun.misc.Launcher$AppClassLoader@14dad5dc
java.net.URLClassLoader@50ce9f3e
虽然访问 默认文件"my.properties"时,我们指定了Thread.currentThread().getContextClassLoader()——java.net.URLClassLoader,但是反射创建对象时God的类装载器,却是AppClassLoader。真tm烦人。最简单地Class.forName(typeName).newInstance()不能够用了。
新版本如下:
package yqj2065.util;
public class God {
public static final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
private static final Locator locator =LocatorFactory.getLocator();
public interface Locator{
public String getValue(String path, String key);
default public String getValue(String key){
return getValue("my.properties", key);
}
public static String getPath() {
return System.getProperty("user.dir");
}
}
private static Object _create(String typeName){
Object obj = null;
if (typeName != null) {
try {
obj =classLoader.loadClass(typeName).newInstance();
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
}
}
return obj;
}
public static Object create(String path, String key) {
String typeName = locator.getValue(path, key);
return _create( typeName);
}
public static Object create(String key) {
String typeName = locator.getValue(key);
return _create( typeName);
}
}
而其他类没有什么变化,如
package yqj2065.util;
/**
*
* @author yqj2065
*/
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Properties;
import static yqj2065.util.Print.pln;
public class PropertiesLocator implements God.Locator{
@Override
public String getValue(String path, String key) {
Properties props = new Properties();
try (InputStream is = God.classLoader.getResourceAsStream(path)) {
try (InputStreamReader isr = new InputStreamReader(is, "UTF-8")) {
props.load(isr);//从流中加载properties文件信息
}
} catch (java.io.FileNotFoundException e) {
pln(e.getClass().getName());
} catch (java.io.IOException e) {
pln(e.getClass().getName());
}
return props.getProperty(key);
}
}
打包后可以使用了。
但是,...................
①我有意留下的小尾巴:
private static final Locator locator =LocatorFactory.getLocator();
②我实在没有兴趣搞的东西,BlueJ的静态数据加载问题。在测试项目中使用了yqj2065.jar,打开BlueJ运行ok。
public class DDD{
public void foo(){
pln("okjhkkkkkkkjhlfkkdffddkk");
}
}
public static void testCreate(){
DDD dd = (DDD)God.create("2065");
dd.foo();
}
修改DDD,编译,再次运行:
java.lang.ClassCastException: a.DDD cannot be cast to a.DDD
(所以,运行前,请在BlueJ中重启JVM)