spring boot 全局异常处理捕获 cglib 代理异常,核心在于确保 @controlleradvice 或 @restcontrolleradvice 能够拦截到被代理对象抛出的异常。由于 cglib 生成的代理类会继承目标类并覆盖其方法,异常通常能正常向上抛出并被 spring aop 的异常通知机制捕获。如果未捕获到,通常是因 @enableaspectjautoproxy 配置缺失、代理模式强制使用 jdk 动态代理导致接口实现问题,或者异常在代理链中被提前消费。通过检查 spring.aop.proxy-target-class 属性并确保切面正确声明,即可解决。

为什么 CGLIB 代理下异常有时不被 @ControllerAdvice 捕获
CGLIB 代理基于继承实现,代理类是目标类的子类。当目标方法抛出异常时,代理方法会将异常重新抛出。Spring MVC 的异常处理机制依赖于 HandlerExceptionResolver 链,其中 ExceptionHandlerExceptionResolver 负责处理 @ControllerAdvice 定义的异常。只要异常没有被业务代码中的 try-catch 块吞掉,且未被其他切面(如日志切面)错误地转换或捕获,它就能到达 DispatcherServlet 的异常处理流程。常见问题在于,如果自定义切面在 @AfterThrowing 中直接返回了结果而未重抛异常,或者 @ControllerAdvice 所在的包扫描路径未覆盖控制器和切面,就会导致捕获失败。

package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true) // 强制使用 CGLIB
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@RestController
class TestController {
@GetMapping("/test")
public String test() {
throw new RuntimeException("CGLIB Proxy Exception");
}
}
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<string> handleRuntimeException(RuntimeException ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Caught: " + ex.getMessage());
}
}</string>
运行上述程序,访问 /test 端点,将返回 HTTP 500 状态码及 Body Caught: CGLIB Proxy Exception。这证明在 proxyTargetClass = true 开启 CGLIB 代理后,@ControllerAdvice 能正常捕获从代理方法抛出的异常。关键在于 @EnableAspectJAutoProxy 必须存在以激活 AOP 支持,且 proxyTargetClass = true 确保即使没有接口也使用 CGLIB。
JDK 动态代理与 CGLIB 在异常传播上的差异
JDK 动态代理仅对接口生效,而 CGLIB 对类生效。在异常传播上,两者行为一致,因为最终都是调用目标方法并捕获其返回值或异常。区别在于,如果使用 JDK 动态代理,但目标类未实现任何接口,Spring 会自动降级为 CGLIB。若强制使用 JDK 动态代理(通过配置 proxy-target-class=false 且类有接口),异常同样能被 @ControllerAdvice 捕获。然而,如果目标类实现了多个接口,且异常发生在某个非公共方法的内部逻辑中,JDK 代理可能无法正确代理该方法(如果该方法未在接口中声明),从而导致异常行为不一致。CGLIB 则能代理所有非 final 方法。因此,在处理复杂继承结构下的异常时,CGLIB 更为可靠。以下代码演示了在无接口情况下,强制使用 JDK 代理会失败,从而迫使 Spring 回退到 CGLIB,此时异常捕获依然有效。
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
@SpringBootApplication
@Configuration
public class NoInterfaceApp {
public static void main(String[] args) {
SpringApplication.run(NoInterfaceApp.class, args);
}
}
@Component
class ServiceWithoutInterface {
public void doSomething() {
throw new IllegalStateException("Service Error");
}
}
@RestController
class ProxyTestController {
private final ServiceWithoutInterface service;
public ProxyTestController(ServiceWithoutInterface service) {
this.service = service;
}
@GetMapping("/no-interface")
public String test() {
service.doSomething();
return "OK";
}
}
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<string> handleIllegalState(IllegalStateException ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Caught: " + ex.getMessage());
}
}</string>
在此示例中,ServiceWithoutInterface 没有实现接口。Spring Boot 默认会使用 CGLIB 为其创建代理。访问 /no-interface 将触发异常,并被 GlobalExceptionHandler 捕获,返回 Caught: Service Error。这验证了即使在无接口的场景下,Spring 自动切换至 CGLIB 后,异常处理机制依然正常工作。开发者无需手动干预代理类型,只需确保 AOP 已启用。
如何调试确认异常是否来自 CGLIB 代理层
当怀疑异常未被正确捕获时,可以通过检查堆栈跟踪信息来确认异常来源。CGLIB 代理生成的类名通常包含 $$ 或 CGLIB 字样。在 @ControllerAdvice 中打印异常的堆栈信息,可以清晰看到方法调用链是否经过代理类。如果堆栈显示方法调用直接指向原始类而非代理类,说明代理未生效,可能是 Bean 注入方式不当(如通过 this 调用)或 proxy-target-class 配置错误。此外,可以使用 AopUtils.isAopProxy() 方法在运行时检查 Bean 是否为代理对象。以下代码展示了如何在异常处理器中诊断代理状态。
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DebugProxyApp {
public static void main(String[] args) {
SpringApplication.run(DebugProxyApp.class, args);
}
}
@Service
class DebugService {
public void fail() {
System.out.println("Is Proxy: " + AopUtils.isAopProxy(this));
throw new RuntimeException("Debug Fail");
}
}
@RestController
class DebugController {
@Autowired
private DebugService debugService;
@GetMapping("/debug")
public String debug() {
debugService.fail();
return "OK";
}
}
@RestControllerAdvice
class DebugExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<string> handle(RuntimeException ex) {
System.out.println("Stack Trace:");
ex.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Caught");
}
}</string>
启动应用并访问 /debug,控制台将输出 Is Proxy: true,表明 Bean 已被代理。同时,堆栈跟踪中将显示 DebugService$$EnhancerBySpringCGLIB$$... 类名,确认异常源自 CGLIB 代理层。如果 Is Proxy: false,则需检查 @Service 是否被正确扫描,以及是否在类内部调用了 fail() 方法(内部调用不会触发代理)。通过这种方式,可以准确定位异常处理失效的根本原因。











