在应用程序中,有些功能是不适合与业务逻辑混合在一起的,否则的话不便于后期人员的维护,比如:日志、声明式事务、安全和缓存等。这些功能需要以切面的形式横贯整个程序之中,减少与业务逻辑之间的耦合度,这种做法叫做面向切面编程(AOP)。本文主要描述spring框架中的AOP是如何使用的。
AOP的术语有:通知、连接点、切点、切面、引入和织入。
通知
通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。
Spring切面可以应用5种类型的通知:
前置通知:在目标方法被调用之前调用通知功能;
后置通知:在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
返回通知:在目标方法成功执行之后调用通知;
异常通知:在目标方法抛出异常后调用通知;
环绕通知:通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。
引入允许我们向现有的类添加新方法或属性。可以在无需修改这些现有的类的情况下,让它们具有新的行为和状态。
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。Spring AOP是以运行时期这种方式织入切面的。
首先定义一个computer接口,该接口有一个playGame方法。
package com.cheng.springdemo.service;
public interface Computer {
void playGame();
}
然后编写playGame()的通知,当调用该方法时,触发对应的切点。
execution(* com.cheng.springdemo.service.Computer.playGame(..))
定义切面上面的表达式中“*”号代表匹配所有的返回值,“com.cheng.springdemo.service.Computer.playGame”是限定类名和方法名,“(..)”代表匹配所有的参数。
一个玩游戏的切面,在玩游戏前,要先打开游戏,打完游戏后关闭游戏,若中途电脑有电脑,需要重启电脑。
使用非环绕通知实现切面:
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class Gamer {
// 定义切点
@Pointcut("execution(* com.cheng.springdemo.service.Computer.playGame(..))")
public void playGame() {}
// 前置通知
@Before("playGame()")
public void openGame() {
System.out.println("打开游戏。。。");
}
// 返回通知
@AfterReturning("playGame()")
public void closeGame() {
System.out.println("关闭游戏。。。");
}
// 异常通知
@AfterThrowing("playGame()")
public void restartComputer() {
System.out.println("重启电脑。。。");
}
}
使用环绕通知实现切面:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class Gamer {
// 定义切点
@Pointcut("execution(* com.cheng.springdemo.service.Computer.playGame(..))")
public void playGame() {}
// 环绕通知
@Around("playGame()")
public void playGame(ProceedingJoinPoint jp) {
try {
System.out.println("打开游戏。。。");
jp.proceed();
System.out.println("关闭游戏。。。");
} catch (Throwable e) {
System.out.println("重启电脑。。。");
}
}
}
在配置类里面配置该切面:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import com.cheng.springdemo.common.aop.Gamer;
/**
* spring 切面配置
* @author hrc
* @date 2019年8月30日
*/
@Configuration
// 启用切面代理,声明这个配置类里面配置的bean是切面
@EnableAspectJAutoProxy
@ComponentScan("com.cheng.springdemo")
public class SpringAopConfig {
@Bean
public Gamer gamer() {
return new Gamer();
}
}
要测试我们所配置的切面,得先实现computer接口,代码如下:
import org.springframework.stereotype.Component;
import com.cheng.springdemo.service.Computer;
@Component
public class NotebookComputer implements Computer {
@Override
public void playGame() {
System.out.println("用笔记本电脑玩游戏......");
}
}
然后,编写单元测试方法,代码如下:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.cheng.springdemo.common.springconfig.SpringAopConfig;
import com.cheng.springdemo.service.Computer;
@ContextConfiguration(classes = SpringAopConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class AopTest {
@Autowired
private Computer computer;
@Test
public void test1() {
computer.playGame();
}
}
最后测试结果如下图所示: