本文内容

1、什么是注解
2、JDK内置注解
3、自定义注解
4、案例实战:模拟ORM,使用反射读取注解

1、什么是注解

Annotation是从JDK5.0开始引入的新技术.

作用

1 Annotation不是程序本身,它是对程序作出解释;

2 可以被其他程序(比如:编译器等)读取 ,根据不同的注解做出不同的处理,比如编译器会检查@Override标记的方法是否在存在于父类、接口中;

格式

以​​@注解名​​ 形式在代码中存在,比如@Override

class Obj extends Object{
private int id;
@Override
public String toString() {
return "Obj{" +
"id=" + id +
'}';
}
}

在哪里使用?

可以附加在package , class , method , field等上面﹐相当于给他们添加了额外的辅助信息,我们可以通过反射机制编程实现对这些元数据的访问

2、JDK内置注解

@Override

注解 @Override 限定重写父类方法或实现接口方法, 该注解只能加在方法上。

@Deprecated

注解@Deprecated 表示所修饰的元素已过时,通常是因为使用该元素很危险(比如后续版本不支持了)或存在更好的选择。

当在非弃用代码中使用或重写@Deprecated程序元素时,编译器会发出警告。

例:

public class AnnotationTest {
public static void main(String[] args) {
// Date构造方法加了@Deprecated注解
Date date = new Date(2021, 10, 11);
new Student().deprecatedMethod();
}
}

class Student{
@Deprecated
void deprecatedMethod(){
System.out.println("deprecatedMethod is invoked.");
}
}
D:\>javac AnnotationTest.java
注: AnnotationTest.java使用或覆盖了已过时的 API。
注: 有关详细信息, 请使用 -Xlint:deprecation 重新编译。

D:\>javac AnnotationTest.java -Xlint:deprecation
AnnotationTest.java:12: 警告: [deprecation] Date中的Date(int,int,int)已过时
Date date = new Date(2021, 10, 11);
^
AnnotationTest.java:13: 警告: [deprecation] Student中的deprecatedMethod()已过时
new Student().deprecatedMethod();
^
2 个警告

@SuppressWarnings

@SuppressWarnings 用于抑制编译器警告

例:

public class AnnotationTest {
public static void main(String[] args) {
List list = new ArrayList();
}
}

程序有警告,如下图

彻底搞定 Java 注解_ide


加了@SuppressWarnings(“rawtypes”)后仍然后警告

彻底搞定 Java 注解_java_02


加了"unused" 后警告消失

public class AnnotationTest {
@SuppressWarnings({ "rawtypes", "unused" })
public static void main(String[] args) {
List list = new ArrayList();
}
}

自定义Annotation

1、使用 ​​@interface 关键字​​​来定义新的 Annotation 类型;
例:

public @interface NormalAnnotation {
String value();
}

2、自定义注解​​自动继承了java.lang.annotation.Annotation接口​​;

Annotation源码如下:

package java.lang.annotation;
public interface Annotation {
/**
* 如果指定的对象表示的注释在逻辑上与此注释等价,则为True,否则为false
*/
boolean equals(Object obj);

int hashCode();
/**
* 返回此注释的字符串表示形式。表示的细节是依赖于实现的
*/
String toString();
/**
* @return 返回此注释的注释类型
*/
Class<? extends java.lang.annotation.Annotation> annotationType();
}

3、 注解的​​成员​​​在注解的定义中​​以无参数方法的形式来声明​​;

​方法名​​​定义了​​成员的名字​​​​返回值​​定义了​​类型​

public @interface NormalAnnotation {
/**
* 成员名为 value
* 类型为 String
*/
String value();
}

类型只能是:
八种基本数据类型、String类型、Class类型、enum类型、Annotation类型、以上所有类型的数组。

4、可以在定义 Annotation 的成员变量时为其指定初始值, 可以使用​​default关键字指定成员变量的默认值​​;

public @interface NormalAnnotation {
// 成员value的默认值为空字符串
String value() default "";
}

5、 ​​如果只有一个成员,建议使用的成员名为 value​​;

6、如果定义的注解含有成员,使用时必须指定成员的值,格式是 ​​“成员名 = 成员值”​

@NormalAnnotation(value="abc")
public class AnnotationTest {
...
}

如果在定义时使用了default 设置了某个成员的默认值,则可以不指定该成员的值 ,例:

public @interface NormalAnnotation {
String value() default "";
}
@NormalAnnotation
public class AnnotationTest {
...
}

如果只有一个参数成员且成员名称为value,可以​​省略“value=”​​,这就是上面第5条建议的价值。例:

@NormalAnnotation("abc")
public class AnnotationTest {
...
}

7、没有成员定义的注解称为​​标记​​;

比如@Override 就是一个标记,源码如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

8、包含成员变量的 Annotation 称为​​元数据注解​

注意:我们定义的注解,你的程序要去使用这个注解,同时你其他的程序要去获取这个注解做一些逻辑或业务上的处理,这样才能体现这个注解的价值。

反射

class NewClass {
public void m1() {
}
private void pm1() {
}

public static void main(String[] args) {
Class<NewClass> cls = NewClass.class;
Method[] methods = cls.getMethods();
methods = cls.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method.getName());
}
}
private static void printMethods(Method[] methods) {
for (Method method : methods) {
System.out.println(method.getName());
}
}
}
// 返回本类中显式定义的所有方法(包括private方法,不包括未覆盖的父类的方法)
Class#getDeclaredMethods
Class#getMethods

练习

JDK中的​​元注解​​(meta-annotation)

JDK 的​​元注解​​是用于修饰其他注解的注解.

可以类比数据库中元数据的概念:

数据库表中有一列一列的数据,而每一列都有列名, 这个列名用来指明这一列是什么数据,这个列名就可以认为是元数据

JDK5.0提供了4个标准的元注解类型,分别是:

@Retention
@Target
@Documented
@Inherited

@Retention

Retention /rɪˈtenʃn/ n. 保持,保留

@Retention​​只能用于修饰注解​​​, 用来​​指定该注解的生命周期​​。

@Retention源码如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}

@Rentention 包含一个 RetentionPolicy 类型的成员, 使用@Rentention 时必须为该 value 成员指定值:

1、RetentionPolicy.SOURCE 表示在源文件中有效,编译器是使用,编译完直接丢弃这个注释(反编译就看不到了)。

比如 @Override的@Retention就是RetentionPolicy.SOURCE,编译器编译时会检查@Override标注的方法,是否在其父类或其实现的接口中存在。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

2、RetentionPolicy.CLASS(默认值) 表示该注解在编译时会编译到class文件中, 当运行 Java 程序时, JVM 不会保留该注解。

3、RetentionPolicy.RUNTIME 表示在运行时也有效,即当运行 Java 程序时, JVM 也会保留该注解。

程序可以通过反射获取该注解。

@Target

@Target 用来指定注解可以标注在哪里。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* 指定注解可应用在哪几个位置
*/
ElementType[] value();
}

枚举类ElementType枚举了10个位置:

public enum ElementType {
/** 类,接口,注解,枚举定义的位置*/
TYPE,
/** 实例变量的位置(包括声明枚举常量的位置) */
FIELD,
/** 方法定义的位置 */
METHOD,
/** 参数位置 */
PARAMETER,
/** 构造方法位置 */
CONSTRUCTOR,
/** 局部变量位置 */
LOCAL_VARIABLE,
/** 声明注解的位置声明*/
ANNOTATION_TYPE,
/** 包定义的位置 */
PACKAGE,
/**
* Type parameter declaration
* @since 1.8
*/
TYPE_PARAMETER,
/**
* Use of a type
*/
TYPE_USE
}

@Documented

@Documented 标注的注解,在使用​​javadoc​​生成标注了该注解的元素的doc文档时,该注解会显示在文档中。

比如@Documented标注了 ​​java.lang.Deprecated​​注解,而java.util.Date的一个构造方法上加了@Deprecated注解:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD
, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

public class Date
implements java.io.Serializable,Cloneable, Comparable<Date>
{
/**
* Allocates a <code>Date</code> object and initializes it so that
* it represents the date and time indicated by the string
* <code>s</code>, which is interpreted as if by the
* {@link Date#parse} method.
*
* @param s a string representation of the date.
* @see java.text.DateFormat
* @see java.util.Date#parse(java.lang.String)
* @deprecated As of JDK version 1.1,
* replaced by <code>DateFormat.parse(String s)</code>.
*/
@Deprecated
public Date(String s) {
this(parse(s));
}
}

点击查看 ​​java8的doc文档​

彻底搞定 Java 注解_annotation_03


@Deprecated 显示在doc文档中

@Inherited

被@Inherited 修饰的注解将具有继承性。

如果某个类使用了被@Inherited 修饰的注解, 则其子类将自动具有该注解。

例:
我们定义了两个注解如下

@Inherited
// 要想在程序运行时能读到这个两个注解,就必须使用`@Retention(RetentionPolicy.RUNTIME)`
@Retention(RetentionPolicy.RUNTIME)
public @interface NormalAnnotation {
String value();
}

public @interface MarkablelAnnotation {
}

下面是测试这两个注解的类,该类使用这两注解进行了标注

class SubAnnotationTest extends  AnnotationTest{
...
}

@MarkablelAnnotation
@NormalAnnotation("abc")
public class AnnotationTest {
public static void main(String[] args) {
// 获取AnnotationTest类上标注的所有注解
Annotation[] annotations = AnnotationTest.class.getAnnotations();

// 打印所有的注解
for (Annotation annotation : annotations) {
System.out.println("AnnotationTest 上的注解:"+annotation);
}
// 获取AnnotationTest的子类SubAnnotationTest上标注的所有注解
annotations = SubAnnotationTest.class.getAnnotations();

for (Annotation annotation : annotations) {
System.out.println("SubAnnotationTest 上的注解:"+annotation);
}
}
}

程序打印内容如下:

AnnotationTest 上的注解:@com.annotation.NormalAnnotation(value=abc)
SubAnnotationTest 上的注解:@com.annotation.NormalAnnotation(value=abc)

结论:
1、SubAnnotationTest 继承了父类AnnotationTest上的注解​​​@NormalAnnotation​​​;
2、​​​@MarkablelAnnotation​​​ 没有使用​​@Retention(RetentionPolicy.RUNTIME)​​ , 所以获取不到;

jdk 8 中注解的新特性

可重复注解、类型注解

(1)可重复注解 @Repeatable

@Repeatable用于指示它注解的注解是可重复的。@Repeatable的值表示包含的注解类型。

@Repeatable源码如下:

/**
* @Repeatable用于指示它注解的注解是可重复的。@Repeatable的值表示包含的注解类型。
* @since 1.8
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
/**
* 指示重复注解包含的注解类型
*/
Class<? extends Annotation> value();
}

例:
在一个注解NormalAnnotation上加@Repeatable,@Repeatable的成员-value值为​​​注解A的数组​

@Retention(RetentionPolicy.RUNTIME)
@Repeatable(NormalAnnotations.class)
public @interface NormalAnnotation {
String value();
}
public @interface NormalAnnotations {
NormalAnnotation[] value();
}

⚠️⚠️: @NormalAnnotation的@Target的ElementType值必须包含@NormalAnnotations的@Target的ElementType值

(2)@Target新增2个ElementType类型成员

(2.1)ElementType.TYPE_PARANETER

​ElementType.TYPE_PARANETER​​ 表示该注解能写在类型的声明语句处(如:泛型声明)。

@Target({ElementType.TYPE_PARAMETER})
public @interface NormalAnnotation {
String value();
}
class UserService<@NormalAnnotation T> {
private T t;
public <@NormalAnnotation T> void setT(T t) {
List<T> list = new ArrayList<>();
}
}

具体用在上面地方?不知道! 我搜索了SpringFramework、SpringBoot2、MyBatis的源码都没有搜到使用TYPE_PARAMETER的地方。

(2.2)ELementType. TYPE_USE

ELementType. TYPE_USE表示该注解能写在使用类型的任何语句中。

class UserService<@NormalAnnotation T>{
@NormalAnnotation
private T t ;
public <@NormalAnnotation T> void setT(@NormalAnnotation T t){
List<@NormalAnnotation T> list = new ArrayList<>();
}
}

彻底搞定 Java 注解_annotation_04


在@Target的成员中添加一个​​ElementType.TYPE_USE​​, 编译错误消失

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE_USE,ElementType.TYPE_PARAMETER})
public @interface NormalAnnotation {
String value() default "";
}

案例实战

现在我们想着想提供基本的对象/关系映射功能,能够自动生成数据库表。

一种方案:使用 XML 文件的方式提供对象/关系映射信息。

更好的方案:使用注解提供对象/关系映射信息,注解看上去更直接和方便,我们可以把映射信息(注解)都保存在 JavaBean 源文件中。

为此我们需要定义一些注解:
1、用于“映射JavaBean到数据表的注解”;
2、用于“映射JavaBean的属性到列的注解”;

1、定义“映射JavaBean到数据表的注解”

/**
* 类的注解,作用:将类映射到表
*/
@Target(ElementType.TYPE) // Applies to classes only
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
// 类对应的表名称
public String name() default "";
}

2、定义 “映射JavaBean的实例变量到列的注解”

/**
* 列的限制属性
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
// 实例变量对应的列是否为主键
boolean primaryKey() default false;
// 实例变量对应的列是否允许为空
boolean allowNull() default true;
// 实例变量对应的列的值是否唯一
boolean unique() default false;
}


/**
* 注解int/Integer类型的变量
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
// 实例变量对应的列名称
String name() default "";
// 实例变量对应的列的限制属性
Constraints constraints() default @Constraints;
}


/**
* 注解 String 类型的实例变量
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
// 实例变量对应字段的长度
int value() default 0;
// 实例变量对应字段的名称
String name() default "";
Constraints constraints() default @Constraints;
}

定义一个Member类,使用我们自定义的注解

@DBTable(name = "MEMBER")
public class Member {
@SQLString(30)
String firstName;
@SQLString(50)
String lastName;
@SQLInteger
Integer age;
@SQLString(value = 30,
constraints = @Constraints(primaryKey = true))
String handle;
static int memberCount;
// get/set 方法略...
}

解析注解的成员,拼接成SQL的DDL语句

public class TableCreator {

/**
* @param args 类全限定名
* @throws Exception
*/
public static void main(String[] args) throws Exception {
checkParam(args);
for (String className : args) {
Class<?> cl = Class.forName(className);
// 1 根据class类上的注解,获取表名
String tableName = getTableName(cl);
if (tableName == null) continue;

// 2 根据class中各字段上的注解,生成定义列的语句
List<String> columnDefs = createColumnDefs(cl);

System.out.println("Table Creation SQL for " + className + " is :");
printCreateTableDDL(tableName, columnDefs);
}
}

private static void printCreateTableDDL(String tableName, List<String> columnDefs) {
StringBuilder sqlCommandBuilder = new StringBuilder("CREATE TABLE " + tableName + "(");
for (String columnDef : columnDefs) {
sqlCommandBuilder.append("\n " + columnDef + ",");
}
// Remove trailing comma
String tableCreate = sqlCommandBuilder.substring(0, sqlCommandBuilder.length() - 1) + "\n);";
System.out.println(tableCreate);
}

private static List<String> createColumnDefs(Class<?> cl) {
List<String> columnDefs = new ArrayList<String>();
for (Field field : cl.getDeclaredFields()) {
Annotation[] fieldAnns = field.getDeclaredAnnotations();
if (fieldAnns.length < 1) {
continue;
}
Annotation firstFieldAnn = fieldAnns[0];
String columnName = null;
if (firstFieldAnn instanceof SQLInteger) {
SQLInteger sInt = (SQLInteger) firstFieldAnn;
// Use field name if name not specified
if (sInt.name().length() < 1)
columnName = field.getName().toUpperCase();
else
columnName = sInt.name();
columnDefs.add(columnName + " INT" + getConstraints(sInt.constraints()));
}
if (firstFieldAnn instanceof SQLString) {
SQLString sqlStr = (SQLString) firstFieldAnn;
// Use field name if name not specified.
if (sqlStr.name().length() < 1)
columnName = field.getName().toUpperCase();
else
columnName = sqlStr.name();
columnDefs.add(columnName + " VARCHAR(" + sqlStr.value() + ")" + getConstraints(sqlStr.constraints()));
}
}
return columnDefs;
}

/**
* @param cl
* @return
*/
private static String getTableName(Class<?> cl) {
DBTable dbTable = cl.getAnnotation(DBTable.class);
if (dbTable == null) {
System.out.println("No DBTable annotations in class " + cl.getName());
return null;
}
String tableName = dbTable.name();
// If the name is empty, use the Class name:
if (tableName.length() < 1) {
tableName = cl.getName().toUpperCase();
}
return tableName;
}

private static void checkParam(String[] args) {
if (args.length < 1) {
System.out.println("arguments: annotated classes");
System.exit(0);
}
}

private static String getConstraints(Constraints con) {
String constraints = "";
if (!con.allowNull())
constraints += " NOT NULL";
if (con.primaryKey())
constraints += " PRIMARY KEY";
if (con.unique())
constraints += " UNIQUE";
return constraints;
}
}