Java基础-反射机制

一、Java反射机制概述

Java Reflection概述

Java Reflection是被视作动态语言的关键,反射机制允许程序在执行期间借助于Reflection API取得任何类的内部消息,并能直接操作任意对象的内部属性和方法

  • 加载完类之后,在堆内存中的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象,这个对象包含了完整的类的结构信息,我们可以通过这个对象看到类的结构。)
  • 这个对象就像一面镜子,透过这个镜子可以看到类的结构,所以我们形象的称之为:反射

image-20200822203753407

动态语言 VS 静态语言

动态:

动态语言是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说,就是在运行时代码可以根据某些条件改变自身结构

  • 主要动态语言:Object-C、C#、JavaScript、PHP、Python、Erlang

静态:

静态语言与动态语言相对应,运行时结构不可以改变的语言就是静态语言,如:Java、C、C++。

Java不是动态语言,但是Java可以称之为准动态语言

即Java有一定的动态性,我们可以利用反射机制、字节码操作获得类似动态语言的特性。

Java反射机制的研究及应用

Java反射机制的功能

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时获取泛型信息
  • 在运行时调用任意一个对象的成员变量和方法
  • 在运行时处理注解
  • 生成动态代理

应用例子

首先我们使用一个例子来感受一下反射,正常情况下,我们如果要调用一个对象的方法,或者访问一个对象的字段,通常会传入对象示例:

import com.gsl.Person

public class Main{
    String getFullName(Person p){
        return p.getFirstName() + "" + p.getLastName();
    }
}

但是,如果我们不能获得Person类,只有一个Object示例的话,比如这样:

String getFullName(Object obj){
    return ???;
}

如果使用强制转换类型的话仍然是需要Person类的,反射就是为了解决在运行期间,对某个实例一无所知的情况下,如何调用其方法:

import com.gsl.Person

String getFullName(Object obj){
    Person p = (Person) obj;
    return p.getFirstName() + "" + p.getLastName();
}

二、理解Class类并获取Class的实例

Class类

Class类概述及理解

Object类中定义了以下的方法,此方法将被所有子类继承:

//与wait()方法一样都在Object类下的方法,所有的对象都可以使用
public final Class getClass();

此方法返回的类型是一个Class的类,此类是Java反射的源头,实际上所谓的反射从程序的运行结果来看也很好理解,即:可以通过对象反射求出类的信息.

注意:这里的Class类型时一个名叫Classclass

public final class Class{
	private Class(){}
}

除了int,double,float等基本类型之外,Java的其他类型全部都是class,(如:String、Object、Runnable、Exception、自定义类型等)。

仔细思考思考,我们可以得出结论:class(包括interface)的本质是数据类型,没有继承关系的数据类型是无法赋值的.

//Double是Number的子类
Number n = new Double(123.456);
//编译报错!Double和String类之间没有继承关系
String s = new Double(123.456);

class是由JVM在执行过程中动态加载的。JVM在第一次读取到一种class类型时,将其加载进内存。

每加载一种class,JVM就为其创建一个Class类型的示例,并关联起来。

String类为例,当JVM加载String类时,它先读取String.class文件到内存,为String类创建一个Class示例并关联起来:

Class cls = new Class(String);

这个Class实例cls是JVM内部创建的,如果我们查看JDK源码,可以发现Class类的构造方法是private,也就是说只有JVM可以创建Class示例,我们自己的Java程序是无法创建Class实例的。

所以,JVM持有的每个class实例都指向一个数据类型(classinterface)。

┌───────────────────────────┐
│      Class Instance       │──────> String
├───────────────────────────┤
│name = "java.lang.String"  │
└───────────────────────────┘
┌───────────────────────────┐
│      Class Instance       │──────> Random
├───────────────────────────┤
│name = "java.util.Random"  │
└───────────────────────────┘
┌───────────────────────────┐
│      Class Instance       │──────> Runnable
├───────────────────────────┤
│name = "java.lang.Runnable"│
└───────────────────────────┘

一个Class实例包含了该class的所有完整信息:

┌───────────────────────────┐
│      Class Instance       │──────> String
├───────────────────────────┤
│name = "java.lang.String"  │
├───────────────────────────┤
│package = "java.lang"      │
├───────────────────────────┤
│super = "java.lang.Object" │
├───────────────────────────┤
│interface = CharSequence...│
├───────────────────────────┤
│field = value[],hash,...   │
├───────────────────────────┤
│method = indexOf()...      │
└───────────────────────────┘

由于JVM为每个加载的class创建了对应的Class实例,并在实例中保存了该class的所有信息(包括类名、包名、父类、实现的接口、所有方法、字段等...)。

因此,如果获取了某个Class实例,我们就可以通过这个Class实例获取到该实例对应的class的所有信息,这种通过Class实例获取class信息的方法称为反射(Reflection)

对于每个类而言,JRE都为其保一个不变的Class类型的对象。一个Class对象包含了特定某个结构的有关信息:

  • Class本身是一个类,而且Class对象只能由JVM创建。
  • 一个加载的类在JVM中只会有一个Class类实例。
  • 一个Class对象对应的是一个加载到JVM中的一个.class文件,不是.java文件。
  • 每个类的实例都会记得自己是由哪个Class实例所生成。
  • 通过Class可以完整的得到一个类种所有被加载的结构。
  • Class类是Reflection的根源,针对任何你想动态加载、运行的类,唯有先获得相应的Class对象。

Class类的常用方法

image-20200822210741813

哪些对象可以有Class对象:

  • class:外部类、成员内部类、静态内部类、局部内部类、匿名内部类
  • interface接口、[]数组、enum枚举类、annotation注解、primitive type基本数据类型、void

例子

public static void main(String[] args) throws Exception {
    printClassInfo("".getClass());
    printClassInfo(Runnable.class);
    printClassInfo(java.time.Month.class);
    printClassInfo(String[].class);
    printClassInfo(int.class);
}
private static void printClassInfo(Class<?> cls){
    System.out.println("----------------------------------------");
    System.out.println("Class name: " + cls.getName());
    System.out.println("Simple name: " + cls.getSimpleName());
    if (cls.getPackage() != null){
        System.out.println("Package name: " + cls.getPackage().getName());
    }
    System.out.println("is interface: " + cls.isInterface());
    System.out.println("is enum: " + cls.isEnum());
    System.out.println("is array: " + cls.isArray());
    System.out.println("is primitive: " + cls.isPrimitive());
}

注意到数组(例如String[])也是一种Class,而且不同于String.class,它的类名是[Ljava.lang.String。此外,JVM为每一种基本类型如int也创建了Class,通过int.class访问。

如果获取到了一个Class实例,我们就可以通过该Class实例来创建对应类型的实例:

//获取String的Class实例
Class cls = String.class;
//创建一个String实例
String s = (String) cls.getDeclaredConstructor().newInstance();

上述代码相当于new String()。通过Class.newInstance()可以创建类实例,它的局限是:只能调用public的无参数构造方法。

带参数的构造方法,或者非public的构造方法都无法通过Class.newInstance()被调用。

反射的应用举例

String str = "test4.Person";
Class clazz = Class.forName(str);
//newInstance方法已经废弃,使用clazz.getDeclaredConstructor().newInstance();
Object obj = clazz.newInstance();
Field field = clazz.getField("name");
field.set(obj, "Peter");
Object name = field.get(obj);
System.out.println(name);
//test4.Person是test4包下的Person类

获取Class类的实例(四种方法)

方法一:若已知具体的类,通过类的class属性获取,该方法最为安全可靠

Class clazz = String.class;

方法二:已知某个类的实例,调用该实例的getClass()方法获取Class对象.

Class clazz = "guosiliang".getClass();

方法三:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能会抛出ClassNotFoundException

Class clazz = Class.forName("java.lang.String");

方法四:(不做要求)

ClassLoader cl = this.getClass().getClassLoader();
Class clazz = cl.loadClass("类的全类名");

因为Class实例在JVM中是唯一的,所以上述方法获取的Class实例是同一个实例。可以用==比较两个实例。

Class clazz1 = String.class;
Class clazz2 = "guosiliang".getClass();

boolean sameClass = clazz1 == clazz2;//true

注意Class实例比较和instanceof的区别

Integer n = new Integer(123);

boolean b1 = n instanceof Integer;//true,因为n就是Integer类型
boolean b2 = n instanceof Number;//true,因为Integer是Number的子类

boolean b3 = n.getClass() == Integer.class;//true,因为n.getClass()返回Integer.class
boolean b4 = n.getClass() == Number.class;//false,因为Inter.class != Number.class

instanceof不但匹配指定类型,还匹配指定类型的子类。而用==判断class实例可以精确的判断数据类型,但不能作子类型比较。

通常情况下,我们应该用instanceof判断数据类型,因为面向抽象编程的时候,我们不关心具体的子类型。只有在需要精确的判断一个类型是不是某个class的时候,我们才使用==判断class实例。

动态加载

JVM在执行Java程序的时候,并不是一次性把所有用到的class全部加载到内存,而是第一次需要用到class时才加载。例如:

public class Main{

    public static void main(String[] args){
        if(args.length > 0){
            create(args[0]);
        }
    }
    
    private static void create(String name){
        Person p = new Person(name);
    }
}

当执行Main.java时,由于用到了Main,因此,JVM首先会把Main.class加载到内存。然而,并不会加载Person.class

除非程序执行到create()方法,JVM发现需要加载Person类时,才会首次加载Person.class。如果没有执行create()方法,那么Person.class根本就不会被加载。

动态加载class的特性对于Java程序非常重要。利用JVM动态加载class的特性,我们才能在运行期根据条件加载不同的实现类。例如,Commons Logging总是优先使用Log4j,只有当Log4j不存在时,才使用JDK的logging。利用JVM动态加载特性,大致的实现代码如下:

//Commons Logging优先使用Log4j
LogFactory factory = null;
if(isClassPresent("org.apache.logging.log4j.Logger")){
    factory = createLog4j();
} else {
    factory = createJdkLog();
}

boolean isClassPresent(String name){
    try{
        //只有当Log4j存在时,才可以执行forName不报错,返回true
        Class.forName(name);
        return true;
    } catch(Exception e){
        return false;
    }
}

这就是为什么我们只需要把Log4j的jar包放到classpath中,Commons Logging就会自动使用Log4j的原因。

三、类的加载与ClassLoader的理解

类的加载过程

由于JVM使用的动态加载方式,当程序第一次主动使用某个类时,才会加载这个类

如果该类还没有被加载到内存中,则系统会通过三个步骤对该类进行初始化:

  • 类的加载:将.class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口(即引用地址)。所有需要访问和使用类数据只能通过这个Class对象。这个加载的过程需要类加载器参与。
  • 类的链接:将Java类的二进制代码合并到JVM的运行状态之中的过程.
    • 验证:确保加载的类信息符合JVM规范,例如:以cafe开头,没有安全方面的问题。
    • 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
    • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
  • 类的初始化:JVM负责对类进行初始化。
    • 执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
    • 当初始化一个类时,如果发现它的父类还没有进行初始化,则需要首先触发其父类的初始化。
    • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。
public class ClassLoadingTest {

    public static void main(String[] args) {
        //类A 的初始化顺序应该是: 静态代码块 -> 静态变量,所以初始化结果m = 100;
        System.out.println(A.m);
    }
}

class A{
    static {
        m = 300;
    }
    static int m = 100;
    //第一步:加载,将A.class文件加载到内存中,生成一个Class类的对象,作为类数据访问的入口
    //第二步:链接结束之后,为类变量分配内存且默认初始值m = 0;
    //第三步:初始化之后,m的值由<clinit>()方法执行决定,这个A的类构造器<clinit>()方法由类变量的赋值和静态代码块中的语句按照顺序合并产生,类似于:
    //<clinit>(){
    //    m = 300;
    //    m = 100;
    //}
}

什么时候发生类初始化?

类的主动引用(一定会发生类的初始化)

  • 当虚拟机启动时,先初始化main()方法所在的类。
  • new一个类的对象
  • 调用类的静态成员(除了final常量)和静态方法。
  • 使用java.lang.reflect包的方法对类进行反射调用。
  • 当初始化一个类时,如果其父类没有被初始化,先初始化父类。

类的被动引用(不会发生类的初始化)

  • 当访问一个静态域时,只有真正声明这个域的类才会被初始化。
    • 当通过子类引用父类的静态变量,不会导致子类初始化。
  • 通过数组定义类引用,不会出发此类的初始化。
  • 引用常量不会出发此类的初始化(常量在链接阶段就存入调用类的常量池中了)。
public class ClassLoadingTest{
    public static void main(String[] args){
        //主动引用:一定会导致A和Father类的初始化
		A a = new A();
		System.out.println(A.m);
		A.method();
		Class.forName("com.guosl.reflect.A");
		//被动引用
		A[] array = new A[5];//通过数组定义类引用,不会触发此类的初始化
		System.out.println(A.b);//通过子类调用父类的静态变量,不会触发子类的初始化
		System.out.println(A.M);//引用常量不会触发此类的初始化
    }
    
    static{
        System.out.println("main所在的类");
    }
}
class A extends Father{

    static {
        System.out.println("子类被加载");
        m = 300;
    }
    static int m = 100;
    static final int M = 1;

    static void method(){
        System.out.println("A的静态方法");
    }

}
class Father{
    static int b = 2;
    static{
        System.out.println("父类被加载");
    }
}

ClassLoader概述及理解

ClassLoader的作用

类加载作用:.class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。

类缓存作用:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。

image-20200823111321039

ClassLoader

类加载器时用来把类装载进内存中,JVM规范定义了三类加载器:

  • 引导类加载器
  • 扩展类加载器
  • 系统类加载器
//1.获取一个系统类加载器
ClassLoader classloader = ClassLoader.getSystemClassLoader();
System.out.println(classloader);
//2.获取系统类加载器的父类加载器,即扩展类加载器
classloader = classloader.getParent();
System.out.println(classloader);
//3.获取扩展类加载器的父类加载器,即引导类加载器
classloader = classloader.getParent();
System.out.println(classloader);
//4.测试当前类由哪个类加载器进行加载
classloader = Class.forName("com.guosl.reflect.ClassTest").getClassLoader();
System.out.println(classloader);
//5.测试JDK提供的Object类由哪个类加载器加载
classloader = Class.forName("java.lang.Object").getClassLoader();
System.out.println(classloader);
//6.关于类加载器的一个主要方法:getResourceAsStream(String str):获取类路径下的置顶文件的输入流
InputStream in = null;
in = this.getClass().getClassLoader().getResourceAsStream("exer2\\test.properties");
System.out.println(in);

四、创建运行时类的对象

有了Class对象,就可以创建类的对象。

创建类的对象:调用Class对象的newInstance()方法,现方法已经废弃。通常先调用Class对象的getDeclaredConstructor()方法获取指定的构造器Constructor<?>实例,再调用newInstance()方法获得本类的对象。

//获得本类的指定形参类型的构造器,当没有参数时,默认获得本类的无参构造器
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException;

例子

//1.根据全类名获取对应的Class对象
Class<?> clazz = Class.forName("com.guosl.common.string.Person");
//调用Class对象的newInstance方法,前提必须要有无参构造器,而且类的构造器访问权限足够
//这个本质上是获得了本类的无参构造器,但是如果没有无参构造器,就只能指定构造器了
Person person = (Person) clazz.getDeclaredConstructor().newInstance();
System.out.println(person);
//如果没有无参构造器的话,可以明确地调用类中的构造器,并将参数传递进去就可以进行实例化操作
//获取到本类的指定构造器,生成Constructor实例
Constructor<?> con = clazz.getDeclaredConstructor(String.class, int.class);
Person peter = (Person) con.newInstance("Peter", 20);
System.out.println(peter);

五、获取运行时类的指定结构

对于任意的一个Object实例,只要我们获取了它的class,我们就可以获取他的一切信息。我们通过反射可以获得运行时类的完整结构包括:

实现的全部接口Interface

获取Interface

//确定此类或接口的接口实现
public Class<?>[] getInterfaces();

由于一个类可能实现一个或多个接口,通过Class我们就可以查询到实现的接口类型。例如,查询Integer实现的接口:

public class Main{
    public static void main(String[] args){
	    Class<Integer> integerClass = Integer.class;
        Class<?>[] interfaces = integerClass.getInterfaces();
		for (Class<?> i :interfaces){
			System.out.println(i);
		}
    }
}

运行上述代码可知:Integer实现的接口有:java.lang.Comparable接口。

getInterfaces()只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型。

Integer的父类是NumberNumber实现的接口是java.io.Serializable,此外对所有InterfaceClass调用getSuperclass()返回的是null,获取接口的父接口要用getInterfaces()

System.out.println(java.io.DataInputStream.class.getSuperclass());//java.io.FilterInputStream,因为DataInputStream继承自FilterInputStream
System.out.println(java.io.Closeable.class.getSuperclass()); // null,对接口调用getSuperclass()总是返回null,获取接口的父接口要用getInterfaces()

如果一个类没有实现任何interface,那么getInterfaces()返回空数组。

所继承的父类Superclass

获取继承关系

当我们获取到某个Class对象时,实际上就获取到了一个类的类型:

Class cls = String.class;//获取到String的class
Class cls2 = "".getClass();//也可以获取到String的class
Class cls3 = Class.forName("java.lang.String");//

获取父类的Class

//返回此实体(类、接口、基本类型)的父类的Class
public Class<? Super T> getSuperclass();

有了Class实例,我们还可以获取它的父类Class

public class Main{
    public static void main(String[] args){
     	Class<Integer> integerClass = Integer.class;//Integer.class
		Class<? super Integer> superclass = integerClass.getSuperclass();
		System.out.println(superclass);//Number.class
		Class<? super Integer> superclass1 = superclass.getSuperclass();
		System.out.println(superclass1);//null
		System.out.println(superclass1.getSuperclass());
    }
}

运行上述代码,可以看到,Integer的父类类型是NumberNumber的父类是ObjectObject的父类是null。除Object外,其他任何非interfaceClass都必定存在一个父类类型。

继承关系

当我们判断一个实例是否是某个类型时,正常情况下,使用instanceof操作符:

Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isSerializable = n instanceof java.io.Serializable; // true

如果是两个Class实例,要判断一个向上转型是否成立,可以调用isAssignableFrom()

 // Integer i = ?
Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer
// Number n = ?
Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number
// Object o = ?
Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object
// Integer i = ?
Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer

全部的构造器Constructor

我们通常使用new操作符创建新的实例:

Person p = new Person();

如果通过反射来创建新的实例,可以调用Class提供的newInstance()方法:

Person p = Person.class.newInstance();

调用Class.newInstance()的局限是:它只能调用该类的public无参构造函数,如果构造方法带有参数,或者不是public,就无法通过Class.newInstance()来调用。

为了调用任意的构造方法,Java的反射API提供了Constructor对象,它包含一个构造方法的所有信息,可以创建一个实例。它是一个构造方法,并且,调用结果总是返回实例:

//返回此类的所有public构造方法
public Constructor<T>[] getConstructors();
//返回此类的所有构造方法
public Constructor<T>[] getDeclaredConstructors();

Constructor类中:
//取得修饰符
public int getModifiers();
//取得方法名称
public String getName();
//取得参数的类型
public Class<?>[] getParameterTypes();
public class Main{
    public static void main(String[] args){
        //获取构造方法Integer(int)
        Constructor<Integer> con1 = Integer.class.getConstructor(int.class);
		Integer n1 = con1.newInstance(123);
		System.out.println(n1);

        //获取构造方法Integer(String)
		Constructor<Integer> con2 = Integer.class.getConstructor(String.class);
		Integer n2 = con2.newInstance("456");
		System.out.println(n2);
    }
}

Constructor总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。

全部的方法Method

我们可以通过Class实例获取到所有的Method信息。Class类提供了以下几个方法来获取Method

	//返回该类或接口的全部方法
public Method[] getDeclaredMethods();
public Method getDeclaredMethod(String name, Class<?>... parameterTypes);
//返回该类或接口的public方法
public Method[] getMethods();
public Method getMethod(String name, Class<?>... parameterTypes);

Method类中:
//取得全部的返回值
public Class<?> getReturnType();
//取得全部的参数
public Class<?>[] getParameterTypes();
//取得修饰符
public int getModifiers();
//取得异常信息
public Class<?>[] getExceptionTypes();

调用方法

public class Main{
    public static void main(String[] args){
        Class<Student> stdClass = Student.class;
		//获取public方法getScore,参数为String
		System.out.println(stdClass.getMethod("getScore", String.class));
		//获取继承的public方法getName,无参数
		System.out.println(stdClass.getMethod("getName"));
		//获取private方法getGrade,参数为int
		System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));
    }
}
class Student extends Person3{
    public int getScore(String type){
        return 99;
    }
    private int getGrade(int year){
        return 1;
    }
}
class Person3{
    String name;
    public String getName(){
        return "Person";
    }
    private void setName(String name) {
        this.name = name;
    }
}

打印结果为:

image-20200824092111553

当我们获取到一个Method对象时,就可以对他进行调用。

Stirng str = "Hello World";
String str2 = str.substring(6);//"World"

同样,我们可以使用反射机制进行调用substring方法,需要以下代码:

public class Main{
    public static void main(String[] args){
        String s = "Hello World";
        Method m = String.class.getMethod("substring", int.class);
        //对Method实例调用invoke就相当于调用该方法,即:m.invoke(s, 6) = s.substring(6);
        String r = (String) m.invoke(s, 6);
        System.out.println(r);
    }
}

Method实例调用invoke就相当于调用该方法,invoke的第一个参数是对象实例,即在哪个实例上调用该方法,后面的可变参数要与方法参数一致,否则将报错。

image-20200824152004346

//invoke第一个参数是对象实例,后面是可变参数
public Object invoke(Object obj, Object... args)
  • Object对应原方法的返回值,若原方法无返回值,此时返回null
  • 若原方法若为静态方法,此时形参Object obj可为null
  • 若原方法形参列表为空,则Object[] argsnull
  • 若原方法声明为private,则需要在调用此invoke()方法前,显式调用 方法对象的setAccessible(true)方法,将可访问private的方法。

调用静态方法

如果获取到的Method表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以invoke方法传入的第一个参数永远为null。我们以Integer.parseInt(String)为例:

public class Main{
    public static void main(String[] args){
        Method m2 = Integer.class.getMethod("parseInt", String.class);
    	// 调用静态方法的话,不需要指定对象,所以invoke传入的第一个参数永远为null
	    Integer n = (Integer) m2.invoke(null, "123456");
	    System.out.println(n);
    }
}

调用非public方法

对于非public方法,我们虽然可以通过Class.getDeclaredMethod()获取该方法实例,但直接对其调用将得到一个IllegalAccessException。为了调用非public方法,我们通过Method.setAccessible(true)允许其调用:

public class Main{
    public static void main(String[] args){
        Person3 p = new Person3();
	    Method m3 = p.getClass().getDeclaredMethod("setName", String.class);
    	m3.setAccessible(true);
	    m3.invoke(p, "Bob");
	    System.out.println(p.name);
    }
}
class Person3 {
  String name;

  public String getName() {
    return "Person";
  }

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

多态

假如一个Person类定义了hello()方法,并且它的子类Student也覆写了hello()方法,那么,从Person.class获取的Method,作用于Student实例时,调用的方法到底是哪个?

public class Main{
    public static void main(String[] args) throws Exception {
        //获取Person的hello方法
		Method hello = Person.class.getMethod("hello");
        //对Student实例调用hello方法
		hello.invoke(new Student2());//Student:Hello
	}
}
class Person {
	public void hello() {
		System.out.println("Person:Hello");
	}
}
class Student2 extends Person {
	@Override
	public void hello() {
		System.out.println("Student:Hello");
	}
}

使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)。上述代码实际上相当于

Method hello = Person.class.getMethod("hello");
hello.invoke(new Student2());//Student:Hello
|
Person p = new Student();
p.hello();//Student:Hello

全部的域Field

访问字段

//返回该类或接口的public的Field
public Field[] getFields();
//根据字段名获取某个public的Field,若没有该Field,会抛出NoSuchFieldException异常
public Field getField(String name);
//返回该类或接口的Field(不包括父类)
public Field[] getDeclaredFields();
//根据字段名获取当前类的某个Field(不包括父类),若没有该Field,会抛出NoSuchFieldException异常
public Field getDeclaredField(String name);

Field方法中:
//以整数形式返回此Field的修饰符,不同的bit代表不同的含义
public int getModifiers();
//得到Field的属性类型
public Class<?> getType();
//返回Field的名称
public String getName();

例子

public class ConstructorTest{
    public static void main(String[] args){
		Class<Student1> studentClass = Student1.class;
		//获取public字段"score"
		System.out.println(studentClass.getField("score"));
		//获取继承的public字段"name"
		System.out.println(studentClass.getField("name"));
		//获取private字段"grade"
		System.out.println(studentClass.getDeclaredField("grade"));
        
        Field value = String.class.getDeclaredField("value");
        System.out.println(value.getName());//vaalue
        System.out.println(value.getType());//class [B 表示byte[]类型
        int modifiers = value.getModifiers();
        System.out.println(Modifier.isFinal(modifiers));//true
        System.out.println(Modifier.isPublic(modifiers));//false
        System.out.println(Modifier.isProtected(modifiers));//false
        System.out.println(Modifier.isPrivate(modifiers));//true
        System.out.println(Modifier.isStatic(modifiers));//false
	}
}

class Student1 extends Person2 {

    public int score;

    private int grade;
}

class Person2{

    public String name;

}

上述代码首先获取StudentClass实例,然后,分别获取public字段、继承的public字段以及private字段,打印出的Field类似:

image-20200823200307499

获取字段值

利用反射拿到字段的一个Field实例只是第一步,我们还可以拿到一个实例对应的该字段的值。

例如:对于一个Person实例,我们可以先拿到name字段对应的Field,在获取这个实例的name字段的值:

public class Main{
    public static void main(String[] args){
        Object p = new Person3("Xiao Ming");
        Class<?> c = p.getClass();
        Field f = c.getDeclaredField("name");
        //获取该类中某个对象的name的值
        Object value = f.get(p);
        System.out.println(value);
    }
}
class Person3 {
    private String name;

    public Person3(String name){
        this.name = name;
    }
}

上述代码先获取Class实例,再获取Field实例,然后,用Field.get(Object)获取指定实例的指定字段的值。

运行代码,如果不出意外,会得到一个IllegalAccessException

image-20200823202844958

这是因为name被定义为一个private字段,正常情况下,Main类无法访问Person类的private字段。要修复错误,可以将private改为public,或者,在调用Object value = f.get(p);前,先写一句:

//不论这个字段是不是public还是其他的修饰符,一律进行访问
//setAccessible启动和禁用访问安全检查的开关
f.setAccessible(true);
  • 参数值为true则指示反射的对象在使用时应该取消Java语言访问检查。
    • 提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为true。
    • 使得原本无法访问的私有成员也可以访问
  • 参数值为false则指示反射的对象应该实施Java语言访问检查

有童鞋会问:如果使用反射可以获取private字段的值,那么类的封装还有什么意义?

答案是正常情况下,我们总是通过p.name来访问Person3name字段,编译器会根据publicprotectedprivate决定是否允许访问字段,这样就达到了数据封装的目的。

而反射是一种非常规的用法,使用反射,首先代码非常繁琐,其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。通过反射读写字段是一种非常规方法,它会破坏对象的封装。

此外,f.setAccessible(true)可能会失败。如果JVM运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对javajavax开头的package的类调用setAccessible(true),这样可以保证JVM核心库的安全。

String p = new String("Xiao Ming");
Class<?> c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true);
Object value = f.get(p);
System.out.println(value);

image-20200823203458930

设置字段值

通过Field实例既然可以获取到指定实例的字段值,自然也可以设置字段的值。

设置字段值是通过Field.set(Object, Object)实现的,其中第一个Object参数是指定的实例,第二个Object参数是待修改的值。示例代码如下:

public class Main{
    public static void main(String[] args) throws Exception {
        Person3 p = new Person3("Xiao Ming");
        System.out.println(p.getName());
        Class<? extends Person3> c = p.getClass();
        Field f = c.getDeclaredField("name");
        //同样的,非public的字段需要先调用该方法
        f.setAccessible(true);
        //将实例p的name设置为"Xiao Hong"
        f.set(p, "Xiao Hong");
        System.out.println(p.getName());
    }
}

以及所有的该类的所有其他信息Annotation,Generic,Package

public Annotation getAnnotation(Class<T> annotationClass); 
public Annotation getAnnotations();

//获取父类泛型类型
public Type getGenericSuperclass();

//获取类所在的包
public Package getPackage();

小结

  1. 在实际的操作中,取得类的信息的操作代码,不会经常开发。
  2. 一定要熟悉java.lang.reflect包的作用,反射机制。
  3. 如何取得属性、方法、构造器的名称,修饰符等。

六、反射的应用:动态代理

代理设计模式的原理及优点

代理设计模式原理

使用一个代理将对象包装起来, 然后用该代理对象取代原始对象。任何对原 始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原 始对象上。

之前为大家讲解过代理机制的操作,属于静态代理,特征是代理类和目标对象的类都是在编译期间确定下来,不利于程序的扩展。同时,每一个代理类只能为一个接口服务,这样一来程序开发中必然产生过多的代理。最好可以通过一个代理类完成全部的代理功能

如下是静态代理的代码编写:

public class StaticProxyTest {
	public static void main(String[] args) {
		//被代理类对象
		NikeClothFactory nikeClothFactory = new NikeClothFactory();
		//代理类对象
		ProxyClothFactory clothFactory = new ProxyClothFactory(nikeClothFactory);
		clothFactory.produceCloth();
	}
}
//代理类
class ProxyClothFactory implements ClothFactory{
	// 用被代理类对象进行实例化
	private ClothFactory factory;
	public ProxyClothFactory(ClothFactory factory){
		this.factory = factory;
	}
	@Override
	public void produceCloth() {
		System.out.println("代理工厂做一些准备工作");
		factory.produceCloth();
		System.out.println("代理工厂做一些后续工作");
	}
}
//被代理类
class NikeClothFactory implements ClothFactory{
	@Override
	public void produceCloth() {
		System.out.println("Nike工厂生产一批运动服");
	}
}
//接口
interface ClothFactory{
	void produceCloth();
}

动态代理相较于静态代理的优点

  • 动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象。
  • 动态代理使用场合:调试、远程方法调用

抽象角色中(接口和抽象类)声明的所有方法都被转移到调用处理器一个集中的方法中 处理,这样,我们可以更加灵活和统一的处理众多的方法。

Java动态代理相关API

Proxy:专门完成代理的操作类,是所有动态代理的父类,通过此类为一个或多个接口动态地生成实现类。

提供用于创建动态代理类和动态代理对象的静态方法:

//创建一个动态代理类所对应的Class对象
static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces);
//直接创建一个动态代理对象
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);
// loader-类加载器、interfaces-得到被代理实现的全部接口 h-得到InvocationHandler接口的实现类实例

动态代理步骤

  1. 创建一个实现接口InvocationHandler的类,它必须实现invoke方法,以完成代理的具体操作。

    /**
     * 定义一个实现了`InvocationHandler`的实例
     * 主要解决:代理类对象调用方法时,如何动态地去调用被代理类中的同名方法
     * 因为当代理类调用 某个方法a 的时候,就会自动地调用`InvocationHandler`的invoke()方法。
     */
    class MyInvocationHandler implements InvocationHandler{
    	//赋值时也需要使用被代理类初始化
    	private Object object;
    	public MyInvocationHandler(Object object){
    		this.object = object;
    	}
    	/**
    	 * 当我们通过代理类的对象,调用方法a是,就会自动的调用如下的方法
    	 * 将被代理类要执行的方法a的功能就声明在invoke中
    	 */
    	@Override
    	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    		//method:即为代理类对象调用的方法,此方法也就作为了被代理类对象要调用的方法
    		//obj:被代理类的对象
    		return method.invoke(object, args);
    	}
    }
    
  2. 创建被代理的类以及接口

    /**
     * 接口
     */
    interface Human{
    
    	String getBelief();
    
    	void eat(String food);
    
    }
    /**
     * 被代理类
     */
    class SuperMan implements Human{
    
    	@Override
    	public String getBelief() {
    		return "I believe I can fly";
    	}
    
    	@Override
    	public void eat(String food) {
    		System.out.println("我喜欢吃" + food);
    	}
    }
    
    
  3. 通过Proxy的静态方法newProxyInstance(),根据被代理类创建代理类.

    /**
     * 这个类用于生成代理类的对象
     */
    class ProxyFactory{
    
    	/**
    	 * 根据被代理的对象,调用此方法,返回一个代理类的对象,解决问题一
    	 */
    	public static Object getProxyInstance(Object object){
    
    		return Proxy.newProxyInstance(
    				//传入被代理类的类加载器
    				object.getClass().getClassLoader(),
    				//传入被代理类的实现接口
    				object.getClass().getInterfaces(),
    				//传入MyInvocationHandler实例,传入被代理类
    				new MyInvocationHandler(object)
    		);
    	}
    }
    
  4. 通过代理类调用方法时,会自动调用InvocationHandlerinvoke()方法.

    public class DynamicProxyTest {
    
    	public static void main(String[] args) {
    		//根据被代理对象,生成一个代理对象
    		Human proxyInstance = (Human) ProxyFactory.getProxyInstance(new SuperMan());
    		//当代理类对象proxyInstance调用getBelief和eat方法时,自动调用invoke方法;
    		System.out.println(proxyInstance.getBelief());
    		proxyInstance.eat("四川麻辣烫");
    	}
    }
    

动态代理与AOP

前面介绍的ProxyInvocationHandler,很难看出这种动态代理的优势,下面介绍一种更实用的动态代理机制。

image-20200825094837907

当我们在写代码的过程中出现了许多相同的代码段,这样的话相同的代码块和整体代码的耦合度比较高,所以需要进行以下改进:

image-20200825095052805

改进后,代码段1、代码段2、代码段3和相同的代码块已经分离开了,但是代码段1、2、3又与方法A产生了耦合。

所以最理想的情况是:代码块1、2、3即可以执行方法A,又无需在程序中以硬编码的方式直接调用相同代码的方法。

解决方法:在动态代理的基础之上,当代理类调用方法时,会自动调用invoke()方法,即改造invoke()方法即可完成:

class HumanUtil{
	public void method1(){
		System.out.println("模拟相同代码块1:------------------------------");
	}
	public void method2(){
		System.out.println("模拟相同代码块2:------------------------------");
	}
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //即面向切面编程
	HumanUtil util = new HumanUtil();
    //类似于Spring中的preHandle
	util.method1();
	//method:即为代理类对象调用的方法,此方法也就作为了被代理类对象要调用的方法
	//obj:被代理类的对象
	Object result = method.invoke(object, args);
    //postHandle
	util.method2();
	return result;
}
  • 使用Proxy生成一个动态代理时,往往并不会凭空产生一个动态代理,这样没有太大意义。通常都是为指定的目标对象生成动态代理
  • 这种动态代理在AOP中被称为AOP代理,AOP代理可代替目标对象,AOP代理包含了目标对象的全部方法。但AOP代理中的方法与目标对象的方法存在差异: AOP代理里的方法可以在执行目标方法之前、之后插入一些通用处理。

posted @ 2020-08-25 10:23  SalemG  阅读(92)  评论(0)    收藏  举报