目录
AOP
一、概述
1. 什么是AOP?
AOP:全称是 Aspect Oriented Programming 即:面向切面编程。
简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强。
2. AOP的作用和优势
作用:
在程序运行期间,不修改源码对已有方法进行增强。
优势:
减少重复代码
提高开发效率
维护方便
3. AOP的实现方式
使用动态代理技术
二、Spring中的AOP
1. Spring中的AOP的细节
① 说明
学习Spring中的AOP,就是通过配置的方式,实现上篇文章中的功能。
② AOP相关术语
Joinpoint
(连接点): 所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的 连接点。
通俗来讲,连接点就是service中所有的方法,所有的方法都是连接点。
Pointcut
(切入点): 所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。
切入点,就是那些被增强的方法,所有被增强的方法都是切入点。
Advice
(通知/增强): 所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。
通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。
比如我们上面增强的service里面的方法:
Introduction
(引介): 引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方 法或 Field。
Target
(目标对象): 代理的目标对象。
Weaving
(织入): 是指把增强应用到目标对象来创建新的代理对象的过程。
spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。
Proxy
(代理): 一个类被 AOP 织入增强后,就产生一个结果代理类。
Aspect
(切面): 是切入点和通知(引介)的结合。
③ 学习spring中的AOP要明确的事
a、开发阶段(我们做的)
编写核心业务代码(开发主线):大部分程序员来做,要求熟悉业务需求。
把公用代码抽取出来,制作成通知。(开发阶段最后再做):AOP 编程人员来做。
在配置文件中,声明切入点与通知间的关系,即切面。:AOP 编程人员来做。
b、运行阶段(Spring框架完成的)
Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对 象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
④ 关于代理的选择
在 spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。
2. 基于XML的AOP配置
① 环境搭建
导入坐标
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.veeja</groupId>
<artifactId>spring_day03_03</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
</dependencies>
</project>
业务层模拟
package com.veeja.service;
/**
* 账户的业务层接口
*
* @Author veeja
* 2020/5/13 20:23
*/
public interface IAccountService {
/**
* 模拟保存账户,无参数无返回值
*/
void saveAccount();
/**
* 模拟更新用户,有参数无返回值
*/
void updateAccount(int i);
/**
* 模拟删除用户,无参数有返回值
*/
int deleteAccount();
}
package com.veeja.service.impl;
import com.veeja.service.IAccountService;
/**
* 业务层实现类
*
* @Author veeja
* 2020/5/13 20:27
*/
public class AccountServiceImpl implements IAccountService {
@Override
public void saveAccount() {
System.out.println("执行了保存");
}
@Override
public void updateAccount(int i) {
System.out.println("执行了更新" + i);
}
@Override
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
模拟记录日志的工具类
package com.veeja.utils;
/**
* 用于记录日志的工具类,提供了公共代码
*
* @Author veeja
* 2020/5/13 20:29
*/
public class Logger {
/**
* 用于打印日志:并且让其在切入点方法执行之前开始执行(切入点方法就是业务层方法)
*/
public void printLog() {
System.out.println("Logger.printLog()...");
}
}
② 配置步骤
配置的过程都在注释里面了:
<?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"
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">
<!--配置spring的IOC,把service对象配置进来-->
<bean id="accountService" class="com.veeja.service.impl.AccountServiceImpl"></bean>
<!--spring中基于xml的AOP配置
1. 把通知bean也交给spring来管理
2. 使用 aop:config标签表明开始AOP的配置
3. 使用 aop:aspect标签表明配置切面
id属性:给切面提供一个唯一标志
ref属性:是指定通知类的bean的id
4. 在 aop:aspect标签内部使用对应的标签来配置通知的类型
我们现在的示例示让printLog方法在切入点方法执行之前执行,所以是前置通知
aop:before 表示配置前置通知
method属性:用于指定Logger类中哪个方法是前置通知
pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法进行增强
切入点表达式的写法:
关键字:execution(表达式)
表达式:访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
标准的表达式写法:
public void com.veeja.service.impl.AccountServiceImpl.saveAccount()
-->
<!--配置logger类-->
<bean id="logger" class="com.veeja.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知的类型并且建立通知方法并且建立通知方法和切入点方法的关联-->
<aop:before method="printLog"
pointcut="execution(public void com.veeja.service.impl.AccountServiceImpl.saveAccount())"></aop:before>
</aop:aspect>
</aop:config>
</beans>
③ 测试类测试
public class AOPTest {
public static void main(String[] args) {
// 1. 获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 2.
IAccountService accountService = (IAccountService) ac.getBean("accountService");
accountService.saveAccount();
}
}
结果:
3. 切入点表达式的写法
很显然,我们在上面只针对一个方法进行了配置,我们想要在每个方法之前都做日志的输出,难道我们要把所有的方法都一样的配置一遍吗?显然我们应该有更简单的方法。
execution:匹配方法的执行(常用)
execution(表达式)
表达式语法:
execution([修饰符] 返回值类型 包名.类名.方法名(参数))
写法说明:
我们首先看一下开始的方式:
public void com.veeja.service.impl.AccountServiceImpl.saveAccount()
首先,访问修饰符可以省略,也就是写成:
void com.veeja.service.impl.AccountServiceImpl.saveAccount()
返回值可以使用通配符,表示任意返回值:
* com.veeja.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包。但是有几级包,就要写几个*.
:
* *.*.*.*.AccountServiceImpl.saveAccount()
包名可以使用..
表示当前包及其子包,那就可以写成:
* *..AccountServiceImpl.saveAccount()
类名和方法名都可以使用*
来实现通配:
把类名改成*
:
* *..*.saveAccount()
把方法名改成*
:
* *..*.*()
参数列表:
基本数据类型可以直接写名称,引用类型可以写包名.类名
的方式,例如java.lang.String
类型也可以使用通配符*
来表示任意类型,但是方法必须有参数。
也可以使用..
表示有无参数均可,有参数可以是任意类型。
也就是:* *..*.*(..)
如果我们把表达式,写成了这样:
我们运行测试类:
public class AOPTest {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccountService accountService = (IAccountService) ac.getBean("accountService");
accountService.saveAccount();
accountService.updateAccount(666);
int i = accountService.deleteAccount();
System.out.println(i);
}
}
运行结果:
然而实际开发中,我们一般不这样写,
一般我们切到业务层实现类下的所有方法:
* com.veeja.service.impl.*.*(..)
4. 四种常用通知类型
我们在Logger类
中添加几个方法,作为我们的其他的通知方法:
package com.veeja.utils;
/**
* 用于记录日志的工具类,提供了公共代码
*
* @Author veeja
* 2020/5/13 20:29
*/
public class Logger {
/**
* 前置通知
*/
public void beforePrintLog() {
System.out.println("前置通知:Logger.printLog()...");
}
/**
* 后置通知
*/
public void afterReturningPrintLog() {
System.out.println("后置通知:Logger.afterReturningPrintLog()...");
}
/**
* 异常通知
*/
public void afterThrowingPrintLog() {
System.out.println("异常通知:Logger.afterThrowingPrintLog()...");
}
/**
* 最终通知
*/
public void afterPrintLog() {
System.out.println("最终通知:Logger.afterPrintLog()...");
}
}
我们重新配置bean.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"
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">
<!--配置spring的IOC,把service对象配置进来-->
<bean id="accountService" class="com.veeja.service.impl.AccountServiceImpl"></bean>
<!--配置logger类-->
<bean id="logger" class="com.veeja.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知的类型并且建立通知方法并且建立通知方法和切入点方法的关联-->
<!--配置前置通知-->
<aop:before method="beforePrintLog" pointcut="execution(* com.veeja.service.impl.*.*(..))"></aop:before>
<!--配置后置通知-->
<aop:after-returning method="afterReturningPrintLog"
pointcut="execution(* com.veeja.service.impl.*.*(..))"></aop:after-returning>
<!--配置异常通知-->
<aop:after-throwing method="afterThrowingPrintLog"
pointcut="execution(* com.veeja.service.impl.*.*(..))"></aop:after-throwing>
<!--配置最终通知-->
<aop:after method="afterPrintLog"
pointcut="execution(* com.veeja.service.impl.*.*(..))"></aop:after>
</aop:aspect>
</aop:config>
</beans>
测试方法:
public class AOPTest {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccountService accountService = (IAccountService) ac.getBean("accountService");
accountService.saveAccount();
System.out.println("-------------------------------------------------------");
accountService.updateAccount(666);
System.out.println("-------------------------------------------------------");
int i = accountService.deleteAccount();
System.out.println(i);
}
}
结果:
5. 通用化切入点表达式
aop:pointcut
:
作用:
用于配置切入点表达式。就是指定对哪些类的哪些方法进行增强。
属性:
expression
:用于定义切入点表达式。
id
:用于给切入点表达式提供一个唯一标识
注意: 此标签写在切面标签aop:aspect标签内部,那么只能当前切面使用
如果写在aop:aspect标签外面,那么就是所有的切面都可以使用
但是,这里有一个约束上的要求,就是aop:pointcut
需要放在aop:aspect标签的前面。
例如,我们可以使用通用化切入点表达式来简化上面的配置:
<!--配置AOP-->
<aop:config>
<!--配置切面-->
<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:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>
<!--
配置切入点表达式,id属性用于指定表达式的唯一标识,expression用于指定表达式内容
此标签写在切面标签aop:aspect标签内部,那么只能当前切面使用
如果写在aop:aspect标签外面,那么就是所有的切面都可以使用
-->
<aop:pointcut id="pt1" expression="execution(* com.veeja.service.impl.*.*(..))"/>
</aop:aspect>
</aop:config>
6. 环绕通知
aop:around
:
作用:
用于配置环绕通知
属性:
method:指定通知中方法的名称。
pointct:定义切入点表达式
pointcut-ref:指定切入点表达式的引用
说明:
它是 spring 框架为我们提供的一种可以在代码中手动控制增强代码什么时候执行的方式。
注意:
通常情况下,环绕通知都是独立使用的
xml配置:
<aop:config>
<aop:pointcut id="pt1" expression="execution(* com.veeja.service.impl.*.*(..))"/>
<aop:aspect id="logAdvice" ref="logger">
<!--配置环绕通知-->
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
方法:
/**
* 环绕通知
* 问题:
* 当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。
* 分析:
* 通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码中却没有。
* 解决:
* Spring框架为我们提供了一个接口: ProceedingJoinPoint. 该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
* 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会 为我们提供该接口的实现类供我们使用。
* <p>
* spring中的环绕通知:
* 它是spring框架为我提供的一种可以在代码中手动控制增强方法在何时执行的方式。
*
* @return
*/
public Object aroundPrintLog(ProceedingJoinPoint pjp) {
Object rtValue = null;
try {
Object[] args = pjp.getArgs(); // 得到方法执行所需的参数
System.out.println("Logger.aroundPrintLog()...前置");
rtValue = pjp.proceed(args); //明确调用业务层方法(切入点方法)
System.out.println("Logger.aroundPrintLog()...后置");
return rtValue;
} catch (Throwable t) {
System.out.println("Logger.aroundPrintLog()...异常");
throw new RuntimeException(t);
} finally {
System.out.println("Logger.aroundPrintLog()...最终");
}
}
三、基于注解的AOP
bean.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">
<!--配置spring在创建容器的时候要扫描的包-->
<context:component-scan base-package="com.veeja"></context:component-scan>
<!--配置spring开启注解AOP的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
AccountService:
package com.veeja.service.impl;
import com.veeja.service.IAccountService;
import org.springframework.stereotype.Service;
/**
* 业务层实现类
*
* @Author veeja
* 2020/5/13 20:27
*/
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
@Override
public void saveAccount() {
System.out.println("执行了保存");
}
@Override
public void updateAccount(int i) {
System.out.println("执行了更新" + i);
}
@Override
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
Logger :(我们先把环绕通知的注解给注释上)
package com.veeja.utils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 用于记录日志的工具类,提供了公共代码
*
* @Author veeja
* 2020/5/13 20:29
*/
@Component("logger")
@Aspect //表示当前类是一个切面类
public class Logger {
/**
* 配置切入点表达式
*/
@Pointcut("execution(* com.veeja.service.impl.*.*(..))")
private void pt1() {
}
/**
* 前置通知
*/
@Before("pt1()")
public void beforePrintLog() {
System.out.println("前置通知:Logger.printLog()...");
}
/**
* 后置通知
*/
@AfterReturning("pt1()")
public void afterReturningPrintLog() {
System.out.println("后置通知:Logger.afterReturningPrintLog()...");
}
/**
* 异常通知
*/
@AfterThrowing("pt1()")
public void afterThrowingPrintLog() {
System.out.println("异常通知:Logger.afterThrowingPrintLog()...");
}
/**
* 最终通知
*/
@After("pt1()")
public void afterPrintLog() {
System.out.println("最终通知:Logger.afterPrintLog()...");
}
/**
* 环绕通知
* 问题:
* 当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。
* 分析:
* 通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码中却没有。
* 解决:
* Spring框架为我们提供了一个接口: ProceedingJoinPoint. 该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
* 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会 为我们提供该接口的实现类供我们使用。
* <p>
* spring中的环绕通知:
* 它是spring框架为我提供的一种可以在代码中手动控制增强方法在何时执行的方式。
*
* @return
*/
// @Around("pt1()")
public Object aroundPrintLog(ProceedingJoinPoint pjp) {
Object rtValue = null;
try {
Object[] args = pjp.getArgs(); // 得到方法执行所需的参数
System.out.println("Logger.aroundPrintLog()...前置");
rtValue = pjp.proceed(args); //明确调用业务层方法(切入点方法)
System.out.println("Logger.aroundPrintLog()...后置");
return rtValue;
} catch (Throwable t) {
System.out.println("Logger.aroundPrintLog()...异常");
throw new RuntimeException(t);
} finally {
System.out.println("Logger.aroundPrintLog()...最终");
}
}
}
我们运行以下方法:
我们可以发现,这里面的最终通知,和后置通知,顺序上是有问题的。这是spring中使用注解实现AOP的时候的一个问题。所以我们要谨慎的使用!
那么我们测试一下环绕通知是否有用呢?
@Around("pt1()")
public Object aroundPrintLog(ProceedingJoinPoint pjp) {
Object rtValue = null;
try {
Object[] args = pjp.getArgs(); // 得到方法执行所需的参数
System.out.println("Logger.aroundPrintLog()...前置");
rtValue = pjp.proceed(args); //明确调用业务层方法(切入点方法)
System.out.println("Logger.aroundPrintLog()...后置");
return rtValue;
} catch (Throwable t) {
System.out.println("Logger.aroundPrintLog()...异常");
throw new RuntimeException(t);
} finally {
System.out.println("Logger.aroundPrintLog()...最终");
}
}
同样的测试代码,我们使用环绕通知,却是没有问题的。
如果想使用纯注解的方式,其实就是在我们之前说的纯注解方式上再加上一个@EnableAspectJAutoProxy
注解就可以了。
@Configuration
@ComponentScan(basePackages = "com.veeja")
@EnableAspectJAutoProxy
@Component("logger")
@Aspect //表示当前类是一个切面类
public class Logger {
}
END.