一、注解的核心作用:不止是 "代码标签"
注解(Annotation)是 Java 的元数据机制,附加在类、方法、字段等程序元素上,为编译器、框架或工具提供额外信息。它不直接参与业务逻辑执行,但能通过解析工具转化为实际功能,核心作用可概括为 3 点:
- 简化配置,解耦逻辑
替代传统 XML 或硬编码配置,让配置与代码紧密结合。例如 Java Web 的@WebServlet("/hello")直接绑定 URL 与 Servlet,无需在web.xml中配置;Spring 的@Autowired自动注入依赖,省去手动获取 Bean 的繁琐步骤。
- 实现通用功能复用
将日志记录、权限校验、参数校验等通用功能封装为注解,一次定义可全局复用。例如通过@LogRecord注解,无需在每个接口手动写日志代码,即可自动记录调用信息。
- 编译时校验与提示
辅助编译器检查代码逻辑,减少错误。例如@Override强制校验方法是否真的重写父类方法,@Deprecated标记过时元素,提醒开发者避免使用。
注解与注释的核心区别
| 特性 | 注解(Annotation) | 注释(Comment) |
|---|---|---|
| 阅读对象 | 程序(编译器 / 框架 / 工具) | 开发者 |
| 作用 | 提供配置 / 校验 / 标记信息 | 解释代码逻辑 |
| 生命周期 | 可保留到运行时 | 编译时被忽略 |
二、元注解:自定义注解的 "规则基石"
元注解是修饰注解的注解,定义自定义注解的使用范围、生命周期等规则,Java 内置 4 种核心元注解,是自定义注解的必备基础:
@Target:限制注解使用范围
指定注解能修饰的程序元素,常用值:
-
ElementType.METHOD:仅用于方法(如日志、权限注解); -
ElementType.TYPE:用于类、接口、枚举(如 Spring 的@Controller); -
ElementType.FIELD:用于字段(如参数校验注解); -
支持多范围配置:
@Target({ElementType.TYPE, ElementType.METHOD})。 -
@Retention:指定注解生命周期
决定注解保留阶段,核心值:
-
RetentionPolicy.RUNTIME:保留到运行时,可通过反射解析(自定义注解常用); -
RetentionPolicy.SOURCE:仅保留在源文件,编译时删除(如@Override); -
RetentionPolicy.CLASS:保留到字节码,运行时不可反射(默认值)。 -
@Documented:纳入 API 文档
让注解信息能被javadoc工具提取到文档中,方便开发者查阅注解用途。
@Inherited:允许子类继承
子类可自动继承父类的注解(仅对类注解有效),例如父类用@Service注解,子类无需重复声明即可被 Spring 识别。
三、自定义注解实战:从 0 到 1 实现
掌握元注解后,即可动手实现自定义注解。以下以 "接口日志记录" 为例,完整演示注解定义、使用、解析全流程。
步骤 1:定义自定义注解
java
import java.lang.annotation.*;
/**
* 接口日志记录注解
*/
@Target(ElementType.METHOD) // 仅用于方法
@Retention(RetentionPolicy.RUNTIME) // 保留到运行时,支持反射
@Documented // 纳入API文档
public @interface LogRecord {
// 接口功能描述(必填)
String value();
// 是否记录请求参数(默认true)
boolean recordParams() default true;
// 是否记录返回结果(默认true)
boolean recordResult() default true;
}
-
注解参数格式:返回值类型 参数名() [default 默认值];
-
无默认值的参数(如value()),使用时必须显式赋值;
-
value()为特殊参数,赋值时可省略 "value="(如@LogRecord("用户查询"))。
步骤 2:使用自定义注解
在目标接口方法上添加注解,标记需要记录日志的方法:
java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
// 标记日志记录:记录参数和结果
@GetMapping("/user/query")
@LogRecord(value = "用户查询接口")
public User queryUser(@RequestParam Long userId) {
// 业务逻辑:查询用户信息
User user = new User();
user.setUserId(userId);
user.setUserName("张三");
return user;
}
// 标记日志记录:不记录返回结果
@GetMapping("/user/count")
@LogRecord(value = "用户数量统计", recordResult = false)
public Integer countUser() {
return 100; // 模拟用户总数
}
}
// 简单用户实体类
class User {
private Long userId;
private String userName;
// getter/setter省略
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public String getUserName() { return userName; }
public void setUserName(String userName) { this.userName = userName; }
@Override
public String toString() {
return "User{userId=" + userId + ", userName='" + userName + "'}";
}
}
步骤 3:解析注解 ------ 基于 AOP 实现无侵入
注解需通过解析工具生效,这里用 Spring AOP 切面统一处理,无需修改业务代码:
(1)引入依赖(Spring Boot 项目)
java
<!-- Spring AOP依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 日志依赖 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
(2)实现 AOP 切面解析注解
java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;
@Aspect // 标记为切面类
@Component // 交给Spring管理
public class LogRecordAspect {
private static final Logger logger = LoggerFactory.getLogger(LogRecordAspect.class);
// 切入点:拦截所有带@LogRecord注解的方法
@Pointcut("@annotation(com.example.annotation.LogRecord)")
public void logPointcut() {}
// 环绕通知:方法执行前后记录日志
@Around("logPointcut()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 获取方法和注解信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
LogRecord logAnnotation = method.getAnnotation(LogRecord.class);
// 2. 提取注解参数和请求信息
String desc = logAnnotation.value();
boolean recordParams = logAnnotation.recordParams();
boolean recordResult = logAnnotation.recordResult();
String methodName = method.getDeclaringClass().getName() + "." + method.getName();
Object[] params = joinPoint.getArgs();
// 3. 记录方法执行前日志
logger.info("【{}】调用开始 - 时间:{},方法:{},参数:{}",
desc, new Date(), methodName,
recordParams ? Arrays.toString(params) : "不记录");
// 4. 执行目标方法(业务逻辑)
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed(); // 执行方法并获取结果
long costTime = System.currentTimeMillis() - startTime;
// 5. 记录方法执行后日志
logger.info("【{}】调用结束 - 耗时:{}ms,结果:{}",
desc, costTime,
recordResult ? result.toString() : "不记录");
return result;
}
}
步骤 4:测试效果
启动项目后访问接口,日志输出如下:
txt
INFO [http-nio-8080-exec-1] c.e.annotation.LogRecordAspect: 【用户查询接口】调用开始 - 时间:Wed Nov 29 17:00:00 CST 2025,方法:com.example.controller.UserController.queryUser,参数:[1001]
INFO [http-nio-8080-exec-1] c.e.annotation.LogRecordAspect: 【用户查询接口】调用结束 - 耗时:8ms,结果:User{userId=1001, userName='张三'}
INFO [http-nio-8080-exec-2] c.e.annotation.LogRecordAspect: 【用户数量统计】调用开始 - 时间:Wed Nov 29 17:01:00 CST 2025,方法:com.example.controller.UserController.countUser,参数:[]
INFO [http-nio-8080-exec-2] c.e.annotation.LogRecordAspect: 【用户数量统计】调用结束 - 耗时:3ms,结果:不记录
四、自定义注解核心要点
-
单一职责:一个注解只处理一件事(如日志注解仅记录日志),避免逻辑混乱;
-
合理设置生命周期:需运行时解析的注解,必须指定@Retention(RetentionPolicy.RUNTIME);
-
简化参数设计:核心参数无默认值,非核心参数提供默认值,减少使用成本;
-
无侵入实现:通过 AOP 或反射解析注解,不侵入业务代码,降低耦合。
五、总结
注解的核心价值是 "解耦与复用"------ 用元数据替代繁琐配置,用通用注解封装重复功能。自定义注解的关键是:通过元注解明确规则,通过反射 / AOP 实现解析逻辑。掌握注解后,能大幅简化开发流程,让代码更简洁、易维护。