JAVA注解使用
参考资料
详解Java如何实现自定义注解
https://www.jb51.net/article/252457.htm
简介
概念说明
概念
- 概念:说明程序的。给计算机看的
- 注释:用文字描述程序的。给程序员看的
- 定义:注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。
作用和作用分类
- 编写文档:通过代码里标识的注解生成文档【生成文档doc文档】
- 代码分析:通过代码里标识的注解对代码进行分析【使用反射】
- 编译检查:通过代码里标识的注解让编译器能够实现基本的编译检查【Override】
JDK中预定义的一些注解
- @Override:检测被该注解标注的方法是否是继承自父类(接口)的
- @Deprecated:该注解标注的内容,表示已过时
- @SuppressWarnings:压制警告
一般传递参数all @SuppressWarnings(“all”)
自定义注解
注解格式
元注解[例如:@Target、@Retention、@Documented]
第三方注解[例如: @Constraint]
public @interface 注解名称{
属性列表;
}
元注解
JDK默认提供了@Target、@Retention、@Documented、@Inherited、@Native、@Repeatable
六大默认注解。
- @Target 解析注解在什么地方可以进行使用,可以同时支持多种,主要包含以下类型:
- TYPE 可以在
类、接口、注解、枚举
上使用 - FIELD 可以在
字段、枚举常量
上使用 - METHOD 可以在
方法
上使用 - PARAMETER 可以在
方法参数
上使用 - CONSTRUCTOR 可以在
构建函数
上使用 - LOCAL_VARIABLE 可以在
本地变量
上使用 - ANNOTATION_TYPE 可以在
注解类型
上使用,例如@Retention就用到了这个注解 - PACKAGE 可以在
包
上使用 - TYPE_PARAMETER 可以在
类型参数
上使用 - TYPE_USE 可以作为一个
类型
使用
- @Retention 标识注解被保留的阶段
- SOURCE 注解在编译阶段会被忽略,不进行保留
- CLASS 注解在编译阶段会被记录下来,但是不会保留在运行时。这是默认的行为
- RUNTIME 注解在编译阶段会被记录下来,并保留在运行时内,所以可以通过反射来读取
- @Documented 注解是否会被输出到JavaDoc文档里
- @Inherited 注解是否可以被继承
- @Native 定义一个可以被Native 编码访问到的常量
- @Repeatable 标识使用@Repeatable申明的注解类型是可以重复的
注解本质
注解本质上就是一个接口,该接口默认继承Annotation接口
public interface MyAnno extends java.lang.annotation.Annotation {}
属性
接口中的抽象方法,比如定义了一个名为name()
的抽象方法,在使用的时候按照属性的方式来进行赋值, 例如name="张超"
要求:
-
1.属性的返回值类型有下列取值
- 基本数据类型
- String
- 枚举
- 注解
- 以上类型的数组
-
2.定义了属性,在使用时需要给属性赋值
- 如果定义属性时,使用default关键字给属性默认初始化值,则使用注解时,可以不进行属性的赋值。
- 如果只有一个属性需要赋值,并且属性的名称是value,则value可以省略,直接定义值即可。
- 数组赋值时,值使用{}包裹。如果数组中只有一个值,则{}可以省略
注解生成文档案例
生成的源代码
我们以DatabaseConfig .java
为示例来生成一份JavaDoc文档
/**
* 数据表类注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DatabaseConfig {
/**
* 数据库表Class集合,用于生成数据库表
* 默认为空集合
* */
Class<?>[] entity() default {};
/**
* 数据库版本
* 默认为1
* */
int version() default 1;
/**
* 数据库名称
* 默认为test_db
* */
String databaseName() default "test_db";
}
生成Doc
执行javadoc -encoding utf-8 DatabaseConfig.java
,然后就可以生成一系列的文档文件了,打开生成后的文档文件,可以看到对类的详细说明
简单的自定义反射演示
注解定义
/**
* 描述需要执行的类名,和方法名
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Reflect {
String className();
String methodName();
}
调用类定义
package com.example.call;
public class BlackPrinter {
public static void print() {
System.out.println("通过注解和反射来打印一条信息");
}
}
测试
@Reflect(className = "com.example.call.BlackPrinter", methodName = "print")
public class ReflectTest {
public static void main(String[] args) throws Exception {
/**
* 前提:不能改变该类的任何代码。可以创建任意类的对象,可以执行任意方法
*/
//1.解析注解
// 1.1获取该类的字节码文件对象
Class<ReflectTest> reflectTestClass = ReflectTest.class;
//2.获取上边的注解对象, 其实就是在内存中生成了一个该注解接口的子类实现对象
/*
public class ReflectImpl implements Reflect{
public String className(){
return "com.example.call.BlackPrinter";
}
public String methodName(){
return "print";
}
}
*/
//3.调用注解对象中定义的抽象方法,获取返回值
Reflect reflect = reflectTestClass.getAnnotation(Reflect.class);
String className = reflect.className();
String methodName = reflect.methodName();
System.out.println(className);
System.out.println(methodName);
// 4.加载该类进内存
Class<?> cls = Class.forName(className);
// 5.创建对象
Object obj = cls.newInstance();
// 6.获取方法对象
Method method = cls.getMethod(methodName);
// 7.执行方法
method.invoke(obj);
}
}
输出如下:
com.example.call.BlackPrinter
print
通过注解和反射来打印一条信息
整个程序的大体逻辑如下:
-
- 在程序最外层通过
@Reflect
注解配置了需要进行反射的类名、需要调用的方法名,这样就可以动态的对需要调用的类、方法进行配置。
- 在程序最外层通过
-
- 程序执行过程中通过解析Test类的字节码文件,动态解析出
@Reflect
对象,然后获取其中配置的类名、方法名。
- 程序执行过程中通过解析Test类的字节码文件,动态解析出
-
- 通过Class对类进行加载、对方法进行调用。
通过注解实现配置类
定义枚举类
/**
* 协议类型枚举
*/
public enum ProtocolType {
WebSocket, MQTT;
}
定义注解
/**
* 白名单配置
*/
@Target(ElementType.TYPE) //描述注解能够作用的位置
@Retention(RetentionPolicy.RUNTIME) //描述注解被保留的阶段
public @interface WhiteConfig {
/**
* 白名单地址列表
* */
String[] whiteIpAddress();
}
/**
* 协议配置注解
*/
@Target(ElementType.TYPE) //描述注解能够作用的位置
@Retention(RetentionPolicy.RUNTIME) //描述注解被保留的阶段
public @interface ProtocolConfig {
// 服务器端口号
int port();
// 协议类型[使用枚举来作为参数]
ProtocolType protocolType();
// 白名单配置[使用另一个注解来作为参数]
WhiteConfig whiteConfig();
// 服务器端口号[使用数组来作为参数]
String[] urls();
}
配置类使用注解
/**
* 配置类
*/
@ProtocolConfig(port = 9100 ,protocolType = ProtocolType.MQTT ,urls="172.16.40.121",
whiteConfig = @WhiteConfig(whiteIpAddress = {"119.16.254.37", "116.51.39.46"}))
public class ServerConfig {
}
测试
/**
* 自定义注解测试
*
* @author panyongjie
* @date 2022/12/29
*/
public class AnnotationTest {
public static void main(String[] args) {
ServerConfig serverConfig = new ServerConfig();
ProtocolConfig protocolConfig = serverConfig.getClass().getAnnotation(ProtocolConfig.class);
System.out.println("port: " + protocolConfig.port());
System.out.println("protocolType: " + protocolConfig.protocolType());
for (String url : protocolConfig.urls()) {
System.out.println("url: " + url);
}
for (String whiteIpAddress : protocolConfig.whiteConfig().whiteIpAddress()) {
System.out.println("whiteIpAddress: " + whiteIpAddress);
}
}
}
运行效果如下
port: 9100
protocolType: MQTT
url: 172.16.40.121
whiteIpAddress: 119.16.254.37
whiteIpAddress: 116.51.39.46
自动生成数据库生成演示
数据库注解定义
这里定义四个注解,分别对应字段
、主键
、数据表
、 数据库
配置。
/**
* 表格字段注解,用于实体类字段与数据表字段之间的映射
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TableField {
// 数据表字段名称
String name();
}
/**
* 主键注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PrimaryKey {
boolean autoIncrease() default false;
}
/**
* 表注解,用于生成数据库结构
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
String name();
}
/**
* 数据表类注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DatabaseConfig {
/**
* 数据库表Class集合,用于生成数据库表
* 默认为空集合
* */
Class<?>[] entity() default {};
/**
* 数据库版本
* 默认为1
* */
int version() default 1;
/**
* 数据库名称
* 默认为test_db
* */
String databaseName() default "test_db";
}
使用注解创建数据表
/**
* 人员信息表
*/
@Table(name = "t_user")
public class User {
@TableField(name = "user_id")
@PrimaryKey(autoIncrease = true)
private int userId;
@TableField(name = "user_name")
private String userName;
private String genderName;
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
/**
* 记录信息表
*/
@Table(name = "t_record")
public class Record {
@TableField(name = "record_id")
@PrimaryKey(autoIncrease = true)
private int recordId;
@TableField(name = "user_id")
private int userId;
@TableField(name = "user_name")
private String userName;
@TableField(name = "record_time")
private String recordTime;
private int syncState;
public int getRecordId() {
return recordId;
}
public void setRecordId(int recordId) {
this.recordId = recordId;
}
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getRecordTime() {
return recordTime;
}
public void setRecordTime(String recordTime) {
this.recordTime = recordTime;
}
public int getSyncState() {
return syncState;
}
public void setSyncState(int syncState) {
this.syncState = syncState;
}
}
使用注解创建数据库
/**
* 数据库测试
*
* @author panyongjie
* @date 2022/12/29
*/
@DatabaseConfig(entity = { User.class, Record.class }, version = 2, databaseName = "staff")
public class DatabaseTest {
/**
* 读取配置,解析数据库配置
* */
public static void main(String[] args) throws ClassNotFoundException {
// 1. 获取当前的Class类型
Class<DatabaseTest> databaseTestClass = DatabaseTest.class;
DatabaseConfig databaseConfig = databaseTestClass.getAnnotation(DatabaseConfig.class);
Class<?>[] classes = databaseConfig.entity();
System.out.println("读取数据库配置,数据库名称: " + databaseConfig.databaseName() +
" ,数据库版本:" + databaseConfig.version() +
" ,当前有" + classes.length + "个表格注解类需要解析");
for (Class<?> cls : classes) {
System.out.println("className: " + cls.getName());
Class<?> userClass = Class.forName(cls.getName());
Table table = userClass.getAnnotation(Table.class);
System.out.println("表格名称: " + table.name());
Field[] fields = userClass.getDeclaredFields();
for (Field field : fields) {
if(field.isAnnotationPresent(TableField.class)) {
TableField tableField = field.getAnnotation(TableField.class);
System.out.println("\t字段: " + field.getName() + "需要添加到数据表中,数据字段名称: " + tableField.name());
if(field.isAnnotationPresent(PrimaryKey.class)) {
PrimaryKey primaryKey = field.getAnnotation(PrimaryKey.class);
System.out.println("\t字段: " + field.getName() + "是当前数据表的主键,主键" + (primaryKey.autoIncrease() ? "自增" : "非自增"));
}
} else {
System.out.println("\t字段: " + field.getName() + "不需要添加到数据表中");
}
}
}
}
}
测试
测试效果如下
读取数据库配置,数据库名称: staff ,数据库版本:2 ,当前有2个表格注解类需要解析
className: com.example.table.User
表格名称: t_user
字段: userId需要添加到数据表中,数据字段名称: user_id
字段: userId是当前数据表的主键,主键自增
字段: userName需要添加到数据表中,数据字段名称: user_name
字段: genderName不需要添加到数据表中
className: com.example.table.Record
表格名称: t_record
字段: recordId需要添加到数据表中,数据字段名称: record_id
字段: recordId是当前数据表的主键,主键自增
字段: userId需要添加到数据表中,数据字段名称: user_id
字段: userName需要添加到数据表中,数据字段名称: user_name
字段: recordTime需要添加到数据表中,数据字段名称: record_time
字段: syncState不需要添加到数据表中
可以看到我们通过配置的注解和注解值都能正常进行解析,这样我们就可以根据这些解析出来的值组装成SQL脚本然后去执行生成数据库的操作。
使用方法注解实现开机自检演示
创建方法注解
/**
* 检查注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Check {
// 检查项名称
String checkItemName();
// 异常类型
String exceptionType();
}
注解调用
import com.example.annotation.Check;
import org.springframework.util.StringUtils;
import java.util.Arrays;
import java.util.List;
/**
* 计算机类
*
* @author panyongjie
* @date 2022/12/29
*/
public class Computer {
/**
* 最大支持内存,单位G
* */
private final int MAX_SUPPORT_MEMORY = 32;
/**
* 最大支持硬盘,单位G
* */
private final int MAX_SUPPORT_DISK = 2048;
/**
* 支持的操作系统
* */
private final List<String> SUPPORT_OS_LIST = Arrays.asList(
"windows 2003", "windows vista",
"windows 7", "windows 10", "windows 11", "windows 12",
"macos", "linux", "ubuntu","centos");
/**
* 内存大小,单位G
* */
private int memory;
/**
* 硬盘大小,单位G
* */
private int disk;
/**
* 系统版本
* */
private String systemVersion;
public Computer(int memory, int disk, String systemVersion) {
this.memory = memory;
this.disk = disk;
this.systemVersion = systemVersion;
}
/**
* 内存检查
* */
@Check(checkItemName = "内存", exceptionType = "超出内存支持上限/下限")
public void memoryCheck() throws Exception {
if(memory > 0 && memory <= MAX_SUPPORT_MEMORY) {
System.out.println("内存自检正常");
} else {
throw new Exception("内存大小超出可用范围!");
}
}
/**
* 硬盘检查
* */
@Check(checkItemName = "硬盘", exceptionType = "超出硬盘支持上限/下限")
public void diskCheck(){
if(disk > 0 && disk <= MAX_SUPPORT_DISK) {
System.out.println("硬盘自检正常");
} else {
throw new RuntimeException("硬盘大小超出可用范围!");
}
}
/**
* 系统检查
* */
@Check(checkItemName = "系统版本", exceptionType = "超出系统版本支持范围")
public void systemCheck(){
if(!StringUtils.isEmpty(systemVersion) && SUPPORT_OS_LIST.contains(systemVersion)) {
System.out.println("系统自检正常");
} else {
throw new RuntimeException("不支持的系统版本!");
}
}
/**
* 屏幕信号检查
* */
@Check(checkItemName = "屏幕信号", exceptionType = "超出显卡支持上限/下限")
public void signalCheck(){
System.out.println("屏幕信号输出正常");
}
public void start(){
System.out.println("开始开机自检...");
}
public void finish(){
System.out.println("开始开机自检完毕!");
}
}
测试
/**
* 开机自检测试类
* 当主方法执行后,会自动自行被检测的所有方法(加了Check注解的方法),判断方法是否有异常,并对异常进行打印输出
*/
public class CheckTest {
public static void main(String[] args) {
// 1.创建计算器对象
Computer computer = new Computer(64, 3072, "windows 10");
// 2.获取所有方法
Method[] methods = computer.getClass().getMethods();
computer.start();
System.out.println();
//出现异常的次数
int count = 0;
for (Method method : methods) {
if (method.isAnnotationPresent(Check.class)) {
Check check = method.getAnnotation(Check.class);
System.out.println("--------------------------");
System.out.println("开始" + check.checkItemName() + "自检...");
try {
method.invoke(computer);
} catch (Exception e) {
count++;
System.out.println(check.checkItemName() + "自检异常!");
System.out.println("自检异常类型: " + check.exceptionType());
System.out.println("自检异常原因: " + e.getCause().getMessage());
}
}
}
System.out.println("--------------------------");
System.out.println();
System.out.println("本次开机自检一共出现 " + count + " 次异常");
computer.finish();
}
}
运行输出如下
开始开机自检...
--------------------------
开始内存自检...
内存自检异常!
自检异常类型: 超出内存支持上限/下限
自检异常原因: 内存大小超出可用范围!
--------------------------
开始硬盘自检...
硬盘自检异常!
自检异常类型: 超出硬盘支持上限/下限
自检异常原因: 硬盘大小超出可用范围!
--------------------------
开始屏幕信号自检...
屏幕信号输出正常
--------------------------
开始系统版本自检...
系统自检正常
--------------------------
本次开机自检一共出现 2 次异常
开始开机自检完毕!
注解自动生效
在Springboot web环境中进行测试
添加pom依赖
<!-- Web启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 验证API -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.4.5</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
自定义约束注解
/**
* 自定义不为0注解
*/
@Documented //是否会生成文档
@Constraint(validatedBy = {LongConstraintValidator.class})
@Retention(RetentionPolicy.RUNTIME) //描述注解被保留的阶段
@Target({ElementType.FIELD, ElementType.METHOD}) //生效的目标,对字段和方法生效
public @interface LongDataValidate {
String message() default "不能为null或者0";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
/**
* Long类型验证器
*/
public class LongConstraintValidator implements ConstraintValidator<LongDataValidate, Long> {
@Override
public void initialize(LongDataValidate constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
System.out.println("调用了 initialize 方法");
}
@Override
public boolean isValid(Long value, ConstraintValidatorContext context) {
System.out.println("调用了 isValid 方法");
return null != value && value != 0;
}
}
使用注解
/**
* 通行记录请求
*/
public class AccessRecordReq {
@LongDataValidate(message = "序列号不能为null或者0")
private Long serialNo;
public AccessRecordReq(Long serialNo) {
this.serialNo = serialNo;
}
public Long getSerialNo() {
return serialNo;
}
public void setSerialNo(Long serialNo) {
this.serialNo = serialNo;
}
}
验证注解
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private int code = 200;
private String msg = "success";
private T data;
}
@RestControllerAdvice
public class ApiController {
public <T> Result<T> success(T t) {
return new Result<>(200, "success", t);
}
@ExceptionHandler({BindException.class, Exception.class})
public Result<Void> handleException(Throwable e) {
Result<Void> result = new Result<>();
if (e instanceof BindException) {
result.setCode(1000);
result.setMsg(((BindException) e).getFieldError().getDefaultMessage());
}
else {
result.setCode(500);
result.setMsg(e.getMessage());
}
return result;
}
}
import javax.annotation.Resource;
import javax.validation.Valid;
import java.io.Serializable;
import java.util.List;
/**
* (Customer)表控制层
*/
@RestController
@RequestMapping("customer")
public class CustomerController extends ApiController {
@GetMapping("validate")
@ResponseBody
public Result<Object> validateData(@Valid AccessRecordReq accessRecordReq) {
return success("验证通过" + accessRecordReq.getSerialNo());
}
}
测试
在浏览器中输入http://localhost:8080/customer/validate?serialNo=
或者
http://localhost:8080/customer/validate?serialNo=0
, 服务器控制台输出
调用了 initialize 方法
调用了 isValid 方法
浏览器上输出
{"code":1000,"msg":"序列号不能为null或者0","data":null}
重新输入http://localhost:8080/customer/validate?serialNo=1000
, 浏览器上输出
{"code":200,"msg":"success","data":"验证通过1000"}
可以发现我们的自定义注解已自动生效并进行按照我们定义的规则进行了数据验证。