Skip to content

注解

为什么需要注解?

在注解出现之前,Java 配置主要依赖 XML 文件:

xml
<!-- 传统 XML 配置 -->
<bean id="userService" class="com.example.UserServiceImpl">
    <property name="userDao" ref="userDao"/>
</bean>

XML 配置的痛点:

  1. 代码与配置分离:修改代码时容易忘记同步修改 XML
  2. 冗长繁琐:一个简单的注入需要多行 XML
  3. 类型不安全:XML 中的类名是字符串,编译期无法检查

注解(JDK 1.5 引入)将配置直接写在代码中,实现声明式编程

java
// 注解替代 XML —— 配置即代码
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;
}

注解的本质是元数据——它是附加在代码上的标签,本身不改变代码逻辑,但可以被编译器、框架或运行时环境读取并做出相应处理。


内置注解

@Override

标记方法重写父类方法。如果方法签名不匹配(不是重写),编译器会报错:

java
class Parent {
    public void doSomething() {}
}

class Child extends Parent {
    @Override
    public void doSomething() {}  // 正确重写

    // @Override
    // public void doSomethng() {}  // 编译错误:拼写错误被立即发现
}

@Deprecated

标记已过时的 API。编译器会发出警告,IDE 通常会在调用处加删除线:

java
@Deprecated
public void oldMethod() {
    // 不推荐使用,请使用 newMethod()
}

@Deprecated(since = "2.0", forRemoval = true)  // Java 9+
public void willBeRemoved() {}

@SuppressWarnings

抑制编译器警告:

java
@SuppressWarnings("unchecked")
public void legacyCode() {
    List list = new ArrayList();  // 原始类型警告被抑制
    list.add("hello");
}

@SuppressWarnings({"unchecked", "deprecation"})
public void suppressMultiple() {
    oldMethod();  // 两个警告都抑制
}

@FunctionalInterface

标记接口是函数式接口(只有一个抽象方法),编译器会强制检查:

java
@FunctionalInterface
interface Calculator {
    int calculate(int a, int b);
    // int subtract(int a, int b);  // 编译错误:只能有一个抽象方法
}

@SafeVarargs

抑制堆污染警告(泛型可变参数):

java
@SafeVarargs
public static <T> List<T> asList(T... items) {
    return Arrays.asList(items);
}

元注解

元注解是注解的注解,用于定义注解的行为。

@Target — 指定注解能用在什么地方

java
@Target(ElementType.METHOD)  // 只能用在方法上
public @interface MyAnnotation {}

@Target({ElementType.TYPE, ElementType.METHOD})  // 可以用在类和方法上
public @interface AnotherAnnotation {}
ElementType适用范围
TYPE类、接口、枚举、注解
FIELD字段(包括枚举常量)
METHOD方法
PARAMETER方法参数
CONSTRUCTOR构造器
LOCAL_VARIABLE局部变量
ANNOTATION_TYPE注解类型
PACKAGE
TYPE_PARAMETER类型参数(Java 8+)
TYPE_USE任何类型使用处(Java 8+)

@Retention — 指定注解保留到哪个阶段

java
@Retention(RetentionPolicy.SOURCE)   // 源码阶段,编译后丢弃
public @interface SourceOnly {}

@Retention(RetentionPolicy.CLASS)    // 保留到 class 文件,但运行时不可见(默认)
public @interface ClassRetained {}

@Retention(RetentionPolicy.RUNTIME)  // 运行时可见,可通过反射读取
public @interface RuntimeVisible {}
RetentionPolicy生命周期典型用途
SOURCE源代码阶段@Override@SuppressWarnings
CLASS编译后保留在 class 文件@NonNull 等 IDE 检查注解
RUNTIME运行时可通过反射获取Spring 注解、JPA 注解

绝大多数业务注解使用 RUNTIME,因为需要通过反射读取。

@Inherited — 控制注解是否被子类继承

java
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface InheritedAnnotation {}

@InheritedAnnotation
class Parent {}

class Child extends Parent {}  // Child 也拥有 @InheritedAnnotation

注意:@Inherited 只对类继承有效,接口实现和接口继承不受影响。

@Documented — 生成 Javadoc 时包含注解

java
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiDoc {
    String value();
}

@Repeatable — 允许在同一位置重复使用(Java 8+)

java
// 容器注解
@Retention(RetentionPolicy.RUNTIME)
@interface Schedules {
    Schedule[] value();
}

// 可重复注解
@Repeatable(Schedules.class)
@Retention(RetentionPolicy.RUNTIME)
@interface Schedule {
    String day();
    String time();
}

// 使用
@Schedule(day = "Monday", time = "10:00")
@Schedule(day = "Friday", time = "14:00")
public class MeetingRoom {}

自定义注解

注解的成员类型

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ApiOperation {
    // 成员类型只能是:基本类型、String、Class、enum、注解、及以上类型的数组
    String value();                    // 字符串
    int order() default 0;            // 基本类型,带默认值
    Class<?> responseType() default Void.class;  // Class 类型
    Status status() default Status.ACTIVE;       // 枚举类型
    String[] tags() default {};                   // 数组类型
}

enum Status { ACTIVE, INACTIVE, DEPRECATED }

注解成员声明规则:

  1. 看起来像方法,实际上是属性,用 () 声明
  2. 返回值类型有限制(见上方注释)
  3. 可以用 default 设置默认值
  4. 如果只有一个名为 value 的成员,使用时可以省略 value=
java
// 使用
@ApiOperation("查询用户")                           // value 可省略
@ApiOperation(value = "查询用户", order = 1)        // 完整写法
public User getUser(Long id) { return null; }

实用示例:方法执行时间统计

java
// 定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExecutionTime {
    String value() default "";
}

