打包后的工具类 God+BlueJ+ClassLoader

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yqj2065/article/details/74603104

工具类 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
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)






猜你喜欢

转载自blog.csdn.net/yqj2065/article/details/74603104