AOP面向切面编程

AOP是一种与语言无关的程序思想、编程范式。项目业务逻辑中,将通用的模块以水平切割的方式进行分离统一处理,常用于日志、权限控制、异常处理等业务中。

快速导航

编程范式主要以下几大类

  • AOP(Aspect Oriented Programming)面向切面编程
  • OOP(Object Oriented Programming)面向对象编程
  • POP(procedure oriented programming)面向过程编程
  • FP(Functional Programming)面向函数编程

引入aop依赖

以下示例是基于Spring Boot实战系列(2)数据存储之Jpa操作MySQL /chapter2/chapter2-1可在Github获取源码

项目根目录 pom.xml 添加依赖 spring-boot-starter-aop

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-aop</artifactId>
  4. </dependency>

aop注解

  • @Aspect: 切面,由通知和切入点共同组成,这个注解标注在类上表示为一个切面。
  • @Joinpoint: 连接点,被AOP拦截的类或者方法,在前置通知中有介绍使用@Joinpoint获取类名、方法、请求参数。
  • Advice: 通知的几种类型
    • @Before: 前置通知,在某切入点@Pointcut之前的通知
    • @After: 后置通知,在某切入点@Pointcut之后的通知无论成功或者异常。
    • @AfterReturning: 返回后通知,方法执行return之后,可以对返回的数据做加工处理。
    • @Around: 环绕通知,在方法的调用前、后执行。
    • @AfterThrowing: 抛出异常通知,程序出错跑出异常会执行该通知方法。
  • @Pointcut: 切入点,从哪里开始。例如从某个包开始或者某个包下的某个类等。

实现日志分割功能

目录 aspect下 新建 httpAspect.java类,在收到请求之后先记录请求的相关参数日志信息,请求成功完成之后打印响应信息,请求处理报错打印报错日志信息。

  1. ```java
  2. package com.angelo.aspect;
  3. import com.google.gson.Gson;
  4. import com.google.gson.GsonBuilder;
  5. import org.aspectj.lang.JoinPoint;
  6. import org.aspectj.lang.ProceedingJoinPoint;
  7. import org.aspectj.lang.annotation.*;
  8. import org.slf4j.Logger;
  9. import org.slf4j.LoggerFactory;
  10. import org.springframework.stereotype.Component;
  11. import org.springframework.web.context.request.RequestContextHolder;
  12. import org.springframework.web.context.request.ServletRequestAttributes;
  13. import javax.servlet.http.HttpServletRequest;
  14. import java.util.HashMap;
  15. import java.util.Map;
  16. @Aspect
  17. @Component
  18. public class HttpAspect {
  19. // 打印日志模块
  20. private final static Logger logger = LoggerFactory.getLogger(HttpAspect.class);
  21. // 下面会一一介绍...

添加切入点

定义切入的入口在哪里,封装一个公共的方法实现复用

  1. ```java
  2. /**
  3. * 定义一个公共的方法,实现服用
  4. * 拦截UserController下面的所有方法
  5. * 拦截UserController下面的userList方法里的任何参数(..表示拦截任何参数)写法:@Before("execution(public * com.angelo.controller.UserController.userList(..))")
  6. */
  7. @Pointcut("execution(public * com.angelo.controller.UserController.*(..))")
  8. public void log() {
  9. }

前置通知

拦截方法之前的一段业务逻辑,获取请求的一些信息,其中用到了Gson处理对象转json输出

  1. ```java
  2. @Before("log()")
  3. public void doBefore(JoinPoint joinPoint) {
  4. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  5. HttpServletRequest request = attributes.getRequest();
  6. Map params = new HashMap();
  7. params.put("url", request.getRequestURL()); // 获取请求的url
  8. params.put("method", request.getMethod()); // 获取请求的方式
  9. params.put("ip", request.getRemoteAddr()); // 获取请求的ip地址
  10. params.put("className", joinPoint.getSignature().getDeclaringTypeName()); // 获取类名
  11. params.put("classMethod", joinPoint.getSignature().getName()); // 获取类方法
  12. params.put("args", joinPoint.getArgs()); // 请求参数
  13. // 输出格式化后的json字符串
  14. Gson gson = new GsonBuilder().setPrettyPrinting().create();
  15. logger.info("REQUEST: {}", gson.toJson(params));
  16. }

后置通知

拦截方法之后的一段业务逻辑

  1. ```java
  2. @After("log()")
  3. public void doAfter() {
  4. logger.info("doAfter");
  5. }

环绕通知

环绕通知是在方法的前后的一段逻辑操作,可以修改目标方法的返回值,第一个参数是org.aspectj.lang.ProceedingJoinPoint类型,注意这里要调用执行目标方法proceed()获取值返回,不然会造成空指针异常。在环绕通知里面也可以捕获错误返回。

  1. ```java
  2. @Around("log()")
  3. public Object doAround(ProceedingJoinPoint point) {
  4. try {
  5. Object o = point.proceed();
  6. System.out.println("方法环绕proceed,结果是 :" + o);
  7. logger.info("doAround1");
  8. return o;
  9. } catch (Throwable e) {
  10. // e.printStackTrace();
  11. logger.info("doAround2");
  12. return null;
  13. }
  14. }

返回后通知

在切入点完成之后的返回通知,此时就不会抛出异常通知,除非返回后通知的业务逻辑报错。

  1. ```java
  2. /**
  3. * 获取响应返回值
  4. * @param object
  5. */
  6. @AfterReturning(returning = "object", pointcut = "log()")
  7. public void doAfterReturning(Object object) {
  8. // logger.info("RESPONSE: {}", object); 会打印出一个对象,想打印出具体内容需要在定义模型处加上toString()
  9. logger.info("RESPONSE: {}", object.toString());
  10. }

异常通知

抛出异常后的通知,此时返回后通知@AfterReturning就不会执行。

  1. ```java
  2. @AfterThrowing(pointcut = "log()")
  3. public void doAfterThrowing() {
  4. logger.error("doAfterThrowing: {}", " 异常情况!");
  5. }

一段段伪代码读懂执行顺序

  1. try {
  2. // @Before 执行前通知
  3. // 执行目标方法
  4. // @Around 执行环绕通知 成功走finall,失败走catch
  5. } finally {
  6. // @After 执行后置通知
  7. // @AfterReturning 执行返回后通知
  8. } catch(e) {
  9. // @AfterThrowing 抛出异常通知
  10. }

测试正常异常两种情况

测试之前先对controller/UserController.java文件的userList方法增加了exception参数

  1. /**
  2. * 查询用户列表
  3. * @return
  4. */
  5. @RequestMapping(value = "/user/list/{exception}")
  6. public List<User> userList(@PathVariable("exception") Boolean exception) {
  7. if (exception) {
  8. throw new Error("测试抛出异常!");
  9. }
  10. return userRepository.findAll();
  11. }
  • 测试正常情况

curl 127.0.0.1:8080/user/list/false

正常情况返回值如下所示:

AOP面向切面编程 - 图1

  • 测试异常情况

curl 127.0.0.1:8080/user/list/true

异常情况返回值如下所示:

AOP面向切面编程 - 图2

通过以上两种情况测试可以看到环绕通知在正常、异常两种情况都可以执行到。

Github查看本文完整示例 chapter3-1