反射
1. Java反射机制概述
-
Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
-
加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射。
Java的反射机制就是为了体现动态的特性。
框架的底层源码会大量的使用反射。
框架 = 反射 + 注解 + 设计模式
-
Java反射机制提供的功能
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时获取泛型信息
- 在运行时调用任意一个对象的成员变量和方法
- 在运行时处理注解
- 生成
动态代理
(主要应用于Spring
的AOP代理模式
)
-
反射相关的主要API
java.lang.Class
:代表一个类 反射的源头java.lang.reflect.Method
:代表类的方法java.lang.reflect.Field
:代表类的成员变量java.lang.reflect.Constructor
:代表类的构造器
假如有一个Person类,在这个Person类外部,不可以通过Person类的对象调用其内部私有(private)
结构
但是通过反射,我们可以做到调用Person类的私有结构;比如:私有的构造器、方法、属性。
疑问1:通过直接new的方式或反射的方式都可以调用公共的结构,开发中到底用哪个?
建议:直接new的方式
简单举个例子:
比如现在有前端代码和后端代码,后端代码已经部署在服务器并处于运行状态,现在通过浏览器访问后台,访问有可能是登录功能,也有可能是注册功能等等,假如当前的一个url是localhost/login
,后端解析完发现是登录请求,这个时候我获取到的值是login
,此时我的后端就new一个login
的类对象,如果传进来的url是localhost/regist
,那么就造一个regist
的类对象,此时的操作都是动态的。
这个情景的实现特性就需要反射来做。
疑问2:反射机制与面向对象中的封装性是不是矛盾的?如何看待两个技术?
不矛盾。理论上看起来是矛盾的,封装性告诉我们出去之后私有的不要用,但反射还可以用,这不是白封装了吗?
对于一个类的封装性
,它其实在告诉我们,私有的方法就不要用了,因为私有方法是人家内部使用的方法,有可能这个私有方法已经被公有方法调用;
封装性
的体现就是:如果你需要调用 只调用公有方法即可,私有方法你用不到
。
对于单例模式
同样,有一个私有的构造器,其实在提示我们,你如果想new对象就不要
用这个私有的,我已经把对象造好了,你直接用就行。
那么对于反射
,有可能这个类提供的公有方法更好,比私有的完善很多,其实是建议你用公有方法
的,如果你非要用反射调用私有方法,也允许,但是违背
了这个类公有私有定义的意义。
对于单例模式
,我已经把这个对象提供给你了,既然是单例就意味着没有必要
造其它的对象,直接拿这个对象就可以完成项目的需求,如果你非要用反射调用私有的构造器,然后再造一个对象,你做的事其实跟它自己做的没有区别
,所以是一个多余
的操作。
封装性解决的是 建议你去调什么,而反射解决的是 我能不能调的问题。
2. 理解Class类并获取Class实例
- 关于
java.lang.Class
的理解
- 类的加载过程
程序经过javac.exe
命令后,会生成一个或多个字节码文件(.class结尾)
接着我们使用java.exe
命令对某个字节码文件进行解释运行,相当于某个字节码文件加载到内存中,此过程就成为类的加载。
加载到内存中的类,我们就称为运行时类,次运行时类,就作为Class
的一个实例。
- 换句话说,
Class
的实例就对应着一个运行时类。 - 加载到内存中的运行时类,会缓存一定的时间;在此时间之内,我们可以通过不同的方式来获取次运行时类,如下:
- 获取Class类的实例(四种方法)
- 通过类的
class属性获取
Class clazz1 = Person.class;
// 前提:已知具体的类
// 该方法最为安全可靠,程序性能最高 但体现不出反射的动态性
- 调用该实例的
getClass()
方法(Object类中的方法)获取Class对象
Person person = new Person();
Class clazz2 = person.getClass();
// 前提:已知某个类的实例
// 经常用于 判断这个对象所属的类
- 通过Class类的静态方法
forName()
获取,可能抛出ClassNotFoundException
Class clazz3 = Class.forName("全类名"); // 全类名例如:com.lyc.Person、java.lang.String
// 前提:已知一个类的全类名
// 常用 能更好的体现反射的动态性
- 通过类的加载器
ClassLoader classLoader = Person.class.getClassLoader();
Class class4 = classLoader.loadClass("全类名");
// 注意 我们自定义的类使用的基本都是 System Class Loader 系统类加载器
// 所以可以借用我们自定义的其他类的类加载器去加载类 生成一个Class对象
// 不常用 了解即可
3. 类的加载与ClassLoader的理解
- 类的加载过程
- 类加载器的作用
- 类加载器的分类
4. 创建运行时类的对象
public class Main {
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
Class<Person> clazz = Person.class;
/*
newInstance():调用此方法,创建对应的运行时类的对象,内部调用了运行时类的空参构造方法
要想此方法正常的创建运行时类的对象,要求:
1.运行时类必须提供空参的构造器
2.空参的构造器的访问权限要够 通常 设置为public
在javabean中要求提供一个public的空参构造器 原因:
1.便于通过反射,创建运行时类的对象
2.便于子类继承此运行时类,默认调用super()时,保证父类有此构造器
*/
Person p = clazz.newInstance();
System.out.println(p);
}
}
5. 获取运行时类的完整结构
- Class类的方法
getName()
获得全类名getSimpleName()
获得类名getPackage()
返回类的包对象getFields
返回本类和父类中声明为public的属性getMethods()
返回本类和父类中声明为public的方法getConstructors()
返回本类声明为public的构造器newInstance()
创建指定类的对象 调用的是类的无参构造器getDeclaredField(String fieldname)
通过属性名 获得属性对象getDeclaredFields()
只能获取本类中所有的属性,返回一个Field数组getDeclaredMethod(String methodName, Class ... argType)
根据方法名、参数列表获得一个方法对象getDeclaredMethods()
获得本类中所有的方法getDeclaredConstructor(Class ... argType)
根据构造器的参数列表获得一个构造器对象getDeclaredConstructors()
获得本类中的所有的构造器,返回Constructor数组getClassLoader()
获取当前类的类加载器getInterfaces()
获取当前类实现的接口(动态代理会用到)isXXX()
判断当前的Class对象的类型是否是某个类型toString()
输出当前Class对象的类型信息 + 全类名isAnnotationPresent(Class annotationClass)
判断某个类对象是否含有指定的注解
小 结:
在实际的操作中,取得类的信息的操作代码,并不会经常开发。
一定要熟悉
java.lang.reflect
包的作用,反射机制。如何取得属性、方法、构造器的名称,修饰符等。
6. 调用运行时类的指定结构
- 反射获取属性并使用
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) throws Exception {
Class clazz = Person.class;
// 1.创建运行时类的对象
Person p = (Person) clazz.newInstance();
// 2.获取指定变量名的属性
Field name = clazz.getDeclaredField("name");
// 3.设置为true 保证当前属性是可访问的(否则只可以对public操作 private等无法进行get set)
name.setAccessible(true);
// set() 设置当前属性的值 参数1:指明设置哪个对象的属性 参数2:将此属性值设置为多少
name.set(p, "xiao_cheng");
// get() 获取当前属性的值 参数1:获取哪个对象的当前属性值
String pName = (String) name.get(p);
System.out.println(pName);
}
}
- 反射获取方法并使用
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
Class clazz = Person.class;
// 1.创建运行时类的对象
Person p = (Person) clazz.newInstance();
// 2.获取指定的某个方法 getDeclaredMethod() 参数1:指明获取方法的名称 参数2:指明获取的方法的形参列表
Method show = clazz.getDeclaredMethod("show", String.class);
// 3.保证当前方法是可访问的
show.setAccessible(true);
// 调用invoke()使用方法 参数1:方法的调用者 参数2:给方法形参赋值的实参 invoke()的返回值就是被调用的方法的返回值
Object returnVal = show.invoke(p, "XIAO_CHENG");
System.out.println(returnVal);
// 获取并调用静态方法 private static void showDesc()
Method showDesc = clazz.getDeclaredMethod("showDesc");
showDesc.setAccessible(true);
Object returnValue = showDesc.invoke(Person.class);
System.out.println(returnValue); // 方法返回值为void 输出null
}
}
- 反射获取构造器并使用
import java.lang.reflect.Constructor;
// 不常用 最常用的还是上面的 Person p = clazz.newInstance();
public class Main {
public static void main(String[] args) throws Exception {
Class clazz = Person.class;
// 1.获取指定的构造器 getDeclaredConstructor() 参数:指明构造器的参数列表
Constructor constructor = clazz.getDeclaredConstructor(String.class);
// 2.保证构造器可访问
constructor.setAccessible(true);
// 3.调用此构造器创建运行时类的对象 newInstance()方法创建对象 参数:指明构造器的参数列表
Person p = (Person) constructor.newInstance("xiao_cheng");
System.out.println(p);
}
}
7. 反射的应用:动态代理
对于静态代理,每一个被代理类都需要一个代理类,这样一来程序开发中必然产生过多的代理;
那我们能不能写一个通用的代理类呢?当然这个类不可能在编译期间确定下来,因为我实现了一个类的接口,另一个类就无法使用了。
所以我们需要在运行期间创建代理类,这时就需要反射来获取被代理类的接口,实现接口创建对应的代理类。
最好可以通过一个代理类完成全部的代理功能 —— 动态代理
-
动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象。
-
动态代理使用场合:
-
调试
-
远程方法调用
-
-
动态代理相比于静态代理的优点:
- 抽象角色中(接口)声明的所有方法都被转移到调用处理器一个集中的方法中处理,这样,我们可以更加灵活和统一的处理众多的方法。
我们通过反射就是想写这些通用的功能。
动态代理简单实现
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/*
公共接口 代理类与被代理类都需要实现
method1: getBelief(): 获取信仰
method2: eat(): 吃
*/
interface Human {
String getBelief();
void eat(String food);
}
/*
被代理类
*/
class SuperMan implements Human {
@Override
public String getBelief() {
System.out.println("I am a superman!");
return "I am a superman!";
}
@Override
public void eat(String food) {
System.out.println("我喜欢吃" + food);
}
}
/*
要想实现动态代理,需要解决的问题
问题一:如何根据加载到内存中的被代理类,动态的创建一个代理类及其对象。
问题二:当通过代理类的对象调用方法a时,如何动态的去调用被代理类中的同名方法a。
*/
class ProxyFactory {
// 调用此方法 返回一个代理类的对象。解决问题一
public static Object getProxyInstance(Object obj) { // obj:被代理类的对象
MyInvocationHandler handler = new MyInvocationHandler();
handler.bind(obj); // 通过被代理对象赋值
// Proxy:专门完成代理的操作类,是所有动态代理类的父类,通过此类为一个或多个接口动态地生成实现类。
// newProxyInstance():创建一个代理类对象 参数1:被代理类的加载器 参数2:被代理类实现的接口 参数3:InvocationHandler接口
return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), handler);
}
}
class MyInvocationHandler implements InvocationHandler {
private Object obj; // 需要使用被代理类的对象进行赋值
public void bind(Object obj) {
this.obj = obj;
}
// 当我们通过代理类的对象,调用方法a时,就会自动的调用如下的方法:invoke()
// 将被代理类要执行的方法a的功能就声明在invoke()中
// invoke() 参数1:代理类对象 参数2:代理类对象调用的方法 参数3:同名方法的参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// method:即为代理类对象调用的方法 此方法也就作为了被代理类对象要调用的方法
// obj:被代理类的对象
Object returnValue = method.invoke(obj, args);
// 上述方法的返回值就作为当前类中的invoke()的返回值
return returnValue;
}
}
public class ProxyTest {
public static void main(String[] args) {
SuperMan superMan = new SuperMan();
Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);
// 当通过代理类对象调用方法时,会自动的调用被代理类中同名的方法
proxyInstance.getBelief();
proxyInstance.eat("麻辣烫");
}
}
代理类并没有在编译的时候显示,而是在运行时根据传进来的被代理类创建的,体现了反射的动态性。
-
动态代理与AOP(Aspect Orient Programming) 面向切面编程
-
使用Proxy生成一个动态代理时,往往并不会凭空产生一个动态代理,这样没有太大的意义。通常都是为指定的目标对象生成动态代理
-
这种动态代理在AOP中被称为AOP代理,AOP代理可代替目标对象,AOP代理包含了目标对象的全部方法。但AOP代理中的方法与目标对象的方法存在差异:AOP代理里的方法可以在执行目标方法之前、之后插入一些通用处理
-
比如我们有三个代码段都有一部分相同的代码,此时我们可以把相同代码写成方法A
,分别让这三个方法都调用方法A
:
此时就是我们AOP
的思想,面向切面编程,中间的方法是动态的,也就是不在三个代码块中把方法A
写死:
利用代码理解这个图片,对上方的代码稍加修改:
// 写一个类名为HumanUtil
class HumanUtil {
public void method1() {
System.out.println("================= 通用方法一 =================");
}
public void method2() {
System.out.println("================= 通用方法二 =================");
}
}
// 在invoke()方法添加两行HumanUtil类的通用方法
class MyInvocationHandler implements InvocationHandler {
private Object obj; // 需要使用被代理类的对象进行赋值
public void bind(Object obj) {
this.obj = obj;
}
// 当我们通过代理类的对象,调用方法a时,就会自动的调用如下的方法:invoke()
// 将被代理类要执行的方法a的功能就声明在invoke()中
// invoke() 参数1:代理类对象 参数2:代理类对象调用的方法 参数3:同名方法的参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
HumanUtil util = new HumanUtil();
util.method1(); // 通用方法一
// method:即为代理类对象调用的方法 此方法也就作为了被代理类对象要调用的方法
// obj:被代理类的对象
Object returnValue = method.invoke(obj, args);
util.method2(); // 通用方法二
// 上述方法的返回值就作为当前类中的invoke()的返回值
return returnValue;
}
}
通过控制台可以看出,我们中间的方法是动态的,这就是AOP(面向切面编程)的体现。
文章整理自:尚硅谷,让天下没有难学的技术