注解&反射
为什么要学习注解与反射?
注解与反射作为Java语言特性的后半程的高级部分,学习曲线是比较陡峭的。但它又是Java初学者必须跨越的门槛。因为在当下Java的市场,技术选型中的SSM占据着大部分份额。而Spring中两大核心内容的Ioc容器与AOP、MyBatis的ORM,它们的底层实现都是由注解与反射所构建的。若注解反射掌握的不好,很难去真正理解框架的本质,也就自然很难脱离他人的协助去很好的借助框架编写项目。可以说,学不好递归,就与掌握非线性数据结构无缘。学不好注解反射,就与掌握Java框架无缘。
学习之前
注解是建立在Java的发射机制上的特性。所以要弄懂注解,就要先弄懂反射,要弄懂反射,就要先弄懂类加载的过程。本文三点都会提及,并不需要自上而下的阅读。
光看并不利于理解,一定要跟着敲。不断地模仿、探索和反思是绝大多数人学习的方式,而不断地积累才会让你有这个能力去灵光一现。过渡妖魔化智力只会让你一事无成。
注解是什么?
注释是在JDK5.0版本被引入的特性。它是既属于程序,也不属于程序。他可以对程序作出解释,也可以被其他程序(编译器)进行解释。在程序中,很多代码块它们在整个程序的逻辑中并不关键,只是起到一个连接,配置的作用。我们认为将它们抽离出来作为一个配置文件,可以有效地提高程序的灵活性。而xml就是这么诞生的。xml就是这么诞生的。而可配置这个思想,后来也诞生了注解。注解与反射的结合,让一切变得更加简洁,当然xml与注解各有适合的应用场景。斟酌选择才是最吼的。
对注解要理解深刻,就需要去阅读JDK文档,再结合反射机制去进行编程。
java.lang
包下的JDK内置注解java.lang.annotation
包
注解的结构
注解作为Java特性,它有JDK官方提供的内置注解,开发者也可以自定义注解。
1 | (RetentionPolicy.RUNTIME) |
- 注解的声明是使用
@interface
,使用@interface
自定义注解时,就会自动继承java.lang.annitation.Annotation接口(手动继承无效,这个接口也不能定义自身为annotation类型) int value()
定义了该注解的一个属性,它看起来像个方法但并不是。注解的可用类型包括所有基本类型、String、Class、Enum、Annotation 以及以上类型的数组形式;注解属性不能有不确定的值,要么在定义注解时有默认值,要么在使用注解的时候提供属性的值,而且注解属性不能使用 null 作为默认值,通常用空字符或0作为默认值;在注解只有一个属性且该属性的名称是 value 的情况下,在使用注解的时候可以省略 value =,直接写需要的值即可。@Retention,@Target
均为元注解,它们也是一种注解。下面会进行介绍。
元注解
元注解负责注解其他注解,JDK定义了4个标准的meta-annotation
,它们被用来对其他annotation类型做说明。
- Target
指示注释类型所适用的程序元素的种类,如果注释类型声明中不存在 Target 元注释,则声明的类型可以用在任一程序元素上。如果存在这样的元注释,则编译器强制实施指定的使用限制。(人话就是描述注解的使用范围)
1 | //我们可以看到元注解也会被其他元注解所注解 |
- Retention
表明annotation能被保留多久。如果当前的annotation没有声明Retention注解,那么保留策略就是默认的
RetentionPolicy.CLASS
1 |
|
- Inherited
表明注解类型是自动继承的。如果一个Inherited元注解是当前注解的声明,同时用户在类声明中查询这个注解类型,如果该类没有该类型的注解类型,那么会自动查询这个类的父类的注解类型。这个过程会持续到直到找到这个类型,或到达了该类层次结构的顶层 (Object) 为止。如果超类没有这个注解的类型,那么查询会表明这个类没有这个的注解。
注意这个元注解类型对除了注解类以外的任何其他事物是无效的。注意这个元注解只被促成从超类继承注解;被实现的接口注解是无效的。
1 |
|
- Docuemented
表明用于被它描述annotation能被类似javadoc的工具文档化。如果是由Documented注解的类型,那么这些类型就会成为注释元素的公共API的一部分。
1 |
|
JDK内置注解
JDK官方提供的注解
- Overrided
一个方法声明打算重写超类中的一个方法声明
1 | ({ElementType.METHOD}) |
- Deprecated
表示不鼓励去使用,通常有更好的选择
1 |
|
- SuppressWarnings
用来抑制编译时的警告信息
1 | ({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE}) |
####注解如何生效
注解一共有两个作用:
- 让编译器检查代码
- 将数据注入到类、字段、方法中
讲到目前,好像还是不知道注解是如何起作用的,我们为什么在JavaWeb项目中对相应的类上方进行@WebServlet()
注解,即可达到效果(使用标注(Annotation)来告诉容器哪些Servlet会提供服务以及额外的信息),其实它的运作方式和寻常的字段区别并不大,等待被方法所读取并使用,只不过很多东西官方把它封装了起来,提前写好了。
1 | //注解PeopleAttribute负责注入实体类People字段中 |
1 |
|
1 |
|
1 | People{id=0, name='null', age=0} //注入前 |
我们可以看到,其实注解在整个方法中,更多起到的是被读取的角色,而注入这个过程,是由反射来完成的。其实在日常的编写中,我们只需要知晓注解的基本原理,剩下的更多的是对反射的掌握。
注解的本质是什么?
注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池。
若想继续深入了解注解,可以查看这篇文章 十分详细。
反射是什么?
反射是Java被视为动态语言的关键,反射机制允许程序在运行时期借助
Reflection API
取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。加载完类之后,在堆内存的方法去就产生了一个Class类型的对象(一个类就只有一个Class对象),这个类就包含了完整的类的机构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,我们形象的称之为“反射”。
为什么我们去强调运行时,在刚刚学习反射时我们常常有一个疑问,为什么不能正常的去new一个对象?要去兜圈子。其实这个解决方案的出现,是由于JavaWeb的大型项目会存在非常多的对象,且随着项目的不断开发,对象之间的依赖关系会越来越复杂,如一片丛林。我们开始思考让JVM来管理这一切。下面是一段引言,我相信只要学习了Spring 的基本思想,就能够大致看懂这段引言。从而去思考注解反射的存在。
使用过Spring框架进行web开发的应该都知道,Spring的两大核心技术是IOC和AOP。而其中IOC又是AOP的支撑。IOC要求由容器来帮我们自动创建Bean实例并完成依赖注入。IOC容器的代码在实现时肯定不知道要创建哪些Bean,并且这些Bean之间的依赖关系是怎样的(如果写死在里面,这框架还能用吗?)。所以其必须在运行期通过扫描配置文件或注解的方式来确定需要为哪些类创建实例。通俗的说,必须在运行时为编译期还不能确定的类创建实例。再直白一点,必须提供一种new Object()之外的创建对象的方法。依赖注入存在类似的问题,容器必须能够在运行时发现所有标注有@Autowired或@Resource的字段或方法,并且能够在不知道对象的任何类型信息的情况下调用其setter方法完成依赖的注入(默认bean的字段都会实现setter方法)。总结一下IOC容器在实现时必须做到的三件看起来“不太可能的事”。
1.提供new之外的创建对象的方法,这个对象的类型在编译期不能确定。
2.能够在运行期知道类的结构信息,包括:方法,字段以及其上的注解信息等。
3.能够在运行期对编译期不能确定任何类型或接口信息的对象进行方法调用。
而这些,在java的反射技术下成为了可能。应该说反射技术并不仅仅在IOC容器中被使用,它是整个Spring框架的底层核心技术之一,是Spring实现通用型和扩展性的基石。
反射相关APi
对反射要理解深刻,就需要去阅读JDK文档,再结合注解去进行编程。
java.lang.Class
java.lang.reflect.Method
java.lang.reflect.Field
java.lang.reflect.Constructor
...
关于反射的知识点
反射做的事,借助lang包Class类的与Reflect包的几大类,去创建对象,方法调用,字段赋值等。
要理解反射原理,需要学习以下几点:
- Class类->Class对象
- 类加载->类加载内存分析
- 类加载器(浅)
- Reflect包API -> 编码实践
Class类
你不必先去理解Class类的意义何为,但可以在阅读JDK文档Class类的方法集、以及以下的讲解,大致的猜想它的作用
对象照镜子后得到的信息;某个类的属性、方法、构造器、某个类到底实现了那些接口。对于每个类而言,JRE都为其保留了一个不变的Class类型的对象。一个Class对象包含了特定(class/interface/enum/annotation/primitive type/void/[])的有关信息。
class本身也是一个类
- Class对象只能由系统所建立
- 一个加载的类JVM中只会有一个Class实例
- 一个Class对象对应的是一个加载到JVM中的一个.class文件
- 每个类的实例都会记得自己是由哪个Class实例所生成
- 通过class可以完整地得到一个类中所有的被加载的结构
- Class类是Reflection的根源,针对任何你想动态加载,运行的类,唯有先获得相应的class对象
Class类常用API
Method | description |
---|---|
forName(String name) | 返回指定类名name的class对象 |
newInstance() | 调用缺省构造函数,返回class对象的一个实例 |
getName() | 返回此Class对象所表示的实体(类、接口、数组类、void)的名称 |
getSuperClass() | 返回当前Class对象的父类Class对象 |
getInterfaces() | 获取当前Class对象 |
getClassLoader() | 返回该类的类加载器 |
getConstructor() | 返回一个包含某些Constructor对象的数组 |
getMethod(String name,class …T) | 返回一个Method对象,此对象的形参类型为paramType |
getDeclaredField() | 返回FIeld对象的一个数组 |
获取Class类的实例(获取Class对象)
- 已知具体的类,通过类的class属性去获取,该方法最为安全可靠,程序性能最高
1 | Class clazz = User.class; |
- 已知某个类的实例,调用该实例的
getClass()
获取class对象
1 | Class clazz = user.getClass(); |
- 已知一个类的全限定类名,且该类在类路径下,可通过class类的静态方法
forName()
获取,可能会抛出ClassNotFoundException
1 | Class clazz = class.forName('xxx.xxx.xxx.xxx.class'); |
Ps: 获得类对象是反射操作的开端,第二种静态方法forName()
会引起类的初始化
哪些类型可以有Class对象
Type | description |
---|---|
Class | 外部类、成员(成员内部类、静态内部类)、局部内部类、匿名内部类 |
interface | 接口 |
[] | 数组 |
enum | 枚举 |
annotation | 注解@interface |
primitive type | 基本数据类型 |
void | 空 |
Ps:只要元素类型与维度一样就是同一个class对象
类加载内存分析
要了解反射,就要了解JVM针对内存、类的基本运行原理
Java内存
- 堆 :存放new的对象,数组;可以被所有的线程共享,不会存放到别的对象引用
- 栈 :存放基本数据类型(会包含这个基本类型的具体数值);引用对象的变量。(会存放这个引用在堆里面的具体地址)
- 方法区 : 可以被所有的线程共享;包含了所有的class和static变量
了解类的加载过程,当初许主动使用某个类时,如果该类还未加载到内存中,则系统会通过三个步骤对该类进行初始化。
类的加载、链接、初始化
加载: 将Class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时结构。在堆内存中生成一个代表这个类的
java.lang.class
对象链接: 将Java类的二进制代码合并到JVM的运行之中的过程
- 验证: 确保加载的类信息符合JVm规范,没有安全方面的问题
- 文件格式验证:如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等
此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java类型信息的要求 - 元数据验证:是否存在父类,父类的继承链是否正确,抽象类是否实现了其父类或接口之中要求实现的所有方法,字段、方法是否 与父类产生矛盾等
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上
- 符号引用验证:在解析阶段中发生,保证可以将符号引用转化为直接引用
- 文件格式验证:如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等
- 准备: 虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程、为类变量分配内存并设置类变量初始值,这些变量所使用 的内存都将在方法区中进行分配
- 解析: 虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行
- 验证: 确保加载的类信息符合JVm规范,没有安全方面的问题
初始化: 执行类构造器
<clinit>()
方法的过程。类构造器<clinit>()
方法是有编译期自动收集类中所有变量的赋值动作和静态代码块儿中的语句拼接出来的。(类构造器是构造类信息的,不是构造该类对象的构造器) 当初始化一个类时,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。 虚拟机汇保证一个类<clinit>()
方法在多线程环境中被正确加锁、同步
类什么时候会发生初始化?
- JVM启动时,先初始化main方法所在的类
- new一个类的对象
- 调用类的静态成员(除了final常量)和静态方法
- 使用java.lang.reflect包的方法对类进行反射调用
- 当初始化一个表,如果父类没有被初始化,则会先初始化它的父亲
类的被动引用(不会发生勒的初始化)
- 当访问一个静态域时,只有正在声明这个域的类才会被初始化。如:当通过子类引用父类的静态变量,不会导致子类初始化。
- 通过数组定义类引用,不会触发此类的初始化。
- 引用常量不会触发此类的初始化(常量在链接阶段就已经存入调用类的常量池中)
类加载器
顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance() 方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
作用:
将class文件字节码内容加载到内存中。并将这些静态数据转换成方法区的运行时数据结构。然后在堆中生成这个累的java.lang.class对象,作为访问方法区中类数据的访问入口。
类缓存:
标准的JavaSE类加载器可以按要求查找类,可一旦某个类被加载到类加载器中,他将维持加载(缓存)一段时间,不过JVM垃圾回收机制(GC)可以回收这些class对象。
类加载器的不同
类加载器作用是用来类装载进内存的。JVM规范定义了如下类型的类的加载器。
- 引导类加载器: 也叫根加载器,由C++编写,Java自带的类加载器,负责Java平台核心库。用来装载核心库类,该加载类无法直接获取(C++编写是这样的)
- 扩展类加载器: 负责jre/lib/ext目录下的jar包或-D java.ext.dirs指定目录下的jar包装入工作库。
- 系统类加载器: 负责java -classpath或-D java.class.path所指的目录下的类与jar包装入工作,是最常用的加载器。
1 | ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); |
1 | SystemClassLoader: sun.misc.Launcher$AppClassLoader 4aac2 |
Ps:图中也阐述了双亲委派机制
双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。
双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
反射基本操作
1 | public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException { |
1 | 获得的无参构造器: public com.jason.entity.People() |
反射操作泛型
Java采用泛型擦除的机制来引入了泛型,Java中的泛型仅仅是给编译器javac使用的,确保数据的安全性和免去强制类型的转换问题,可一旦编译完成,所有和泛型有关的类型全部擦除。
为了通过反射操作这些类型,Java新增了
Parameter
,GenericArrayType
,TypeVariable和WildcardType几种类型来代表不能被归一到Class类中的类型但是又和原始类型齐名的类型。
Type | description |
---|---|
ParameterizedType | 表示一种参数化类型,例Collection<String> |
GenericArrayType | 表示一种元素类型是参数化类型或者参数变量的数组类型 |
TypeVariable | 是各种类型变量的公共接口 |
WildcardType | 代表一种通配符类型表达式 |
1 | public void test01(Map<String, User> map, List<User> list) { |
1 | java.util.Map<java.lang.String, com.jason.entity.User> |
关于setAccessible()
置访问性,反射类的方法,设置为true就可以访问private
修饰的东西,否则无法访问
即使是public
修饰我们也要用setAccessible方法
,对于反射性能的提升是非常大的。
1 | public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { |
1 | 正常调用10亿次0.412s |
反射+注解实现ORM功能
1 | //注解包 |
1 | //实体类 |
1 | /*** |
利用注解+反射的机制拼接除了简单的SQL语句(甚至可以学到StringJoiner hhh)1
CREATE TABLE Stu_Table(ID VARCHAR(30) PRIMARY KEY ,NAME VARCHAR(30) NOT NULL ,AGE INT ,PHONENUMBER INT NOT NULL UNIQUE )
总结
反射可以实现动态创建对象与编译,体现出了很大的灵活性。但它对性能却有着很明显的影响,反射本身是一种解释操作,告诉JVM我们希望做什么并且它满足我们的要求,这类操作总是慢于直接执行相同的操作。过多的反射确实能减少开发的工作量,但会给服务器带来多出的负担,一定要在权衡了开发效率和执行性能后,视场景和性能要求谨慎使用。
注解+反射的组合远远没有这些用途,它真正的用途是作用与目前Java的实施标准框架的Spring的底层核心,IOC以及AOP。动态代理是一块非常重要的点,而要掌握它,就要先掌握基本的注解与反射。