【Java基础 | 第五篇】注解 和 异常
目录
注解
Java 注解的原理
Annotation (注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
注解本质是一个继承了Annotation 的特殊接口:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
public interface Override extends Annotation{
}
JDK 提供了很多内置的注解(比如 @Override、@Deprecated),同时,我们还可以自定义注解。
注解解析的底层实现
1. 注解的本质
Java 注解(Annotation)是 JDK5 引入的元数据(Metadata),本质是一种特殊的接口(编译后会继承java.lang.annotation.Annotation接口),用于在代码中添加标记信息,本身不直接影响代码逻辑,需通过反射 / 编译器 / 框架解析才能生效。
2. 底层实现
(1)生命周期和作用范围
-
定义注解时,需通过元注解(@Retention、@Target 等) 指定注解的生命周期和作用范围:
-
@Retention:决定注解的存活阶段(源码级SOURCE、字节码级CLASS、运行时RUNTIME); -
@Target:指定注解可修饰的元素(类、方法、字段等)。
-
-
编译阶段:注解会被 javac 编译器处理,根据
@Retention的配置,决定是否将注解信息写入字节码(.class文件):-
SOURCE:仅保留在源码,编译后丢弃(如 @Override、@SuppressWarnings); -
CLASS:保留在字节码,但 JVM 加载类时不载入内存(默认值); -
RUNTIME:保留在字节码且载入 JVM,可通过反射获取(如 Spring 的 @Autowired)。
-
(2)解析机制
注解的核心价值在于 “被解析”,解析方式分为两类:
-
编译期解析:编译器(javac)或 APT(注解处理器)在编译时扫描注解,生成代码 / 做语法检查(如 @Override 检查方法重写、Lombok 的 @Data 生成 getter/setter);
-
运行时解析:通过 Java 反射 API(
Class/Method/Field的getAnnotation()、getAnnotations()等方法)读取注解信息,动态执行逻辑(如 Spring 通过反射解析 @Controller、@RequestMapping,实现请求映射)。
实例:极简版 “请求映射” 逻辑
步骤 1:定义两个核心注解(模拟 @Controller 和 @RequestMapping)
import java.lang.annotation.*;
// 标记类为控制器(保留到运行时,作用于类)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyController {
}
// 标记方法的请求路径(保留到运行时,作用于方法)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyRequestMapping {
// 定义请求路径属性
String value();
}
步骤 2:编写控制器类(标记注解)
// 标记为控制器
@MyController
public class UserController {
// 标记该方法处理 /user/add 请求
@MyRequestMapping("/user/add")
public String addUser() {
return "添加用户成功";
}
// 标记该方法处理 /user/delete 请求
@MyRequestMapping("/user/delete")
public String deleteUser() {
return "删除用户成功";
}
}
步骤 3:核心解析类(用反射读取注解,实现映射 + 执行)
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class AnnotationDispatcher {
// 存储“请求路径→方法”的映射关系
private static Map<String, Method> urlMethodMap = new HashMap<>();
public static void main(String[] args) throws Exception {
// 步骤1:初始化——扫描并解析注解,建立映射
init("UserController"); // 传入要扫描的控制器类名
// 步骤2:模拟前端请求,根据路径执行对应方法
// 模拟请求 /user/add
String result1 = handleRequest("/user/add");
System.out.println(result1); // 输出:添加用户成功
// 模拟请求 /user/delete
String result2 = handleRequest("/user/delete");
System.out.println(result2); // 输出:删除用户成功
}
/**
* 初始化:扫描类,解析注解,建立路径-方法映射
*/
private static void init(String className) throws ClassNotFoundException {
// 1. 通过类名反射获取Class对象
Class<?> controllerClass = Class.forName(className);
// 2. 判断该类是否有@MyController注解(是控制器才处理)
if (controllerClass.isAnnotationPresent(MyController.class)) {
// 3. 获取该类的所有方法
Method[] methods = controllerClass.getDeclaredMethods();
// 4. 遍历方法,解析@MyRequestMapping注解
for (Method method : methods) {
if (method.isAnnotationPresent(MyRequestMapping.class)) {
// 5. 获取注解实例,读取value(请求路径)
MyRequestMapping requestMapping = method.getAnnotation(MyRequestMapping.class);
String url = requestMapping.value();
// 6. 存入映射表:路径 → 方法
urlMethodMap.put(url, method);
}
}
}
}
/**
* 处理请求:根据路径找到方法,通过反射执行
*/
private static String handleRequest(String url) throws Exception {
// 1. 从映射表中获取对应的方法
Method targetMethod = urlMethodMap.get(url);
if (targetMethod == null) {
return "404:路径不存在";
}
// 2. 创建控制器实例(反射创建对象)
Object controllerInstance = targetMethod.getDeclaringClass().newInstance();
// 3. 反射执行方法,获取返回结果
return (String) targetMethod.invoke(controllerInstance);
}
}
(3)注解的底层结构
编译后的注解本质是:
// 自定义注解@MyAnnotation编译后的等效代码
public interface MyAnnotation extends java.lang.annotation.Annotation {
String value() default ""; // 注解的属性对应接口的抽象方法
}
JVM 会为每个注解生成代理类($Proxy),反射获取注解时,实际返回的是代理类实例,通过该实例调用注解的属性方法,即可获取注解的配置值。
自定义注解的实例:接口限流
步骤 1:定义限流注解
import java.lang.annotation.*; 本质还是使用的反射。
/**
* 自定义限流注解:限制方法的调用频率
*/
// 注解仅作用在方法上
@Target(ElementType.METHOD)
// 保留到运行时,允许反射读取
@Retention(RetentionPolicy.RUNTIME)
// 生成Javadoc时包含该注解
@Documented
public @interface RateLimit {
/**
* 限流阈值(每秒允许调用次数)
*/
int limit() default 10;
/**
* 提示信息
*/
String message() default "请求过于频繁,请稍后再试";
}
步骤 2:使用自定义注解
/**
* 业务类:使用限流注解
*/
public class UserService {
// 使用注解,指定自定义属性值
@RateLimit(limit = 5, message = "用户查询接口限流啦!")
public String queryUserById(String userId) {
return "用户信息:" + userId;
}
// 使用注解,使用默认属性值
@RateLimit
public void updateUser(String userId) {
System.out.println("更新用户:" + userId);
}
}
步骤 3:解析注解(核心)
import java.lang.reflect.Method;
/**
* 注解解析器:读取并处理限流注解
*/
public class RateLimitParser {
public static void checkRateLimit(Object target, String methodName) throws Exception {
// 1. 获取目标类的Class对象
Class<?> clazz = target.getClass();
// 2. 获取目标方法
Method method = clazz.getMethod(methodName);
// 3. 判断方法是否有@RateLimit注解
if (method.isAnnotationPresent(RateLimit.class)) {
// 4. 获取注解实例,读取属性值
RateLimit rateLimit = method.getAnnotation(RateLimit.class);
int limit = rateLimit.limit();
String message = rateLimit.message();
// 5. 模拟限流逻辑(实际项目中可结合Redis实现)
System.out.println("【限流检查】方法:" + methodName);
System.out.println("限流阈值:" + limit + "次/秒");
System.out.println("提示信息:" + message);
// 这里可添加实际的限流判断逻辑(如统计调用次数,超过则抛异常)
} else {
System.out.println("方法" + methodName + "未配置限流注解");
}
}
// 测试解析逻辑
public static void main(String[] args) throws Exception {
UserService userService = new UserService();
// 解析queryUserById方法的注解
checkRateLimit(userService, "queryUserById");
System.out.println("---");
// 解析updateUser方法的注解
checkRateLimit(userService, "updateUser");
}
}
【限流检查】方法:queryUserById
限流阈值:5次/秒
提示信息:用户查询接口限流啦!
---
【限流检查】方法:updateUser
限流阈值:10次/秒
提示信息:请求过于频繁,请稍后再试
总结:
Java 注解是元数据,本质是继承 Annotation 接口的特殊接口,通过元注解(@Retention/@Target)定义生命周期和作用范围。注解本身无逻辑,需通过两种方式生效:①编译期由编译器 / APT 解析(如 @Override);②运行时通过反射 API 读取注解信息,结合业务逻辑实现增强(如 Spring 注解)。核心是 “标记 + 解析”,是框架实现配置简化、逻辑解耦的核心手段。
Java 注解的作用域
指的是注解可以应用在哪些程序元素上。
@Target:主要是类、方法、字段等。
异常
Java 异常
Throwable 有两个重要的子类:Error 和 Exception,它们分别代表了不同类型的异常情况。
-
Error(错误): 表示运行环境的错误,错误是程序无法处理的严重问题,如虚拟机错误、动态链接库失效等。程序不应该尝试捕获这类错误。例如,OutOfMemoryError、StackOverflowError等。 -
Exception(异常): 表示程序本身可以处理的异常情况。异常分为两大类:-
非运行时异常(受检异常,Checked Exception): 这类异常在编译时就必须被捕获或者声明抛出。它们通常是外部错误,如文件不存在 (
FileNotFoundException)、类未找到 (ClassNotFoundException) 等。非运行时异常强制程序员处理这些可能出现的问题,增强了程序的健壮性。 -
运行时异常(非受检异常,Unchecked Exception 或 RuntimeException): 这类异常特指
RuntimeException及其子类。它与Error一起构成了 Java 中的非受检异常家族。运行时异常由程序逻辑错误导致,如空指针访问 (NullPointerException)、数组越界 (ArrayIndexOutOfBoundsException) 等。运行时异常是不需要在编译时强制捕获或声明的。
-

Java 异常的处理
-
try-catch语句块:用于捕获并处理可能抛出的异常。try块中包含可能抛出异常的代码,catch块用于捕获并处理特定类型的异常。可以有多个catch块来处理不同类型的异常。
-
补充:try块中的代码将按顺序执行,如果抛出异常,将在catch块中进行匹配和处理,然后程序将继续执行catch块之后的代码。如果没有匹配的catch块,异常将被传递给上一层调用的方法。
-
-
finally块:用于定义无论是否发生异常都会执行的代码块。通常用于释放资源,确保资源的正确关闭。
-
throw语句:用于手动抛出异常。可以根据需要在代码中使用throw语句主动抛出特定类型的异常。
if (1 > 0) {
throw new RuntimeException("手动抛出异常");
}
-
throws关键字:用于在方法声明中声明可能抛出的异常类型。如果一个方法可能抛出异常,但不想在方法内部进行处理,可以使用throws关键字将异常传递给调用者来处理。
public void methodName() throws ExceptionType {
}
不需要用 throws的情况
抛出运行时异常时,不用 throws ,是因为 Java 不强制声明,写了也只是 “告知调用者可能抛异常”,不写也合法;
抛出编译时异常时,必须用 throws(或内部 try-catch),否则编译报错;
throw 负责 “抛异常”,throws 负责 “声明异常”,二者是 “动作” 和 “声明” 的关系,而非必须绑定。
为什么不要在 finally 语句块中使用 return
当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中(因为为了保证finally必须被执行,所以才暂存),当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
-
当
try里有return时,Java 不会直接返回结果,而是先把return的值临时存到一个 “临时变量” 里; -
接着必须执行
finally块(finally 是 “必执行” 的); -
如果
finally里也有return,就会用finally的返回值覆盖之前暂存的try返回值; -
最终方法返回的是
finally里的 return 值,try的 return 相当于 “白写了”。
异常使用需要注意的地方
-
不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
-
抛出的异常信息一定要有意义。
-
建议抛出更加具体的异常,比如字符串转换为数字格式错误的时候应该抛出
NumberFormatException而不是其父类IllegalArgumentException。 -
避免重复记录日志:如果在捕获异常的地方已经记录了足够的信息(包括异常类型、错误信息和堆栈跟踪等),那么在业务代码中再次抛出这个异常时,就不应该再次记录相同的错误信息。重复记录日志会使得日志文件膨胀,并且可能会掩盖问题的实际原因,使得问题更难以追踪和解决。
上述内容也同步在我的飞书,欢迎访问
https://my.feishu.cn/wiki/QLauws6lWif1pnkhB8IcAvkhncc?from=from_copylink
如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,你们的支持就是我坚持下去的动力!
更多推荐




所有评论(0)