假设你们公司的系统里面有一个匿名意见版块,所有人可以在上面匿名发表自己的意见,且意见可以被所有人看到。有一天,你的老板突然找到你,说要在匿名意见版块里面增加一个功能:将提意见者的真名和他的意见打印在后台日志里。你一边感慨老板的阴险,一边默默照做。
起初你想到直接在业务层里面打印这段日志,但感觉这样代码有所耦合,然后你又想到增加一个拦截器来打印日志,但是拦截器一般是做拦截校验的,与这个功能似乎不太匹配,突然灵光一闪你想到了可以用AOP来实现这个功能。
引入jar包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
原先service层的代码:
@Service
public class UserServiceImpl implements IUserService {
/**
* 匿名建议
* @param suggestion 建议内容
*/
@Override
public void anonymousSuggestion(String suggestion) {
//省略业务逻辑
System.out.println("将意见存入数据库");
}
}
在springboot的启动类上添加@EnableAspectJAutoProxy
,然后创建添加切面类
@Aspect
@Component
@Slf4j
public class ExampleAspect {
@Before("execution(public void com.czx.xxr.service.IUserService.anonymousSuggestion(String))")
public void log() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
String suggestion = request.getParameter("suggestion");
String username = (String)request.getSession().getAttribute("username");//用户名存在session中
// log.info(username + "-提出意见:"+ suggestion);
System.out.println(username + "-提出意见:"+ suggestion);
}
}
调用这个接口之后控制台打印出的结果:
可以看到,切面类的log()方法已经在service层的anonymousSuggestion(String)方法之前被执行了。
AOP面向切面编程,我的理解就是在代码的执行流程中间插入一个切面,这个切面中有多个切点,每一个切点其实就是一个方法,当代码流程经过切面时,先执行切点的方法,然后再继续执行下去。比如上面的例子,原本的代码执行流程是:控制器层→业务层→持久层,要添加一个打印日志的功能,我也可以直接在业务层中添加打印日志的代码,但是这样的话就是在业务层中加入了一段与业务处理无关的代码,增加了代码耦合。而我用AOP注解@Before的方式就相当于在控制器层和业务层之间架起了一个切点,代码的执行流程就变为了控制器层→切点→业务层→持久层,这样一来,我就把打印日志的功能放在这个切面层里,减少了代码的耦合。
SpringAOP的概念介绍
- Aspect (切面)用
@Aspect
注解加在一个类上来声明这个类是一个切面类,切面类上还需要使用@Component
将其标记未一个Bean。再切面中可以包含一些切点(Pointcut)和通知(Advice) - Pointcut (切点)。用
@Pointcut
注解加在一个方法上用来声明一个切点,切点的名字就是方法名 - Joint point(连接点)也就是业务流程在运行过程中需要插入切面的具体位置。
- Advice(通知)是切面的具体实现方法。可分为前置通知
@Before
、后置通知@AfterReturning
、异常通知@AfterThrowing
、最终通知@After
和环绕通知@Around
五种。 - Target(目标对象)被一个或者多个切面所通知的对象。
- Proxy(代理对象)
- Weaving(切入,也叫织入)将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译期、类装载期及运行期。
我将ExampleAspect类稍作改造
@Aspect
@Component
@Slf4j
public class ExampleAspect {
@Pointcut("execution(public void com.czx.xxr.service.IUserService.anonymousSuggestion(String))")//切点表达式
public void shareCut() {
//这个方法不需要方法体,方法名就是切点名称
}
@Before("shareCut()")
public void log() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
String suggestion = request.getParameter("suggestion");
String username = (String)request.getSession().getAttribute("username");//用户名存在session中
// log.info(username + "-提出意见:"+ suggestion);
System.out.println(username + "-提出意见:"+ suggestion);
}
}
我将原来@Before()
注解中的切点表达式放在了@Pointcut()
注解中,然后将@Before()
中的表达式改为shareCut()
(即切点名称)
上面这个切点会匹配在 IUserService类中 anonymousSuggestion方法的调用,并且需要该方法是 public 的,只有一个 Long 的参数。
匹配字符串中可以支持一些通配符:
切点中的指示器
切点的表达式以 指示器 开始, 指示器 就是一种关键字,用来告诉 Spring AOP 如何匹配连接点,Spring AOP 提供了以下几种指示器:
execution()
用于匹配连接点的执行方法,其实就是定义拦截哪些方法within()
限制连接点匹配指定的类型@annotation
限定匹配带有指定注解的连接点