反射注解与动态代理
本文将逐一讲解反射、注解、动态代理。之所以将这几个知识点放在一起,是因为三者存在着一定的联系,即反射机制贯穿着注解和动态代理。并且由于在日常实际开发中,虽然很少接触它们的底层,但是对于了解各类框架的底层原理十分有帮助。
反射
什么是反射
在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取信息以及动态调用对象的方法的功能称为Java语言的反射机制。
反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。
通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
获取 Class 对象的四种方式
如果我们动态获取到这些信息,我们需要依靠 Class 对象。Class 类对象将一个类的方法、变量等信息告诉运行的程序。而Java 提供了四种方式获取 Class 对象:
- 知道具体类的情况下:
1 | Class alunbarClass = TargetObject.class; |
- 通过
Class.forName()
传入类的全路径:
我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化
1 | Class alunbarClass1 = Class.forName("cn.smallboat.TargetObject"); |
- 通过对象实例
instance.getClass()
获取:
1 | TargetObject o = new TargetObject(); |
- 通过类加载器
xxxClassLoader.loadClass()
传入类路径获取:
1 | ClassLoader.getSystemClassLoader().loadClass("cn.smallboat.TargetObject"); |
通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一系列步骤,静态代码块和静态对象不会得到执行。
反射的基本操作
- 创建一个用来反射的类
1 | package cn.smallboat; |
- 使用反射操作这个类的方法及参数
1 | package cn.smallboat; |
注解
什么是注解
Java注解(Annotation),也叫元数据
。一种代码级别的说明。它是JDK1.5
及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数
等的前面,用来对这些元素进行说明,注释。
元数据: 元数据是一个非常广泛的概念,元数据的定义也非常简单,只要是描述数据的数据都是元数据。举个例子🌰:如果有一个数字
20
,我们并不能知道20
是什么,但是我们对其加以描述,例如20天
、20kg重量
,那么天数
、重量
这类信息就是描述20这个数据的元数据。
注解是以@注解名
在代码中存在的,根据注解参数的个数,我们可以将注解分为:标记注解、单值注解、完整注解
三类。它们都不会直接影响到程序的语义,只是作为注解(标识)存在,我们可以通过反射机制编程实现对这些元数据(用来描述数据的数据)的访问。
注意: 注解本身是没有任何作用的,比如
@RequestMapping
,在controller
层@RequestMapping
基本上可以说是必见的,我们都知道他的作用是请求映射,通过他来设置请求的url地址,举例:将@RequestMapping("config")
写在方法上,然后我们就可以通过url
地址访问到这个方法了,但是记住这并不是@RequestMapping
注解的功能,SpringMVC
会通过反射将注解当中设置的属性值config
拿到,然后将url
和你声明注解的方法进行绑定。
所以: 注解只是起标记作用,注解真正的功能都是由框架通过反射来实现的。换句话说,注解离开了反射啥也不是!
注解的分类
注解大致可以根据注解参数(成员变量)个数、注解作用进行分类。
标记注解
标记注解不包含成员/元素。它仅用于标记声明。
其语法为:@AnnotationName()
由于这些注解不包含元素,因此不需要括号。例如:
@Override
单元素注解
单个元素注解仅包含一个元素。
其语法为:@AnnotationName(elementName = "elementValue")
如果只有一个元素,习惯上将该元素命名为
value
:@AnnotationName(value = "elementValue")
在这种情况下,也可以移除元素名称。元素名称默认为value
:@AnnotationName("elementValue")
多元素注解
这些注解包含多个用逗号分隔的元素。
其语法为:@AnnotationName(element1 = "value1", element2 = "value2")
预定义注解
预定义注解都是java.lang
包下的,也就是Java提供的基础注解,直接使用,无需导包。
预定义注解有:
@Deprecated
:该注解可作用于方法、属性、类,表示已过时,不建议使用,但仍可以使用。@Override
:该注解的作用是对覆盖超类中的方法进行标记,如果被标记的方法未被实际覆盖,则编译器会警告。@SuppressWarnings
:作用是消除编译器的警告,满足程序员的洁癖。@SafeVarargs
:在声明具有模糊类型(比如:泛型)的可变参数的构造函数或方法时,Java编译器会报unchecked
警告。鉴于这些情况,如果程序员断定声明的构造函数和方法的主体不会对其varargs
参数执行潜在的不安全的操作,可使用@SafeVarargs
进行标记。但是只能作用于构造函数和static
或final
修饰的可变长参数方法上。@FunctionalInterface
:该注解只能作用域函数接口,标记该接口是一个函数接口,一旦不满足就会报错。
元注解
元注解都是来自java.lang.annotation
包下,用于描述注解的属性(元数据描述数据,元注解描述注解),所以这些注解只能修饰注解,但是@Native
除外,它是作用于Field
(字段)。
自定义注解
自定义注解主要包含以下几个部分:
- 元注解
public @interface {注解名} {属性}
自定义注解实战:
- 自定义
@Init
注解
1 |
|
- 创建
User
类
1 | public class User { |
- 创建
UserFactory
工厂(注解实际发挥作用的地方)
1 | import java.lang.reflect.Method; |
- 测试
1 | public static void main(String[] args) { |
- 输出结果
1 | smallboat |
动态代理
什么是动态代理
动态代理(Dynamic Proxy) 是一种在运行时动态创建代理对象的机制,它允许你在调用实际对象的方法之前或之后执行额外的逻辑。动态代理通常用于实现横切关注点(cross-cutting concerns)如日志记录、性能监测、事务管理等。
动态代理的工作原理是在运行时创建一个代理类或者代理对象,这个代理对象实现了与被代理对象相同的接口,同时还包含了额外的逻辑。当调用代理对象的方法时,实际上是调用了代理对象的处理器(InvocationHandler)中的逻辑,处理器再决定是否调用被代理对象的方法以及在何时调用。
动态代理的作用在于创建代理对象对原有对象进行功能增强的同时,降低程序的耦合度。
当前实现动态代理的方式有两种:
- JDK提供的动态代理
- 第三方提供的CGLIB动态代理
两者的区别:
- JDK 动态代理只能代理实现了接口的类,而 CGLIB 动态代理可以代理没有实现接口的类。
- JDK 动态代理生成的代理类是目标类的接口的实现类,而 CGLIB 动态代理生成的代理类是目标类的子类。
- JDK 动态代理不需要额外的依赖,而 CGLIB 动态代理需要单独的依赖。
- JDK 动态代理性能相对较低,因为使用了反射,而 CGLIB 动态代理性能相对较高,因为使用了字节码技术。
图片来自JavaGuide
JDK动态代理
JDK 动态代理是 Java 标准库提供的一种动态代理机制,它允许在运行时动态生成代理类和代理对象,用于实现横切关注点(cross-cutting concerns)如日志记录、性能监测、事务管理等。JDK 动态代理主要基于两个核心类:java.lang.reflect.Proxy
和 java.lang.reflect.InvocationHandler
。
特点:
- JDK 动态代理是 Java 标准库的一部分,不需要额外的依赖。
- 它只能代理实现了接口的类,即目标类必须实现至少一个接口。
- 使用反射生成代理类和代理对象,代理类和代理对象都是在运行时动态生成的。
- 代理类和代理对象都是直接实现了被代理接口的,因此不需要继承被代理类。
优点:
- DK 动态代理是 Java 标准库的一部分,因此不需要额外的依赖。
- 相对简单易用,适用于代理接口的场景。
缺点:
- 只能代理接口,无法代理类。
- 要求目标类必须实现接口,有时会导致代码结构的调整。
- 性能相对较低,因为代理类和代理对象都是通过反射动态生成的。
代码示例(实现add功能的同时增加日志):
- 定义接口
1 | interface Calculator { |
- 定义接口实现类
1 | class CalculatorImpl implements Calculator { |
- 实现
InvocationHandler
接口(日志记录拦截器)
1 | class LoggingHandler implements InvocationHandler { |
- 使用代理处理业务
1 | public class DynamicProxyExample { |
CGLIB动态代理
CGLIB 是一个强大的、高性能的代码生成库,它用于在运行时生成字节码,通过扩展被代理类来创建代理对象。与 Java 标准库中的动态代理不同,CGLIB 可以代理那些没有实现接口的类,并且不需要目标对象实现任何接口。
特点:
- CGLIB 是一个独立的第三方库,需要单独的依赖。
- 它能够代理没有实现接口的类,即目标类可以是普通的类,而不需要实现接口。
- 使用字节码技术生成代理类,代理类是目标类的子类,因此可以代理 final 类和方法。
- 生成的代理对象是目标类的子类,因此需要继承目标类。
优点:
- 能够代理没有实现接口的类,更加灵活。
- 性能相对较高,因为代理类是目标类的子类,不需要使用反射。
缺点:
- 需要额外的依赖,不是 Java 标准库的一部分。
- 生成的代理类是目标类的子类,可能会有类加载器和字节码兼容性等问题。
- 无法代理 final 类和方法以及 static 方法。
代码示例(实现add功能的同时增加日志):
- 实际实现类(注意这次不需要实现接口)
1 | class Calculator { |
- 实现
InvocationHandler
接口(日志记录拦截器)
1 | class LoggingInterceptor implements MethodInterceptor { |
- 使用代理处理业务
1 | public class CglibProxyExample { |