1. Java 中的注解
1.1 注解概念
首先看看官方对注解的描述:
An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.
大致意思:注解是一种能被添加到java代码中的元数据,类、方法、变量、参数和包都可以用注解来修饰。注解对于它所修饰的代码并没有直接的影响
简单来说,注解就是一个标记(可以给类、方法、字段等打一个标记)
在底层实现上,所有定义的注解都会自动继承java.lang.annotation.Annotation接口
通过 jad 反编译一个自定义注解,如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTable {
String name() default "test";
}
反编译后:
1.2 元注解
元注解的作用就是负责注解其他注解。Java5.0 定义了 4 个标准的 meta-annotation 类型,它们被用来提供对其它 annotation 类型作说明。Java5.0 中定义的元注解:
- @Target
- @Retention
- @Documented
- @Inherited
这些类型和它们所支持的类位于 java.lang.annotation 包下
1.2.1 @Target 注解
查看 @Target 源码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
@Target 作用:用于描述注解的使用范围
它的取值来源于 ElementType 类型的数组元素 value。其中 ElementType 是一个枚举类型:TYPE、FIELD、METHOD…其含义如下:
- TYPE:标明该注解可以用于类、接口(包括注解类型)或enum声明
- FIELD:标明该注解可以用于字段(域)声明,包括enum实例
- METHOD:标明该注解可以用于方法声明
- …
如:
// 定义一个注解
@Target(ElementType.TYPE)
public @interface MyTable {
String name() default "test";
}
// 使用此注解
@MyTable(name = "tab_user")
public class User {
private String name;
}
自定义一个注解 @MyTable,并将此注解用在一个 User 类上。
1.2.2 @Retention 注解
查看 @Retention 注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}
@Retention 作用:约束注解的生命周期
它的取值来源于 RetentionPolicy 类型的元素 value。其中 RetentionPolicy 是一个枚举类型:SOURCE、CLASS、RUNTIME。其含义如下:
- SOURCE:注解将被编译器丢弃(该类型的注解信息只会保留在源码里,源码经过编译后,注解信息会被丢弃,不会保留在编译好的class文件里)
- CLASS:注解在class文件中可用,但会被VM丢弃(该类型的注解信息会保留在源码里和class文件里,在执行的时候,不会加载到虚拟机中),请注意,当注解未定义Retention值时,默认值是CLASS,如Java内置注解,@Override、@Deprecated、@SuppressWarnning等
- RUNTIME:注解信息将在运行期(JVM)也保留,因此可以通过反射机制读取注解的信息(源码、class文件和执行的时候都有注解的信息),如SpringMvc中的@Controller、@Autowired、@RequestMapping等
1.2.3 @Documented 注解
查看 @Documented 注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
@Documented 作用:被修饰的注解会生成到 javadoc 中
1.2.4 @Inherited 注解
查看 @Inherited 注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
@Inherited 作用:可以让注解被继承,但这并不是真的继承,只是通过使用 @Inherited,可以让子类 Class 对象使用 getAnnotations() 获取父类被 @Inherited修饰的注解
如:
@Inherited
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Doc {
}
@Doc
public class Parent {}
public class Son extends Parent {}
public class Test {
public void static main(String[] args) {
A a = new B();
System.out.println("已使用的@Inherited注解:" + Arrays.toString(a.getClass().getAnnotations()));
}
}
1.3 Java 内置注解
看看Java提供的内置注解,主要有3个,位于 java.lang 包下
如下:
@Override:用于标明此方法覆盖了父类的方法,源码如下
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Deprecated:用于标明已经过时的方法或类
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
@SuppressWarnnings:用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
其内部有一个String数组,主要接收值如下:
deprecation:使用了不赞成使用的类或方法时的警告;
unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型;
fallthrough:当 Switch 程序块直接通往下一种情况而没有 Break 时的警告;
path:在类路径、源文件路径等中有不存在的路径时的警告;
serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告;
finally:任何 finally 子句不能正常完成时的警告;
all:关于以上所有情况的警告。
1.4 注解类型元素
之前,在类中可以定义构造方法、属性、方法等。那么,在注解中只可以定一个东西:注解类型元素。
如:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyUser {
String name() default "zzc";
int age();
}
注解类型元素支持的数据类型:
- 所有基本类型(int, float, boolean, byte, double, char, long, short)
- String
- Class
- enum
- Annotation
- 上述类型的数组
如:
// 定义一个注解 MyTable
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTable {
String name() default "test";
}
public @interface MyTest {
// 声明一个枚举
enum Status {FIXED, NORMAL};
Status status() default Status.FIXED;
boolean isTrue() default false;
String name() default "test";
Class clazz() default Void.class;
MyTable table() default @MyTable(name = "table");
int[] value() default {1, 2, 3};
}
1.5 特殊用法
1.5.1 特殊语法一:
如果注解本身没有注解类型元素,那么在使用注解的时候可以省略(),直接写为:@注解名,它和标准语法@注解名()等效!
如:
@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
}
// 等效于 @TestAnnotation()
@TestAnnotation
public class Test {}
1.5.2 特殊语法二:
如果注解本本身只有一个注解类型元素,而且命名为value,那么在使用注解的时候可以直接使用:@注解名(注解值),其等效于:@注解名(value = 注解值)
@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
String value();
}
// 等效于 @TestAnnotation(value = "This is a Test")
@TestAnnotation("This is a Test")
public class Test {}
1.5.3 特殊用法三:
如果注解中的某个注解类型元素是一个数组类型,在使用时又出现只需要填入一个值的情况,那么在使用注解时可以直接写为:@注解名(类型名 = 类型值),它和标准写法:@注解名(类型名 = {类型值})等效!
@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
String[] name();
}
// 等效于 @TestAnnotation(name = {"This is a Test"})
@TestAnnotation(name = "This is a Test")
public class Test {}
1.6 注解与反射机制
前面经过反编译后,我们知道 Java 中的注解都继承了 Annotation 接口。也就是说,Java 使用 Annotation 接口代表注解元素,该接口是所有 Annotation 类型的父接口。同时为了运行时能准确获取到注解的相关信息,Java 在 java.lang.reflect 包下新增了 AnnotatedElement 接口,它主要用于表示目前正在 VM 中运行的程序中已使用注解的元素,通过该接口提供的方法可以利用反射技术地读取注解的信息,如反射包的 Constructor 类、Field 类、Method 类、Package 类和 Class 类都实现了 AnnotatedElement 接口。
下面是 AnnotatedElement 中相关的 API 方法,以上5个类都实现以下的方法::
返回值 | 方法名称 | 说明 |
---|---|---|
getAnnotation(Class annotationClass) | 该元素如果存在指定类型的注解,则返回这些注解,否则返回 null | |
Annotation[] | getAnnotations() | 返回此元素上存在的所有注解,包括从父类继承的 |
boolean | isAnnotationPresent(Class<? extends Annotation> annotationClass) | 如果指定类型的注解存在于此元素上,则返回 true,否则返回 false |
Annotation[] | getDeclaredAnnotations() | 返回直接存在于此元素上的所有注解,注意,不包括父类的注解,调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响,没有则返回长度为0的数组 |
案例:
@DocumentA
class A{ }
@DocumentB
public class B extends A{
public static void main(String... args){
Class<?> clazz = B.class;
//根据指定注解类型获取该注解
DocumentA documentA = clazz.getAnnotation(DocumentA.class);
System.out.println("A:" + documentA);
//获取该元素上的所有注解,包含从父类继承
Annotation[] an= clazz.getAnnotations();
System.out.println("an:" + Arrays.toString(an));
//获取该元素上的所有注解,但不包含继承!
Annotation[] an2=clazz.getDeclaredAnnotations();
System.out.println("an2:" + Arrays.toString(an2));
//判断注解DocumentA是否在该元素上
boolean b=clazz.isAnnotationPresent(DocumentA.class);
System.out.println("b:" + b);
}
}
执行结果:
A:@com.zejian.annotationdemo.DocumentA()
an:[@com.zejian.annotationdemo.DocumentA(), @com.zejian.annotationdemo.DocumentB()]
an2:@com.zejian.annotationdemo.DocumentB()
b:true
1.7 自定义注解
场景:使用自定义注解来组装数据库 SQL 语句
自定义注解 @MyTable
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTable {
String name() default "test";
}
此注解用于类-----对应类
自定义注解 @MyConstraints
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyConstraints {
// 判断是否是主键
boolean isPrimaryKey() default false;
// 判断是否为空
boolean isNull() default false;
// 判断是否唯一
boolean isUnique() default false;
}
此注解用于类中的属性-----约束属性
自定义注解 @MyColumn
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyColumn {
// 字段名
String name() default "";
// 字段类型
String type() default "varchar";
// 字段长度
int length() default 0;
// 字段备注
String comment() default "";
// 约束
MyConstraints constraints() default @MyConstraints;
}
在实体类 User 中使用自定义注解
@MyTable(name = "TAB_USER")
public class User {
@MyColumn(name = "ID", type = "int", length = 11, comment = "主键", constraints = @MyConstraints(isPrimaryKey = true, isNull = false, isUnique = true))
private Integer id;
@MyColumn(name = "NAME", comment = "用户名", length = 30)
private String name;
@MyColumn(name = "AGE", type = "int", length = 3, constraints = @MyConstraints(isNull = true))
private Integer age;
// getter()/setter()
}
createTableSql() 方法:
创建一个注解解析器,来解析注解。
public class TableCreator {
/**
*
* 功能描述: 通过类名返回一个创建表的 SQL 语句
*
* CREATE TABLE [IF NOT EXISTS] tbleName (
* columnName type[(size)] [NOT NULL | NULL] [DEFAULT defaultValue] [UNIQUE [KEY]] [[PRIMARY] KEY] [COMMENT 'string']
* )
*
* @param className 类名
* @return java.lang.String
* @author zzc
* @date 2020/12/5 14:09
*/
public static String createTableSql(String className) {
Class<?> clazz = null;
try {
clazz = Class.forName(className);
// 通过某个类的 Class 对象获取某个注解
MyTable myTable = clazz.getAnnotation(MyTable.class);
if (null == myTable) {
System.out.println("No MyTable annotations in class " + className);
}
// 获取表名
String tableName = myTable.name();
// 如果表名为空,则使用类名
if (null != tableName) {
if (tableName.length() < 1) {
tableName = clazz.getName().toUpperCase();
}
}
// 获取字段列表定义
List<String> columnDefs = new ArrayList<>();
StringBuilder sb = null;
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
sb = new StringBuilder();;
// 获取字段上面的所有的注解
Annotation[] fieldAnnotations = field.getDeclaredAnnotations();
if (fieldAnnotations.length < 1) {
continue;
}
// 遍历字段上面的注解
for (int i = 0; i < fieldAnnotations.length; i++) {
// 字段上面只有 @MyColumn 注解
MyColumn myColumn = (MyColumn) fieldAnnotations[i];
// 字段名
String columnName = null;
if (myColumn instanceof MyColumn) {
columnName = myColumn.name();
if (null != columnName) {
if (columnName.length() < 1) {
columnName = field.getName().toUpperCase();
}
}
}
// 字段类型
String type = myColumn.type();
// 字段长度
int length = myColumn.length();
// 字段备注
String comment = myColumn.comment();
// 字段约束
MyConstraints constraints = myColumn.constraints();
// 生成字段列定义
sb.append(columnName).append(" ").append(type);
if (length > 0) {
sb.append("(").append(length).append(")");
}
if (null != constraints) {
sb.append(" ").append(getMyConstrains(constraints));
}
if (!comment.isEmpty()) {
sb.append(" ").append("COMMENT").append(" ").append("'").append(comment).append("'");
}
columnDefs.add(sb.toString());
}
}
// 如果类中的属性有对应的列,则将构建数据表语句
StringBuilder createCommand = null;
if (columnDefs.size() > 0) {
createCommand = new StringBuilder();
createCommand.append("CREATE TABLE").append(" ").append(tableName).append("(");
for (String columnDef : columnDefs) {
createCommand.append("\n ").append(columnDef).append(",");
}
}
// 确保最后是一个逗号“,”,并将最后一个多余的逗号“,”给去掉
if (null != createCommand) {
String tableSql = createCommand.toString();
int lastIndex = tableSql.length() - 1;
if (tableSql.charAt(lastIndex) == ',') {
tableSql = tableSql.substring(0, lastIndex) + "\n);";
return tableSql;
}
}
throw new RuntimeException("类 " + className + " 中的属性没有对应的列注解 " + MyColumn.class);
} catch (ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
}
getMyConstrains() 方法:
/**
*
* 功能描述: 将 约束类型为 MyConstraints 转换为 字符串String 类型
*
* @param myConstrains 约束
* @return java.lang.String
* @author zzc
* @date 2020/12/5 14:37
*/
private static String getMyConstrains(MyConstraints myConstrains) {
StringBuilder sb = new StringBuilder();
boolean isNull = myConstrains.isNull();
boolean isPrimaryKey = myConstrains.isPrimaryKey();
boolean isUnique = myConstrains.isUnique();
// 约束是否为空
if (isNull) {
if (isPrimaryKey) {
throw new RuntimeException("主键不能为空");
}
sb.append("NULL");
} else {
sb.append("NOT NULL");
}
// 约束为主键
if (isPrimaryKey) {
sb.append(" ").append("PRIMARY KEY");
}
// 约束为唯一键
if (isUnique) {
sb.append(" ").append("UNIQUE KEY");
}
return sb.toString();
}
测试:
public static void main(String[] args) {
String className = "com.tinady.annotation.User";
System.out.println("Table Creation SQL for " +
className + " is :\n" + createTableSql(className));
}
以上便是利用注解结合反射来构建SQL语句的简单的处理器模型