什么是注解?
注解是JDK1.5
引入的一个语法糖,它主要用来当作元数据,简单的说就是用于解释数据的数据。在Java中,类、方法、变量、参数、包都可以被注解。很多开源框架都使用了注解,例如Spring
、MyBatis
、Junit
。我们平常最常见的注解可能就是@Override
了,该注解用来标识一个重写的函数。
注解的作用:
配置文件:替代
xml
等文本文件格式的配置文件。使用注解作为配置文件可以在代码中实现动态配置,相比外部配置文件,注解的方式会减少很多文本量。但缺点也很明显,更改配置需要对代码进行重新编译,无法像外部配置文件一样进行集中管理(所以现在基本都是外部配置文件+注解混合使用)。数据的标记:注解可以作为一个标记(例如:被
@Override
标记的方法代表被重写的方法)。减少重复代码:注解可以减少重复且乏味的代码。比如我们定义一个
@ValidateInt
,然后通过反射来获得类中所有成员变量,只要是含有@ValidateInt
注解的成员变量,我们就可以对其进行数据的规则校验。
定义一个注解非常简单,只需要遵循以下的语法规则:
|
|
我们发现上面的代码在定义注解时也使用了注解,这些注解被称为元注解。作用于注解上的注解称为元注解(元注解其实就是注解的元数据),Java
中一共有以下元注解。
@Target
:用于描述注解的使用范围(注解可以用在什么地方)。ElementType.CONSTRUCTOR:构造器。
ElementType.FIELD:成员变量。
ElementType.LOCAL_VARIABLE:局部变量。
ElementType.PACKAGE:包。
ElementType.PARAMETER:参数。
ElementType.METHOD:方法。
ElementType.TYPE:类、接口(包括注解类型) 或enum声明。
@Retention
:注解的生命周期,用于表示该注解会在什么时期保留。RetentionPolicy.RUNTIME:运行时保留,这样就可以通过反射获得了。
RetentionPolicy.CLASS:在class文件中保留。
RetentionPolicy.SOURCE:在源文件中保留。
@Documented
:表示该注解会被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。@Inherited
:表示该注解是可被继承的(如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类)。
了解了这些基础知识之后,接着完成上述定义的@ValidateInt
,我们定义一个Cat
类然后在它的成员变量中使用@ValidateInt
,并通过反射进行数据校验。
|
|
本文作者为:SylvanasSun(sylvanas.sun@gmail.com),首发于SylvanasSun’s Blog。
原文链接:https://sylvanassun.github.io/2017/10/15/2017-10-15-JavaAnnotation/
(转载请务必保留本段声明,并且保留超链接。)
注解的实现
注解其实只是Java
的一颗语法糖(语法糖是一种方便程序员使用的语法规则,但它其实并没有表面上那么神奇的功能,只不过是由编译器帮程序员生成那些繁琐的代码)。在Java
中这样的语法糖还有很多,例如enum
、泛型、forEach
等。
通过阅读JLS(Java Language Specification(当你想了解一个语言特性的实现时,最好的方法就是阅读官方规范)发现,注解是一个继承自java.lang.annotation.Annotation
接口的特殊接口,原文如下:
|
|
|
|
我们将上节定义的@ValidateInt
注解进行反编译来验证这个说法。
|
|
public interface com.sun.annotation.ValidateInt extends java.lang.annotation.Annotation
,很明显ValidateInt
继承自java.lang.annotation.Annotation
。
那么,如果注解只是一个接口,又是如何实现对属性的设置呢?这是因为Java
使用了动态代理对我们定义的注解接口生成了一个代理类,而对注解的属性设置其实都是在对这个代理类中的变量进行赋值。所以我们才能用反射获得注解中的各种属性。
为了证实注解其实是个动态代理对象,接下来我们使用CLHSDB(Command-Line HotSpot Debugger)
来查看JVM
的运行时数据。如果有童鞋不了解怎么使用的话,可以参考R大的文章借HSDB来探索HotSpot VM的运行时数据 - Script Ahead, Code Behind - ITeye博客。
|
|
注解的类型为com/sun/proxy/$Proxy1
,这正是动态代理生成代理类的默认类型,com/sun/proxy
为默认包名,$Proxy
是默认的类名,1
为自增的编号。
实践-包扫描器
我们在使用Spring
的时候,只需要指定一个包名,框架就会去扫描该包下所有带有Spring
中的注解的类。实现一个包扫描器很简单,主要思路如下:
先将传入的包名通过类加载器获得项目内的路径。
然后遍历并获得该路径下的所有class文件路径(需要处理为包名的格式)。
得到了class文件的路径就可以使用反射生成Class对象并获得其中的各种信息了。
定义包扫描器接口:
|
|
函数2需要传入一个ScannedClassHandler
接口,该接口是我们定义的回调函数,用于在扫描所有类文件之后执行的处理操作。
|
|
我想要包扫描器可以识别和支持不同的文件类型,定义一个枚举类ResourceType
:
|
|
PathUtils
是一个用来处理路径和包转换等操作的工具类:
|
|
定义了这些辅助类之后,就可以去实现包扫描器了。
|
|
函数getResource()
会根据包名来通过类加载器获得当前项目下的URL对象,如果这个URL为空则直接返回一个空的ArrayList
。
|
|
函数parseUrlThenScan()
会解析URL对象并进行扫描,最终返回一个类列表。
|
|
函数getClassListFromFile()
会扫描路径下的所有class文件,并拼接包名生成Class对象。
|
|
函数getClassListFromJar()
会扫描Jar中的class文件。
|
|
函数invokeCallback()
遍历类对象列表,然后执行回调函数。
|
|
本节中实现的包扫描器源码地址:https://gist.github.com/SylvanasSun/6ab31dcfd9670f29a46917decdba36d1