说到面向切面编程,想必大家都不会陌生。不就是AOP——Aspect Orient Programming
但是说了那么久,你所理解的面向切面编程,用一句话概括到底是什么呢?
目录
1.什么是AOP(面向切面编程)
2.AOP相关术语
1.连接点(Joinpoint)
2.切点(Pointcut)
3.增强(Advice)
4.目标对象(Target)
5.引介(Introduction):
6.织入(Weaving):
7.代理(Proxy)
9.切面(Aspect)
3.实践
3.1 简单的自定义注解增强
3.2 通过指定类/方法进行增强
在我的理解来说:
在运行时,动态的 将代码 切入到 类的 指定方法。 这种编程思想就称为面向切面编程
来举个例子吧,你写了A方法之后,老板想让你看看这段代码的执行时间。ok,那你就在A方法的前后加上代码,查看时间。。。
过了几天,你又完成了B方法之后,老板又想让看你记录这段代码的日志信息。没问题,加上
又过了几天,老板又要让你每个功能都加上日志功能。。。没过几天你就。。。
久而久之,你就发现,你的代码中会出现越来越多与业务无关的东西。后来你发现它们都是有共同点的,于是你将这些公共的代码整体抽成common方法进行调用,想办法降低耦合。但是由于是在方法中进行调用,该污染代码还是会污染,如果严格说来还是有很强的关联系的。比如说一旦涉及到部分参数的传递,那么耦合度还是很高的。
在你想解耦合且方便维护的时候,AOP出现了。
spring提供这个工具,让你能解决上述老板给你提出的这些要求,避免你修改次数太多删库跑路~
程序执行的某个特定位置:如类开始初始化前,类初始化后,类某个方法调用前。一个类或一段代码拥有一些边界性质的特定点,这些代码中的特定点就被称为“连接点”。Spring仅支持方法的连接点,既仅能在方法调用前,方法调用后,方法抛出异常时等这些程序执行点进行织入增强。
连接点由两个信息确定:第一是用方法表示的程序执行点;第二是用相对点表示的方位。如在Test.foo()方法执行前的连接点,执行点为Test.foo(),方位为该方法执行前的位置。Spring使用切点对执行点进行定位,而方位则在增强类型中定义。
每个程序类都拥有多个连结点,如一个拥有两个方法的类,这两个方法都是连接点,既连接点是程序中客观存在的事务。当某个连接点满足指定要求时,就可以被添加Advice,成为切点,比如:
pointcut xxxPorintcut()
:execution(void H*.say*())
每个方法被调用,都只是连接点,如果某个方法属于H开头的类,而且方法一say开头,则该方法就变成了切入点。如何使用表达式定义切入点,是AOP的核心,Spring默认使用AspectJ切入点语法。
连接上一条,你可以在切点这里做一些事情,有“around”,“before”,“after”等类型。
被AOP框架进行增强处理的对象。如果是动态AOP的框架,则对象是一个被代理的对象。
引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过AOP的引介功能,我们可以动态的为该事务添加接口的实现逻辑,让业务类成为这个接口的实现类。
织入是将增强添加对目标类具体连接点上的过程,AOP象一台织布机,将目标类增强或引介AOP这台织布机天衣无缝的编织在一起。
一个类被AOP织入增强后,就产生了一个结果类,它是融合了原类和增前逻辑的代理类。根据不同的代理方式,代理类及可能是和原类具有相同的接口的类,也可能是原类的子类,所以我们可以采用调用原类得相同方式调用代理类。
切面由切点和增强(引介)组成,它既包括了横切逻辑的定义,也包括了连接点的定义,Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的链接点中。
以上内容其实一搜一大堆,个人认为需要认识理解的切点、增强、切面即可,不需要去死记硬背这些点。使用的多了,会自然的熟悉这些知识点。实践出真理
简单的介绍一下,方便大家来记忆:
有一个方法a,但是这个方法只支持进行A操作。
增强,这我想在这个方法a前/后添加一个B动作,那么这个增加的B动作就叫做增强。
切点,你针对这个a方法做了增强操作,那么这个a方法就称为切点
切面,切点和增点组成切面,它是一个大的概念,一个统称
先给出控制层代码
@RestController
@RequestMapping(value = "/a")
public class AController {
@GetMapping(value = "/test1")
@PermissionAnnotation
public String test1(){
return "a.test1";
}
@GetMapping(value = "/test2")
public String test2(){
return "a.test2";
}
}
@RestController
@RequestMapping(value = "/b")
public class BController {
@GetMapping(value = "/test1")
public String test1(){
return "b.test1";
}
@GetMapping(value = "/test2")
public String test2(){
return "b.test2";
}
}
引入pom
org.springframework.boot
spring-boot-starter-aop
创建一个注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PermissionAnnotation {
}
创建对应的切面
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TestAspect {
@Pointcut("@annotation(com.example.aspectdemo.annotation.PermissionAnnotation)")
private void permissionCheck() {
}
@Before("permissionCheck()")
public void doBefore() {
System.out.println("doBefore");
}
@Around("permissionCheck()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("do something。。。before");
return joinPoint.proceed();
}
@After("permissionCheck()")
public void doAfter() {
System.out.println("doAfter");
}
}
调用方法: 直接调用方法,http://localhost:8888/a/test1
打印结果:
让我们加断点进去看看
从上面的几张图,我们可以看到一个较为详细的执行流程,为了加深印象大家可以自己创建之后跟进去试试看。这样会有更深的印象。
下面用一段代码来总结一下执行的流程。但是注意不管是前置通知还是后置通知,一定是要进入proceed()这个方法之后才会执行的,下面这样写只是为了方便去理解执行的流程
try {
//前置通知@Before
System.out.println("环绕前置通知");
//目标方法执行
result = proceedingJoinPoint.proceed(args);
//环绕返回通知@AfterReturning
System.out.println("环绕返回通知");
} catch (Throwable throwable) {
//环绕异常通知@AfterThrowing
System.out.println("环绕异常通知");
throw new RuntimeException(throwable);
} finally {
//最终通知@After
System.out.println("环绕最终通知");
}
其实也可以有多个@Before、@After联合使用。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TestAspect {
@Before("permissionCheck()")
public void doBeforeY() {
System.out.println("doBeforeY");
}
@Before("permissionCheck()")
public void doBeforeX() {
System.out.println("doBeforeX");
}
@Before("permissionCheck()")
public void doBeforey() {
System.out.println("doBeforey");
}
@Before("permissionCheck()")
public void doBeforex() {
System.out.println("doBeforex");
}
@Before("permissionCheck()")
public void doBefore2() {
System.out.println("doBefore2");
}
@Pointcut("@annotation(com.example.aspectdemo.annotation.PermissionAnnotation)")
private void permissionCheck() {
}
@Before("permissionCheck()")
public void doBefore1() {
System.out.println("doBefore1");
}
@Around("permissionCheck()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("do something。。。before");
return joinPoint.proceed();
}
@After("permissionCheck()")
public void doAfter() {
System.out.println("doAfter");
}
}
这段代码,我们就加入了很多的前置处理和后置处理。那它的运行结果是啥呢?大家可以猜猜看,是按照代码的顺序来执行呢?还是怎么来执行呢?
可以明显的看一些端倪,当前执行的优先级:数字>大写字母>小写字母
其实执行顺序还有很多有趣的地方,这里就不再做过多的描述了。有兴趣的可以去看看aop从加载到执行的spring源码。
不了解表达式的同学可以参考下这位大佬写的博客:spring AOP切面表达式详解_人有匪气,代码才能飞起-CSDN博客_切面表达式
下面列出一些常见的,方便大家进行对比实践
execution(public * *(..)) // 匹配任意公共方法
execution(* set*(..)) // 匹配任意名称为 set 开头的方法
execution(* com.xyz.service.AccountService.*(..)) // 匹配 AccountService 类中的任意方法
execution(* com.xyz.service.*.*(..)) // 匹配 com.xyz.service 包下的任意类的任意方法
execution(* com.xyz.service..*.*(..)) // 匹配 com.xyz.service 包及子包下的任意类的任意方法
within(com.xyz.service.*) // 匹配 com.xyz.service 包下的任意类的任意方法
within(com.xyz.service..*) // 匹配 com.xyz.service 包及子包下的任意类的任意方法
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TestAspect {
@Pointcut("execution(* com.example.aspectdemo.controller.BController.*(..))")
private void pointCut() {
}
@Around("pointCut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("do something。。。before");
return joinPoint.proceed();
}
@Before("pointCut()")
public void doBefore1() {
System.out.println("doBefore1");
}
@After("pointCut()")
public void doAfter() {
System.out.println("doAfter");
}
}
直接调用方法,http://localhost:8888/b/test1