[关闭]
@king 2015-01-27T19:40:18.000000Z 字数 20522 阅读 5161

类加载机制与反射

Java


类的加载、连接和初始化


JVM和类

当我们调用Java命令运行某个Java程序时,该命令会启动一个Java虚拟机进程。
当系统出现以下几种情况时,JVM进程将被终止。

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

当Java程序运行结束时,JVM进程结束,该进程在内存中的状态将会丢失。


类的加载

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

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

类的加载由类加载器完成,类加载器通常由JVM提供。JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

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

  • 从本地文件系统加载class文件。
  • 从JAR包加载class文件
  • 通过网络加载class文件
  • 把一个Java源文件动态编译,并执行加载

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


类的连接

当类被加载后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类的连接又可以分为如下3个阶段

  • 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。
  • 准备:类准备阶段则负责为类的静态成员变量分配内存,并设置默认初始值。
  • 解析:将类的二进制数据中的符号引用替换成直接引用。

类的加载、连接和初始化

虚拟机负责对类进行初始化,主要就是对静态成员变量进行初始化。
JVM初始化一个类包含如下几个步骤:

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

类初始化的时机

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

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

除此之外,以下几种情形需要特别指出。
对于一个final型的静态成员变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个静态成员变量出现的地方替换成它的值,因此即使程序使用该静态成员变量,也不会导致该类的初始化。相当于使用常量
反之,如果final类型的静态成员变量的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态成员变量,则会导致该类被初始化。

当使用ClassLoader类的loadClass()方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。使用Class的forName()静态方法才会导致强制初始化该类。


类加载器

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


类加载器简介

类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会再次载入了。
在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但是在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。这意味着不同的类加载器ClassLoader实例加载的同包同名类是不同的,它们所加载的类也是完全不同、互不兼容的。

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

  • Bootstrap ClassLoader:根类加载器
  • Extension ClassLoader:扩展类加载器
  • System ClassLoader:系统类加载器

Bootstrap ClassLoader被称为引导(也称为原始或根)类加载器,它负责加载Java的核心类。在Sun的JVM中,当执行java.exe命令时,使用-Xbootclasspath选项或使用-D选项指定sun.boot.class.path系统属性值可以指定加载附加的类。
根类加载器非常特殊,它并不是java.lang.ClassLoader的子类,而是由JVM自身实现的。
以下代码可以获得根类加载器所加载的核心类库

  1. public class Test{
  2. public static void main(String[] args){
  3. //获取根类加载器所加载的全部URL数组
  4. URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
  5. // 遍历、输出根类加载器加载的全部URL
  6. for(int i = 0; i < urls.length; i++){
  7. System.out.println(urls[i].toExternalForm());
  8. }
  9. }
  10. }

Extension ClassLoader 被称为扩展类加载器,它负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext或者由java.ext.dirs系统属性指定的目录)中JAR包的类。通过这种方式可以为Java扩展核心类以外的新功能,把自己开发的类打包成JAR文件,然后放入JAVA_HOME/jre/lib/ext路径即可。

System ClassLoader 被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以系统类加载器作为父加载器。


类加载机制

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

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

类加载器之间的父子关系并不是类继承上的父子关系,而是类加载器实例之间的关系。

JVM 中4种类加载器的层次结构:根类加载器>扩展类加载器>系统类加载器>用户类加载器

  1. //获取系统类加载器
  2. ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
  3. System.out.println("系统类加载器:" + systemLoader);
  4. /**
  5. * 获取系统类加载器的加载路径——通常由CLASSPATH环境变量指定
  6. * 如果操作系统没有指定CLASSPATH环境变量,则默认以当前路径作为系统类加载器的加载路径
  7. */
  8. Enumeration<URL> eml = systemLoader.getResources("");
  9. while(eml.hasMoreElements()){
  10. System.out.println(eml.nextElement());
  11. }
  12. // 获取系统类加载器的父类加载器,得到扩展类加载器
  13. ClassLoader extensionLoader = systemLoader.getParent();
  14. System.out.println("扩展类加载器:" + extensionLoader);
  15. System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));
  16. System.out.println("扩展类加载器的parent:" + extensionLoader.getParent());

从运行结果可以看出,系统类加载器的加载路径是程序运行的当前路径,扩展类加载器的父加载器是null,并不是根类加载器,这是因为根类加载器并没有继承ClassLoader抽象类。实际上,扩展类加载器的父类加载器是根类加载器,只是根类加载器并不是Java实现的。

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

