11.AOP开发

十一、AOP开发

1、Spring Boot实现 AOP

11.1.1、SpringBootAop简介

Spring Boot的AOP编程和Spring框架中AOP编程的唯一区别是:引入依赖的方式不同,其他内容完全一样

Spring Boot中AOP编程需要引入aop启动器:

复制代码
<!--aop启动器-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

可以看到当引入aop启动器之后会引入aop依赖aspectj依赖

  • aop依赖:如果只有这一个依赖,也可以实现AOP编程,这种方式表示使用了纯Spring AOP实现aop编程
  • aspectj依赖:一个独立的可以完成AOP编程的AOP框架,属于第三方的,不属于Spring框架(通常用它,因为它的功能更加强大)

11.1.2、SpringBootAop实现

实现功能:项目中很多service,要求执行任何service中的任何方法之前记录日志

(1)、引入依赖
复制代码
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.longdidi</groupId>
    <artifactId>springboot-11-001</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>21</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--aop启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
(2)、编写service
复制代码
package com.longdidi.service;

public interface UserService {

    /**
     * 保存用户信息
     *
     * @param id   用户id
     * @param name 用户名
     */
    void save(Long id, String name);

    /**
     * 根据id删除用户
     *
     * @param id 用户id
     */
    void deleteById(Long id);
}
(3)、编写service实现类
复制代码
package com.longdidi.service.impl;

import com.longdidi.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    @Override
    public void save(Long id, String name) {
        System.out.println("正在保存用户信息:" + name);
    }

    @Override
    public void deleteById(Long id) {
        System.out.println("正在删除用户" + id + "信息");
    }
}
(4)、编写切面
复制代码
package com.longdidi.component;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component // 纳入IoC容器
@Aspect // 指定该类为切面类
public class LoggingAspect {

    // 日期格式化器
    private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss SSS");

    // 定义切入点:匹配所有以 "service" 结尾的包下的所有方法
    // 切入点表达式
    @Pointcut("execution(* com.longdidi.service..*(..))")
    public void serviceMethods() {
    }

    // 前置通知
    // 切入点表达式:service包下任意类的任意方法
    @Before("execution(* com.longdidi.service..*.*(..))")
    public void sysLog(JoinPoint joinPoint) throws Throwable {
        StringBuilder log = new StringBuilder();
        LocalDateTime now = LocalDateTime.now();
        String strNow = formatter.format(now);
        // 追加日期
        log.append(strNow);
        // 追加冒号
        log.append(":");
        // 追加方法签名
        log.append(joinPoint.getSignature().getName());
        // 追加方法参数
        log.append("(");
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            log.append(args[i]);
            if (i < args.length - 1) {
                log.append(",");
            }
        }
        log.append(")");
        System.out.println(log);
    }
}
(5)、编写测试类
复制代码
package com.longdidi.controller;

import com.longdidi.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    @Autowired
    UserService userService;

    @RequestMapping("/testUser")
    public void testUser() {
        userService.save(1L, "jack");
        userService.deleteById(1L);
    }
}
(6)、测试

访问http://localhost:8080/testUser

查看控制台输出

2、统一异常处理

11.2.1、异常处理

在controller层如果程序出现了异常并且这个异常未被捕获,springboot提供的异常处理机制将生效

Spring Boot 提供异常处理机制主要是为了提高应用的健壮性和用户体验

它的好处包括

  1. 统一错误响应:可以定义全局异常处理器来统一处理各种异常,确保返回给客户端的错误信息格式一致,便于前端解析。
  2. 提升用户体验:能够优雅地处理异常情况,避免直接将技术性错误信息暴露给用户,而是显示更加友好的提示信息。
  3. 简化代码:开发者不需要在每个可能抛出异常的方法中重复编写异常处理逻辑,减少冗余代码,使业务代码更加清晰简洁。
  4. 增强安全性:通过控制异常信息的输出,防止敏感信息泄露,增加系统的安全性。

11.2.2、自适应异常机制

springboot会根据请求头的Accept字段来决定错误的响应格式

这种机制的好处就是:客户端设备自适应,提高用户的体验

在springboot-11-002中测试

不用编写任何代码,直接启动程序访问

测试返回html错误

测试返回json格式错误

11.2.3、SpringMVC的异常

重点:如果程序员使用了SpringMVC的错误处理方案,SpringBoot的错误处理方案不生效

(1)、局部控制

局部异常控制需要在方法上使用@ExceptionHandler注解进行标注

凡是这个控制器 当中出现了对应的异常,则走这个方法来进行异常的处理,只在当前控制器局部生效

【示例】在springboot-11-002模块中测试

复制代码
UserController.java

在控制器当中编写一个方法,方法使用@ExceptionHandler注解进行标注

复制代码
package com.longdidi.controller;

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

@RestController
public class UserController {

