Java 注解(Annotation)用法和自定义注解处理器

前言

在 Java EE(原 J2EE)开发中,注解(Annotation)极大地简化了配置过程,避免了繁琐的 XML 文件生成。例如在 Hibernate 实体声明中,仅通过几个注解即可完成原本需要 XML 配置的操作。本文将主要论述 Annotation 的基本用法以及如何自定义注解处理器。当创建具有描述符性质的类或接口,且存在大量重复性工作时,利用注解可以有效提升开发效率。

基本语法

Java 目前包括三种标准注解和四种元注解。元注解(Meta-Annotation)主要负责注解其他注解。

1. 三种标准注解

  • @Override:表示当前的方法定义覆盖了父类中的方法。必须要有相同的方法签名(即方法名、参数类型、参数顺序、参数个数均一致),否则在编译过程中会发出错误提示。
  • @Deprecated:对不应该再使用的方法添加注解。当使用这个方法的时候,编译器会显示提示信息。
  • @SuppressWarnings:用于关闭不当的编译器报警信息。

2. 四种元注解

  • @Target:表示该注解可以用在什么地方。

    • 例如:CONSTRUCTOR(构造器声明)、FIELD(域声明)、METHOD(方法声明)、TYPE(类、接口或 enum 声明)。
  • @Retention:表示需要在什么级别保存该注解信息。

    • SOURCE:注解将被编译器丢弃。
    • CLASS:注解在 class 文件可用,但会被 JVM 丢弃。
    • RUNTIME:JVM 将在运行期间也保留注解,可以使用反射机制读取注解信息。
  • @Documented:将此注解包含到 Javadoc 中。
  • @Inherited:允许子类继承父类的注解。

定义注解

自定义注解以 @interface 为标志,如同一个接口的定义。这里面定义的每个方法名,就是使用注解时候的元素名;方法的返回值就是元素的类型。可以利用 default 来声明默认值,不过对于非基本类型,不能设置为 null 为默认值,一般对于字符串使用空字符串作为其默认值。

如下所示:

package whut.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 定义一个注解
@Target(ElementType.METHOD) // 定义该注解将应用于什么地方,方法或者域
@Retention(RetentionPolicy.RUNTIME) // 定义该注解在哪一个级别可用
public @interface UseCase {
    // 注解元素,可以指定默认值,在使用注解的时候,可以直接给元素赋值如 id=5
    public int id();
    public String description() default "no description";
    
    // 利用枚举来设置参数类型
    public enum ParameterType { 
        STRING, SHORT, INT, BOOL, LONG, OBJECT 
    };
    
    // 默认值,在使用注解的时候,只需要为元素赋值
    public ParameterType type() default ParameterType.STRING;
}

使用注解

在类中任意的域值前,或者方法前等直接使用 @注解名,如 @UseCase(id=5)。使用注解的过程中,必须对于没有设置默认值的元素进行赋值操作,对于每个元素进行按照名 - 值对的方式赋值。如果在注解定义中有名为 value 的元素,并且它是唯一需要赋值的,可以直接在括号里给出 value 所需要的值。

注意:注解是不能继承的。

实战案例:基于反射的注解处理器模型

下面是一个基本的利用非 APT(Annotation Processing Tool,编译期注解处理工具)实现的注解处理器模型,而是通过运行时反射来处理。这个模型可以注解实体,进行数据库的映射建表操作,是最基本的操作演示。

1. 注解定义

将四个注解名定义在不同的文件中(此处为了展示方便合并在同一代码块中说明,实际开发建议分文件):

package whut.annotationDB;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 定义字段的约束
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
    boolean primaryKey() default false;
    boolean allowNull() default true;
    boolean unique() default false;
}

////////////////////////////

// 定义表名的注解
@Target(ElementType.TYPE) // 类,接口或 enum
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
    public String name() default "";
}

///////////////////////////

// 定义 Integer 类型字段
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
    String name() default "";
    // 嵌套注解的功能,将 column 类型的数据库约束信息嵌入其中
    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;
}

2. 实体类使用注解

这里是运用了运行时处理注解,所以 RetentionPolicy 设置为 RUNTIME

package whut.annotationDB;

@DBTable(name = "MEMBER")
public class Member {
    // 在使用注解过程中,如果有元素是 value,并且只有 value 需要赋值,
    // 则只需要在 () 中将值写入
    @SQLString(30)
    private String firstName;
    
    @SQLString(50)
    private String lastName;
    
    @SQLInteger
    private Integer age;
    
