【Java教程】Day22-13 Spring框架:使用AOP——避坑指南

在本节中,我们将深入探讨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 {    @Autowired    UserService userService;    public String sendMail() {        ZoneId zoneId = userService.zoneId;        String dt = ZonedDateTime.now(zoneId).toString();        return "Hello, it is " + dt;    }}

 

2.3 测试代码

我们编写一个main方法来测试MailServiceUserService的功能。


 
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: zone    at 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 {    @Autowired    UserService 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时遇到的常见问题。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值