面向切面编程(AOP)是针对面向对象编程(OOP)的补充,可以非侵入式的为多个不具有继承关系的对象引入相同的公共行为例如日志、安全、事务、性能监控等等。SpringAOP允许将公共行为从业务逻辑中抽离出来,并将这些行为以一种非侵入的方式织入到所有需要的业务逻辑中,相较于OOP纵向模式的业务逻辑实现,其关注的方向是横向的切面。
从Spring2.0开始,引入AspectJ注释来对POJO进行标注,支持通过切点函数、逻辑运算符、通配符等高级功能来对切点进行灵活的定义,结合各种类型的通知来形成强大的连接点描述能力。
下面先给出一个实现例子,然后介绍SpringAOP的基本概念并通过源码分析其实现原理。
第一部分 使用样例
1 一个简单例子
在低版本Spring中定义一个切面是比较麻烦的,需要实现特定的接口,并进行一些较复杂的配置。从Spring2.0开始,引入AspectJ注释来对POJO进行标注,从而可以很方便的定义个包含切点信息和增强逻辑的切面。
为使用AspectJ的注释,除了Spring的核心库,我们还需要依赖aspectjweaver的jar包。
本例使用SpringBoot,用maven管理jar包依赖,在启动pom.xml引入SpringBoot核心启动器并自己引入aspectjweaver的jar包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
或者直接使用SpringBoot的aop启动器:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
两者效果都一样,将Spring核心模块Spring core、Spring beans、Spring context、Spring aop以及AspectJ注释提供库aspectjweaver引入项目。(另外包括SpringBoot特有的自动配置模块等等)
1.1 编写业务逻辑MyTestService
@Service
public class MyTestService {
Logger logger = LoggerFactory.getLogger(MyTestService.class);
public String doSomething1(){
logger.info("invoking doSomething1......");
return "doSomething1";
}
public String doSomething2(){
logger.info("invoking doSomething2......");
return "doSomething2";
}
}
1.2 定义切面
@Aspect
@Component
public class MyIntercepter {
private static final Logger logger = LoggerFactory.getLogger(MyIntercepter.class);
@Pointcut("execution(public * aopnew.service.MyTestService.doSomething*(..))")
public void doSomethingPointcut(){};
@Before("doSomethingPointcut()")
public void auth(JoinPoint pjp) throws Throwable{
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod(); //获取被拦截的方法
String methodName = method.getName(); //获取被拦截的方法名
logger.info("权限认证:调用方法为:{}", methodName);
};
@AfterReturning(value = "doSomethingPointcut()", returning = "returnVal")
public void logNormal(JoinPoint pjp, Object returnVal) throws Throwable{
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod(); //获取被拦截的方法
String methodName = method.getName(); //获取被拦截的方法名
logger.info("正常返回记日志:调用方法为:{};返回结果为:{}", methodName, returnVal);
};
@AfterThrowing(value = "doSomethingPointcut()", throwing = "e")
public void logThrowing(JoinPoint pjp, Throwable e) throws Throwable{
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod(); //获取被拦截的方法
String methodName = method.getName(); //获取被拦截的方法名
logger.info("抛出异常记日志:调用方法为:{};异常信息为:{}", methodName, e.getMessage());
};
@After(value = "doSomethingPointcut()")
public void afterall(JoinPoint pjp) throws Throwable{
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod(); //获取被拦截的方法
String methodName = method.getName(); //获取被拦截的方法名
logger.info("方法调用完成:调用方法为:{}", methodName);
}
@Around("doSomethingPointcut()")
public Object timer(ProceedingJoinPoint pjp) throws Throwable{
long beginTime = System.currentTimeMillis();
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod(); //获取被拦截的方法
String methodName = method.getName(); //获取被拦截的方法名
logger.info("计时切面:请求开始,方法:{}", methodName);
Object result = null;
try {
// 一切正常的情况下,继续执行被拦截的方法
result = pjp.proceed();
} catch (Throwable e) {
logger.info("exception: ", e);
}
long endTime = System.currentTimeMillis();
logger.info("计时切面:请求结束,方法:{},执行时间:{}", methodName, (endTime-beginTime));
return result;
}
}
上面定义了一个切点Pointcut,并围绕这个切点定义了5中不同类型的通知Advice,每个切点及其通知以及通知执行的逻辑共同构成了一个切面Advisor,用以在方法执行过程的各个时间点切入,执行一些特定逻辑。
1.3 测试代码
引入测试依赖jar包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
编写测试代码:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = AOPConfiguration.class)
public class AOPTest {
@Autowired
MyTestService myTestService;
@Test
public void test() throws IOException {
myTestService.doSomething1();
}
}
执行结果如下:
aopnew.aspect.MyIntercepter : 计时切面:请求开始,方法:doSomething1
aopnew.aspect.MyIntercepter : 权限认证:调用方法为:doSomething1
aopnew.service.MyTestService2 : invoking doSomething1......
aopnew.aspect.MyIntercepter : 计时切面:请求结束,方法:doSomething1,执行时间:28
aopnew.aspect.MyIntercepter : 方法调用完成:调用方法为:doSomething1
aopnew.aspect.MyIntercepter : 正常返回记日志:调用方法为:doSomething1;返回结果为:doSomething1
由此可见,我们刚才以无侵入的形式在方法调用的前后增加了很多横切向的业务逻辑,业务逻辑代码并不必关心这些横切逻辑,只需要专注于自己的业务逻辑的实现就好。
而在实际执行当中,在方法调用前后我们定义的切面都开始执行了。SpringAOP确保我们定义的切面织入到业务逻辑代码中,并在执行时发挥作用,具体的实现原理将在下面章节分析。
另外如结果所示多个切面的执行顺序也并不是按照方法定义的顺序执行,其顺序我们将在下面讲述。
2 基于自定义注释的切面定义
直接使用execution(public * aopnew.service.MyTestService.doSomething*(..))这种切面定义方式与实际的类路径、类名或方法名紧密绑定显得不怎么优雅。我们希望像SpringCache那样基于自定义注释的方式启动各种切面,SpringAOP通过切点函数@annotation和@Within来支持这种方式。
2.1 先编写两个自定义注释
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestLogger {
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestTimer {
}
其中TestLogger是定义在类上的注释;TestTimer是定义在方法上的注释。
2.2 定义基于自定义注释的切面
@Aspect
@Component
public class MyIntercepter2 {
private static final Logger logger = LoggerFactory.getLogger(MyIntercepter2.class);
@Pointcut("@annotation(aopnew.annotation.TestTimer)")
public void timerPointcut(){};
@Pointcut("@within(aopnew.annotation.TestLogger)")
public void recordLogPointcut(){};
@Before("recordLogPointcut()")
public void log(JoinPoint pjp) throws Throwable{
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod(); //获取被拦截的方法
String methodName = method.getName(); //获取被拦截的方法名
logger.info("开始记日志:调用方法为:{}", methodName);
}
@Around("timerPointcut()")
public Object timer(ProceedingJoinPoint pjp) throws Throwable{
long beginTime = System.currentTimeMillis();
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod(); //获取被拦截的方法
String methodName = method.getName(); //获取被拦截的方法名
logger.info("请求开始,方法:{}", methodName);
Object result = null;
try {
// 一切正常的情况下,继续执行被拦截的方法
result = pjp.proceed();
} catch (Throwable e) {
logger.info("exception: ", e);
}
long endTime = System.currentTimeMillis();
logger.info("请求结束,方法:{},执行时间:{}", methodName, (endTime-beginTime));
return result;
}
}
上述代码表示打了@TestLogger注释的类,其中的所有方法被调用时都会记日志;而不管什么类,其打了@TestTimer注释的方法都会监控其执行时间。
切点函数@annotation表示匹配方法上的注释,切点函数@within表示匹配类上的注释,具体详情我们将在后面详细解释。
2.3 编写业务逻辑并使用切面
编写服务,并在需要执行相应横切面逻辑的地方打上相应注释:
@Service
@TestLogger
public class MyTestService2 {
Logger logger = LoggerFactory.getLogger(MyTestService2.class);
public String sayHello(){
logger.info("invoking method sayHello......");
return "Hello world!";
}
@TestTimer
public int count(){
logger.info("invoking method count......");
return 10;
}
}
根据服务MyTestService2中的注释,其表达的意思是MyTestService2中所有方法调用时都需要记日志,另外count()方法被调用时候需要监控执行时间。
2.4 编写测试代码并查看执行结果
测试代码如下:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = AOPConfiguration.class)
public class AOPTest {
@Autowired
MyTestService2 myTestService2;
@Test
public void test() throws IOException {
myTestService2.sayHello();
myTestService2.count();
}
}
执行结果如下:
aopnew.aspect.MyIntercepter2 : 开始记日志:调用方法为:sayHello
aopnew.service.MyTestService2 : invoking method sayHello......
aopnew.aspect.MyIntercepter2 : 请求开始,方法:count
aopnew.aspect.MyIntercepter2 : 开始记日志:调用方法为:count
aopnew.service.MyTestService2 : invoking method count......
aopnew.aspect.MyIntercepter2 : 请求结束,方法:count,执行时间:1
由上可见,由于我们标注的注释的不同,在调用方法sayHello时只将记日志的逻辑切入进来,而在调用方法count时,将记日志和监控执行时间的逻辑都切入进来了。
小结: 本篇通过两个例子来了解SpringAOP的基本用法,通过面向切面在不侵入业务代码的前提下将一些公共逻辑织入到应用中,这为系统提供了非常强大的扩展性和良好的可维护性。
SpringAOP在实际中应用非常广泛,从SpringFramework自己内置的SpringCache与SpringTransaction等就是通过SpringAOP来扩展实现的;另外,在诸如日志、事务、安全、性能监控等各方面应用非常广泛。下面的章节我们将介绍SpringAOP的基本概念与实现原理。