目录

注解

Java 注解的原理

注解解析的底层实现

Java 注解的作用域

异常

Java 异常

Java 异常的处理

不需要用 throws的情况

为什么不要在 finally 语句块中使用 return

异常使用需要注意的地方


注解

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/FieldgetAnnotation()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 有两个重要的子类:ErrorException,它们分别代表了不同类型的异常情况。

  1. Error (错误): 表示运行环境的错误,错误是程序无法处理的严重问题,如虚拟机错误、动态链接库失效等。程序不应该尝试捕获这类错误。例如,OutOfMemoryErrorStackOverflowError 等。

  2. Exception (异常): 表示程序本身可以处理的异常情况。异常分为两大类:

    1. 非运行时异常(受检异常,Checked Exception): 这类异常在编译时就必须被捕获或者声明抛出。它们通常是外部错误,如文件不存在 (FileNotFoundException)、类未找到 (ClassNotFoundException) 等。非运行时异常强制程序员处理这些可能出现的问题,增强了程序的健壮性。

    2. 运行时异常(非受检异常,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 返回值。

  1. try 里有 return 时,Java 不会直接返回结果,而是先把 return 的值临时存到一个 “临时变量” 里;

  2. 接着必须执行 finally 块(finally 是 “必执行” 的);

  3. 如果 finally 里也有 return,就会用 finally 的返回值覆盖之前暂存的 try 返回值;

  4. 最终方法返回的是 finally 里的 return 值,try 的 return 相当于 “白写了”。

异常使用需要注意的地方

  • 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。

  • 抛出的异常信息一定要有意义。

  • 建议抛出更加具体的异常,比如字符串转换为数字格式错误的时候应该抛出NumberFormatException而不是其父类IllegalArgumentException

  • 避免重复记录日志:如果在捕获异常的地方已经记录了足够的信息(包括异常类型、错误信息和堆栈跟踪等),那么在业务代码中再次抛出这个异常时,就不应该再次记录相同的错误信息。重复记录日志会使得日志文件膨胀,并且可能会掩盖问题的实际原因,使得问题更难以追踪和解决。


上述内容也同步在我的飞书,欢迎访问

https://my.feishu.cn/wiki/QLauws6lWif1pnkhB8IcAvkhncc?from=from_copylink

如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,你们的支持就是我坚持下去的动力!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