    @SQLString(value = 30, constraints = @Constraints(primaryKey = true))
    private String handle;
    
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    public String getHandle() {
        return handle;
    }
    public void setHandle(String handle) {
        this.handle = handle;
    }
}

3. 注解处理器实现

具体的非 APT 实现的注解处理器,通过反射解析注解并生成 SQL 语句。

package whut.annotationDB;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class TableCreator {
    public Connection getConnection() {
        String user = "root";
        String password = "";
        String serverUrl = "jdbc:mysql://localhost:3306/carrent?user=root&password=";
        try {
            Class.forName("com.mysql.jdbc.Driver");
            Connection con = DriverManager.getConnection(serverUrl, user, password);
            return con;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    
    // 实现创建表
    public static void main(String[] args) {
        TableCreator tc = new TableCreator();
        tc.executeCreateDB(Member.class);
    }
    
    public void executeCreateDB(Class<?> entity) {
        String sqlStr = explainAnnotation(entity);
        Connection con = getConnection();
        PreparedStatement psql = null;
        if (con != null && !sqlStr.equals("error")) {
            try {
                psql = con.prepareStatement(sqlStr);
                psql.execute();
            } catch (SQLException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (psql != null)
                        psql.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
                try {
                    if (con != null)
                        con.close(); // 修正原代码中的资源关闭逻辑
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        } else {
            System.out.println("failure to...");
        }
    }
    
    // 真正的处理器,Class<?> 必须用这个表明
    public String explainAnnotation(Class<?> entity) {
        // 获取指定类型的注解
        DBTable dbtable = entity.getAnnotation(DBTable.class);
        if (dbtable == null) {
            System.out.println("No DBTable annotation in class " + entity.getName());
            return "error";
        } else {
            String tableName = dbtable.name(); // 获取注解 name 值,即表名称
            // 当没有设置 name 值,直接利用类的名作为表名
            if (tableName.length() < 1)
                tableName = entity.getName().toUpperCase(); // 转换大写
            
            // 准备处理字段注解
            List<String> columnsDefs = new ArrayList<String>();
            // 获取该类的所有字段
            for (Field field : entity.getDeclaredFields()) {
                String columnName = null;
                // 获取该字段所有的注解
                Annotation[] anns = field.getDeclaredAnnotations();
                
                // 当有注解的时候
                if (anns.length >= 1) {
                    // 判断注解的类型
                    if (anns[0] instanceof SQLInteger) {
                        SQLInteger sInt = (SQLInteger) anns[0];
                        // 当没有 name 时候,将字段大写为列名
                        if (sInt.name().length() < 1)
                            columnName = field.getName().toUpperCase();
                        else
                            columnName = sInt.name();
                        columnsDefs.add(columnName + " INT" + getConstraints(sInt.constraints()));
                    }
                    if (anns[0] instanceof SQLString) {
                        SQLString sString = (SQLString) anns[0];
                        // 当没有 name 时候,将字段大写为列名
                        if (sString.name().length() < 1)
                            columnName = field.getName().toUpperCase();
                        else
                            columnName = sString.name();
                        columnsDefs.add(columnName + " VARCHAR(" + sString.value() + ")" + getConstraints(sString.constraints()));
                    }
                }
            }
            
            StringBuilder createDB = new StringBuilder("CREATE TABLE " + tableName + "(");
            for (String cols : columnsDefs)
                createDB.append(" " + cols + ",");
            
            // 移除最后一个逗号
            String tableSQL = createDB.substring(0, createDB.length() - 1) + ");";
            
            // 输出创建表的过程
            System.out.println("Table Creation SQL is:\n" + tableSQL);
            return tableSQL;
        }
    }
    
    // 返回指定的约束
    public String getConstraints(Constraints con) {
        String constras = "";
        if (!con.allowNull())
            constras += " NOT NULL";
        if (con.primaryKey())
            constras += " PRIMARY KEY";
        if (con.unique())
            constras += " UNIQUE";
        return constras;
    }
}

说明

  1. 版本兼容性:文中代码使用了 com.mysql.jdbc.Driver,这是 MySQL Connector/J 5.x 及更早版本的驱动类名。在新版本(6.x+)中通常建议使用 com.mysql.cj.jdbc.Driver
  2. 技术时效:本文示例基于反射(Reflection)在运行时处理注解。现代开发中,对于复杂的元数据处理,常使用编译期注解处理器(APT)框架(如 Lombok 或 MapStruct 的原理),性能更优。
  3. 代码逻辑:示例代码主要用于演示注解与反射的结合用法,实际生产环境中连接池管理、SQL 防注入及资源关闭逻辑需更加严谨。