AOP-Demo

一、概念

AOP:面向切面编程,是对面向对象编程的一种补充。

常见的业务场景:统一日志处理

二、背景

现在我有一段代码如下:

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>aop-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring.boot.version>2.2.2.RELEASE</spring.boot.version>
    </properties>

    <!--环境: SpringBoot + Spring MVC + Spring AOP-->

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>

        <!--基于 Spring Boot 的 Spring MVC-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>

    </dependencies>

</project>

CalculatorController.java

package com.example.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * Created by 19921224 on 2023/8/15 14:59
 */
@RestController
@RequestMapping("/cal")
public class CalculatorController {

    @GetMapping("/add/{a}/{b}")
    public Map<String, Object> add(@PathVariable("a") Integer a, @PathVariable("b") Integer b) {
        System.out.println(a + " + " + b + " = " + (a + b));
        Map<String, Object> map = new HashMap<>();
        map.put("code", "000000");
        map.put("msg", "成功");
        map.put("data", a + b);
        return map;
    }

    @GetMapping("/sub/{a}/{b}")
    public Map<String, Object> sub(@PathVariable("a") Integer a, @PathVariable("b") Integer b) {
        System.out.println(a + " - " + b + " = " + (a - b));
        Map<String, Object> map = new HashMap<>();
        map.put("code", "000000");
        map.put("msg", "成功");
        map.put("data", a - b);
        return map;
    }

    @GetMapping("/multi/{a}/{b}")
    public Map<String, Object> multi(@PathVariable("a") Integer a, @PathVariable("b") Integer b) {
        System.out.println(a + " * " + b + " = " + (a * b));
        Map<String, Object> map = new HashMap<>();
        map.put("code", "000000");
        map.put("msg", "成功");
        map.put("data", a * b);
        return map;
    }

    @GetMapping("/div/{a}/{b}")
    public Map<String, Object> div(@PathVariable("a") Integer a, @PathVariable("b") Integer b) {
        System.out.println(a + " / " + b + " = " + (a / b));
        Map<String, Object> map = new HashMap<>();
        map.put("code", "000000");
        map.put("msg", "成功");
        map.put("data", a / b);
        return map;
    }
}

现在我们访问看一下接口是否正常:

问题描述

可以看到,我在上面的4个方法中都添加了打印输出日志的功能,这样实在是比较麻烦,尤其当方法比较多以后,那该如何解决类似的日志输出问题呢?

三、使用 Spring AOP 改造

其原理如下:

对于上面的四个方法进行横切,每个方法横切后都会出现一个如下所示的切面,然后我们对这4个切面进行抽象为切面对象,即为面向切面编程。之后只需要把想实现的日志功能添加在这个抽象出来的切面对象中即可。

改造步骤

步骤一:自定义注解

我们需要让AOP知道目标方法在哪里(即AOP需要处理的是哪些方法),那怎么让它知道呢?这里我们采用通过 注解的方式,因此需要 自定义注解

package com.example.annotation;

import java.lang.annotation.*;

/**
 * Created by 19921224 on 2023/8/15 15:53
 * 自定义日志注解,至少需要如下 三个元注解(元注解:描述注解的注解)
 */
@Target(ElementType.METHOD)   // 该注解作用的范围
@Retention(RetentionPolicy.RUNTIME)    // 该注解运行的机制
@Documented
public @interface LogAnnotation {
    String value() default "";  // 这是一个方法,到时候就可以通过 @LogAnnotation("作用") 来使用了
}

步骤二:使用注解进行标记

将上面的自定义注解作用在你想要添加日志的方法上即可,改造后代码如下:

package com.example.controller;

import com.example.annotation.LogAnnotation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * Created by 19921224 on 2023/8/15 14:59
 */
@RestController
@RequestMapping("/cal")
public class CalculatorController {

    @LogAnnotation("加法")
    @GetMapping("/add/{a}/{b}")
    public Map<String, Object> add(@PathVariable("a") Integer a, @PathVariable("b") Integer b) {
        // System.out.println(a + " + " + b + " = " + (a + b));
        Map<String, Object> map = new HashMap<>();
        map.put("code", "000000");
        map.put("msg", "成功");
        map.put("data", a + b);
        return map;
    }

