阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

装配AOP

29次阅读
没有评论

共计 3883 个字符,预计需要花费 10 分钟才能阅读完成。

在 AOP 编程中,我们经常会遇到下面的概念:

  • Aspect:切面,即一个横跨多个核心逻辑的功能,或者称之为系统关注点;
  • Joinpoint:连接点,即定义在应用程序流程的何处插入切面的执行;
  • Pointcut:切入点,即一组连接点的集合;
  • Advice:增强,指特定连接点上执行的动作;
  • Introduction:引介,指为一个已有的 Java 对象动态地增加新的接口;
  • Weaving:织入,指将切面整合到程序的执行流程中;
  • Interceptor:拦截器,是一种实现增强的方式;
  • Target Object:目标对象,即真正执行业务的核心逻辑对象;
  • AOP Proxy:AOP 代理,是客户端持有的增强后的对象引用。

看完上述术语,是不是感觉对 AOP 有了进一步的困惑?其实,我们不用关心 AOP 创造的“术语”,只需要理解 AOP 本质上只是一种代理模式的实现方式,在 Spring 的容器中实现 AOP 特别方便。

我们以 UserServiceMailService为例,这两个属于核心业务逻辑,现在,我们准备给 UserService 的每个业务方法执行前添加日志,给 MailService 的每个业务方法执行前后添加日志,在 Spring 中,需要以下步骤:

首先,我们通过 Maven 引入 Spring 对 AOP 的支持:

  • org.springframework:spring-aspects:6.0.0

上述依赖会自动引入 AspectJ,使用 AspectJ 实现 AOP 比较方便,因为它的定义比较简单。

然后,我们定义一个LoggingAspect

@Aspect
@Component
public class LoggingAspect {// 在执行 UserService 的每个方法前执行:
    @Before("execution(public * com.itranswarp.learnjava.service.UserService.*(..))")
    public void doAccessCheck() {System.err.println("[Before] do access check...");
    }

    // 在执行 MailService 的每个方法前后执行:
    @Around("execution(public * com.itranswarp.learnjava.service.MailService.*(..))")
    public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {System.err.println("[Around] start" + pjp.getSignature());
        Object retVal = pjp.proceed();
        System.err.println("[Around] done" + pjp.getSignature());
        return retVal;
    }
}

观察 doAccessCheck() 方法,我们定义了一个 @Before 注解,后面的字符串是告诉 AspectJ 应该在何处执行该方法,这里写的意思是:执行 UserService 的每个 public 方法前执行 doAccessCheck() 代码。

再观察 doLogging() 方法,我们定义了一个 @Around 注解,它和 @Before 不同,@Around可以决定是否执行目标方法,因此,我们在 doLogging() 内部先打印日志,再调用方法,最后打印日志后返回结果。

LoggingAspect 类的声明处,除了用 @Component 表示它本身也是一个 Bean 外,我们再加上 @Aspect 注解,表示它的 @Before 标注的方法需要注入到 UserService 的每个 public 方法执行前,@Around标注的方法需要注入到 MailService 的每个 public 方法执行前后。

紧接着,我们需要给 @Configuration 类加上一个 @EnableAspectJAutoProxy 注解:

@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class AppConfig {...}

Spring 的 IoC 容器看到这个注解,就会自动查找带有 @Aspect 的 Bean,然后根据每个方法的 @Before@Around 等注解把 AOP 注入到特定的 Bean 中。执行代码,我们可以看到以下输出:

[Before] do access check...
[Around] start void com.itranswarp.learnjava.service.MailService.sendRegistrationMail(User)
Welcome, test!
[Around] done void com.itranswarp.learnjava.service.MailService.sendRegistrationMail(User)
[Before] do access check...
[Around] start void com.itranswarp.learnjava.service.MailService.sendLoginMail(User)
Hi, Bob! You are logged in at 2020-02-14T23:13:52.167996+08:00[Asia/Shanghai]
[Around] done void com.itranswarp.learnjava.service.MailService.sendLoginMail(User)

这说明执行业务逻辑前后,确实执行了我们定义的 Aspect(即 LoggingAspect 的方法)。

有些童鞋会问,LoggingAspect定义的方法,是如何注入到其他 Bean 的呢?

其实 AOP 的原理非常简单。我们以 LoggingAspect.doAccessCheck() 为例,要把它注入到 UserService 的每个 public 方法中,最简单的方法是编写一个子类,并持有原始实例的引用:

public UserServiceAopProxy extends UserService {private UserService target;
    private LoggingAspect aspect;

    public UserServiceAopProxy(UserService target, LoggingAspect aspect) {this.target = target;
        this.aspect = aspect;
    }

    public User login(String email, String password) {// 先执行 Aspect 的代码:
        aspect.doAccessCheck();
        // 再执行 UserService 的逻辑:
        return target.login(email, password);
    }

    public User register(String email, String password, String name) {aspect.doAccessCheck();
        return target.register(email, password, name);
    }

    ...
}

这些都是 Spring 容器启动时为我们自动创建的注入了 Aspect 的子类,它取代了原始的 UserService(原始的UserService 实例作为内部变量隐藏在 UserServiceAopProxy 中)。如果我们打印从 Spring 容器获取的 UserService 实例类型,它类似UserService$$EnhancerBySpringCGLIB$$1f44e01c,实际上是 Spring 使用 CGLIB 动态创建的子类,但对于调用方来说,感觉不到任何区别。

注意

Spring 对接口类型使用 JDK 动态代理,对普通类使用 CGLIB 创建子类。如果一个 Bean 的 class 是 final,Spring 将无法为其创建子类。

可见,虽然 Spring 容器内部实现 AOP 的逻辑比较复杂(需要使用 AspectJ 解析注解,并通过 CGLIB 实现代理类),但我们使用 AOP 非常简单,一共需要三步:

  1. 定义执行方法,并在方法上通过 AspectJ 的注解告诉 Spring 应该在何处调用此方法;
  2. 标记 @Component@Aspect
  3. @Configuration 类上标注@EnableAspectJAutoProxy

至于 AspectJ 的注入语法则比较复杂,请参考 Spring 文档。

Spring 也提供其他方法来装配 AOP,但都没有使用 AspectJ 注解的方式来得简洁明了,所以我们不再作介绍。

拦截器类型

顾名思义,拦截器有以下类型:

  • @Before:这种拦截器先执行拦截代码,再执行目标代码。如果拦截器抛异常,那么目标代码就不执行了;
  • @After:这种拦截器先执行目标代码,再执行拦截器代码。无论目标代码是否抛异常,拦截器代码都会执行;
  • @AfterReturning:和 @After 不同的是,只有当目标代码正常返回时,才执行拦截器代码;
  • @AfterThrowing:和 @After 不同的是,只有当目标代码抛出了异常时,才执行拦截器代码;
  • @Around:能完全控制目标代码是否执行,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能。

练习

使用 AOP 实现日志。

下载练习

小结

在 Spring 容器中使用 AOP 非常简单,只需要定义执行方法,并用 AspectJ 的注解标注应该在何处触发并执行。

Spring 通过 CGLIB 动态创建子类等方式来实现 AOP 代理模式,大大简化了代码。

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-08-05发表,共计3883字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中