java基础强化——深入理解java注解(附简单ORM功能实现)
1. 什么是注解
注解是 java1.5 引入的新特性, 它是嵌入代码中的元数据信息, 元数据是解释数据的数据。通俗的说,注解是解释代码的代码。这个定义强调了三点,
- 1. 注解是代码
这意味着注解可以被程序读取并解析。它可以被编译器编译成 class 文件, 也可以被 JVM 加载进内存在运行时进行解析。JDK 中的 "@Override" 就是注解。它不仅解释了这是个重写方法, 还能在被错误使用 (被注解的方法没有重写父类方法) 时让编译器给出错误提示。Spring 中的“Controller”就是注解, 它可以在运行时被 JVM 读取到并为被其修饰的类创建实例。 - 2. 注解起到的是描述和解释作用。这点和注释有点像。但注释面向的对象主要是开发者, 且只能在源码阶段存在;注解面向的对象主要是程序, 且可以再编译期和运行期存在。
- 3. 注解需要关联特定的代码, 如果不存在需要解释的代码, 那么注解就毫无意义了。
2.1 注解的组成
下面是一个自定义注解的例子:
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
public @interface ClassAnnotation {
String <span class="hljs-title function_">name</span><span class="hljs-params">()</span> <span class="hljs-keyword">default</span> <span class="hljs-string">""</span>;
<span class="hljs-type">boolean</span> <span class="hljs-title function_">singleton</span><span class="hljs-params">()</span> <span class="hljs-keyword">default</span> <span class="hljs-literal">false</span>;
}
注解由声明, 属性,元注解三部分构成。
- 1. 注解声明
由@interface
声明ClassAnnotation
为注解类型, 注意比interface
多了个@
符号。 - 2. 注解的属性
上面定义了两个属性:String 类型的 name 属性, 默认值为空字符串;boolean 类型的 singleton 属性,默认值为 false. 注意虽然后面带了括号, 但并不是方法。如果注解内部只定义了一个属性,该属性名通常为 value, 且在使用的时候可以省略value=
, 直接写值。
注解的属性类型支持的类型有: 所有基本类型,String,Class,enum,Anotation 以及上述类型的数组类型。 - 3. 元注解
元注解是注解的注解。有点绕,只要知道它是注解, 并且使用在注解上, 可以对注解进行解释就行。上面使用了两个元注解@Retention
和@Target
。这是最常使用的元注解。关于它们有后面会进行详细说明。
2.2 注解的类层级结构
任何注解类型都默认继承自 java.lang.annotation 包下的 Annotation 接口, 表明这是一个注解类型,这是编译器自动帮我们完成的。但是手动继承 Annotation 没有这个效果, 即不会把它当成注解类型。甚至 Annotation 接口本身也并不意味着它是注解类型。很奇怪也很绕, 然而很遗憾规则就是这么定义的。可以简单的理解为: 我们可以也只可以通过@interface
的方式来定义注解类型, 这个注解类型默认会实现 Annotation 接口。来看看 Annotation 接口的结构
根据面向接口编程原则, 在编写代码时可以用 Annotation 接口引用不同的注解类型, 在运行时才通过接口的 annotationType() 方法获得具体的注解信息。
2.3 如何在运行时获得注解信息
注解通过设置可以一直保留到运行期, 此时 VM 通过反射的方式读取注解信息。由上面的介绍可知, 注解是解释代码的代码, 它必须存在于特定的代码元素之上,可以是类,可以是方法,可以是字段等等。
为了更好的在运行时解析这些代码元素上的注解,java 在反射包下为它们提供了一个抽象, 如下图所示
里面定义了一些获取该元素上注解信息的方法。
而 Class,Field,Method,Constructor 等可以在运行时被反射获取的元素都实现了 AnnotationElement 接口, 如下图所示
因此当我们在获得了包含注解的 Clazz,Method,Field 等对象后, 可以直接通过 AnnotationElement 接口中的方法获得其上的注解信息。
3. 几种元注解介绍
3.1 @Retention
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
用来表示被其修饰的注解的生命周期, 即该注解的信息会在什么级别被保留。Retention 只有一个属性 value, 类型为 RetentionPolicy, 这是一个枚举值, 可以由以下取值
- SOURCE
源码有效: 表示该注解 (被 @Retention 注解的注解) 仅在源码阶段存在, 编译阶段就会被编译器丢弃。 - CLASS
编译期有效: 注解信息会被编译进 class 文件中, 但是不会被 JVM 加载。当注解未定义 Retention 值时,这是默认的级别。 - RUNTIME
运行期有效: 注解信息会被编译进 class 文件中, 且会被 JVM 加载并可在运行期被 JVM 以反射的方式读取。
3.2 @Target
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation type
* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
ElementType[] value();
}
用来表示被其修饰的注解可以用在什么地方。该注解只有一个属性值 value,类型为 ElementType 数组, 这意味着通常注解可以被用在多个不同的地方。来看看 ElementType 都有哪些值, 分别代表什么意思。
- TYPE
表示类,接口 (包括注解类型), 枚举类型 - FIELD
表示类成员 - METHOD
表示方法 - PARAMETER
表示方法参数 - CONSTRUCTOR
表示构造方法 - LOCAL_VARIABLE
表示局部变量 - ANNOTATION_TYPE
表示注解类型 - PACKAGE
表示包 - TYPE_PARAMETER
1.8 新加, 表示类型参数 - TYPE_USE
1.8 新加, 表示类型使用
可以看到 ElementType 枚举值相当多, 几乎囊括了所有元素类型。这也意味着注解几乎可以用在所有地方。但最常见得还是用在类, 成员变量和成员方法上。
3.3 @Documented
这是一个标记注解。用来表示被其修饰的注解在被使用时会被 Javadoc 工具文档化。
3.4 @Inherited
这也是一个标记注解。表示被其修饰的注解可被继承。通俗的解释: 若注解 A 被元注解 @Inherited 修饰, 则当注解 A 被用在父类上时, 其子类也会自动继承这个注解 A。来看下面这个演示的例子。
- 创建一个被 @Inherited 描述的自定义注解 @InheritedAnnotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface InheritedAnnotation {
}
- 创建父类, 并在类上标注 @InheritedAnnotation 注解
@InheritedAnnotation
public class SuperClass {
}
- 子类继承父类并测试
class TestClass extends SuperClass{
public static void main(<span class="hljs-type">String</span>[] args) {
<span class="hljs-type">Annotation</span>[] annotations = <span class="hljs-type">TestClass</span>.<span class="hljs-keyword">class</span>.getAnnotations();
<span class="hljs-keyword">for</span>(<span class="hljs-type">Annotation</span> annotation:annotations){
<span class="hljs-type">System</span>.out.println(annotation);
}
}
}
- 测试结果
可以看到子类虽然没有被 @InheritedAnnotation 注解,但是其继承的父类上有该注解,故而 @InheritedAnnotation 注解也作用在了子类上。
原理如下:当 JVM 要查询的注解是一个被 @Inherited 描述的注解,会不断递归的检查父类中是否存在该注解,如果存在,则会认为该类也被该注解修饰。
3.5 @Repeatable
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
/**
* Indicates the <em>containing annotation type</em> for the
* repeatable annotation type.
* @return the containing annotation type
*/
Class<? extends Annotation> value();
}
这是 java8 种引入的一个新的元注解, 被其修饰的注解将能够被在同一个地方重复使用, 这在原来是办不到的。注意每一个可重复使用的注解都必须有一个容纳这些可重复使用注解的容器注解。这个容器注解就是 Repeatable 的 value 属性值。
来看一个简单的例子
- 自定义可重复注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(RepeatableAnnotations.class)
public @interface RepeatableAnnotation {
String <span class="hljs-title function_">name</span><span class="hljs-params">()</span> <span class="hljs-keyword">default</span> <span class="hljs-string">""</span>;
}
- 自定义可重复注解的容器注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RepeatableAnnotations {
<span class="hljs-selector-tag">RepeatableAnnotation</span><span class="hljs-selector-attr">[]</span> <span class="hljs-selector-tag">value</span>();
}
Repeatable(RepeatableAnnotations.class) 指定了 @RepeatableAnnotation 为可重复使用的注解, 同时指定了该注解的容器注解为 @RepeatableAnnotations。那我们该如何在运行时获得这些重复注解的信息?
- 运行时获取注解
@RepeatableAnnotation("first")
@RepeatableAnnotation("second")
public class AnnotationTest {
<span class="hljs-keyword">public</span> static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
Class<?> clazz = Class.forName(<span class="hljs-string">"com.takumiCX.AnnotationTest"</span>);
<span class="hljs-comment">//当元素上有重复注解时,使用该方法会返回null</span>
RepeatableAnnotation annotation1 = clazz.getAnnotation(RepeatableAnnotation.<span class="hljs-keyword">class</span>);
System.<span class="hljs-keyword">out</span>.println(annotation1);
<span class="hljs-comment">//使用该方法获取元素上的重复注解</span>
RepeatableAnnotation[] annotations = clazz.getAnnotationsByType(RepeatableAnnotation.<span class="hljs-keyword">class</span>);
<span class="hljs-keyword">for</span>(Annotation <span class="hljs-keyword">annotation</span>:annotations){
System.<span class="hljs-keyword">out</span>.println(<span class="hljs-keyword">annotation</span>);
}
}
}
注意多个重复注解会被自动存放到与之关联的容器注解里。所以我们这里要获得所有 @RepeatableAnnotation 注解, 不能使用 getAnnotation 方法, 而应该使用 getAnnotationByType 方法。最后的结果如下
4. 使用反射和注解完成简单的 ORM 功能
4.1 ORM 原理简介
ORM 是对象关系映射的意思。他建立起了以下映射关系:
- 类对应于表
- 对象对应于表中的记录
- 对象的属性对应于表的字段
有了这种映射关系, 我们在编写代码时就可以通过操作对象来映射对数据库表的操作, 比如添加记录, 更新记录, 删除记录等等。常见的 Mybatis,Hibernate 就是 ORM 框架。而实现 ORM 功能最常用的手段就是注解 + 反射。由注解维护这种映射关系, 然后运行期通过反射技术解析注解, 完成对应关系的转换, 从而形成一句完整的 sql 去执行。
下面以建表为例, 实现简单的 ORM 功能。
4.2 ORM 实战
- 自定义表注解, 完成类和表的映射。
/**
* 自定义表注解, 完成类和表的映射
*/
@Retention(RetentionPolicy.RUNTIME) // 因为要使用到反射, 故注解信息必须保留到运行时
@Target(ElementType.TYPE)// 只能用在类上
public @interface MyTable {
<span class="hljs-comment">//表名</span>
<span class="hljs-selector-tag">String</span> <span class="hljs-selector-tag">value</span>();
}
- 自定义字段注解
/**
* 自定义字段注解, 完成类属性和表字段的映射
*/
@Retention(RetentionPolicy.RUNTIME)//要反射,故注解信息需要保留到运行期
@Target(ElementType.FIELD)//只能用在类属性上
public @interface MyColumn {
<span class="hljs-comment">//字段名</span>
String <span class="hljs-built_in">value</span>();
<span class="hljs-comment">//字段类型,默认为字符串类型</span>
String <span class="hljs-built_in">type</span>() default "<span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">30</span>)";<span class="hljs-comment">//字段类型,默认为VARCHAR类型</span>
<span class="hljs-comment">//类型为注解类型的字段约束,默认的约束为:非主键,非唯一字段,不能为null</span>
Constraints <span class="hljs-built_in">constraint</span>() default <span class="hljs-keyword">@Constraints</span>;
}
- 自定义字段约束注解
/**
* 约束注解: 主键, 是否为空, 是否唯一等信息。
*/
@Retention(RetentionPolicy.RUNTIME)// 运行期
@Target(ElementType.FIELD)// 只能在类属性上使用
public @interface Constraints {
<span class="hljs-comment">//字段是否为主键约束</span>
<span class="hljs-selector-tag">boolean</span> <span class="hljs-selector-tag">primaryKey</span>() <span class="hljs-selector-tag">default</span> <span class="hljs-selector-tag">false</span>;
<span class="hljs-comment">//字段是否允许为null</span>
<span class="hljs-selector-tag">boolean</span> <span class="hljs-selector-tag">nullable</span>() <span class="hljs-selector-tag">default</span> <span class="hljs-selector-tag">false</span>;
<span class="hljs-comment">//字段是否唯一</span>
<span class="hljs-selector-tag">boolean</span> <span class="hljs-selector-tag">unique</span>() <span class="hljs-selector-tag">default</span> <span class="hljs-selector-tag">false</span>;
}
- 带注解的实体类
/**
* 带注解的实体类, 建立了对象和表的映射关系, 可以再运行时被解析
*/
@MyTable("t_user")
public class User {
<span class="hljs-comment">//主键,对应表字段id,类型为VARCHAR</span>
<span class="hljs-variable">@MyColumn</span>(value = <span class="hljs-string">"id"</span>, constraint = <span class="hljs-variable">@Constraints</span>(primaryKey = true))
private String id;
<span class="hljs-comment">//对应表字段name,类型为类型为VARCHAR</span>
<span class="hljs-variable">@MyColumn</span>(value = <span class="hljs-string">"name"</span>)
private String name;
<span class="hljs-comment">//对应表字段age,类型为INT,且可为null</span>
<span class="hljs-variable">@MyColumn</span>(value = <span class="hljs-string">"age"</span>, type = <span class="hljs-string">"INT"</span>, constraint = <span class="hljs-variable">@Constraints</span>(nullable = true))
private int age;
<span class="hljs-comment">//对应表字段phone_number,类型为VARCHAR,且有唯一约束</span>
<span class="hljs-variable">@MyColumn</span>(value = <span class="hljs-string">"phone_number"</span>, constraint = <span class="hljs-variable">@Constraints</span>(unique = true))
private String phoneNumber;
}
- 运行时注解解析器
/**
* 运行时注解解析器
*/
public class TableGenerator {
<span class="hljs-comment">/**
* 运行时解析注解生成对应的建表语句
*
* <span class="hljs-doctag">@param</span> clazz 与表对应的实体的Class对象
* <span class="hljs-doctag">@return</span>
*/</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-title class_">String</span> <span class="hljs-title function_">genSQL</span>(<span class="hljs-params">Class clazz</span>) {
<span class="hljs-title class_">String</span> table;<span class="hljs-comment">//表名</span>
<span class="hljs-title class_">List</span><<span class="hljs-title class_">String</span>> columnSegments = <span class="hljs-keyword">new</span> <span class="hljs-title class_">ArrayList</span><>();
<span class="hljs-comment">//获取表注解</span>
<span class="hljs-title class_">MyTable</span> myTable = (<span class="hljs-title class_">MyTable</span>) clazz.<span class="hljs-title function_">getAnnotation</span>(<span class="hljs-title class_">MyTable</span>.<span class="hljs-property">class</span>);
<span class="hljs-keyword">if</span> (myTable == <span class="hljs-literal">null</span>) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">IllegalArgumentException</span>(<span class="hljs-string">"表注解不能为空!"</span>);
}
<span class="hljs-comment">//获取表名</span>
table = myTable.<span class="hljs-title function_">value</span>();
<span class="hljs-comment">//获取所有字段</span>
<span class="hljs-title class_">Field</span>[] fields = clazz.<span class="hljs-title function_">getDeclaredFields</span>();
<span class="hljs-keyword">for</span> (<span class="hljs-title class_">Field</span> field : fields) {
<span class="hljs-title class_">MyColumn</span> column = field.<span class="hljs-title function_">getAnnotation</span>(<span class="hljs-title class_">MyColumn</span>.<span class="hljs-property">class</span>);
<span class="hljs-keyword">if</span> (column == <span class="hljs-literal">null</span>) {
<span class="hljs-keyword">continue</span>;<span class="hljs-comment">//为null说明该字段不为映射字段,也就是没有加上字段注解</span>
}
<span class="hljs-title class_">StringBuilder</span> columnSegement = <span class="hljs-keyword">new</span> <span class="hljs-title class_">StringBuilder</span>();<span class="hljs-comment">//字段分片,eg:"id varchar(50) primary key"</span>
<span class="hljs-title class_">String</span> columnType = column.<span class="hljs-title function_">type</span>().<span class="hljs-title function_">toUpperCase</span>();<span class="hljs-comment">//字段类型</span>
<span class="hljs-title class_">String</span> columnName = column.<span class="hljs-title function_">value</span>().<span class="hljs-title function_">toUpperCase</span>();<span class="hljs-comment">//字段名</span>
columnSegement.<span class="hljs-title function_">append</span>(columnName).<span class="hljs-title function_">append</span>(<span class="hljs-string">" "</span>).<span class="hljs-title function_">append</span>(columnType).<span class="hljs-title function_">append</span>(<span class="hljs-string">" "</span>);
<span class="hljs-title class_">Constraints</span> constraint = column.<span class="hljs-title function_">constraint</span>();
<span class="hljs-built_in">boolean</span> primaryKey = constraint.<span class="hljs-title function_">primaryKey</span>();
<span class="hljs-built_in">boolean</span> nullable = constraint.<span class="hljs-title function_">nullable</span>();
<span class="hljs-built_in">boolean</span> unique = constraint.<span class="hljs-title function_">unique</span>();
<span class="hljs-keyword">if</span> (primaryKey) {
<span class="hljs-comment">//主键唯一且不为空</span>
columnSegement.<span class="hljs-title function_">append</span>(<span class="hljs-string">"PRIMARY KEY "</span>);
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (!nullable) {
<span class="hljs-comment">//字段不为null</span>
columnSegement.<span class="hljs-title function_">append</span>(<span class="hljs-string">"NOT NULL "</span>);
}
<span class="hljs-keyword">if</span> (unique) {
<span class="hljs-comment">//有唯一键</span>
columnSegement.<span class="hljs-title function_">append</span>(<span class="hljs-string">"UNIQUE "</span>);
}
columnSegments.<span class="hljs-title function_">add</span>(columnSegement.<span class="hljs-title function_">toString</span>());
}
<span class="hljs-keyword">if</span> (columnSegments.<span class="hljs-title function_">size</span>() < <span class="hljs-number">1</span>) {
<span class="hljs-comment">//没有映射任何表字段,抛出异常</span>
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">IllegalArgumentException</span>(<span class="hljs-string">"没有映射任何表字段!"</span>);
}
<span class="hljs-title class_">StringJoiner</span> joiner = <span class="hljs-keyword">new</span> <span class="hljs-title class_">StringJoiner</span>(<span class="hljs-string">","</span>, <span class="hljs-string">"("</span>, <span class="hljs-string">")"</span>);
<span class="hljs-keyword">for</span> (<span class="hljs-title class_">String</span> segement : columnSegments) {
joiner.<span class="hljs-title function_">add</span>(segement);
}
<span class="hljs-comment">//生成SQL语句</span>
<span class="hljs-keyword">return</span> <span class="hljs-title class_">String</span>.<span class="hljs-title function_">format</span>(<span class="hljs-string">"CREATE TABLE %s"</span>, table) + joiner.<span class="hljs-title function_">toString</span>();
}
}
通过该解析器的 genSQL 方法在运行时生成建表 SQL, 通过传入的 Class 参数在运行时解析类和属性上的注解, 分别得到表名, 字段名, 字段类型,约束条件等信息,然后拼装成 SQL。由于只是为了做演示, 对 SQL 语法的支持比较弱, 只允许字段为 int 和 varchar 类型。且解析语法时也没有考虑一些边界情况。但是通过这段代码演示可以知道 ORM 框架在解析注解时的大概工作和流程是怎么样的。
- 测试
public class TableGeneratorTest {
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">main</span>(<span class="hljs-params"><span class="hljs-built_in">String</span>[] args</span>) {
<span class="hljs-title class_">String</span> sql = <span class="hljs-title class_">TableGenerator</span>.<span class="hljs-title function_">genSQL</span>(<span class="hljs-title class_">User</span>.<span class="hljs-property">class</span>);
<span class="hljs-title class_">System</span>.<span class="hljs-property">out</span>.<span class="hljs-title function_">println</span>(sql);
}
}
最后得到的建表语句如下
CREATE TABLE t_user(ID VARCHAR(30) PRIMARY KEY ,NAME VARCHAR(30) NOT NULL ,AGE INT ,PHONE_NUMBER VARCHAR(30) NOT NULL UNIQUE )
最后我们验证下生成的建表 SQL 语法是否有问题, 在 mysql 客户端上执行该 sql
如上图所示, 执行成功, 说明我们的建表语句是正确的。