一、概念
1、什么是AOP?
在单体架构下的软件开发中,一个大型项目通常是依照功能拆分成各个模块。但是如日志、安全和事务管理此类重要且繁琐的开发却没有必要参与到各个模块中,将这些功能与业务逻辑相关的模块分离就是面向切面编程所要解决的问题。
AOP即为面向切面编程,它把系统需求按照功能分门归类,把它们封装在一个个切面中,然后再指定这些系统功能往业务功能中织入的规则。最后由第三方机构根据你指定的织入规则,将系统功能整合到业务功能中。
你的程序写好了,现在发现要针对所有业务操作添加一个日志,或者在前面加一道权限控制,怎么办呢?传统的做法是,改造每个业务方法,这样势必把代码弄得一团糟
而且以后再扩展还是更乱,aop的思想是引导你从另一个切面来看待和插入这些工作
日志,不管加在哪,它其实都是属于日志系统这个角度的,权限控制也一样
aop允许你以一种统一的方式在运行时期在想要的地方插入这些逻辑。
AOP采取的是横向抽取机制,取代了传统纵向继承体系重复性代码。
2、那么何为软件的横向和纵向?
从纵向结构来看就是我们软件的各个模块,它所负责的是软件的核心业务(如购商品购买、添加购物车等);从横向来看的话,软件的各个模块之间又有所关联,其中会包含一些公共模块(例如日志、权限等);这些公共模块可以存在于各个核心业务中,而AOP的处理将两者分离,使开发人员可以专注于核心业务的开发,提高了开发效率。
3、AOP 的作用及优势
作用: 在程序运行期间,不修改源码对已有方法进行增强。
优势: 减少重复代码 提高开发效率 维护方便
4、AOP底层原理
使用动态代理实现
-
基于JDK的代理
适用于有接口情况,使用动态代理创建接口实现类代理对象 -
基于CGLIB动态代理
适用于没有接口情况,使用动态代理创建类的子类代理对象
二、Spring中的AOP
相关术语:
- Joinpoint(连接点):那些被拦截到的点
- Pointcut(切入点):对哪些连接点进行拦截的定义(也就是被增强的Joinpoint)
- Advice(通知/增强):指拦截到 Joinpoint 之后所要做的事情。
通知的类型:
前置通知(before):在切入点方法执行之前执行
后置通知(before returning):在切入点方法正常执行之后执行
异常通知(after throwing):在切入点方法产生异常之后执行
最终通知(after):无论如何都会执行
环绕通知
public IAccountService getAccountService(){
return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(),
new InvocationHandler() { 整个invoke就是环绕通知
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rt = null;
try {
//1、开启事务
transactionManager.beginTransaction();---前置通知
//2、执行操作
rt = method.invoke(accountService,args);
//3、提交事务
transactionManager.commit();---后置通知
//4、返回结果
return rt;
} catch (Exception e) {
//5、回滚操作
transactionManager.rollback();---异常通知
throw new RuntimeException(e);
} finally {
//6、释放连接
transactionManager.release();---最终通知
}
}
});
- Introduction (引介): 引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方 法或 Field。 Target(目标对象): 代理的目标对象。
- Weaving (织入): 是指把增强应用到目标对象来创建新的代理对象的过程。 Spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。
- Proxy(代理): 一个类被 AOP 织入增强后,就产生一个结果代理类。
- Aspect(切面): 是切入点和通知(引介)的结合。
二、Spring中基于 xml 的 AOP 配置步骤
- 把通知 Bean 也交给 Spring 来管理
<bean id="accountService" class="com.zg.service.impl.AccountService"></bean>
- 使用
aop:config
标签来表明开始 AOP 的设置 - 使用
aop:aspect
标签配置切面
id 属性:给切面提供一个唯一标识
ref 属性:指定通知类 Bean 的 id - 在
aop:aspect
标签的内部使用对应标签来配置通知的类型
<aop:config>
<aop:pointcut id="pt1" expression="execution(* com.zg.service.impl.*.*(..))"/>
<!--配置一个切面-->
<aop:aspect id="logAdvice" ref="logger">
<aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>
</aop:aspect>
</aop:config>
aop:before
配置前置通知- method 属性:用于指定类中哪个方法是前置通知
- pointcut 属性:用于指定切入点表达式,该切入点表达式指的是对业务层中哪些方法增强
- 切入点表达式的写法:
- 关键字:execution ( 表达式 )
- 表达式:
-
标准写法:访问修饰符 + 返回值 + 包名.类名.方法名(参数列表)
例:public void com.zg.service.impl.AccountServiceImpl.saveAccount ( )
-
访问修饰符可以省略
void com.zg.service.impl.AccountServiceImpl.saveAccount ( )
-
返回值可以使用通配符,表示任意返回值
* com.zg.service.impl.AccountServiceImpl.saveAccount ( )
-
包名可以使用通配符,表示任意包,但是有几级包就需要写几个
*.
* *.*.*.*.AccountServiceImpl.saveAccount ( )
-
包名可以使用
..
表示当前包和子包
* *..AccountServiceImpl.saveAccount ( )
-
类名和方法名都可以使用
*
来实现通配* *..*.*()
- 参数列表
可以直接使写数据类型:
基本类型直接写名称(如 int )
引用类型写包名.类名的方式 (如 java.lang.String )
可以使用通配符表示任意类型,但是必须有参数
可以使用..
表示有无参数即可,有参数可以是任意类型,于是就变成了全通配写法
- 参数列表
-
全通配写法:
* *..*.*(..)
-
实际开发中切入点表达式的通常写法:
切到业务层类实现下的所有方法:
* com.zg.service.impl.*.*(..)
-
- 配置切入点表达式(
aop:pointcut
)
<aop:pointcut id="pt1" expression="execution(* com.zg.service.impl.*.*(..))"/>
- id属性用于指定表达式的唯一标识
- expression属性用于指定表达式内容
- 此标签写在
aop:aspect
标签内部只能当前切面使用,在其外部则所有切面可用,写在外部时要写在所有切面之前。
- 环绕通知
- 问题:
当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。 - 分析:
通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码中没有。 - 解决:
Spring框架为我们提供了一个接口:ProceedingJoinPoint
。该接口有一个方法proceed()
,此方法就相当于明确调用切入点方法。
该接口可以作为环绕通知的方法参数,在程序执行时,Spring框架会为我们提供该接口的实现类供我们使用。 - Spring中的环绕通知:
它是Spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。写在通知之前就成了前置通知,写在之后是后置,写在异常就是异常通知。
- 问题:
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rt = null;
try {
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("beforePrintLog");
rt = pjp.proceed(args);//调用切入点方法
System.out.println("afterReturningPrintLog");
return rt;
} catch (Throwable e) {
System.out.println("afterThrowingPrintLog");
throw new RuntimeException(e);
}finally {
System.out.println("afterPrintLog");
}
}
三、Spring中基于 注解的 AOP 配置
业务层
@Service("accountService")
public class AccountService implements IAccountService {
@Override
public void saveAccount() {
System.out.println("save");
}
@Override
public void updateAccount(int i) {
System.out.println("update");
}
@Override
public int deleteAccount() {
System.out.println("delete");
return 0;
}
}
工具类
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {
@Pointcut("execution(* com.zg.service.impl.*.*(..))")
public void Pt1(){}
@Before("Pt1()")
public void beforePrintLog(){
System.out.println("beforePrintLog");
}
@AfterReturning("Pt1()")
public void afterReturningPrintLog(){
System.out.println("afterReturningPrintLog");
}
@AfterThrowing("Pt1()")
public void afterThrowingPrintLog(){
System.out.println("afterThrowingPrintLog");
}
@After("Pt1()")
public void afterPrintLog(){
System.out.println("afterPrintLog");
}
/* @Around("Pt1()")
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rt = null;
try {
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("beforePrintLog");
rt = pjp.proceed(args);//调用切入点方法
System.out.println("afterReturningPrintLog");
return rt;
} catch (Throwable e) {
System.out.println("afterThrowingPrintLog");
throw new RuntimeException(e);
}finally {
System.out.println("afterPrintLog");
}
}*/
}
xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.zg"></context:component-scan>
<!-- 配置spring创建容器时要扫描的包-->
<bean id="logger" class="com.zg.utils.Logger"></bean>
<!-- 配置spring开启注解AOP的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>