类加载器加载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()方法来实现自己的载入过程。


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

JVM中除根类加载器之外的所有类都是ClassLoader子类的实例,开发者可以扩展ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。
ClassLoader有如下两个关键方法:

  • loadClass(String name, boolean resolve):使用指定的二进制名称来加载类。系统调用ClassLoader的该方法来获取指定类对应的Class对象。
  • findClass(String name):使用指定的二进制名称查找类。

自定义ClassLoader可以选择性重写上述两个方法。

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

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

从上面步骤中可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略;如果重写loadClass()方法,则实现逻辑更为复杂。
ClassLoader类里还有一个核心方法:defineClass(String name, byte[] b, int off, int len),将指定类的字节码文件(.class文件)读入字节数组byte[] b内,并把它转换为Class对象,该字节码文件可以来源于文件、网络等。

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

  1. package test;
  2. import java.io.File;
  3. import java.io.FileInputStream;
  4. import java.io.IOException;
  5. import java.lang.reflect.Method;
  6. public class CompileClassLoader extends ClassLoader {
  7. //读取一个文件的内容
  8. private byte[] getBytes(String filename) throws IOException{
  9. File file = new File(filename);
  10. long len = file.length();
  11. byte[] raw = new byte[(int)len];
  12. try(FileInputStream fin = new FileInputStream(file)){
  13. //一次读取Class文件的全部二进制数据
  14. int r = fin.read(raw);
  15. if (r != len){
  16. throw new IOException("无法读取全部文件" + r + " != " + len);
  17. }
  18. return raw;
  19. }
  20. }
  21. // 定义编译指定Java文件的方法
  22. private boolean compile(String javaFile) throws IOException{
  23. System.out.println("CompileClassLoader:正在编译" + javaFile + "...");
  24. // 调用系统的javac命令
  25. Process p = Runtime.getRuntime().exec("javac " + javaFile);
  26. try{
  27. //当前线程等待p所表示的进程执行完成
  28. p.waitFor();
  29. } catch (InterruptedException e){
  30. e.printStackTrace();
  31. }
  32. // 获取javac线程的退出值
  33. int ret = p.exitValue();
  34. // 返回编译是否成功
  35. return ret == 0;
  36. }
  37. // 重写ClassLoader的findClass方法
  38. protected Class<?> findClass(String name) throws ClassNotFoundException{
  39. Class clazz = null;
  40. // 将包路径中的点替换成斜线
  41. String fileStub = name.replace(".", "/");
  42. String javaFilename = fileStub + ".java";
  43. String classFilename = fileStub + ".class";
  44. File javaFile = new File(javaFilename);
  45. File classFile = new File(classFilename);
  46. //当指定Java源文件存在,且Class文件不存在,
  47. //或者Java源文件的修改时间比Class文件的修改时间更晚时,重新编译
  48. if (javaFile.exists() && (!classFile.exists()
  49. || javaFile.lastModified() > classFile.lastModified())){
  50. try{
  51. //如果编译失败,或者该Class文件不存在
  52. if (!compile(javaFilename) || !classFile.exists()){ //这一步compile重新编译了一次
  53. throw new ClassNotFoundException("ClassNotFoundException:" + javaFilename);
  54. }
  55. } catch (IOException e){
  56. e.printStackTrace();
  57. }
  58. }
  59. // 如果Class文件存在,系统负责将该文件转换成Class对象
  60. if (classFile.exists()){
  61. try{
  62. //将Class文件的二进制数据读入数组
  63. byte[] raw = getBytes(classFilename);
  64. // 调用ClassLoader的defineClass方法将二进制数据成Class对象
  65. clazz = defineClass(name, raw, 0, raw.length);
  66. }catch(IOException e){
  67. e.printStackTrace();
  68. }
  69. }
  70. //如果clazz为null,则表明加载失败,抛出异常
  71. if (clazz == null){
  72. throw new ClassNotFoundException(name);
  73. }
  74. return clazz;
  75. }
  76. public static void main(String[] args) throws Exception {
  77. //如果运行该程序时没有参数,即没有目标类
  78. if (args.length < 1){
  79. System.out.println("缺少目标类,请按如下格式运行源文件:");
  80. System.out.println("java CompileClassLoader ClassName");
  81. }
  82. // 第一个参数是需要运行的类
  83. String progClass = args[0];
  84. // 剩下的参数作为运行目标类时的参数
  85. // 将这些参数复制到一个新数组中
  86. String[] progArgs = new String[args.length-1];
  87. System.arraycopy(args, 1, progArgs, 0, progArgs.length);
  88. CompileClassLoader ccl = new CompileClassLoader();
  89. // 加载需要运行的类
  90. Class<?> clazz = ccl.loadClass(progClass);
  91. // 获取需要运行的类的主方法
  92. Method main = clazz.getMethod("main", (new String[0]).getClass());
  93. Object argsArray[] = {progArgs};
  94. main.invoke(null, argsArray);
  95. }
  96. }

