1 引包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2 aop:面向切面的编程,就是在顺序执行的程序中打断原本顺序,先执行一个插入的程序
3 切面编程可能会使用的地方
monitor监听请求次数、响应时间响应时间
权限管理、表单验证、事务管理、信息过滤、拦截器、过滤器、页面转发
3 aop的优点:减少重复代码,减少对业务代码的入侵
例如我需要监听一个项目里所有方法的执行时间,那么就要在每个方法执行之前做一个当前时间的记录;方法执行之后做一个时间的记录,用2个时间计算当前方法的耗时,这样就要给所有的方法添加时间监控,并且原本的业务代码没有监控,现在回因为添加监控制造一个大的缺陷
4 如何实现aop
创建一个切面的类
import java.util.Date;
public class Halder {
public void startTime(){
System.out.println("开始时间"+new Date());
}
public void endTime(){
System.out.println("结束时间"+new Date());
}
}
5 将该类用myAop.xml文件加入spring容器中托管
<bean id="halder" class="com.longteng.lesson2.my.service.Halder"></bean>
6 创建一个业务类UserDaoTest ,也用myAop.xml加入spring容器中
public class UserDaoTest {
public String add(String name,Integer age){
return name+age;
}
}
<bean id="userDaoTest" class="com.longteng.lesson2.my.UserDaoTest"></bean>
7 在myAop.xml文件中配置aop
<aop:config>
<!--id就是给这个切入的方法起个名字,ref就是要哪个类切入,就是上面的Halder类的beanid-->
<aop:aspect id="aa" ref="halder">
<!--id是触发条件的名字,expression在执行UserDaoTest类的add方法的时候触发Halder切面类介入的表达式-->
<!--括号里第一个星号与包名之间有个空格,add方法里有2个参数所以用两个星号代替,中间用逗号隔开-->
<!--若表达式里的方法有多个参数就用多个星号,若没有参数就不用加任何星号-->
<!--若是把add改成星号,意思就时执行userDaoTest类的任何方法都进入切面,每个方法的参数个数要一致才能这样写-->
<aop:pointcut id="addMethod" expression="execution(* com.longteng.lesson2.my.UserDaoTest.add(*,*))"/>
<!--在触发条件表达式之前执行,执行halder类里的startTime方法,pointcut-ref能触发条件的名字-->
<aop:before method="startTime" pointcut-ref="addMethod"></aop:before>
<!--在触发条件表达式之后执行,执行halder类里的endTime方法,pointcut-ref能触发条件的名字-->
<aop:after method="endTime" pointcut-ref="addMethod"></aop:after>
</aop:aspect>
</aop:config>
8 测试类启动容器,执行测试
public class Test {
ApplicationContext context;
@BeforeClass
public void beforeTest() {
context=new ClassPathXmlApplicationContext("myAop.xml");
}
@org.testng.annotations.Test
public void test(){
UserDaoTest userDaoTest=(UserDaoTest) context.getBean("userDaoTest");
userDaoTest.add("xx",10 );
}
}
9 执行结果
这个时间的打印是从切面类Halder里打印的,证明切面执行成功,可以在方法之前、之后切入
10 获取被切入的是哪个类里的哪个方法
joinPoint.getTarget().getClass().getSimpleName()获取的是被切的类名
joinPoint.getSignature().getName()获取的是被切的方法名
在Halder类中获取
public class Halder {
public void startTime(JoinPoint joinPoint){
System.out.println(joinPoint.getTarget().getClass().getSimpleName()+ "类"+
joinPoint.getSignature().getName()+ "方法开始时间"+new Date());
}
public void endTime(JoinPoint joinPoint){
System.out.println(joinPoint.getTarget().getClass().getSimpleName()+ "类"+
joinPoint.getSignature().getName()+ "方法结束时间"+new Date());
}
}
被切入的类中有3个方法,
add(String name,Integer age)
delete(String name,Integer age)
update(String name,Integer age)
测试类中分别调用这3个方法
public class Test {
ApplicationContext context;
@BeforeClass
public void beforeTest() {
context=new ClassPathXmlApplicationContext("myAop.xml");
}
@org.testng.annotations.Test
public void test(){
UserDaoTest userDaoTest=(UserDaoTest) context.getBean("userDaoTest");
userDaoTest.add("xx",10 );
userDaoTest.delete("qq",12 );
userDaoTest.update("ww",13 );
}
}
前提是myAop.xml文件中切点的方法名用*代替,并且每个方法的参数个数一样,这样被切的类就能每个方法都执行切入
<aop:pointcut id="Method" expression="execution(* com.longteng.lesson2.my.UserDaoTest.*(*,*))"/>
before、after切入的JoinPoint joinPoint参数的方法
joinPoint.getArgs();//获取入参
joinPoint.getTarget();//获取目标类
joinPoint.getSignature().getName();//获取执行方法名
JoinPoint 是个接口,环绕切入的特性不再JoinPoint 里,在另一个接口ProceedingJoinPoint里
11 环绕切入
环绕切入就是将before和after总和,可以决定目标方法是否执行、什么时候执行、执行时是否需要替换方法参数、执行完毕是否需要替换返回值
环绕切入的实现
.xml里配置环绕切入,与before和after相同
<bean id="userDaoTest" class="com.longteng.lesson2.my.UserDaoTest"></bean>
<bean id="halder" class="com.longteng.lesson2.my.service.Halder"></bean>
<aop:config>
<aop:aspect id="aa" ref="halder">
<aop:pointcut id="Method" expression="execution(* com.longteng.lesson2.my.UserDaoTest.*(*))"/>
//环绕切入用around
<aop:around method="aroundTest" pointcut-ref="Method"></aop:around>
</aop:aspect>
</aop:config>
要切入的Halder类
public class Halder {
/**
* 目标类UserDaoTest的adds方法的返回类型是List,
* 所以用Object类型接收,入参是ProceedingJoinPoint类型
* 因为ProceedingJoinPoint类型才能使用环绕通知的特性
* **/
public Object aroundTest(ProceedingJoinPoint proceedingJoinPoint){
/**
* 获取入参,因为参数默认是数组,用下标为0获取第一个入参,
* 因为返回的是Object类型,所以用Object类型接收
* **/
Object objectArgs=proceedingJoinPoint.getArgs()[0];
//将获取的入参强转为String类型
String s=(String) objectArgs;
//打印没有执行环绕通知之前从外部传入的参数
System.out.println(s+"$$$$$$");
//定义个Object类型的数组接收获取到的参数
Object[] args=proceedingJoinPoint.getArgs();
//修改没有执行环绕通知之前从外部传入的参数,
// 将获取到的参数修改为“环绕切入之前修改的入参”
args[0]="环绕切入之前修改的入参@@@@@@@@@@";
try {
/**
* 真正执行目标类里的切点adds方法之前执行proceed方法,
* 就是说proceedingJoinPoint.proceed();这句话之前执行的相当于after
* 就是上面执行的获取从外部传入的参数、执行环绕通知前修改参数
* proceedingJoinPoint.proceed();这句话之后执行的相当于after
* 就是执行了环绕通知之后修改了返回参数
* 就是说在环绕通知之前可以做什么?
* 1 可以决定目标方法是否执行
* 2 什么时候执行
* 3 执行时是否需要替换方法参数
* 4 等等其他操作
* 就是说在环绕通知之后可以做什么?
* 修改执行之后原本的返回参数
* 本来的返回参数是“环绕切入之前修改的入参@@@@@@@@@@”
* 现在修改为“修改返回的参数##########”
* 执行环绕通知时将执行前修改的参数传递到proceed方法
* 执行环绕通知之后返回的是一个Object类型,用Object类型接收
* **/
Object x=proceedingJoinPoint.proceed(args);
//打印执行环绕通知之后的返回参数
System.out.println(x+"执行环绕通知之后的返回参数");
//将返回的Object类型转为List类型
List list=(List)x;
//清除该list,修改环绕通知执行后的返回值
list.clear();
list.add("修改返回的参数##########");
return x;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
}
被切入的目标类
public class UserDaoTest {
public List adds(String s){
List list=new ArrayList();
list.add(s);
return list;
}
}
测试类
public class Test {
ApplicationContext context;
@BeforeClass
public void beforeTest() {
context=new ClassPathXmlApplicationContext("myAop.xml");
}
@org.testng.annotations.Test
public void test(){
UserDaoTest userDaoTest=(UserDaoTest) context.getBean("userDaoTest");
List list=userDaoTest.adds("环绕通知的传参!!!!!!!");
//获取最终返回的参数,就是执行环绕通知之后被修改的参数
list.forEach(l-> System.out.println(l));
}
}
整个请求链路
进入测试类——执行初始化方法——将xml文件全部读完——执行测试方法——获取bean——调用被切目标类的adds方法(执行切入前)——获取被切目标类的参数(就是测试类传入的参数)、修改该传参——将改变的参数传入被切目标类(就是业务类)——执行完成,拿到返回——改变返回——执行测试类遍历返回并打印
用环绕通知实现监控方法的执行时间就更方便了
在Object x=proceedingJoinPoint.proceed(args);之前、之后分别执行一句System.currentTimeMillis();即可,就会方便很多
aop在实际工作中如何使用
录制线上数据,版本上线前做录制回放,省去了回归测试,包括回归的测试用例
loadrunner、jmeter都是录制前端的请求
fiddler、F12、还有其他抓包工具也是录制的前端的请求
共同特性就是录制前端发往服务器的请求
能录制到的范围是浏览器到控制层的请求,不能录制的范围是控制层到各个应用服务的请求,例如:搜索是一个单独服务、订单是一个单独服务、商品是一个单独服务、售后是一个单独服务
Java agent:统计代码覆盖率的工具
jvm sendbox(是一个阿里开源项目):实现线上录制,线下回放的工具,(解决了每次上线前的手动回归)
关于jvm sendbox的一个连接:https://blog.csdn.net/brucehurrican/article/details/79176468
12 AfterThrowing异常通知
业务类UserDaoTest 创建了一个throwTest的带参方法,并主动创建了一个空指针的异常
public class UserDaoTest {
public void throwTest(String s){
throw new NullPointerException();
}
}
切面类Halder 判断业务类的参数ex是不是空
import org.aspectj.lang.JoinPoint;
public class Halder {
/**
* 切面类创建一个切入的方法throwingTest
* 有2个参数,JoinPoint参数一定要在前面
*/
public void throwingTest(JoinPoint joinPoint,Throwable ex){
/**
* instanceof关键字的用法
* A instanceof B ,
* 返回值为boolean类型,
* 用来判断A是否是B的实例对象或者B子类的实例对象。
* 如果是则返回true,否则返回false。
* 这里的意思是:ex是不是一个空对象,如果是空对象就打印下面这句话
* **/
if(ex instanceof NullPointerException ){
System.out.println("空指针异常!");
}
}
}
xml中的配置
<!--将业务类、切面类加入spring容器中托管-->
<bean id="userDaoTest" class="com.longteng.lesson2.my.UserDaoTest"></bean>
<bean id="halder" class="com.longteng.lesson2.my.service.Halder"></bean>
<aop:config>
<!--创建切面,id:切面的名字,ref:将哪个bean作为切面-->
<aop:aspect id="aa" ref="halder">
<!--触发条件的名字,触发条件:执行业务类的throwTest方法,
一定要确定触发条件的方法是否带参,是否多参数,第一个星号要与表达式的包邮个空格-->
<aop:pointcut id="Method" expression="execution(* com.longteng.lesson2.my.UserDaoTest.throwTest(*))"/>
<!--异常要用after-throwing,
切入的方法是切面类Halder的throwingTest方法,
throwing的值一定要与切面类Halder的throwingTest方法的第二个参数Throwable的名字一致-->
<aop:after-throwing method="throwingTest" throwing="ex" pointcut-ref="Method"/>
</aop:aspect>
</aop:config>
测试类
public class Test {
ApplicationContext context;
@BeforeClass
public void beforeTest() {
context=new ClassPathXmlApplicationContext("myAop.xml");
}
@org.testng.annotations.Test
public void test(){
UserDaoTest userDaoTest=(UserDaoTest) context.getBean("userDaoTest");
userDaoTest.throwTest("xx");
}
}
测试类启动容器,调用业务类的throwTest方法,发生空指针,这个异常是发生的时候才会被打印,若是被catch住了,就等于没有异常,就不会被打印
13 AfterReturning后置返回通知获取返回值
业务类
public class UserDaoTest {
public String AfterReturn(String s){
return s;
}
}
切面类
import org.aspectj.lang.JoinPoint;
public class Halder {
public void doAfterReturningAdvice1(JoinPoint joinPoint,Object keys){
System.out.println(joinPoint.getTarget().getClass()+"类" +
joinPoint.getSignature().getName()+"方法获取到的返回值:"+keys);
}
}
xml文件
<!--将业务类、切面类加入spring容器中托管-->
<bean id="userDaoTest" class="com.longteng.lesson2.my.UserDaoTest"></bean>
<bean id="halder" class="com.longteng.lesson2.my.service.Halder"></bean>
<aop:config>
<aop:aspect id="aa" ref="halder">
<!--添加切点-->
<aop:pointcut id="Method" expression="execution(* com.longteng.lesson2.my.UserDaoTest.AfterReturn(*))"/>
<!--method是Halder类的方法,
pointcut-ref是切点名称,
returning是切面类方法中doAfterReturningAdvice1方法的第二个参数名-->
<aop:after-returning method="doAfterReturningAdvice1" pointcut-ref="Method" returning="keys"></aop:after-returning>
</aop:aspect>
</aop:config>
测试类
public class Test {
ApplicationContext context;
@BeforeClass
public void beforeTest() {
context=new ClassPathXmlApplicationContext("myAop.xml");
}
@org.testng.annotations.Test
public void test(){
UserDaoTest userDaoTest=(UserDaoTest) context.getBean("userDaoTest");
userDaoTest.AfterReturn("XX");
}
}
测试类调用业务类的AfterReturn方法,能获取到返回参数
这里需要注意的是:
1 如果参数中的第一个参数为JoinPoint,则第二个参数为返回值的信息
2 如果参数中的第一个参数不为JoinPoint,则第一个参数为returning中对应的参数
returning 限定了只有目标方法返回值与通知方法相应参数类型时才能执行后置返回通知,否则不执行,也就是说如果你返回的是个MAP类型,然后aop定义的String类型,不执行returning对应的通知方法参数为Object类型将匹配任何目标返回值
3 就是切面类中的方法只有一个参数时,用Object类型
14 Declare-parents
假设有一个已知的API,我们不能修改其类,只能通过外部包装。但是如果通过之
前的AOP前置或后置通知,又不太合理,最简单的办法就是实现某个我们自定义的
接口,这个接口包含了想要添加的方法。
已知的api接口、接口实现类,不能修改
接口
public interface Chinese {
void Say();
}
接口实现类
public class Li implements Chinese {
public void Say() {
System.out.println("我是中国人!"); }
}
自定义接口
public interface Add {
void Todo();
}
自定义接口实现类
public class DoSomething implements Add {
public void Todo() {
System.out.println("我爱中国!"); }
}
xml文件
<!--已知的不能修改的接口实现类、自定义接口实现类添加为bean-->
<bean id="doSomething" class="com.longteng.lesson2.my.myaop.DoSomething"></bean>
<bean id="li" class="com.longteng.lesson2.my.myaop.Li"></bean>
<aop:config>
<aop:aspect>
<!--types-matching的值是不能修改的接口实现类
implement-interface的值是自定义接口名
default-impl是自定义接口的实现类-->
<aop:declare-parents types-matching="com.longteng.lesson2.my.myaop.Li"
implement-interface=" com.longteng.lesson2.my.myaop.Add"
default-impl="com.longteng.lesson2.my.myaop.DoSomething"/>
</aop:aspect>
</aop:config>
测试类
public class Test {
ApplicationContext context;
@BeforeClass
public void beforeTest() {
context=new ClassPathXmlApplicationContext("myAop.xml");
}
@org.testng.annotations.Test
public void test(){
//不能修改的接口类型(其实就是不能修改的接口实现类的实例)获取的是li这个bean,调用Li接口实现类的Say()方法
//重写的接口类型(其实就是重写的接口实现类的实例)获取的也是li这个bean,调用Add接口实现类的Todo()方法
Chinese li=(Chinese) context.getBean("li");
li.Say();
Add li2=(Add) context.getBean("li");
li2.Todo();
}
}
这样既没有原本的Chinese接口与Chinese的接口实现类Li,但是还让这个接口包含了原本没有的Todo()方法