aop是spring的核心功能之一,之前也有用过,但用得不多(最常用的也就是使用spring aop来管理事务),以致于在一段时间没有接触后,对spring的这个功能渐渐有点生疏了,趁现在有时间,于是又重头看了一遍,无意间在网上看到了开涛老师介绍spring aop的博客,介绍得非常详细,自己看到以后也是受益匪浅,想要深入了解spring aop的人建议去看一下,地址【第六章】 AOP 之 6.1 AOP基础 ——跟我学spring3,看完之后自己也按照开涛老师博客的指导写了一个例子,顺便记录下来,以加深印象,以后查看也方便。
aop(面向方面编程)是对oop(面向对象编程)的补充,aop提供了另一种方式来思考程序结构,它提供了一种可插拔的编程方式,将系统中的非业务模块(诸如日志记录,性能监控等)与业务模块进行解耦。
假设有这么一个业务需求,实现银行转账,在转账之前和转账之后要打印操作记录。先来看看在传统的oop编程中怎么现实这么一个功能。
首先先定义一个银行接口:
package com.chenqa.springaop.example.service;
public interface BankService {
/**
* 模拟的银行转账
* @param from 出账人
* @param to 入账人
* @param account 转账金额
* @return
*/
public boolean transfer(String from, String to, double account);
}
接下来再定义一个银行转账的公共抽象类,用于记录打印日志:
package com.chenqa.springaop.example.service;
public abstract class AbstractBankService implements BankService {
//在这个方法中打印转账之前与转账之后的日志
public boolean transfer(String form, String to, double account) {
//转账之前打印日志
System.out.println("开始转账...");
boolean result = realTransfer(form, to, account);
//转账之前打印日志
System.out.println("转账结束...");
return result;
}
//真正的转账操作,由具体的子类实现
protected abstract boolean realTransfer(String form, String to, double account);
}
接下来再定义两个具体的转账类,实现转账操作:
package com.chenqa.springaop.example.service.impl;
import com.chenqa.springaop.example.service.AbstractBankService;
public class CCBBankServiceImpl extends AbstractBankService {
protected boolean realTransfer(String form, String to, double account) {
// TODO 调用银行转账接口
//这里直接打印信息,达到测试效果即可
System.out.println(form+"向"+to+"建行账户转账了"+account+"元");
return true;
}
}
package com.chenqa.springaop.example.service.impl;
import com.chenqa.springaop.example.service.AbstractBankService;
public class ICBCBankServiceImpl extends AbstractBankService {
protected boolean realTransfer(String form, String to, double account) {
// TODO 调用银行转账接口
//这里直接打印信息,达到测试效果即可
System.out.println(form+"向"+to+"工行账户转账了"+account+"元");
return true;
}
}
创建spring配置文件spring-aop.xml在里面增加两个bean定义:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="ccbBankService" class="com.chenqa.springaop.example.service.impl.CCBBankServiceImpl"/>
<bean id="icbcBankService" class="com.chenqa.springaop.example.service.impl.ICBCBankServiceImpl"/>
</beans>
编写测试类:
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-aop.xml");
BankService ccbBankService = context.getBean("ccbBankService", BankService.class);
BankService icbcBankService = context.getBean("icbcBankService", BankService.class);
ccbBankService.transfer("张三", "李四", 200);
icbcBankService.transfer("王五", "赵六", 200);
}
运行后成功打印出了:
以上就是oop的实现方式,日志记录代码与核心业务代码耦合到了一起,如果哪天不想要日志打印功能或者想增加性能监控功能,就需要在AbstractBankService类中去修改transfer方法里的代码,为了一些非业务性的功能而去修改核心业务类里的代码这也不是一个好的设计方式。
但是有了aop以后,我们可以将诸如日志打印,性能监控这些模块从业务模块中抽离出来,形成一个横切面,然后在执行转账操作时,将切面通知(如:具体的日志记录操作,性能监控操作)织入到目标对象(icbcBankServiceImpl、ccbBankServiceImpl)中去,这样就实现了转账时的日志记录与性能监控功能,将切面通知织入到目标对象后的业务模型可用下图来表示:。
那么spring aop是如何实现在运行时将切面通知织入到目标对象中去的呢,这里要分两种情况:1、如果目标对象实现了某个接口,则采用jdk的动态代理功能,为目标对象生成一个代理,然后在代理对象中进行日志记录。2、如果目标对象没有实现接口,则采用cglib代理来实现上述功能。
大概了解了aop的实现过程以后,接下来看一下aop中的一些专业术语,有些术语在之前两段话已经过介绍到:
切面(Aspect):在spring中切面表现为一个类,这个类中定义了一系列的要横切到目标对象的操作。
通知(Advice):切面中定义的要横切到目标类的一个具体操作。通知可以分为前置通知(before),后置通知(after),环绕通知(around)。
连接点(Join point):目标对象执行期间允许切面通知切入的点,比如方法执行时、处理异常时。在spring aop中连接点只能是在方法执行时。
切入点(Pointcut):连接点的匹配模式,切入点通过表达式来指定将切面通知具体切入到哪个目标对象的哪个方法上。
目标对象(Target):切面通知切入的某个具体对象,spring aop是通过运行时代理来实现的,因此目标对象总是一个代理对象。
织入(Weaving):切面通知切入到某个具体对象的过程,这个过程可以在编译时完成(使用aspectj编译器可以实现),也可以在类加载时完成,也可能在运行时完成。在spring aop中只能是在运行时完成。
引入(Introduction):为已有类动态的添加一些属性或方法。
接下来再来看看使用aop如何来实现前面提到的给转账功能加入日志打印和性能监控功能,同样定义两个具体的银行转账实现类,在使用spring aop以后实现类不再继承AbstractBankService抽象类,直接实现BankService接口:
package com.chenqa.springaop.example.service.impl;
import com.chenqa.springaop.example.service.BankService;
public class BOCBankServiceImpl implements BankService {
public boolean transfer(String form, String to, double account) {
// TODO 调用银行转账接口
//这里直接打印信息,达到测试效果即可
System.out.println(form+"向"+to+"中行账户转账了"+account+"元");
return true;
}
}
package com.chenqa.springaop.example.service.impl;
import com.chenqa.springaop.example.service.BankService;
public class ABCBankServiceImpl implements BankService {
public boolean transfer(String form, String to, double account) {
// TODO 调用银行转账接口
//这里直接打印信息,达到测试效果即可
System.out.println(form+"向"+to+"农行账户转账了"+account+"元");
return true;
}
}
再定义打印日志横切面和性能监控横切面:
package com.chenqa.springaop.example.aspect;
public class LogAspect {
public void before() {
System.out.println("开始转账...");
}
public void after() {
System.out.println("转账结束...");
}
}
package com.chenqa.springaop.example.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
public class MonitorAspect {
//环绕通知必须要有joinPoint且需要定义返回参数
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();//必须调用joinPoint.proceed(),否则目标对象的方法不会执行
//简单的打印执行时间
System.out.println("耗时:"+(System.currentTimeMillis()-start)+"毫秒");
return result;
}
}
再在spring-aop.xml里添加如下配置:
<bean id="bocBankService" class="com.chenqa.springaop.example.service.impl.BOCBankServiceImpl"/>
<bean id="abcBankService" class="com.chenqa.springaop.example.service.impl.ABCBankServiceImpl"/>
<!-- 定义切面 -->
<bean id="logAspect" class="com.chenqa.springaop.example.aspect.LogAspect"/>
<bean id="monitorAspect" class="com.chenqa.springaop.example.aspect.MonitorAspect"/>
<!-- spring aop配置 -->
<aop:config>
<!-- 切入点匹配 com.chenqa.springaop.example.service.impl包下任何返回类型的以BankServiceImpl结尾的类的所有方法-->
<aop:pointcut expression="execution(* com.chenqa.springaop.example.service.impl.*BankServiceImpl.*(..))" id="pointcut"/>
<!-- 日志切面 -->
<aop:aspect ref="logAspect">
<!-- 前置通知,会在目标方法执行前执行logAspect里的before方法 -->
<aop:before method="before" pointcut-ref="pointcut"/><!-- 引用前面定义的pointcut -->
<!-- 后置通知,会在目标方法执行后执行logAspect里的before方法 -->
<aop:after method="after" pointcut-ref="pointcut"/><!-- 引用前面定义的pointcut -->
</aop:aspect>
<!-- 监控切面 -->
<aop:aspect ref="monitorAspect">
<!-- 环绕通知 -->
<aop:around method="around" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
最后编写测试方法:
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-aop.xml");
BankService abcBankService = context.getBean("abcBankService", BankService.class);
BankService bocBankService = context.getBean("bocBankService", BankService.class);
abcBankService.transfer("张三", "李四", 200);
bocBankService.transfer("王五", "赵六", 200);
}
执行结果如下:
通过aop将程序的业务代码与其他功能代码完全分离开来,并且如果不想要日志打印功能或者性能监控功能只要在配置文件里把相应的配置注释掉即可。
除了使用配置文件来配置aop功能,spring还支持使用annotation来实现aop,要使用annotation,需要在spring-aop.xml里删除掉以下代码:
<!-- 定义切面 -->
<bean id="logAspect" class="com.chenqa.springaop.example.aspect.LogAspect"/>
<bean id="monitorAspect" class="com.chenqa.springaop.example.aspect.MonitorAspect"/>
<!-- spring aop配置 -->
<aop:config>
<!-- 切入点匹配 com.chenqa.springaop.example.service.impl包下任何返回类型的以BankServiceImpl结尾的类的所有方法-->
<aop:pointcut expression="execution(* com.chenqa.springaop.example.service.impl.*BankServiceImpl.*(..))" id="pointcut"/>
<!-- 日志切面 -->
<aop:aspect ref="logAspect">
<!-- 前置通知,会在目标方法执行前执行logAspect里的before方法 -->
<aop:before method="before" pointcut-ref="pointcut"/><!-- 引用前面定义的pointcut -->
<!-- 后置通知,会在目标方法执行后执行logAspect里的before方法 -->
<aop:after method="after" pointcut-ref="pointcut"/><!-- 引用前面定义的pointcut -->
</aop:aspect>
<!-- 监控切面 -->
<aop:aspect ref="monitorAspect">
<!-- 环绕通知 -->
<aop:around method="around" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
并加上如下配置:
<!-- 自动扫描 com.chenqa.springaop.example.aspect包下的@Aspect组件,将其注册为切面-->
<context:component-scan base-package="com.chenqa.springaop.example.aspect"/>
<!-- 开始aspectj自动代理功能 -->
<aop:aspectj-autoproxy/>
增加MyPointcut类:
package com.chenqa.springaop.example.aspect;
import org.aspectj.lang.annotation.Pointcut;
public class MyPointcut {
//定义切入点
@Pointcut("execution(* com.chenqa.springaop.example.service.impl.*BankServiceImpl.*(..))")
public static void transferJoinPoint() {}
}
为方便引用,将MyPointcut类放至与LogAspect和MonitorAspect至同一包中,然后修改LogAspect和MonitorAspect,修改后的代码如下:
package com.chenqa.springaop.example.aspect;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class LogAspect {
@Before(value="MyPointcut.transferJoinPoint()")
public void before() {
System.out.println("开始转账...");
}
@After(value="MyPointcut.transferJoinPoint()")
public void after() {
System.out.println("转账结束...");
}
}
package com.chenqa.springaop.example.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MonitorAspect {
//环绕通知必须要有joinPoint且需要定义返回参数
@Around(value="MyPointcut.transferJoinPoint()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();//必须调用joinPoint.proceed(),否则目标对象的方法不会执行
//简单的打印执行时间
System.out.println("耗时:"+(System.currentTimeMillis()-start)+"毫秒");
return result;
}
}
也可实现同样的效果。