    @GetMapping("/resource/{id}")
    public String getResource(@PathVariable Long id) {
        if (id == 1) {
            throw new IllegalArgumentException("无效ID:" + id);
        }
        return "ID = " + id;
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public String handler(IllegalArgumentException e) {
        return "错误信息:" + e.getMessage();
    }
}

再编写一个OtherController,让它也发生IllegalArgumentException异常,看看它会不会走局部的错误处理机制

复制代码
package com.longdidi.controller;


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

@RestController
public class OtherController {
    @GetMapping("/resource2/{id}")
    public String getResource(@PathVariable Long id) {
        if (id == 1) {
            throw new IllegalArgumentException("无效ID:" + id);
        }
        return "ID = " + id;
    }
}
测试

访问http://localhost:8080/resource/1

访问http://localhost:8080/resource2/1

测试通过

(2)、全局控制

全局控制使用@ControllerAdvice + @ExceptionHandler

可以把局部生效的方法单独放到一个类当中,这个类使用@ControllerAdvice注解标注,凡是任何控制器 当中出现了对应的异常,则走这个方法来进行异常的处理

编写全局异常处理类

java 复制代码
package com.longdidi.config;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseBody
    public String handler(IllegalArgumentException e) {
        return "错误信息:" + e.getMessage();
    }
}

编写测试类

java 复制代码
package com.longdidi.controller;

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

@RestController
public class GlobalController {
    @GetMapping("/resource3/{id}")
    public String getResource(@PathVariable Long id) {
        if (id == 1) {
            throw new IllegalArgumentException("无效ID:" + id);
        }
        return "ID = " + id;
    }
}

测试http://localhost:8080/resource3/1

11.2.4、SpringBoot的异常

(1)、处理顺序

重点:如果SpringMVC没有对应的处理方案,会开启SpringBoot默认的错误处理方案

SpringBoot默认的错误处理方案如下

  1. 如果客户端要的是json,则直接响应json格式的错误信息
  2. 如果客户端要的是html页面,则按照下面方案
  • 第一步(精确错误码文件)

    classpath:/templates/error/目录下找404.html``500.html精确错误码.html文件

    如果找不到则去静态资源目录下的/error目录下找

    如果还是找不到进入第二步查找

  • 第二步(模糊错误码文件)

    classpath:/templates/error/目录下找4xx.html``5xx.html模糊错误码.html文件

    如果找不到则去静态资源目录下的/error目录下找

    如果还是找不到进入第三步

  • 第三步(通用错误页面)

    去找classpath:/templates/error.html位置查找

    如果找不到则进入第四步

  • 第四步(默认错误处理)

    如果上述所有步骤都未能找到合适的错误页面,Spring Boot 会使用内置的默认错误处理机制,即 /error 端点

【示例】springboot-11-003

项目结构

访问任意接口,该接口实际上不存在,因此出现404错误

访问http://localhost:8080/test

修改/templates/error/404.html页面名为任意名后继续测试

修改/static/error/404.html页面名为任意名后继续测试

修改/templates/error/4xx.html页面名为任意名后继续测试

修改/static/error/4xx.html页面名为任意名后继续测试

修改/templates/error.html页面名为任意名后继续测试

(2)、获取错误信息

Spring Boot 默认会在模型Model中放置以下信息

  • timestamp: 错误发生的时间戳
  • status: HTTP 状态码
  • error: 错误类型(如 "Not Found")
  • exception: 异常类名
  • message: 错误消息
  • trace: 堆栈跟踪

在thymeleaf中使用 ${message}即可取出信息

注意:springboot3.3.5 版本默认只向Model对象中绑定了timestamp``status``error。如果要保存exception``message``trace,需要开启以下三个配置:

properties 复制代码
server.error.include-stacktrace=always
server.error.include-exception=true
server.error.include-message=always

【示例】在模块springboot-11-004中测试

引入依赖

xml 复制代码
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>

在templates文件夹创建error.html页面

html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>error</title>
</head>
<body>
<h1>error通用错误</h1>
异常发生时间:<span th:text="${timestamp}"></span><br>
HTTP状态码:<span th:text="${status}"></span><br>
错误类型:<span th:text="${error}"></span><br>
异常类名:<span th:text="${exception}"></span><br>
错误信息:<span th:text="${message}"></span><br>
堆栈跟踪:<span th:text="${trace}"></span><br>
</body>
</html>

测试访问http://localhost:8080/test

添加如下配置

properties 复制代码
server.error.include-stacktrace=always
server.error.include-exception=true
server.error.include-message=always

再次访问测试

(3)、前后端分离异常

统一使用SpringMVC的错误处理方案,定义全局的异常处理机制:@ControllerAdvice + @ExceptionHandler

返回json格式的错误信息,其它的就不需要管了,因为前端接收到错误信息怎么处理是他自己的事儿。

(4)、服务器端错误处理方案

建议使用SpringBoot的错误处理方案

  1. 如果发生的异常是HTTP错误状态码
    1. 建议常见的错误码给定精确错误码.html
    2. 建议不常见的错误码给定模糊错误码.html
  2. 如果发生的异常不是HTTP错误状态码而是业务相关异常
    1. 在程序中处理具体的业务异常,自己通过程序来决定跳转到哪个错误页面
  3. 建议提供classpath:/templates/error.html来处理通用错误

11.2.5、实用异常示例

11.2.5.1、基于AOP的异常

项目结构图

引入依赖

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.longdidi</groupId>
    <artifactId>springboot-11-005</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>21</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

开启aop配置

properties 复制代码
spring.aop.proxy-target-class=true

定义异常处理

java 复制代码
package com.longdidi.config;

import jakarta.servlet.http.HttpServletResponse;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.io.IOException;
import java.io.PrintWriter;

@Aspect
@Component
public class GlobalAspect {
    private static final Logger logger = LoggerFactory.getLogger(GlobalAspect.class);

    //定义切入点
    //凡是注解了RequestMapping的方法都被拦截
    //@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    @Pointcut("execution(public * com.longdidi..*.*(..))")
    private void webPointcut() {
    }

    /**
     * 拦截web层异常、记录异常日志并返回友好信息到前端目前只拦截Exception,是否要拦截Error需再做考虑
     *
     * @parame异常对象
     */
    @AfterThrowing(pointcut = "webPointcut()", throwing = "e")
    public void handleThrowing(Exception e) {
        if (e != null) {
            e.printStackTrace();
            logger.error("发现异常!" + e.getMessage());
            //这里输入友好性信息
            writeContent("出现异常");
        }
    }

    /**
     * 将内容输出到浏览器
     *
     * @paramcontent输出内容
     */
    private void writeContent(String content) {
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .getResponse();
        response.reset();
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-Type", "text/plain;charset=UTF-8");
        response.setHeader("icop-content-type", "exception");
        PrintWriter writer = null;
        try {
            writer = response.getWriter();
        } catch (IOException e) {
            e.printStackTrace();
        }
        writer.print(content);
        writer.flush();
        writer.close();
    }

}

添加测试类

java 复制代码
package com.longdidi.controller;

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

@RestController
public class HelloController {
    @RequestMapping("/")
    public String hello() {
        int i = 0;
        if (1 / i == 2) {
            System.out.println(111);
        }
        return "hello";
    }
}

测试

http://localhost:8080/

11.2.5.2、基于注解的异常
(1)、项目结构
(2)、添加依赖

添加web依赖、lombok依赖、fastjson2依赖

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.longdidi</groupId>
    <artifactId>springboot-11-006</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>21</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 引入Lombock依赖 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2-extension-spring6 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2-extension-spring6</artifactId>
            <version>2.0.55</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.55</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
(3)、全局异常处理类
java 复制代码
package com.longdidi.exceptionHander;

import com.longdidi.result.ExceptionCodeEnum;
import com.longdidi.result.R;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.IOException;

@ControllerAdvice
public class ExceptionHander {
    private static final String logExceptionFormat = "Capture Exception By GlobalExceptionHandler: Code: %s Detail: %s";
    private static Logger log = LoggerFactory.getLogger(ExceptionHander.class);

    //运行时异常
    @ExceptionHandler(RuntimeException.class)
    @ResponseBody
    public R runtimeExceptionHandler(RuntimeException ex) {
        return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED1, ex);
    }

    //空指针异常
    @ExceptionHandler(NullPointerException.class)
    @ResponseBody
    public R nullPointerExceptionHandler(NullPointerException ex) {
        return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED2, ex);
    }

    //类型转换异常
    @ExceptionHandler(ClassCastException.class)
    @ResponseBody
    public R classCastExceptionHandler(ClassCastException ex) {
        return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED3, ex);
    }

    //IO异常
    @ExceptionHandler(IOException.class)
    @ResponseBody
    public R iOExceptionHandler(IOException ex) {
        return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED4, ex);
    }

    //未知方法异常
    @ExceptionHandler(NoSuchMethodException.class)
    @ResponseBody
    public R noSuchMethodExceptionHandler(NoSuchMethodException ex) {
        return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED5, ex);
    }

    //数组越界异常
    @ExceptionHandler(IndexOutOfBoundsException.class)
    @ResponseBody
    public R indexOutOfBoundsExceptionHandler(IndexOutOfBoundsException ex) {
        return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED6, ex);
    }

    //400错误
    @ExceptionHandler({HttpMessageNotReadableException.class})
    @ResponseBody
    public R requestNotReadable(HttpMessageNotReadableException ex) {
        return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED7, ex);
    }

    //400错误
    @ExceptionHandler({TypeMismatchException.class})
    @ResponseBody
    public R requestTypeMismatch(TypeMismatchException ex) {
        return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED8, ex);
    }

    //400错误
    @ExceptionHandler({MissingServletRequestParameterException.class})
    @ResponseBody
    public R requestMissingServletRequest(MissingServletRequestParameterException ex) {
        return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED9, ex);
    }

    //405错误
    @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
    @ResponseBody
    public R request405(HttpRequestMethodNotSupportedException ex) {
        return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED10, ex);
    }

    //406错误
    @ExceptionHandler({HttpMediaTypeNotAcceptableException.class})
    @ResponseBody
    public R request406(HttpMediaTypeNotAcceptableException ex) {
        return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED11, ex);
    }

    //500错误
    @ExceptionHandler({ConversionNotSupportedException.class, HttpMessageNotWritableException.class})
    @ResponseBody
    public R server500(RuntimeException ex) {
        return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED12, ex);
    }

    //栈溢出
    @ExceptionHandler({StackOverflowError.class})
    @ResponseBody
    public R requestStackOverflow(StackOverflowError ex) {
        return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED13, ex);
    }

    //除数不能为0
    @ExceptionHandler({ArithmeticException.class})
    @ResponseBody
    public R arithmeticException(ArithmeticException ex) {
        return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED14, ex);
    }

    //其他错误
    @ExceptionHandler({Exception.class})
    @ResponseBody
    public R exception(Exception ex) {
        return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED15, ex);
    }

    private R resultFormat(ExceptionCodeEnum codeEnum, Throwable ex) {
        ex.printStackTrace();
        //log.error(String.format(logExceptionFormat, ex.getMessage()));
        return R.FAIL(codeEnum);
    }
}
(4)、错误枚举类
java 复制代码
package com.longdidi.result;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@NoArgsConstructor
@AllArgsConstructor
public enum ExceptionCodeEnum {

    OK(200, "成功"),
    FAIL(400, "失败"),
    BAD_REQUEST(400, "请求错误"),
    NOT_FOUND(404, "未找到资源"),
    INTERNAL_ERROR(500, "内部服务器错误"),
    MODIFICATION_FAILED(400, "修改失败"),
    DELETION_FAILED(400, "删除失败"),
    CREATION_FAILED(400, "创建失败"),
    EXCEPTION_FAILED1(1, "异常1"),
    EXCEPTION_FAILED2(2, "异常2"),
    EXCEPTION_FAILED3(3, "异常3"),
    EXCEPTION_FAILED4(4, "异常4"),
    EXCEPTION_FAILED5(5, "异常5"),
    EXCEPTION_FAILED6(6, "异常6"),
    EXCEPTION_FAILED7(7, "异常7"),
    EXCEPTION_FAILED8(8, "异常8"),
    EXCEPTION_FAILED9(9, "异常9"),
    EXCEPTION_FAILED10(10, "异常10"),
    EXCEPTION_FAILED11(11, "异常11"),
    EXCEPTION_FAILED12(12, "异常11"),
    EXCEPTION_FAILED13(13, "异常13"),
    EXCEPTION_FAILED14(14, "异常14"),
    EXCEPTION_FAILED15(15, "异常15");

    @Getter
    @Setter
    private int code;
    @Getter
    @Setter
    private String msg;

}
(5)、统一数据返回类
java 复制代码
package com.longdidi.result;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class R<T> {
    private int code; // 响应的状态码
    private String msg; // 响应的消息
    private T data; // 响应的数据体

    // 用于构建成功的响应,不携带数据
    public static <T> R<T> OK() {
        return R.<T>builder()
                .code(ExceptionCodeEnum.OK.getCode())
                .msg(ExceptionCodeEnum.OK.getMsg())
                .build();
    }

    // 用于构建成功的响应,携带数据
    public static <T> R<T> OK(T data) {
        return R.<T>builder()
                .code(ExceptionCodeEnum.OK.getCode())
                .msg(ExceptionCodeEnum.OK.getMsg())
                .data(data)
                .build();
    }

    // 用于构建失败的响应,不带任何参数,默认状态码为400,消息为"失败"
    public static <T> R<T> FAIL() {
        return R.<T>builder()
                .code(ExceptionCodeEnum.FAIL.getCode())
                .msg(ExceptionCodeEnum.FAIL.getMsg())
                .build();
    }

    // 用于构建失败的响应,自定义状态码和消息
    public static <T> R<T> FAIL(ExceptionCodeEnum codeEnum) {
        return R.<T>builder()
                .code(codeEnum.getCode())
                .msg(codeEnum.getMsg())
                .build();
    }
}
(6)、JSON配置
properties 复制代码
#spring.web.resources.add-mappings=false

fastjson.date-format=yyyy-MM-dd HH:mm:ss
fastjson.charset=UTF-8
(7)、测试类
java 复制代码
package com.longdidi.controller;

import com.alibaba.fastjson2.JSONObject;
import com.longdidi.result.R;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class JSONController {
    /**
     * 测试正常返回
     *
     * @return
     */
    @GetMapping("/json/getStudent")
    public R getStudent() {
        JSONObject student = new JSONObject();
        student.put("age", "11");
        student.put("name", "学习笔记");
        return R.builder().data(student).build();
    }

    /**
     * 测试抛出RuntimeException异常
     *
     * @return
     */
    @RequestMapping("/json/getUserException")
    public R getUserException() {
        throw new RuntimeException();
    }

    /**
     * 测试抛出全局异常处理
     *
     * @return
     */
    @RequestMapping("/json/getAllException")
    public R getAllException() throws Exception {
        throw new Exception();
    }
}
(8)、测试

访问http://localhost:8080/json/getStudent

访问http://localhost:8080/json/getUserException

访问http://localhost:8080/json/getAllException

3、统一日志处理

11.3.1、WEB日志

(1)、项目结构
(2)、引入依赖

因为需要对web请求做切面来记录日志,所以引入web模块和aop模块,当然一些工具的依赖也需要引入

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.longdidi</groupId>
    <artifactId>springboot-11-007</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>21</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--引用工具-->
        <!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2-extension-spring6 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2-extension-spring6</artifactId>
            <version>2.0.55</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.55</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.36</version>
        </dependency>
        <dependency>
            <groupId>eu.bitwalker</groupId>
            <artifactId>UserAgentUtils</artifactId>
            <version>1.21</version>
        </dependency>
        <!--引用AOP-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
(3)、开启代理

开启AOP的配置,其中spring.aop.auto属性默认是开启的,也就是说只要引入了AOP依赖后,默认已经增加了@EnableAspectJAutoProxy

properties 复制代码
spring.aop.auto=true

当需要使用CGLIB来实现AOP的时候,需要配置spring.aop.proxy-target-class=true

properties 复制代码
spring.aop.proxy-target-class=true
(4)、WEB接口
java 复制代码
package com.longdidi.controller;

import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class LogController {
    @RequestMapping("/hello")
    public String hello() {
        int i = 0;
        if (1 / i == 2) {
            System.out.println(111);
        }
        return "hello";
    }

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public String login() {

        return "success";
    }

    @RequestMapping(value = "/echo", method = RequestMethod.GET)
    public String login(String name) {
        return "hello," + name;
    }

    @RequestMapping("/test")
    public String hello(@RequestBody Map<String, Object> str) {
        Object aaa = str.get("aaa");
        Object bbb = str.get("bbb");
        System.out.println("第一个参数:" + aaa);
        System.out.println("第二个参数:" + bbb);
        int i = 1;
        if (1 / i == 2) {
            System.out.println(111);
        }
        return "hello";
    }
}
(5)、日志切面
java 复制代码
package com.longdidi.config;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.longdidi.utils.IpUtils;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

@Aspect
@Component
@Order(1)
public class WebLogAspect {
    private static Logger logger = LoggerFactory.getLogger(WebLogAspect.class);

    ThreadLocal<Long> startTime = new ThreadLocal<>();

    /**
     * 此处定义切入点
     */
    @Pointcut("execution(public * com.longdidi.controller..*.*(..))")
    public void webLog() {
    }

    /**
     * 前置通知:方法调用前被调用
     * 通过JoinPoint 获取通知的签名信息:如目标方法名、目标方法参数信息等
     * 把接口中的requestMsg参数重新设置进request
     *
     * @param joinPoint
     */
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        logger.info("aop Before");
        //设置方法开始执行时间
        startTime.set(System.currentTimeMillis());
        //获取request
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
        Map<String, String[]> parameterMap = request.getParameterMap();
        StringBuffer sb = new StringBuffer();
        // ip地址
        String ipAddr = IpUtils.getIpAddr(request);
        sb.append("\n【请求 URL】:").append(request.getRequestURL().toString());
        sb.append("\n【请求 IP】:").append(ipAddr);

        sb.append("\n【请求类名】:").append(joinPoint.getSignature().getDeclaringTypeName());
        sb.append("\n【请求方法名】:").append(joinPoint.getSignature().getName());
        sb.append("\n【Http方法】:").append(request.getMethod());


        Object[] args = joinPoint.getArgs();
        Object[] arguments = new Object[args.length];
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse || args[i] instanceof MultipartFile) {
                //ServletRequest不能序列化,从入参里排除,否则报异常:java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
                //ServletResponse不能序列化 从入参里排除,否则报异常:java.lang.IllegalStateException: getOutputStream() has already been called for this response
                continue;
            }
            arguments[i] = args[i];
        }
        String paramter = "";
        if (arguments != null) {
            try {
                paramter = JSONObject.toJSONString(arguments);
            } catch (Exception e) {
                paramter = arguments.toString();
            }
        }

        //构造参数组集合
        List<Object> argList = new ArrayList<>();
        for (Object arg : joinPoint.getArgs()) {
            //request、response无法使用toJson
            if (arg instanceof HttpServletRequest) {
                argList.add("request");
            } else if (arg instanceof HttpServletResponse) {
                argList.add("response");
            } else {
                argList.add(JSON.toJSON(arg));
            }
        }
    }

    /**
     * 后置最终通知(目标方法只要执行完了就会执行后置通知方法)
     *
     * @After注解表示在方法执行之后执行
     */
    @After("webLog()")
    public void after(JoinPoint joinPoint) {
        logger.info("aop after");
    }

    @AfterReturning(pointcut = "webLog()")
    public void doAfterReturning(JoinPoint joinPoint) throws Throwable {
        logger.info("aop AfterReturning");
        //获取request
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
        //从切面植入点处通过反射机制获取植入点处的方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //获取切点所在的类
        String className = joinPoint.getSignature().getDeclaringTypeName();
        //获取切点所在的方法
        Method method = signature.getMethod();
        //IP
        String ipAddr = IpUtils.getIpAddr(request);
        //URL
        String url = request.getRequestURL().toString();
        StringBuffer sb = new StringBuffer();

        sb.append("\n【请求 URL】:").append(request.getRequestURL().toString());
        sb.append("\n【请求 IP】:").append(ipAddr);

        sb.append("\n【请求类名】:").append(joinPoint.getSignature().getDeclaringTypeName());
        sb.append("\n【请求方法名】:").append(joinPoint.getSignature().getName());
        sb.append("\n【Http方法】:").append(request.getMethod());

        sb.append("\n[耗时]:").append((System.currentTimeMillis() - startTime.get()) + "毫秒");

    }

    /**
     * 环绕操作
     *
     * @param proceedingJoinPoint 切入点
     * @return 原方法返回值
     * @throws Throwable 异常信息
     */
    @Around("webLog()")
    public Object aroundLog(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        //设置方法开始执行时间
        startTime.set(System.currentTimeMillis());
        //从切面植入点处通过反射机制获取植入点处的方法
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        //获取切点所在的方法
        Method method = signature.getMethod();
        //请求类名
        String className = proceedingJoinPoint.getTarget().getClass().getName();
        //获取请求的方法名
        String methodName = method.getName();
        //请求参数
        Object[] args = proceedingJoinPoint.getArgs();
        //将参数所在的数组转换成json
        //Stream<?> stream = ArrayUtils.isEmpty(args) ? Stream.empty() : Arrays.asList(args);
        //String params = JSON.toJSONString(args);


        //获取request
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();

        // ip地址
        String ipAddr = IpUtils.getIpAddr(request);
        Map<String, String[]> parameterMap = request.getParameterMap();
        StringBuffer sb = new StringBuffer();
        sb.append("\n【请求 URL】:").append(request.getRequestURL().toString());
        sb.append("\n【请求 IP】:").append(ipAddr);

        sb.append("\n【请求类名】:").append(className);
        sb.append("\n【请求方法名】:").append(methodName);
        sb.append("\n【Http方法】:").append(request.getMethod());
        //sb.append("\n【请求参数】:").append(params);

        //开始调用时间
        logger.info(sb.toString());
        try {
            Object result = proceedingJoinPoint.proceed();
            logger.info("请求类:" + className + ";方法:" + methodName + ";执行时间:" + (System.currentTimeMillis() - startTime.get()) + "毫秒");
            return result;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }

    }


    /**
     * 拦截web层异常,记录异常日志,并返回友好信息到前端目前只拦截Exception,是否要拦截Error需再做考虑
     * 当目标方法抛出异常返回后,将把目标方法抛出的异常传给通知方法;
     *
     * @parame异常对象
     */
    @AfterThrowing(pointcut = "webLog()", throwing = "exception")
    public void handleThrowing(JoinPoint joinPoint, Throwable exception) {
        if (exception != null) {
            exception.printStackTrace();
            logger.error("发现异常!" + exception.getMessage());
            //这里输入友好性信息
            writeContent("出现异常");
        }
    }

    /**
     * 将内容输出到浏览器
     *
     * @paramcontent输出内容
     */
    private void writeContent(String content) {
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .getResponse();
        response.reset();
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-Type", "text/plain;charset=UTF-8");
        response.setHeader("icop-content-type", "exception");
        PrintWriter writer = null;
        try {
            writer = response.getWriter();
        } catch (IOException e) {
            e.printStackTrace();
        }
        writer.print(content);
        writer.flush();
        writer.close();
    }
}
(6)、IP工具类
java 复制代码
package com.longdidi.utils;

import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.regex.Pattern;

public class IpUtils {
    private static final Logger logger = LoggerFactory.getLogger(IpUtils.class);

    public static final String _255 = "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";

    public static final Pattern pattern = Pattern.compile("^(?:" + _255 + "\\.){3}" + _255 + "$");


    public static String longToIpV4(long longIp) {

        int octet3 = (int) ((longIp >> 24) % 256);

        int octet2 = (int) ((longIp >> 16) % 256);

        int octet1 = (int) ((longIp >> 8) % 256);

        int octet0 = (int) ((longIp) % 256);

        return octet3 + "." + octet2 + "." + octet1 + "." + octet0;

    }


    public static long ipV4ToLong(String ip) {

        String[] octets = ip.split("\\.");

        return (Long.parseLong(octets[0]) << 24) + (Integer.parseInt(octets[1]) << 16)

                + (Integer.parseInt(octets[2]) << 8) + Integer.parseInt(octets[3]);

    }


    public static boolean isIPv4Private(String ip) {

        long longIp = ipV4ToLong(ip);

        return (longIp >= ipV4ToLong("10.0.0.0") && longIp <= ipV4ToLong("10.255.255.255"))

                || (longIp >= ipV4ToLong("172.16.0.0") && longIp <= ipV4ToLong("172.31.255.255"))

                || longIp >= ipV4ToLong("192.168.0.0") && longIp <= ipV4ToLong("192.168.255.255");

    }


    public static boolean isIPv4Valid(String ip) {

        return pattern.matcher(ip).matches();

    }
    /**
     * https://blog.csdn.net/shanchahua123456/article/details/84773292
     */

    /**
     * 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址;
     * <p>
     * 以下整理了各个代理服务器自己开发的转发服务请求头,这些请求头都不是标准的http请求头,不一定所有的代理都会带上这些请求头,所以通过这方式只能尽可能的获取到真实ip,但不能保证一定可以获取到真实ip,而且代理服务器请求头中获取的ip是可伪造的。
     * 参数:
     * <p>
     * X-Forwarded-For:Squid 服务代理
     * <p>
     * Proxy-Client-IP:apache 服务代理
     * <p>
     * WL-Proxy-Client-IP:weblogic 服务代理
     * <p>
     * HTTP_CLIENT_IP:有些代理服务器
     * <p>
     * X-Real-IP:nginx服务代理
     *
     * @param request
     * @return
     */
    /**
     * request.getRemoteAddr() 获取的值为0:0:0:0:0:0:0:1的原因及解决办法
     * <p>
     * 0:0:0:0:0:0:0:1 是IPV6 相当于127.0.0.1
     * <p>
     * 遇到了request.getRemoteAddr()获取的值为0:0:0:0:0:0:0:1,这是为什么呢,
     * 照道理讲,应该是127.0.0.1才对,为什么这个获取的值变成了ipv6了呢,而且我发现这种情况只有在服务器和客户端都在同一台电脑上才会出现
     * (例如用localhost访问的时候才会出现),
     * 后来上网查了查原因,原来是/etc/hosts这个东西作怪(在windows上应该是C:\Windows\System32\drivers\etc\hosts这个文件),
     * 只需要注释掉文件中的 # ::1 localhost 这一行即可解决问题。另外localhost这个文件很有用,这里你可以添加自己的条目,
     * 例如添加 192.168.0.212 myweb 这样子,在浏览器中原来只能使用192.168.0.212来访问的,并可以使用myweb来进行替换。
     *
     * @param request
     * @return
     */
    public static String getIpAddr(HttpServletRequest request) {
        //X-Forwarded-For:Squid 服务代理
        String ipAddress = request.getHeader("x-forwarded-for");
        //Proxy-Client-IP:apache 服务代理
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("Proxy-Client-IP");
        }
        //WL-Proxy-Client-IP:weblogic 服务代理
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("WL-Proxy-Client-IP");
        }
        //HTTP_CLIENT_IP:有些代理服务器
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("HTTP_CLIENT_IP");
        }
        //X-Real-IP:nginx服务代理
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("X-Real-IP");
        }
        //还是不能获取到,最后再通过request.getRemoteAddr();获取
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddr();
            String localIp = "127.0.0.1";
            String localIpv6 = "0:0:0:0:0:0:0:1";
            if (ipAddress.equals(localIp) || ipAddress.equals(localIpv6)) {
                // 根据网卡取本机配置的IP
                InetAddress inet = null;
                try {
                    inet = InetAddress.getLocalHost();
                    ipAddress = inet.getHostAddress();
                } catch (UnknownHostException e) {
                    //输出异常信息
                    e.printStackTrace();
                }
            }
        }
        //有些网络通过多层代理,那么获取到的ip就会有多个,一般都是通过逗号(,)分割开来,并且第一个ip为客户端的真实IP
        String ipSeparate = ",";
        int ipLength = 15;
        if (ipAddress != null && ipAddress.length() > ipLength) {
            if (ipAddress.indexOf(ipSeparate) > 0) {
                ipAddress = ipAddress.substring(0, ipAddress.indexOf(ipSeparate));
                //ipAddress = ipAddress.split(",")[0];
            }
        }
        return ipAddress;
    }

}
(7)、测试请求

http://localhost:8080/echo?name=张三

(8)、AOP切面的同步问题

在WebLogAspect切面中,分别通过doBefore和doAfterReturning两个独立函数实现了切点头部和切点返回后执行的内容,若想统计请求的处理时间,就需要在doBefore处记录时间,并在doAfterReturning处通过当前时间与开始处记录的时间计算得到请求处理的消耗时间。

可以在WebLogAspect切面中定义一个成员变量来给doBefore和doAfterReturning一起访问,为了解决同步提可以引入ThreadLocal对象

像下面这样进行记录

java 复制代码
@Aspect
@Component
@Order(1)
public class WebLogAspect {
    private static Logger logger = LoggerFactory.getLogger(WebLogAspect.class);

    ThreadLocal<Long> startTime = new ThreadLocal<>();

    /**
     * 此处定义切入点
     */
    @Pointcut("execution(public * com.longdidi.controller..*.*(..))")
    public void webLog() {
    }

    @AfterReturning(pointcut = "webLog()")
    public void doAfterReturning(JoinPoint joinPoint) throws Throwable {
        logger.info("aop AfterReturning");
        //获取request
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
        //从切面植入点处通过反射机制获取植入点处的方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //获取切点所在的类
        String className = joinPoint.getSignature().getDeclaringTypeName();
        //获取切点所在的方法
        Method method = signature.getMethod();
        //IP
        String ipAddr = IpUtils.getIpAddr(request);
        //URL
        String url = request.getRequestURL().toString();
        StringBuffer sb = new StringBuffer();

        sb.append("\n【请求 URL】:").append(request.getRequestURL().toString());
        sb.append("\n【请求 IP】:").append(ipAddr);

        sb.append("\n【请求类名】:").append(joinPoint.getSignature().getDeclaringTypeName());
        sb.append("\n【请求方法名】:").append(joinPoint.getSignature().getName());
        sb.append("\n【Http方法】:").append(request.getMethod());

        sb.append("\n[耗时]:").append((System.currentTimeMillis() - startTime.get()) + "毫秒");

    }
}
(9)、AOP切面的优先级

由于通过AOP程序得到了很好的解耦,但是也会带来一些问题。比如可能会对Web层做多个切面,校验用户,校验头信息等等,这个时候经常会碰到切面的处理顺序问题。

所以需要定义每个切面的优先级,可以使用@Order(i)注解来标识切面的优先级:i的值越小优先级越高

假设还有一个切面是CheckNameAspect用来校验name必须为didi,为其设置@Order(10),而上文中WebLogAspect设置为@Order(5),所以WebLogAspect有更高的优先级,这个时候执行顺序是这样的

  1. 在@Before中优先执行@Order(5)的内容,再执行@Order(10)的内容
  2. 在@After和@AfterReturning中优先执行@Order(10)的内容,再执行@Order(5)的内容

所以可以这样总结

  1. 在切入点前的操作,按order的值由小到大执行
  2. 在切入点后的操作,按order的值由大到小执行

11.3.2、Service日志

