小明辛苦忙了一整年终于完成了包含300个接口的业务系统项目。项目圆满上线并稳定运行了一段时间了。突然有一天总监说,对于会造成数据变化的所有接口,我们必须记录用户的操作日志。然后小明就吭哧吭哧给其中150个接口,挨个加上日志代码,累得真够呛。
过了一阵子总监又说,所有变化很少的数据全部都加上缓存,缓存涉及到刷新缓存、获取缓存、删除缓存的问题。于是乎,小明就又吭哧吭哧地给其中的100个接口加上缓存相关的代码。
又过了一阵子总监说,所有涉及充值退款费用相关的接口,需要生成发票单存入数据库。这时候小明又需要吭哧吭哧给涉及到的50个接口,挨个加上发票存储操作。
小明天天加班也没在工期内完成任务,并且原本的业务代码已经变得臃肿不堪了。
原本的代码:
/** * 业务方法 */ public static void method() { // 业务操作 doBusiness(); } 复制代码
经过硬编码添加各种非业务性代码后的业务代码:
/** * 业务方法 */ public static void method() { // 日志操作 doLog(); // 业务操作 doBusiness(); // 缓存操作 doLog(); // 发票操作 doReceipt(); } 复制代码
读者应该能明显感受到在没有AOP代理的情况下的缺点
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。主要目标还是致力于解耦,我们可以看到解耦这一理念贯穿于我们的整个编码工作中。我们通过各种设计模式或者设计原则来使得对象之间解耦。通过Spring IOC容器中利用依赖注入使得对象之间的耦合度更低。而AOP的思想解耦得更彻底,通过动态的添加功能来增强实现,并且做到毫无代码的侵入性。利用AOP可以对业务逻辑和非业务逻辑的部分进行隔离,可以提取非业务逻辑的部分,提高程序的可重用性,同时提高了开发的效率。
如何理解“切面”二字呢?
我们的业务流程方法都是自顶向下垂直的,而当我们需要给这些业务方法统一加上某些非业务功能的话,就会发现这些非业务功能方法在图上会连成一条直线,并与原来的业务流程方法垂直横切。
核心业务代码与切面代码解耦,切面代码对核心业务代码完全无侵入,遵守单一职责原则,完全隔离核心业务代码与切面代码。
低耦合带来可维护性高,修改或者新增一个切面代码仅需集中在一处进行更改。低耦合也意味着切面代码可复用性高。
Spring IOC容器天然地为AOP的实现提供了便利,IOC和AOP的结合使得Spring的解耦能力更强。
先声明切面类:
/** * 注解@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 { } 复制代码
测试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概念,接下来我们针对这些概念进行逐一解释。