引子:最近学了一下 Tomcat ,觉得自己有必要回顾一下 Java 反射机制,加深理解了。
一、反射知识的回顾
这里部分引用 李兴华的java se 实战经典中对 Java 反射的讲解。
反射之中包含了一个“反”的概念,所以要想解释反射就必须先从“正”开始解释,一般而言,当用户使用一个类的时候,应该先知道这个类,而后通过这个类产生实例化对象,但是“反”指的是通过对象找到类。
package cn.mldn.demo;
class Person {}
public class TestDemo {
public static void main(String[] args) throws Exception {
Person per = new Person() ; // 正着操作
System.out.println(per.getClass().getName()); // 反着来
}
}
以上的代码使用了一个getClass()
方法,而后就可以得到对象所在的“包.类”名称,这就属于“反”了,但是在这个“反”的操作之中有一个getClass()
就作为发起一切反射操作的开端。
Person 的父类是 Object 类,而上面所使用getClass()
方法就是 Object 类之中所定义的方法。
取得 Class 对象:public final Class<?> getClass()
。反射之中的所有泛型都定义为 ?,返回值都是 Object。
而这个 getClass()
方法返回的对象是 Class 类的对象,所以这个 Class 就是所有反射操作的源头。但是在讲解其真正使用之前还有一个需要先解释的问题,既然 Class 是所有反射操作的源头,那么这个类肯定是最为重要的,而如果要想取得这个类的实例化对象,Java 中定义了至少四种方式:
方式一:通过 Object 类的getClass()
方法取得,基本不用:因为这已经使用了new
关键字,使用反射的必要性不大了。
package cn.mldn.demo;
class Person {}
public class TestDemo {
public static void main(String[] args) throws Exception {
Person per = new Person() ; // 正着操作
Class<?> cls = per.getClass() ; // 取得Class对象
System.out.println(cls.getName()); // 反着来
}
}
方式二:使用类.class
取得,在日后学习 Hibernate 开发的时候使用
package cn.mldn.demo;
class Person {}
public class TestDemo {
public static void main(String[] args) throws Exception {
Class<?> cls = Person.class ; // 取得Class对象
System.out.println(cls.getName()); // 反着来
}
}
方式三:使用 Class 类内部定义的一个 static 方法,最多使用
取得 Class 类对象:public static Class<?> forName(String className) throws ClassNotFoundException;
package cn.mldn.demo;
class Person {}
public class TestDemo {
public static void main(String[] args) throws Exception {
Class<?> cls = Class.forName("cn.mldn.demo.Person") ; // 取得Class对象
System.out.println(cls.getName()); // 反着来
}
}
方式四:使用类加载器来加载相关类,得到Class<?>对象:
package cn.mldn.demo;
class Person {}
public class TestDemo {
public static void main(String[] args) throws Exception {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class<?> cls = cl.loadClass("cn.mldn.demo.Person") ; // 取得Class对象
System.out.println(cls.getName()); // 反着来
}
}
那么现在一个新的问题又来了,取得了 Class 类的对象有什么用处呢?对于对象的实例化操作之前一直依靠构造方法和关键字 new 完成,可是有了 Class 类对象之后,现在又提供了另外一种对象的实例化方法:
通过反射实例化对象:public T newInstance() throws InstantiationException, IllegalAccessException;
范例:通过反射实例化对象
package cn.mldn.demo;
class Person {
@Override
public String toString() {
return "Person Class Instance .";
}
}
public class TestDemo {
public static void main(String[] args) throws Exception {
Class<?> cls = Class.forName("cn.mldn.demo.Person") ; // 取得Class对象
Object obj = cls.newInstance() ; // 实例化对象,和使用关键字new一样
Person per = (Person) obj ; // 向下转型
System.out.println(per);
}
}
那么现在可以发现,对于对象的实例化操作,除了使用关键字 new 之外又多了一个反射机制操作,而且这个操作要比之前使用的 new 复杂一些,可是有什么用?
对于程序的开发模式之前一直强调:尽量减少耦合,而减少耦合的最好做法是使用接口,但是就算使用了接口也逃不出关键字 new,所以实际上 new 是造成耦合的关键元凶。
范例:回顾一下之前所编写的工厂设计模式
package cn.mldn.demo;
interface Fruit {
public void eat() ;
}
class Apple implements Fruit {
public void eat() {
System.out.println("吃苹果。");
};
}
class Factory {
public static Fruit getInstance(String className) {
if ("apple".equals(className)){
return new Apple() ;
}
return null ;
}
}
public class FactoryDemo {
public static void main(String[] args) {
Fruit f = Factory.getInstance("apple") ;
f.eat() ;
}
}
以上为之前所编写最简单的工厂设计模式,但是在这个工厂设计模式之中有一个最大的问题:如果现在接口的子类增加了,那么工厂类肯定需要修改,这是它所面临的最大问题,而这个最大问题造成的关键性的病因是 new
,那么如果说现在不使用关键字new
了,变为了反射机制呢?
反射机制实例化对象的时候实际上只需要“包.类”就可以,于是根据此操作,修改工厂设计模式。
package cn.mldn.demo;
interface Fruit {
public void eat() ;
}
class Apple implements Fruit {
public void eat() {
System.out.println("吃苹果。");
};
}
class Orange implements Fruit {
public void eat() {
System.out.println("吃橘子。");
};
}
class Factory {
public static Fruit getInstance(String className) {
Fruit f = null ;
try {
f = (Fruit) Class.forName(className).newInstance() ;
} catch (Exception e) {
e.printStackTrace();
}
return f ;
}
}
public class FactoryDemo {
public static void main(String[] args) {
Fruit f = Factory.getInstance("cn.mldn.demo.Orange") ;
f.eat() ;
}
}
发现,这个时候即使增加了接口的子类,工厂类照样可以完成对象的实例化操作,这个才是真正的工厂类,可以应对于所有的变化。如果单独从开发角度而言,与开发者关系不大,但是对于日后学习的一些框架技术这个就是它实现的命脉,在日后的程序开发上,如果发现操作的过程之中需要传递了一个完整的“包.类”名称的时候几乎都是反射机制作用。
上述工程模式提供的反射机制实际上也不算彻底,因为我们可以将所有对象以 Object 类型引用,通过反射的方法来调用对象的方法。下面就来看看 Tomcat 中的 Servlet 容器 CatalinaDaemon 中使用的反射机制实现原理。
二、CatalinaDaemon 实例对象构造过程中使用的反射
ClassLoader catalinaLoader = null;//先得到一个实例对象引用变量
catalinaLoader = createClassLoader("server", commonLoader);//得到卡特琳娜类加载器实例
/**
* 以下两个方法用于设置在特定情况下默认使用的类加载器
*/
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");//通过类加载器来加载一个类,返回 Class 对象
Object startupInstance = startupClass.getConstructor().newInstance();//通过 Class 对象得到构造器,然后在调用newInstance()方法,相当于调用了无参构造方法,返回一个实例对象
/**
* 以下方法用于设置父类加载器
*/
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
/**
* 以下方法用于得到一个 Catalina 实例对象,使用 Object 类变量引用它
*/
catalinaDaemon = startupInstance;
使用反射来进行方法调用的原因:
这里我们使用反证法来说明原因,假设使用一般的方法:instance.method0()
通过实例对象来调用方法的方式(假设我们都不知道可以使用反射来解决)。
上面已经提到了,我们得到一个 Catalina 实例对象,使用 Object 类变量引用它。那么,如果不进行向下转型,编译器中就只能调用父类 Object 的方法,这个显然不能满足要求。为了避免这个情况,我们可以使用强制类型转换,即向下转型。但是,这又引发了一个问题,我们之前引入反射机制一步一步得到 Class 对象、Object 引用的实例对象,其目的是为了不直接在代码中使用指定类型,这样有助于代码解耦,容易后续修改。如果使用强制类型转换就破坏了上面做的所有努力。
所以实际上,catalinaDaemon 实例对象的所有方法调用都是通过反射实现的。例如上个 code block 中提到的部分代码:
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
附上个人博客地址:https://jiangweijia.xyz