
在Java开发中,注解(Annotation)是一种元数据形式,它不直接影响代码的执行逻辑,却能为代码提供额外的描述信息,被编译器、框架或工具解析利用。从JDK内置的@Override、@Deprecated,到Spring的@Controller、MyBatis的@Mapper,注解已成为框架开发、代码简化、逻辑解耦的核心手段。而自定义注解,更是让开发者能够根据业务需求封装专属元数据,实现灵活的功能扩展。本文将从基础概念到实战场景,全面拆解自定义注解的定义、解析与应用,助力开发者掌握这一核心技能。
注解是Java 5引入的特性,本质是一种特殊的接口(继承自java.lang.annotation.Annotation),用于在代码中嵌入元数据。其核心价值在于“解耦”——将非业务逻辑(如日志、权限、校验、配置)与业务代码分离,由专门的处理器解析注解并执行对应逻辑,大幅提升代码的可维护性和灵活性。
按使用场景和作用范围,注解可分为三类:
@Override(重写校验)、@Deprecated(标记过时)、@SuppressWarnings(压制警告);@Autowired(依赖注入)、MyBatis的@Select(SQL映射)、Lombok的@Data(属性生成);自定义注解的核心是“元注解 + 注解属性”:元注解用于定义注解的自身行为(如生效范围、生命周期),注解属性用于存储自定义配置信息。
JDK提供4个核心元注解,用于约束自定义注解的特性,是自定义注解的必备要素:
元注解 | 作用 | 常用取值 |
|---|---|---|
@Target | 指定注解可作用的Java元素范围 | ElementType.TYPE(类/接口)、METHOD(方法)、FIELD(字段)、PARAMETER(参数)、CONSTRUCTOR(构造器)等 |
@Retention | 指定注解的生命周期(保留阶段) | RetentionPolicy.SOURCE(仅源码,编译后丢弃)、CLASS(编译后保留在class文件,运行时丢弃,默认)、RUNTIME(运行时保留,可通过反射解析,最常用) |
@Documented | 指定注解是否被javadoc工具生成文档 | 无取值,仅作为标记 |
@Inherited | 指定注解是否可被子类继承(仅作用于类注解) | 无取值,仅作为标记 |
核心提醒:@Retention(RetentionPolicy.RUNTIME)是多数实战场景的必备配置,只有设置为RUNTIME,才能在程序运行时通过反射解析注解,实现动态功能。
自定义注解使用@interface关键字声明,语法结构为:元注解 + 注解名称 + 注解属性。
// 元注解组合
@Target({ElementType.METHOD, ElementType.TYPE}) // 作用于类和方法
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,支持反射解析
@Documented // 生成javadoc文档
@Inherited // 允许子类继承
public @interface MyAnnotation {
// 注解属性:格式为“属性类型 属性名() [default 默认值];”
String value() default ""; // 特殊属性:value,使用时可省略属性名直接赋值
int order() default 0; // 普通属性,带默认值
String[] tags(); // 数组类型属性,无默认值,使用时必须赋值
}注解属性的声明有严格规则,不符合规则会导致编译失败:
default指定默认值,无默认值的属性,使用注解时必须显式赋值。{}包裹,如tags = {"log", "auth"}。定义完注解后,可在指定范围的Java元素上使用,根据属性是否有默认值,赋值方式不同:
// 作用于类,所有属性显式赋值
@MyAnnotation(value = "user-service", order = 1, tags = {"service", "user"})
public class UserService {
// 作用于方法,value属性省略名称,tags赋值单个值
@MyAnnotation("get-user")
public User getUserById(@MyAnnotation("user-id") Long id) {
return new User();
}
}注解本身仅为元数据,需通过“注解处理器”解析并执行逻辑。根据注解的生命周期,解析方式分为两类:SOURCE阶段通过APT(注解处理器工具)解析,RUNTIME阶段通过反射解析。其中,反射解析是最常用、最灵活的方式,适用于多数业务场景。
需求:自定义@LogRecord注解,标注在接口方法上,记录方法调用者、调用时间、执行耗时,实现无侵入式日志记录。
import java.lang.annotation.*;
@Target(ElementType.METHOD) // 仅作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,支持反射
@Documented
public @interface LogRecord {
// 日志描述,默认取方法名
String desc() default "";
// 是否记录请求参数
boolean recordParam() default true;
// 是否记录返回结果
boolean recordResult() default false;
}通过Spring AOP拦截标注了@LogRecord的方法,反射解析注解属性,执行日志记录逻辑(非Spring环境可通过动态代理实现)。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
// 切面类,拦截注解标注的方法
@Aspect
@Component
public class LogRecordAspect {
// 切入点:拦截所有标注@LogRecord的方法
@Around("@annotation(com.example.annotation.LogRecord)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 反射获取方法及注解属性
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
LogRecord logRecord = method.getAnnotation(LogRecord.class);
// 2. 解析注解属性
String desc = logRecord.desc().isEmpty() ? method.getName() : logRecord.desc();
boolean recordParam = logRecord.recordParam();
boolean recordResult = logRecord.recordResult();
// 3. 日志前置记录
LocalDateTime startTime = LocalDateTime.now();
String username = getCurrentUsername(); // 模拟获取当前登录用户
System.out.printf("[日志记录] 时间:%s,用户:%s,操作:%s%n", startTime, username, desc);
if (recordParam) {
Object[] args = joinPoint.getArgs();
System.out.printf("[日志记录] 请求参数:%s%n", args);
}
// 4. 执行目标方法
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(); // 执行原方法
long cost = System.currentTimeMillis() - start;
// 5. 日志后置记录
if (recordResult) {
System.out.printf("[日志记录] 返回结果:%s%n", result);
}
System.out.printf("[日志记录] 执行耗时:%dms%n", cost);
return result;
}
// 模拟获取当前登录用户
private String getCurrentUsername() {
return "admin";
}
}@RestController
@RequestMapping("/user")
public class UserController {
// 使用日志注解,配置记录参数和结果
@LogRecord(desc = "根据ID查询用户", recordParam = true, recordResult = true)
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
User user = new User();
user.setId(id);
user.setUsername("zhangsan");
return user;
}
}调用接口后,控制台输出日志:
[日志记录] 时间:2026-01-29T15:30:00,用户:admin,操作:根据ID查询用户
[日志记录] 请求参数:[1]
[日志记录] 返回结果:User(id=1, username=zhangsan)
[日志记录] 执行耗时:12ms需求:自定义@RequiresPermission注解,标注在接口方法上,校验当前用户是否拥有指定权限,无权限则抛出异常。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequiresPermission {
// 所需权限标识(支持多个)
String[] value();
// 无权限时的提示信息
String message() default "无权限访问该接口";
}@Aspect
@Component
public class PermissionAspect {
@Around("@annotation(com.example.annotation.RequiresPermission)")
public Object checkPermission(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 解析注解
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
RequiresPermission permission = signature.getMethod().getAnnotation(RequiresPermission.class);
String[] requiredPermissions = permission.value();
String message = permission.message();
// 2. 获取当前用户拥有的权限(模拟从登录上下文获取)
List<String> userPermissions = getCurrentUserPermissions();
// 3. 权限校验
boolean hasPermission = Arrays.stream(requiredPermissions)
.anyMatch(userPermissions::contains);
if (!hasPermission) {
throw new RuntimeException(message);
}
// 4. 校验通过,执行目标方法
return joinPoint.proceed();
}
// 模拟获取当前用户权限
private List<String> getCurrentUserPermissions() {
return Arrays.asList("user:query", "user:add"); // admin用户拥有的权限
}
}@RestController
@RequestMapping("/user")
public class UserController {
// 要求拥有"user:query"权限
@RequiresPermission("user:query")
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return new User(id, "zhangsan");
}
// 要求拥有"user:delete"权限(当前用户无此权限)
@RequiresPermission(value = "user:delete", message = "无删除用户权限")
@DeleteMapping("/{id}")
public String deleteUser(@PathVariable Long id) {
return "删除成功";
}
}调用删除接口时,因无权限抛出异常:java.lang.RuntimeException: 无删除用户权限,实现权限校验功能。
可将多个注解组合为一个复合注解,简化使用场景。例如,将日志记录与权限校验注解组合为@LogAndAuth,标注一次即可触发两个功能。
// 复合注解:组合@LogRecord和@RequiresPermission
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@LogRecord(desc = "需权限校验的操作", recordParam = true)
@RequiresPermission("admin:operate")
public @interface LogAndAuth {
// 可重写组合注解的属性(可选)
String logDesc() default "";
}使用时,标注复合注解即可:
@LogAndAuth(logDesc = "管理员操作")
@PostMapping("/admin/operate")
public String adminOperate() {
return "操作成功";
}当注解属性取值有限时,可结合枚举类型,避免硬编码,提升代码规范性。
// 定义日志级别枚举
public enum LogLevel {
INFO, WARN, ERROR
}
// 注解属性使用枚举
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
LogLevel level() default LogLevel.INFO; // 枚举类型属性
String desc();
}
// 使用注解
@Log(level = LogLevel.WARN, desc = "敏感操作日志")
public void sensitiveOperate() {
// 业务逻辑
}对于@Retention(RetentionPolicy.SOURCE)的注解,可通过APT(Annotation Processing Tool)在编译期解析注解,自动生成代码(如Lombok的@Data生成getter/setter)。核心是实现javax.annotation.processing.AbstractProcessor,重写process方法解析注解并生成代码。
适用场景:需在编译期生成模板代码的场景(如ORM映射、DTO转换),优点是无运行时反射开销,性能优异;缺点是开发复杂度高于反射解析。
现象:标注注解后,无对应功能触发,控制台无日志或校验逻辑未执行。 规避方案:
@Retention是否设置为RUNTIME,非RUNTIME无法通过反射解析;@Aspect和@Component,且包路径被Spring扫描;@Target)与使用位置一致(如方法注解不能标注在类上)。现象:使用注解时,编译提示“Attribute value must be constant”或“Missing required attribute”。 规避方案:
{}包裹,单值可直接赋值;现象:Spring AOP场景下,注解标注的方法未被切面拦截,功能不生效。 规避方案:
@Around等注解的切入点能匹配目标方法(如注解全类名正确);@Service/@Controller的类无法被拦截。现象:父类标注注解,子类继承后,子类方法无对应功能。 规避方案:
@Inherited元注解的类注解可被继承,方法注解、字段注解不支持继承;自定义注解的核心价值在于“用元数据驱动逻辑,实现代码解耦与复用”,它让非业务逻辑(日志、权限、校验)与业务代码分离,大幅提升代码的可维护性和扩展性。实际使用中需遵循以下原则:
@RequiresPermission、@LogRecord)。从框架封装到业务开发,自定义注解已成为Java开发者的必备技能。掌握本文所述的定义语法、解析方式与实战场景,可灵活运用注解简化开发、封装通用逻辑,让代码更优雅、更具扩展性。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。