// 使用注解
public class UserService {
    @ExecutionTime("查询用户")
    public User findById(Long id) {
        // 模拟耗时操作
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        return new User(id, "张三");
    }

    @ExecutionTime("保存用户")
    public void save(User user) {
        try { Thread.sleep(200); } catch (InterruptedException e) {}
    }
}

// 通过反射处理注解
public class ExecutionTimeProcessor {
    public static void process(Object obj) throws Exception {
        Class<?> clazz = obj.getClass();
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.isAnnotationPresent(ExecutionTime.class)) {
                ExecutionTime annotation = method.getAnnotation(ExecutionTime.class);
                long start = System.currentTimeMillis();
                method.invoke(obj);  // 实际调用
                long end = System.currentTimeMillis();
                System.out.printf("方法 [%s] 耗时: %dms%n",
                        annotation.value(), end - start);
            }
        }
    }
}

注解处理

运行时处理(反射)

运行时处理是最常用的方式,Spring 框架大量使用此方式:

java
// 解析类上的注解
@Table(name = "t_user")
public class User {
    @Column(name = "user_name", length = 50)
    private String name;

    @Column(name = "user_age")
    private int age;
}

// 注解处理器
public class TableGenerator {
    public static String generateCreateTableSQL(Class<?> clazz) {
        Table table = clazz.getAnnotation(Table.class);
        if (table == null) return null;

        StringBuilder sql = new StringBuilder();
        sql.append("CREATE TABLE ").append(table.name()).append(" (\n");

        for (Field field : clazz.getDeclaredFields()) {
            Column column = field.getAnnotation(Column.class);
            if (column != null) {
                sql.append("  ").append(column.name())
                   .append(" ").append(mapType(field.getType()))
                   .append(",\n");
            }
        }
        sql.deleteCharAt(sql.lastIndexOf(","));
        sql.append(")");
        return sql.toString();
    }

    private static String mapType(Class<?> type) {
        if (type == String.class) return "VARCHAR(255)";
        if (type == int.class || type == Integer.class) return "INT";
        return "TEXT";
    }
}

编译期处理(APT)

编译期注解处理通过 javax.annotation.processing.AbstractProcessor 实现,在编译阶段扫描注解并生成代码。典型应用:

  • Lombok@Data@Getter@Setter 生成 getter/setter
  • MapStruct@Mapping 生成对象转换代码
  • ButterKnife@BindView 生成 findViewById 代码
java
// 编译期处理器示例(简化版)
@SupportedAnnotationTypes("com.example.Builder")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class BuilderProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations,
                           RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(Builder.class)) {
            // 使用 JavaPoet 等工具生成 Builder 类源码
            generateBuilderClass((TypeElement) element);
        }
        return true;
    }
}

Spring 注解驱动开发

Spring 的注解体系是运行时注解处理的最佳实践:

分层常用注解作用
组件注册@Component@Service@Repository@Controller将类注册为 Spring Bean
配置@Configuration@Bean@ComponentScanJava 配置替代 XML
依赖注入@Autowired@Qualifier@Resource@Value自动注入依赖
事务@Transactional声明式事务管理
AOP@Aspect@Before@After@Around面向切面编程
Web@RequestMapping@GetMapping@PostMapping@PathVariable@RequestBody请求映射
条件@Conditional@Profile@ConditionalOnClass条件化配置
异步@Async@EnableAsync异步方法执行
调度@Scheduled@EnableScheduling定时任务

Spring 处理 @Autowired 的简化原理:

java
// Spring 容器启动时扫描 @Autowired
public void injectDependencies(Object bean) throws Exception {
    for (Field field : bean.getClass().getDeclaredFields()) {
        if (field.isAnnotationPresent(Autowired.class)) {
            field.setAccessible(true);
            Object dependency = applicationContext.getBean(field.getType());
            field.set(bean, dependency);
        }
    }
}

常见面试题

Q1: 注解是什么?和注释有什么区别?

注解是代码的元数据标签,可以被编译器或运行时环境读取处理;注释是给人看的说明文字,编译后完全消失。注解可以影响程序行为,注释不能。

Q2: @Retention 的三种策略有什么区别?

  • SOURCE:只在源码中保留,编译后丢弃(如 @Override
  • CLASS:保留到 class 文件,但运行时不可见(如 Lombok 的 @NonNull
  • RUNTIME:运行时可通过反射获取(如 Spring 注解)

Q3: @Target 不指定时,注解可以用在哪些地方?

可以用在所有 ElementType 上(除了 TYPE_PARAMETERTYPE_USE)。

Q4: 如何通过反射获取注解?

java
// 类上的注解
clazz.getAnnotation(MyAnnotation.class);
clazz.isAnnotationPresent(MyAnnotation.class);

// 方法上的注解
method.getAnnotation(MyAnnotation.class);

// 方法参数上的注解
Annotation[][] paramAnnotations = method.getParameterAnnotations();

Q5: @Inherited 有什么限制?

只对类继承有效,接口实现和接口继承不受影响。注解在接口上时,实现类不会继承该注解。

Q6: 注解的成员可以有哪些类型?

基本类型(int、long 等)、String、Class、枚举、其他注解,以及以上类型的一维数组。不能是包装类、不能是自定义对象、不能是二维数组。

Q7: 编译期注解处理和运行时注解处理有什么区别?

编译期处理在编译阶段通过 AbstractProcessor 生成新代码(如 Lombok),不增加运行时开销。运行时处理通过反射在程序运行期间读取注解(如 Spring),有少量性能开销但更灵活。

Q8: Spring 如何根据 @Autowired 自动注入?

Spring 容器启动时扫描所有 Bean 的字段,如果字段上有 @Autowired,通过反射 field.setAccessible(true)field.set(bean, dependency) 注入对应的 Bean 实例。