java反射机制实例学习与解析

 

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;简单说就是:只要给定类的名字,那么就可以通过反射机制来获得类的所有信息。

这种动态获取以及动态调用对象方法的功能称为java语言的反射机制。

一、例子解读JAVA反射机制

有一个用户自定义的类Car.Class

public class Car {
    private String brand;
    private String color;
    private int maxSpeed;
    private String owner;//私有成员,没有对应的get和set方法;

    //①默认构造函数
    public Car(){}

    //②带参构造函数
    public Car(String brand,String color,int maxSpeed){
        this.brand = brand;
        this.color = color;
        this.maxSpeed = maxSpeed;
    }

    //③未带参的方法
    public void introduce() {
        System.out.println("brand:"+brand+"; color:"+color+"; maxSpeed:" +maxSpeed);
    }

    //私有方法
    private void whoHasIt(){
        System.out.println("The owner of this car is:" + this.owner );
    }

    //参数的getter/Setter方法
    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public int getMaxSpeed() {
        return maxSpeed;
    }

    public void setMaxSpeed(int maxSpeed) {
        this.maxSpeed = maxSpeed;
    }
}

反射机制怎么做到:动态获取以及动态调用对象方法

public class ReflectCall {
    public static Car initByDefaultConst() throws Throwable
    {
         /**
          * ①通过类装载器获取Car类对象
          * 1.获取当前线程的ClassLoader
          * 2.通过指定的全限定类“reflect.Car”装载Car类对应的反射实例
          */
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        Class class_car = loader.loadClass("reflect.Car");

         /**
          * ②获取类的默认构造器对象并通过它实例化Car
          * 1.通过Car的反射类对象获取Car的构造函数对象cons
          * 2.通过构造函数对象的newInstrance()方法实例化Car对象,其效果等同于new Car()
          */
        Constructor cons = class_car.getDeclaredConstructor((Class[])null);
        Car car = (Car)cons.newInstance();

        /**
         * ③获取类的成员变量(包含私有成员),并对成员变量赋值
         */
        Field ownerFld = class_car.getDeclaredField("owner");
        //取消Java语言访问检查以访问private变量
        ownerFld.setAccessible(true);
        ownerFld.set(car,"小明");


        /**
         * ④通过反射方法设置属性
         * 通过Car的反射类对象的getMethod(String methodName,Class paramClass)获取属性的Setter方法对象,
         * 第一个参数是目标Class的方法名;第二个参数是方法入参的对象类型。
         */
        Method setBrand = class_car.getMethod("setBrand",String.class);
        /**
         * 通过invoke(Object obj,Object param)方法调用目标类的方法,
         * 该方法的第一个参数是操作的目标类对象实例;第二个参数是目标方法的入参。
         */
        setBrand.invoke(car,"红旗CA72");

        Method setColor = class_car.getMethod("setColor", String.class);
        setColor.invoke(car, "黑色");
        Method setMaxSpeed = class_car.getMethod("setMaxSpeed",int.class);
        setMaxSpeed.invoke(car, 200);

        /**
         * ⑤获取类的私有方法,并调用它
         */
        Method whoHasItMtd = class_car.getDeclaredMethod("whoHasIt",(Class[])null);
        //取消Java语言访问检查以访问private方法
        whoHasItMtd.setAccessible(true);
        whoHasItMtd.invoke(car,(Object[])null);

        return car;
    }

    public static void main(String[] args) throws Throwable {
        Car car = initByDefaultConst();
        car.introduce();
    }
}
运行结果:
The owner of this car is:小明
brand:红旗CA72; color:黑色; maxSpeed:200

在 ReflectCall 中,使用了几个重要的反射类,分别是ClassLoader、Class、Constructor、Method和Field,通过这些反射类就可以间接调用目标Class的各项功能了。

Class反射对象:描述类语义结构,可以从Class对象中获取构造函数、成员变量、方法类等类元素的反射对象,并以编程的方式通过这些反射对象对目标类对象进行操作。

这些反射对象类在java.reflect包中定义,下面是最主要的三个反射类: 

Constructor

Constructor:类的构造函数反射类,通过Class#getConstructors()方法可以获得类的所有构造函数反射对象数组。在JDK5.0中,还可以通过getConstructor(Class... parameterTypes)获取拥有特定入参的构造函数反射对象。Constructor的一个主要方法是newInstance(Object[] initargs),通过该方法可以创建一个对象类的实例,相当于new关键字。在JDK5.0中该方法演化为更为灵活的形式:newInstance (Object... initargs)。

Method

Method:类方法的反射类,通过Class#getDeclaredMethods()方法可以获取类的所有方法反射类对象数组Method[]。在JDK5.0中可以通过getDeclaredMethod(String name, Class... parameterTypes)获取特定签名的方法,name为方法名;Class...为方法入参类型列表。Method最主要的方法是invoke(Object obj, Object[] args),obj表示操作的目标对象;在JDK 5.0中,该方法的形式调整为invoke(Object obj, Object... args)。此外,Method还有很多用于获取类方法更多信息的方法:
(1)Class getReturnType():获取方法的返回值类型; 
(2)Class[] getParameterTypes():获取方法的入参类型数组; 
(3)Class[] getExceptionTypes():获取方法的异常类型数组; 
(4)Annotation[][] getParameterAnnotations():获取方法的注解信息,JDK 5.0中的新方法;

Field

Field:类的成员变量的反射类,通过Class#getDeclaredFields()方法可以获取类的成员变量反射对象数组,通过Class#getDeclaredField(String name)则可获取某个特定名称的成员变量反射对象。Field类最主要的方法是set(Object obj, Object value),obj表示操作的目标对象,通过value为目标对象的成员变量设置值。如果成员变量为基础类型,用户可以使用Field类中提供的带类型名的值设置方法,如setBoolean(Object obj, boolean value)、setInt(Object obj, int value)等。

二、ClassLoader相关介绍

1. classloader 

classloader用来加载Class文件到JVM,以供程序使用的。java程序可以动态加载类定义,而这个动态加载的机制就是通过ClassLoader来实现的。

(1)bootstrap classloader

既然ClassLoader是用来加载类到JVM中的,那么ClassLoader又是如何被加载呢?
这是因为存在一个ClassLoader不是用java语言所编写的,而是JVM实现的一部分,这个ClassLoader就是bootstrap classloader(启动类加载器)。

这个bootstrap classloader在JVM运行的时候加载java核心的API以满足java程序最基本的需求,其中就包括用户定义的ClassLoader(这里所谓的用户定义是指通过java程序实现的ClassLoader),一个是ExtClassLoader,一个是AppClassLoader。

(2)ExtClassLoader

ExtClassLoader,这个ClassLoader是用来加载java的扩展API的,也就是/lib/ext中的类;

(3)AppClassLoader

AppClassLoader,这个ClassLoader是用来加载用户机器上CLASSPATH设置目录中的Class的,通常在没有指定ClassLoader的情况下,程序员自定义的类就由AppClassLoader进行加载。

2. 一个程序最基本的类加载流程

当运行一个程序的时候,JVM启动,运行bootstrap classloader,该ClassLoader加载java核心API(ExtClassLoader和AppClassLoader也在此时被加载),然后调用ExtClassLoader加载扩展API,最后AppClassLoader加载CLASSPATH目录下定义的Class,这就是一个程序最基本的加载流程。 

java.png

3. ClassLoader使用双亲委托模式进行类加载

每一个自定义ClassLoader都必须继承ClassLoader这个抽象类,而每个ClassLoader都会有一个parent ClassLoader(注意,这个parent不是指的被继承的类,而是在实例化该ClassLoader时指定的一个ClassLoader),如果这个parent为null,那么就默认该ClassLoader的parent是bootstrap classloader。

下面这个小例子可以看到: 当前线程的上下文类加载器是什么,还有它的parent ClassLoader,还有它的parent ClassLoader的parent ClassLoader。

public class ClassLoaderTest {
    public static void main(String[] args) {

        /*
         * Thread.currentThread().getContextClassLoader()
         * 返回该线程的上下文类加载器。这个上下文类加载器由线程的创建者所提供,为了代码运行在该线程时加载类和资源。
         * 所以这里的loader是当前线程的上下文类加载器。
         */
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println("current loader:      "+loader);
        System.out.println("parent loader:       "+loader.getParent());
        //祖父ClassLoader是根类装载器,因为在Java中无法获得它的句柄,所以仅返回null
        System.out.println("grandparent loader:  "+loader.getParent(). getParent());
    }
}
输出结果为:
current loader:      sun.misc.Launcher$AppClassLoader@6da21389
parent loader:       sun.misc.Launcher$ExtClassLoader@2bb0bf9a
grandparent loader:  null

由上面的例子还可得出:AppClassLoader的parent ClassLoader是ExtClassLoader,而ExtClassLoader的parent ClassLoader是bootstrap classloader。

下面我们再看看为啥说ClassLoader是使用双亲委托模式进行类加载的?

下面代码是public abstract class 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;
        }
    }

①首先判断这个class是否已经被加载了,如果已经被加载了,则直接返回这个class
②如果当前的class未被加载,则判断这个ClassLoad的parent ClassLoad是否为null:
(1)如果parent不为空,则调用parent的loadClass来加载这个类;
(2)如果parent为空,则调用BootstrapClassLoader来加载这个类;
③ 如果经过上面的过程,还是没有找到,则调用自身的findClass进行加载 。

所以,我们要实现一个自定义类的ClassLoad的时候,只需要实现findClass方法即可。

为什么要使用这种双亲委托模式呢?

避免重复加载: 当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。 
安全考虑:以String类为例,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时被加载。

参考资料:http://www.iteye.com/topic/83978

三、思考

学了上面那么多理论知识,我就在想,在实际使用中,我是否用过这个机制呢?

然后发现在我之前写过的一篇文章中《数据库操作(JDBC->mybatis -> mybatis + Spring -> tddl)》,我们要通过jdbc建立数据库的连接,首先需要加载一个驱动类,下面的例子可以看出,这个驱动类就是通过反射机制加载进来:

public class JDBCUnit {

    //表示数据库的连接对象
    private static Connection conn = null;
    public static String DBDRIVER = "com.mysql.jdbc.Driver";
    //连接地址是由各个数据库生产商单独提供的,所以需要单独记住
    public static String DBURL = "jdbc:mysql://127.0.0.1:80/union_cps";
    //连接数据库的用户名
    public static String DBUSER = "username";
    //连接数据库的密码
    public static String DBPASS = "password";


    public static Connection getConnection() {
        try {
            /*
             * 1、使用CLASS 类加载驱动程序
             *  这里用了java的反射机制来加载com.mysql.jdbc.Driver这个类
             */
            Class.forName(DBDRIVER);  
            //2、连接数据库
            conn = DriverManager.getConnection(DBURL, DBUSER, DBPASS);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return conn;
    }
}

Class.forName(DBDRIVER);

在Class.forName加载完驱动类,开始执行静态初始化代码时,会自动新建一个Driver的对象,并调用DriverManager.registerDriver把自己注册到DriverManager中去。

下面看下Driver类的内容:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can\'t register driver!");
        }
    }
}

conn = DriverManager.getConnection(DBURL, DBUSER, DBPASS);

这个时候debug去看,已经注册的Driver有2个,其中一个就是我们注册进去的:com.mysql.jdbc.Driver

java2.png

然后再看getConnection方法是怎么去取得连接的:

private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
……
for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }

            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }

        }
……
}

Connection con = aDriver.driver.connect(url, info);
com.mysql.jdbc.Driver.connect(url,info)执行之后,发现url= "jdbc:mysql://127.0.0.1:80/union_cps";
这个是它可以处理的,所以就返回对应的connect给con变量。

补充介绍:Class.forName(String className) 与ClassLoader.loadClass(String className)的区别

Class.forName(String className) :加载类,并且执行类初始化
ClassLoader.loadClass(String className):仅仅加载类,不执行类初始化

可以通过Class.forName(String name, boolean initialize,ClassLoader loader) 第二个参数的赋值,来选择是否初始化类。

以上纯属学习总结,如果有错误,请及时指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章