前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java Review(三十九、类加载机制与反射)

Java Review(三十九、类加载机制与反射)

作者头像
三分恶
发布2020-07-16 13:32:16
8240
发布2020-07-16 13:32:16
举报
文章被收录于专栏:三分恶的专栏三分恶的专栏

类的加载、 连接和初始化

系统可能在第一次使用某个类时加载该类, 也可能采用预加载机制来加载某个类。

JVM 和类

当调用 java 命令运行某个 Java 程序时, 该命令将会启动一个 Java 虚拟机进程, 不管该 Java 程序有多么复杂, 该程序启动了多少个线程, 它们都处于该 Java 虚拟机进程里。 同一个 JVM的所有线程、 所有变量都处于同一个进程里, 它们都使用该 JVM 进程的内存区。

JVM运行时数据区

在这里插入图片描述
在这里插入图片描述

当系统出现以下几种情况时, JVM 进程将被终止:

  • 程序运行到最后正常结束。
  • 程序运行到使用 System.exit()或 Runtime.getRuntime().exit()代码处结束程序。
  • 程序执行过程中遇到未捕获的异常或错误而结束。
  • 程序所在平台强制结束了 JVM 进程。

从上面的介绍可以看出, 当 Java 程序运行结束时, JVM 进程结束, 该进程在内存中的状态将会丢失。

下面以类的类变量来说明这个问题。 下面程序先定义了一个包含类变量的类:

public class A
{
	// 定义该类的类变量
	public static int a = 6;
}

接下来定义一个类创建 A 类的实例, 并访问 A 对象的类变量 a:

public class ATest1
{
	public static void main(String[] args)
	{
		// 创建A类的实例
		A a = new A();
		// 让a实例的类变量a的值自加
		a.a ++;
		System.out.println(a.a);
	}
}

下面程序也创建 A 对象, 并访问其类变量 a 的值:

public class ATest2
{
	public static void main(String[] args)
	{
		// 创建A类的实例
		A b = new A();
		// 输出b实例的类变量a的值
		System.out.println(b.a);
	}
}

在 ATestl.java 程序中创建了 A 类的实例, 并让该实例的类变量 a 的值自加, 程序输出该实例的类变量 a 的值将看到 7。 运行第二个程序 ATest2 时, 程序再次创建了 A 对象, 并输出 A 对象类变量的 a 的值, 此时 a 的值是多少呢? 结果依然是 6, 并不是 7。 这是因为运行 ATestl 和 ATest2 是两次运行 JVM 进程, 第一次运行 JVM 结束后, 它对 A 类所做的修改将全部丢失——第二次运行 JVM 时将再次初始化 A 类。

类的加载

Java类的生命周期

在这里插入图片描述
在这里插入图片描述

当程序主动使用某个类时, 如果该类还未被加载到内存中, 则系统会通过加载、 连接、 初始化三个步骤来对该类进行初始化。 如果没有意外, JVM 将会连续完成这三个步骤, 所以有时也把这三个步骤统称为类加载或类初始化。

类加载指的是将类的 class 文件读入内存, 并为之创建一个 java.lang.Class 对象, 也就是说, 当程序中使用任何类时, 系统都会为之建立一个java.lang.Class 对象。

类的加载由类加载器完成, 类加载器通常由 JVM 提供, 这些类加载器也是前面所有程序运行的基础, JVM 提供的这些类加载器通常被称为系统类加载器。 除此之外, 开发者可以通过继承 ClassLoader基类来创建自己的类加载器。

通过使用不同的类加载器, 可以从不同来源加载类的二进制数据, 通常有如下几种来源。

  • 从本地文件系统加载 class 文件, 这是前面绝大部分示例程序的类加载方式。
  • 从 JAR 包加载 class 文件, 这种方式也是很常见的, 前面介绍 JDBC 编程时用到的数据库驱动类就放在 JAR 文件中, JVM 可以从 JAR 文件中直接加载该 class 文件。
  • 通过网络加载 class 文件。
  • 把 一 个 Java 源文件动态编译, 并执行加载。

类加载器通常无须等到“ 首次使用” 该类时才加载该类, Java 虚拟机规范允许系统预先加载某些类。

类的连接

当类被加载之后, 系统为之生成一个对应的 Class 对象, 接着将会进入连接阶段, 连接阶段负责把类的二进制数据合并到 JRE 中。 类连接又可分为如下三个阶段:  (1) 验证: 验证阶段用于检验被加载的类是否有正确的内部结构, 并和其他类协调一致。  (2) 准备: 类准备阶段则负责为类的类变量分配内存, 并设置默认初始值。  (3 ) 解析: 将类的二进制数据中的符号引用替换成直接引用。

类的初始化

在类的初始化阶段, 虚拟机负责对类进行初始化, 主要就是对类变量进行初始化。

在 Java 类中对类变量指定初始值有两种方式:

  • ① 声明类变量时指定初始值;
  • ② 使用静态初始化块为类变量指定初始值。

例如下面代码片段:

public class Test{
 // 声明变量 a 时指定初始值
  static int a = 5
  static int b;
  static int c;
  static{
  // 使用静态初始化块为变量 b 指定初始值
   b=6;
  }
}   

对于上面代码, 程序为类变量 a、 b 都显式指定了初始值, 所以这两个类变量的值分别为 5、 6, 但类变量 c 则没有指定初始值, 它将采用默认初始值 0。

声明变量时指定初始值, 静态初始化块都将被当成类的初始化语句, JVM 会按这些语句在程序中的排列顺序依次执行它们, 例如下面的类:

public class Test
{
	static
	{
		// 使用静态初始化块为变量b指定出初始值
		b = 6;
		System.out.println("----------");
	}
	// 声明变量a时指定初始值
	static int a = 5;
	static int b = 9;         // ①
	static int c;
	public static void main(String[] args)
	{
		System.out.println(Test.b);
	}
}

上面代码先在静态初始化块中为 b 变量赋值, 此时类变量 b 的值为 6; 接着程序向下执行, 执行到①号代码处, 这行代码也属于该类的初始化语句, 所以程序再次为类变量 b 赋值。 也就是说, 当 Test类初始化结束后, 该类的类变量 b 的值为 9。

JVM 初始化一个类包含如下几个步骤:

  • 假如这个类还没有被加载和连接, 则程序先加载并连接该类。
  • 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  • 假如类中有初始语句 , 则系统依次执行这些初始化语句

类初始化的时机

当 Java 程序首次通过下面 6 种方式来使用某个类或接口时, 系统就会初始化该类或接口:

  • 创建类的实例。 为某个类创建实例的方式包括: 使用 new 操作符来创建实例, 通过反射来创建实例, 通过反序列化的方式来创建实例。
  • 调用某个类的类方法(静态方法)。
  • 访问某个类或接口的类变量, 或为该类变量赋值。
  • 使用反射方式来强制创建某个类或接口对应 的 java.lang.Class 对 象 。 例 如 代 码 : Class.forName(MPerson"), 如果系统还未初始化 Person 类, 则这行代码将会导致该 Person 类被初始化, 并返回 Person 类对应的 java.lang.Class 对象。
  • 初始化某个类的子类。 当初始化某个类的子类时, 该子类的所有父类都会被初始化。
  • 直 接 使 用 java.exe 命令来运行某个主类。 当运行某个主类时, 程序会先初始化该主类。

类加载器

类加载器负责将.class 文件( 可能在磁盘上, 也可能在网络上) 加载到内存中, 并为之生成对应的java.lang.Class 对象。

类加载机制

类加载器负责加载所有的类, 系统为所有被载入内存中的类生成java.lang.Class 实例。

一个类被载入 JVM 中, 同 一个类就不会被再次载入了——正如一个对象有一个唯一的标识一样, 一个载入 JVM 中的类也有一个唯一的标识。 在 Java 中, 一 个类用其全限定类名( 包括包名和类名) 作为标识; 但在 JVM 中, 一个类用其全限定类名和其类加载器作为唯一标识。 例如, 如果在 pg 的包中有一个名为 Person 的类, 被类加载器 ClassLoader 的实例 kl负责加载, 则该 Person 类对应的 Class 对象在 JVM 中表示为 Person、pg、kl ) 这意味着两个类加载器加载的同名类: (Person 、pg、 kl ) 和( Person、 pg、 kl2) 是不同的, 它们所加载的类也是完全不同、互不兼容的。

当 JVM 启动时, 会形成由三个类加载器组成的初始类加载器层次结构。

  • 根类加载器(Bootstrap ClassLoader):其负责加载Java的核心类,比如String、System这些类
  • 拓展类加载器(Extension ClassLoader):其负责加载JRE的拓展类库
  • 系统类加载器(System ClassLoader):其负责加载CLASSPATH环境变量所指定的JAR包和类路径

除了可以使用 Java 提供的类加载器之外, 开发者也可以实现自己的类加载器, 自定义的类加载器通过继承 ClassLoader 来实现。

  • 用户类加载器:用户自定义的加载器,以类加载器为父类

类加载器的层次

在这里插入图片描述
在这里插入图片描述

双亲委派模型:如上图所示的类加载器之间的这种层次关系,就称为类加载器的双亲委派模型(Parent Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是通过组合(Composition)关系来复用父加载器的代码。 双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。

JVM 的类加载机制主要有如下三种:

  • 全盘负责。 所谓全盘负责, 就是当一个类加载器负责加载某个 Class 时, 该 Class 所依赖的和引用的其他 Class 也将由该类加载器负责载入, 除非显式使用另外一个类加载器来载入。
  • 父类委托。 所谓父类委托, 则是先让 parent (父) 类加载器试图加载该 Class, 只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
  • 缓存机制。缓存机制将会保证所有加载过的 Class 都会被缓存, 当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class, 只有当缓存区中不存在该 Class 对象时, 系统才会读取该类对应的二进制数据, 并将其转换成Class 对象, 存入缓存区中。 这就是为什么修改了 Class 后,必须重新启动 JVM, 程序所做的修改才会生效的原因。

下面程序示范了访问 JVM 的类加载器:

import java.util.*;
import java.net.*;
import java.io.*;

public class ClassLoaderPropTest {
	public static void main(String[] args) throws IOException {
		// 获取系统类加载器
		ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
		System.out.println("系统类加载器:" + systemLoader);
		/*
		 * 获取系统类加载器的加载路径——通常由CLASSPATH环境变量指定 如果操作系统没有指定CLASSPATH环境变量,默认以当前路径作为
		 * 系统类加载器的加载路径
		 */
		Enumeration<URL> em1 = systemLoader.getResources("");
		while (em1.hasMoreElements()) {
			System.out.println(em1.nextElement());
		}
		// 获取系统类加载器的父类加载器:得到扩展类加载器
		ClassLoader extensionLader = systemLoader.getParent();
		System.out.println("扩展类加载器:" + extensionLader);
		System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));
		System.out.println("扩展类加载器的parent: " + extensionLader.getParent());
	}
}

运行结果:

在这里插入图片描述
在这里插入图片描述

系统类加载器是 AppClassLoader 的实例, 扩展类加载器 PlatformClassLoader 的实例。 实际上, 这两个类都是 URLClassLoader 类的实例。

根类加载器并不是Java实现的,而且由于程序通常须访问根加载器,因此访问扩展类加载器的父类加载器时返回NULL。

类加载器加载 Class 大致要经过如下 8 个步骤(对应上面的双亲委派模型):

  • (1) 检测此Class是否载入过(即在缓存区中是否有此Class),如果有则直接进入第8步,否则接着执行第2步。
  • (2) 如果父类加载器不存在(如果没有父类加载器,则要么parent -定是根类加载器,要么本身就 是根类加载器),则跳到第4步执行; 如果父类加载器存在,则接着执行第3步。
  • (3) 请求使用父类加载器去载入目标类,如果成功载入则跳到第8步,否则接着执行第5步。
  • (4) 请求使用根类加载器来载入目标类,如果成功载入则跳到第8步,否则跳到第7步。
  • (5) 当前类加载器尝试寻找Class文件(从与此ClassLoader相关的类路径中寻找),如果找到则执 行第6步,如果找不到则跳到第7步。
  • (6) 从文件中载入Class,成功载入后跳到第8步。
  • (7) 抛出 ClassNotFoundException 异常。
  • (8) 返回对应的java.lang.Class对象。

其中,第5、6步允许重写ClassLoader的findClass()方法来实现自己的载入策略,甚至重写loadClass() 方法来实现自己的载入过程。

API:java.lang.ClassLoader

创建并使用自定义的类加载器

JVM 中除根类加载器之外的所有类加载器都是 ClassLoader 子类的实例, 开发者可以通过扩展ClassLoader 的子类, 并重写该 ClassLoader 所包含的方法来实现自定义的类加载器。 查阅 API 文档中关于 ClassLoader的方法不难发现, ClassLoader 中包含了大量的 protected 方法 这些方法都可被子类重写。

ClassLoader 类有如下两个关键方法:

  • loadClass(String name,boolean resolve) 该方法为 ClassLoader 的入口点, 根据指定名称来加载类, 系统就是调用 ClassLoader 的该方法来获取指定类对应的 Class 对象。
  • findClass(String name) 根据指定名称来查找类。如果需要实现自定义的ClassLoader, 则可以通过重写以上两个方法来实现, 通常推荐重写 fmdClass()方法, 而不是重写 loadClass()方法。

loadClass()方法的执行步骤如下:

  • 用 flndLoadedClass(String) 来检查是否已经加载类, 如果己经加载则直接返回。
  • 在父类加载器上调用loadClass()方法。如果父类加载器为null,则使用根类加载器来加载。
  • 调用findClass(String)方法查找类。

从上面步骤中可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两 种策略;如果重写loadClass()方法,则实现逻辑更为复杂。

在 ClassLoader 里还有一个核心方法:Class defineClass(String name, byte[] b, int off, int len),该方法 负责将指定类的字节码文件(即Class文件,如Hello.class)读入字节数组byte[] b内,并把它转换为 Class对象,该字节码文件可以来源于文件、网络等。

defineClass()方法管理JVM的许多复杂的实现,它负责将字节码分析成运行时数据结构,并校验有效性等。

除此之外,ClassLoader里还包含如下一些普通方法。

  • findSystemClass(String name):从本地文件系统装入文件。它在本地文件系统中寻找类文件,如 果存在,就使用defineClass()方法将原始字节转换成Class对象,以将该文件转换成类。
  • static getSystemClassLoader():这是一个静态方法,用于返回系统类加载器。
  • getParent():获取该类加载器的父类加载器。
  • resolveClass(CIass<?> c):链接指定的类。类加载器可以使用此方法来链接类c。
  • findLoadedClass(String name):如果此Java虚拟机已加载了名为name的类,则直接返回该类对 应的Class实例,否则返回null。该方法是Java类加载缓存机制的体现。

下面程序开发了一个自定义的ClassLoader,该ClassLoader通过重写findClass()方法来实现自定义 的类加载机制。这个ClassLoader可以在加载类之前先编译该类的源文件,从而实现运行Java之前先编 译该程序的目标,这样即可通过该ClassLoader直接运行Java源文件。

public class MyClassLoader extends ClassLoader {
    private String root;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args)  {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("E:\\temp");

        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("com.neo.classloader.Test2");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。这里有几点需要注意:

  • 1、这里传递的文件名需要是类的全限定性名称,即 com. paddx.test . classloading.Test格式的,因为 defineClass 方法是按这种格式进行处理的。
  • 2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。
  • 3、这类Test 类本身可以被 AppClassLoader类加载,因此我们不能把com/paddx/test/classloading/Test.class放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由AppClassLoader加载,而不会通过我们自定义类加载器来加载。

URLCIassLoader 类

Java 为 ClassLoader 提供了一个 URLClassLoader 实现类, 该类也是系统类加载器和扩展类加载器的父类( 此处的父类, 就是指类与类之间的继承关系)。 URLClassLoader 功能比较强大, 它既可以从本地文件系统获取二进制文件来加载类, 也可以从远程主机获取二进制文件来加载类。

在应用程序中可以直接使用 URLClassLoader 加载类, URLClassLoader 类提供了如下两个构造器:

  • URLClassLoader(URL[] urls): 使用默认的父类加载器创建一个 ClassLoader 对象, 该对象将从urls 所指定的系列路径来查询并加载类。
  • URLClassLoader(URL[] urls,ClassLoader parent): 使用指定的父类加载器创建一个 ClassLoader对象, 其他功能与前一个构造器相同。

一旦得到了 URLClassLoader 对象之后, 就可以调用该对象的 loadClass()方法来加载指定类。 下面程序示范了如何直接从文件系统中加载 MySQL 驱动, 并使用该驱动来获取数据库连接。 通过这种方式来获取数据库连接, 可以无须将 MySQL 驱动添加到 CLASSPATH 环境变量中。

import java.sql.*;
import java.util.*;
import java.net.*;

public class URLClassLoaderTest {
	private static Connection conn;

	// 定义一个获取数据库连接方法
	public static Connection getConn(String url, String user, String pass) throws Exception {
		if (conn == null) {
			// 创建一个URL数组
			URL[] urls = { new URL("file:mysql-connector-java-5.1.30-bin.jar") };
			// 以默认的ClassLoader作为父ClassLoader,创建URLClassLoader
			URLClassLoader myClassLoader = new URLClassLoader(urls);
			// 加载MySQL的JDBC驱动,并创建默认实例
			Driver driver = (Driver) myClassLoader.loadClass("com.mysql.jdbc.Driver").getConstructor().newInstance();
			// 创建一个设置JDBC连接属性的Properties对象
			Properties props = new Properties();
			// 至少需要为该对象传入user和password两个属性
			props.setProperty("user", user);
			props.setProperty("password", pass);
			// 调用Driver对象的connect方法来取得数据库连接
			conn = driver.connect(url, props);
		}
		return conn;
	}

	public static void main(String[] args) throws Exception {
		System.out.println(getConn("jdbc:mysql://localhost:3306/mysql", "root", "32147"));
	}
}

API:java.net.URLClassLoader

通过反射查看类信息

Java 程序中的许多对象在运行时都会出现两种类型: 编译时类型和运行时类型, 例如代码: Person p=new Student();,这行代码将会生成一个 p 变量, 该变量的编译时类型为 Person,运行时类型为 Student;除此之外, 还有更极端的情形, 程序在运行时接收到外部传入的一个对象, 该对象的编译时类型是 Object,但程序又需要调用该对象运行时类型的方法。

获得 Class 对象

在 Java 程序中获得 Class 对象通常有如下三种方式:

  • 使用 Class 类的 forName(String clazzName)静态方法。 该方法需要传入字符串参数, 该字符串参数的值是某个类的全限定类名( 必须添加完整包名)。
  • 调用某个类的 class 属性来获取该类对应的 Class 对象。 例如, Person.class 将会返回 Person 类对应的 Class 对象。
  • 调用某个对象的 getClass()方法。 该方法是 java.lang.Object 类中的一个方法, 所以所有的 Java对象都可以调用该方法, 该方法将会返回该对象所属类对应的 Class 对象。

对于第一种方式和第二种方式都是直接根据类来取得该类的 Class 对象, 相比之下, 第二种方式有如下两种优势:

  • 代码更安全。 程序在编译阶段就可以检查需要访问的 Class 对象是否存在。
  • 程序性能更好。 因为这种方式无须调用方法, 所以性能更好。

也就是说, 大部分时候都应该使用第二种方式来获取指定类的 Class 对象。 但如果程序只能获得一个字符串, 例如”java.lang.String”, 若需要获取该字符串对应的 Class 对象, 则只能使用第一种方式, 使用Class 的 forName(String clazzName)方法获取 Class 对象时, 该方法可能抛出一个 ClassNotFoundException异常。

一旦获得了某个类所对应的 Class 对象之后, 程序就可以调用 Class 对象的方法来获得该对象和该类的信息了。

从 Class 中获取信息

Class 类提供了大量的实例方法来获取该 Class 对象所对应类的详细信息,Class 类大致包含如下方法, 下面每个方法都可能包括多个重载的版本, 应该参照官方API。

下面 4 个方法用于获取 Class 对应类所包含的构造器:

  • Connstructor<\T> getConstructor(Class<?>...parameterTypes): 返回此 Class 对象对应类的、 带指定形参列表的 public 构造器。
  • Constructor<?>[]getConstructors(): 返回此 Class 对象对应类的所有 public 构造器。
  • Constructor<\T> getDeclaredConstructor(Class<?>...parameterTypes): 返回此 Class 对象对应类的、带指定形参列表的构造器, 与构造器的访问权限无关。
  • Constructor<?>[]getDeclaredConstructors(): 返回此 Class 对象对应类的所有构造器, 与构造器的访问权限无关。

下面 4 个方法用于获取 Class 对应类所包含的方法:

  • Method getMethod(String name,Class<?>...parameterTypes): 返回此 Class 对象对应类的、 带指定形参列表的 public 方法。
  • Method[]getMethods(): 返回此 Class 对象所表示的类的所有 public 方法。
  • Method getDeclaredMethod(String name,Class<?>...parameterTypes): 返回此 Class 对象对应类的、带指定形参列表的方法, 与方法的访问权限无关。
  • Method[]getDeclaredMethods(): 返回此 Class 对象对应类的全部方法, 与方法的访问权限无关。

如下 4 个方法用于访问 Class 对应类所包含的成员变量:

  • Field getField(String name): 返回此 Class 对象对应类的、 指定名称的 public 成员变量。
  • Field[]getFields(): 返回此 Class 对象对应类的所有 public 成员变量。
  • Field getDeclaredField(String name): 返回此 Class 对象对应类的、 指定名称的成员变量, 与成员变量的访问权限无关。
  • Field[]getDeclaredFields(): 返回此 Class 对象对应类的全部成员变量, 与成员变量的访问权限无关。

如下几个方法用于访问 Class 对应类上所包含的 Annotation:

  • <A extends Annotation〉 A getAnnotation(Class<\A> annotationClass): 尝试获取该 Class 对象对应类上存在的、 指定类型的 Annotation; 如果该类型的注解不存在, 则返回 null。
  • <A extends Annotation〉 A getDeclaredAnnotation(Class<\A> annotationClass): 这是 Java 8 新增的方法, 该方法尝试获取直接修饰该 Class 对象对应类的、 指定类型的 Annotation; 如果该类型的注解不存在, 则返回 null。
  • Annotation[] getAnnotations(): 返回修饰该 Class 对象对应类上存在的所有 Annotation。
  • Annotation[] getDeclaredAnnotations(): 返回直接修饰该 Class 对应类的所有 Annotation。
  • < A extends Annotation> A[] getAnnotationsByType(Class<\A> annotationClass): 该方法的功能与前面介绍的 getAnnotation()方法基本相似。 但由于 Java 8 增加了重复注解功能, 因此需要使用该方法获取修饰该类的、 指定类型的多个 Annotation。
  • < A extends Annotation> A[] getDeclaredAnnotationsByType(Class<\A> annotationClass): 该方法的 功能与前面介绍的 getDeclaredAnnotations ()方法基本相似。 但由于 Java 8 增加了重复注解功能,因此需要使用该方法获取直接修饰该类的、 指定类型的多个 Annotation。

如下方法用于访问该 Class 对象对应类包含的内部类:

  • Class<?>[] getDeclaredClasses(): 返回该 Class 对象对应类里包含的全部内部类。

如下方法用于访问该 Class 对象对应类所在的外部类:

  • Class<?> getDeclaringClass(): 返回该 Class 对象对应类所在的外部类。

如下方法用于访问该 Class 对象对应类所实现的接口:

  • Class<?>[] getlnterfaces(): 返回该 Class 对象对应类所实现的全部接口。

如下几个方法用于访问该 Class 对象对应类所继承的父类:

  • Class<? super T> getSuperclass(): 返回该 Class 对象对应类的超类的 Class 对象。

如下方法用于获取 Class 对象对应类的修饰符、 所在包、 类名等基本信息:

  • int getModifiers(): 返回此类或接口的所有修饰符。 修饰符由 public、 protected、 private、 final、static、 abstract 等对应的常量组成, 返回的整数应使用 Modifier 工具类的方法来解码, 才可以获取真实的修饰符。
  • Package getPackage(): 获取此类的包。
  • String getName(): 以字符串形式返回此 Class 对象所表示的类的名称。
  • String getSimpleName(): 以字符串形式返回此 Class 对象所表不的类的简称。

除此之外, Class 对象还可调用如下几个判断方法来判断该类是否为接口、 枚举、 注解类型等:

  • boolean isAnnotation(): 返回此 Class 对象是否表示一个注解类型(由@interface 定义)。
  • boolean isAnnotationPresent(Class<? extends Annotation〉 annotationClass): 判断此 Class 对象是否使用了 Annotation 修饰。
  • boolean isAnonymousClass(): 返回此 Class 对象是否是一个匿名类。
  • boolean isArray(): 返回此 Class 对象是否表不一个数组类。
  • boolean isEnum(): 返回此 Class 对象是否表不一个枚举(由
  • boolean islnterface(): 返回此 Class 对象是否表示一个接口( 使用 interface 定义)。
  • boolean isInstance(Object obj): 判断 obj 是否是此 Class 对象的实例, 该方法可以完全代替instanceof 操作符。

上面的多个 getMethod()方法和 getConstructor()方法中, 都需要传入多个类型为 Class<?〉的参数, 用于获取指定的方法或指定的构造器。 关于这个参数的作用, 假设某个类内包含如下三个 info 方法签名:

  • public void info()
  • public void info(String str)
  • public void info(String str , Integer num)

在程序中获取该方法使用如下代码:

// 前一个参数指定方法名, 后面的个数可变的 Class 参数指定形参类型列表
clazz.getMethod("info" , String.class)

如果需要获取第三个 info 方法, 则使用如下代码:

// 前一个参数指定方法名, 后面的个数可变的 Class 参数指定形参类型列表
clazz.getMethod("info" , String.class, Integer.class)

下面程序示例了如何通过该 Class 对象来获取对应类的详细信息:

import java.util.*;
import java.lang.reflect.*;
import java.lang.annotation.*;


// 定义可重复注解
@Repeatable(Annos.class)
@interface Anno {
}

@Retention(value = RetentionPolicy.RUNTIME)
@interface Annos {
	Anno[] value();
}

// 使用4个注解修饰该类
@SuppressWarnings(value = "unchecked")
@Deprecated
// 使用重复注解修饰该类
@Anno
@Anno
public class ClassTest {
	// 为该类定义一个私有的构造器
	private ClassTest() {
	}

	// 定义一个有参数的构造器
	public ClassTest(String name) {
		System.out.println("执行有参数的构造器");
	}

	// 定义一个无参数的info方法
	public void info() {
		System.out.println("执行无参数的info方法");
	}

	// 定义一个有参数的info方法
	public void info(String str) {
		System.out.println("执行有参数的info方法" + ",其str参数值:" + str);
	}

	// 定义一个测试用的内部类
	class Inner {
	}

	public static void main(String[] args) throws Exception {
		// 下面代码可以获取ClassTest对应的Class
		Class<ClassTest> clazz = ClassTest.class;
		// 获取该Class对象所对应类的全部构造器
		Constructor[] ctors = clazz.getDeclaredConstructors();
		System.out.println("ClassTest的全部构造器如下:");
		for (Constructor c : ctors) {
			System.out.println(c);
		}
		// 获取该Class对象所对应类的全部public构造器
		Constructor[] publicCtors = clazz.getConstructors();
		System.out.println("ClassTest的全部public构造器如下:");
		for (Constructor c : publicCtors) {
			System.out.println(c);
		}
		// 获取该Class对象所对应类的全部public方法
		Method[] mtds = clazz.getMethods();
		System.out.println("ClassTest的全部public方法如下:");
		for (Method md : mtds) {
			System.out.println(md);
		}
		// 获取该Class对象所对应类的指定方法
		System.out.println("ClassTest里带一个字符串参数的info()方法为:" + clazz.getMethod("info", String.class));
		// 获取该Class对象所对应类的上的全部注解
		Annotation[] anns = clazz.getAnnotations();
		System.out.println("ClassTest的全部Annotation如下:");
		for (Annotation an : anns) {
			System.out.println(an);
		}
		System.out.println("该Class元素上的@SuppressWarnings注解为:"
				+ Arrays.toString(clazz.getAnnotationsByType(SuppressWarnings.class)));
		System.out.println("该Class元素上的@Anno注解为:" + Arrays.toString(clazz.getAnnotationsByType(Anno.class)));
		// 获取该Class对象所对应类的全部内部类
		Class<?>[] inners = clazz.getDeclaredClasses();
		System.out.println("ClassTest的全部内部类如下:");
		for (Class c : inners) {
			System.out.println(c);
		}
		// 使用Class.forName方法加载ClassTest的Inner内部类
		Class inClazz = Class.forName("ClassTest$Inner");
		// 通过getDeclaringClass()访问该类所在的外部类
		System.out.println("inClazz对应类的外部类为:" + inClazz.getDeclaringClass());
		System.out.println("ClassTest的包为:" + clazz.getPackage());
		System.out.println("ClassTest的父类为:" + clazz.getSuperclass());
	}
}

API:java.lang.Class

使用反射生成并操作对象

Class 对象可以获得该类里的方法( 由 Method 对象表示)、 构造器( 由 Constructor 对象表示)、 成员变量( 由 Field 对象表示), 这三个类都位于 java.lang.reflect 包下, 并实现了 java.lang.reflect.Member接口。 程序可以通过 Method 对象来执行对应的方法, 通过 Constructor 对象来调用对应的构造器创建实例, 能通过 Field 对象直接访问并修改对象的成员变量值。

创建对象

通过反射来生成对象需要先使用 Class 对象获取指定的 Constructor 对象, 再调用 Constructor 对象的 newlnstance()方法来创建该 Class 对象对应类的实例。 通过这种方式可以选择使用指定的构造器来创建实例。

在很多 Java EE (例如Spring)框架中都需要根据配置文件信息来创建 Java 对象,从配置文件读取的只是某个类的字符串类名, 程序需要根据该字符串来创建对应的实例, 就必须使用反射。

下面程序就实现了一个简单的对象池, 该对象池会根据配置文件读取 key-value 对, 然后创建这些对象, 并将这些对象放入一个 HashMap 中:

import java.util.*;
import java.io.*;


public class ObjectPoolFactory {
	// 定义一个对象池,前面是对象名,后面是实际对象
	private Map<String, Object> objectPool = new HashMap<>();

	// 定义一个创建对象的方法
	// 该方法只要传入一个字符串类名,程序可以根据该类名生成Java对象
	private Object createObject(String clazzName) throws Exception, IllegalAccessException, ClassNotFoundException {
		// 根据字符串来获取对应的Class对象
		Class<?> clazz = Class.forName(clazzName);
		// 使用clazz对应类的默认构造器创建实例
		return clazz.getConstructor().newInstance();
	}

	// 该方法根据指定文件来初始化对象池
	// 它会根据配置文件来创建对象
	public void initPool(String fileName)
			throws InstantiationException, IllegalAccessException, ClassNotFoundException {
		try (FileInputStream fis = new FileInputStream(fileName)) {
			Properties props = new Properties();
			props.load(fis);
			for (String name : props.stringPropertyNames()) {
				// 每取出一对key-value对,就根据value创建一个对象
				// 调用createObject()创建对象,并将对象添加到对象池中
				objectPool.put(name, createObject(props.getProperty(name)));
			}
		} catch (Exception ex) {
			System.out.println("读取" + fileName + "异常");
		}
	}

	public Object getObject(String name) {
		// 从objectPool中取出指定name对应的对象
		return objectPool.get(name);
	}

	public static void main(String[] args) throws Exception {
		ObjectPoolFactory pf = new ObjectPoolFactory();
		pf.initPool("obj.txt");
		System.out.println(pf.getObject("a")); // ①
		System.out.println(pf.getObject("b")); // ②
	}
}

程序调用 Class 对象的 newlnstance()方法即可创建一个 Java 对象。 程序中的 initPool()方法会读取属性文件, 对属性文件中每个 key-value 对创建一个 Java 对象, 其中 value 是该 Java 对象的实现类, 而 key 是该 Java 对象放入对象池中的名字。 为该程序提供如下属性配置文件:

obj.txt

a=java.util.Date
b=javax.swing.JFrame

如果不想利用默认构造器来创建 Java 对象, 而想利用指定的构造器来创建 Java 对象, 则需要利用Constructor 对象, 每个 Constructor 对应一个构造器。 为了利用指定的构造器来创建 Java 对象, 需要如下三个步骤:

  • 获取该类的 Class 对象:
  • 利用 Class 对象的 getConstructor()方法来获取指定的构造器。
  • 调用 Constructor 的 newlnstance()方法来创建 Java 对象。

下面程序利用反射来创建一个 JFrame 对象, 而且使用指定的构造器:

import java.lang.reflect.*;

public class CreateJFrame {
	public static void main(String[] args) throws Exception {
		// 获取JFrame对应的Class对象
		Class<?> jframeClazz = Class.forName("javax.swing.JFrame");
		// 获取JFrame中带一个字符串参数的构造器
		Constructor ctor = jframeClazz.getConstructor(String.class);
		// 调用Constructor的newInstance方法创建对象
		Object obj = ctor.newInstance("测试窗口");
		// 输出JFrame对象
		System.out.println(obj);
	}
}

调用方法

当获得某个类对应的 Class 对象后, 就可以通过该 Class 对象的 getMethods()方法或者 getMethod()方法来获取全部方法或指定方法—这两个方法的返回值是 Method 数组, 或者 Method 对象。每个 Method 对象对应一个方法, 获得 Method 对象后, 程序就可通过该 Method 来调用它对应的方法。 在 Method 里包含一个 invoke()方法, 该方法的签名如下:

  • Object invoke(Object obj,Object...args): 该方法中的 obj 是执行该方法的主调, 后面的 args 是执行该方法时传入该方法的实参。

下面程序对前面的对象池工厂进行加强, 允许在配置文件中增加配置对象的成员变量的值, 对象池工厂会读取为该对象配置的成员变量值, 并利用该对象对应的 setter 方法设置成员变量的值:

import java.util.*;
import java.io.*;
import java.lang.reflect.*;


public class ExtendedObjectPoolFactory {
	// 定义一个对象池,前面是对象名,后面是实际对象
	private Map<String, Object> objectPool = new HashMap<>();
	private Properties config = new Properties();

	// 从指定属性文件中初始化Properties对象
	public void init(String fileName) {
		try (FileInputStream fis = new FileInputStream(fileName)) {
			config.load(fis);
		} catch (IOException ex) {
			System.out.println("读取" + fileName + "异常");
		}
	}

	// 定义一个创建对象的方法
	// 该方法只要传入一个字符串类名,程序可以根据该类名生成Java对象
	private Object createObject(String clazzName) throws Exception {
		// 根据字符串来获取对应的Class对象
		Class<?> clazz = Class.forName(clazzName);
		// 使用clazz对应类的默认构造器创建实例
		return clazz.getConstructor().newInstance();
	}

	// 该方法根据指定文件来初始化对象池
	// 它会根据配置文件来创建对象
	public void initPool() throws Exception {
		for (String name : config.stringPropertyNames()) {
			// 每取出一个key-value对,如果key中不包含百分号(%)
			// 这就表明是根据value来创建一个对象
			// 调用createObject创建对象,并将对象添加到对象池中
			if (!name.contains("%")) {
				objectPool.put(name, createObject(config.getProperty(name)));
			}
		}
	}

	// 该方法将会根据属性文件来调用指定对象的setter方法
	public void initProperty() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
		for (String name : config.stringPropertyNames()) {
			// 每取出一对key-value对,如果key中包含百分号(%)
			// 即可认为该key用于控制调用对象的setter方法设置值
			// %前半为对象名字,后半控制setter方法名
			if (name.contains("%")) {
				// 将配置文件中的key按%分割
				String[] objAndProp = name.split("%");
				// 取出调用setter方法的参数值
				Object target = getObject(objAndProp[0]);
				// 获取setter方法名:set + "首字母大写" + 剩下部分
				String mtdName = "set" + objAndProp[1].substring(0, 1).toUpperCase() + objAndProp[1].substring(1);
				// 通过target的getClass()获取它的实现类所对应的Class对象
				Class<?> targetClass = target.getClass();
				// 获取希望调用的setter方法
				Method mtd = targetClass.getMethod(mtdName, String.class);
				// 通过Method的invoke方法执行setter方法
				// 将config.getProperty(name)的值作为调用setter方法的参数
				mtd.invoke(target, config.getProperty(name));
			}
		}
	}

	public Object getObject(String name) {
		// 从objectPool中取出指定name对应的对象
		return objectPool.get(name);
	}

	public static void main(String[] args) throws Exception {
		ExtendedObjectPoolFactory epf = new ExtendedObjectPoolFactory();
		epf.init("extObj.txt");
		epf.initPool();
		epf.initProperty();
		System.out.println(epf.getObject("a"));
	}
}

为上面程序提供如下配置文件:

ext0bj.text

a=javax.swing.JFrame
b=javax.swing.JLabel
#set the title of a
a%title=Test Title

Spring 框架就是通过这种方式将成员变量值以及依赖对象等都放在配置文件中进行 ,管理的, 从而实现了较好的解耦。 这也是 Spring 框架的 IOC 的原理。

当通过Method的invoke()方法来调用对应的方法时,Java会要求程序必须有调用该方法的权限。 如果程序确实需要调用某个对象的private方法,则可以先调用Method对象的如下方法: * setAccessible(boolean flag):将Method对象的accessible设置为指定的布尔值。值为true,指示 该Method在使用时应该取消Java语言的访问权限检査;值为false,则指示该Method在使用时 要实施Java语言的访问权限检查。

API:java.lang.reflect.Method

访问成员变量值

通过 Class 对象的 getFields()或 getField()方法可以获取该类所包括的全部成员变量或指定成员变量。 Field 提供了如下两组方法来读取或设置成员变量值:

  • getXxx(Object obj): 获取 obj 对象的该成员变量的值。 此处的 Xxx 对应 8 种基本类型, 如果该成员变量的类型是引用类型, 则取消 get 后面的 Xxx。
  • setXxx(Object obj ,Xxx val): 将 obj 对象的该成员变量设置成 val 值。 此处的 Xxx 对 应 8 种 基 本类型, 如果该成员变量的类型是引用类型, 则取消 set 后面的 Xxx。

使用这两个方法可以随意地访问指定对象的所有成员变量, 包括 private 修饰的成员变量。

import java.lang.reflect.*;

class Person {
	private String name;
	private int age;

	public String toString() {
		return "Person[name:" + name + " , age:" + age + " ]";
	}
}

public class FieldTest {
	public static void main(String[] args) throws Exception {
		// 创建一个Person对象
		Person p = new Person();
		// 获取Person类对应的Class对象
		Class<Person> personClazz = Person.class;
		// 获取Person的名为name的成员变量
		// 使用getDeclaredField()方法表明可获取各种访问控制符的成员变量
		Field nameField = personClazz.getDeclaredField("name");
		// 设置通过反射访问该成员变量时取消访问权限检查
		nameField.setAccessible(true);
		// 调用set()方法为p对象的name成员变量设置值
		nameField.set(p, "Yeeku.H.Lee");
		// 获取Person类名为age的成员变量
		Field ageField = personClazz.getDeclaredField("age");
		// 设置通过反射访问该成员变量时取消访问权限检查
		ageField.setAccessible(true);
		// 调用setInt()方法为p对象的age成员变量设置值
		ageField.setInt(p, 30);
		System.out.println(p);
	}
}

API:java.lang.reflect.Field

操作数组

在 java.lang.reflect 包下还提供了一个 Array 类, Array 对象可以代表所有的数组。 程序可以通过使用 Array 来动态地创建数组, 操作数组元素等。

Array 提供了如下几类方法:

  • static Object newInstance(Class<?> componentType,int… length): 创建一个具有指定的元素类型、指定维度的新数组。
  • static xxx getXxx(Object array,int index): 返回 array 数组中第 index 个元素。 其中 xxx 是各种基本数据类型, 如果数组元素是引用类型, 则该方法变为 get(Object array,int index)。
  • static void setXxx(Object array,int index,xxx val): 将 array 数组中第 index 个元素的值设为 val。其中 XXX 是各种基本数据类型, 如果数组元素是引用类型, 则该方法变成 set(Object array, int index,Object val)。

下面程序示范了如何使用 Array 来生成数组, 为指定数组元素赋值, 并获取指定数组元素的方式:

import java.lang.reflect.*;

public class ArrayTest1 {
	public static void main(String args[]) {
		try {
			// 创建一个元素类型为String ,长度为10的数组
			Object arr = Array.newInstance(String.class, 10);
			// 依次为arr数组中index为5、6的元素赋值
			Array.set(arr, 5, "疯狂Java讲义");
			Array.set(arr, 6, "轻量级Java EE企业应用实战");
			// 依次取出arr数组中index为5、6的元素的值
			Object book1 = Array.get(arr, 5);
			Object book2 = Array.get(arr, 6);
			// 输出arr数组中index为5、6的元素
			System.out.println(book1);
			System.out.println(book2);
		} catch (Throwable e) {
			System.err.println(e);
		}
	}
}

API:java.lang.reflect.Array

使用反射生成 JDK 动态代理

代理分为静态代理和动态代理,静态代理是在编译时就将接口、实现类、代理类一股脑儿全部手动完成,但如果我们需要很多的代理,每一个都这么手动的去创建实属浪费时间,而且会有大量的重复代码,此时我们就可以采用动态代理,动态代理可以在程序运行期间根据需要动态的创建代理类及其实例,来完成具体的功能。

使用 Proxy 和 InvocationHandler 创建动态代理


代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。 这里用到编程中的一个思想:不要随意去修改别人已经写好的代码或者方法,如果需改修改,可以通过代理的方式来扩展该方法。 举个例子来说明代理的作用:假设我们想邀请一位明星,那么并不是直接连接明星,而是联系明星的经纪人,来达到同样的目的.明星就是一个目标对象,他只要负责活动中的节目,而其他琐碎的事情就交给他的代理人(经纪人)来解决.这就是代理思想在现实中的一个例子。

在这里插入图片描述
在这里插入图片描述

Proxy 提供了用于创建动态代理类和代理对象的静态方法, 它也是所有动态代理类的父类。 如果在程序中为一个或多个接口动态地生成实现类, 就可以使用 Proxy 来创建动态代理类; 如果需要为一个或多个接口动态地创建实例, 也可以使用 Proxy 来创建动态代理实例。

Java动态代理UML图

在这里插入图片描述
在这里插入图片描述

Proxy 提供了如下两个方法来创建动态代理类和动态代理实例:

  • static Class getProxyClass(ClassLoader loader,Class… interfaces): 创建一个动态代理类所对应的 Class 对象, 该代理类将实现 interfaces 所指定的多个接口。 第一个 ClassLoader 参数指定生成动态代理类的类加载器。
  • static Object newProxyInstance(ClassLoader loader,Class<?>[]interfaces,InvocationHandler h): 直接创建一个动态代理对象, 该代理对象的实现类实现了 interfaces 指定的系列接口, 执行代理对象的每个方法时都会被替换执行 InvocationHandler 对象的 invoke 方法。

实际上, 即使采用第一个方法生成动态代理类之后, 如果程序需要通过该代理类来创建对象, 依然需要传入一个 InvocationHandler 对象。 也就是说, 系统生成的每个代理对象都有一个与之关联的InvocationHandler 对象。

程序中可以采用先生成一个动态代理类, 然后通过动态代理类来创建代理对象的方式生成一个动态代理对象。 代码片段如下:

// 创建一个 InvocationHandler 对象
InvocationHandler handler = new MylnvocationHandler ( ...);
// 使 用 Proxy 生成一个动态代理类 proxyClass
Class proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader()
, new Class[] { Foo.class });
// 会取 proxyClass_中带一个
Constructor ctor = proxyClass. getConstructor(new Class[]
InvocationHandler 参数的构造器
{ InvocationHandler•class });
// 调用 ctor 的 newlnstance 方法来创建动态实例
Foo f (Foo)ctor.newlnstance(new Object[]{handler});

上面代码也可以简化成如下代码:

// 创建一个 InvocationHandler 对象
InvocationHandler handler = new MyInvocationHandler(...);
// 使用 Proxy 直接生成一个动态代理对象
Foo f = (Foo)Proxy.newProxyInstance(Foo.class.getClassLoader()
, new Class[]{Foo.class} , handler);

下面程序示范了使用 Proxy 和 InvocationHandler 来生成动态代理对象:

import java.lang.reflect.*;


interface Person {
	void walk();

	void sayHello(String name);
}

class MyInvokationHandler implements InvocationHandler {
	/*
	 * 执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法 其中: proxy:代表动态代理对象 method:代表正在执行的方法
	 * args:代表调用目标方法时传入的实参。
	 */
	public Object invoke(Object proxy, Method method, Object[] args) {
		System.out.println("----正在执行的方法:" + method);
		if (args != null) {
			System.out.println("下面是执行该方法时传入的实参为:");
			for (Object val : args) {
				System.out.println(val);
			}
		} else {
			System.out.println("调用该方法没有实参!");
		}
		return null;
	}
}

public class ProxyTest {
	public static void main(String[] args) throws Exception {
		// 创建一个InvocationHandler对象
		InvocationHandler handler = new MyInvokationHandler();
		// 使用指定的InvocationHandler来生成一个动态代理对象
		Person p = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class[] { Person.class },
				handler);
		// 调用动态代理对象的walk()和sayHello()方法
		p.walk();
		p.sayHello("孙悟空");
	}
}

上面程序首先提供了一个 Person 接口, 该接口中包含了 walk()和 sayHello()两个抽象方法, 接着定义了一个简单的 InvocationHandler 实现类, 定义该实现类时需要重写 invoke()方法—调用代理对象的所有方法时都会被替换成调用该 invoke()方法。 该 invoke()方法中的三个参数解释如下。

  • proxy:代表动态代理对象。
  • method: 代表正在执行的方法。
  • args: 代表调用目标方法时传入的实参。

API:java.lang.reflect.Proxy API:java.lang.reflect.InvocationHandler

动态代理和 AOP

根据前面介绍的 Proxy 和 InvocationHandler, 实在很难看出这种动态代理的优势。 下面介绍一种更实用的动态代理机制。

开发实际应用的软件系统时, 通常会存在相同代码段重复出现的情况, 在这种情况下, 对于许多刚开始从事软件开发的人而言, 他们的做法是: 选中那些代码, 一路“ 复制“ 、“ 粘贴”, 立即实现了系统功能, 如果仅仅从软件功能上来看, 他们确实己经完成了软件开发。通过这种“ 复制”、 “ 粘贴” 方式开发出来的软件如图a所示。

图 a:多个地方包含相同代码的软件

在这里插入图片描述
在这里插入图片描述

采用图 a所示结构实现的软件系统, 在软件开发期间可能会觉得无所谓, 但如果有一天需要修改程序的深色代码的实现, 则意味着打开三份源代码进行修改。 如果有 100 个地方甚至 1000 个地方使用了这段深色代码段, 那么修改、 维护这段代码的工作量将变成噩梦。

在这种情况下, 大部分稍有经验的开发者都会将这段深色代码段定义成一个方法, 然后让另外三段代码段直接调用该方法即可。 在这种方式下, 软件系统的结构如图 b 所示。

图 b:通过方法调用实现代码复用

在这里插入图片描述
在这里插入图片描述

对于如图 b 所示的软件系统, 如果需要修改深色部分的代码, 则只要修改一个地方即可, 而调用该方法的代码段, 不管有多少个地方调用了该方法, 都完全无须任何修改, 只要被调用方法被修改了,所有调用该方法的地方就会自然改变——通过这种方式, 大大降低了软件后期维护的复杂度。

但采用这种方式来实现代码复用依然产生一个重要问题: 代码段 1、 代码段 2、 代码段 3 和深色代码段分离开了, 但代码段 1、 代码段 2 和代码段 3 又和一个特定方法耦合了! 最理想的效果是: 代码块1、 代码块 2 和代码块 3 既可以执行深色代码部分, 又无须在程序中以硬编码方式直接调用深色代码的方法, 这时就可以通过动态代理来达到这种效果。

由于 JDK 动态代理只能为接口创建动态代理, 所以下面先提供一个 Dog 接口, 该接口代码非常简单, 仅仅在该接口里定义了两个方法。

public interface Dog {
	// info方法声明
	void info();

	// run方法声明
	void run();
}

上面接口里只是简单地定义了两个方法, 并未提供方法实现。 如果直接使用 Proxy 为该接口创建动态代理对象, 则动态代理对象的所有方法的执行效果又将完全一样。 实际情况通常是, 软件系统会为该Dog 接口提供一个或多个实现类。 此处先提供一个简单的实现类: GunDog:

public class GunDog implements Dog {
	// 实现info()方法,仅仅打印一个字符串
	public void info() {
		System.out.println("我是一只猎狗");
	}

	// 实现run()方法,仅仅打印一个字符串
	public void run() {
		System.out.println("我奔跑迅速");
	}
}

Dog 的实现类为每个方法提供了一个简单实现。 再看需要实现的功能: 让代码段 1、 代码段 2 和代码段 3 既可以执行深色代码部分, 又无须在程序中以硬编码方式直接调用深色代码的方法。 此处假设 info()、 run()两个方法代表代码段 1、 代码段 2, 那么要求: 程序执行 info()、 nm()方法时能调用某个通用方法, 但又不想以硬编码方式调用该方法。

下面提供一个 DogUtil类, 该类里包含两个通用方法:

public class DogUtil {
	// 第一个拦截器方法
	public void method1() {
		System.out.println("=====模拟第一个通用方法=====");
	}

	// 第二个拦截器方法
	public void method2() {
		System.out.println("=====模拟通用方法二=====");
	}
}

借助于 Proxy 和 InvocationHandler 就可以实现 当程序调用 info()方法和 run()方法时, 系统可以“自动” 将 methodl()和 method2()两个通用方法插入 info()和 run()方法中执行。

这个程序的关键在于下面的 MylnvokationHandler 类, 该类是一个 InvocationHandler 实现类, 该实现类的 invoke()方法将会作为代理对象的方法实现:

import java.lang.reflect.*;

public class MyInvokationHandler implements InvocationHandler {
	// 需要被代理的对象
	private Object target;

	public void setTarget(Object target) {
		this.target = target;
	}

	// 执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法
	public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
		DogUtil du = new DogUtil();
		// 执行DogUtil对象中的method1。
		du.method1();
		// 以target作为主调来执行method方法
		Object result = method.invoke(target, args);
		// 执行DogUtil对象中的method2。
		du.method2();
		return result;
	}
}

上面程序实现 invoke()方法时包含了一行关键代码, 这行代码通过反射以 target作为主调来执行 method 方法, 这就是回调了 target 对象的原有方法。 在粗体字代码之前调用 DogUtil对象的 methodl()方法, 在粗体字代码之后调用 DogUtil 对象的 method2()方法。

下面再为程序提供一个 MyProxyFactory 类, 该对象专为指定的 target 生成动态代理实例:

import java.lang.reflect.*;


public class MyProxyFactory {
	// 为指定target生成动态代理对象
	public static Object getProxy(Object target) throws Exception {
		// 创建一个MyInvokationHandler对象
		MyInvokationHandler handler = new MyInvokationHandler();
		// 为MyInvokationHandler设置target对象
		handler.setTarget(target);
		// 创建、并返回一个动态代理
		return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler);
	}
}

上面的动态代理工厂类提供了一个 getProxy()方法, 该方法为 target 对象生成一个动态代理对象,这个动态代理对象与 target 实现了相同的接口, 所以具有相同的 public 方法—从这个意义上来看, 动态代理对象可以当成 target 对象使用。 当程序调用动态代理对象的指定方法时,实际上将变为执行MylnvokationH andler 对象的 invoke()方法。

例如, 调用动态代理对象的 info()方法, 程序将幵始执行invoke()方法, 其执行步骤如下:

  • 创建 DogUtil 实例
  • 执行 DogUtil 实例的 method1()方法。
  • 使用反射以 target 作为调用者执行 info()方法。
  • 执行 DogUtil 实例的 method2()方法。

通过上面的执行过程可以发现: 当使用动态代理对象来代替 target 对象时, 代理对象的方法就实现了前面的要求—程序执行 info()、 nm()方法时既能“ 插入” methodl()、 method2()通用方法,但 GunDog 的方法中又没有以硬编码方式调用 methodl()和 method2()方法。

以一个主程序来测试这种动态代理的效果。

public class Test {
	public static void main(String[] args) throws Exception {
		// 创建一个原始的GunDog对象,作为target
		Dog target = new GunDog();
		// 以指定的target来创建动态代理
		Dog dog = (Dog) MyProxyFactory.getProxy(target);
		dog.info();
		dog.run();
	}
}

上面程序中的 dog 对象实际上是动态代理对象, 只是该动态代理对象也实现了 Dog 接口, 所以也可以当成 Dog 对象使用。 程序执行 dog 的 info()和 run()方法时, 实际上会先执行 DogUtil 的 method1()方法, 再执行 target 对象的 info()和 run()方法, 最后执行 DogUtil 的 method2()方法。

运行上面程序, 会看到如图 c 所示的运行结果:

图 c:运行结果

在这里插入图片描述
在这里插入图片描述

通过图 c 所示的运行结果来看, 可以发现采用动态代理可以非常灵活地实现解耦。 通常而言,使用 Proxy 生成一个动态代理时, 往往并不会凭空产生一个动态代理, 这样没有太大的实际意义。 通常都是为指定的目标对象生成动态代理。

这种动态代理在 AOP ( Aspect Orient Programming, 面向切面编程) 中被称为 AOP 代理, AOP 代理可代替目标对象, AOP 代理包含了目标对象的全部方法。 但 AOP 代理中的方法与目标对象的方法存在差异: AOP 代理里的方法可以在执行目标方法之前、 之后插入一些通用处理。

AOP 代理包含的方法与目标对象包含的方法示意图如图 d所示。

图d AOP 代理的方法与目标对象的方法示意图

在这里插入图片描述
在这里插入图片描述

CGLIB动态代理: JDK中提供的生成动态代理类的机制有个鲜明的特点是: 某个类必须有实现的接口,而生成的代理类也只能代理某个类接口定义的方法。 比如:如果上面例子的GunDog实现了继承自Dog接口的方法外,另外实现了方法eat(),则在产生的动态代理类中不会有这个方法了!更极端的情况是:如果某个类没有实现接口,那么这个类就不能用JDK产生动态代理了! 这时候就应该引入CGLIB动态代理了,“CGLIB(Code Generation Library),是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。” CGLIB创建某个类A的动态代理类的模式是: 1.查找A上的所有非final的public类型的方法定义; 2.将这些方法的定义转换成字节码; 3.将组成的字节码转换成相应的代理的class对象; 4.实现 MethodInterceptor接口,用来处理对代理类上所有方法的请求(这个接口和JDK动态代理InvocationHandler的功能和角色是一样的)

参考: 【1】:程序员圈子里,有哪些高颜值程序媛? 路人甲的回答 【2】:程序员圈子里,有哪些高颜值程序媛? 【3】:身边有个漂亮的女程序员是什么体验? 【3】:《疯狂Java讲义》 【4】:《深入理解Java虚拟机:JVM高级特性与最佳实践》 【5】:JVM内存模型——JAVA的根基 【6】:《揭秘Java虚拟机-JVM设计原理与实现》 【7】:纯洁的微笑:Jvm 系列(一):Java 类的加载机制 【8】:JAVA类加载机制全解析 【9】:廖雪峰的官方网站:Class类 【10】:Java技术驿站:Java基础系列-静态代理和动态代理 【11】:廖雪峰的官方网站:动态代理 【12】:Spring系列之IOC的原理及手动实现 【13】:菜鸟教程:工厂模式 【14】:犀利豆的博客:徒手撸框架--实现IOC 【15】:Java代理模式 【16】:Java的三种代理模式 【17】:从代理机制到Spring AOP

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-06-06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 类的加载、 连接和初始化
    • JVM 和类
      • 类的加载
        • 类的连接
          • 类的初始化
            • 类初始化的时机
            • 类加载器
              • 类加载机制
                • 创建并使用自定义的类加载器
                  • URLCIassLoader 类
                  • 通过反射查看类信息
                    • 获得 Class 对象
                      • 从 Class 中获取信息
                      • 使用反射生成并操作对象
                        • 创建对象
                          • 调用方法
                            • 访问成员变量值
                              • 操作数组
                              • 使用反射生成 JDK 动态代理
                                • 使用 Proxy 和 InvocationHandler 创建动态代理
                                  • 动态代理和 AOP
                                  相关产品与服务
                                  云数据库 MySQL
                                  腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
                                  领券
                                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档