一、AOP概念
AOP:Aspect Oriented Programming,中文翻译为”面向切面编程“。面向切面编程是一种编程范式,它作为OOP面向对象编程的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务管理、权限控制、缓存控制、日志打印等等。AOP采取横向抽取机制,取代了传统纵向继承体系的重复性代码
AOP把软件的功能模块分为两个部分:核心关注点和横切关注点。业务处理的主要功能为核心关注点,而非核心、需要拓展的功能为横切关注点。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点进行分离
使用AOP有诸多好处,如:
1.集中处理某一关注点/横切逻辑
2.可以很方便的添加/删除关注点
3.侵入性少,增强代码可读性及可维护性
二、AOP的术语
1.Join point(连接点)
Spring 官方文档的描述:
A point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution.
程序执行过程中的一个点,如方法的执行或异常的处理。在Spring AOP中,连接点总是表示方法的执行。通俗的讲,连接点即表示类里面可以被增强的方法
2.Pointcut(切入点)
Pointcut are expressions that is matched with join points to determine whether advice needs to be executed or not. Pointcut uses different kinds of expressions that are matched with the join points and Spring framework uses the AspectJ pointcut expression language
切入点是与连接点匹配的表达式,用于确定是否需要执行通知。切入点使用与连接点匹配的不同类型的 表达式,Spring框架使用AspectJ切入点表达式语言。我们可以将切入点理解为需要被拦截的Join point
@Pointcut注解包括表达式和签名(一个空方法)。表达式有以下10种类型的:
2.1 execution:一般用于指定方法的执行,用的最多
匹配所有方法 execution(* *(..))
匹配所有公共方法 execution(public * *(..))
匹配所有add()方法 execution(* add())
匹配所有抛出Exception的方法 execution(* *(..) throws Exception)
匹配类或者接口中的方法 execution(* com.test.ServiceImpl.*(..))
匹配指定包(不包含子包)下所有类中的所有方法 execution(* com.test.*.*(..))
匹配指定包及其子包下的所有类中以add开头的所有public方法 execution(public * com.test..*.add*(..))
2.2 within 指定某些类的全部方法执行,也可用来指定一个包。
匹配service包(不包含子包)中任意类的方法 within(com.test.service.*)
匹配service包及子包中任意类的方法 within(com.test.service..*)
2.3 this
Spring Aop是基于代理的,this就表示代理对象。this类型的Pointcut表达式的语法是this(type),当生成的代理对象可以转换为type指定的类型时则表示匹配。基于JDK接口的代理和基于CGLIB的代理生成的代理对象是不一样的。
匹配生成的代理对象是IUserService类型的所有方法的外部调用 this(com.test.UserService)
2.4 target
Spring Aop是基于代理的,target则表示被代理的目标对象。当被代理的目标对象可以被转换为指定的类型时则表示匹配。
匹配被代理的目标对象能够转换为UserService类型的所有方法的外部调用target(com.elim.spring.aop.service.UserService)
this 和 target 的不同点:
-- this作用于代理对象,target作用于目标对象
-- this表示目标对象被代理之后生成的代理对象和指定的类型匹配会被拦截,匹配的是代理对象
-- target表示目标对象和指定的类型匹配会被拦截,匹配的是目标对象
2.5 args 用来匹配方法参数的。
匹配任何不带参数的方法 args()
匹配任何只带一个String类型参数的方法 args(java.lang.String)
匹配带任意参数的方法 args(..)
匹配带任意个参数,但是第一个参数的类型是String的方法 args(java.lang.String,..)
匹配带任意个参数,但是最后一个参数的类型是String的方法 args(..,java.lang.String)
2.6@target 匹配当被代理的目标对象对应的类型及其父类型上拥有指定的注解时。
目标对象中包含指定注解 @target(com.elim.spring.support.MyAnnotation)
2.7@args 方法参数所属的类型上有指定的注解
匹配方法参数类型上拥有MyAnnotation注解的方法调用。如我们有一个方法add(MyParam param)接收一个MyParam类型的参数,而MyParam这个类是拥有注解MyAnnotation
@args(com.elim.spring.support.MyAnnotation)
2.8@within用于匹配被代理的目标对象对应的类型或其父类型拥有指定的注解的情况,但只有在调用拥有指定注解的类上的方法时才匹配。
匹配被调用的方法声明的类上拥有RestController注解
@within(org.springframework.web.bind.annotation.RestController)
比如有一个ClassA上使用了注解MyAnnotation标注,并且定义了一个方法a(),那么在调用ClassA.a()方法时将匹配该Pointcut;如果有一个ClassB上没有MyAnnotation注解,但是它继承自ClassA,同时它上面定义了一个方法b(),那么在调用ClassB().b()方法时不会匹配该Pointcut。但是在调用ClassB().a()时将匹配该方法调用,因为a()是定义在父类型ClassA上的,且ClassA上使用了MyAnnotation注解。但是如果子类ClassB覆写了父类ClassA的a()方法,则调用ClassB.a()方法时也不匹配该Pointcut。
2.9 @annotation用于匹配方法上拥有指定注解的情况。
匹配所有拥有MyAnnotation注解的方法 @annotation(io.swagger.annotations.ApiOperation)
2.10 bean 这是Spring增加的一种方法,spring独有。用于匹配当调用的是指定的Spring的某个bean的方法时
bean(abc) 匹配Spring Bean容器中id或name为abc的bean的方法调用
bean(user*) 匹配所有id或name为以user开头的bean的方法调用
Pointcut定义时,还可以使用&&、||、! 这三个运算
bean(userService) && args() 匹配id或name为userService的bean的所有无参方法。
bean(userService) || @annotation(MyAnnotation) 匹配id或name为userService的bean的方法调用,或者是方法上使用了MyAnnotation注解的方法调用。
bean(userService) && !args() 匹配id或name为userService的bean的所有有参方法调用
3.Advice(增强/通知)
所谓通知是指拦截到Joinpoint之后所要做的事情就是通知,通知分为前置通知、后置通知、异常通知、最终通知和环绕通知(切面要完成的功能)
4.Aspect(切面)
Aspect切面表示Pointcut(切入点)和Advice(增强/通知)的结合,一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。在Spring AOP中,切面可以使用基于模式)或者基于@Aspect注解的方式来实现。
5. Joint point
连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。
6. Weaving
织入,就是通过动态代理,在目标对象方法中执行处理内容的过程。
三、Advice注解
五种Advice注解:
1.@Before前置通知
前置通知在切入点运行前执行,不会影响切入点的逻辑
2.@After后置通知
后置通知在切入点正常运行结束后执行,如果切入点抛出异常,则在抛出异常前执行
3.@AfterThrowing异常通知
异常通知在切入点抛出异常前执行,如果切入点正常运行(未抛出异常),则不执行
4.@AfterReturning返回通知
返回通知在切入点正常运行结束后执行,如果切入点抛出异常,则不执行
5.@Around环绕通知
环绕通知是功能最强大的通知,可以在切入点执行前后自定义一些操作。环绕通知需要负责决定是继续处理join point(调用ProceedingJoinPoint的proceed方法)还是中断执行
四、代理模式
1、静态代理
所谓代理,用我们现实中的例子来说就是,代理就是明星的经纪人。明星的任务就是演戏、唱歌、上综艺节目、参加公益活动了,这之类的。明星的任务就是做就行了,至于其他的,全交给经纪人处理,这里经纪人是代理。联想到我们的代码中,比如有个xxxService接口还有一个xxxServiceImpl接口的实现类,还有一个proxy代理,那么实现类就是明星,proxy就是经理人,实现类和proxy都要实现xxxService接口,因为明星是不对外沟通演出的事情的,所以要经纪人知道这个明星都是可以做什么,只有实现接口,在proxy中才会有接口中的一些功能。
这样做有个缺点,就是我们有一个实现类,就要添加一个代理,代码需要写的太多了,以至于静态代理几乎都不用。。。。;
2、动态代理
在静态代理的基础上我们不用在手写代理类了,直接由程序自动生成,这个代理类我们看不到的,这个代码也不会出现在我们的程序中,所以相对于静态代理来讲,我们可以省很多的代码(这里怎么做动态代理就不一一介绍了,大致的就是,定义目标类,将目标类交给Spring管理也就是配置成bean对象,然后创建额外功能类,也是将此交给Spring管理,在后面,创建切入点,最后织入);
五、使用详解
开发步骤:
1、定义业务组件
2、定义切入点,一个切入点可能横切多个业务组件
3、定义增强处理,增强处理就是在AOP框架为普通业务组件织入的处理动作
1.pom.xml 增加依赖包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
2.编写切面组件
使用@Component(扫描) @Aspect(切面) @Around (环绕处理) @Pointcut (切入点) @Before (方法执行前) @After (方法执行后) @AfterReturning(捕获切入方法执行完之后的返回值) @AfterThrowing(当被切方法执行过程中抛出异常时)
定义业务组件:
@Service("productService") public class ProductServiceImpl implements ProductService { @Autowired private AuthService authService; @Override public Long deleteProductById(Long id) { System.out.println("删除商品id为" + id + "的商品成功!"); return id; } @Override public void deleteProductByName(String name) { System.out.println("删除商品名称为" + name + "的商品成功!"); } @Override public void selectProduct(Long id) { if("100".equals(id.toString())) { System.out.println("查询商品成功!"); } else { System.out.println("查询商品失败!"); throw new RuntimeException("该商品不存在!"); } } }
2.1 使用within表达式匹配包类型
//定义切入点,匹配ProductServiceImpl类里面的所有方法 @Pointcut("within(com.aop.service.impl.ProductServiceImpl)") public void matchType() {} //匹配com.aop.service包及其子包下所有类的方法 @Pointcut("within(com.aop.service..*)") public void matchPackage() {}
2.2 使用this、target、bean表达式匹配对象类型
//匹配AOP对象的目标对象为指定类型的方法,即ProductServiceImpl的aop代理对象的方法 @Pointcut("this(com.aop.service.impl.ProductServiceImpl)") public void matchThis() {} //匹配实现ProductService接口的目标对象 @Pointcut("target(com.aop.service.ProductService)") public void matchTarget() {} //匹配所有以Service结尾的bean里面的方法 @Pointcut("bean(*Service)") public void matchBean() {}
2.3 使用args表达式匹配参数
//匹配第一个参数为Long类型的方法 @Pointcut("args(Long, ..) ") publicvoidmatchArgs() {}
2.4 使用@annotation、@within、@target、@args匹配注解
//匹配标注有AdminOnly注解的方法@Pointcut("@annotation(com.aop.annotation.AdminOnly)") public void matchAnno() {} //匹配标注有Beta的类底下的方法,要求annotation的Retention级别为CLASS @Pointcut("@within(com.google.common.annotations.Beta)") public void matchWithin() {} //匹配标注有Repository的类底下的方法,要求annotation的Retention级别为RUNTIME @Pointcut("@target(org.springframework.stereotype.Repository)") public void matchTarget() {} //匹配传入的参数类标注有Repository注解的方法 @Pointcut("@args(org.springframework.stereotype.Repository)") public void matchArgs() {}
2.5 使用execution表达式
execution表达式是我们在开发过程中最常用的,它指定要触发方法。它的语法如下:
execution( modifier-pattern? ret-type-pattern declaring-type-pattern? modifier-pattern hrows-pattern? )
modifier-pattern:用于匹配public、private等访问修饰符
ret-type-pattern:用于匹配返回值类型,不可省略
declaring-type-pattern:用于匹配包类型
modifier-pattern(param-pattern):用于匹配类中的方法,不可省略
throws-pattern:用于匹配抛出异常的方法
定义切面
@Component @Aspect public class SecurityAspect { @Autowired private AuthService authService; //匹配com.aop.service.impl.ProductServiceImpl类下的方法名以delete开头、参数类型为Long的public方法 @Pointcut("execution(public * com.aop.service.impl.ProductServiceImpl.delete*(Long))") public void matchCondition() {} //使用matchCondition这个切入点进行增强 @Before("matchCondition()") public void before() { System.out.println("before 前置通知......"); authService.checkAccess(); } }
/** * 用于记录日志的工具类,它里面提供了公共的代码 */ @Component @Aspect // 表示当前类是一个切面类 public class Logger { /** * 配置切入点表达式 */ @Pointcut("execution(* com.aop.service.impl.ProductServiceImpl.delete*(Long))") private void match(){} /** * 前置通知 */ @Before("match()") public void beforePrintLog(){ System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。"); } /** * 后置通知 */ @AfterReturning("match()") public void afterReturningPrintLog(){ System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。"); } /** * 异常通知 */ @AfterThrowing("match()") public void afterThrowingPrintLog(){ System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了。。。"); } /** * 最终通知 */ @After("match()") public void afterPrintLog(){ System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。"); } /** * 环绕通知和上面四个不能同时存在 */ // @Around("match()") public Object aroundPringLog(ProceedingJoinPoint pjp){ Object rtValue = null; try{ Object[] args = pjp.getArgs();//得到方法执行所需的参数 System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置"); rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法) System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置"); return rtValue; }catch (Throwable t){ System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常"); throw new RuntimeException(t); }finally { System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终"); } } }
单元测试:
@Autowired private ProductService productService; @Test public void contextLoads() { //设置用户名 CurrentUserHolder.set("hello"); productService.selectProduct(100L); productService.deleteProductByName("衣服"); productService.deleteProductById(100L); }
可以在多个表达式之间使用连接符匹配多个条件, 如使用||表示“或”,使用 &&表示“且”
//匹配com.aop.service.impl.ProductServiceImpl类下方法名以select或delete开头的所有方法 @Pointcut("execution(* com.aop.service.impl.ProductServiceImpl.select*(..)) || " + "execution(* com.aop.service.impl.ProductServiceImpl.delete*(..))") public void matchCondition() {} //使用matchCondition这个切入点进行增强 @Before("matchCondition()") public void before() { System.out.println("before 前置通知......"); authService.checkAccess(); }
2.6 Advice注解
//匹配com.aop.service.impl.ProductServiceImpl类下面的所有方法 @Pointcut("execution(* com.aop.service.impl.ProductServiceImpl.*(..))") public void matchAll() {} @Around("matchAll()") public Object around(ProceedingJoinPoint joinPoint) { Object result = null; authService.checkAccess(); System.out.println("befor 在切入点执行前运行"); try{ result = joinPoint.proceed(joinPoint.getArgs());//获取参数 System.out.println("after 在切入点执行后运行,result = " + result); } catch (Throwable e) { System.out.println("after 在切入点执行后抛出exception运行"); e.printStackTrace(); } finally { System.out.println("finally......"); } return result; }
📢文章下方有交流学习区!一起学习进步!需要资料也可以前往官网,加入官方微信交流群💪💪💪
📢创作不易,如果觉得文章不错,可以点赞👍收藏📁评论📒
📢你的支持和鼓励是我创作的动力❗❗❗
官网:Doker 多克;官方旗舰店:首页-Doker 多克-淘宝网 全品优惠