1. 注解与反射机制

前面经过反编译后,我们知道 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

2. 自定义注解

2.1 场景一:使用自定义注解来创建数据表

步骤:

  1. 根据自定义注解组装 SQL 语句
  2. 连接数据库,并执行 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()
}

TableCreator#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);
                throw new RuntimeException("类:" + className + "上没有注解@MyTable");
            }
            Field[] fields = clazz.getDeclaredFields();
            if (null == fields || fields.length == 0) {
                throw new RuntimeException("类:" + className + "没有字段");
            }

            // 获取表名
            String tableName = myTable.name();
            // 如果表名为空,则使用类名
            if (null != tableName) {
                if (tableName.length() < 1) {
                    tableName = clazz.getName().toUpperCase();
                }
            }
            // 获取字段列表定义
            List<String> columnDefs = new ArrayList<>();
            for (Field field : fields) {
            	// 解析字段
                parseFieldOnMyColumnAnnotation(field, columnDefs);
            }

            // 如果类中的属性有对应的列,则将构建数据表语句
            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;
        }
    }
}

TableCreator#parseFieldOnMyColumnAnnotation():解析字段

public static void parseFieldOnMyColumnAnnotation(Field field, List<String> columnDefs) {
    StringBuilder sb = new StringBuilder();
    // 获取字段上面的所有的注解
    Annotation[] fieldAnnotations = field.getDeclaredAnnotations();
    if (fieldAnnotations.length == 0) {
        return;
    }
    // 遍历字段上面的注解
    for (int i = 0; i < fieldAnnotations.length; i++) {
        // 字段上面只有 @MyColumn 注解
        MyColumn myColumn = (MyColumn) fieldAnnotations[i];

        // 1.字段名
        String columnName = null;
        if (myColumn instanceof MyColumn) {
            columnName = myColumn.name();
            if (null != columnName) {
                if (columnName.length() == 0) {
                    columnName = field.getName().toUpperCase();
                }
            }
        }
        // 2.字段类型
        String type = myColumn.type();
        // 3.字段长度
        int length = myColumn.length();
        // 4.字段备注
        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());
    }
}

TableCreator#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();
}

DbUtil:数据库连接工具类:

public class DbUtil {

    public static final String JDBC_PROPERTY = "jdbc.properties";
    public static Properties properties = new Properties();
    public static String JDBC_DRIVER = null;
    public static String JDBC_URL = null;
    public static String JDBC_USERNAME = null;
    public static String JDBC_PASSWORD = null;

    static {
        InputStream in = DbUtil.class.getClassLoader().getResourceAsStream(JDBC_PROPERTY);
        try {
            properties.load(in);
            JDBC_DRIVER = properties.getProperty("jdbc.driver");
            JDBC_URL = properties.getProperty("jdbc.url");
            JDBC_USERNAME = properties.getProperty("jdbc.username");
            JDBC_PASSWORD = properties.getProperty("jdbc.password");
            Class.forName(JDBC_DRIVER);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static Connection getConnection() throws SQLException {
        Connection con= DriverManager.getConnection(JDBC_URL, JDBC_USERNAME, JDBC_PASSWORD);
        return con;
    }

    public static void  close(Connection con){
        if(con != null){
            try {
                con.close();
            } catch (SQLException e) {
                e.printStackTrace();
                System.out.println("关闭异常");
            }
        }
    }
}

DbUtil#executeSql():执行 SQL

public static void executeSql(String sql){
    Connection con = null;
    Statement stmt = null;
    ResultSet re = null;
    try {
        con = getConnection();
        stmt = con.createStatement();
        stmt.execute(sql);
    }
    catch (SQLException e) {
        System.out.println("数据库访问异常");
        throw new RuntimeException(e);
    }
    finally{
        try {
            if(re!=null){
                re.close();
            }
            if(stmt!=null){
                stmt.close();
            }
        } catch (SQLException e2) {
            System.out.println("关闭异常");
        }
        close(con);
    }
}

jdbc.properties:JDBC 配置文件内容

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/zzc?useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=root

测试:

public class Test {

    public static void main(String[] args) {
        String className = "com.tinady.annotation.User";
        String tableSql = TableCreator.createTableSql(className);
        DbUtil.executeSql(tableSql);
    }

}

以上便是利用注解结合反射来构建SQL语句的简单的处理器模型

2.2 场景二:使用 @MyTest 注解写一个测试框架

使用 @MyTest 注解写一个测试框架,用来测试程序员的方法是否有问题

先自定义一个注解 @MyTest

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTest {

}

再自定义一些需要测试的方法:

public class TestAnnotation {

    @MyTest
    public void add() {
        System.out.println("ADD~~~");
    }

    @MyTest
    public void sub() {
        System.out.println("SUB~~~");
    }

    @MyTest
    public void mul() {
        System.out.println("MUL~~~");
    }

    @MyTest
    public void divide() {
        int i = 3 / 0;
        System.out.println("DIVIDE~~~");
    }
}

通过反射进行调用,并生成测试报告:

public class TestTool {

    public static void main(String[] args) {
        TestAnnotation testAnnotation = new TestAnnotation();
        Class<?> clazz = testAnnotation.getClass();
        int errorNum = 0;
        StringBuilder errorMsg = new StringBuilder();

        // 只获取 public 修饰的方法
        Method[] methods = clazz.getMethods();
        if (null == methods || methods.length < 1) {
            System.out.println("该类" + clazz.getName() + "没有需要测试的方法");
            return;
        }

        for (Method method : methods) {
            // 只测试带有 @MyTest 注解的方法
            boolean isAnnotationMyTest = method.isAnnotationPresent(MyTest.class);
            if (!isAnnotationMyTest) {
                continue;
            }

            method.setAccessible(true);
            try {
                method.invoke(testAnnotation, null);
            } catch (Exception e) {
                errorNum++;
                errorMsg.append(method.getName()).append(" ").append("has error:").append("\r\n  caused by ")
                        .append(e.getCause().getClass().getSimpleName()).append("\r\n");
                errorMsg.append(e.getCause().getMessage()).append("\r\n");
            }
        }

        errorMsg.append(clazz.getName()).append(" ").append("has ").append(errorNum).append(" errors.");
        // 生成测试报告
        System.out.println(errorMsg.toString());
    }
}