(1)、项目结构
(2)、引入依赖
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.longdidi</groupId>
    <artifactId>springboot-11-008</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--引用工具-->
        <!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2-extension-spring6 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2-extension-spring6</artifactId>
            <version>2.0.55</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.55</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.36</version>
        </dependency>
        <dependency>
            <groupId>eu.bitwalker</groupId>
            <artifactId>UserAgentUtils</artifactId>
            <version>1.21</version>
        </dependency>
        <!--引用AOP-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
(3)、定义监控注解
java 复制代码
package com.longdidi.monitor;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 系统日志记录监控器
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogMonitor {

    /**
     * 模块信息
     */
    String value();
}
(4)、配置日志切面
java 复制代码
package com.longdidi.config;

import com.longdidi.monitor.LogMonitor;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.text.SimpleDateFormat;

@Component
@Aspect
public class LogAspect {
    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    @Pointcut("@annotation(com.longdidi.monitor.LogMonitor)")
    public void pointCut() {

    }

    @Around(value = "pointCut()  && @annotation(systemLogRecordMonitor)")
    public Object recordSystemLog(ProceedingJoinPoint joinPoint, LogMonitor systemLogRecordMonitor) throws Throwable {
        long startTime = System.currentTimeMillis();
        String format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(startTime));

        Object result = joinPoint.proceed(joinPoint.getArgs());
        long endTime = System.currentTimeMillis();
        String value = systemLogRecordMonitor.value();
        String userName = "参数应从result中获取";
        // 权限框架使用的是SpringSecurity
        //var authentication = SecurityContextHolder.getContext().getAuthentication();
        //var username = authentication.getName();
        LOGGER.info("用户 [ {} ] 于 [ {} ]  访问了 [{}] 模块 耗时 {}/MS.", userName, format, value, (endTime - startTime));
        return result;
    }

}
(5)、定义Service
java 复制代码
package com.longdidi.service;

import com.longdidi.monitor.LogMonitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    @LogMonitor("删除")
    public void delete() {
        LOGGER.info("一级 delete");
    }

    @LogMonitor("创建")
    public void create() {
        LOGGER.info("一级 create");
    }

    @LogMonitor("更新")
    public void update() {
        LOGGER.info("一级 update");
    }

    @LogMonitor("获取全部")
    public void getAll() {
        LOGGER.info("一级 getAll");
    }
}
(6)、定义测试Controller
java 复制代码
package com.longdidi.controller;

import com.longdidi.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LogController {
    @Autowired
    UserService userService;

    @RequestMapping("/delete")
    public void delete() {
        userService.delete();
    }

    @RequestMapping("/create")
    public void create() {
        userService.create();
    }

    @RequestMapping("/update")
    public void update() {
        userService.update();
    }

    @RequestMapping("/getAll")
    public void getAll() {
        userService.getAll();
    }
}
(7)、测试访问

http://localhost:8080/create

lt中获取";

// 权限框架使用的是SpringSecurity

//var authentication = SecurityContextHolder.getContext().getAuthentication();

//var username = authentication.getName();

LOGGER.info("用户 [ {} ] 于 [ {} ] 访问了 [{}] 模块 耗时 {}/MS.", userName, format, value, (endTime - startTime));

return result;

}

}

复制代码
#### (5)、定义Service

```java
package com.longdidi.service;

import com.longdidi.monitor.LogMonitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    @LogMonitor("删除")
    public void delete() {
        LOGGER.info("一级 delete");
    }

    @LogMonitor("创建")
    public void create() {
        LOGGER.info("一级 create");
    }

    @LogMonitor("更新")
    public void update() {
        LOGGER.info("一级 update");
    }

    @LogMonitor("获取全部")
    public void getAll() {
        LOGGER.info("一级 getAll");
    }
}
(6)、定义测试Controller
java 复制代码
package com.longdidi.controller;

import com.longdidi.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LogController {
    @Autowired
    UserService userService;

    @RequestMapping("/delete")
    public void delete() {
        userService.delete();
    }

    @RequestMapping("/create")
    public void create() {
        userService.create();
    }

    @RequestMapping("/update")
    public void update() {
        userService.update();
    }

    @RequestMapping("/getAll")
    public void getAll() {
        userService.getAll();
    }
}
(7)、测试访问

http://localhost:8080/create

相关推荐
ai大佬4 分钟前
Java 开发玩转 MCP:从 Claude 自动化到 Spring AI Alibaba 生态整合
java·spring·自动化·api中转·apikey
Mr__Miss28 分钟前
面试踩过的坑
java·开发语言
爱喝一杯白开水30 分钟前
POI从入门到上手(一)-轻松完成Apache POI使用,完成Excel导入导出.
java·poi
向哆哆1 小时前
Java 安全:如何防止 DDoS 攻击?
java·安全·ddos
啥都想学的又啥都不会的研究生1 小时前
Kubernetes in action-初相识
java·docker·微服务·容器·kubernetes·etcd·kubelet
毅航1 小时前
MyBatis 事务管理:一文掌握Mybatis事务管理核心逻辑
java·后端·mybatis
宝耶2 小时前
面试常问问题:Java基础篇
java·面试·职场和发展
来自星星的猫教授2 小时前
spring,spring boot, spring cloud三者区别
spring boot·spring·spring cloud
躲在云朵里`2 小时前
IDEA搭建环境的五种方式
java·ide·intellij-idea
喵手2 小时前
从 Java 到 Kotlin:在现有项目中迁移的最佳实践!
java·python·kotlin