Java中比较重要的一个特性就是反射,可能对于初级编程几乎没有什么接触,但是对于后期的高阶编程中是一个不可或缺的知识点。这里面涉及到的知识点就有jvm的class加载机制。这一机制在这篇文章:
深入分析Java ClassLoader原理中,有很好的讲解。

1,什么是反射?

按照oracle官网的介绍,反射的概念如下:

Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions. The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.

也就是说反射是一种动态的方法使得在任意位置能够动态获取类信息,从而通过类信息获取类的成员变量和调用成员方法。

2,获取类对象的几种方法

(1)使用Class类的forName静态方法
举例说明:
一般JDBC中获取一个Connection的时候,都会事先调用一下如下一个方法:
Class.forName(“com.mysql.jdbc.Driver”);//mysql
Class.forName(“org.postgresql.Driver”);//postgresql
Class.forName(“oracle.jdbc.driver.OracleDriver”);//Oracle
具体含义在下面介绍,现在先给读者一个概念。
(2)通过xxx.class来获取
例如:
Class classname = this .getClass();
//通过子类获取父类对象
ClassName cn = new ClassName();
UserClass = cn.getClass();
Class SubUserClass = UserClass.getSuperclass();
上面的xxx.class中的xxx可以为this,也可以为binary name
(3)通过调用某个对象的getClass()方法获取
StringBuilder str = new StringBuilder(“123”);
Class< ?> class = str.getClass();

3,类的加载机制

类加载分为四层,按照从上层到下层的顺序,总共有四层classloader,分别为
BootStrap ClassLoader:称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等。

Extension ClassLoader:称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。

App ClassLoader:称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。

Custom ClassLoader:即自定义加载器,这个需要继承自ClassLoader类。并覆盖相关的方法(主要是findclass()方法)。

我们称之为四层类加载模型,同时采用双亲委托机制来避免重复加载的问题。

具体原理和验证可以参考本文开头的那篇文章。而本文的主要重点在于介绍Class.forName()这种方法。以及回答笔者一直以来对于为什么每个数据库驱动开头都会首先调用Class.forName()方法的困惑。

4,Class.forName()方法实现类加载

下面以数据库驱动为例(oracle driver)讲解Class.forName()
在连接数据库的时候,每一个不同的数据库都必须首先向DiverManager注册。然后才能根据DiverManager.getConnection(String url,String user, String password)方法来获取连接。
对于下面这行代码:

Class.forName("oracle.jdbc.driver.OracleDriver");

这个函数中的参数oracle.jdbc.driver.OracleDriver,这个是一个class的binary name。也就是包名+class名。这行代码会在运行到这行代码的时候加载这个类,同时因为这个类里面有一个static代码块,因此会执行static里面的代码。至于为什么在加载类信息的时候会运行static代码块,这个需要参考一下类的加载过程
和初始化过程。
下面是oralce.jdbc.driver.OracleDriver这个类里面的static代码块的关键部分的代码。

static {
        try {
            if (defaultDriver == null) {
                defaultDriver = new oracle.jdbc.OracleDriver();
                DriverManager.registerDriver(defaultDriver);
            }

            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    OracleDriver.registerMBeans();
                    return null;
                }
            });
            Timestamp localTimestamp = Timestamp.valueOf("2000-01-01 00:00:00.0");
        } catch (SQLException localSQLException) {
            Logger.getLogger("oracle.jdbc.driver").log(Level.SEVERE, "SQLException in static block.",
                    localSQLException);
        } catch (RuntimeException localRuntimeException) {
            Logger.getLogger("oracle.jdbc.driver").log(Level.SEVERE, "RuntimeException in static block.",
                    localRuntimeException);
        }

        try {
            ClassRef localClassRef = ClassRef.newInstance("oracle.security.pki.OraclePKIProvider");
            Object localObject = localClassRef.get().newInstance();
        } catch (Throwable localThrowable) {
        }

        systemTypeMap = new Hashtable(3);
        try {
            systemTypeMap.put("SYS.XMLTYPE", ClassRef.newInstance("oracle.xdb.XMLTypeFactory"));
        } catch (ClassNotFoundException localClassNotFoundException1) {
        }

        try {
            systemTypeMap.put("SYS.ANYDATA", ClassRef.newInstance("oracle.sql.AnyDataFactory"));
            systemTypeMap.put("SYS.ANYTYPE", ClassRef.newInstance("oracle.sql.TypeDescriptorFactory"));
        } catch (ClassNotFoundException localClassNotFoundException2) {
        }

        _Copyright_2007_Oracle_All_Rights_Reserved_ = null;
    }

上面的defaultDriver定义如下:

private static OracleDriver defaultDriver = null;

从上面的代码来看,上面的static方法内有几句代码:

if (defaultDriver == null) {
    defaultDriver = new oracle.jdbc.OracleDriver();
    DriverManager.registerDriver(defaultDriver);
}

这几句就是用来向DiverManager注册驱动的。
因为Class.forName()方法会调用static方法,然后就会注册驱动。
那么有一个问题来了,如果多次调用Class.forName()方法会不会重复调用static方法呢?因为其实我们只需要一次注册驱动就可以了。
其实问题也就简化了,也就是说,加载过的class会不会重复加载。

为了验证这个问题,笔者做了一个实验。

5,Class.forName()多次调用的时候会不会重复加载?

5.1 创建一个简单的工程,创建一个package,名为reflect,下面有一个类,名为:
ClassForNameTest
ClassForNameTest.java:

package reflect;

public class ClassForNameTest {
    private static String a;
    static{
        if(a==null){
            a="";
            System.out.println("first load");
        }else{
            System.out.println("has loaded...");
        }
    }

}

然后将这个代码打包成jar包,引入到工程中,然后在另一个类中,编写测试代码如下:

public static void main(String[] args) throws ClassNotFoundException {
        for(int i=0;i<10;i++){
            Class.forName("reflect.ClassForNameTest");
        }
    }

运行结果,如下:

first load
根据结果,我们可以知道,static代码块只会执行一次,也就是说,类加载只会加载一次。

那么我们再看另一个问题:

我们是不是可以像平常的方法一样,通过import数据库的驱动类,然后再进行new创建。
答案是:可以的,但是,有一个问题,所以不推荐。

下面是测试代码:

package db;

import java.net.MalformedURLException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import oracle.jdbc.driver.OracleDriver;

public class DBConnectionCreater {

    public static void main(String[] args) throws Exception{
        //method1
//      oracle.jdbc.driver.OracleDriver driver=new OracleDriver();
//      DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl","user","password");
        //method2
        for(int i=0;i<10;i++){
            Class.forName("oracle.jdbc.driver.OracleDriver");
            System.out.println("test");
        }
        DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl","user","password");
        System.out.println("connect");
    }

}

上面的ConnectionCreaterForTG()方法调用了另一个函数PropertiesUtil.getProperties(),这个是笔者读取properties文件的键值的方法,文中没有给出。

上面的两种方法都是可以的,运行成功。

但是第一种方法有一个问题就在于:

每次创建对象的时候,都会调用static代码块,所以对于数据库驱动这个例子而言,就会重复注册驱动。但是这个是不必要的。所以还是用方法Class.forName()比较好!

参考:

1,深入分析Java ClassLoader原理
2,数据库链接与 Class.forName()用法详解
3,深入解析Java反射(1) - 基础


上面是笔者自己经过测试之后的理解,如果有更深入理解的读者,可以在评论下面交流!谢谢!