    @LogAnnotation("减法")
    @GetMapping("/sub/{a}/{b}")
    public Map<String, Object> sub(@PathVariable("a") Integer a, @PathVariable("b") Integer b) {
        // System.out.println(a + " - " + b + " = " + (a - b));
        Map<String, Object> map = new HashMap<>();
        map.put("code", "000000");
        map.put("msg", "成功");
        map.put("data", a - b);
        return map;
    }

    @LogAnnotation("乘法")
    @GetMapping("/multi/{a}/{b}")
    public Map<String, Object> multi(@PathVariable("a") Integer a, @PathVariable("b") Integer b) {
        // System.out.println(a + " * " + b + " = " + (a * b));
        Map<String, Object> map = new HashMap<>();
        map.put("code", "000000");
        map.put("msg", "成功");
        map.put("data", a * b);
        return map;
    }

    @LogAnnotation("除法")
    @GetMapping("/div/{a}/{b}")
    public Map<String, Object> div(@PathVariable("a") Integer a, @PathVariable("b") Integer b) {
        // System.out.println(a + " / " + b + " = " + (a / b));
        Map<String, Object> map = new HashMap<>();
        map.put("code", "000000");
        map.put("msg", "成功");
        map.put("data", a / b);
        return map;
    }
}

步骤三:实现切面的业务逻辑

即将日志功能统一添加到抽象出来的切面中,而这个切面是一个 对象(它是从一系列的切面中抽象出来的对象),那么我们要想对一个 对象 进行的相关的操作,是不是需要一个 来操作呢?因此,我们需要生成一个 切面类 (从而由这个 切面类 来生成 上图 3-1 中的 切面对象 ),如下:

切面对象
package com.example.aspect;

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 19921224 on 2023/8/15 16:11
 * 切面类
 */
@Component  // 由于是Spring Boot项目,因此已经使用到了Spring IoC,所以我们只需要向Spring容器表明它是容器中的一个组件即可,就会自动生成对象
// 同时需要注意:启动类中的配置扫描可以扫描到该类,这样才会自动注入进去IoC中
@Aspect // 另外这个对象,它不是一个普通的对象,它是一个特殊的对象,叫切面对象,因此需要用 @Aspect 注解单独来标志它是一个切面对象
public class LogAspect {

    // 将刚才 @LogAnnotation 注解标注的方法与 切面对象建立联系
    @Pointcut("@annotation(com.example.annotation.LogAnnotation)")   // 即 @LogAnnotation 注解标注的地方即为 切点
    public void logPointCut() {
        // 该方法仅仅是找出 切点的
    }

    // 真正实现的业务逻辑(此处是 日志打印功能)
    @Around("logPointCut()")    // 将切点与 around 联系起来
    public Object around(ProceedingJoinPoint point) throws Throwable {   // 不同的方法,传入的连接点对象不同
        // 日志输出
        String methodName = point.getSignature().getName();
        // 通过反射 获取 @LogAnnotation 注解的 value 描述
        //// 1. 先获取 切点 注解的 目标方法签名
        MethodSignature signature = (MethodSignature) point.getSignature();
        //// 2. 获取方法
        Method method = signature.getMethod();
        //// 3. 获取方法上的注解
        LogAnnotation annotation = method.getAnnotation(LogAnnotation.class);
        //// 如果 annotation 为 null,说明该方法上没有使用 @LogAnnotation 注解
        if (annotation != null) {
            //// 4. 获取注解的value值
            String value = annotation.value();
            System.out.println("【系统日志】 当前操作: " + value + "调用了 " + methodName + " 方法,返回值是: " + point.proceed());
        }


        // 调用连接点方法的返回值
        return point.proceed();
    }
}

然后这时候启动程序,请求接口,打印如下:

相关的示例代码:https://github.com/hello-github-ui/aop-demo

posted @ 2023-08-15 22:14  LoremMoon  阅读(8)  评论(1编辑  收藏  举报