目录
AOP面向切面编程
作用:在不改动源码的情况下进行功能增强
应用:
- 在工程运行慢的过程中,对目标方法进行耗时分析
- 对目标方法添加事务管理
- 对目标方法添加权限访问控制
- ...
AOP的核心概念
- 连接点:程序执行过程中的任意位置,大多数为一个方法的执行
- 切入点:匹配连接点的式子
- 在springAOP中,一个切入点可以只描述一个方法,也可以匹配多个方法
- 通知:在切入点处执行的操作,也就是增强的内容
- 切面:描述通知与切入点的对应关系
AOP的简单入门案例:测试一个方法使用的时间
第一步:导坐标
html<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency>
第二步:编写SpringConfig配置类
java@Configuration @ComponentScan("com.hhh") public class SpringConfig { }
第三步:编写dao层
java
//让这个类交给spring管理,变成bean
@Repository
public class UserMapperImpl implements UserMapper {
@Override
public void save() {
System.out.println("UserMapper dao");
}
}
第四步:编写通知类,并导入AOP所需要的坐标
html<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.7</version> </dependency>
java
//对谁增强
//什么时候增强
//增强什么东西
@Component
@Aspect
public class MyAdvice {//通知类
private long begin;
private long end;
//对谁做增强
@Pointcut("execution(void com.hhh.dao.UserMapper.save())")
public void point(){
}
//什么时候增强,增强什么内容
@Before("point()")
public void before(){
begin=System.currentTimeMillis();
System.out.println(begin);
}
@After("point()")
public void after(){
end=System.currentTimeMillis();
long result=end-begin;
System.out.println(end);
System.out.println("花费的时间为"+result);
}
}
我们对这个类使用了两个注解
@Component把这个类交给spring管理
@Aspect告诉spring这是一个通知类
增强我们要知道对谁增强,什么时候增强,增强什么
对谁增强:定义一个point方法,并加上注解@Pointcut
"execution(void com.hhh.dao.UserMapper.save())"这个就是一个切点表达式,也就是切入点,切入点要写的有方法返回的类型,以及方法所在的位置。通过切入点我们也可以发现UserMapper类的save()方法就是连接点。这样一来point()方法就代表了save()方法
什么时候增强:使用注解@Before和@After
增强什么东西:
在before(),after()方法进行增强内容的书写,书写的代码内容就是通知
注解加上方法就是切面-->描述了切入点和通知的关系
第五步:让spring开启AOP功能
写完通知类之后,还要再SpringConfig配置类中加上注解@EnableAspectJAutoProxy
java
@Configuration
@ComponentScan("com.hhh")
//开启AOP功能
@EnableAspectJAutoProxy
public class SpringConfig {
}
测试:
java
public class UserMapperTest {
@Test
public void test(){
//加载配置类,创建IoC容器
ApplicationContext ctx=new AnnotationConfigApplicationContext(SpringConfig.class);
//UserMapper userMapperImpl = (UserMapper) ctx.getBean("userMapperImpl");
UserMapper userMapper = ctx.getBean(UserMapper.class);
//这里不能使用UserMapperImpl.class,因为在容器里的bean是实现类的代理类型
userMapper.save();
}
}
结果:
错误分析:
如果没有出现增强的内容
去看一下通知类有没有加@Component交给spring管理,有没有加@Aspect,切入点表达式有没有问题,SpringConfig配置类有没有加上注解 @EnableAspectJAutoProxy**,开启AOP功能**
AOP的工作流程
1.Spring容器启动
2.读取 所有切面配置中的切入点
3.初始化bean,判定bean对应的类中方法是否匹配到任意切入点
- 匹配失败,创建对象
- 匹配成功,创建原始对象的代理对象,成为目标对象
4.获取bean执行方法
- 获取bean,调用方法并执行,完成操作
- 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法和增强的内容,完成操作
AOP切入点表达式
execution(void com.hhh.dao.UserMapper.save(int))
- 动作关键字:描述切入点的行为动作,例如execution
- 访问修饰符:public,private-->可以省略
- 返回值
- 报名
- 类/接口名
- 方法名
- 参数
- 异常名:方法定义中抛出指定异常,可以省略
使用通配符描述切入点
* : 单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
.. : 多个连续的任意符号,可以独立出现 ,常用于简化包名与参数名的书写
如:
@Pointcut("execution(* com.hhh.dao.*Mapper.*(..))")
返回类型使用*替代,*Mapper(UserMapper等)文件下的所有方法都作为连接点,而方法参数使用..
- : 专用于匹配子类类型
@Pointcut("execution(* com.hhh.dao.*Mapper+.*(..))")
包括了所有*Mapper文件的所有子类(即实现类等)
AOP通知类型
- 前置通知-->@Before
- 后置通知-->@After
- 环绕通知 -->@Around
- 返回后通知-->@AfterReturning(原方法执行完后执行)
- 返回异常后通知-->@AfterThrowing 和返回后通知只有一个能执行
java
@Component
@Aspect
public class MyAdvice {//通知类
//对谁做增强
@Pointcut("execution(* com.hhh.dao.*Mapper+.*(..))")
public void point(){
}
//什么时候增强,增强什么内容
@Before("point()")
public void before(){
System.out.println("before");
}
@After("point()")
public void after(){
System.out.println("after");
}
//环绕通知
@Around("point()")
public void around(ProceedingJoinPoint jointPoint) throws Throwable {
System.out.println("around before");
//执行被代理的方法(原方法)
jointPoint.proceed();
System.out.println("around after");
}
//返回后通知即原方法执行完后执行
@AfterReturning("point()")
public void afterReturning(){
System.out.println("afterReturning");
}
//返回异常后通知
@AfterThrowing("point()")
public void afterThrowing(){
System.out.println("afterThrowing");
}
}
结果:
@Around注意事项
1.环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后增强
2.通知中如果未使用ProceedingJoinPoint对原始方法的调用,将跳过原始方法的执行
3.对原始方法的调用不接收返回值 ,通知方法设置成void即可,如果接收返回值,必须设置成Object类型,并返回
4.原始方法的返回值如果是void类型,通知方法返回值的类型可以设置为void,也可以为Object
5.由于无法预知原始方法运行后是否会抛出异常,因此环绕方法必须抛出Throwable对象
如:
java@Override public Integer update() { System.out.println("UserMapper update"); return 20; }
让原始方法返回20整型值
javapublic class UserMapperTest { @Test public void test(){ //加载配置类,创建IoC容器 ApplicationContext ctx=new AnnotationConfigApplicationContext(SpringConfig.class); //UserMapper userMapperImpl = (UserMapper) ctx.getBean("userMapperImpl"); UserMapper userMapper = ctx.getBean(UserMapper.class); Integer i = userMapper.update();//接收返回值并打印 System.out.println(i); } }
结果:
可以发现无法拿到返回值,因为在@Around环绕通知里没有返回原方法的值
所以进行修改
java//环绕通知 @Around("point()") public Object around(ProceedingJoinPoint jointPoint) throws Throwable { System.out.println("around before"); //执行被代理的方法(原方法) Object proceed = jointPoint.proceed();//接收原方法的返回值 System.out.println("around after"); return proceed;//返回 }
结果:
我们也可以在通知中对原方法的返回值进行修改
java//环绕通知 @Around("point()") public Object around(ProceedingJoinPoint jointPoint) throws Throwable { System.out.println("around before"); //执行被代理的方法(原方法) Integer proceed = (Integer) jointPoint.proceed(); System.out.println("around after"); proceed=100;//修改 return proceed; }
使用@Around环绕通知计算某个方法执行一万次的时间
java//计算某个方法执行一万次的时间 @Around("point()") public Object around(ProceedingJoinPoint jointPoint) throws Throwable { //获取签名 Signature signature = jointPoint.getSignature(); //通过签名获取接口名 String typeName = signature.getDeclaringTypeName(); //通过签名获取执行方法的方法名字 String name = signature.getName(); long begin=System.currentTimeMillis(); Integer proceed=null; for(int i=0;i<10000;i++){ proceed = (Integer) jointPoint.proceed();//执行原方法 } long end=System.currentTimeMillis(); System.out.println(typeName+"."+name+"执行一万次的时间为"+(end-begin)+"ms"); return proceed; }
javapublic class UserMapperTest { @Test public void test(){ //加载配置类,创建IoC容器 ApplicationContext ctx=new AnnotationConfigApplicationContext(SpringConfig.class); //UserMapper userMapperImpl = (UserMapper) ctx.getBean("userMapperImpl"); UserMapper userMapper = ctx.getBean(UserMapper.class); Integer i = userMapper.update(); System.out.println(i); } }
结果:
AOP通知获取原方法的数据
获取切入点方法的参数
- JointPoint:使用于前置,后置,返回后通知,抛出异常后通知
- ProceedJointPonit:适用于环绕通知
java@Override public Integer update(String name,Integer age) { System.out.println("name="+name+" "+"age="+age); System.out.println("UserMapper update"); return 20; }
通知类
java@Component//交给IoC容器管理 @Aspect//这是一个通知类 public class MyAdvice2 { //写切入点表达式 @Pointcut("execution(* com.hhh.dao.UserMapper.update(..))")//参数里面要写.. public void point(){ } @Before("point()") public void before(JoinPoint joinPoint){ //获取原方法的参数 Object[] args = joinPoint.getArgs(); System.out.println("before 参数为"+Arrays.toString(args)); } @Around("point()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { //获取签名 Signature signature = joinPoint.getSignature(); //获取执行方法的接口 String typeName = signature.getDeclaringTypeName(); //获取方法名 String name = signature.getName(); //获取方法的参数 Object[] args = joinPoint.getArgs(); System.out.println(typeName+"."+name+"的参数为"+ Arrays.toString(args)); //让原方法运行 Object proceed = joinPoint.proceed(); //返回原方法的返回值 return proceed; } }
测试:
javapublic class UserMapperTest { @Test public void test(){ //加载配置类,创建IoC容器 ApplicationContext ctx=new AnnotationConfigApplicationContext(SpringConfig.class); //UserMapper userMapperImpl = (UserMapper) ctx.getBean("userMapperImpl"); UserMapper userMapper = ctx.getBean(UserMapper.class); Integer i = userMapper.update("hhh",19); System.out.println(i); } }
结果:
获取切入点方法返回值
- 返回后通知
- 环绕通知
java@Component @Aspect public class MyAdvice3 { @Pointcut("execution(* com.hhh.dao.UserMapper.update(..))") public void point(){} @AfterReturning(value = "point()",returning="num") public void afterReturning(Integer num){//通过参数获取原方法的返回值 System.out.println("原方法的返回值为"+num); } @Around("point()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { //获取原方法的返回值 Object proceed = joinPoint.proceed(); return proceed; } }
结果:
获取切入点方法运行异常信息
- 抛出异常后通知
- 环绕通知
java@Component @Aspect public class MyAdvice4 { @Pointcut("execution(* com.hhh.dao.UserMapper.update(..))") public void point(){} @AfterThrowing(value = "point()",throwing = "t") public void throwing(Throwable t){ System.out.println("AfterThrowing的"+t); } @Around("point()") public Object around(ProceedingJoinPoint joinPoint){ Object proceed=null; try { proceed = joinPoint.proceed(); } catch (Throwable e) { System.out.println("around的"+e);//捕获原方法的异常 } return proceed; } }
在原方法写一个异常
java@Override public Integer update(String name,Integer age) { Integer i=1/0; System.out.println("name="+name+" "+"age="+age); System.out.println("UserMapper update"); return 20; }
结果:
小案例:去除密码空格
java@Service//交给Spring管理,变成bean public class ResourceServiceImpl implements ResourceService { @Override public void save(String url, String password) { System.out.println("url="+url); System.out.println("password="+password); //验证有没有去除空格 System.out.println(password.equals("1234")?"现在密码没有空格":"现在密码有空格"); } }
通知类:
java@Component @Aspect public class MyAdviceForResource { @Pointcut("execution(* com.hhh.service.ResourceService.save(..))") public void point(){ } @Around("point()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("around"); //先获取原方法的参数 Object[] args = joinPoint.getArgs(); //这里不能使用增强for循环,因为增强for循环只能用于遍历,不能改变原参数的值 for(int i=0;i<args.length;i++){ if(args[i].getClass().equals(String.class)){//判断参数是不是String类型 args[i]=args[i].toString().trim();//如果是String类型就去空格 } } //执行原方法 Object proceed = joinPoint.proceed(args);//重新传参 return proceed; } }
测试
javapublic class ResourceServiceTest { @Test public void test(){ //加载配置类,初始化IoC容器 ApplicationContext ctx= new AnnotationConfigApplicationContext(SpringConfig.class); ResourceService bean = ctx.getBean(ResourceService.class); bean.save("http://localhost:8080/database"," 1234 "); } }
结果: