AOP介绍
Spring有两个核心概念,一个是IOC/DI,一个是AOP。AOP全称是Aspect Oriented Programming 即面向切面编程。它是一种编程范式,是一种编程思想。AOP的目的是在不惊动代码原始设计的基础上为其进行功能的增强。
AOP的核心概念有以下几点:
- 连接点:在Spring中实现类的所有方法被称为连接点。
- 切入点:实现类中需要被增强的方法称为切入点。
- 通知:通知类中的存放共性功能的方法被称为通知。
- 通知类:通知方法所存在的类被称为通知类。
- 切面:通知和切入点之间的关系描述被称为通知。
- 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的。
- 代理(Proxy)目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现。
AOP的实现思路
- 导入坐标(pom.xml)
- 制作连接点(原始操作,Dao接口和实现类)
- 制作共性功能(通知类与通知)
- 定义切入点
- 绑定切入点与通知关系(切面)
AOP入门案例
需求分析:使用SpringAOP的注解方式完成在方法执行前打印出当前系统时间。
代码实现:
- 导入坐标,需要导入aspectjweaver以及spring-aop。其中spring-aop随着spring-context中,所以导入spring-context和aspectjweaver即可。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
- 制作连接点(原始操作,Dao接口和实现类)
package com.itheima.dao;
public interface BookDao {
public void save();
public void update();
}
package com.itheima.dao.impl;
import com.itheima.dao.BookDao;
import org.springframework.stereotype.Repository;
@Repository
public class BookDaoImpl implements BookDao {
@Override
public void save() {
System.out.println(System.currentTimeMillis());
System.out.println("save.....");
}
@Override
public void update() {
System.out.println("update....");
}
}
- 创建aop包,在aop包下创建MyAdvice通知类,制作共性功能(通知类与通知)。
package com.itheima.aop;
public class MyAdvice {
public void showTime(){
System.out.println(System.currentTimeMillis());
}
}
- 在通知类下定义切入点方法。权限必须为private 返回值为void 方法体为空。
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
- 绑定切入点与通知的关系,即获取切面
@Before("pt()")
public void showTime(){
System.out.println(System.currentTimeMillis());
}
- 使用注解将通知类加入到ioc容器,并申明为aspect。
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Before("pt()")
public void showTime(){
System.out.println(System.currentTimeMillis());
}
}
- 在Spring配置类上添加注解,支持注解方式AOP
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}
至此便完成了SpringAOP入门,即使update方法体内没有申明打印系统时间的代码。但是我们通过AOP注解的方式,在不修改代码的基础上为update()方法做了功能增强。
学习总结
Spring中实现AOP的步骤如下:
- 导入依赖,spring-aop以及aspectjweaver的jar包
- 创建连接点,编写Dao层接口和实现类
- 创建通知类,在通知类上加@Component和@Aspect。表明此类是通知类并作为bean放入容器。
- 在通知类中使用@PointCut(“execution(void 包名.方法名(参数…))”)修饰一个private void 方法名(){}的方法,该方法即为切入点。
- 在通知类中使用@Before(“切入点方法名”)修饰通知方法。
- 最后在Spring配置类上添加支持使用注解实现AOP的注解@EnableAspectJAutoProxy
对Spring中面向接口编程的一些想法
在学习过Spring的IOC、ID以及AOP后,特别是使用注解完成Spring开发后。我对Spring中面向接口的印象越来越深。
面向接口编程的好处是程序结构清晰,并且降低了耦合。
第一次遇到面向接口在运行类中获取ApplicationContext对象,它的产生是可以通过new ClassPathXmlApplication(xxx.xml)
也可以是new AnnotationConfigApplicationContext(SpringConfig.class)
第二次遇到是在使用注解开发IOC/DI的运行类中ctx.getBean(BookDao.class)
的时候,在为BookDaoImpl上添加了@Repository注解后,我们既可以通过ctx.getBean(BookDao.class)
获取BookDao对象,也可以通过ctx.getBean(BookDaoImpl.class)
获取。但通常我们使用BookDao.class。有多个实现类的话
第三次是在使用注解完成AOP,在配置切入点的时候使用@Pointcut("execution(void com.itheima.dao.BookDao.update())")
并且在注解实现AOP时,运行类中不能使用ctx.getBean(BookDaoImpl.class)
否则会报错。
总而言之,在Spring开发中,尽量使用面向接口编程。
AOP配置管理
AOP切入点表达式
- 切入点:要进行增强的方法
- 切入点表达式:要进行增强的方法的描述方式
在之前的学习中,对切入点的描述有两种方式。
第一种,执行接口中的无参数的方法。比如@Pointcut("execution(void com.itheima.dao.BookDao.update())")
第二种,执行实现类中的无参数方法。不如@Pointcut("execution(void com.itheima.dao.impl.BookDaoImpl.update())")
这样的描述方式有一个坏处。如果有很多切入点。那么就要编写很多和上面一样的切入点代码。这样非常的繁琐。通配符的出现使得不再需要书写大量切入点表达式。
通配符介绍
使用通配符描述切入点,主要的目的就是简化之前的配置。通配符具体有以下三种:
*
:单个单独的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
execution(public * com.itheima.*.UserService.find*(*))
..
:多个连续的任意符号,可以独立出现,常用于简化包名和参数的书写
execution(User com..UserService.findById(..))
+
:专用于匹配子类类型
execution(* *..*Service+.*(..))
AOP通知类型
AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码也要将去加入到合理的位置。可以通过在通知方法上加注解的方式来选择位置。一共有5种通知类型。
- 前置通知
- 后置通知
- 环绕通知(重点)
- 返回后通知(了解)
- 抛出异常后通知(了解)
代码实现:
使用@Before(“pt()”)实现前置通知,具体操作在前面AOP入门的获取切面部分。
略
使用@After(“pt()”)实现后置通知,操作与前置通知类似
略
使用@Around(“pt()”)实现环绕通知。重点!!!
@Around("pt()")
public void showTime(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice");
//表示对原始操作进行调用
pjp.proceed();
System.out.println("around after advice");
}
以上是对于没有返回值的切入点实现的环绕通知,对于有返回值的切入点,如果直接用以上代码会报错!!!查看报错原因的大概意思是:空的返回值不匹配原始方法的返回。
对于有返回值的方法,不能直接用以上的代码。而是需要根据原始方法的返回值来设置环绕通知的返回值。
具体的解决方案为:
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.*())")
private void pt(){}
@Around("pt()")
public Object showTime(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice");
//表示对原始操作进行调用
Object ret = pjp.proceed();
System.out.println("around after advice");
return ret;
}
}
使用@AfterReturning(“pt()”)实现返回后通知。如果原始方法有抛出异常,则不会执行。
略
使用@AfterThrowing(“pt()”)实现抛出异常后通知。
略
AOP通知获取数据
目前我们从AOP仅仅是在原始方法的前后追加了一些操作,还没有涉及到对数据相关内容的操作。接下来我们将从获取参数、获取返回值和获取异常3个方面来研究切入点的相关信息。
获取切入点方法的参数,所有的通知类型都可以获取参数
- JoinPoint:适用于前置、后置、返回后、抛出异常后通知
- ProceedingJoinPoint:适用于环绕通知
获取切入点方法的返回值,前置和抛出异常是没有返回值,后置通知可有可无,所以不做研究。
- 返回后通知
- 环绕通知
获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究。
- 抛出异常后通知
- 环绕通知
获取参数
非环绕通知 获取方式:在方法的形参中添加JoinPoint,通过JoinPoint的对象来获取参数。代码如下(代码中通知类型为@After):
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.*())")
private void pt(){}
@After("pt()")
public void showTime(JoinPoint jp) throws Throwable {
System.out.println("advice");
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
}
}
环绕通知获取方式:在通知方法的形参中添加ProceedingJoinPoint,调用其对象来获取参数
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.*())")
private void pt(){}
@Around("pt()")
public void showTime(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("advice");
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
pjp.proceed();
}
}
获取返回值类型
对于获取返回值,分为环绕通知获取返回值和返回后通知获取返回值。
使用环绕通知获取返回值的代码如下:
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.*())")
private void pt(){}
@Around("pt()")
public Object showTime(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("advice");
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
Object ret = pjp.proceed();
return ret;
}
}
上述代码中,ret就是方法的返回值,如果有需要还可以对其进行修改。
使用返回后通知获取返回值的代码如下:
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.*())")
private void pt(){}
@AfterReturning(value = "pt()",returning = "ret")
public void showTime(Object ret) throws Throwable {
System.out.println("AfterReturning advice..."+ret);
}
}
其中 @AfterReturning(value = "pt()",returning = "ret")
中returning为ret,则 public void showTime(Object ret)
的参数名也必须为ret
获取异常
对于获取抛出的异常,只有抛出异常后通知和环绕通知可以获取。
环绕通知获取异常
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.*())")
private void pt(){}
@Around("pt()")
public Object showTime(ProceedingJoinPoint pjp) {
System.out.println("advice");
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
Object ret = null;
try {
ret = pjp.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
return ret;
}
}
使用try-catch的方式即可轻松获取异常。
抛出异常后通知获取异常
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.*())")
private void pt(){}
@AfterThrowing(value = "pt()",throwing = "t")
public void showTime(Throwable t) {
System.out.println("afterThrowing advice..."+t);
}
}
以上代码中@AfterThrowing(value = "pt()",throwing = "t")
中throwing的值必须和showTime(Throwable t)
中参数名一样。