再随便写一个类,就可以用java CompileClassLoader 类名 参数的方式来运行它,不必编译。

上例比较简单,实际上,使用自定义的类加载器,可以实现如下常见功能:

  • 执行代码前自动验证数字签名
  • 根据用户提供的密码解密代码, 从而可以实现代码混淆器来避免反编译Class文件
  • 根据用户需求来动态地加载类
  • 根据应用需求把其他数据以字节码的形式加载到应用中

URLClassLoader类

该类是系统类加载器和扩展类加载器的父类(此处指的是类与类之间的继承关系)。它既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类

应用程序中可以直接使用URLClassLoader来加载类。一旦得到URLClassLoader对象之后,就可以调用该对象的loadClass()方法来加载指定类。

下面程序示范了如何直接从文件系统中加载MySQL驱动,并使用该驱动来获取数据库连线。通过这种方式来获取数据库连接,可以无须将MySQL驱动添加到CLASSPATH环境变量中。

  1. package test;
  2. import java.net.URL;
  3. import java.net.URLClassLoader;
  4. import java.sql.Connection;
  5. import java.sql.Driver;
  6. import java.util.Properties;
  7. public class URLClassLoaderTest {
  8. private static Connection conn;
  9. //定义一个获取数据库连接的方法
  10. public static Connection getConn(String url, String user, String pass) throws Exception{
  11. if(conn == null){
  12. //创建一个URL数组
  13. URL[] urls = {new URL("file:mysql-connector-java-3.1.10-bin.jar")};
  14. // 以默认的ClassLoader作为父ClassLoader,创建URLClassLoader
  15. URLClassLoader myClassLoader = new URLClassLoader(urls);
  16. // 加载MySQL的JDBC驱动,并创建默认实例
  17. Driver driver = (Driver)myClassLoader.loadClass("com.mysql.jdbc.Driver").newInstance();
  18. // 创建一个设置JDBC连接属性的Properties对象
  19. Properties props = new Properties();
  20. // 至少需要为该对象传入user和password两个属性
  21. props.setProperty("user", user);
  22. props.setProperty("password", pass);
  23. // 调用Driver对象的connect方法来取得数据库连接
  24. conn = driver.connect(url, props);
  25. }
  26. return conn;
  27. }
  28. public static void main(String[] args) throws Exception {
  29. System.out.println(getConn("jdbc:mysql://localhost:3306/mysql", "root", "32147"));
  30. }
  31. }

创建URLClassLoader时传入一个URL数组参数,该ClassLoader就可以从这系列URL指定的资源中加载指定类,这里的URL可以以file:为前缀,表明从本地文件系统加载;可以以http:为前缀,表明从互联网通过HTTP访问来加载;也可以以ftp:为前缀,表明从互联网通过FTP访问来加载……


通过反射查看类信息

Java程序中许多对象在运行时都会出现两种类型:编译时类型和运行时类型。
例如Person p = new Student()这行代码将会生成一个p变量,编译时类型为Person,运行时类型为Student。
再如程序在运行时接收到外部传入的一个对象,该对象的编译时类型是Object,但程序又需要调用该对象运行时类型的方法。
为解决这些问题,程序需要在运行时发现对象和类的真实信息。

  • 第一种做法是假设在编译和运行时都完全知道类型的具体信息。直接先使用instanceof运行符进行判断,再强制类型转换
  • 第二种做法是编译时根本无法预知该对象和类可能属于哪些类,程序只能依靠运行时信息来发现其真实信息,这时必须使用反射。

获得Class对象

程序中获得Class对象通常有3种方式:

  • Class类的forName()方法,需要传入类名(必须添加完整包名)
  • 调用某个类的class属性
  • 调用某个对象的getClass()方法

第二种方式代码更安全(编译期就能检查),程序性能更好(无须调用方法)


从Class中获取信息

获取Class类所包含的构造器:API中Class类的getXXXConstructor方法。
方法、注释、内部类、成员变量等同理

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

  1. package king.reflect;
  2. import java.lang.annotation.Annotation;
  3. import java.lang.reflect.Constructor;
  4. import java.lang.reflect.Method;
  5. // 使用两个注释修饰该类
  6. @SuppressWarnings(value = "unchecked")
  7. @Deprecated
  8. public class ClassTest {
  9. private ClassTest(){}
  10. public ClassTest(String name){
  11. System.out.println("执行有参数构造器");
  12. }
  13. public void info(){
  14. System.out.println("执行无参info方法");
  15. }
  16. public void info(String str){
  17. System.out.println("执行有参数的info方法,其str参数值:" + str);
  18. }
  19. class Inner{}
  20. public static void main(String[] args) throws Exception {
  21. // 获取Class
  22. Class<ClassTest> clazz = ClassTest.class;
  23. // 获取该Class对象所对应类的全部构造器
  24. Constructor[] ctors = clazz.getDeclaredConstructors();
  25. System.out.println("ClassTest的全部构造器如下:");
  26. for(Constructor c : ctors){
  27. System.out.println(c);
  28. }
  29. // 获取该Class对象对应类的全部public方法
  30. Method[] mtds = clazz.getMethods();
  31. System.out.println("ClassTest的全部public方法如下:");
  32. for (Method md : mtds){
  33. System.out.println(md);
  34. }
  35. // 获取该Class对象所对应类的指定方法
  36. System.out.println("Class里带一个字符串参数的info方法为:"
  37. + clazz.getMethod("info", String.class));
  38. // 获取该Class对象对应类的全部注释
  39. Annotation[] anns = clazz.getAnnotations();
  40. System.out.println("ClassTest的全部Annotation如下:");
  41. for(Annotation an : anns){
  42. System.out.println(an);
  43. }
  44. System.out.println("该Class元素上的@SuppressWarnings注释为:"
  45. + clazz.getAnnotation(SuppressWarnings.class));
  46. // 获取该Class对象所对应类的全部内部类
  47. Class<?>[] inners = clazz.getDeclaredClasses();
  48. System.out.println("ClassTest的全部内部类如下:");
  49. for (Class c : inners){
  50. System.out.println(c);
  51. }
  52. // 加载内部类
  53. Class inClazz = Class.forName("king.reflect.ClassTest$Inner");
  54. // 通过getDeclaringClass()访问外部类
  55. System.out.println("inClazz对应的外部类为"
  56. + inClazz.getDeclaringClass());
  57. System.out.println("ClassTest的包为:" + clazz.getPackage());
  58. System.out.println("ClassTest的父类为:" + clazz.getSuperclass());
  59. }
  60. }

使用反射生成并操作对象


创建对象

一是使用Class对象的newInstance()调用默认构造器
二是使用Class对获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建该Class对象对应类的实例。

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

  1. package king.reflect;
  2. import java.io.FileInputStream;
  3. import java.io.IOException;
  4. import java.util.HashMap;
  5. import java.util.Map;
  6. import java.util.Properties;
  7. public class ObjectPoolFactory {
  8. // 对象池,对象名+对象
  9. private Map<String, Object> objectPool = new HashMap<>();
  10. // 定义一个创建对象的方法
  11. private Object createObject(String clazzName) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
  12. // 根据字符串来获取对应的Class对象
  13. Class<?> clazz = Class.forName(clazzName);
  14. // 使用默认构造器创建实例
  15. return clazz.newInstance();
  16. }
  17. // 根据指定文件来初始化对象池,根据配置文件创建对象
  18. public void initPool(String fileName) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
  19. try(FileInputStream fis = new FileInputStream(fileName)){
  20. Properties props = new Properties();
  21. props.load(fis);
  22. for (String name : props.stringPropertyNames()){
  23. // 每取出一对key-value对,就根据value创建一个对象
  24. // 调用 createObject() 创建对象,并将对象添加到对象池中
  25. objectPool.put(name, createObject(props.getProperty(name)));
  26. }
  27. } catch (IOException ex){
  28. System.out.println("读取" + fileName + "异常");
  29. }
  30. }
  31. public Object getObject(String name){
  32. return objectPool.get(name);
  33. }
  34. public static void main(String[] args)
  35. throws ClassNotFoundException, InstantiationException, IllegalAccessException {
  36. ObjectPoolFactory pf = new ObjectPoolFactory();
  37. pf.initPool("obj.txt");
  38. System.out.println(pf.getObject("a"));
  39. System.out.println(pf.getObject("b"));
  40. }
  41. }

Spring 采用这种方式大大简化了Java EE 应用的开发

如果想利用指定的构造器来创建Java对象:

  1. // 获取Class对象
  2. Class<?> jframeClazz = Class.forName("javax.swing.JFrame");
  3. // 获取JFrame中带一个字符串参数的构造器
  4. Constructor ctor = jframeClazz.getConstructor(String.class);
  5. // 调用Constructor 的newInstance方法创建对象
  6. Object obj = ctor.newInstance("测试窗口");

调用方法

获取Method对象后,调用它的invoke()方法。

下面对上述对象池工厂进行加强,允许在配置文件中增加配置对象的属性值。

  1. package king.reflect;
  2. import java.io.FileInputStream;
  3. import java.io.IOException;
  4. import java.lang.reflect.InvocationTargetException;
  5. import java.lang.reflect.Method;
  6. import java.util.HashMap;
  7. import java.util.Map;
  8. import java.util.Properties;
  9. public class ExtendedObjectPoolFactory {
  10. // 对象池
  11. private Map<String, Object> objectPool = new HashMap<>();
  12. private Properties config = new Properties();
  13. // 从指定属性文件中初始化Properties对象
  14. public void init(String fileName){
  15. try(FileInputStream fis = new FileInputStream(fileName)){
  16. config.load(fis);
  17. } catch (IOException e){
  18. System.out.println("读取" + fileName + "异常");
  19. }
  20. }
  21. // 定义一个创建对象的方法
  22. private Object createObject(String clazzName) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
  23. // 根据字符串来获取对应的Class对象
  24. Class<?> clazz = Class.forName(clazzName);
  25. // 使用默认构造器创建实例
  26. return clazz.newInstance();
  27. }
  28. // 根据指定文件来初始化对象池
  29. public void initPool() throws ClassNotFoundException, InstantiationException, IllegalAccessException{
  30. for (String name : config.stringPropertyNames()){
  31. // 每取出一个key-value对,如果key中不包含百分号,则表达是根据value来创建一个对象
  32. // 调用createdObject创建对象,并添加到对象池中
  33. if (!name.contains("%")){
  34. objectPool.put(name, createObject(config.getProperty(name)));
  35. }
  36. }
  37. }
  38. // 根据指定文件来初始化对象中的对象
  39. public void initProperty() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException{
  40. for (String name : config.stringPropertyNames()){
  41. // 每取出一个key-value对,如果key中包含百分号,说明是为对象变量设置值
  42. // %前半为对象名字,后半为变量名
  43. if (name.contains("%")){
  44. String[] objAndProp = name.split("%"); // 分成对象名+变量名
  45. // 取出需要设置值的目标对象
  46. Object target = getObject(objAndProp[0]);
  47. // 该变量对应setter方法名:set + "属性的首字母大写" + 剩下的部分
  48. String mtdName = "set" + objAndProp[1].substring(0, 1).toUpperCase()
  49. + objAndProp[1].substring(1);
  50. // 通过target的getClass()获取它的实现类所对应的Class对象
  51. Class<?> targetClass = target.getClass();
  52. // 获取该属性对应的setter方法
  53. Method mtd = targetClass.getMethod(mtdName, String.class);
  54. // 通过Method的invoke方法执行setter方法
  55. // 对应的变量放在config中
  56. mtd.invoke(target, config.getProperty(name));
  57. }
  58. }
  59. }
  60. public Object getObject(String name){
  61. return objectPool.get(name);
  62. }
  63. /**
  64. * @param args
  65. */
  66. public static void main(String[] args)
  67. throws Exception{
  68. ExtendedObjectPoolFactory epf = new ExtendedObjectPoolFactory();
  69. epf.init("extObj.txt");
  70. epf.initPool();
  71. epf.initProperty();
  72. System.out.println(epf.getObject("a"));
  73. }
  74. }

通过Method的invoke()方法来调用方法时必须有相应权限,如果需要调用某个对象的private方法,则必须先用Method的父类AccessibleObject的setAccessible(true)方法,取消权限检查。同样,Constructor,Field都可调用该方法


访问属性值

使用getXxx或get方法来获取属性值,设置也类似

  1. Person p = new Person();
  2. Class<Person> personClazz = Person.class;
  3. // 获取Person的名为name的变量
  4. // 使用getDeclaredField,可获取各种访问控制符的变量
  5. Field nameField = personClazz.getDeclaredField("name");
  6. // 设置通过反射访问该变量时取消权限检查
  7. nameField.setAccessible(true);
  8. nameField.set(p, "King");

操作数组

java.lang.reflect.Array类。

  1. // 创建长度为10的String数组
  2. Object arr = Array.newInstance(String.class, 10);
  3. // 赋值
  4. Array.set(arr, 5, "king");
  5. Array.set(arr, 6, "java");
  6. // 取出
  7. Object book1 = Array.get(arr, 5);

使用反射生成JDK动态代理

java.lang.reflect包下的Proxy类和 InvocationHandler 接口,可以生成JDK动态代理类或动态代理对象


使用Proxy和InvocationHandler创建动态代理

Proxy 提供了用于创建动态代理类和代理对象的静态方法。它也是所有动态代理类的父类。

  • static Class getProxyClass(ClassLoader loader, Class... interfaces):返回代理类的 java.lang.Class 对象,并向其提供类加载器和接口数组。
  • static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h):返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。

就算采用第一个方法获取一个动态代理类,当程序需要通过该代理类来创建对象时一样需要传入一个 InvocationHandler 对象。也就是说每个代理对象都有一个与之关联的 InvocationHandler 对象。

当执行动态代理对象里的方法时,实际上会替换成调用InvocationHandler对象的invoke方法。

可以先生成一个动态代理类,然后创建对象

  1. // 创建一个InvocationHandler对象
  2. InvocationHandler handler = new MyInvocationHandler(...);
  3. // 使用Proxy生成一个动态代理类proxyClass
  4. Class proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), new Class[]{Foo.class});
  5. // 获取 proxyClass 类中一带一个InvocationHandler 参数的构造器
  6. Constructor ctor = proxyClass.getConstructor(new Class[] {InvocationHandler.class});
  7. // 创建实例
  8. Foo f = (Foo) ctor.newInstance(new Object[]{handler});

简化如下:

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

完整示例:

  1. package king.reflect;
  2. import java.lang.reflect.InvocationHandler;
  3. import java.lang.reflect.Method;
  4. import java.lang.reflect.Proxy;
  5. interface Person{
  6. void walk();
  7. void sayHello(String name);
  8. }
  9. public class MyInvokationHandler implements InvocationHandler {
  10. /*
  11. * 执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法
  12. * 其中:
  13. * proxy:代表动态代理对象
  14. * method:代表正在执行的方法
  15. * args:代表调用目标方法时传入的实参
  16. */
  17. @Override
  18. public Object invoke(Object proxy, Method method, Object[] args)
  19. throws Throwable {
  20. System.out.println("-----正在执行的方法:" + method);
  21. if (args != null){
  22. System.out.println("下面是执行方法时传入的实参");
  23. for (Object val : args){
  24. System.out.println(val);
  25. }
  26. } else {
  27. System.out.println("该用该方法没有实参!");
  28. }
  29. return null;
  30. }
  31. public static void main(String[] args)throws Exception {
  32. // 创建一个InvocationHandler对象
  33. InvocationHandler handler = new MyInvokationHandler();
  34. // 生成动态代理对象
  35. Person p = (Person)Proxy.newProxyInstance(Person.class.getClassLoader()
  36. , new Class[]{Person.class}, handler);
  37. // 调用动态代理对象的walk()和sayHello()方法
  38. p.walk();
  39. p.sayHello("孙悟空");
  40. }
  41. }

定义InvocationHandler实现类时需要重写invoke()方法,调用代理对象的所有方法时都会被替换成调用该invoke()方法。

普通编程过程中无须使用动态代理,但在编写框架或底层基础代码时,动态代理的作用就非常大。


动态代理和AOP

如果既要多个类调用同一个方法,又无须在程序中以硬编码的方式直接调用,就可以通过动态代理来达到这种效果。

