一)什么是 AOP?以及AOP的组成?
AOP:面向切面编程,它是一种编程思想,是针对某一类事情的集中处理,它能够在不修改原有代码的情况下针对功能进行增强,就是代码已经写好了,使用AOP能够在不修改原有代码的情况下进行功能的增强,对于一个功能,可以基于AOP完成对该功能执行效率的计算,能够在功能正式执行前或者是执行以后添加其他的功能执行,能够在该功能发生异常以后,对异常进行处理;
OOP:面向对象编程
AOP:面向切面编程,和咱们之前写的封装公共方法的思想是十分类似的,某一方面功能是相同的,就是某一方面,相当于加上一层拦截,在所有要定位到用户登录验证的方法都是要进行拦截的,统一进行功能处理,无需进行登录授权验证的方法就直接过,符合拦截的URL都要进行登录校验,通用性+这些代码只是一个登录校验,和接下来的业务代码没啥关系
如果说在进行统一功能处理的时候发现用户登录校验通过了(直接写一个类进行校验),那么还是可以正常的访问Controller里面的方法,但是如果统一功能处理不通过的话,直接打回前端,不可以访问后端Controller的代码;
1)AOP:是面向切面编程,它是一种思想,它是对某一类事情的集中处理,比如说咱们之前学过用户登录权限的校验,从这个角度来说就是对登录验证的集中处理,没有学AOP之前,所有需要判断用户登陆的页面的方法都要各自实现或者调用用户的登陆验证的方法,就会降低开发效率,咱们在进入博客列表页,博客详情页,博客编辑页都要进行对用户是否进行登录做出验证,但是当你的功能越来越多,你所要写的登录验证也是越来越多的,但是这些方法又是相同的,这么多的方法增加代码修改和维护的成本,对于这种功能统一,况且使用地方较多的功能,我们就可以考虑使用AOP来进行处理了,成千上万个Controller都要写登录拦截功能,很麻烦;
2)AOP是一种思想,但是SpringAop是一个框架,提供了一种对于AOP思想的实现,他们的关系类似于IOC和DI的类似,针对程序中的某一类或者某一个方面做一下集中的处理的框架,否则针对每一个Controller都要写一遍登录验证就十分麻烦;
3)这些用户登录的方法和接下来要实现的业务几乎没有任何关联,比如说编写博客,但在每一个方法里面都要写一遍,所以说需要提供一个公共的AOP方法来进行统一用户进行登陆操作的权限迫在眉睫况且代码很复杂,每一个重要的方法都要去进行登陆权限校验;
4)当学习了AOP之后,我们只需要在某一处进行配置一下,那么这个时候所有进行判断用户登录页面的方法就可以实现用户登陆验证了,我们就不需要在每一个方法里面都要写相同的用户登陆验证了
5)所以说在后面的具体业务实现的时候,我们是看不到具体的用户登录权限认证的代码的;
也就是说AOP可以扩充多个对象的某个能力,所以说AOP可以说是OOP面向对象编程的补充和完善
除了做可以进行用户统一登录验证的功能外,还可以做以下事情:
1)统一的日志记录;
2)统一方法接口的执行时间,看看对哪一个接口请求执行时间长从而进行优化;
3)统一的返回格式处理,统一返回HashMap,方法返回的是int,但是AOP框架程序会打包成一个JSON格式的数据返回给前端;
4)统一的异常处理,后端出现错误了,代码出现异常了,前端是无法进行感知的,因为后端没有将这个异常信息打包成JSON发送给前端,前端只能看到500这个错误,前端的ajax里面的success是不会继续向下执行的,可以对当前项目中的所有异常做一个拦截,只要程序出现500,立马就可以感知到,把这些异常信息封装成JSON格式的数据,然后把这个JSON返回给前端,最终返回的JSON格式的异常信息字段message给前端,success就可以往下走了,保证项目的稳定性;
5)自动开启事务的开启和提交;
也就是说使用SpringAop可以进行扩充多个对象的某一个能力,所以AOP可以说是OOP的补充和完善,他可以横切关注点从应用程序的主业务逻辑中分离出来,使得这些关注点可以集中处理,从而提升代码的复用性,可维护性和系统的可扩展性
SpringAOP学习主要是分成以下三个部分:
1)学习AOP是如何组成的,也就是AOP组成的相关概念
2)学习Spring AOP的使用
3)学习Spring AOP的底层实现原理
二)AOP面向切面编程的组成:
AOP就是针对某一方面功能做统一的处理,这个方面就叫做切面,一个切面包含多个切点
1)切面(Aspect):通俗来讲,是指哪一方面的意思,针对某一方面的功能做统一的处理,AOP是面向切面编程,那么这个切面,是指哪一方面呢?到底是哪一个功能?这里面的切面指的是登录权限的校验
1.1)切面是定义AOP是针对统一哪一个统一的功能的,这一个功能就叫做一个切面,比如说用户登录权限的校验或方法的统计日志,他们就各自是一个切面,是由切点+通知组成的
1.2)切面是包含了通知和切点,相当于AOP实现的某个功能的集合,针对于某一个功能的具体定义,某一个功能可能是登录验证功能,也有可能是日志记录功能,还是统计方法的执行时间?一个功能就对应一个切面,相当于是业务系统的数据库,博客系统有博客系统的数据库,在线OJ有在线OJ的数据库,换个角度来说,切面可以理解为,到底是页面登陆的AOP呢还是记录日志功能的AOP呢?这就是切面;
1.3)如果说你是实现用户登录授权的,用户登录授权的这个统一的判定就是叫做一个切面;
2)连接点(joincut):应用程序执行过程中可以插入切面的一个点,这个点可以是方法调用时,抛出异常时,甚至修改字段时,切面代码可以利用这些点插入到应用程序的正常流程中,并添加新的行为,是切面中的某一个方法,要被多个Controller所使用
连接点就相当于是所有方法的集合,相当于是需要被增强的某一个AOP功能的所有方法,可以触发AOP的这些点,再比如说咱们要进行登录权限的验证的切面,程序里面有很多方法,现在项目里面有100个方法,其中有98个方法需要进行登录权限的校验,那么这98个方法都是连接点,所有可能触发AOP拦截方法的点就称之为连接点,无论是否有AOP,那么连接点始终都是存在的;
3)切点(pointcut):提供匹配拦截规则,提供一组规则用于匹配AOP拦截规则,描述哪些方法需要被拦截,切点来匹配连接点的,让连接点来进行触发规则的,定义AOP拦截的规则,最终的作用就是让哪些连接点触发拦截,哪些方法进入到拦截器实施通知方法,哪一个接口走登录授权验证呢?哪几个接口不需要走登录权限的验证呢?
4)通知:切面也是有目标的,描述了切面要完成的工作,规定了切面什么时候执行,还解决了什么时候执行这个工作的问题,切点就是一个空壳,但是通知就是执行具体的业务逻辑,是咱们进行拦截的具体方法实现,规定AOP执行时机还是执行的方法
再进一步说就是定义了切面是什么,何时使用,以及描述了切面要完成的工作,还解决是何时完成这个工作的问题,在Spring切面类里面,可以在方法上面使用这个注解,会设置方法是通知方法,在满足拦截条件后会通知本方法进行调用;
前置通知:使用@Before,通知方法在目标方法调用之前执行,所有调用该方法之前的页面先调用前置通知的方法,在执行目标方法之前执行的方法就叫做前置通知;
后置通知:使用@After,通知方法会在目标方法返回或者抛出异常之后调用,方法执行完记录日志;
返回数据之后通知:使用@AfterReturning通知方法在目标方法返回之后调用;
抛出异常之后通知:使用@AfterThrowing,通知方法会在目标方法抛出异常之后调用,记录错误日志给运维人员或者是开发人员;
环绕通知:使用@Around,包裹通知的方法,会在被通知的方法执行之前和调用之后执行自定义的行为
1)当想要再执行时间的记录下,有的人可能会说在前置通知记录一个时间戳,在后置通知的情况下记录一个时间戳,直接相减不就可以了吗?但是不适用于多线程环境下面;
2)此时就可以使用其环绕通知;
通知目标:当连接点要调用我们的方法的时候,进行的行为
下面是实现登陆验证的AOP的一个功能
1)这里面的切面就是登录权限的校验的完整功能的这个事情;
2)连接点就是这些触发登陆权限验证的url,比如说访问博客列表页的url;
3)切点规定哪些url可以触发通知,哪些url可以触发登录权限的校验,那些url不能触发登录权限的校验,这都是切点描述的,就是一个方法体,没有实现;
4)通知就是具体的实现,拿到session里面的httpSession就是拿键值对的一个过程,判断session里面的值是否为null;
SpringAOP的实现(是Spring的一个功能模块)
1)要基于SpringAOP框架来进行实现一下AOP的功能,完成的目标是拦截所有UserController里面的方法,每一次进行调用UserController里面的任意一个方法的时候,都进行执行相应的通知事件;
2)那么我们进行执行SpringAop的实现步骤如下:
2.1)添加SpringAOP框架支持
2.1.1)项目创建之初就要添加SpringAOP的框架;
2.1.2)项目生产环境中写代码的时候就需要用到这些框架;
<!--http://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
我们是可以删除SpringAOP的版本号,因为在SpringBoot内部内置了和SpringBoot版本对应的AOP的版本,这种框架可以直接去maven中央仓库中进行查找;
2.2)定义切面和切点,创建方法,有方法的声明,他们都没有具体的实现
2.3)定义通知
三)定义切面和切点
//1.切面,针对的是哪一方面 @Aspect @Component public class CommonData { //2.切点,定义拦截规则 @Pointcut("execution(* com.example.demo.Controller.UserController.*(..))") public void start(){} //3.各种通知: @Before("start()") public void before(){ System.out.println("开始执行前置通知"); } @After("start()") public void after(){ System.out.println("开始执行后置通知"); } @AfterReturning("start()") public void afterReturning(){ System.out.println("开始执行方法返回后通知"); } @AfterThrowing("start()") public void afterThrowing(){ System.out.println("开始执行方法异常后通知"); } @Around("start()") public void around(ProceedingJoinPoint point){ try { System.out.println("环绕通知前的方法"); point.proceed(); System.out.println("环绕通知以后的方法"); } catch (Throwable e) { throw new RuntimeException(e); } } }
想要查找SpringBoot项目常见框架的版本号,点击这两个即可:
先打开pom.xml,再来进行点击dependencies标签,最后就找到了常见框架的版本号
1)切点指的是要具体的处理哪一类问题,我们要在切点定义拦截的规则;
2)声明了@Aspect声明这个类是切面类,而切面类是一个包含了切点,通知的类,在这个类里面我们用一个空方法表示切点;
3)加上@PointCut注解指定切点要拦截的规则,切面和切点是一对多的关系,切点和通知是一对多的关系;
4)再用@Before()注解修饰一个方法表示一个通知,里面要指定一个参数是修饰切点的那一个空方法声明,在这个类里面就可以写进行拦截的具体方法了
@Aspect @Component//注意一定要将这个类注入到SpringIOC容器里面 public class SpringAop { @Pointcut("execution(* com.example.demo.Controller.UserController.*(..)") //定义拦截规则,来进行定义切点 public void start(){ //下面我们要定义拦截通知,来进行描述拦截执行时机和咱们的具体方法实现的 } @Before("start()") //因为切点是由多个的,随意我们要写他归属于哪一个切点,咱们的切点名是固定的,但是他一定要和我们的通知的所归属的切点要保持一致 public void run() { System.out.println("执行了前置通知"); } }
1)定义切面:@Aspect,他是一个切面注解,是用来修饰类的,是用来定义一个切面的,表示整个拦截的一个过程,切面的定义就是包含了通知,切面和切点的类,是一个传统的类注解,表示整个功能实现一个过程,整个拦截过程都是在类里面实现的;所以它是一个类注解,用@Aspect表示这是一个切面类,而这个类我们还要放在Spring框架里面,所以我们还需要加上@Component注解,表示把这个类注册到Spring里面,因为在Spring框架里面要启动切面类,进行拦截;
2)定义切点:@Pointcut("execution(* com.example.demo.Controller.UserController.*(..)"),我们最外边的类是一个切面,那么这个start方法就是一个切点,里面要定义我们的拦截规则,这个方法里面是没有实现的,具体的拦截实现我们要在通知里面实现
总结:切点注解修饰的类是一个空方法,他是不需要有方法体的,此方法名就是起到一个标识的作用,标识下面的通知方法是属于哪一个切点,因为切点很可能有很多个,咱们必须指定该通知方法是属于哪一个切点
3)定义通知:用五大注解@Before来进行定义通知,里面要指定是属于哪一个切点的,不同的切点,要进行拦截的类不同,拦截的方法不同,执行的业务逻辑也就不会相同;
下面我们再来写一段代码来进行演示一下:
我们在浏览器上面进行访问的URL:
http://127.0.0.1:8081/Java100?username=李佳伟&password=12503487
写的代码结构如上:
一:咱们的UserController里面的代码:
@Controller public class UserController { @RequestMapping("/Java100") @ResponseBody public String run(String username,String password) { return username+password; } }
二:咱们的SpringAOP里面的代码:
@Aspect @Controller public class SpringAOP { @Pointcut("execution(* com.example.demo.Controller.UserController.*(..))") public void start(){ } //@Bafore表示通知执行的时间,在执行目标方法之前执行这个通知 @Before("start()")----里面要和具体的切点关联起来 public void StartRun() { System.out.println("执行了前置通知"); } @After("start()") public void AfterRun() { System.out.println("执行了后置通知"); } @AfterReturning("start()") public void AfterReturning() { System.out.println("执行了后置方法幼"); } @Around("start()") public Object Around(ProceedingJoinPoint joinPoint) { System.out.println("执行环绕通知的前置方法"); Object obj; try { obj=joinPoint.proceed();//执行通知方法 System.out.println("执行环绕通知的后置方法"); return obj; } catch (Throwable throwable) { throwable.printStackTrace(); } return null; } }
还要注意,在写环绕通知的时候,一定要写上返回值
环绕通知包括了整个方法的执行过程,点击下面白色的+号可以观测变量的值;
我们还可以通过打断点来观测整个所有通知的执行流程;
1)@Aspect表名当前类是一个切面
2)@Component注解,表名当前的类要注册到Spring里面
3)@Pointcut注解,它的作用是表示有哪些连接点可以访问到这个切点,也就是说可以拦截哪些方法的路径,里面有一个execution属性:
第一个*表示任意返回值的方法都进行拦截,写完这个,加上空格;
第二个表示包名.类名,表示要进行拦截的具体位置;(表示要拦截哪一个类)
第三个.*表示拦截这个类里面所有的方法,下面就表示拦截UserController里面所有的方法
第四个(..)表示拦截匹配所有的参数,还可以这么写(String,int),表示不定参数
@Point("exectution(* com.example.demo.controller.UserController.*(..))")
1)这个代码就表示要拦截com包底下的example包底下的的demo包底下的UserController类下的任意返回值的任意方法;
2)切点,这个注解修饰的方法是空方法,是不需要有方法体的,这个方法名只是起到一个标识的作用,标识下面的通知方法具体是指哪一个切点;
3)定义通知:描述拦截的执行的实机和具体拦截的方法实现(Advice)
可以在前置方法里面定义一个时间,后置方法里面定义一个时间,这样就可以看到方法执行的一个时间戳了,可以在方法上面使用以下注解,会设置方法为通知方法,在满足条件之后会通知本方法进行调用,从本质上来说是方法执行的时机,是在Controller方法执行前还是Controller方法执行后执行这个方法,执行这个拦截,用户登陆操作是可以进行前置通知的
1)前置通知:使用@Before,通知方法会在目标方法调用之前执行,目标方法就是咱们UserController里面的方法;
2)后置通知:使用@After:通知方法会在目标方法返回之后进行调用
返回之后进行通知:使用@AfterReturning,通知方法会在目标方法返回之后进行调用
抛出异常之后进行通知:使用@AfterThrowing,通知方法会在目标方法抛出异常之后进行调用
3)环绕通知:使用@Around:通知包裹被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为;
4)AspectJ表达式:描述拦截的规则,要进行截取哪些类和哪些方法,以及要拦截方法的参数类型
定义通知:描述拦截执行的时机和具体的方法的实现
项目的前端页面基本上都是在static目录下面的
切点表达式说明AspectJ语法:SpringAOP切点的匹配语法
1)要被拦截的方法也叫做连接点
2)SpringAOP和AspectJ都是AOP思想的具体实现,现在的SpringAOP使用了AspectJ的语法并加上原来的SpringAOP的体系,SpringAOP已经向AspectJ靠拢了;
一:Aspect语法通配符介绍:
1)"*":匹配任意一个字符,*是只能匹配一个元素的(包,类,方法,或者方法参数)
2)"..":匹配任意个字符,可以进行匹配多个元素,在表示类的时候,必须和*进行联合使用,用在方法参数上面,通常表示可变参数;
@Pointcut("execution(* com.example.demo.Controller.UserController.*(..)")
3)"+":必须跟在类名后面,比如说com.cad.Car+,表示继承该Car类的所有子类包括本身;
表示按照类型匹配指定类的所有类,必须跟在类名后边;
4)execution(<修饰符><返回类型><包.类.方法(参数)> <异常>),这几个<>用空格分割;
二:常见写法:包名可以省略
一:修饰符:这个值可以省略不写的,它的值一共有两个参数,public表示公共方法,*表示任意修饰符,也可以直接省略,写完后面加空格+另一项的规则,可以省略表示匹配所有方法;
二)返回类型:是不可以省略的,这里面一共是可以写三种参数的,void表示返回没有值,String表示返回值是字符串类型,*表示任意返回值,后面加空格;三)包名也就是包的路径,包是可以省略的
四)类名:这里面可以写四种值:类名是可以省略
1)直接写类名:UserController制定具体的某一个类
2)设置后缀*Controller表示以Controller结尾的类
3)设置前缀:User*表示以User开头的类
4)通配符:*表示任意的类五)方法名:是不可以进行省略的
1)直接指定指定具体的方法,直接写方法名就可以;
2)我们可以直接设置前缀:比如说,user*指定以user开头的方法
3)我们还可以进行设置后缀:*Add指定以Add结尾的方法,
4)*指定任意方法
5).*拦截所有方法
六)参数:
()没有任何参数
(int)表示拦截一个整型
(int,int)表示连接两个整型
(..)是可以进行匹配任意参数的,而不是拦截(*),不是这种写法,表示是不定参数,表示参数不做限制;
七)throws,可以进行省略,一般是不会写的三:现在来举几个常见的例子练一下:
1)com.gyf.crm这是固定包名
2)com.gyf.crm.*.service,crm包和service包之间的包,比如说com.gyf.staff.service
3)com.gyf.crm..,我们在这里面匹配的是crm下面的任意子包,包含自己
4)com.gyf.crm.*.service..,crm包下面的任意子包,包含着固定目录service,service目录任意包
使用AOP的环绕通知来统计UserController里面的每一个方法的执行时间:
1)在SpringBoot对象里面,我们专门使用StopWatch对象来做时间的统计,我们可以把这个对象注入进来或者是直接new;
2)我们应该在SpringBoot中的时间统计应该在try语句块中开始进行统计方法执行时间;
@Component
@Aspect
public class AppController {
@Pointcut("execution(* com.example.demo.UserController.*(..))")
public void pointcut(){};
@Around("pointcut()")
public Object Around(ProceedingJoinPoint joinPoint) throws Throwable {
StopWatch stopWatch=new StopWatch();
Object obj;
System.out.println("即将执行环绕通知");
stopWatch.start();//统计方法的执行时间,代码执行到这里开始进行记时
obj=joinPoint.proceed();//执行目标方法以及目标方法所相应的通知
stopWatch.stop();//统计方法的执行时间停止计时
System.out.println("执行环绕通知成功");
System.out.println(joinPoint.getSignature().getDeclaringType()+"."+joinPoint.getSignature().getName()+
"方法花费的时间"+stopWatch.getTotalTimeSeconds()+"s");
return obj;
}
}
Proceeding表示进度,JoinPoint汉语是连接点,ProceedingJoinPoint表示连接点的进度
1)joinPoint.getSignature().getDeclaringType()表示被拦截的包名和类名
2)joinPoint.getSignature().getName()表示被拦截的方法
3)stopWatch.getTotalTimeSeconds()表示方法执行了多长时间,StopWatch底层也是使用System.currentTimeMillis()来实现的