[Java]Spring-Day 3
[Java]Spring-Day3
学习使用工具
黑马2023新版Java视频教程 https://www.bilibili.com/video/BV1Fv4y1q7ZH?p=8&vd_source=03da0cdb826d78c565cd22a83928f4c2
Java程序员进阶之路 https://tobebetterjavaer.com/overview/java-can-do-what.html
黑马程序员SSM框架教程 https://www.bilibili.com/video/BV1Fi4y1S7ix?p=2&vd_source=03da0cdb826d78c565cd22a83928f4c2
一、AOP简介
Spring有两个核心的概念,一个是IOC/DI
,一个是AOP
。前面已经对IOC/DI
进行了系统的学习,接下来要学习它的另一个核心内容,就是AOP。
- 定义:面向切面编程,一种编程范式,指导开发者如何组织程序结构。
- 特点:在不改原有代码的前提下对其进行增强。
- 核心概念
- 连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
- 在SpringAOP中,理解为方法的执行
- 切入点(Pointcut):匹配连接点的式子
- 在SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法
- 一个具体的方法:如com.itheima.dao包下的BookDao接口中的无形参无返回值的save方法
- 匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法
- 连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点。
- 在SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法
- 通知(Advice):在切入点处执行的操作,也就是共性功能
- 在SpringAOP中,功能最终以方法的形式呈现
- 通知类:定义通知的类
- 切面(Aspect):描述通知与切入点的对应关系。
- 连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
二、AOP入门案例
-
需求分析:使用SpringAOP的注解方式,完成在方法执行前输出当前系统时间。
-
思路分析
- 导入坐标(pom.xml)
- 制作连接点(原始操作,Dao接口与实现类)
- 制作共性功能(通知类与通知)
- 定义切入点
- 绑定切入点与通知关系(切面)
-
环境准备
-
BookDao和BookDaoImpl类。save方法中已有输出当前系统时间的代码,update方法则没有。
public interface BookDao { public void save(); public void update(); } @Repository public class BookDaoImpl implements BookDao { public void save() { System.out.println(System.currentTimeMillis()); System.out.println("book dao save ..."); } public void update(){ System.out.println("book dao update ..."); } }
-
Spring配置类
@Configuration @ComponentScan("com.itheima") public class SpringConfig { }
-
App运行类
public class App { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); BookDao bookDao = ctx.getBean(BookDao.class); bookDao.save(); } }
-
-
AOP实现步骤
-
在pom.xml中添加依赖项,导入AspectJ。AspectJ是AOP思想的一个具体实现,Spring有自己的AOP实现,但是相比于AspectJ来说比较麻烦,所以我们直接采用Spring整合ApsectJ的方式进行AOP开发。
-
定义通知类和通知
通知就是将共性功能抽取出来后形成的方法,共性功能指的就是当前系统时间的打印。
public class MyAdvice { public void method(){ System.out.println(System.currentTimeMillis()); } }
-
定义切入点
public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} public void method(){ System.out.println(System.currentTimeMillis()); } }
切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑。
-
制作切面
切面用来描述通知和切入点之间的关系。@Before表示通知会在切入点方法执行之前执行。
public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Before("pt()") public void method(){ System.out.println(System.currentTimeMillis()); } }
-
将通知类配给容器并标识为切面类
@Component @Aspect public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Before("pt()") public void method(){ System.out.println(System.currentTimeMillis()); } }
-
开启注解格式AOP功能
@Configuration @ComponentScan("com.itheima") @EnableAspectJAutoProxy public class SpringConfig { }
-
三、AOP工作流程与核心概念
- 工作流程
- 容器启动
- 读取所有切面配置中的切入点
- 初始化bean,判定bean对应的类中的方法是否匹配到任意切入点。匹配失败,创建原始对象。匹配成功,创建原始对象(目标对象)的代理对象。因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象,最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强。
- 获取bean执行方法
- 核心概念
- 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的。也叫原始对象,它在运行的过程中对于要增强的内容是缺失的。
- 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现。SpringAOP是在不改变原有设计(代码)的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象。
四、AOP配置管理
-
AOP切入点表达式
@Pointcut("execution(void com.itheima.dao.BookDao.save())")
execution(public User com.itheima.service.UserService.findById(int))
- execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
- public:访问修饰符,还可以是public,private等,可以省略
- User:返回值,写返回值类型
- com.itheima.service:包名,多级包使用点连接
- UserService:类/接口名称
- findById:方法名
- int:参数,直接写参数的类型,多个类型用逗号隔开
- 异常名:方法定义中抛出指定异常,可以省略
-
通配符
如果每一个方法对应一个切入点表达式,编写起来会比较麻烦。使用通配符可以简化配置描述。
-
*
:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现execution(public * com.itheima.*.UserService.find*(*))
匹配com.itheima包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法
-
..
:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写execution(public User com..UserService.findById(..))
匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法
-
+
:专用于匹配子类类型execution(* *..*Service+.*(..))
使用率较低,描述子类的。*Service+,表示所有以Service结尾的接口的子类。
execution(* com.itheima.dao.impl.BookDaoImpl.update(*)) 返回值任意,但是update方法必须要有一个参数 execution(void com.*.*.*.*.update()) 返回值为void,com包下的任意包三层包下的任意类的update方法,匹配到的是实现类 execution(void com.*.*.*.update()) 返回值为void,com包下的任意两层包下的任意类的update方法,匹配到的是接口 execution(void *..update()) 返回值为void,方法名是update的任意包下的任意类,能匹配 execution(* *..*(..)) 匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广 execution(* *..u*(..)) 匹配项目中任意包任意类下只要以u开头的方法 execution(* *..*e(..)) 匹配项目中任意包任意类下只要以e结尾的方法 execution(void com..*()) 返回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法 execution(* com.itheima.*.*Service.find*(..)) 将项目中所有业务层方法的以find开头的方法匹配 execution(* com.itheima.*.*Service.save*(..)) 将项目中所有业务层方法的以save开头的方法匹配
- 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了
- 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)
- 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
- 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
- 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名
- 方法名书写以动词进行精准匹配,名词采用匹配,例如getById书写成getBy,selectAll书写成selectAll
- 参数规则较为复杂,根据业务方法灵活调整
- 通常不使用异常作为匹配规则
-
-
类型介绍
5种通知类型:
- 前置通知
@Before("pt()")
:追加功能到方法执行前,类似于在代码1或者代码2添加内容 - 后置通知
@After("pt()")
:追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容 - 环绕通知
@Around("pt()")
:环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,它可以实现其他四种通知类型的功能。 - 返回后通知
@AfterReturning("pt2()")
:追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,如果方法执行抛出异常,返回后通知将不会被添加 - 抛出异常后通知
@AfterThrowing("pt2()")
:追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内容,只有方法抛出异常后才会被添加
- 前置通知
-
环绕通知
@Component @Aspect public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Around("pt()") public void around(){ System.out.println("around before advice ..."); System.out.println("around after advice ..."); } }
public interface BookDao { public void update(); } @Repository public class BookDaoImpl implements BookDao { public void update(){ System.out.println("book dao update ..."); } }
此时运行,会发现通知的内容打印出来,但是原始方法的内容却没有被执行。
因为环绕通知需要在原始方法的前后进行增强,所以环绕通知就必须要能对原始操作进行调用。因此添加对原始操作的调用。
@Component @Aspect public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Around("pt()") public void around(ProceedingJoinPoint pjp) throws Throwable{ System.out.println("around before advice ..."); //表示对原始操作的调用 pjp.proceed(); System.out.println("around after advice ..."); } }
-
原始方法有返回值时,要根据原始方法的返回值来设置环绕通知的返回值。
@Around("pt2()") public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable { System.out.println("around before advice ..."); //表示对原始操作的调用 Object ret = pjp.proceed(); System.out.println("around after advice ..."); return ret; } }
为什么返回的是Object而不是int的主要原因是Object类型更通用。在环绕通知中是可以对原始方法返回值就行修改的。
-
环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知。通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
-
对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型,原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
-
由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常
-
五、案例:记录万次执行的时间
@Component
@Aspect
public class ProjectAdvice {
//配置业务层的所有方法
@Pointcut("execution(* com.itheima.service.*Service.*(..))")
private void servicePt(){}
//@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
@Around("servicePt()")
public void runSpeed(ProceedingJoinPoint pjp){
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
pjp.proceed();
}
long end = System.currentTimeMillis();
System.out.println("业务层接口万次执行时间: "+(end-start)+"ms");
}
}
程序优化:多个方法一起执行测试的时候,控制台都打印的是业务层接口万次执行时间:xxxms
,没有办法区分到底是哪个接口的哪个方法执行的具体时间。
@Component
@Aspect
public class ProjectAdvice {
//配置业务层的所有方法
@Pointcut("execution(* com.itheima.service.*Service.*(..))")
private void servicePt(){}
//@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
@Around("servicePt()")
public void runSpeed(ProceedingJoinPoint pjp){
//获取执行签名信息
Signature signature = pjp.getSignature();
//通过签名获取执行操作名称(接口名)
String className = signature.getDeclaringTypeName();
//通过签名获取执行操作名称(方法名)
String methodName = signature.getName();
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
pjp.proceed();
}
long end = System.currentTimeMillis();
System.out.println("万次执行:"+ className+"."+methodName+"---->" +(end-start) + "ms");
}
}
六、AOP通知获取数据
-
获取切入点方法的参数,所有的通知类型都可以获取参数
-
JoinPoint:适用于前置、后置、返回后、抛出异常后通知
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))") private void pt(){} @Before("pt()") public void before(JoinPoint jp) Object[] args = jp.getArgs(); System.out.println(Arrays.toString(args)); System.out.println("before advice ..." ); } }
-
ProceedingJoinPoint:适用于环绕通知。
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))") private void pt(){} @Around("pt()") public Object around(ProceedingJoinPoint pjp)throws Throwable { Object[] args = pjp.getArgs(); System.out.println(Arrays.toString(args)); Object ret = pjp.proceed(); return ret; } //其他的略 }
pjp.proceed()方法有两个构造方法
-
调用无参数的proceed,当原始方法有参数,会在调用的过程中自动传入参数
-
所以调用这两个方法的任意一个都可以完成功能
-
但是当需要修改原始方法的参数时,就只能采用带有参数的方法
@Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable{ Object[] args = pjp.getArgs(); System.out.println(Arrays.toString(args)); args[0] = 666; Object ret = pjp.proceed(args); return ret; }
-
-
-
获取切入点方法返回值,只有返回后
AfterReturing
和环绕Around
这两个通知类型可以获取。-
返回后通知
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))") private void pt(){} @AfterReturning(value = "pt()",returning = "ret") public void afterReturning(Object ret) { System.out.println("afterReturning advice ..."+ret); } }
注意returning = "ret" 与
public void afterReturning(Object ret)
的Object ret中的ret位置名称必须一致。为了能匹配更多的参数类型,参数一般写成Object类型。
如果有JoinPoint参数,参数必须要放在第一位。
-
环绕通知,
ret
就是方法的返回值@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))") private void pt(){} @Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable{ Object[] args = pjp.getArgs(); System.out.println(Arrays.toString(args)); args[0] = 666; Object ret = pjp.proceed(args); return ret; } }
-
-
获取切入点方法运行异常信息,只有抛出异常后
AfterThrowing
和环绕Around
这两个通知类型可以获取。-
抛出异常后通知
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))") private void pt(){} @AfterThrowing(value = "pt()",throwing = "t") public void afterThrowing(Throwable t) { System.out.println("afterThrowing advice ..."+t); } }
注意
throwing = "t"
与参数部分Throwable t
的"t"位置名称必须一致。 -
环绕通知
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))") private void pt(){} @Around("pt()") public Object around(ProceedingJoinPoint pjp){ Object[] args = pjp.getArgs(); System.out.println(Arrays.toString(args)); args[0] = 666; Object ret = null; try{ ret = pjp.proceed(args); }catch(Throwable throwable){ t.printStackTrace(); } return ret; } }
-