以下为我平时的学习记录,有不对的地方,一定要指出哦。
一、开场白
反射是一个非常重要的知识点,在学习Spring、SpringBoot框架时,Bean的初始化,切面编程,破坏单例模式时,获取标注的注解时等等,我们都会用到反射。
在现在面试造火箭,工作拧螺丝的招聘场景中,反射是一个非常热点的话题,当我们看了很多的开源框架源码时,就会经常发现里面有用到反射的地方。今天主要聊一下反射的相关概念、应用以及反射的性能!
二、反射是啥?
我们平时创建一个Java对象的时候,最最最常用的应该就是new 对象()了,这个我们就叫它一下正射吧,主要和反射做对比。
Object o = new Object();
o.getclass();
我们一开始学习Java的时候,可能会使用很多的if…else…根据参数来判断返回的是什么,简单的以Map来举例,比如以下代码,每次都是通过new一个对象来返回:
public Map<String, String> getMap(String mapType){
Map<String, String> map = null;
if (mapType.equals("HashMap")) {
map = new HashMap<>();
} else if (mapType.equals("LinkedHashMap")) {
map = new LinkedHashMap<>();
} else if (mapType.equals("ConcurrentSkipListMap")) {
map = new ConcurrentSkipListMap<>();
}
return map;
}
这种在编译器就可以把对象类型确定下来,但是
当我们以后突然想再添加一个ConcurrentHashMap的时候,就不得不修改该方法,然后重新编译,这违背了开闭原则,在扩展功能的时候,还得修改源代码,如果扩展的功能越多,那么修改后导致的问题可能就会越多。
而反射是在程序运行过程中确定和解析数据类的类型。 对于在 编译期 无法确定使用哪个数据类的场景,通过反射可以在程序运行时构造出不同的数据类实例。
这个时候,反射就可以派上大用场了。
我们可以修改代码为:
public Map<String, String> getMap(String className){
Class clazz = Class.forName(className);
Consructor cs = clazz.getConsructor();
return (Map<String, String>) cs.newInstance();
}
这样我们就不需要每次扩展功能的时候都去修改源码。如果我们有很多分支的时候,采用这种方式最为便捷。
三、基本使用
Java反射主要有以下四个组成部分:
在反射中,我们用的最多的常见功能是:
- 运行时获取一个类的Class对象
- 运行时构造一个类的实例对象
- 运行时获取一个类的所有信息,如:注解、属性、方法、构造器
以下实例类以People为模板:
public class People {
static {
System.out.println("ppp");
}
private char sex;
String name;
int age;
public People() {
}
public People(char sex, int age,String name) {
this.sex = sex;
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
1.获取Class对象
我们Java代码编译后会生成的字节码文件 .class,字节码文件中包含了类的所有信息,当字节码被装载进虚拟机中后,会在内存中生成Class对象,它包含了该类内部的所有信息,并且在运行时可以去获取到这些信息。
而获取Class对象主要有以下方法:
- 通过类名.class
Class clazz = People.class;
- 通过实例对象.getClass();
People p = new People();
Class clazz = p.getClass();
- 通过全限定名Class.forname(className);
Class clazz = Class.forName("com.saltice.People");
而且,这三种方式获取到的Class对象都是同一个。
public static void main(String[] args) throws ClassNotFoundException {
Class clazz1 = People.class;
People p = new People();
Class clazz2 = p.getClass();
Class clazz3 = Class.forName("com.saltice.People");
System.out.println(clazz1 == clazz2);
System.out.println(clazz2 == clazz3);
}
不考虑static代码块,运行结果为
true
true
因为类的Class对象只有一个,这涉及到**JVM类加载机制的双亲委派模型。** 它保证了每个类只会被加载一次,所以就只能有一个Class对象啦。
2.实例化对象
通过反射实例对象主要有两种:
- Class对象调用newInstance()方法;
Class clazz1 = People.class;
// 这要求People的无参构造方法存在
People pp = (People) clazz1.newInstance();
- Constructor构造器调用newInstance()方法;
Class clazz1 = People.class;
Constructor constructor = clazz1.getConstructor(char.class,int.class,String.class);
constructor.setAccessible(true);
People pp = (People) constructor.newInstance('男',18,"Saltice");
3.获取类的所有信息
获取类中的变量(Field)
- Field[] getFields():获取类中所有被public修饰的所有变量
- Field getField(String name):根据变量名获取类中的一个变量,该变量必须被 public 修饰
- Field[] getDeclaredFields():获取类中所有的变量,但无法获取继承下来的变量
- Field getDeclaredField(String name):根据姓名获取类中的某个变量,无法获取继承下来的变量
获取类中的方法(Method)
-
Method[] getMethods():获取类中被public修饰的所有方法
-
Method getMethod(String name, Class…<?> paramTypes):根据名字和参数类型获取对应方法,该方法必须被public修饰
-
Method[] getDeclaredMethods():获取所有方法,但无法获取继承下来的方法
-
Method getDeclaredMethod(String name, Class…<?> paramTypes):根据名字和参数类型获取对应方法,无法获取继承下来的方法
获取类的构造器(Constructor)
- Constuctor[] getConstructors():获取类中所有被public修饰的构造器
- Constructor getConstructor(Class…<?> paramTypes):根据参数类型获取类中某个构造器,该构造器必须被public修饰
- Constructor[] getDeclaredConstructors():获取类中所有构造器
- Constructor getDeclaredConstructor(class…<?> paramTypes):根据参数类型获取对应的构造器
每个功能的内部又可以细分为两类:
-
有Declared修饰的方法:可以获取该类内部包含的所有变量、方法和构造器,但是无法获取继承下来的信息。
-
无Declared修饰的方法:可以获取该类中public修饰的变量、方法和构造器,可获取继承下来的信息
所以,如果想获取类中 所有的(包括继承) 变量、方法和构造器,则需要同时调用getXXXs()和getDeclaredXXXs()两个方法,用Set集合存储它们获得的变量、构造器和方法。
如果父类的属性用protected 修饰,利用反射是无法获取的到。
protected 修饰符的作用范围:只允许同一个包下或者子类访问,可以继承到子类。
获取注解
注解并不是专属于Class对象的一种信息,每个变量、方法和构造函数都可以被注解修饰,所以Field,Constructor 和 Method 类对象都可以调用下面这些方法获取标注在它们之上的注解。
- Annotation[] getAnnotations():获取该对象上的所有注解
- Annotation getAnnotation(Class annotaionClass):传入注解类型,获取该对象上的特定一个注解
- Annotation[] getDeclaredAnnotations():获取该对象上的显式标注的所有注解,无法获取继承下来的注解
- Annotation getDeclaredAnnotation(Class annotationClass):根据注解类型,获取该对象上的特定一个注解,无法获取继承下来的注解。
只有注解的@Retension标注为RUNTIME时,才能够通过反射获取到该注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
}
@Retension 有3种保存策略:
- SOURCE:只在源文件(.java)中保存,即该注解只会保留在源文件中,编译时编译器会忽略该注解,例如 @Override 注解
- CLASS:保存在字节码文件(.class)中,注解会随着编译跟随字节码文件中,但是运行时不会对该注解进行解析
- RUNTIME:一直保存到运行时,用得最多的一种保存策略,在运行时可以获取到该注解的所有信息。
这个是@Override注解的代码,它是的策略是SOURCE
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
我们新建一个简单的注解,策略是RUNTIME
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Tags {
}
再变动一下People.java,添加以下代码:
@Override
@Tags
public String toString() {
return name+">"+sex+">"+age ;
}
通过反射获取一下注解试试:
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class clazz1 = People.class;
Constructor constructor = clazz1.getConstructor(char.class,int.class,String.class);
constructor.setAccessible(true);
People pp = (People) constructor.newInstance('男',18,"Saltice");
Method method = clazz1.getMethod("toString");
Annotation[] annotation = method.getDeclaredAnnotations();
System.out.println(Arrays.toString(annotation));
结果只有
[@com.saltice.Tags()]
并没有获取到Override
通过反射调用方法
通过反射获取到某个 Method 类对象后,可以通过调用invoke方法执行。
invoke(Oject obj, Object… args):参数`1指定调用该方法的对象,参数2是方法的参数列表值。
如果调用的方法是静态方法,参数 1 只需要传入null,因为静态方法不与某个对象有关,只与某个类有关。
以下测试一下,invoke调用toString()方法,
Class clazz1 = People.class;
Constructor constructor = clazz1.getConstructor(char.class,int.class,String.class);
constructor.setAccessible(true);
People pp = (People) constructor.newInstance('男',18,"Saltice");
Method method = clazz1.getDeclaredMethod("toString");
System.out.println(method.invoke(pp,null));
执行结果为Saltice>男>18
到此,反射最基本的操作,基本上说完了。
接下来看一下反射的应用场景
四、反射的应用场景
-
Spring 实例化对象:当程序启动时,Spring 会读取配置文件applicationContext.xml并解析出里面所有的 标签实例化到IOC容器中。
-
SpringBoot中的拦截器:是在面向切面编程的就是在你的service或者一个方法,前调用一个方法,或者在方法后调用一个方法比如动态代理就是拦截器的简单实现,在你调用方法前打印出字符串(或者做其它业务逻辑的操作),也可以在你调用方法后打印出字符串,甚至在你抛出异常的时候做业务逻辑的操作。拦截器是面向切面 AOP( Aspect-Oriented Programming)的一种实现,底层通过动态代理模式完成。
-
反射 + 工厂模式:通过反射消除工厂中的多个分支,如果需要生产新的类,无需关注工厂类,工厂类可以应对各种新增的类,反射可以使得程序更加健壮。
-
JDBC 连接数据库:使用 JDBC 连接数据库时,指定连接数据库的驱动类时用到反射加载驱动类,在SpringBoot项目中,常常可以看到在application.yml中的数据库配置有这样一条配置:
driver-class-name: com.mysql.cj.jdbc.Driver
,这是因为MySQL版本不同引起的驱动类不同,这体现使用反射的好处:不需要修改源码,仅加载配置文件就可以完成驱动类的替换。 -
Netty中ServerSockerChannel的创建,就是典型的范型+反射+工厂模式
-
…
五、反射的优势与缺陷
- 添加程序的灵活性,降低耦合性,提高自适应能力:允许程序创建和控制任何类的对象,无需提前硬编码目标类,就像连接数据库的数据源配置,利用反射,可以适配各个数据源;
- 破坏类的封装性??:它可以强制访问private修饰的信息,这一特点,直接就影响了单例模式的饿汉与懒汉两种模式,但是可以通过枚举来避免;这篇文章也有相关介绍,但是,既然小偷可以访问和搬走你私有的家具,那安装防盗门还有意义么?同样的道理嘛。并且Java从应用层给我们提供了安全管理机制——安全管理器,每个Java应用都可以拥有自己的安全管理器,它会在运行阶段检查需要保护的资源的访问权限及其它规定的操作权限,保护系统免受恶意操作攻击,以达到系统的安全策略。所以其实在使用反射时,内部有安全控制,如果安全设置禁止了这些,那么反射机制就无法访问私有成员。我们在反射调用私有方法时,通常会调用这条代码:
method.setAccessible(true);
来取消Java的权限控制检查。 - 性能损耗:反射调用方法不像直接调用new出来对象的方法,它中间会有很多的检查步骤和解析步骤,你直接new一个对象并调用方法或者访问属性时,编译器会在编译器提前检查可访问性,如果你有不正确的访问,IDE会提前提示错误的,例如参数传递类型不匹配,非法访问 private 属性和方法。而反射调用只有在程序运行时调用反射的代码时才会从头开始检查、调用、返回结果,JVM 也无法对反射的代码进行优化。
虽然反射会有性能损耗,但是我们不能一概而论,反射需要调用很多次才可能体现出来它的损耗,在单次调用反射的过程中,性能损耗可以忽略不计。如果程序的性能要求很高,那么尽量不要使用反射。
总之,反射很强大,使用需谨慎。