资讯专栏INFORMATION COLUMN

Java类加载器及自定义

hiyang / 3517人阅读

摘要:自定义类加载器示例代码类加载器获取的字节流字节流解密被加载的类测试代码以上代码,展示了自定义类加载器加载类的方法。这就需要自定义类加载器,以便对加载的类库进行隔离,否则会出现问题对于非的文件,需要转为类,就需要自定义类加载器。

Java类加载器的作用是寻找类文件,然后加载Class字节码到JVM内存中,链接(验证、准备、解析)并初始化,最终形成可以被虚拟机直接使用的Java类型。

类加载器种类

有两种类加载器:
1 启动类加载器(Bootstrap ClassLoader)
由C++语言实现(针对HotSpot VM),负责将存放在lib目录或-Xbootclasspath参数指定的路径中的类库加载到JVM内存中,像java.lang.、java.util.、java.io.*等等。可以通过vm参数“-XX:+TraceClassLoading”来获取类加载信息。我们无法直接使用该类加载器。

2 其他类加载器(Java语言实现)
1)扩展类加载器(Extension ClassLoader)
负责加载libext目录或java.ext.dirs系统变量指定的路径中的所有类库。我们可以直接使用这个类加载器。
2)应用程序类加载器(Application ClassLoader),或者叫系统类加载器
负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
3)自定义类加载器
通过继承ClassLoader类实现,主要重写findClass方法。

类加载器使用顺序

在JVM虚拟机中,如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

也就是说,对于每个类加载器,只有父类(依次递归)找不到时,才自己加载 。这就是双亲委派模型

为什么需要双亲委派模型呢?这可以提高Java的安全性,以及防止程序混乱。
提高安全性方面:
假设我们使用一个第三方Jar包,该Jar包中自定义了一个String类,它的功能和系统String类的功能相同,但是加入了恶意代码。那么,JVM会加载这个自定义的String类,从而在我们所有用到String类的地方都会执行该恶意代码。
如果有双亲委派模型,自定义的String类是不会被加载的,因为最顶层的类加载器会首先加载系统的java.lang.String类,而不会加载自定义的String类,防止了恶意代码的注入。

防止程序混乱
假设用户编写了一个java.lang.String的同名类,如果每个类加载器都自己加载的话,那么会出现多个String类,导致混乱。如果本加载器加载了,父加载器则不加载,那么以哪个加载的为准又不能确定了,也增加了复杂度。

自定义类加载器

我们可以自定义类加载器,只需继承ClassLoader抽象类,并重写findClass方法(如果要打破双亲委派模型,需要重写loadClass方法)。原因可以查看ClassLoader的源码:

protected Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

这个是ClassLoader中的loadClass方法,大致流程如下:
1)检查类是否已加载,如果是则不用再重新加载了;
2)如果未加载,则通过父类加载(依次递归)或者启动类加载器(bootstrap)加载;
3)如果还未找到,则调用本加载器的findClass方法;
以上可知,类加载器先通过父类加载,父类未找到时,才有本加载器加载。

因为自定义类加载器是继承ClassLoader,而我们再看findClass方法:

protected Class findClass(String name) throws ClassNotFoundException {    
    throw new ClassNotFoundException(name);
}

可以看出,它直接返回ClassNotFoundException。
因此,自定义类加载器必须重写findClass方法。

自定义类加载器示例代码:
类加载器HClassLoader:

class HClassLoader extends ClassLoader {

private String classPath;

public HClassLoader(String classPath) {
    this.classPath = classPath;
}

@Override
protected Class findClass(String name) throws ClassNotFoundException {
    try {
        byte[] data = loadByte(name);
        return defineClass(name, data, 0, data.length);
    } catch (Exception e) {
        e.printStackTrace();
        throw new ClassNotFoundException();
    }

}

/**
 * 获取.class的字节流
 *
 * @param name
 * @return
 * @throws Exception
 */
private byte[] loadByte(String name) throws Exception {
    name = name.replaceAll(".", "/");
    FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
    int len = fis.available();
    byte[] data = new byte[len];
    fis.read(data);
    fis.close();

    // 字节流解密
    data = DESInstance.deCode("1234567890qwertyuiopasdf".getBytes(), data);

    return data;
  }
}

被加载的类Car:

public class Car {

    public Car() {
        System.out.println("Car:" + getClass().getClassLoader());
        System.out.println("Car Parent:" + getClass().getClassLoader().getParent());
    }

    public String print() {
        System.out.println("Car:print()");
        return "carPrint";
    }
 }

测试代码:

@Test
public void testClassLoader() throws Exception {
    HClassLoader myClassLoader = new HClassLoader("e:/temp/a");
    Class clazz = myClassLoader.loadClass("com.ha.Car");
    Object o = clazz.newInstance();
    Method print = clazz.getDeclaredMethod("print", null);
    print.invoke(o, null);
}

以上代码,展示了自定义类加载器加载类的方法。

需要注意的是:
执行测试代码前,必须将Car.class文件移动到e:/temp/a下,并且按包名建立层级目录(这里为com/ha/)。因为如果不移动Car.class文件,那么Car类会被AppClassLoader加载(自定义类加载器的parent是AppClassLoader)。

