SpringBoot 统一功能处理(拦截器、@ControllerAdvice、Spring AOP)

发布于:2025-07-21 ⋅ 阅读:(19) ⋅ 点赞:(0)

拦截器、@ControllerAdvice、Spring AOP 都是 Spring 中实现 “统一功能处理” 的工具,但它们是从不同层面解决问题:

  • 拦截器专注于 HTTP 请求生命周期的增强(如登录验证、日志记录)
  • @ControllerAdvice 专门针对控制器层的统一增强
  • Spring AOP 是最底层、最通用的统一处理方案,覆盖全应用, 更适合对普通 Bean 的方法进行横切增强

三者均通过抽取横切关注点并进行统一处理,既实现了代码复用(避免重复开发),又完成了业务逻辑与通用功能的解耦,其核心优势在于无侵入性增强—— 在程序运行期间,无需修改原有业务代码。最终简化开发流程并显著提升系统的可维护性

拦截器快速入门

拦截器是 Spring MVC 提供的核心功能,用于拦截 Web 请求,允许在请求处理前后执行预定义逻辑,甚至拒绝请求。它是 Spring MVC 框架层面对 AOP 思想的实现,但其底层不依赖 Spring AOP 的动态代理机制,而是基于 DispatcherServlet 的请求处理链和 Java 反射实现

拦截器的使用步骤分为三步:

  1. 定义拦截器(具体做什么):实现 HandlerInterceptor 接口,重写 preHandle(请求处理前)、postHandle(请求处理后视图渲染前)、afterCompletion(整个请求完成后),编写具体拦截逻辑
  2. 注册拦截器(注册到框架中):创建配置类实现 WebMvcConfigurer 接口,重写 addInterceptors 方法,通过 InterceptorRegistry 注册自定义拦截器
  3. 配置拦截规则(拦截哪些请求):在注册时通过 addPathPatterns 定义需要拦截的请求路径,通过 excludePathPatterns 定义需要排除的请求路径(如静态资源、登录页等)

示例

自定义拦截器:实现 HandlerInterceptor 接口,并重写其所有方法

@Slf4j
@Component
public class UserInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("UserInterceptor 目标方法执行前执行..");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("UserInterceptor 目标方法执行后执行");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("UserInterceptor 视图渲染完毕后执行,最后执行");
    }
}
  • preHandle():在目标方法(Controller 方法)执行前被调用。返回 true 表示允许请求继续向下执行(会调用后续拦截器和目标方法);返回 false 则会中断请求流程,后续操作(包括其他拦截器和目标方法)都不会执行
  • postHandle():在目标方法执行完成后、视图渲染之前被调用。此时可以对模型数据或视图进行修改
  • afterCompletion():在整个请求处理完成(视图渲染完毕)后执行,是拦截器中最后执行的方法,通常用于资源清理等操作

注册配置拦截器:实现 WebMvcConfigurer 接口,并重写 addInterceptors 方法

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private UserInterceptor userInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(userInterceptor) //注册自定义拦截器对象
                .addPathPatterns("/**");         //设置拦截器拦截的请求路径(/** 表示拦截所有请求)
    }
}

启动服务,试试访问任意请求,观察后端日志

@Slf4j
@RestController
public class UserController {

    @RequestMapping("/sayHi")
    public String sayHi() {
        log.info("打印日志");
        return "Hi";
    }
}

在这里插入图片描述

可以看到 preHandle 方法执行之后放行了,开始执行目标方法,目标方法执行完后执行 postHandle 和 afterCompletion 方法

我们把拦截器中 preHandle 方法的返回值改为 false,再观察运行结果

在这里插入图片描述

可以看到,拦截器拦截了请求,没有进行响应

拦截器详解

接下来介绍拦截器的使用细节。主要介绍两个部分:

  1. 配置拦截路径
  2. 拦截器执行流程

拦截路径

拦截路径是指:我们定义的这个拦截器对哪些请求生效

我们可以通过 addPathPatterns () 方法指定要拦截哪些请求,也可以通过excludePathPatterns () 指定不拦截哪些请求,excludePathPatterns 的优先级高于 addPathPatterns

除了可以设置 /** 匹配所有请求外,还有一些常见的拦截路径设置:

拦截路径 含义 举例
/* 仅匹配一级路径 能匹配/user,/book,/login,不能匹配/user/login
/** 能匹配任意层级路径 能匹配/user,/user/login,/book/addBook/1
/user/* 匹配/user下的一级子路径 能匹配/user/addUser,不能匹配/user/addUser/1,/user
/user/** 匹配/user下的任意级路径 能匹配/user(包括自身),/user/addBook,/user/addUser/1

上述代码中,我们配置的是 /** ,表示拦截所有的请求。如果有用户登录接口,我们希望可以排除它不被拦截,还有此项目的静态文件 (图片文件,JS 和 CSS 等文件)也得排除,不然页面样式乱了、交互没了,页面没法看了

示例(登录校验)

  1. 定义拦截器

如果能从 Session 中获取用户信息,返回true;否则返回false, 并设置 http 状态码为 401

@Component
public class UserInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute(Constants.SESSION_USER_KEY) != null) {
            // Session 存在且用户标识属性存在,放行
            return true;
        }
        // 未登录,设置响应状态码为 401(未授权),不放行
        response.setStatus(401);
        return false;
    }
}
  1. 注册配置拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private UserInterceptor userInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(userInterceptor)     //注册自定义拦截器对象
                .addPathPatterns("/**")              //拦截所有请求
                .excludePathPatterns("/user/login",  //排除用户登录接口
                "/static/**");                       //排除静态资源
    }

//        // 另一种写法
//        registry.addInterceptor(userInterceptor)
//                .addPathPatterns("/**")
//                .excludePathPatterns("/user/login")
//                .excludePathPatterns("/static/**");
}

也可以改成

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private UserInterceptor userInterceptor;

    private List<String> excludePaths = Arrays.asList(
            "/user/login",
            "/static/**"
    );

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(userInterceptor)     //注册自定义拦截器对象
                .addPathPatterns("/**")              //拦截所有请求
                .excludePathPatterns(excludePaths);  //排除拦截的路径
    }
}

拦截器执行流程

观察第一次有请求进入应用时打印的日志:

在这里插入图片描述

Spring MVC 中请求处理与拦截器的执行流程由 DispatcherServlet 统一调度,整体顺序如下:

  1. 请求发起:前端发送接口调用等请求,首先抵达 DispatcherServlet,由其通过 doDispatch 方法启动调度流程
  2. 拦截器 preHandle:在 Controller 处理请求前,按拦截器注册顺序依次执行 preHandle 方法(前置处理)。该方法用于登录校验、权限检查等前置逻辑,返回 false 则中断后续流程(包括 Controller),直接进入后续清理步骤;返回 true 则继续执行
  3. Controller 业务处理:所有拦截器 preHandle 均通过后,Controller 开始处理具体业务,调用 Service、Mapper 等完成数据操作
  4. 拦截器 postHandle:Controller 处理完成但未返回响应前,按拦截器注册的逆序执行 postHandle 方法(后置处理),可在此修改响应内容(如统一处理 JSON 返回值)或操作 ModelAndView
  5. 响应渲染与返回:根据项目类型(前后端分离 / 服务端渲染),将结果转换为 JSON 或渲染为 HTML,通过 DispatcherServlet 返回给前端
  6. 拦截器 afterCompletion:响应返回后,按拦截器注册的逆序执行 afterCompletion 方法(完成后处理),主要用于资源清理(如记录请求完成的日志、关闭连接等)

多拦截器特殊规则:

先按拦截器注册顺序依次执行各拦截器的 preHandle 方法

  • 若某拦截器 preHandle 返回 false,后续拦截器的 preHandle 及所有后续流程(包括 Controller)均不执行,直接从当前拦截器开始按逆序执行已通过 preHandle 的拦截器的 afterCompletion,最后由 DispatcherServlet 直接返回响应给浏览器
  • 所有拦截器的 preHandle 均返回 true 时,postHandle 和 afterCompletion 会按逆序执行,确保拦截器的 "前置"顺序 与 "后置 / 清理"顺序 形成对称

整个过程由 DispatcherServlet 全程管控,保证拦截器与请求生命周期各阶段的有序衔接

全局控制器增强机制(@ControllerAdvice)

@ControllerAdvice 用于定义全局控制器通知类,可以对所有控制器(@Controller@RestController 标注的类)进行全局增强,统一处理一些横切关注点(如异常处理、数据绑定、全局数据预处理等)

拦截器决定是否让请求进入 Controller,如果请求被拦截(preHandle返回 false),@ControllerAdvice不会生效;如果请求未被拦截(preHandle返回 true),@ControllerAdvice的逻辑会正常执行

@ControllerAdvice 默认对所有控制器生效,若需指定范围,可通过属性限制:

  • basePackages:指定一个或多个包路径,仅对这些包下的控制器生效,例如:
@ControllerAdvice(basePackages = {"org.example.controller.user", "org.example.controller.order"})
  • assignableTypes:指定具体的控制器类,仅对这些类生效,例如:
@ControllerAdvice(assignableTypes = {UserController.class, OrderController.class})
  • annotations:指定特定注解,仅对标注了该注解的控制器生效,例如:
@ControllerAdvice(annotations = {RestController.class})

统一数据返回格式(@ControllerAdvice + ResponseBodyAdvice)

先​​自定义通用返回结果封装类

@Data
public class Result<T> {
    private Integer code;
    private String errMsg;
    private T data;

    public static <T> Result<T> success(T data) {
        Result result = new Result<>();
        result.setCode(200);
        result.setErrMsg("");
        result.setData(data);
        return result;
    }

    //其他错误
    public static <T> Result<T> fail(String errMsg) {
        Result result = new Result();
        result.setCode(-1);
        result.setErrMsg(errMsg);
        result.setData(null);
        return result;
    }
}

再创建标注 @ControllerAdvice 的 ResponseAdvice 类,让其实现 ResponseBodyAdvice 接口

@Slf4j
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        //获取执行的类
        Class<?> declaringClass = returnType.getMethod().getDeclaringClass();
        //获取执行的方法
        Method method = returnType.getMethod();
        log.info("执行的类和方法: {}.{}", declaringClass.getName(), method.getName());
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        //获取执行的类
        Class<?> declaringClass = returnType.getMethod().getDeclaringClass();
        //获取执行的方法
        Method method = returnType.getMethod();
        log.info("执行的类和方法: {}.{}", declaringClass.getName(), method.getName());
        return Result.success(body);
    }
}
  • supports方法用于判断是否需要执行 beforeBodyWrite(返回 true则执行,false则跳过)。可以通过 MethodParameter的信息(如返回值类型、方法名等)过滤不需要处理的返回值
  • beforeBodyWrite的核心职责是处理返回值(如包装成统一格式)

访问http://127.0.0.1:8080/sayHi注意排除一下拦截器的路径,发现发生了内部错误

如果返回类型是String,而统一返回时会把String包装进对象再返回,这会导致类型不匹配,抛出 ClassCastException,其他类型则不会,这是因为String和别的类型用的不是一个处理器,处理过程中将对象强转成String的时候会报错,所以可以把String转成JSON格式

在这里插入图片描述
解决方案:

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    private static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 如果返回结果为String类型,使用SpringBoot内置的Jackson来实现信息的序列化
        if (body instanceof String) {
            return objectMapper.writeValueAsString(Result.success(body));
        }
        return Result.success(body);
    }
}

重新测试,结果返回正常。但看着正常,实际是JSON格式的 String,而不是 JSON 对象,所以拿不到里面的键值对

在这里插入图片描述

如果不想全局修改处理器,可在返回 String 的 Controller 方法中,声明当前接口返回的响应数据格式为 JSON 类型

@Slf4j
@RestController
public class UserController {

    @RequestMapping(value = "/sayHi", produces = "application/json")
    public String sayHi() {
        log.info("打印日志");
        return "Hi";
    }
}

在这里插入图片描述

也可以直接返回包装对象(而非原始 String),从源头避免类型问题

@Slf4j
@RestController
public class UserController {

    @RequestMapping("/sayHi")
    public Result sayHi() {
        log.info("打印日志");
        return Result.success("Hi");
    }
}

在这里插入图片描述

如果返回的结果已经是 Result 类型了,那就直接返回 Result 类型的结果即可

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    private static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 如果返回结果为String类型,使用SpringBoot内置的Jackson来实现信息的序列化
        if (body instanceof String) {
            return objectMapper.writeValueAsString(Result.success(body));
        }
        if (body instanceof Result){
            return body;
        }
        return Result.success(body);
    }
}

优点:

  1. 提升前端数据处理效率
  2. 增强系统可维护性
  3. 推动技术规范标准化
  4. 优化异常排查与问题定位

注意:当 Controller 方法抛出异常时,异常会被 Spring 的异常处理机制(如@ExceptionHandler)捕获并处理,导致异常响应的格式可能与正常响应不一致

​​全局异常处理机制​​(@ControllerAdvice + @ExceptionHandler)

任何 Controller 方法抛出异常​​时,Spring 会匹配 @ExceptionHandler方法​​(基于异常类型),并执行该方法​​,生成最终的错误响应

可以把 @ControllerAdvice 看作一个 ​​"全局警察局"​​,而 @ExceptionHandler 是里面的 ​​"案件处理专员"​​:

  • ​​@ControllerAdvice​​:负责管辖所有 Controller 的异常(相当于警察局的辖区)
  • @ExceptionHandler​​:负责处理特定类型的案件(相当于警察局里的不同部门,如刑事案件组、民事案件组)

异常处理的优先级​​

Spring 会按照以下顺序查找异常处理器:

  1. ​​当前 Controller 内部的 @ExceptionHandler​​(最优先)
  2. @ControllerAdvice中的 @ExceptionHandler​​(全局处理)
  3. Spring 默认的异常处理器​​(如 DefaultHandlerExceptionResolver)

示例

模拟异常(注意排除拦截器路径):

@RestController
public class TestController {

    @RequestMapping("/t1")
    public String t1(){
        return "t1";
    }

    @RequestMapping("/t2")
    public boolean t2(){
        int a = 10/0; //抛出ArithmeticException
        return true;
    }

    @RequestMapping("/t3")
    public Integer t3(){
        String a = null;
        System.out.println(a.length()); //抛出NullPointerException
        return 200;
    }
}

全局处理异常的类名,方法名和返回值可以自定义,重要的是注解

@ResponseBody
@ControllerAdvice
public class ErrorAdvice {

    @ExceptionHandler
    public Result<?> handler(Exception e) {
        return Result.fail(e.getMessage());
    }
}

@ExceptionHandler 方法的返回值默认会被视为 “视图名”(用于页面跳转)。添加 @ResponseBody 后,或者直接使用 @RestControllerAdvice 才能将返回的 Result 对象序列化为 JSON 响应体。而上面的 ResponseBodyAdvice 是 Spring 提供的用于增强响应体处理的接口,其核心方法 beforeBodyWrite 的返回值会直接作为控制器方法的最终响应体,无需额外通过 @ResponseBody 标记

我们还可以针对不同的异常,返回不同的结果:

@RestControllerAdvice
public class ErrorAdvice {

    // 兜底异常处理:当没有更具体的异常处理器时,处理所有 Exception 及其子类(优先级最低)
    @ExceptionHandler
    public Result<?> handler(Exception e) {
        return Result.fail(e.getMessage());
    }

    // 显式指定处理 NullPointerException
    @ExceptionHandler(value = NullPointerException.class)
    public Result<?> handleNullPointerException(NullPointerException e) {
        return Result.fail("空指针异常:" + e.getMessage());
    }

    // 同时处理 ArithmeticException 和 IndexOutOfBoundsException
    @ExceptionHandler(value = {ArithmeticException.class, IndexOutOfBoundsException.class})
    public Result<?> handleMultipleExceptions(Exception e) {
        return Result.fail("数值或索引异常:" + e.getMessage());
    }

    // 未指定 value,Spring 会根据参数推断处理RuntimeException及其子类(优先级低于更具体的异常)
    @ExceptionHandler
    public Result<?> handleRuntimeException(RuntimeException e) {
        return Result.fail("运行时异常:" + e.getMessage());
    }
}

有多个异常通知时,@ExceptionHandler 对异常的匹配遵循 最具体异常优先 的核心原则,具体规则如下:

  • 精确类型匹配

Spring 会优先查找与抛出异常类型完全一致的 @ExceptionHandler 方法

例如:若抛出 NullPointerException,则优先匹配参数为 NullPointerException 的处理器,而非其祖先类的处理器

  • 父类异常匹配

若没有精确匹配的处理器,Spring 会向上搜索异常的继承链,匹配离该异常最近的父类异常对应的处理器

例如:IndexOutOfBoundsException 是 RuntimeException 的子类,RuntimeException 又是 Exception 的子类。若仅定义了@ExceptionHandler(RuntimeException .class) 和 @ExceptionHandler(Exception .class)这两个,则 IndexOutOfBoundsException 会优先匹配 RuntimeException 处理器

  • 全局与局部的优先级

若同一异常在当前 Controller 内部和 @ControllerAdvice 全局类中都有 @ExceptionHandler 方法,则局部处理器(当前 Controller 内)优先于全局处理器

总结:匹配顺序本质是 “从具体到抽象”—— 越精确的异常类型(或越近的父类)对应的处理器优先级越高。这种设计确保了特定异常能被针对性处理,而通用异常(如 Exception)可作为 “兜底” 处理器,避免未捕获的异常导致系统崩溃

全局数据预处理(@ControllerAdvice + @InitBinder)

@InitBinder的作用:在 Controller 方法绑定请求参数前,通过自定义参数转换规则,将请求中的原始参数转换为目标数据类型

@InitBinder的作用范围​​:

  • ​​局部作用域:当 @InitBinder 直接定义在某个 @Controller 类中时,其配置的参数绑定规则仅对当前 Controller 内的请求参数生效
  • 全局作用域:当 @InitBinder 与 @ControllerAdvice 结合时,规则会对所有 @Controller 生效,成为全局共享的参数绑定规则(如日期格式化、自定义类型转换等)

注意:

  • 即使 Controller 方法的参数不添加任何注解,@InitBinder 定义的参数绑定规则仍然生效,只要该参数的类型与 @InitBinder 中配置的转换规则匹配
  • 对 @RequestParam(简单类型)、@ModelAttribute(对象绑定) 生效,对 @RequestBody(JSON 格式参数)不生效
@ControllerAdvice
public class GlobalBinderAdvice {

    @InitBinder
    public void initDateBinder(WebDataBinder binder) {
        // 注册自定义编辑器
        binder.registerCustomEditor(
                // 目标类型: 将请求参数绑定到 Date 类型时生效
                Date.class,  
                new CustomDateEditor(
                        // 日期格式只接受 "yyyy-MM-dd" 
                        new SimpleDateFormat("yyyy-MM-dd"),
                        // 如果请求中没有该日期参数,不会报错(允许为 null)
                        true  
                )
        );
    }
}

当前端传递日期字符串(如 2025-07-17)时,Spring MVC 会自动通过该规则将其转换为 java.util.Date 对象,无需在每个控制器中重复配置。若前端传递的日期格式不符合 yyyy-MM-dd,会抛出 TypeMismatchException

AOP

AOP 是 Spring 框架的第二大核心 (第一大核心是 IoC),是对某一类事情的集中处理

全称是 Aspect Oriented Programming(面向切面编程),这里的切面就是指某一类特定问题,所以 AOP 也可以理解为面向特定方法编程。比如上面的 "登录校验"就是一类特定问题,登录校验拦截器就是对 “登录校验” 这类问题的统一处理

什么是 Spring AOP?

AOP 是一种思想,它的实现方法有很多,Spring AOP 是其中的一种实现方式

Spring AOP 快速入门

我们先通过下面的程序体验下 AOP 的开发,并掌握开发步骤

需求:统计业务接口执行耗时

引入 AOP 依赖,在 pom.xml 文件中添加配置

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

编写AOP程序,记录每个方法的执行时间

我的目录结构如下,切点表达式会涉及到:

在这里插入图片描述

@Slf4j
@Component
@Aspect //表示这是一个切面类
public class TimeRecordAspect {

    //记录方法耗时
    @Around("execution(* org.example.j20250715.*.*(..))")
    public Object timeRecordAspect(ProceedingJoinPoint joinPoint) throws Throwable {
        //1.记录开始时间,在切点方法执行前执行
        long start = System.currentTimeMillis();
        //2.执行目标方法
        Object result = joinPoint.proceed();
        //3.记录耗时,在切点方法执行后执行
        log.info(joinPoint.getSignature()+"耗时: {}ms", System.currentTimeMillis()-start);
        return result;
    }
}

运行程序,访问http://127.0.0.1:8080/sayHi,观察日志

在这里插入图片描述

对程序进行简单的讲解:

  • @Aspect:标识这是一个切面类
  • @Around:声明环绕通知,可在目标方法执行前后添加逻辑。后面的切点表达式用于指定 “切入点”,表示对哪些方法进行增强
  • ProceedingJoinPoint: 封装当前被拦截的目标方法的详细信息,proceed()触发被拦截的目标方法执行

Spring AOP 详解

Spring AOP 核心概念

  • 连接点(JoinPoint):程序运行中​​可以被 AOP 拦截并增强的所有时机 / 位置(比如方法调用、方法执行、异常抛出、字段访问等),在 Spring AOP 中,连接点通常指的是​​目标对象中的方法执行
  • 切点 (Pointcut):从所有可能的连接点中,筛选出需要被切面拦截的具体连接点,通过表达式定义,上面的表达式 execution(* org.example.j20250715.*.*(..)) 就是切点表达式
  • 通知 (Advice):在切点匹配的连接点上具体要执行的增强逻辑,也就是共性功能 (最终体现为一个方法),比如上述程序中记录业务方法的耗时并打印就是通知
  • 切面 (Aspect): 切点 (要拦截的位置) + 通知 (拦截后的具体操作) 的组合,描述了当前 AOP 程序要针对哪些方法,在什么时候执行什么样的操作。切面所在的类,我们一般称为切面类 (被 @Aspect 注解标识的类)

用一句话串联:切面通过切点锁定要拦截的连接点,然后在程序运行到被切点选中的连接点时触发通知逻辑

通知类型

上面讲了什么是通知,接下来学习通知的类型,Spring AOP 的通知类型有以下几种:

  • @Around:环绕通知,它包裹目标方法,能在目标方法执行前后都插入逻辑,甚至可以控制目标方法是否执行(通过 ProceedingJoinPoint.proceed() 控制)
  • @Before:前置通知,在目标方法执行之前执行。常用于准备资源、参数校验等场景
  • @After:后置通知,在目标方法执行之后执行,无论是否发生异常都会执行。类似于 try…finally 中的 finally 块,常用于释放资源、清理操作等
  • @AfterReturning:正常返回后通知,在目标方法正常执行完成并返回结果后执行,如果发生异常则不会执行。可以获取目标方法的返回值(通过 returning 属性指定)
  • @AfterThrowing:异常通知,在目标方法抛出异常后执行,正常执行时不会触发。可以捕获异常信息(通过 throwing 属性指定异常变量)

注意事项:

@Around作为环绕通知,其核心作用是包裹目标方法执行过程。当调用proceed()时,会触发目标方法的执行并返回其结果 —— 这个结果需要被@Around方法捕获后原样返回,否则调用方将无法获取目标方法的真实返回值(可能得到null或异常)

因此,@Around方法的返回值类型必须声明为Object(以兼容所有可能的返回类型),或者严格指定为目标方法的返回类型(适用于切入点明确的场景)。在切面需要处理多种返回类型(如Result、Boolean等)的场景中更应声明为Object,并避免对返回值进行强制类型转换,而是根据实际类型动态处理

JoinPoint 与 ProceedingJoinPoint

  • JoinPoint:仅用于获取连接点信息,没法控制目标方法的执行
  • ProceedingJoinPoint 是 JoinPoint 的子接口,因此它拥有 JoinPoint 的所有方法,并额外提供了 proceed() 方法,用于触发目标方法的执行。这是环绕通知的专属参数,因为环绕通知需要控制目标方法的执行时机(甚至可以决定是否执行)

接下来我们通过代码来加深这几个通知的理解,为了防止干扰,每用新的切面类,最好把之前用的注释掉:

@Slf4j
@Component
@Aspect //表示这是一个切面类
public class AspectDemo {

    //环绕通知
    @Around("execution(* org.example.j20250715.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取目标方法所在类名
        String className = joinPoint.getTarget().getClass().getName();
        // 获取目标方法名
        String methodName = joinPoint.getSignature().getName();
        // 获取方法参数
        Object[] args = joinPoint.getArgs();

        log.info("环绕通知 - 开始执行方法: {}.{},参数: {}", className, methodName, Arrays.toString(args));
        Object result = joinPoint.proceed();
        log.info("环绕通知 - 方法: {}.{} 执行完成,返回结果: {}", className, methodName, result);
        return result;
    }

    //前置通知
    @Before("execution(* org.example.j20250715.*.*(..))")
    public void doBefore(JoinPoint joinPoint) {
        // 获取目标方法所在类名
        String className = joinPoint.getTarget().getClass().getSimpleName();
        // 获取目标方法名
        String methodName = joinPoint.getSignature().getName();
        // 获取方法参数
        Object[] args = joinPoint.getArgs();

        log.info("前置通知 - 准备执行 {}.{} 方法,参数: {}",
                className,
                methodName,
                Arrays.toString(args));
    }

    //后置通知
    @After("execution(* org.example.j20250715.*.*(..))")
    public void doAfter() {
        log.info("执行After方法");
    }

    //正常返回后通知
    @AfterReturning("execution(* org.example.j20250715.*.*(..))")
    public void doAfterReturning() {
        log.info("执行AfterReturning方法");
    }

    //抛出异常后通知
    @AfterThrowing("execution(* org.example.j20250715.*.*(..))")
    public void doAfterThrowing() {
        log.info("执行AfterThrowing方法");
    }
}

直接用 TestController 里的方法测试,运行程序,观察日志:

  • 正常运行的情况(http://127.0.0.1:8080/t1)

在这里插入图片描述

程序正常运行的情况下,@AfterThrowing 标识的通知方法不会执行

在这里插入图片描述

  • 抛出异常的情况 (http://127.0.0.1:8080/t2),如果异常被捕获并不继续抛出就是上面那种正常情况

程序抛出异常的情况下:

  • @AfterReturning 标识的通知方法不会执行,@AfterThrowing 标识的通知方法执行了
  • @Around中 proceed()之后的代码(即 “环绕后” 的逻辑)不会执行

在这里插入图片描述

@PointCut

上述代码存在大量重复的切点表达式,Spring 提供了 @PointCut 注解把公共的切点表达式提取出来,需要用时引用该切点表达式即可

注意:一个切面类可以有多个切点和多个通知,本文就不细举了

上述代码就可以修改为:

@Slf4j
@Component
@Aspect //表示这是一个切面类
public class AspectDemo {

    //定义切点(公共的切点表达式)
    @Pointcut("execution(* org.example.j20250715.*.*(..))")
    private void pt() {}

    //环绕通知
    @Around("pt()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("目标方法执行前");
        Object result = joinPoint.proceed();
        log.info("目标方法执行后");
        return result;
    }

    //前置通知
    @Before("pt()")
    public void doBefore() {
        log.info("执行Before方法");
    }

    //后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执行After方法");
    }

    //正常返回后通知
    @AfterReturning("pt()")
    public void doAfterReturning() {
        log.info("执行AfterReturning方法");
    }

    //抛出异常后通知
    @AfterThrowing("pt()")
    public void doAfterThrowing() {
        log.info("执行AfterThrowing方法");
    }
}

当切点方法(被@Pointcut标注的方法)使用private修饰时,其作用域仅限于当前切面类内部,其他切面类无法访问和引用。若需要让其他切面类复用当前切点定义,需将权限修饰符改为public(或protected,但public更通用),此时该切点可被外部切面类引用

其他切面类引用时,需通过 全限定类名.切点方法名() 的格式指定,例如:

@Slf4j
@Component
@Aspect //表示这是一个切面类
public class AspectDemo2 {
    //前置通知
    @Before("org.example.j20250715.AspectDemo.pt()")
    public void doBefore() {
        log.info("执行 AspectDemo2 的 Before 方法");
    }
}

切面优先级(@Order)

当项目中存在多个切面类,且它们的切点都匹配到同一个目标方法时。当目标方法执行,那么这几个通知方法的执行顺序是什么样的呢?

我们还是通过程序来求证:

定义多个切面类,之前的切面类注释掉不要忘了

@Slf4j
@Component
@Aspect //表示这是一个切面类
public class AspectDemoOrder {
    @Pointcut("execution(* org.example.j20250715.*.*(..))")
    private void pt(){}

    //前置通知
    @Before("pt()")
    public void doBefore() {
        log.info("执行 AspectDemoOrder -> Before 方法");
    }

    //后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执行 AspectDemoOrder -> After 方法");
    }
}
@Slf4j
@Component
@Aspect //表示这是一个切面类
public class AspectDemoOrder2 {
    @Pointcut("execution(* org.example.j20250715.*.*(..))")
    private void pt(){}

    //前置通知
    @Before("pt()")
    public void doBefore() {
        log.info("执行 AspectDemoOrder2 -> Before 方法");
    }

    //后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执行 AspectDemoOrder2 -> After 方法");
    }
}
@Slf4j
@Component
@Aspect //表示这是一个切面类
public class AspectDemoOrder3 {
    @Pointcut("execution(* org.example.j20250715.*.*(..))")
    private void pt(){}

    //前置通知
    @Before("pt()")
    public void doBefore() {
        log.info("执行 AspectDemoOrder3 -> Before 方法");
    }

    //后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执行 AspectDemoOrder3 -> After 方法");
    }
}

运行程序,访问http://127.0.0.1:8080/sayHi,观察日志:

在这里插入图片描述

可以看出当存在多个切面类且未显式指定优先级时,默认按切面类名的 ASCII 码顺序决定通知执行顺序,具体表现为:

  • @Before 通知: ASCII 码靠前的先执行
  • @After 通知:ASCII 码靠前的后执行

这种顺序呈现「前置顺排,后置逆排」的特点

但这种方式不方便管理,而且我们的类名更多还是具备一定含义的,Spring 为我们提供了@Order 注解来控制这些切面通知的执行顺序

使用方式如下:

@Slf4j
@Order(2)
@Component
@Aspect //表示这是一个切面类
public class AspectDemoOrder {
    //....代码省略
}
@Slf4j
@Order(1)
@Component
@Aspect //表示这是一个切面类
public class AspectDemoOrder2 {
    //....代码省略
}
@Slf4j
@Order(3)
@Component
@Aspect //表示这是一个切面类
public class AspectDemoOrder3 {
    //....代码省略
}

通过上述程序的运行结果得出 ,@Order(n) 中 n 的数值越小,切面优先级越高。对于匹配同一目标方法的多个切面,优先级高的切面会先执行目标方法前的逻辑(如@Before、@Around的前置代码),而在目标方法执行完成后,会后执行目标方法后的逻辑(如@After、@Around的后置代码),整体遵循「先进后出」的栈式逻辑

切点表达式

Spring AOP 中,切点表达式最常用的两种核心类型是:

  • execution (…):根据方法签名匹配目标方法
  • @annotation(…):通过方法上是否标注特定注解来匹配

execution 表达式

execution 是最常用的切点表达式,语法为:

execution([访问修饰符] 返回类型 [包名.类名.]方法名(参数列表) [throws 异常类型])

支持的通配符表达:

  • *(单元素通配符) 仅匹配单个任意元素 (返回类型、单个包名、类名、方法名或单个参数类型),不能跨层级
  • ..(层级通配符) 匹配​​零个或多个连续的任意元素或层级​,可以标识此包以及此包下的所有子包,或任意个任意类型的参数

语法各部分说明:

  • 访问修饰符(可选):如 public、private 等,省略时匹配任意修饰符。例:execution(public * *(..)) 匹配所有 public 修饰的方法

  • 返回类型(必填):指定方法返回值类型

  • 包名.类名(可选):限定方法所在的类或包,可以不指定包名和类名,此时表达式会匹配所有类中符合条件的方法(不限包和类的范围)。例:execution(* ..TestController.*(..))匹配所有包下面的TestController的所有方法

  • 方法名(必填):指定方法名

  • 参数列表(必填):指定参数列表,例:execution(* ..update*(Long, ..)) 匹配所有方法名以 update 开头,第一个参数为 Long ,后续任意参数的方法

  • throws 异常类型(可选):指定方法声明抛出的异常,省略时匹配任意异常(包括不抛异常的方法)

@annotation

execution表达式更适用有规则的,如果我们要匹配多个无规则的方法,我们可以用「自定义注解 + @annotation 切点表达式」

实现步骤:

  1. 编写自定义注解
  2. 使用 @annotation 表达式来描述切点
  3. 在目标方法(连接点)上添加自定义注解

自定义一个注解类 @MyAspect ,和创建 Class 文件一样的流程,选择 Annotation 就可以了

在这里插入图片描述

@Target(ElementType.METHOD) // 仅作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,确保AOP能识别
public @interface MyAspect {
}

@Target@Retention 是 Java 中定义注解时的两个核心元注解,它们分别控制注解的适用范围和生命周期,具体说明如下:

  • @Target 用于指定当前注解可以标注在哪些程序元素上(如类、方法、参数等),若没有则默认注解可用于所有元素,但实际开发中通常会精确限定,避免滥用,常用取值:
    • ElementType.TYPE:可标注在类、接口、注解类或枚举类上
    • ElementType.METHOD:可标注在方法上
    • ElementType.PARAMETER:可标注在方法参数上
    • ElementType.FIELD:可标注在字段(成员变量)上
    • ElementType.TYPE_USE:可标注在任意类型声明处(如变量类型、返回值类型等,是 Java 8 新增的灵活选项)

支持多值组合,需用 {} 包裹,例如:

@Target({ElementType.METHOD, ElementType.TYPE})
public @interface MyAnnotation {}
  • @Retention 用于指定注解在代码的哪个阶段保留(是否被编译器或 JVM 保留),决定了程序运行时能否获取到注解信息,如果自定义注解不添加 @Retention 元注解,Java 会采用默认的生命周期规则,即默认使用 RetentionPolicy.CLASS。取值有三:
    • RetentionPolicy.SOURCE: 源代码注解。表示注解仅存在于源代码中,编译成字节码后会被丢弃。这意味着仅对编译器可见。比如:lombok 提供的注解 @Data, @Slf4j
    • RetentionPolicy.CLASS:编译时注解。表示注解存在于源代码和字节码中,但在运行时会被丢弃。所以在实际运行时无法获取
    • RetentionPolicy.RUNTIME:运行时注解。表示注解存在于源代码,字节码和运行时中。这意味着实际运行时可以通过反射获取到该注解的信息。通常用于一些需要在运行时处理的注解,如 Spring 的 @Controller @ResponseBody

使用 @annotation 切点表达式定义切点,匹配所有标注了 @MyAspect 注解的方法,切面类代码如下:

@Slf4j
@Component
@Aspect //表示这是一个切面类
public class MyAspectDemo {
    // 前置通知
    @Before("@annotation(org.example.j20250715.MyAspect)")
    public void before(){
        log.info("MyAspect -> before ...");
    }

    // 后置通知
    @After("@annotation(org.example.j20250715.MyAspect)")
    public void after(){
        log.info("MyAspect -> after ...");
    }
}

在TestController中的t1()上添加自定义注解 @MyAspect

    @MyAspect
    @RequestMapping("/t1")
    public String t1(){
        return "t1";
    }

运行程序,测试接口http://127.0.0.1:8080/t1,观察日志看到,切面通知被执行了

Spring AOP 原理

上面我们学习了 Spring AOP 的应用,接下来我们来学习 Spring AOP 的原理,也就是 Spring 是如何实现 AOP 的

Spring AOP 的实现基础是动态代理,主要通过两种方式实现:JDK 动态代理和 CGLIB 动态代理,具体使用哪种方式取决于项目配置和被代理的对象特性

这两种动态代理也是 Java 中常见的动态代理实现方式,各自具有不同的特点:

  • JDK 动态代理:是 Java 原生提供的动态代理实现,它的使用有一定限制,只能对实现了接口的类进行代理
  • CGLIB 动态代理:作为一种第三方实现的动态代理方式,它的适用范围更广泛,既可以代理实现了接口的类,也可以直接代理没有实现接口的类

Spring AOP 正是基于这两种动态代理方式构建,根据被代理对象是否实现接口等情况,选择合适的代理方式来完成 AOP 的功能

代理模式

定义:为其他对象提供一种代理以控制对这个对象的访问。它的作用就是通过提供一个代理类,让我们在调用被代理对象的目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强

在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在调用方和目标对象之间起到中介的作用

代理模式的主要角色:

  • Subject(不一定有):业务接口类,定义代理类和目标类共同遵循的接口或抽象类,规定了核心业务方法
  • RealSubject:业务实现类,也就是目标对象(被代理对象),负责实现具体的业务逻辑
  • Proxy:代理类,持有目标对象的引用,在调用目标方法前后可以添加额外逻辑,最终会调用目标对象的方法

比如房屋租赁:

  • Subject:提前定义的租房服务规范,交给中介代理
  • RealSubject:房东
  • Proxy:中介

通过中介(代理),客户无需直接接触房东(目标对象),却能完成租房流程(目标方法),同时中介还能提供附加价值

根据代理的创建时期,代理模式分为静态代理和动态代理

  • 静态代理:代理类在编译期就已确定(由程序员编写或工具生成),并且会生成对应的.class文件
  • 动态代理:代理类在程序运行时动态生成,无需提前编写代理类代码,动态代理正是 Spring AOP 实现的基础,通过动态生成代理对象,实现了对目标方法的增强(如日志记录、事务管理等),而无需修改目标类本身的代码

静态代理

我们通过代码来加深理解,以房屋租赁为例:

  1. 定义接口 (定义房东要做的事情,也是中介需要做的事情)
public interface HouseSubject {
    void rentHouse();
}
  1. 实现接口 (房东出租房子)
public class RealHouseSubject implements HouseSubject{

    @Override
    public void rentHouse() {
        System.out.println("我是房东,我出租房子");
    }
}
  1. 代理 (中介帮房东出租房子)
public class HouseProxy implements HouseSubject{
    //将被代理对象声明为成员变量
    private HouseSubject houseSubject;
    
    public HouseProxy(HouseSubject houseSubject) {
        this.houseSubject = houseSubject;
    }

    @Override
    public void rentHouse() {
        //开始代理
        System.out.println("我是中介,开始代理");
        //代理房东出租房子
        houseSubject.rentHouse();
        //代理结束
        System.out.println("我是中介,代理结束");
    }
}
  1. 使用
public class StaticMain {
    public static void main(String[] args) {
        //创建代理类
        HouseProxy proxy = new HouseProxy(new RealHouseSubject());
        //通过代理类访问目标方法
        proxy.rentHouse();
    }
}

运行结果:

在这里插入图片描述

上面这个代理实现方式就是静态代理 (仿佛啥也没干)

从上述程序可以看出,虽然静态代理也完成了对目标对象的代理,但是由于代码都写死了,对目标对象的每个方法的增强都是手动完成的,非常不灵活,所以日常开发几乎看不到静态代理的场景

接下来中介又新增了代理房屋出售的业务

  1. 接口定义修改
public interface HouseSubject {
    void rentHouse();
    void saleHouse();
}
  1. 接口实现修改
public class RealHouseSubject implements HouseSubject{
    @Override
    public void rentHouse() {
        System.out.println("我是房东,我出租房子");
    }

    @Override
    public void saleHouse() {
        System.out.println("我是房东,我出售房子");
    }
}
  1. 代理类修改
public class HouseProxy implements HouseSubject{
    //将被代理对象声明为成员变量
    private HouseSubject houseSubject;

    public HouseProxy(HouseSubject houseSubject) {
        this.houseSubject = houseSubject;
    }

    @Override
    public void rentHouse() {
        //开始代理
        System.out.println("我是中介,开始代理");
        //代理房东出租房子
        houseSubject.rentHouse();
        //代理结束
        System.out.println("我是中介,代理结束");
    }

    @Override
    public void saleHouse() {
        //开始代理
        System.out.println("我是中介,开始代理");
        //代理房东出售房子
        houseSubject.saleHouse();
        //代理结束
        System.out.println("我是中介,代理结束");
    }
}

可以看出,当我们修改接口(Subject)时,业务实现类(RealSubject)和代理类(Proxy)往往都需要随之修改;同样地,新增接口(Subject)时,也必须对应新增业务实现类(RealSubject)和代理类(Proxy)

不难发现,所有代理类的核心流程其实是相似的 —— 都是在调用目标方法前后添加增强逻辑,最终再执行目标方法。既然如此,是否存在一种方式,能让一个代理机制自动适配不同的接口和目标对象,而无需为每个接口单独编写代理类呢?

这正是动态代理技术要解决的问题:它能在程序运行时根据接口动态生成代理对象,无论接口如何变化或新增,都无需手动编写对应的代理类,从而极大地提升了代码的灵活性和可维护性

动态代理

相比于静态代理,动态代理更加灵活

我们不需要针对每个目标对象都单独创建一个代理对象,而是把这个创建代理对象的工作推迟到程序运行时由 JVM 来实现。也就是说动态代理在程序运行时,根据需要动态创建生成

静态代理就像 “专属中介”—— 比如专门对接 “租房” 业务的中介、专门对接 “卖房” 业务的中介,每新增一种业务(比如 “房屋托管”),就必须再培训一个对应的新中介,成本高且不够灵活

而动态代理更像 “全能中介”—— 不需要提前知道会有哪些业务(租房、卖房、托管等),来了任何业务都能当场根据需求 “动态适配”,用同一套逻辑框架处理不同的业务场景。就像 JVM 在运行时根据接口动态生成代理对象,无需提前为每个目标对象编写代理类,完美解决了静态代理中 “一对一绑定” 的局限性

JDK 动态代理类实现步骤

  1. 定义接口及其实现类 (静态代理中的 HouseSubject 和 RealHouseSubject)
  2. 创建动态代理类实现 InvocationHandler 接口 并重写 invoke 方法,在 invoke 方法中我们会调用目标方法 (被代理类的方法) 并自定义一些处理逻辑,当代理对象的方法被调用时,会自动触发invoke方法的执行
  3. 通过 Proxy.newProxyInstance 方法创建代理对象,该方法需要三个参数:类加载器、目标对象实现的接口数组、自定义的 InvocationHandler 实例

定义 JDK 动态代理类并实现 InvocationHandler 接口

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class JDKInvocationHandler implements InvocationHandler {
    //目标对象(被代理的原始对象)
    private Object target;

    public JDKInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 代理增强:方法执行前的逻辑(前置增强)
        System.out.println("我是中介,开始代理");
        // 通过反射调用目标对象的实际方法
        Object retVal = method.invoke(target, args);
        // 代理增强:方法执行后的逻辑(后置增强)
        System.out.println("我是中介,代理结束");
        // 返回目标方法的执行结果
        return retVal;
    }
}

创建一个代理对象并使用

import java.lang.reflect.Proxy;

public class DynamicMain {
    public static void main(String[] args) {
        HouseSubject target=  new RealHouseSubject();
        // 创建代理对象
        HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),  // 目标对象的类加载器
                new Class[]{HouseSubject.class},     // 代理需要实现的接口
                new JDKInvocationHandler(target)     // 绑定调用处理器,传入目标对象
        );

        // 调用代理对象的方法
        // 此时并不会直接调用目标对象的方法,而是先触发处理器的invoke方法
        // 在invoke方法中会执行增强逻辑,并通过反射调用目标对象的对应方法
        proxy.rentHouse();
    }
}

在这里插入图片描述

CGLIB 动态代理

JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。而有些场景下,我们的业务代码是直接实现的,并没有接口定义。为了解决这个问题,我们可以用 CGLIB 动态代理机制来解决

CGLIB (Code Generation Library) 是一个基于 ASM 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成,并通过继承方式实现代理

  • Spring 中的 AOP,如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理
  • Spring Boot 中的 AOP,2.0 之前和 Spring 一样;2.0 及之后为了解决使用 JDK 动态代理可能导致的类型转化异常而默认使用 CGLIB。如果需要默认使用 JDK 动态代理可以通过配置项spring.aop.proxy-target-class=false来进行修改

CGLIB 动态代理类实现步骤

  1. 定义一个类 (被代理类)
  2. 实现 MethodInterceptor 并重写 intercept 方法,intercept 用于增强目标方法,和 JDK 动态代理中的 invoke 方法类似
  3. 通过 Enhancer 类的 create () 创建代理类

接下来看具体实现:

和 JDK 动态代理不同,CGLIB 属于一个开源项目,如果要使用它的话,需要手动添加相关依赖

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

实现 MethodInterceptor(方法拦截器)接口

import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CGLIBInterceptor implements MethodInterceptor {

    private Object target;

    public CGLIBInterceptor(Object target){
        this.target = target;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // 代理增强:方法执行前的逻辑(前置增强)
        System.out.println("我是中介,开始代理");
        // 通过CGLIB的MethodProxy直接调用目标方法(比反射更快)
        Object retVal = methodProxy.invoke(target, objects);
        // 代理增强:方法执行后的逻辑(后置增强)
        System.out.println("我是中介,代理结束");
        // 返回目标方法的执行结果
        return retVal;
    }
}

创建代理类并使用

import org.springframework.cglib.proxy.Enhancer;

// 动态代理测试类,使用CGLIB创建代理对象并调用方法
public class DynamicMain {
    public static void main(String[] args) {
        // 1. 创建目标对象(被代理的真实对象)
        HouseSubject target = new RealHouseSubject();

        // 2. 使用CGLIB的Enhancer创建代理对象
        //    - 第一个参数:目标对象的Class类型
        //    - 第二个参数:MethodInterceptor拦截器实例,用于增强目标方法
        HouseSubject proxy = (HouseSubject) Enhancer.create(
                target.getClass(),
                new CGLIBInterceptor(target)
        );

        // 3. 通过代理对象调用方法,此时会触发拦截器中的增强逻辑
        proxy.rentHouse();
    }
}

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到