1. 反射
Spring框架中也大量使用了动态代理,而动态代理的实现依赖于反射。
引入:(请看完有助于你理解)
在程序运行期间,系统始终为所有对象维护一个运行时类型标识。这个信息会跟踪每个对象所属的类。(一个类可能有多个运行时类型标识跟踪)可以使用一个特殊的Java类访问这些信息。保存这些信息的类名叫Class
Class对象实际上描述的是一个类型,这可能是类,也可能不是类。例如,int不是类,但int.Class确实是一个Class类型的对象
注意:虚拟机为每个类型管理一个唯一的Class对象(一个类只能有一个Class对象)
1.1 反射的概述:
反射可以获取到字段(成员方法or构造方法),并对其解剖,得到该变量的修饰符、变量名、类型等
注意:上面提到的“获取”是在类的字节码文件中获取(所以使用反射的第一步就是要得到该类的字节码文件对象)
1.2 获取字节码文件对象的三种方式
-
Class.forName("全类名")
-
类名.class
-
对象.getClass()
代码示例:
Class clazz1 = Class.forName("com.itheima.reflectdemo.Student");
//clazz1 就是当Student.class这个文件加载到内存之后,产生的字节码文件对象
Class clazz2 = Student.class;
Student s = new Student();
Class clazz3 = s.getClass();
上面生成的字节码文件对象都是同一个,即clazz1 == clazz2
1.3 字节码文件和字节码文件对象
java文件:自己编写的.java
文件
字节码文件:编译之后的.class
文件
字节码文件对象:当.class
文件加载到内存之后,虚拟机自动创建出来的对象。
1.4 获取构造器
方法名 | 说明 |
---|---|
Constructor<?>[] getConstructors() | 获得所有的构造(只能public修饰) |
Constructor<?>[] getDeclaredConstructors() | 获得所有的构造(包含private修饰) |
Constructor<T> getConstructor(Class<?>... parameterTypes) | 获取指定构造(只能public修饰) |
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) | 获取指定构造(包含private修饰) |
代码示例:
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name) {
this.name = name;
}
private Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {return name;}
public void setName(String name) {this.name = name;}
public int getAge() {return age;}
public void setAge(int age) {this.age = age;}
public String toString() {
return "Student{name = " + name + ", age = " + age + "}";
}
}
//1.获得class字节码文件对象
Class clazz = Class.forName("com.itheima.reflectdemo.Student");
//获取空参构造
Constructor con1 = clazz.getConstructor();
//获取指定参数构造器
Constructor con2 = clazz.getConstructor(String.class,int.class);
1.6 获取构造器应用
利用获取到的构造器,创建对象:
第一步:获取整体的字节码文件对象clazz
第二步:获取有参构造方法con
第三步:创建对象(注意要修改构造方法的访问权限)
//1
Class clazz = Class.forName("com.itheima.a02reflectdemo1.Student");
//2
Constructor con = clazz.getDeclaredConstructor(String.class, int.class);
//3
con.setAccessible(true);//暴力访问
Student stu = (Student) con.newInstance("zhangsan", 23);
System.out.println(stu);
1.7 获取成员变量
方法名 | 说明 |
---|---|
Field[] getFields() | 返回所有成员变量对象的数组(只能拿public的) |
Field[] getDeclaredFields() | 返回所有成员变量对象的数组,存在就能拿到 |
Field getField(String name) | 返回单个成员变量对象(只能拿public的) |
Field getDeclaredField(String name) | 返回单个成员变量对象,存在就能拿到 |
代码示例:
public class Student {
private String name;
private int age;
public String gender;
public String address;
public Student() {
}
public Student(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
public Student(String name, int age, String gender, String address) {
this.name = name;
this.age = age;
this.gender = gender;
this.address = address;
}
//省略get和set方法
}
//1.获取class对象
Class clazz = Class.forName("com.itheima.reflectdemo.Student");
Field[] fields = clazz.getFields();
1.8 获取成员变量应用
方法 | 说明 |
---|---|
void set(Object obj, Object value) | 赋值(修改obj对象的变量值为value) |
Object get(Object obj) | 获取值 |
利用获取到的成员变量,获取变量值和修改变量值
代码示例:
Student s = new Student("zhangsan",23,"广州");
Student ss = new Student("lisi",24,"北京");
//1.获取class对象
Class clazz = Class.forName("com.itheima.reflectdemo.Student");
//2.获取name成员变量
Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
field.set(s,"wangwu");
String result = (String)field.get(s);
1.9 获取成员方法
方法名 | 说明 |
---|---|
Method[] getMethods( ) | 返回所有成员方法对象的数组(只能拿public的) |
Method[] getDeclaredMethods( ) | 返回所有成员方法对象的数组,存在就能拿到 |
Method getMethod(String name, Class<?>... parameterTypes) | 返回单个成员方法对象(只能拿public的) |
Method getDeclaredMethod(String name, Class<?>... parameterTypes) | 返回单个成员方法对象,存在就能拿到 |
代码示例:
public class Student {
private String name;
private int age;
public String gender;
public String address;
public void sleep(){
System.out.println("sleep");
}
public String eat(String something){
System.out.println("学生在吃" + something);
return "学生已经吃完了,非常happy";
}
public void playGame(){
System.out.println("playGame");
}
}
Class<?> clazz = Class.forName("com.itheima.reflectdemo.Student");
Method[] methods1 = clazz.getMethods();
Method[] methods2 = clazz.getDeclaredMethods();
Method method3 = clazz.getMethod("sleep");
Method method4 = clazz.getMethod("eat",String.class);
Method method5 = clazz.getDeclaredMethod("playGame");
1.10 获取成员方法并运行
方法
Object invoke(Object obj, Object... args) :运行方法
参数一:用obj对象调用该方法
参数二:调用方法的传递的参数(如果没有就不写)
返回值:方法的返回值(如果没有就不写)
代码示例:
public class Student {
private String name;
private int age;
public String gender;
public String address;
public String eat(String something){
System.out.println("学生在吃" + something);
return "学生已经吃完了,非常happy";
}
}
public class ReflectDemo6 {
public static void main(String[] args) throws Exception {
//1.获取字节码文件对象
Class clazz = Class.forName("com.itheima.a02reflectdemo1.Student");
//2.获取一个对象
Student s = new Student();
//3.获取一个指定的方法
//参数一:方法名
//参数二:参数列表,如果没有可以不写
Method eatMethod = clazz.getMethod("eat",String.class);
//运行
//参数一:表示方法的调用对象
//参数二:方法在运行时需要的实际参数
//注意点:如果方法有返回值,那么需要接收invoke的结果
//如果方法没有返回值,则不需要接收
String result = (String) eatMethod.invoke(s, "重庆小面");
System.out.println(result);
}
}
面试题:
你觉得反射好不好?好,有两个方向
第一个方向:无视修饰符访问类中的内容。但是这种操作在开发中一般不用,都是框架底层来用的。
第二个方向:反射可以跟配置文件结合起来使用,动态的创建对象,动态的调用方法。
1.11 练习泛型擦除(掌握概念,了解代码)
概念:
集合中的泛型只在java文件中存在,当编译成class文件之后,就没有泛型了。
在代码中定义List<Object>
和List<String>
等类型,在编译后都会变成List
,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到的。
例2.通过反射添加其它类型元素
package com.itheima.reflectdemo;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
public class ReflectDemo8 {
public static void main(String[] args) throws Exception {
//1.创建集合对象
ArrayList<Integer> list = new ArrayList<>();
list.add(123);
//2.利用反射运行add方法去添加字符串
//因为反射使用的是class字节码文件
//获取class对象
Class clazz = list.getClass();
//获取add方法对象
Method method = clazz.getMethod("add", Object.class);
//运行方法
method.invoke(list,"aaa");//相当于:list.add("aaa");
}
}
在程序中定义了一个ArrayList
泛型类型实例化为Integer
对象,如果直接调用add()
方法,那么只能存储整数数据,不过当我们利用反射调用add()
方法的时候,却可以存储字符串,这说明了Integer
泛型实例在编译之后被擦除掉了,只保留了原始类型Object。
下面的代码中类Pair使用泛型。在编译阶段会发生泛型擦除。JVM最后看到的只是原始类型
例3.原始类型Object
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
Pair的原始类型为:
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
从上面的例2中,我们也可以明白ArrayList<Integer>
被擦除类型后,原始类型也变为Object
,所以通过反射我们就可以存储字符串了。
如果类型变量有限定,那么原始类型就用第一个边界的类型变量类替换。
比如: Pair这样声明的话
public class Pair<T extends Comparable> {}
那么原始类型就是Comparable
。
1.12 练习:强行修改字符串的内容(掌握概念,了解代码)
在这个练习中,我需要你掌握的是字符串不能修改的真正原因。
字符串,在底层是一个byte类型的字节数组,名字叫做value
private final byte[] value;
真正不能被修改的原因:final和private
final修饰value表示value记录的地址值不能修改。
private修饰value而且没有对外提供getvalue和setvalue的方法。所以,在外界不能获取或修改value记录的地址值。是不是不太明白为甚么?看看下面的文章就好了。相关阅读:如何理解 String 类型值的不可变? - 知乎提问
如果要强行修改可以用反射:
代码示例:(了解)
String s = "abc";
String ss = "abc";
Class clazz = s.getClass();
Field field = clazz.getDeclaredField("value");
field.setAccessible(true);
byte[] bytes = (byte[]) field.get(s);
bytes[0] = 100;
System.out.println(s);//dbc
System.out.println(ss);//dbc
1.13 练习:反射和配置文件结合动态获取的练习(重点)
需求: 利用反射根据文件中的不同类名和方法名,创建不同的对象并调用方法。
分析:
①通过Properties加载配置文件
②得到类名和方法名
③通过类名反射得到Class对象
④通过Class对象创建一个对象
⑤通过Class对象得到方法
⑥调用方法
代码示例:
public class ReflectDemo9 {
public static void main(String[] args) throws Exception {
//1.读取配置文件的信息
Properties prop=new Properties();
FileInputStream inputStream=new FileInputStream(
new File("reflect_property.txt")
);
prop.load(inputStream);
String classname = prop.get("classname") + "";
String methodname = prop.get("methodname") + "";
//2.获取字节码文件对象
Class clazz = Class.forName(classname);
//3.要先创建这个类的对象
Constructor con = clazz.getDeclaredConstructor();
con.setAccessible(true);
Object o = con.newInstance();
//4.获取方法的对象
Method method = clazz.getDeclaredMethod(methodname);
method.setAccessible(true);
//5.运行方法
method.invoke(o);
}
}
配置文件中的信息:
classname=com.itheima.a02reflectdemo1.Student
methodname=sleep
1.14 利用反射保存对象中的信息(重点)
Teacher类
public class Teacher {
private String name;
private double salary;
public Teacher(String name, double salary) {
this.name = name;
this.salary = salary;
}
}
Student类
public class Student {
private String name;
private int age;
private char gender;
private double height;
private String hobby;
public Student(String name, int age, char gender, double height, String hobby) {
this.name = name;
this.age = age;
this.gender = gender;
this.height = height;
this.hobby = hobby;
}
}
利用反射获取对象(s 、t)中的属性信息。将信息存储在磁盘文件中
public class MyReflectDemo {
public static void main(String[] args) throws Exception {
/*
对于任意一个对象,都可以把对象所有的字段名和值,保存到文件中去
*/
Student s = new Student("小A",23,'女',167.5,"睡觉");
Teacher t = new Teacher("播妞",10000);
saveObject(s);
}
//把对象里面所有的成员变量名和值保存到本地文件中
public static void saveObject(Object obj) throws Exception {
//1.获取字节码文件的对象
Class clazz = obj.getClass();
//2. 创建IO流
BufferedWriter bw = new BufferedWriter(new FileWriter("myreflect\\a.txt"));
//3. 获取所有的成员变量
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
//获取成员变量的名字
String name = field.getName();
//获取成员变量的值
Object value = field.get(obj);
//写出数据
bw.write(name + "=" + value);
bw.newLine();
}
bw.close();
}
}
2. 动态代理
2.1 好处
无侵入式的给方法增强功能
2.2 为什么要代理?
现在有一个大明星需要举办一场六安演唱会。大明星发现一场演唱会涉及到的很多事情比如:场地准备、门票的发布和宣传。大明星头疼了,他说他只想唱歌和跳舞。这些杂七杂八的事情我们找一个中介公司来处理。
那么问题又来了,代理中要有哪些方法?才能满足大明星的要求?
答案:代理中的方法一定是大明星需要被代理的方法。比如,大明星现在需要被代理的方法是sing和dance。那么,代理的代码中一样要有相对应的方法sing和dance。在对应的方法中调用大明星的sing和dance
现在明白了吗?那么,问题来了:代理是如何知道大明星有哪些方法需要被代理?
答案:我么通过定义一个接口,接口中的方法都是大明星要被代理的方法。而且,大明星和代理人都要实现该接口。
2.3 上面实例的代码实现
大明星类
public class BigStar implements Star{
private String name;//大明星名字
public BigStar (String name) {
this.name = name;
}
/**
* 唱歌
* @param name 歌曲的名字
*/
public String sing(String name){
System.out.println(this.name +"正在唱"+name);
return "谢谢大家来参加我的演唱会";
}
/**
* 跳舞
*/
public void dance(){
System.out.println(this.name +"正在跳舞"+name);
}
}
接口
public interface Star {
String sing(String name);
void dance();
}
现在到了本次案例的重点:大明星的代理类如何创建?
答案:java.lang.reflect.Proxy类,提供了为对象产生代理对象的方法:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
//参数一:用于指定用哪个类加载器,去加载生成的代理类
//参数二:指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法
//参数三:用来指定生成的代理对象要干什么事情
类加载器:将类的字节码文件加载到内容中
创建代理
/**
* 类的作用:创建一个代理
*/
public class ProxyUtil {
/**
* 方法作用:给大明星对象创建一个代理
* @param bigStar 被代理的明星对象
* @return 给明星创建的代理
*/
public static Star createProxy(BigStar bigStar){
Star star = (Star)Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(),
new Class[]{Star.class},
new InvocationHandler() {
@Override
public Object invoke (Object proxy,
Method method,
Object[] args) throws Throwable {
//....省略一万字
}
}
);
return star;
}
}
好了,我们把架子搭好了。现在有必要说明一下:代理类的用法
假设,现在有人想要大明星唱一首歌。他应该完成下面的步骤:
-
获取代理对象
-
代理对象 = ProxyUtil.createProxy(大明星对象)
-
-
调用代理类的唱歌方法
-
代理对象.唱歌的方法()
-
好了,有必要说明一下,此时,底层就是去调用invoke方法(invoke方法在上面代码)
-
创建代理续写
//用来指定生成的代理对象要干什么事情
new InvocationHandler() {
@Override
public Object invoke (Object proxy,
Method method,
Object[] args) throws Throwable {
/**
* proxy:代理对象
* method:要运行的方法
* args:调用方法时,传递的参数
*/
if("sing".equals(method.getName())){
System.out.println("准备话筒,收钱");
}else if("dance".equals(method.getName())){
System.out.println("准备场地,收钱");
}
//去找大明星唱歌和跳舞(调用大明星类中相应的方法)
//这是在前面反射中提到的方法
return method.invoke(bigStar,args);
}
}
测试
public class test {
public static void main (String[] args) {
BigStar bigStar = new BigStar("程浩韵");
Star proxy = ProxyUtil.createProxy(bigStar);
//唱歌
String result = proxy.sing("如果爱忘了");
System.out.println(result);
//跳舞
proxy.dance();
}
}
结果
2.4 代码示例(上面的书写方式不常用到,这里的才是常用的)
接口
public interface Star {
String sing(String name);
void dance();
}
大明星类
public class BigStar implements Star{
private String name;//大明星名字
public BigStar (String name) {
this.name = name;
}
public String sing(String name){
System.out.println(this.name +"正在唱"+name);
return "谢谢大家来参加我的演唱会";
}
public void dance(){
System.out.println(this.name +"正在跳舞");
}
}
定义一个 JDK 动态代理类
public class DebugInvocationHandler implements InvocationHandler {
//被代理的对象(前面说的大明星对象)
private final Object target;
public DebugInvocationHandler (Object target) {
this.target = target;
}
@Override
public Object invoke (Object proxy, Method method, Object[] args) throws Throwable {
/**
* proxy:代理对象
* method:要运行的方法
* args:调用方法时,传递的参数
*/
if("sing".equals(method.getName())){
System.out.println("准备话筒,收钱");
}else if("dance".equals(method.getName())){
System.out.println("准备场地,收钱");
}
Object result = method.invoke(target, args);//相当于:target.method(args)
return result;
}
}
invoke()
方法: 当我们的动态代理对象调用原生方法的时候,最终实际上调用到的是 invoke()
方法,然后 invoke()
方法代替我们去调用了被代理对象的原生方法。
生成代理类的工厂
//代理类工厂
public class JdkProxyFactory {
public static Object getProxy(Object target){
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), //目标类的类加载器
target.getClass().getInterfaces(),//代理需要实现的接口
new DebugInvocationHandler(target)//代理对象自定义InvocationHandler
);
}
}
getProxy()
:主要通过Proxy.newProxyInstance()
方法获取某个类的代理对象
测试
BigStar star = new BigStar("孟晏城");
Star proxy = (Star) JdkProxyFactory.getProxy(star);
proxy.dance();
proxy.sing(" 因为刚好遇见你 ");
结果
准备场地,收钱
孟晏城正在跳舞
准备话筒,收钱
孟晏城正在唱 因为刚好遇见你
Process finished with exit code 0
2.5 额外扩展
动态代理,还可以拦截方法
比如:
在这个故事中,如果别人让邀请大明星去唱歌,打篮球,代理人就增强功能。
但是如果别人让大明星去扫厕所,经纪人就要拦截,不会去调用大明星的方法。
/*
* 类的作用:创建一个代理
* */
public class ProxyUtil {
public static Star createProxy(BigStar bigStar){
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces, InvocationHandler h)
Star star = (Star) Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(),
new Class[]{Star.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy,
Method method,
Object[] args) throws Throwable {
if("cleanWC".equals(method.getName())){
System.out.println("拦截,不调用大明星的方法");
return null;
}
//如果是其他方法,正常执行
return method.invoke(bigStar,args);
}
}
);
return star;
}
}
2.6 动态代理的练习
动态代码可以增强也可以拦截
对add方法进行增强,对remove方法进行拦截,对其他方法不拦截也不增强
public class MyProxyDemo1 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
List proxyList = (List) Proxy.newProxyInstance(
MyProxyDemo1.class.getClassLoader(),
new Class[]{List.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy,Method method,
Object[] args) throws Throwable {
//对add方法做一个增强,统计耗时时间
if (method.getName().equals("add")) {
long start = System.currentTimeMillis();
//调用集合的方法,真正的添加数据
method.invoke(list, args);
long end = System.currentTimeMillis();
System.out.println("耗时时间:" + (end - start));
//需要进行返回,返回值要跟真正增强或者拦截的方法保持一致
return true;
}else if(method.getName().equals("remove") && args[0] instanceof Integer){
System.out.println("拦截了按照索引删除的方法");
return null;
}else if(method.getName().equals("remove")){
System.out.println("拦截了按照对象删除的方法");
return false;
}else{
//如果当前调用的是其他方法,我们既不增强,也不拦截
method.invoke(list,args);
return null;
}
}
}
);
proxyList.add("aaa");
proxyList.add("bbb");
proxyList.add("ccc");
proxyList.add("ddd");
proxyList.remove(0);
proxyList.remove("aaa");
//打印集合
System.out.println(list);
}
}
本篇文章是我结合黑马阿伟老师的JavaSE课程的笔记和自己的理解写的关于反射和动态代理知识点。如有不对的地方请在评论区指正。
参考: