一、AOP的概念
AOP 为 (Aspect Oriented Programming) 的缩写,意为:面向切面编程,底层是使用动态代理的技术实现对目标方法的增强和控制访问等功能。
其中AOP中有几个重要的概念:
1、通知:增强的逻辑,或者后期要加入的代码。
2、目标:被增强的对象(真实类)。
3、代理:通过SpringAOP生成的代理对象。
4、切点:目标对象中需要被增强的方法。
5、连接点:目标对象的方法。
6、切面:通知+切点。
二、切点表达式
2.1、切点表达式的概念
切点表达式是使用execution关键字定义的,用来指定什么类中的什么方法需要被增强。
2.2、切点表达式的格式
切点表达式的完整格式为:
execution(@注解? 访问修饰符? 返回值 包名.类名?.方法名(方法参数) throws 异常?)
一般采用:
其中
1、* 可以通配任意返回值类型、包名、类名、方法名、或任意类型的一个参数
2、.. 可以通配任意层级的包、或任意类型、任意个数的参数
3、注解可省略(没啥用)
4、访问修饰符可省略(没啥用,仅能匹配 public、protected、包级,private 不能增强)
5、包名.类名可省略
6、thorws 异常可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
例如:
execution(* com.itboy.service.PersonService.*(..))
将com.itboy.service包下的PersonService类中的所有方法进行增强,其中方法的返回值为任意的,方法参数的类型和数量也是任意的。
1、*表示返回值类型为任意的
2、com.itboy.service.PersonService表示将com.itboy.service包下的PersonService这个类中的方法进行增强。
3、PersonService后面的.*表示对PersonService中的所有方法进行增强。
4、(..)表示方法参数的数量和类型任意,所以方法参数可以为0或者多个。
5、方法中参数的含义如下:
三、SpringAop入门
3.1、定义一个接口
PersonService
package com.itboy.service;
import com.itboy.pojo.Person;
import java.util.List;
public interface PersonService {
void save(Person person);
void update(Person person);
void delete(int id);
List<Person> findAll();
List<Person> findByPage(int page, int size);
Person findById(int id);
}
3.2、定义一个目标对象(真实类)
PersonServiceImpl
package com.itboy.service.impl;
import com.itboy.pojo.Person;
import com.itboy.service.PersonService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
@Service
@Slf4j
public class PersonServiceImpl implements PersonService {
@Override
public void save(Person person) {
log.info("save({})", person);
}
@Override
public void update(Person person) {
log.info("update({})", person);
}
@Override
public void delete(int id) {
log.info("delete:id={}", id);
}
@Override
public List<Person> findAll() {
log.info("findAll...");
return Arrays.asList(new Person(1, "张益达"), new Person(2, "snake"));
}
@Override
public List<Person> findByPage(int page, int size) {
log.info("findByPage:page={},size={}", page, size);
return Arrays.asList(new Person(1, "张益达"), new Person(2, "snake"));
}
@Override
public Person findById(int id) {
log.info("findById:id={}", id);
return new Person(1, "张益达");
}
}
3.3、定义一个切面类
Aspect1
package com.itboy.aspect;
import com.itboy.anno.MyAnno;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* 切面类:定义增强逻辑,也就是通知+切点
*/
@Component //让Spring管理Bean对象,创建Bean对象。
@Aspect //表示该类是一个切面类,将来需要定义增强逻辑
@Slf4j
public class Aspect1 {
/**
* 定通知:使用@Around注解表示该方法是一个通知方法,将来在通知方法中写通知内容
* 定切点:通过@Around注解中的表达式找到要增强的方法
* 通配符:
* * :表示任意返回值类型、任意方法
* .. :表示任意多个参数占位符
* @param pjp 封装了要增强的方法,用来执行目标对象方法
* @return 哪里调用代理对象的方法就返回到哪里
*/
@Around("execution(void com.itboy.service.impl.PersonServiceImpl.save(..))") //可以
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//System.out.println(("anno的值为"+anno.value()));
//log.info("Aspect1...");
//定义增强逻辑
//1 记录开始时间
long start = System.nanoTime();
System.out.println("Aspect1提供的增强方法:开始时间为"+start+".....");
//2 调用目标对象方法,获取返回值进行返回
Object result=pjp.proceed();
//3 记录结束时间,计算耗时并打印
long end = System.nanoTime();
System.out.println("Aspect1提供的增强方法:结束时间为"+end+".....");
System.out.println("共耗时:"+(end-start));
return result;
}
}
3.4、定义一个测试类
Spring01ApplicationTests
package com.itboy;
import com.itboy.pojo.Person;
import com.itboy.service.PersonService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest
@Slf4j
class Spring01ApplicationTests {
@Autowired
private PersonService personService; //此处使用的是代理对象
@Test
public void test1() {
personService.save(new Person());
}
@Test
public void test2() {
personService.update(new Person());
}
@Test
public void test3() {
System.out.println("personService的类型为:" + personService.getClass());
}
@Test
public void test4() {
personService.delete(1);
}
}
3.5、得到最终的结果
分别执行test1和test2,得到最终的结果。
test1的执行结果:
test2的执行结果:
发现只有save方法被增强了,update方法没有被增强。
因为切面表达式为:
execution(void com.itboy.service.impl.PersonServiceImpl.save(..))
表示对com.itboy.service.impl包下的PersonServiceImpl的save方法进行增强,其中方法的返回值类型为void,方法参数类型和个数是任意的。
3.6、分析
对切面类Aspect1进行分析,切面类要放入Spring容器中,所以Aspect1类上方要加上@Component注解,并且要加上@Aspect注解,表示该类是一个切面类,将来需要定义增强逻辑
切面表达式由@Around 注解包裹,使用@Around注解表示该方法是一个通知方法,将来在通知方法中写增强逻辑。
Object result=pjp.proceed();表示调用目标对象的方法,并获取返回值。并可以在该方法的前后
写增强逻辑
3.7、整个调用的执行流程
我们在Spring01ApplicationTests这个类上方加了@SpringBootTest注解,那么当我们启动test1时,Spring容器也会启动,因为@SpringBootTest注解表示该测试类是基于spring容器实现的。然后Spring就会扫描Aspect1类上的@Component注解和@Aspect注解,再通过切面表达式中指定的类,就会创建PersonServiceImpl对象的代理对象并放入Spring的容器中。
此时我们依赖注入的不是目标对象PersonServiceImpl,而是它的代理对象。
我们执行test3,也能看到它的类型为代理对象,并且是通过cglib实现的。
生成的代理对象是PersonServiceImpl目标对象的子类对象,继承了PersonServiceImpl中的所有方法,调用的是save方法,就会去Aspect1中的切面表达式进行匹配,如果能匹配上,就会进入@Around注解标注的方法中。
图形总结:
3.8、调用流程总结
1、生成目标对象的代理对象并放入Spring的容器中(生成的代理对象是目标对象的子类对象)
2、
@Autowired
private PersonService personService;
这里的personService已经被代理对象所替换。
3、
personService.save(new Person());就会去切面表达式中进入匹配,如果能匹配上就进入到切面表达式定义的方法中,如果不能匹配上,就会进入到目标对象的方法中。
四、SpringAop进阶
4.1、提取切面表达式
可以使用pointcut注解对切点表达式进行抽取,增强它的复用性:类名.方法名即可。这里的方法的方法体为空,只是为了承接pointcut注解的。
4.2、代码
定义一个提取类:MyPointcut
package com.itboy.aspect;
import org.aspectj.lang.annotation.Pointcut;
/**
* @Author zhouxiangyang
* @Date 2022/5/12 14:47
*/
public class MyPointcut {
/**
* @Pointcut注解:定义切点表达式
* 方法名就是切点表达式的名称,将来通过类名.方法名()获取切点表达式
*/
@Pointcut("execution(* com.itboy.service.impl.PersonServiceImpl.save*(..))")
public void pt1(){}
@Pointcut("execution(String com.itboy.service.impl.PersonServiceImpl.update*(..))")
public void pt2(){}
@Pointcut("execution(* com.itboy.service.impl.PersonServiceImpl.delete*(..))")
public void pt3(){}
}
定义一个切面类:Aspect1
package com.itboy.aspect;
import com.itboy.anno.MyAnno;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* 切面类:定义增强逻辑,也就是通知+切点
*/
@Component //让Spring管理Bean对象,创建Bean对象。
@Aspect //表示该类是一个切面类,将来需要定义增强逻辑
@Slf4j
public class Aspect1 {
/**
* 定通知:使用@Around注解表示该方法是一个通知方法,将来在通知方法中写通知内容
* 定切点:通过@Around注解中的表达式找到要增强的方法
* 通配符:
* * :表示任意返回值类型、任意方法
* .. :表示任意多个参数占位符
* @param pjp 封装了要增强的方法,用来执行目标对象方法
* @return 哪里调用代理对象的方法就返回到哪里
*/
@Around("MyPointcut.pt1()|| MyPointcut.pt2() || MyPointcut.pt3()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//System.out.println(("anno的值为"+anno.value()));
//log.info("Aspect1...");
//定义增强逻辑
//1 记录开始时间
long start = System.nanoTime();
System.out.println("Aspect1提供的增强方法:开始时间为"+start+".....");
//2 调用目标对象方法,获取返回值进行返回
Object result=pjp.proceed();
//3 记录结束时间,计算耗时并打印
long end = System.nanoTime();
System.out.println("Aspect1提供的增强方法:结束时间为"+end+".....");
System.out.println("共耗时:"+(end-start));
return result;
}
}
MyPointcut.pt1()|| MyPointcut.pt2() || MyPointcut.pt3()
只要其中一个表达式满足要求即可,并且只会增强满足要求的,例如Update方法不满足切面表达式,所以只有save方法和delete方法会被增强。
4.3、通知类型
之前介绍的是环绕通知,它是功能最为强大的通知,但除此以外还有四种通知类型
@Before - 此注解标注的通知方法在目标方法前被执行
@AfterReturning - 此注解标注的通知方法在目标方法后被执行,有异常不会执行
@AfterThrowing - 此注解标注的通知方法发生异常后执行
@After - 此注解标注的通知方法在目标方法后被执行,无论是否有异常
以@Before注解为例:此注解标注的通知方法在目标方法前被执行。
定义一个切面类:
Aspect2
package com.itboy.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* @Author zhouxiangyang
* @Date 2022/5/12 15:01
*/
@Component
@Aspect
@Slf4j
public class Aspect2 {
//1 前置通知:被@Before注解标注的方法为前置通知方法,在目标方法执行之前执行
@Before("execution(* com.itboy.service.impl.PersonServiceImpl.*(..))")
public void before(JoinPoint jp){
System.out.println("前置通知:before...");
//1 获取方法签名 返回值类型 包名....类名.方法名(参数类型)
MethodSignature signature = (MethodSignature) jp.getSignature();
System.out.println("signature = " + signature);
//2 获取完整的方法信息 修饰符 返回值类型 包名....类名.方法名(参数类型)
Method method = signature.getMethod();
System.out.println("method = " + method);
//3 获取返回值类型
Class returnType = signature.getReturnType();
System.out.println("returnType = " + returnType);
//4 获取参数值
Object[] args = jp.getArgs();
System.out.println("args = " + Arrays.toString(args));
}
}
再去Spring01ApplicationTests类中执行test1方法获得结果
4.4、SpringAop中动态代理的方式
Spring中既支持jdk动态代理,也支持cglib动态代理,默认采用的是cglib动态代理。
Springboot 默认配置 spring.aop.proxy-target-class=true 此时无论目标是否实现接口,都是采用cglib 技术,生成的都是子类代理
如果设置了 spring.aop.proxy-target-class=false,那么又分两种情况 如果目标实现了接口,Spring 会采用jdk 动态代理技术生成代理 如果目标没有实现接口,Spring 会采用cglib 技术生成代理