情景案例 小明辛苦忙了一整年终于完成了包含 300 个接口的业务系统项目。项目圆满上线并稳定运行了一段时间了。突然有一天总监说,对于会造成数据变化的所有接口,我们必须记录用户的操作日志。然后小明就吭哧吭哧给其中 150 个接口,挨个加上日志代码,累得真够呛。 过了一阵子总监又说,所有变化很少的数据全部都加上缓存,缓存涉 ..

Spring AOP 详解

情景案例

小明辛苦忙了一整年终于完成了包含 300 个接口的业务系统项目。项目圆满上线并稳定运行了一段时间了。突然有一天总监说,对于会造成数据变化的所有接口,我们必须记录用户的操作日志。然后小明就吭哧吭哧给其中 150 个接口,挨个加上日志代码,累得真够呛。

过了一阵子总监又说,所有变化很少的数据全部都加上缓存,缓存涉及到刷新缓存、获取缓存、删除缓存的问题。于是乎,小明就又吭哧吭哧地给其中的 100 个接口加上缓存相关的代码。

又过了一阵子总监说,所有涉及充值退款费用相关的接口,需要生成发票单存入数据库。这时候小明又需要吭哧吭哧给涉及到的 50 个接口,挨个加上发票存储操作。

小明天天加班也没在工期内完成任务,并且原本的业务代码已经变得臃肿不堪了。

原本的代码:

/**
 * 业务方法
 */
public static void method() {

    // 业务操作
    doBusiness();

}

经过硬编码添加各种非业务性代码后的业务代码:

/**
 * 业务方法
 */
public static void method() {
    // 日志操作
    doLog();
    // 业务操作
    doBusiness();
    // 缓存操作
    doLog();
    // 发票操作
    doReceipt();

}

读者应该能明显感受到在没有 AOP 代理的情况下的缺点

  1. 业务代码和非业务代码混杂在一起,原本清晰的业务流程淹没在与业务不相关的代码中。
  2. 增加非业务性的功能时,都需要手工硬编码去实现,费时费力。
  3. 代码变得不好维护,一是代码耦合度高,二是需要通过硬编码的方式去拓展或者修改功能。

AOP 是什么?

在软件业,AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。主要目标还是致力于解耦,我们可以看到解耦这一理念贯穿于我们的整个编码工作中。我们通过各种设计模式或者设计原则来使得对象之间解耦。通过 Spring IoC 容器中利用依赖注入使得对象之间的耦合度更低。而 AOP 的思想解耦得更彻底,通过动态的添加功能来增强实现,并且做到毫无代码的侵入性。利用 AOP 可以对业务逻辑和非业务逻辑的部分进行隔离,可以提取非业务逻辑的部分,提高程序的可重用性,同时提高了开发的效率。

如何理解“切面”二字呢?

15703790331.png

我们的业务流程方法都是自顶向下垂直的,而当我们需要给这些业务方法统一加上某些非业务功能的话,就会发现这些非业务功能方法在图上会连成一条直线,并与原来的业务流程方法垂直横切。

为什么使用 AOP?

  1. 核心业务代码与切面代码解耦,切面代码对核心业务代码完全无侵入,遵守单一职责原则,完全隔离核心业务代码与切面代码。

  2. 低耦合带来可维护性高,修改或者新增一个切面代码仅需集中在一处进行更改。低耦合也意味着切面代码可复用性高。

  3. Spring IoC 容器天然地为 AOP 的实现提供了便利,IoC 和 AOP 的结合使得 Spring 的解耦能力更强。

AOP 例子

先声明切面类:

/**
 *  注解@Aspect标识该类为切面类
 */
@Component
@Aspect
public class PersonAspect {

    // 通过表达式定义切入点
    @Pointcut("execution(* com.valarchie.aop.MeetingServiceImpl.meeting(..))")
    public void conference() {}
 
    // 前置通知
    @Before("meeting()")
    public void takeSeats() {
        System.out.println("开会前,找到位置坐");
    }
 
    // 前置通知
    @Before("meeting()")
    public void silenceCellPhones() {
        System.out.println("开会前,手机调成静音");
    }
 
    // 后置通知
    @After("meeting()")
    public void summary() {
        System.out.println("开会后,写总结报告");
    }
 
}

创建要被代理的接口,即 MeetingService 会议服务

public interface MeetingService {
    void meeting();
}

创建 MeetingService 会议服务的具体实现

@Component
public class MeetingServiceImpl implements MeetingService {
 
    @Override
    public void meeting() {
        System.out.println("会议进行中..");
    }
 
}

定义 AOP 配置

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ComponentScan("com.valarchie")
public class AppConfig {
 
}
  1. 注解@EnableAspectJAutoProxy 开启代理;
  2. 如果属性 proxyTargetClass 默认为 false, 表示使用 jdk 动态代理织入增强;
  3. 如果属性 proxyTargetClass 设置为 true,表示使用 Cglib 动态代理技术织入增强;
  4. 如果属性 proxyTargetClass 设置为 false,但是目标类没有声明接口, Spring aop 还是会使用 Cglib 动态代理,也就是说非接口的类要生成代理都用 Cglib。

测试 AOP:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class AopTest {
 
    @Autowired
    private MeetingServiceImpl meetingService;
 
    @Test
    public void testAopAnnotation() {
        meetingService.meeting();
    }
 
}

运行结果:

开会前,手机调成静音
开会前,找到位置坐
会议进行中..
开会后,写总结报告

在这个 AOP 的例子当中我们没有在会议服务实现类当中硬编码需要添加的切面功能,而是通过另外新建一个类来描述切面,以及需要在切面上增强的功能。这样的实现是不是更优雅呢?

关于切入点的表达式稍微解析一下:

例如定义切入点表达式  execution (* com.sample.service.impl..*.*(..))

execution()是最常用的切点函数,其语法如下所示:

 整个表达式可以分为五个部分:

 1、execution(): 表达式主体。

 2、第一个*号:表示返回类型,*号表示所有的类型。

 3、包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,
 com.sample.service.impl包、子孙包下所有类的方法。

 4、第二个*号:表示类名,*号表示所有的类。

 5、*(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法
 的参数,两个句点表示任何参数。

以上的例子当中涉及不少 AOP 概念,接下来我们针对这些概念进行逐一解释。

AOP 中的概念阐述

  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    618 引用 • 1214 回帖 • 790 关注
回帖
请输入回帖内容...