自定义类加载器的应用

上面介绍了Java类加载器的相关知识。对于自定义类加载器,哪里可以用到呢?
主流的Java Web服务器,比如Tomcat,都实现了自定义的类加载器。因为它要解决几个问题:
1)Tomcat上可以部署多个不同的应用,但是它们可以使用同一份类库的不同版本。这就需要自定义类加载器,以便对加载的类库进行隔离,否则会出现问题;
2)对于非.class的文件,需要转为Java类,就需要自定义类加载器。比如JSP文件。

这里举一个其它的例子:Java核心代码的加密。
假设我们项目当中,有一些核心代码不想让别人反编译看到。当前知道有两种方法,一种是通过代码混淆(推荐Allatori,商用收费);一种是自己编写加密算法,对字节码加密,加大反编译难度。

代码混淆如果用Allatori,比较简便。注意控制自己编写类的访问权限即可。接口用public,内部方法用private,其他的用默认的(即不加访问修饰符)或者protected。代码混淆这里不过多说明,这里主要介绍一下字节码加密。
大概的流程可以如下:

.class加密代码:

@Test
public void testEncode() {
    String classFile = "e:/temp/a/com/ha/Car.class";
    FileInputStream fis = null;
    try {
        fis = new FileInputStream(classFile);
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();

        data = DESInstance.enCode("1234567890qwertyuiopasdf".getBytes(), data);

        String outFile = "e:/temp/a/com/ha/EnCar.class";
        FileOutputStream fos = new FileOutputStream(outFile);
        fos.write(data);
        fos.close();
    } catch (Exception e) {
        e.printStackTrace();
    }

}

类加载器中解密,查看上文中的:

// 字节流解密
data = DESInstance.deCode("1234567890qwertyuiopasdf".getBytes(), data);

加解密工具类:

public class DESInstance {

    private static String ALGORITHM = "DESede";

    /**
     * 加密
     *
     * @param key
     * @param src
     * @return
     */
    public static byte[] enCode(byte[] key, byte[] src) {

        byte[] value = null;
        SecretKey deskey = new SecretKeySpec(key, ALGORITHM);
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, deskey);
            value = cipher.doFinal(src);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return value;
    }

    /**
     * 解密
     *
     * @param key
     * @param src
     * @return
     */
    public static byte[] deCode(byte[] key, byte[] src) {
        byte[] value = null;
        SecretKey deskey = new SecretKeySpec(key, ALGORITHM);

        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, deskey);
            value = cipher.doFinal(src);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return value;
    }
}

注意秘钥是24位,否则会报错:
java.security.InvalidKeyException: Invalid key length
如果解密密码错误,则是如下错误:
javax.crypto.BadPaddingException: Given final block not properly padded

当然,这样做还是会被反编译破解,要加大难度,还需要其他处理的。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68320.html

相关文章

  • Hibernate配置及自定义注册映射文件

    摘要:一配置属性详解可以在各式各样不同环境下工作而设计的因此存在着大量的配置参数。以简便操作,多数配置参数都有默认的配置值也是我们日常使用的必须品。 Hibernate (开放源代码的对象关系映射框架) Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装, 它将POJO与数据库表建立映射关系,是一个全自动的orm框架,hibernat...

    coordinate35 评论0 收藏0
  • JVM 的 工作原理,层次结构 以及 GC工作原理

    摘要:执行引擎作用执行字节码,或者执行本地方法运行时数据区其实就是指在运行期间,其对内存空间的划分和分配。 虽是读书笔记,但是如转载请注明出处https://uestc-dpz.github.io..拒绝伸手复制党 JVM Java 虚拟机 Java 虚拟机(Java virtual machine,JVM)是运行 Java 程序必不可少的机制。JVM实现了Java语言最重要的特征:即平台...

    qqlcbb 评论0 收藏0
  • HystrixFeign的详细构建过程及自定义扩展功能

    摘要:重要以及内部类都是访问级别,可以注入自定义的。的目的是将包装成风格以便开发。示例以下示例参考其中的和是自定义的。需要自定义,则实现类,需要自定义,则实现即可总结由于构建过程所用到的是访问级别的,不能使用自定义的以及是,给了我们扩展的空间。 spring-cloud-openfeign-core-2.1.1.RELEASE.jar 中 HystrixFeign 的详细构建过程: @Ena...

    曹金海 评论0 收藏0
  • android知识大总结 - 收藏集 - 掘金

    摘要:中简单搞定接口访问,以及简析掘金最近总结的一些经验,对于或中使用接口的一些心得。这里,本文将数据结构之学习总结掘金前言前面介绍了的数据结构,今天抽空学习总结一下另一种数据结构。浅析事件传递掘金中的事件传递主要涉及三个方法和。 Android 系统中,那些能大幅提高工作效率的 API 汇总(持续更新中...) - 掘金前言 条条大路通罗马。工作中,实现某个需求的方式往往不是唯一的,这些不...

    luodongseu 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<