跳至主要內容

Java基础-拓展1

wangdx大约 7 分钟

自定义类加载器

为什么需要自定义类加载器

网上的大部分自定义类加载器文章,几乎都是贴一段实现代码,然后分析一两句自定义 ClassLoader 的 原理。但是我觉得首先得把为什么需要自定义加载器这个问题搞清楚,因为如果不明白它的作用的情况 下,还要去学习它显然是很让人困惑的。

首先介绍自定义类的应用场景:

  • (1)加密:Java 代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将 编译后的代码用某种加密算法加密,类加密后就不能再用 Java 的 ClassLoader 去加载类了,这时就需要自 定义 ClassLoader 在加载类的时候先解密类,然后再加载。
  • (2)从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载 器,从指定的来源加载类。
  • (3)以上两种情况在实际中的综合运用:比如你的应用需要通过网络来传输 Java 类的字节码,为了安 全性,这些字节码经过了加密处理。这个时候你就需要自定义类加载器来从某个网络地址上读取加密后 的字节代码,接着进行解密和验证,最后定义出在 Java 虚拟机中运行的类。

1. 双亲委派模型

在实现自己的 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
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

双亲委派模型的工作过程如下:

  • (1)当前类加载器从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加 载的类。
  • (2)如果没有找到,就去委托父类加载器去加载(如代码 c = parent.loadClass(name, false)所示)。 父类加载器也会采用同样的策略,查看自己已经加载过的类中是否包含这个类,有就返回,没有就委托 父类的父类去加载,一直到启动类加载器。因为如果父加载器为空了,就代表使用启动类加载器作为父 加载器去加载。
  • (3)如果启动类加载器加载失败(例如在$JAVA_HOME/jre/lib 里未查找到该 class),则会抛出一个异 常 ClassNotFoundException,然后再调用当前加载器的 findClass()方法进行加载。

双亲委派模型的好处:

  • (1)主要是为了安全性,避免用户自己编写的类动态替换 Java 的一些核心类,比如 String。
  • (2)同时也避免了类的重复加载,因为 JVM 中区分不同类,不仅仅是根据类名,相同的 class 文件被不 同的 ClassLoader 加载就是不同的两个类。

2. 自定义类加载器

  • (1)从上面源码看出,调用 loadClass 时会先根据委派模型在父加载器中加载,如果加载失败,则会调 用当前加载器的 findClass 来完成加载。
  • (2)因此我们自定义的类加载器只需要继承 ClassLoader,并覆盖 findClass 方法,下面是一个实际例 子,在该例中我们用自定义的类加载器去加载我们事先准备好的 class 文件。
2.1 自定义一个 People.java 类做例子
public class People {
    private String name;

    public People() {
    }

    public People(String name) {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String toString() {
        return "I am a people, my name is " + name;
    }
}
2.2 自定义类加载器

自定义一个类加载器,需要继承 ClassLoader 类,并实现 findClass 方法。其中 defineClass 方法可以把二 进制流字节组成的文件转换为一个 java.lang.Class(只要二进制字节流的内容符合 Class 文件规范)。

Java 中的异常处理

在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable: 有两个重 要的子类:Exception(异常) 和 Error(错误) ,二者都是 Java 异常处理的重要子类,各自都包含大 量子类。

Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执 行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java 虚拟机运行错误 (Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如 Java 虚拟机运行错误 (Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它 们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的 应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java 中,错 误通过 Error 的子类描述。

  • Exception(异常):是程序本身可以处理的异常。Exception 类有一个重要的子类
  • RuntimeException。RuntimeException 异常由 Java 虚拟机抛出。
  • NullPointerException(要访问的变量没有引用任何对象时,抛出该异常)、
  • ArithmeticException(算术运算异常,一个整数除以 0 时,抛出该异常)和
  • ArrayIndexOutOfBoundsException (下标越界异常)。

注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。

Throwable 类常用方法

  • • public string getMessage():返回异常发生时的详细信息
  • • public string toString():返回异常发生时的简要描述
  • • public string getLocalizedMessage():返回异常对象的本地化信息。使用 Throwable 的子类覆盖 这个方法,可以声称本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage() 返回的结果相同
  • • public void printStackTrace():在控制台上打印 Throwable 对象封装的异常信息

异常处理总结

  • • try 块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • • catch 块:用于处理 try 捕获到的异常。
  • • finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。 当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。在以下 4 种特殊 情况下,finally 块不会被执行:
      1. 在 finally 语句块中发生了异常。
      1. 在前面的代码中用了 System.exit()退出程序。
      1. 程序所在的线程死亡。
      1. 关闭 CPU。

Java 序列化中如果有些字段不想进行序列化 怎么办

对于不想进行序列化的变量,使用 transient 关键字修饰。 transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。 transient 只能修饰变量,不能修饰类和方法。

获取用键盘输入常用的的两种方法

  • 方法 1:通过 Scanner Scanner input = new Scanner(System.in); String s = input.nextLine(); input.close();
  • 方法 2:通过 BufferedReader BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); String s = input.readLine();
上次编辑于: