Spring 框架的一个关键组件是面向切面的编程(AOP)框架。面向方面的编程需要把程序逻辑分解成不同的部分称为所谓的关注点。跨一个应用程序的多个点的功能被称为横切关注点,这些横切关注点在概念上独立于应用程序的业务逻辑。有各种各样的常见的很好的方面的例子,如日志记录、审计、声明式事务、安全性和缓存等。
在 OOP 中,关键单元模块度是类,而在 AOP 中单元模块度是方面。依赖注入帮助你对应用程序对象相互解耦和 AOP 可以帮助你从它们所影响的对象中对横切关注点解耦。
Spring AOP 模块提供拦截器来拦截一个应用程序,例如,当执行一个方法时,你可以在方法执行之前或之后添加额外的功能。
下面是关于AOP的一些术语:
Aspect | 切面,一个模块具有一组提供横切需求的 APIs。例如,一个日志模块为了记录日志将被 AOP 方面调用。应用程序可以拥有任意数量的方面,这取决于需求。 |
Join point | 切点,在你的应用程序中它代表一个点,你可以在插件 AOP 方面。你也能说,它是在实际的应用程序中,其中一个操作将使用 Spring AOP 框架。 |
Advice | 这是实际行动之前或之后执行的方法。这是在程序执行期间通过 Spring AOP 框架实际被调用的代码。 |
Pointcut | 这是一组一个或多个连接点,通知应该被执行。可以使用execution表达式或模式指定切入点 |
Introduction | 引用允许你添加新方法或属性到现有的类中。 |
Target object | 被一个或者多个方面所通知的对象,这个对象永远是一个被代理对象。也称为被通知对象。 |
Weaving | 把切面连接到其它应用程序类型或者对象上并创建一个被通知的对象。这些可以在编译时、类加载时和运行时完成。 |
通过一个例子来学习切面编程,先新建一个spring boot 的项目,引入aop依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
新建一个controller:
package com.springboot.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
@GetMapping("/")
public String index() {
System.out.println("index");
return "index";
}
}
运行项目,浏览器输出:
控制台输出:
项目运行正常。
新建类:
@Aspect
@Component
public class PrintAdvice {
@Pointcut("execution(* com.springboot.controller.*(..))..*")
public void print() {
}
@Before("print()")
public void beforeAdvice() {
System.out.println("start....");
}
@After("print()")
public void afterAdvice() {
System.out.println("end......");
}
}
运行报错:
Caused by: java.lang.IllegalArgumentException: warning no match for this type name: com.springboot.controller [Xlint:invalidAbsoluteTypeName]
将
@Pointcut("execution(* com.springboot.controller.*(..))..*")
改为:
@Pointcut("execution(* com.springboot.controller..*(..))..*")
再次运行,项目起来了,控制台输出:
· 成功!
在这个例子中,只是用before和after两种通知类型,但spring支持5种通知类型:
前置通知 before | 在一个方法执行之前,执行通知。 |
后置通知 after | 在一个方法执行之后,不考虑其结果,执行通知。 |
返回后通知 AfterReturning | 在一个方法执行之后,只有在方法成功完成时,才能执行通知。 |
抛出异常后通知 AfterThrowing | 在一个方法执行之后,只有在方法退出抛出异常时,才能执行通知。 |
环绕通知 Around | 在建议方法调用之前和之后,执行通知。 |
接下来修改PrintAdvice类,添加AfterReturning和AfterThrowing通知:
@Aspect
@Component
public class PrintAdvice {
@Pointcut("execution(* com.springboot.controller..*(..))..*")
public void print() {
}
@Before("print()")
public void beforeAdvice() {
System.out.println("beforeAdvice...");
}
@After("print()")
public void afterAdvice() {
System.out.println("afterAdvice...");
}
@AfterReturning("print()")
public void afterReturningAdvice() {
System.out.println("afterReturningAdvice...");
}
@AfterThrowing("print()")
public void afterThrowingAdvice() {
System.out.println("afterThrowingAdvice...");
}
}
重启项目,再次访问,控制台输出:
然后修改IndexController:
@RestController
public class IndexController {
@GetMapping("/")
public String index() {
System.out.println("index");
printThrowException();
return "index";
}
public void printThrowException() {
System.out.println("Exception raised");
throw new IllegalArgumentException();
}
}
这时控制台输出为:
最后再来看一下环绕通知,在PrintAdvice中加入代码:
@Around("print()")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("Around advice");
Object[] args=proceedingJoinPoint.getArgs();
if(args.length>0){
System.out.print("Arguments passed: " );
for (int i = 0; i < args.length; i++) {
System.out.println("arg "+(i+1)+": "+args[i]);
}
}
Object result=proceedingJoinPoint.proceed(args);
System.out.println("Returning " + result);
}
新建一个controller类:
package com.springboot.controller;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping("/{name}")
public String sayHello(@PathVariable String name) {
return "hello " + name;
}
}
访问localhost:8090/hello/song ,控制台显示:
注意,此时浏览器时空白的,不会显示返回值。因为PrintAdvice.aroundAdvice 方法的返回值类型就是void,更改此方法:
@Around("print()")
public String aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("Around advice");
Object[] args=proceedingJoinPoint.getArgs();
if(args.length>0){
System.out.print("Arguments passed: " );
for (int i = 0; i < args.length; i++) {
System.out.println("arg "+(i+1)+": "+args[i]);
}
}
Object result=proceedingJoinPoint.proceed(args);
System.out.println("Returning " + result);
return result.toString();
}
此时浏览器有值了,环绕通知可以控制返回对象,即你可以返回一个与目标对象完全不同的返回值,虽然这很危险,但是你却可以办到。而后置方法是无法办到的,因为他是在目标方法返回值后调用