写在前面
- 本文中的内容非原创,主要来源见文末的参考链接,本人仅作整理工作,用以记录自己的学习过程,由于个人水平有限,故部分内容可能会出现错误,还请包涵
思维导图
问题来源
- 做系统设计时,一个很重要的工作就是将系统分解,按业务功能分解成一个个低耦合、高内聚的模块
- 分解之后发现有些功能是通用的,如
日志
:对特定的操作输出日志来记录安全
:在执行操作之前进行操作检查性能
:要统计每个方法的执行时间事务
:方法开始之前要开始事务, 结束后要提交或者回滚事务- 等等。。。
解决方案
最简单的方法
- 将通用模块的接口写好,在实现业务模块时调用相应的接口方法
- 存在的问题
- 日志、性能、事务相关的代码几乎将真正的业务代码给淹没
- 所有希望有日志、性能等功能的类都要如此,重复代码很多
设计模式:模板方法
1 | public abstract class BaseCommand { |
- 在父类中将通用的非业务代码都写好,子类只关注业务逻辑
- 存在的问题
- 要执行哪些非功能代码,以什么顺序执行等等,子类只能无条件接受
- 如果有个子类不需要事务,但其本身没有办法将事务代码去除
设计模式:装饰者
1 | public interface Command { |
现在让 PlaceOrderCommand 能够打印日志、进行性能统计分析
1
2
3
4Command cmd = new LoggerDecorator(
new PerformanceDecorator(
new PlaceOrderCommand()));
cmd.execute();让 PaymentCommand 只打印日志,装饰一次就可以了
1
2
3Command cmd = new LoggerDecorator(
new PaymentCommand());
cmd.execute();可以使用任意数量装饰器,还可以以任意次序执行(严格意义上来说是不行的)
- 存在的问题:
- 一个处理日志、性能、业务的类为什么要实现业务接口(Command)
- 如果别的业务模块,没有实现Command接口,但是也想利用日志/性能/事务等功能,该怎么办
AOP
- 鉴于装饰者模式的缺点,最好把日志、安全、事务这样的代码和业务代码完全隔离开来,因为它们的关注点和业务代码的关注点完全不同
AOP 实现
首先有一个
切面
类(Aspect),是一个 POJO,以一个事务类为例1
2
3
4
5
6
7
8public class Transaction {
public void beginTx() {
//开始业务
}
public void commitTx() {
//提交事务
}
}想要实现的功能:对于某个 package 中所有类的 execute 方法,在方法调用之前需要调用 Transaction.beginTx() 方法,在调用之后需要调用 Transaction.commitTx() 方法
对于某个 package 中所有类的 execute 方法
:即切入点(PointCut),可以是一个方法或一组方法(可以通过通配符来支持)在方法调用之前/之后需要调用
:即通知(Advice)- 想要描述这些规则,可以使用:
- 使用注解创建切面,
@Aspect
- 使用 JavaConfig 类创建切面
- 使用 XML 配置文件
- 使用注解创建切面,
Spring AOP 术语
横切关注点
- 分布在整个应用多处的功能,如日志、安全、事务等
- 可以被模块化为特殊的类,这些类被称为
切面
(aspect)通知(Advice)
- 切面的工作称为通知
- 通知定义了切面是
什么
以及何时使用
- 通知有 5 种类型:
前置通知
(Before):在目标方法被调用之前调用通知功能后置通知
(After):在目标方法完成之后调用通知, 此时不会关心方法的输出是什么返回通知
(After-returning):在目标方法成功执行之后调用通知异常通知
(After-throwing):在目标方法抛出异常后调用通知环绕通知
(Around):通知包裹了被通知的方法, 在被通知的方法调用之前和调用之后执行自定义的行为
连接点(Join Point)
- 在应用执行过程种能够插入切面的一个点
- 切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为
切点(Pointcut)
- 一个切面并不需要通知应用的所有连接点
- 切点有助于缩小切面所通知的连接点的范围
- 如果说通知定义了切面是
什么
及何时
的话,那么切点就定义了何处
切面(Aspect)
- 切面是通知和切点的结合,通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能
引入(Introduction)
- 引入允许我们向现有的类添加新方法或属性
织入(Weaving)
- 织入是把切面应用到目标对象并创建新的代理对象的过程
- 切面是在指定的连接点被织入到目标对象中
- 在目标对象的声明周期里有多个点可以进行织入:
编译期
:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的类加载期
:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5 的加载时织入(load-time weaving, LTW)就支持以这种方式织入切面运行期
:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态地创建一个代理对象。Spring AOP 就是以这种方式织入切面的
Spring AOP 实现机制
- AOP 实现的关键在于 AOP 框架自动创建的 AOP 代理
- AOP 代理主要分为
静态代理
和动态代理
静态代理:
- 在编译阶段生成 AOP 代理类,即生成的字节码织入了增强后的 AOP 对象
- 以
AspectJ
为代表
动态代理:
- 不会修改字节码,而是在内存中临时生成一个 AOP 对象,该对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法
- 以
Spring AOP
为代表
JDK 动态代理
- 通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口
- 核心是
InvocationHandler
接口和Proxy
类 - 具体解析可阅读《Java 动态代理》
具体使用
定义一个
Performance
接口和一个具体实现类Movie
1
2
3
4
5
6
7
8
9
10
11
12
13
14public interface Performance {
public void perform();
}
//@Component(任选一个注解均可以实现)
@Service
public class Movie implements Performance {
@Override
public void perform() {
System.out.println("The movie is performing");
}
}现在我们希望电影开始和结束时有观众的动作,定义一个
Audience
类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19@Aspect
public class Audience {
@Pointcut("execution(** concert.Performance.perform(..))")
public void performance() {}
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("Silencing cell phones");
System.out.println("Taking seats");
jp.proceed();
System.out.println("CLAP CLAP CLAP!!!");
} catch (Throwable e) {
System.out.println("Demanding a refund");
}
}
}使用 JavaConfig 类来进行配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages="concert") //(扫描 concert 包下的所有组件)
public class Config {
@Bean
public Audience audience() {
return new Audience();
}
@Bean
public Performance performance() {
return new Movie();
}
}编写一个测试类来查看效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=Config.class)
public class TestPerformance {
@Autowired
private Audience audience;
@Autowired
private Performance performance;
@Test
public void testAudience() {
performance.perform();
}
}测试结果如下
1
2
3
4Silencing cell phones
Taking seats
The movie is performing
CLAP CLAP CLAP!!!
CGLIB 动态代理
- 若目标类没有实现接口,则 Spring AOP 会使用
CGLIB
来动态代理目标类 - CGLIB 是一个代码生成的类库,可以在运行时动态的生成某个类的子类
- 通过继承的方式实现动态代理,因此若某个类被标记为 final,则无法通过 CGLIB 实现动态代理,诸如 private 的方法也是不可以作为切面的
具体实现
- To do