面向切面编程AOP
19面向切面编程AOP
一、Spring AOP简介
AOP即 Aspect Oriented Program 面向切面编程。首先,在面向切面编程的思想里面,把功能分为核心业务功能和周边业务功能。
- 所谓核心业务:比如登陆、增加数据、删除数据都叫核心业务
- 所谓周边功能:比如性能统计、日志、事务管理等等
周边功能在Spring的面向切面编程思想里,被定义为切面。
在面向切面编程思想里,核心业务和切面功能分别独立进行开发,然后把切面功能和核心业务功能"编织"在一起,这就叫AOP。
1.1 AOP的目的
AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务管理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。
1.2 AOP中的概念
概念 | 含义 |
---|---|
Aspect | 切面。周边功能 |
Join Point | 连接点,Spring AOP里总是代表一次方法执行。可以说目标对象中的方法就是一个连接点 |
Advice | 通知,在连接点执行的动作。在方法执行的时候(方法前、方法后、方法前后)做什么 |
Pointcut | 切入点,说明如何匹配连接点。即在哪些类,哪些方法上切入,连接点的集合 |
Introduction | 引入,为现有类型声明额外的方法和属性 |
Target object | 目标对象 |
AOP proxy | AOP代理对象,可以是JDK动态代理,也可以是CGLIB代理 |
Weaving | 织入,连接切面与目标对象或类型创建代理的过程。把切面加入到对象,并创建出代理对象的过程 |
1.3常用注解
• @Aspect:声明定义切面类
• @Pointcut:声明切点
• @Before:Advice的一种,方法执行前通知
• @After / @AfterReturning / @AfterThrowing:Advice的一种,方法执行后通知
• @Around:Advice的一种,环绕通知
1.4 举个例子
在上面的例子中,包租婆的核心业务就是签合同、收房租,那么这就够了,,灰色框起来的部分都是边缘的事情,交给中介就好了。这就是AOP的一个思想:让关注点代码与业务代码分离。
二、talk is cheap ,show me the code
2.1 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2 实现切面
package com.lucky.spring.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* Created by zhangdd on 2020/8/22
*/
@Aspect
@Component
@Slf4j
public class DaoOpsAspect {
@Pointcut("execution(* com.lucky.spring.dao..*(..))")
private void repositoryOps() {
}
@Around("repositoryOps()")
public Object logPerformance(ProceedingJoinPoint pjp) throws Throwable {
long startTime = System.currentTimeMillis();
String name = "-";
String result = "Y";
try {
name = pjp.getSignature().toShortString();
return pjp.proceed();
} catch (Throwable throwable) {
result = "N";
throw throwable;
} finally {
long endTime = System.currentTimeMillis();
log.info("{},{},{}ms", name, result, endTime - startTime);
}
}
}
- 创建Aspect类,通过@Aspect声明该类是一个切面
- 使用@Component声明该切面类是个Bean,让Spring容器管理
- 通过@Pointcut声明切入点
- 通常情况下使用execution模式可以完成绝大多数场景的设置,详细可以参考这里
- 这里声明的切入点repositoryOps是
com.lucky.spring.dao
这个包下的所有类的所有方法,即sql执行方法
- 通过@Around连接切点和切面的业务
- 业务里首先获取sql执行的方法名,执行的起始时间,结束时间,最后打印出了所用的时间
2.3 核心业务实现
@Service
public class CoffeeServiceImpl implements CoffeeService {
@Autowired
private CoffeeMapperExt coffeeMapperExt;
@Override
public List<Coffee> findAllCoffee() {
return coffeeMapperExt.queryAllCoffee();
}
}
- 这里以一个查询作为核心业务的内容
2.4 启动服务
package com.lucky.spring;
import com.lucky.spring.model.Coffee;
import com.lucky.spring.service.CoffeeService;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.List;
@SpringBootApplication
@MapperScan("com.lucky.spring.dao")
public class Application implements CommandLineRunner {
@Autowired
private CoffeeService coffeeService;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Override
public void run(String... args) throws Exception {
List<Coffee> allCoffee = coffeeService.findAllCoffee();
allCoffee.forEach(c -> System.out.println(c));
}
}
打印结果如下:
2020-08-22 13:45:14.020 INFO 24567 --- [ main] com.lucky.spring.aspect.DaoOpsAspect : CoffeeMapperExt.queryAllCoffee(),Y,332ms
Coffee(super=com.lucky.spring.model.Coffee@f6c1129, id=12, name=espresso, price=2000, createTime=Sun Aug 02 14:11:01 CST 2020, updateTime=Sun Aug 02 14:11:01 CST 2020)
Coffee(super=com.lucky.spring.model.Coffee@f897fb0, id=13, name=latte, price=2500, createTime=Sun Aug 02 14:11:01 CST 2020, updateTime=Sun Aug 02 14:11:01 CST 2020)
Coffee(super=com.lucky.spring.model.Coffee@ba2f9386, id=14, name=capuccino, price=2500, createTime=Sun Aug 02 14:11:01 CST 2020, updateTime=Sun Aug 02 14:11:01 CST 2020)
Coffee(super=com.lucky.spring.model.Coffee@5f716224, id=15, name=mocha, price=3000, createTime=Sun Aug 02 14:11:01 CST 2020, updateTime=Sun Aug 02 14:11:01 CST 2020)
Coffee(super=com.lucky.spring.model.Coffee@15642e3c, id=16, name=macchiato, price=3000, createTime=Sun Aug 02 14:11:01 CST 2020, updateTime=Sun Aug 02 14:11:01 CST 2020)
- 从日志中可以看出执行sql查询的时候,打印除了sql的执行时间
三、切入点 execution 表达式语法
Spring 的AOP支持多种切点的申明方式。我们上面使用了最常用的一种 execution。
还有如下切点匹配表达式,他们的主要区别就是粒度:
- execution:可以定义到方法的的最小粒度是参数的返回类型,修饰符,包名,类名,方法名,Spring AOP主要也是使用这个匹配表达式。
- within:只能定义到类
- this:当前生成的代理对象的类型匹配
- target:目标对象类型匹配
- args:只针对参数
使用形式如下:
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {}
@Pointcut("execution(* com.lucky.spring.dao..*(..))")
private void repositoryOps() {}
所以无论哪种表达式,语法还是要知道是什么意思的。
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
除了返回类型模式(ret-type-pattern)、名称模式(name-pattern)、参数模式(param-pattern)之外其他的都是可选的
- execution():表达式主体
- modifiers-pattern:模式修饰符 可选
- ret-type-pattern:返回类型模式。确定该方法的返回类型必须是什么才能与连接点匹配。
*
是最常用的返回类型,它匹配任何返回类型。 - declaring-type-pattern:声明类型模式 可选
- name-pattern:名称模式 ,名称是与方法名匹配的,可以使用通配符
*
作用名称的全部或一部分 - param-pattern:参数模式,参数模式稍微复杂一些
- ()匹配不带参数的方法
- (..)匹配任意数量(零个或多个)的参数
- (*)模式与采用任何类型的一个参数的方法匹配
- (*,String)与采用两个参数的方法匹配。第一个可以是任何类型,而第二个必须是字符串
- throws-pattern:异常模式 可选
下面是一些常见的切点表达式:
@Pointcut("execution(* com.lucky.spring.dao..*(..))")
private void repositoryOps() {
}
第一个*
代表 返回类型模式ret-type-pattern,*表示匹配所有返回类型com.lucky.spring.dao..
代表declaring-type-pattern,这里的两个..
代表当前com.lucky.spring.dao
包及其子包第二个*
表示方法名,*表示匹配所有方法(..)
表示任意数量的参数
execution(public * *(..))
返回值类型是public的方法
execution(* set*(..))
以set开头的方法
execution(* com.xyz.service.AccountService.*(..))
AccountService类中的所有方法
execution(* com..*.*Dao.find*(..))
匹配包名前缀为com的任何包下类名后缀为Dao的方法,方法名必须以find为前缀。如com.baobaotao.UserDao findByUserId()、com.baobaotao.dao.ForumDao findById()的方法都匹配切点
四、通知类型
上面的例子中使用了环绕通知,另外还有如下场景的通知类型。
-
前置通知(Before advice):在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。
-
后置通知(After returning advice):在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
-
异常通知(After throwing advice):在方法抛出异常退出时执行的通知。
-
最终通知(After (finally) advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
-
环绕通知(Around Advice):包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。