1 AOP的概念
- AOP的概念
面向切面编程,通过预编译方式和运行期间动态代理实现程序功功能统一维护的一种技术.AOP是OOP的延续,是软件开发的一个热点,也是Spring框架中的一个重要内容.是函数式编程的一种衍生范型,利用AOP可以对业务逻辑各个部分进行隔离,从而使得业务逻辑各部分之间耦合度降低,提高程序得可重用性,同时提高了开发的效率. - AOP的作用及优势
- 优势:在程序运行期间,不修改源码对已有的方法进行增强
- 作用:减少重复代码,提高开发效率,维护方便
我们可以通过AccountServiceImpl_old来看一看,AccountServiceImpl_old的里的各个地方都可以进行分离,提高代码质量也就是上面说提到的减少重复代码和提高开发效率,当我们的工具类方法名发生变化时,使用动态代理实现的,可能只有一个地方会有问题,而在AccountServiceImpl_old里每个方法都在调用已改过名的方法,就很难维护
AOP是通过动态代理的方式实现的
Spring中的AOP是通过配置的方式实现刚刚我们在银行转账这一章节的功能.
2.spring中的aop术语和细节
这些术语对于我们今天的课程中没有什么太多的应用,但是对于后续自己学习一些内容的时候,翻看一些资料的时候,是有很大的帮助的.
- JoinPoint (连接点)
所谓连接点是指那些被拦截到的点.在spring中,这些点是指的方法,因为spring只支持方法类型的连接点,它连接了业务和我们的增强方法中的点.
也就是说,打开我们的业务层接口,你看到的方法,全都是连接点,就比如我们是如何把事务控制增强的方法增加到业务中的?就是只能通过这些方法加上事务的支持,让我们的业务方法形成完整的业务逻辑
-
Pointcut(切入点)
所谓切入点是指我们要对哪些Joinpoint进行拦截定义。
接下来,在业务层中的增强代码加入判定
当我们加入这些代码时我们可以看到,不是业务层里所有的代码我们都支持事务。Test方法就没有事务控制支持,业务层中所有的方法都可以称为连接点,而切入点就是那些被增强的方法。
切入点指的是那些被增强的方法 Advice(通知/增强)
所谓的通知就是指拦截到JoinPoint之后所要做的事情就是通知
通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。
也就是说,当我们invoke方法拦截到了需要增强的方法时,接下来我们就要对方法进行增强,Service需要得到事务控制的支持,也就是说,增强的方法是TransactionManeger,也就是我们的adviece(通知/增强)
增强当然不是一口气全部完成的,它有顺序的,有要求的,所以说就有前置通知,后置通知,异常通知,最终通知,环绕通知的区分。
我们是如何界定的?找到我们执行操作的那一步,在他之前就是前置通知,在它之后就是后置通知,进入catch里的就是异常通知,finally那一步就是最终通知。
环绕通知就是指整个invoke方法在执行就是环绕通知。
在环绕通知中,有明确的切入点方法调用。
- Target(目标对象)
代理的目标对象,也就是被代理对象。 - Weaving(织入)
是指把增强应用到目标对象来创建新的代理对象的过程。
也就是我们的Service本来没有事务的支持,于是用了动态代理创建了一个新的对象返回了一个代理对象,我们在返回代理对象的时候,添加上了对事务的支持,那在这个过程中,添加事务支持的过程叫做织入 - Proxy(代理)
一个类被织入增强后,就产生的一个结果代理类 - Aspect(切面)
是指切入点和通知的结合。
这是个很抽象的概念,切入点是指我们的那些被增强过的方法,通知指提供公共代码的类。
那这些公共代码(比如事务控制)是什么时候去执行的呢?开启事务在执行代码之前,提交事务在执行代码之后,什么时候把这些顺序说明白呢?建立切入点方法和通知方法在执行调用的对应关系,就是切面。
我们如果用配置的方式要说明这些关系,所以从这点来说,配置他们的关系,整个的过程,就是切面。
是概念性的观点,在AOP的配置中没有起到太大的意义。我们在后续的学习中需要知道他是什么意识。
-
Spring中AOP要明确的事
编写核心业务代码(主线),把公共代码抽取出来,制作成通知,在配置文件中,声明切入点与通知的关系,即切面。
3.spring基于XML的AOP-编写必要的代码
1.编写接口IAccountService,这里会有三个方法,分别是保存,更新,删除,提供这三个方法主要是展示三种情况:无返回值,无参数;无返回值,有参数;无返回值,有参数
IAccountService
package com.service;
/**
* 账户的业务层接口
*/
public interface IAccountService {
/**
* 模拟保存,无返回值,无参,为了把这三类方法表现出来
*/
void saveAccount();
/**
* 模拟更新 无返回值,有参
*/
void updateAccount(int i);
/**
*
* 模拟删除,有返回值,无参
* @return
*/
int delete();
//
}
接下来编写AccountServiceImpl,里面只需简单打印实现模拟功能
package com.service.impl;
import com.service.IAccountService;
public class AccountServiceImpl implements IAccountService {
public void saveAccount() {
System.out.println("保存了");
}
public void updateAccount(int i) {
System.out.println("更新了");
}
public int delete() {
System.out.println("删除了");
return 0;
}
}
接下来再编写工具类Logger,创建一个utils包,里面创建Logger类,这个类是公共代码,用于记录日志的工具类,里面的方法printLog,简单的让它模拟日志记录.
package com.utils;
/**
* 用于记录日志的工具类,它里面提供了公共的代码
*/
public class Logger {
/**
* 用于打印日志,计划让其在切入点方法之前执行
*/
public void printLog(){
System.out.println("Logger类printLog开始打印了");
}
}
作为一个日志类,我们需要它在saveAccount,update,Accountdelete方法执行之前执行.
在之前的代码里,我们如果要实现在saveAccount,update,Accountdelete这三个方法之前执行,基于动态代理的方式,我们需要创建一个IAccountService的代理对象,在他们执行之前执行就可以.如果我们不用动态代理的方式,而是用配置的方式,如何实现它呢?
5.切入点表达式的写法
Spring任何内容的运行都需要依赖IOC,所以接下来我们在resource下面创建,bean.xml,我们来配置bean.xml.
现在我们需要用到aop的约束了
有aop的约束
把accountService 和 logger 类放进IOC容器中
接下来我们就需要来配置切面了
- spring中基于xml的AOP配置步骤
1.把通知Bean也交给Spring来管理
2.使用 aop:config 标签来表明开始AOP的配置
3.使用 aop:ascpect 标签表明开始切面的配置
属性 id:切面的唯一标识
属性 ref:是指通知类的bean id
4.在 aop:aspect内部使用对应的标签来配置通知类型
aop:before 表示配置前置通知
method属性:表示通知的方法
printcut属性:用于指定printcut表达式,该表达式的是指明对业务层的哪些方法进行增强
- printcut的写法 :访问修饰符 返回值 包.包.类.方法(参数列表)
捋捋思路,目前为止,我们先把accountService和Logger扔进IOC容器进行统一管理,接下来配置好accounService和Logger,我们就需要来配置切面了.
①accountService需要增强,业务层里的方法想要在执行之前开启Loggie日志记录
②配置了Logger类,里面的printLog方法来提供日志记录。
③接下来配置切面,配置了Logger为前置通知,aop:before 的属性method指明通知方法,printcut属性指明关联类,即需要执行前置通知的方法
完整 ↓
接下来我们写个测试类来测试一下
package com.itheima;
import com.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class test {
public static void main(String[] args) {
//获取IOC容器
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");
IAccountService accountService = (IAccountService) applicationContext.getBean("accountService");
accountService.saveAccount();
}
结果
Logger类printLog开始打印了
保存了
Process finished with exit code 0
关于这段配置其实就只能这样配置,主要还是要理解动态代理,这里面我们需要注意一下切入点表达式。
一开始我们就创建了三种方法,分别为无返回值,无参数;无返回值,有参数;无返回值,有参数 这三种类型。
现在我们把这三种方法都调用一下
public class test {
public static void main(String[] args) {
//获取IOC容器
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");
IAccountService accountService = (IAccountService) applicationContext.getBean("accountService");
accountService.saveAccount();
accountService.updateAccount(1);
accountService.delete();
}
}
结果
Logger类printLog开始打印了
保存了
更新了
删除了
很显然我们只是对saveAccounr方法进行了增强,我们如何对这三个方法都进行增强呢?
有一个全通配的写法
切入点表达式的一些写法:
标准写法:public void 包名.包名.包名...类名.方法名(参数列表)
全通配写法 * *..*.*(..)
此时,我们把配置写上 * *..*.*(..)
,用test测试看一下
结果如下
Logger类printLog开始打印了
保存了
Logger类printLog开始打印了
更新了
Logger类printLog开始打印了
删除了
标准写法是如何变成通配写法的呢
①访问修饰符可以省略 ,那么标准写法就变成了 void 包名.包名.包名...类名.方法名(参数列表)
②返回值可以使用通配符表示任意返回值,也就是这个void用 * 替代,就任何返回值都可以用了
< /aop:before>
测试一下任意返回值的写法,结果没有任何问题,是可以使用的
Logger类printLog开始打印了
保存了
更新了
删除了
③包名可以使用通配符表示任意包,但是有几级包,就需要写几个*.
做个对比,三个 *. 分别对应了com. service. impl.
④包名可以使用..
表示当前包以及子包,这就代表着任意包下只要有AccountServiceImpl类都可以被增强
⑤类名和方法名都可以使用*
来实现通配
代替类名
代替方法名
此时我执行test方法可以看到有updateAccount方法没有被增强
Logger类printLog开始打印了
保存了
更新了
Logger类printLog开始打印了
删除了
因为updateAccount是有参数的
⑥参数列表:
可以直接写数据类型:
基本类型直接写名称 int
引用类型写包名.类名的方式 java.lang.String
可以使用通配符表示任意类型,但是必须有参数
可以使用..
有无参数均可
我们updateAccount是返回int的
此时执行test方法可以看到
保存了
Logger类printLog开始打印了
更新了
删除了
可以看到只有有int返回值的updateAccount被增强了
我们试试使用通配符表示任意类型,但是必须有参数的写法
执行结果:
保存了
Logger类printLog开始打印了
更新了
删除了
可以看到还是只有有参数的更新方法执行了
最后试试..
的方式
结果全部都执行了
Logger类printLog开始打印了
保存了
Logger类printLog开始打印了
更新了
Logger类printLog开始打印了
删除了
在实际开发中不要写出全通配的方式,这意味着所有类都要被增强,切入点表达式的通常写法是切到业务层类下的所有方法
针对我们的代码,就应该是com.service.impl.*.*(..)
6.四种常用通知类型
- 前置通知
- 后置通知
- 异常通知
- 最终通知
- 环绕通知
在Logger类分别加上四种通知,他们是前置通知,后置通知,异常通知和最终通知
package com.utils;
/**
* 用于记录日志的工具类,它里面提供了公共的代码
*/
public class Logger {
/**
* 前置通知
*/
public void beforePrintLog(){
System.out.println("beforePrintLog开始打印了");
}
/**
* 后置通知
*/
public void afterReturningPrintLog(){
System.out.println("afterReturningPrintLog开始打印了");
}
/**
* 异常通知
*/
public void afterThrowingPrintLog(){
System.out.println("afterThrowingPrintLog开始打印了");
}
/**
* 最终通知
*/
public void afterPrintlog(){
System.out.println("afterPrintlog开始打印了");
}
}
在bean.xml中配置通知,不过这里有个问题就是切入点表达式
7.通用化切入点表达式
因为都是accoutService业务层的方法需要增强,所以切入点都写入expression="execution(void com.service.impl.AccountServiceImpl.saveAccount(..))"
会过于重复,我们把它单独提出来,之前的通知
aop:aspect里还有个标签 是
,他有两个属性,id属性用于指定表达式的唯一标识,expression属性用于指定表达式内容
除了pointcut
属性用于指定切入点表达式的属性还有pointcut-ref
,里面就写上
的id
需要注意的是,此标签写在aop:aspect标签内部只能当前切面使用,他还可以写在aop:aspect外面,此时就变成了所有切面可用,以后再开发中,也许会遇到这种情况,标签换个位置后就不能再用了,原因是因为约束,如果要写到aop:aspec
t的外面,那只能写在aop:aspect
标签之前
8.spring中的环绕通知
在执行环绕通知之前,我们先屏蔽掉bean.xml中的前置,后置,异常,最终通知,以免影响我们的环绕通知。
bean.xml中创建环绕通知,使用标签
,它的属性method是环绕通知的方法,我们现在没有,在Logger类建一个就可以了。
先在xml.中配置
再在Logger类中添加环绕通知方法
/**
* 环绕通知
*/
public void aroundPrintLog(){
System.out.println("aroundPrintLog开始打印了");
}
执行结果↓,结果是环绕通知打印了,但是切入点方法却没有执行。
aroundPrintLog开始打印了
问题:当我们执行了环绕通知后,切入点方法没有执行,而通知方法执行了.
分析:当我们在2.spring中的aop术语和细节时演示了环绕通知,与我们现在的环绕通知相比可以看到,invoke方法做为一整个环绕通知它有明确的切入点调用
所以解决问题的思路就来了,我们可以为环绕通知加入切入点方法。
解决:spring框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。
Spring框架为我们提供了ProceedingJoinPoint接口,我们就可以直接拿来使用,搁在参数列表里.
ProceedingJoinPoint对象有proceed()方法,这一行就是明确调用业务层方法的,proceed()方法里面有Object类型的参数,这个参数就是我们的方法执行所需要的参数,这个参数可以从ProceedingJoinPoint对象的方法getArgs()得到.
proceed()方法会抛出异常,我们用try{}catch{}来解决,只是需要注意用Throwalble来捕获.
它是有Object返回值的,最后要把它返回回去.
proceed()是切入点方法,所以在它前面执行的就是前置通知,在它后面执行的就是后置通知,抛出异常的时候就是异常通知,最后就是最终通知,
我们之前讲的通知类型,都是通过配置的方式来手动指定增强的代码什么时候执行,现在我们是通过代码的方式来指定增强代码什么时候执行.
它是spring框架为我们提供一种可以在代码中手动控制增强代码何时执行的方式
9.spring基于注解的AOP配置
我们把这个工程改造为基于注解的Aop配置,可以看到我们原本的xml配置.
那么首先我们用注解配置accountService和Logger
accountService
package com.service.impl;
import com.service.IAccountService;
import org.springframework.stereotype.Service;
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
public void saveAccount() {
System.out.println("saveAccount方法执行了保存");
int i = 1 / 0;
}
public void updateAccount(int i) {
System.out.println("updateAccount方法执行了更新");
}
public int delete() {
System.out.println("delete方法执行了删除");
return 0;
}
}
再用注解配置Logger,在@Componet
注解下面加上@aspect
注解表示它是通知类,相应的四个常用通知类型和环绕通知为
@Before
前置通知
@After
后置通知
@AfterThrowing
异常通知
@Around
环绕通知
还有一点需要配置切入点,使用@Pointut
注解,里面填写切入点表达式.
package com.utils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 用于记录日志的工具类,它里面提供了公共的代码
*/
@Component("logger")
@Aspect
public class Logger {
@Pointcut("execution(* com.service.impl.AccountServiceImpl.saveAccount(..))")
private void pt1(){}
/**
* 前置通知
*/
@Before("pt1()")
public void beforePrintLog(){
System.out.println("前置通知---beforePrintLog开始打印了");
}
/**
* 后置通知
*/
@After("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置通知---afterReturningPrintLog开始打印了");
}
/**
* 异常通知
*/
@AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("异常通知---afterThrowingPrintLog开始打印了");
}
/**
* 最终通知
*/
@AfterThrowing("pt1()")
public void afterPrintlog(){
System.out.println("最终通知---afterPrintlog开始打印了");
}
/**
* 环绕通知
*/
@Around("pt1()")
public Object aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint){
try {
Object rtValue = null;
Object[] args = proceedingJoinPoint.getArgs();
rtValue = proceedingJoinPoint.proceed(args);
return rtValue;
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
}
}
此时在test中执行结果(有异常的情况),可以看到通知的执行顺序是有问题的.
"D:\Program Files\Java\jdk-12.0.2\bin\java.exe" "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=56248:D:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath D:\WorkSpace\spring03_eesy_05anotation\target\test-classes;D:\WorkSpace\spring03_eesy_05anotation\target\classes;D:\maven\LocalWarehouse\org\springframework\spring-context\5.1.2.RELEASE\spring-context-5.1.2.RELEASE.jar;D:\maven\LocalWarehouse\org\springframework\spring-aop\5.1.2.RELEASE\spring-aop-5.1.2.RELEASE.jar;D:\maven\LocalWarehouse\org\springframework\spring-beans\5.1.2.RELEASE\spring-beans-5.1.2.RELEASE.jar;D:\maven\LocalWarehouse\org\springframework\spring-core\5.1.2.RELEASE\spring-core-5.1.2.RELEASE.jar;D:\maven\LocalWarehouse\org\springframework\spring-jcl\5.1.2.RELEASE\spring-jcl-5.1.2.RELEASE.jar;D:\maven\LocalWarehouse\org\springframework\spring-expression\5.1.2.RELEASE\spring-expression-5.1.2.RELEASE.jar;D:\maven\LocalWarehouse\org\aspectj\aspectjweaver\1.9.4\aspectjweaver-1.9.4.jar test
前置通知---beforePrintLog开始打印了
saveAccount方法执行了保存
后置通知---afterReturningPrintLog开始打印了
最终通知---afterPrintlog开始打印了
异常通知---afterThrowingPrintLog开始打印了
Exception in thread "main" java.lang.RuntimeException: java.lang.ArithmeticException: / by zero
at com.utils.Logger.aroundPrintLog(Logger.java:58)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633)
at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.aspectj.AspectJAfterAdvice.invoke(AspectJAfterAdvice.java:47)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:62)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:62)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
at com.sun.proxy.$Proxy18.saveAccount(Unknown Source)
at test.main(test.java:11)
Process finished with exit code 1
在基于注解的AOP中,通知的执行顺序是不明确的在实际开发中,在选择上要考虑.
如果使用环绕通知,执行顺序是正确的,毕竟我们把执行顺序交给了代码来控制.所以如果要使用注解配置AOP,建议以环绕通知的方式.