前言:本文为Java反射专题面试八股文,博主通过javaguide还有网上的资料自行整理的,还在准备暑期实习,应该会持续更新...
疑问:“为什么把反射、注解和代理放在一个文档?”——因为他们仨内容集中而且都是经常同时考的,放在一起方便复习,即使没有基础也能快速有个印象;比如问到Spring框架的时候,AOP就是代理,代理底层是反射实现的,而一般使用就是通过注解方式,也是基于反射。
参考javaguide:Java基础常见面试题总结(下)、Java 反射机制详解、Java 代理模式详解
目录
CGLIB 动态代理(Code Generation Library)
反射
何为反射?⭐
运行时、分析类、执行类
-
反射就是 在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法
-
反射赋予了在运行时 分析类 以及 执行类 中方法的能力。通过反射可以获取任意一个类的所有属性和方法,还可以调用这些方法和属性
-
反射被称为框架的灵魂
反射的应用场景?⭐
框架、注解
-
业务代码很少会接触到直接使用反射机制的场景
主要出现:
-
框架和底层 :Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射
-
Java 中的一大利器 注解 的实现也用到了反射
为什么使用 Spring 的时候 ,一个@Component
注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value
注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?
——这些都是因为可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。获取到注解之后,就可以做进一步的处理
反射机制的优缺点
灵活+框架便利 / 安全问题+性能差
优点:
-
让代码更加灵活
-
为各种框架提供开箱即用的功能提供了便利
缺点:
-
让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。
-
反射的性能也要稍差点,因为:
-
编译器不能进行任何优化
-
必须发现所有被调用/创建的东西
-
Java反射API有多少种?
反射 API 用来生成 JVM 类别、接口或对象的信息。
-
Class 类:反射的核心类,可获取类属性、方法等信息
-
Field 类:Java.lang.reflec 包中的类,表示类的成员变量,可用于获取和设置类中的属性值
-
Method 类:Java.lang.reflec 包中的类,表示类的方法,可用于获取方法信息或执行方法
-
Constructor 类:Java.lang.reflec 包中的类,表示类的结构方法
反射使用步骤
-
获取想要操作的Class对象,它是反射的核心,我们可以通过Class对象任意调用
-
调用 Class 类中的方法是反射的使用阶段
-
使用反射 API 操作这些信息
反射常用的API
获取反射中Class对象的四种方式 ⭐
第一种,使用 Class.forName()
静态方法
当知道该类的全路径名时,可以使用该方法获取 Class 类对象
Class clz = Class.forName("java.lang.String");
第二种,使用 .class
方法
这种方法只适合在编译前就知道操作的 Class 具体类
但是一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化
Class clz = String.class;
第三种,使用类对象实例的 instance.getClass()
方法
String str = new String("Hello");
Class clz = str.getClass();
第四种,通过类加载器xxxClassLoader.loadClass()
传入类路径获取
通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一系列步骤,静态代码块和静态对象不会得到执行
ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject");
通过反射创建类对象
第一种:通过 Class 对象的 newInstance()
方法
Class clz = Apple.class;
Apple apple = (Apple)clz.newInstance();
第二种:通过 Constructor
对象的 newInstance()
方法
Class clz = Apple.class;
Constructor constructor = clz.getConstructor();
Apple apple = (Apple)constructor.newInstance();
通过反射获取类属性、方法、构造器
-
getFields()
: 可以获取 Class 类的属性,但无法获取私有属性。 -
getDeclaredFields()
: 可以获取包括私有属性在内的所有属性
与获取类属性一样,当我们去获取类方法、类构造器时,如果要获取私有方法或私有构造器,则必须使用有 declared 关键字的方法。
反射的实现方法 (源码解析)
剖析 JDK 源码中是如何实现反射的
很多框架都使用了反射,而反射中最重要的的就是 Method
类的 invoke
方法了
invoke
逻辑从上到下依次是:
-
Method.invoke()
-
MethodAccessor.invoke()
-
DelegatingMethodAccessorImpl.invoke()
-
NativeMethodAccessorImpl.invoke()
// 反射找不到类或者发生启动异常的时候,通常指向 invoke() 方法 Method.invoke() ;
// MethodAccessor 对象其实就是具体去生成反射类的入口
// 这一步中,ma获取的是 DelegatingMethodAccessorImpl类对象
// 所以使用的是 DelegatingMethodAccessorImpl
MethodAccessor ma = methodAccessor;
ma.acquireMethodAccessor();
ma.invoke();
// DelegatingMethodAccessorImpl的invoke方法 调用了 delegate 属性的 invoke
// 这里的 delegate 是 NativeMethodAccessorImpl 对象
// 所以进入NativeMethodAccessorImpl 的 Invoke()方法
delegate.invoke();
// 在NativeMethodAccessorImpl 的invoke方法中,会判断调用次数是否超过阀值
// 如果超过该阀值,那么就会生成另一个 MethodAccessor 对象
// 并将原来的DelegatingMethodAccessorImpl 对象中的 delegate 属性指向最新的 MethodAccessor 对象。
MethodAccessor实现有两个实现版本,一个是 Native 版本,一个是 Java 版本;
-
Native 版本一开始启动快,但是随着运行时间变长,速度变慢
-
Java 版本一开始加载慢,但是随着运行时间变长,速度变快
-
所以第一次加载的时候我们会发现使用的是
NativeMethodAccessorImpl
的实现,而当反射调用次数超过 15 次之后,则使用MethodAccessorGenerator
生成的MethodAccessorImpl
对象去实现反射
困惑的问题
Java为什么能实现反射
反射(Reflection)是指在运行时(runtime)查询、访问和修改类以及对象的能力,这包括获取类的方法、字段、构造函数等信息,并能够动态地调用方法或修改字段值。
Java能够实现反射,归因于其运行时环境(JRE)和Java虚拟机(JVM)的设计,以及语言本身对面向对象概念的深度支持
-
类型信息的保留:在字节码中保留了大量的类型信息,这包括类的结构信息、方法签名、字段以及运行时可用的注解信息等。这些信息在运行时被JVM加载,并可以通过反射API进行访问。
-
动态类加载:JAVA类加载机制允许在运行时动态地加载类。反射API提供了方法来动态地加载类并查询其信息
-
运行时环境和JVM的支持:Java虚拟机(JVM)和Java运行时环境(JRE)提供了执行Java字节码所需的环境。反射API是JRE中的一部分,充分利用了JVM对类型信息的管理能力,允许开发者在运行时进行类的查询和操作。
-
面向对象的语言特性:反射API进一步扩展了这些特性,允许程序在运行时动态地与对象交互,包括创建对象实例、调用方法、访问字段,甚至是修改类的结构
C/C++语言能实现反射么
-
C/C++语言本身并没有直接内置的反射机制。
-
C++设计重点放在编译时的类型安全和性能上,而不是运行时的类型信息和动态特性。
-
C/C++语言本身不直接支持反射,缺乏运行时类型信息(RTTI)和元数据支持
-
反射需要能够在运行时查询对象的类型信息,包括类的结构、方法、属性等,而C/C++语言的编译模型通常在编译时就已经确定了类型信息,运行时不保留这些信息。
-
但是可以通过方法来模拟反射。例如自定义元数据、第三方库。
相关面试题
说一说JAVA的反射机制 ⭐
1.概念
-
反射就是 在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法
-
Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性
2.应用
-
业务很少用,主要是框架底层(动态代理)和注解中会涉及到
3.使用
Java反射机制主要包含以下几个部分:
-
Class类:代表了java中的类信息,包括类名、父类、接口等信息。
-
Field类:代表了java中的字段信息,包括字段名、类型、修饰符等信息。
-
Method类:代表了java中的方法信息,包括方法名、参数类型、返回值类型等信息。
-
Constructor类:代表了java中的构造方法信息。
4.优缺点
-
优点:让代码更加灵活,同时为各种框架提供开箱即用的功能提供了便利
-
缺点:让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时);同时反射的性能也要稍差点
说说JAVA的反射机制的应用场景 ⭐
-
平时的项目开发过程中,基本上很少会直接使用到反射机制
-
模块化的开发,通过反射去调用对应的字节码
-
Spring 框架的 IOC(动态加载管理 Bean)创建对象以及 AOP(动态代理)功能都和反射有联系;
-
JDBC连接数据库时使用
Class.forName()
通过反射加载数据库的驱动程序 -
动态配置实例的属性;
-
Java注解
举例说明:
例1:JDBC 的数据库的连接
-
通过
Class.forName()
加载数据库的驱动程序Driver (通过反射加载,前提是引入相关了Jar包) -
通过
DriverManager
类进行数据库的连接,连接的时候要输入数据库的连接地址、用户名、密码 -
通过
Connection
接口接收连接
public class ConnectionJDBC {
//驱动程序就是之前在classpath中配置的JDBC的驱动程序的JAR 包中
public static final String DBDRIVER = "com.mysql.jdbc.Driver";
//连接地址是由各个数据库生产商单独提供的,所以需要单独记住
public static final String DBURL = "jdbc:mysql://localhost:3306/test";
//连接数据库的用户名
public static final String DBUSER = "root";
//连接数据库的密码
public static final String DBPASS = "";
public static void main(String[] args) throws Exception {
Connection con = null;//表示数据库的连接对象
Class.forName(DBDRIVER); //1、使用CLASS 类加载驱动程序 ,反射机制的体现
con = DriverManager.getConnection(DBURL,DBUSER,DBPASS);//2、连接数据库
System.out.println(con);
con.close(); // 3、关闭数据库
}
}
例2:Spring 框架的使用
Spring 通过 XML 配置模式装载 Bean 的过程:
-
将程序内所有 XML 或 Properties 配置文件加载入内存中
-
Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息
-
使用反射机制,根据这个字符串获得某个类的Class实例
-
动态配置实例的属性
注解
注解是什么? ⭐
-
Java5 开始引入的新特性,是代码中的特殊标记,这些标记可在编译、类加载、运行时被读取,并执行相应的处理
-
Annotation
(注解),可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用 -
注解本质是一个继承了
Annotation
的特殊接口 -
JDK 提供了很多内置的注解(比如
@Override
、@Deprecated
),同时也可以自定义注解
如何实现自定义注解?
分以下三步自定义注解:
-
注解声明:首先使用
@interface
关键字定义一个注解。这个声明定义了注解的结构,包括它可以包含的元素(即注解的属性)以及默认值 -
元注解:用于注解其他注解的注解。Java提供了一些内置的元注解,用来指定注解的使用范围、生命周期等属性。例如:
-
@Target
:目标,指定注解可以应用的Java元素类型(如方法、字段、类等) -
@Retention
:保留,指定注解在哪一个级别可用(源码SOURCE、类文件CLASS、运行时RUNTIME)其中日常开发中运行时
用的最多 -
@Inherited
:继承,指示注解类型被自动继承 -
@Documented
:文档,指示注解应该被javadoc工具记录
-
-
注解处理:注解可以在编译时、类加载时或运行时被处理,这取决于它们的
@Retention
策略-
编译时处理:通过注解处理器(Annotation Processor),可以在编译时读取和处理注解信息,生成额外的源代码或资源文件
-
运行时处理:通过反射API(如
Class.getAnnotation
方法),代码可以在运行时查询注解信息,从而实现动态的处理逻辑
-
@Target(ElementType.METHOD) // 元注解
@Retention(RetentionPolicy.SOURCE) // 元注解
public @interface Override { //注解声明
//...
}
// "@interface" 其实就等于 "extends Annotation"
public interface Override extends Annotation{
//...
}
注解的解析方法有哪几种?⭐
注解只有被解析之后才会生效,常见的解析方法有两种:
-
编译期直接扫描
-
编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用
@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法 -
例如,Lombok的实现原理,为什么@Data注解就能有set/get方法,其实是在编译时加上去的
-
-
运行期通过反射处理
-
像框架中自带的注解(比如 Spring 框架的
@Value
、@Component
)都是通过反射来进行处理的
-
相关面试题
在Spring里有哪些常用注解?
-
⭐JAVA原生/JDK内置:
@Override
、@Deprecated
-
⭐元注解:
@Retention
、@Target
-
⭐跟层级相关的:
@Controller
、@Service
、@Repository
-
⭐跟Bean相关的:
@Bean
、@Autowired
、@Resource
-
跟组件相关的:
@Component
-
跟HTTP相关的:
@Requestmapping
、@Param
-
lombok的注解:
@Slf4j
、@Data
有没有在项目中自定义过注解?
有,例如日志、限流、权限认证模块,结合AOP
的方式做切面编程,然后通过注解注入
代理
代理模式 ⭐
代理模式是一种比较好理解的设计模式
-
使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。符合开闭原则
-
主要作用是 扩展目标对象的功能,比如说在目标对象的某个方法执行前后可以增加一些自定义的操作
-
代理模式实现方式有两种: 静态代理、动态代理
什么是静态代理
静态代理中,对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)
从 JVM 层面来说,静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
静态代理实现步骤:
-
定义一个接口及其实现类;
-
创建一个代理类同样实现这个接口
-
将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
静态代理缺点:
-
当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:
-
只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
-
新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类
-
-
当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护
动态代理
什么是动态代理
-
动态代理相比于静态代理更加灵活
-
动态代理就是可以不事先为每个需要代理的来写代理类,而是在运行的时候,动态地创建对应的代理类,然后在系统中用代理类替换被代理的类
-
从 JVM 角度 来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的
-
典型例子:Spring AOP、RPC 框架
-
动态代理实现方式又分为
JDK动态代理
和CGLIB动态代理
(子类代理)
JDK 动态代理机制
-
在 Java 动态代理机制中
InvocationHandler
接口和Proxy
类是核心。 -
Proxy
类中使用频率最高的方法是:newProxyInstance()
,这个方法主要用来生成一个代理对象。
// loader ,是类的加载器
// interfaces,是委托类的接口类型,证代理类返回的是同一个实现接口下的类型,保持代理类与抽象角色行为的一致
// h,是代理类本身,即告诉代理类,代理类遇到某个委托类的方法时该调用哪个类下的invoke方法
public static Object newProxyInstance(
ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h){
//...
}
-
当动态代理对象调用一个方法时,这个方法的调用就会被转发到实现
InvocationHandler
接口类的invoke
方法来调用。
JDK 动态代理类使用步骤
-
定义一个接口及其实现类;
-
自定义
InvocationHandler
并重写invoke
方法,在invoke
方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑; -
通过
Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
方法创建代理对象
SpringAOP 实现方式是在Bean初始化完成后,执行动态代理,然后把动态代理类写入Map中
CGLIB 动态代理(Code Generation Library)
JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。
为了解决这个问题,可以用 CGLIB 动态代理机制来避免。
CGLIB 是一个基于ASMopen in new window的字节码生成库,允许我们在运行时对字节码进行修改和动态生成。
Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
在 CGLIB 动态代理机制中 MethodInterceptor
接口和 Enhancer
类是核心。
通过 Enhancer
类来动态获取被代理类,当代理类调用方法的时候,实际调用的是 MethodInterceptor
中的 intercept
方法
CGLIB 动态代理类使用步骤
-
定义一个类
-
自定义
MethodInterceptor
并重写intercept
方法,intercept
用于拦截增强被代理类的方法,和 JDK 动态代理中的invoke
方法类似 -
通过
Enhancer
类的create()
创建代理类
JDK 动态代理和 CGLIB 动态代理对比
-
JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类(最主要的区别)
-
CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
-
效率上,大部分情况都是 JDK 动态代理更优秀
静态代理和动态代理的对比⭐
-
灵活性
-
动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。
-
静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,非常麻烦
-
-
JVM 层面
-
静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件
-
动态代理是在运行时动态生成类字节码,并加载到 JVM 中
-
(持续更新...)