在本节中,我们将深入探讨Spring框架中的AOP(面向切面编程)。无论是通过AspectJ语法,还是配合注解,AOP的核心思想都是通过代理模式动态地将逻辑“织入”到目标方法中。我们将会用一个实际的例子来阐述Spring如何通过CGLIB实现动态代理,以及在此过程中可能出现的问题和解决方案。
1. AOP的基本概念
AOP的本质是代理模式,Spring通过CGLIB在运行期动态创建代理类,将切面逻辑织入目标方法执行的前后。这样,调用方在不知情的情况下,依然能够执行目标方法,同时,运行时自动执行其他的附加逻辑。
1.1 AOP的工作原理
Spring使用CGLIB(Code Generation Library)来为目标对象动态生成代理类。CGLIB会通过字节码技术生成目标类的子类,并在代理类中覆盖目标类的方法,以实现方法增强功能。这些增强功能通常由切面类(Aspect)定义。
2. 一个实际的AOP示例
让我们通过一个实际例子来演示如何使用Spring的AOP功能。
2.1 定义UserService类
首先,我们定义一个UserService类,包含一些简单的方法和一个成员变量。
java@Componentpublic class UserService {public final ZoneId zoneId = ZoneId.systemDefault();public UserService() {System.out.println("UserService(): init...");System.out.println("UserService(): zoneId = " + this.zoneId);}public ZoneId getZoneId() {return zoneId;}public final ZoneId getFinalZoneId() {return zoneId;}}
2.2 定义MailService类
然后,我们定义一个MailService类,在其中注入UserService。
java@Componentpublic class MailService {@AutowiredUserService userService;public String sendMail() {ZoneId zoneId = userService.zoneId;String dt = ZonedDateTime.now(zoneId).toString();return "Hello, it is " + dt;}}
2.3 测试代码
我们编写一个main方法来测试MailService和UserService的功能。
java@Configuration@ComponentScanpublic class AppConfig {public static void main(String[] args) {ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);MailService mailService = context.getBean(MailService.class);System.out.println(mailService.sendMail());}}
输出结果将显示:
scssUserService(): init...UserService(): zoneId = Asia/ShanghaiHello, it is 2020-04-12T10:23:22.917721+08:00[Asia/Shanghai]
2.4 添加AOP支持
接下来,我们为UserService添加一个简单的LoggingAspect,用于在方法执行前打印日志。
java@Aspect@Componentpublic class LoggingAspect {@Before("execution(public * com..*.UserService.*(..))")public void doAccessCheck() {System.err.println("[Before] do access check...");}}
在AppConfig类上添加@EnableAspectJAutoProxy来启用AOP支持。
java@Configuration@ComponentScan@EnableAspectJAutoProxypublic class AppConfig {public static void main(String[] args) {ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);MailService mailService = context.getBean(MailService.class);System.out.println(mailService.sendMail());}}
2.5 运行并遇到异常
重新运行程序时,你会遇到如下异常:
phpException in thread "main" java.lang.NullPointerException: zoneat java.base/java.util.Objects.requireNonNull(Objects.java:246)at java.base/java.time.Clock.system(Clock.java:203)at java.base/java.time.ZonedDateTime.now(ZonedDateTime.java:216)at com.itranswarp.learnjava.service.MailService.sendMail(MailService.java:19)at com.itranswarp.learnjava.AppConfig.main(AppConfig.java:21)
2.6 追踪问题
在MailService.sendMail()方法中,zoneId字段的值为null,这是由于在代理类中没有初始化zoneId字段。我们通过深入分析CGLIB代理的实现来发现,代理类并没有初始化继承自UserService的成员变量,尤其是final字段。
2.7 AOP代理机制解析
Spring通过CGLIB生成代理类时,会生成一个类似UserService$$EnhancerBySpringCGLIB的类,它会继承UserService并覆写所有公共方法。代理类不会初始化继承的成员变量,因此zoneId字段未被初始化,导致NullPointerException。
2.8 修复方案
为了解决这个问题,我们需要避免直接访问字段,而是通过方法访问成员变量。修改MailService中的代码,使用getZoneId()方法来访问zoneId:
java@Componentpublic class MailService {@AutowiredUserService userService;public String sendMail() {ZoneId zoneId = userService.getZoneId(); // 通过方法访问成员变量System.out.println(zoneId);...}}
这样,无论注入的是原始实例还是代理实例,都可以正常访问zoneId。
3. AOP中的final方法问题
如果在UserService中定义了final方法,例如getFinalZoneId(),当我们尝试通过AOP代理调用这个方法时,会遇到NullPointerException。这是因为CGLIB无法代理final方法,代理类无法覆写该方法,因此会导致final字段在代理类中为null。
3.1 避免final方法带来的问题
为了避免此类问题,我们建议不要在可能被AOP代理的类中定义public final方法。这样可以确保代理类能够正确地覆写方法,避免NullPointerException。
4. 小结
-
使用AOP时,应该避免直接访问被代理Bean的字段,尤其是
final字段。 -
代理类通过CGLIB实现时,不会初始化继承的成员变量。
-
遇到
CglibAopProxy相关的日志时,要特别注意检查final方法和字段,避免因未初始化字段而导致NullPointerException。
5. 练习
-
修复启用AOP导致的
NullPointerException。 -
思考如何保护一个Bean避免被AOP代理。
通过本文的学习,你应该已经对Spring AOP的原理有了更加深入的了解,并掌握了如何避免在使用AOP时遇到的常见问题。
168万+

被折叠的 条评论
为什么被折叠?



