彻底搞定 Java 注解

本文内容

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 注解
加了@SuppressWarnings(“rawtypes”)后仍然后警告
彻底搞定 Java 注解
加了"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只能用于修饰注解, 用来指定该注解的生命周期

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

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

比如 @Override的@Retention就是RetentionPolicy.SOURCE

@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 注解
@Deprecated 显示在doc文档中

@Inherited

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

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

例:

@MarkablelAnnotation
@NormalAnnotation("abc")
public class AnnotationTest {
    public static void main(String[] args) {
        Annotation[] annotations = AnnotationTest.class.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println(annotation);
        }
        annotations = SubAnnotationTest.class.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println(annotation);
        }
    }
}

输出

@com.annotation.NormalAnnotation(value=abc)
@com.annotation.NormalAnnotation(value=abc)

⚠️⚠️注意:想要有输出,必须使用@Retention(RetentionPolicy.RUNTIME)

@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface NormalAnnotation {
    String value();
}

public @interface MarkablelAnnotation {
}

@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 注解
在@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;
    }
}
上一篇:通用Mapper(二)创建具体Mapper接口与Mapper接口介绍


下一篇:接口(interface)