文章目录
AOP是OOP的延续,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,使业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高 了开发的效率。
AOP基础
AOP(Aspect Oriented Programming)面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
AOP术语
先了解一下AOP相关的重要术语:
项 | 描述 |
---|---|
切面 Aspect | 一个具有一组API的模块,提供交叉要求;应用程序可以拥有任意数量的切面。 |
连接点 Join point | 应用程序中可以插入AOP 切面的点(java的方法和异常处理代码块)。即,应用程序中使用Spring AOP框架采取操作的实际位置。 |
通知 Advice | 方法执行之前或之后采取的实际操作。即Spring AOP框架的程序执行期间调用的实际代码片段。 |
切点 Pointcut | 是一组一个或多个连接点,在其中应该执行的通知。可使用表达式或模式指定切入点。 |
介绍 Introduction | 允许添加新方法或属性到现有的类中。 |
目标对象 Target object | 被一个或者多个方面所通知的对象,这个对象永远是一个被代理对象。也称为被通知对象。 |
编织 Weaving | 将切面与其他应用程序类型或对象进行链接以创建通知对象的过程。这些可以在编译时,类加载时和运行时完成。 |
通知类型
Spring中使用的五种通知类型:
通知 | 描述 |
---|---|
before 前置通知 | 在方法执行之前通知。 |
after 后置通知 | 在方法执行之后(不考虑执行结果)执行通知。 |
after-returning 返回后通知 | 只有在方法执行成功后,才会通知。 |
after-throwing 抛出异常后通知 | 只有在方法因抛出异常完成时,才会通知。 |
around 环绕通知 | 在调用通知方法之前和之后,都可运行通知。 |
切点指示符
使用@Pointcut
来定义切点:
@Pointcut("within(@org.springframework.stereotype.Repository *)")
public void repositoryClassMethods() {}
@Pointcut("@annotation(com.example.study.service.aop.RequestLogger)")
public void logPointcut(){}
切点指示符是切点定义的关键字,切点表达式以切点指示符开始。有以下9种切点指示符:execution、within、this、target、args、@target、@args、@within、@annotation
。
切点匹配
可使用与(&&)、或(||)、非(!)来组合切入点表达式。
类型匹配时,可使用通配符:
*
:匹配任意数量字符;..
:匹配任何数量字符的重复;在类型模式中匹配任何数量子包,而在方法参数模式中匹配任意数量参数(0或多个)。+
:匹配指定类型的子类型;仅能作为后缀放在类型模式后边
java.*.String : 匹配java包下的任何“一级子包”下的String类型;
java..* : 匹配java包及任何子包下的任何类型;
java.lang.*ing : 匹配任何java.lang包下的以ing结尾的类型;
java.lang.Number+ : 匹配java.lang包下的任何Number的自类型;如java.lang.Integer,java.math.BigInteger
模式 | 描述 |
---|---|
public * *(…) | 任何公共方法的执行 |
* cn.javass…IPointcutService.*() | cn.javass包及所有子包下IPointcutService接口中的任何无参方法 |
* cn.javass….(…) | cn.javass包及所有子包下任何类的任何方法 |
* (!cn.javass…IPointcutService+).*(…) | 非“cn.javass包及所有子包下IPointcutService接口及子类型”的任何方法 |
* cn.javass…IPointcutService+.*() | cn.javass包及所有子包下IPointcutService接口及子类型的的任何无参方法 |
* cn.javass…IPointcut*(…) throws IllegalArgumentException | cn.javass包及所有子包下IPointcut前缀类型的的任何方法,且抛出IllegalArgumentException异常 |
@java.lang.Deprecated * *(…) | 任何持有@java.lang.Deprecated注解的方法 |
@java.lang.Deprecated @cn.javass…Secure * *(…) | 任何持有@java.lang.Deprecated和@cn.javass…Secure注解的方法 |
@(java.lang.Deprecated || cn.javass…Secure) * *(…) | 任何持有@java.lang.Deprecated或@ cn.javass…Secure注解的方法 |
(@cn.javass…Secure *) *(…) | 任何返回值类型持有@cn.javass…Secure的方法 |
execution
execution是一种使用频率比较高的切点指示符,用来匹配方法签名(使用全限定名,包括访问修饰符(public/private/protected)、返回类型,包名、类名、方法名、参数)。
@Pointcut("execution(public String com.example.study.findById(Long))")
within
within用来匹配指定类型内的方法,如匹配某个包下面所有类的方法(包括子包下面的所有类方法):
@Pointcut("within(com.example.study..*)")
this和target
this用来匹配的连接点所属的对象引用是某个特定类型的实例,target用来匹配的连接点所属目标对象必须是指定类型的实例。通俗的来讲就是,如果当前要代理的类对象没有实现某个接口的话,则使用this;如果当前要代理的目标对象有实现了某个接口的话,则使用target:
@Pointcut("target(com.example.study.BarDao)")
args
匹配方法中的参数,匹配第一个参数类型为UserModel的所有方法:
@Pointcut("args(com.example.study.UserModel,..)")
@target与@within
@target匹配的目标对象的类有一个指定的注解:
@target(com.example.study.Anno1)
@within指定匹配必须包含某个注解的类里的所有连接点:
@within(com.example.study.Anno1)
两种的区别:
- @target(Anno1):判断被调用的目标对象中是否声明了注解Anno1,如果有,会被拦截;
- @within(Anno1): 判断被调用的方法所属的类中是否声明了注解Anno1,如果有,会被拦截;
- @target关注的是被调用的对象,@within关注的是调用的方法所在的类;
@annotation
匹配有指定注解的方法(注解作用在方法上面)
@annotation(com.example.study.Anno1)
@args
方法参数所属的类型的类上有指定的注解,被匹配:
@args(com.example.study.Anno1)
第一个参数类型的类上有注解Anno1时匹配。
注解AOP示例
以定义一个controller上的AOP注解为例。
依赖包
为例使用AOP,需要添加对应依赖:
<!--springBoot的aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
定义注解
定义函数上的注解,用于记录函数相关日志:
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLogger {
}
实现AOP
通过@Aspect来定义一个切面类,通过@Component注册到容器中。
通过@Pointcut定义切面与注解关联;然后定义对应的通知:
@Aspect
@Component
public class RequestLogImpl {
private final Logger _logger = LoggerFactory.getLogger(this.getClass());
public RequestLogImpl(){
_logger.info("AOP load");
}
@Pointcut("@annotation(com.example.study.service.aop.RequestLogger)")
public void logPointcut(){}
@Before("logPointcut()")
public void doBeforeLog(JoinPoint point){
_logger.info("before");
}
@Around("logPointcut()")
public Object doAroundLog(ProceedingJoinPoint point) throws Throwable {
String methodName = point.getSignature().toShortString();
Object[] methodArgs = point.getArgs();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
_logger.info("around before {}", methodName);
for(Object arg:methodArgs){
_logger.info("arg: {}", arg);
}
Object result = point.proceed();
_logger.info("around after {}", methodName);
return result;
}
@After("logPointcut()")
public void doAfterLog(JoinPoint point){
_logger.info("after");
}
}
通过RequestContextHolder.getRequestAttributes()
可以获取rest请求与应答。
使用AOP
在rest请求函数上添加注解,即会自动注入日志记录功能:
@RequestLogger
@GetMapping("testApi")
public String testApi(HttpServletRequest request, String echo){
_logger.info("testApi");
return echo;
}
各切面的执行顺序是:
- around before
- before
- testApi
- around after
- after