JDK 动态代理只能为接口创建动态代理。下面先提供一个接口

  1. public interface Dog{
  2. void info();
  3. void run();
  4. }

如果直接使用Proxy为该接口创建动态代理对象,则所有方法的执行效果又将完全一样。这时先为该Dog接口提供一个简单的实现类

  1. public class GunDog implements Dog{
  2. public void info(){
  3. System.out.println("我是一只猎狗");
  4. }
  5. public void run(){
  6. System.out.println("我奔跑迅速");
  7. }
  8. }

要求info()、run()两个方法即能调用某个通用的方法,又不想以硬编码方式调用该方法。下面提供一个DogUtil类,该类包含两个通用方法:

  1. public class DogUtil{
  2. public void method1(){
  3. System.out.println("======模拟第一个通用方法=========");
  4. }
  5. public void method2(){
  6. System.out.println("==============模拟通用方法二==========");
  7. }
  8. }

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

  1. public class MyInvokationHandler implements InvocationHandler{
  2. // 需要被代理的对象
  3. private Object target;
  4. public void setTarget(Object target){
  5. this.target = target;
  6. }
  7. // 执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法。
  8. public Object invoke (Object proxy, Method method, Object[] args) throws Exception{
  9. DogUtil du = new DogUtil();
  10. // 执行DogUtil对象中的method1方法
  11. du.method();
  12. // 以target为主调来执行method方法
  13. Object result = method.invoke(target, args);
  14. // 执行DogUtil对象中的method2方法
  15. du.method2();
  16. return result;
  17. }
  18. }

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

  1. public class MyProxyFactory{
  2. // 为指定的target生成动态代理对象
  3. public static Object getProxy(Object target) throws Exception{
  4. MyInvokationHandler handler = new MyInvokationHandler();
  5. // 设置target对象
  6. handler.setTarget(target);
  7. return Proxy.newProxyInstance(target.getClass().getClassLoader()
  8. , target.getClass().getInterfaces(), handler);
  9. }
  10. }

上述代理工厂类提供了一个getProxy()方法,该方法为target对象生成一个动态代理对象,实现了与target相同的接口,可以当成target对象来使用。

  1. public class Test{
  2. public static void main(String[] args) throws Exception{
  3. Dog target = new GunDog();
  4. Dog dog = (Dog)MyProxyFactory.getProperty(target);
  5. dog.info();
  6. dog.run();
  7. }
  8. }

采用动态代理可以非常灵活地实现解耦。
AOP (Aspect Orient Programming, 面向切面编程)。
这种动态代理在AOP中被称为AOP代理,AOP代理可替代目标对象,AOP代理包含了目标对象的全部方法,但AOP代理的方法里可以在执行目标方法之前、之后插入一些通用处理。


反射和泛型

String.class的类型实际上是Class,如果Class对应的类暂时未知,则使用Class。通过在反射中使用泛型,可以避免使用反射生成的对象需要强制类型转换。

  1. public static <T> T getInstance(Class<T> cls){
  2. return cls.netInstance();
  3. }

使用反射来获取泛型信息

获取成员变量的类型:

  1. Class<?> a = f.getType();

这只对普通类型的变量有效。
为获得指定变量的泛型类型,应先使用如下方法来获取指定变量的泛型类型

  1. Type gType = f.getGenericType();

然后将Type对象强制类型转换为 ParameterizedType 对象, ParameterizedType 代表被参数化的类型,也就是增加了泛型限制的类型。
ParameterizedType的方法:

  • Type[] getActualTypeArguments():返回泛型参数的原型
  • Type getOwnerType():返回 Type 对象,表示此类型是其成员之一的类型。
  • Type getRawType():返回无泛型信息的原始类型
  1. Class<GenericTest> clazz = GenericTest.class;
  2. Field f = clazz.getDeclaredField("score");
  3. Class<?> a = f.getType(); // 只对普通类型的变量有效
  4. Type gType = f.getGenericType();
  5. if (gType instanceof ParameterizedType){
  6. ParameterizedType pType = (ParameterizedType) gType;
  7. Type rType = pType.getRawType(); // 原始类型
  8. Type[] tArgs = pType.getActualTypeArguments(); //泛型类型的泛型参数
  9. for (int i = 0; i < tArgs.length; i++){
  10. // 输出
  11. ...
  12. }
  13. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注