简介
典型的应用场景就是日志,我们需要在某段业务代码的前后做一些日志输出的话,如果我们将日志代码与业务代码混在一起,是非常难以解耦合的。
aop就是应对这种情况产生的技术。
概念
通知 | |切点 ↓ ——*——*——*——程序执行→ ↑ ↑ ↑ 连接点
通知
切面的工作被称为通知。
通知以日志为例,就是想要插入到业务代码的日志程序。
Spring切面的5种类型的通知:
- 前置通知(Before)
- 后置通知(After)
- 返回通知(After-returning)
- 异常通知(After-throwing)
- 环绕通知(Around)
在什么时候执行通知。
连接点
连接点是在应用执行过程中能够插入切面的一个点。
这个点就是触发执行通知的时机:如调用方法时,抛出异常时,修改字段时。
切点
一个切面并不需要通知应用的所有连接点,切点有助于缩小切面所通知的连接点的范围。
相对于连接点而言,连接点是所有可以供通知切入的地方,切点就是满足设定条件的连接点。
切面
切面 = 通知 + 切点
引入
向现有类添加新方法或属性。
织入
把切面应用到目标对象,并创建新的代理对象的过程。
在目标对象的生命周期里可织入的点:
- 编译期
- 类加载期
- 运行期
AOP支持
Spring提供的4种类型的AOP支持:
- 基于代理的经典Spring AOP(过于笨重复杂,直接使用ProxyFactory Bean。)
- 纯POJO切面
- @AspectJ注解驱动的切面
- 注入式AspectJ切面
如需更负责的AOP需求,如构造器和属性拦截,需要使用
AspectJ
实现。
Spring的AspectJ自动代理仅仅使用
@AspectJ
作为创建切面的指导,切面依然是基于代理的。在本质上,它依然是Spring基于代理的切面。这意味着尽管使用的是@AspectJ
注解,但仍然限于代理方法的调用。(如果想利用AspectJ的所有能力,我们必须在运行时使用AspectJ并且不依赖Spring来创建基于代理的切面)
Spring只支持方法级别的连接点
因为Spring基于动态代理,所以Spring只支持方法连接点。
Spring在运行时通知对象
不使用AOP:
┌─────┐ ┌───────┐ │调用者│----->│目标对象│ └─────┘ └───────┘
使用AOP:
┌─────────┐ │代理类 │ ┌─────┐ │┌───────┐│ │调用者│-----> ││目标对象││ └─────┘ │└───────┘│ └─────────┘
通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,再调用目标bean方法之前,会执行切面逻辑。
通过切点选择连接点
spring借助AspectJ的切点表达式语言来定义Spring切面
AspectJ指示器 | 描述 |
---|---|
execution() | 用于匹配是连接点的执行方法 |
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 |
target | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里) |
@annotation | 限定匹配带有指定注解的连接点 |
在Spring中尝试使用AspectJ其它指示器时,将会抛出
IllegalArgument-Exception
异常。
上述指示器,只有execution
指示器是实际执行匹配的,而其它的都是用来限制的。
对于xml配置
采用注解和自动代理的方式创建切面,是十分便利的方式。
但面向注解的切面声明有一个明显的劣势:你必须能够为通知类添加注解。为了做到这一点,必须要有源码。
没有通知类的源码,只能采用xml配置文件声明切面。
详细
切点
编写切点
expression:
execution(modifiers-pattern? ret-type-pattern? declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
- modifiers-pattern:修饰符
- ret-type-pattern:返回类型
- declaring-type-pattern:类路径
- name-pattern:方法名
- param-pattern:参数
- throws-pattern:异常类型
修饰符和返回类型可以使用一个
*
表示。
在方法执行时触发 方法所属的类 方法 ┌───────┐ ┌─────────┐ ┌──┐ execution( * com.yww.Log.info(..) ) └─┘ └──┘ 返回任意类型 使用任意参数
执行Log.info()方法 当com.yww包下的任意类的方法被调用时 ┌─────────────────────────────────┐ ┌───────────────┐ execution( * com.yww.Log.info(..) ) && within(com.yww.*) └──┘ 与(and)操作
&
在xml中由特殊含义,所以spring在xml的配置中可以使用and
替代&&
,同理or
,not
替代||
,!
。
在切点中选择bean
execution(* com.yww.Login.info()) and bean('work') execution(* com.yww.Login.info()) and !bean('work')
切面
代码
示例几种使用:
- 基本使用
- 处理通知中的参数
- 通过注解引入新功能
基本使用
目录结构
. ├── build.gradle ├── settings.gradle └── src ├── main │ ├── java │ │ └── com │ │ └── yww │ │ ├── Config.java │ │ ├── Log.java │ │ ├── Main.java │ │ └── Work.java │ └── resources └── test ├── java └── resources
build.gradle
build.gradle
:引入的库.
plugins { id 'java' } group 'com.yww' version '1.0-SNAPSHOT' sourceCompatibility = 1.8 repositories { mavenCentral() } ext{ springVersion = '5.2.0.RELEASE' } dependencies { compile "org.springframework:spring-core:$springVersion" compile "org.springframework:spring-context:$springVersion" compile "org.springframework:spring-beans:$springVersion" compile "org.springframework:spring-expression:$springVersion" compile "org.springframework:spring-aop:$springVersion" compile "org.springframework:spring-aspects:$springVersion" testCompile "junit:junit:4.12" testCompile "org.springframework:spring-test:$springVersion" } jar { from { configurations.runtime.collect{zipTree(it)} } manifest { attributes 'Main-Class': 'com.yww.Main' } }
业务代码
Work.java
:将这个类的功能作为业务代码为例。就是一个普通的bean。
package com.yww; import org.springframework.stereotype.Component; @Component public class Work { public void working(){ System.out.println("-w-o-r-k-i-n-g-"); } }
切面
Log.java
:使用@Aspect
声明切面。
写通知,在通知方法上使用@Before
和@After
等声明切点。
也可以使用@Pointcut
声明切点位置,减少重复。
package com.yww; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect @Component public class Log { @Pointcut("execution(* com.yww.Work.working())") public void working(){} @Before("working()") public void infoStart(){ System.out.println("start"); } @After("working()") public void infoEnd(){ System.out.println("end"); } @Around("working()") public void infoAround(ProceedingJoinPoint jp){ System.out.println("--->"); try { jp.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } System.out.println("<---"); } }
@Component
直接将其注册为Bean,给AspectJ代理使用。
ProceedingJoinPoint
可以不调用proceed()
,以阻塞对被通知方法的调用。
启用AspectJ代理
LogConfig.java
:使用@EnableAspectJAutoProxy
开启AspectJ自动代理。
package com.yww; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @ComponentScan @EnableAspectJAutoProxy public class LogConfig { }
Main
Main.java
:主函数,这是一个普通的应用,通过上下文加载java配置类启动组件扫描
和AspectJ自动代理
功能。
执行业务代码后,定义在切面的通知也会在适当时机执行。
package com.yww; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class Main { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(com.yww.LogConfig.class); Work work = context.getBean(Work.class); work.working(); } }
处理通知中的参数
作用:获取业务方法的参数,在通知中做额外处理。
需要:在切点表达式中声明参数,这个参数传入到通知方法中。
在方法执行时触发 方法所属的类 方法 ┌───────┐ ┌──────────┐ ┌───┐ execution( * com.yww.Work.clock(String) ) && args(username) └─┘ └────┘ └────────────┘ 返回任意类型 接受String类型的参数 指定参数
目录结构
. ├── build.gradle └── src ├── main │ ├── java │ │ └── com │ │ └── yww │ │ ├── Counter.java │ │ ├── Config.java │ │ ├── Main.java │ │ └── Work.java │ └── resources └── test ├── java └── resources
业务代码
Work.java
:业务代码。
package com.yww; import org.springframework.stereotype.Component; @Component public class Work { public void clock(String username){ System.out.println(username + " 打卡"); } }
切面
Counter.java
:在业务代码执行clock()
方法时,在通知里记录用户打卡次数。
package com.yww; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; @Aspect @Component public class Counter { private Map<String, Integer> counter = new HashMap<>(); @Pointcut("execution(* com.yww.Work.clock(String)) && args(username)") public void clock(String username){} /** * 记录打卡次数 */ @Before("clock(username)") public void count(String username){ int curCount = getCount(username); counter.put(username, curCount+1); } /** * 获取打卡次数 */ public int getCount(String username){ return counter.containsKey(username) ? counter.get(username) : 0; } }
启用AspectJ代理
LogConfig.java
:使用@EnableAspectJAutoProxy
开启AspectJ自动代理。
package com.yww; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @ComponentScan @EnableAspectJAutoProxy public class LogConfig { }
Main
Main.java
:主函数。
package com.yww; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class Main { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(com.yww.Config.class); Work work = context.getBean(Work.class); Counter counter = context.getBean(Counter.class); work.clock("zhangsan"); work.clock("lisi"); work.clock("lisi"); System.out.println(counter.getCount("lisi")); } }
通过注解引入新功能
通过代理暴露新接口的方式,让切面所通知的bean看起来像是实现了新接口.
┌─────────────┐ │代理 │ 现有方法 │┌───────────┐│ ┌-----> ││被通知的Bean││ ┌─────┐-------┘ │└───────────┘│ │调用者│ │ │ └─────┘-------┐ │┌───────────┐│ └-----> ││ 引入的代理 ││ 被引入的方法 │└───────────┘│ └─────────────┘
目录结构
. ├── build.gradle └── src ├── main │ ├── java │ │ └── com │ │ └── yww │ │ ├── Config.java │ │ ├── EnhancePerson.java │ │ ├── Main.java │ │ ├── Man.java │ │ ├── Person.java │ │ ├── WalkImpl.java │ │ └── Walk.java │ └── resources └── test ├── java └── resources
业务类
Person.java
:业务的接口.
package com.yww; public interface Person { public void getName(); }
Man.java
:业务的实现.
package com.yww; import org.springframework.stereotype.Component; @Component public class Man implements Person { @Override public void getName() { System.out.println("a man"); } }
新增的功能
Walk.java
:新增功能的接口.
package com.yww; public interface Walk { void walk(); }
`WalkImpl.java:新增功能的实现.
package com.yww; public class WalkImpl implements Walk { @Override public void walk() { System.out.println("新增 walk"); } }
切面配置
EnhancePerson.java
:给业务类Person
新增Walk
功能.尽管没有真正的添加方法,但通过代理的方式,也可看成了功能的添加.
package com.yww; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.DeclareParents; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect @Component public class EnhancePerson { @DeclareParents(value = "com.yww.Person+", defaultImpl = WalkImpl.class) public static Walk walk; }
@DeclareParents
注解由三部分组成:
- value属性指定了哪种类型的bean要引入该接口.(此处是所有实现了
Person
接口的类型).标记符后面的加号+
表示是Person
的所有子类型,而不是Person
本身.- defaultImpl属性指定了为引入功能提供实现的类.
@Declarearents
注解所标注的静态属性指明了要引入的接口.
Main
Main.java
:主函数,如何使用新增的功能.
package com.yww; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class Main { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(com.yww.Config.class); // 方式一 Person person = context.getBean(Person.class); person.getName(); Walk w1 = (Walk) person; w1.walk(); // 方式二 Walk w2 = context.getBean("man", Walk.class); w2.walk(); } }
附:
AspectJ
AspectJ声明的切面将其声明为bean,与Spring中声明为bean的配置无太多区别,最大的不同在于使用了factory-method
属性.
因为Spring bean由Spring容器初始化,但AspectJ切面是由AspectJ在运行期创建的.等到Spring有机会为其注入依赖时,该切面已实例化了.
所以Spring需要通过aspectOf()工厂方法获得切面的引用,然后像bean规定的那样在该对象上执行依赖注入.