Appearance
注解
为什么需要注解?
在注解出现之前,Java 配置主要依赖 XML 文件:
xml
<!-- 传统 XML 配置 -->
<bean id="userService" class="com.example.UserServiceImpl">
<property name="userDao" ref="userDao"/>
</bean>XML 配置的痛点:
- 代码与配置分离:修改代码时容易忘记同步修改 XML
- 冗长繁琐:一个简单的注入需要多行 XML
- 类型不安全: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 }注解成员声明规则:
- 看起来像方法,实际上是属性,用
()声明 - 返回值类型有限制(见上方注释)
- 可以用
default设置默认值 - 如果只有一个名为
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、@ComponentScan | Java 配置替代 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_PARAMETER 和 TYPE_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 实例。
