简介
典型的应用场景就是日志,我们需要在某段业务代码的前后做一些日志输出的话,如果我们将日志代码与业务代码混在一起,是非常难以解耦合的。
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规定的那样在该对象上执行依赖注入。