Chat2Excel 项目通用服务开发

一、声明日志体系

1、创建 @LogOperation 注解

开关都在注解上,切面按配置决定记什么

java 复制代码
package com.linzhixin.common.annotation;

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) // 运行时仍可见,AOP才能通过反射读到
public @interface LogOperation {

    /**
     *  操作描述 用于标识业务的含义 接口名称
     */
    String value() default "";

    /**
     * 是否需要记录请求参数
     */
    boolean logRequest() default true;

    /**
     * 是否需要记录响应参数
     */
    boolean logResponse() default true;

    /**
     * 是否记录接口的调用耗时
     */
    boolean logExecutionTime() default true;
}

2、创建 LogOperationAspect 切面

这是真正干活的类。@Aspect + @Component 让 Spring 把它注册成 Bean,并对匹配方法织入通知。

java 复制代码
package com.linzhixin.common.aspect;


import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.linzhixin.common.annotation.LogOperation;
import jakarta.servlet.http.HttpServletRequest;
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.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;


@Aspect
@Component
@Slf4j
public class LogOperationAspect {
    /**
     * 用于序列话请求/响应的 Jackson 工具
     */
    private final ObjectMapper objectMapper;

    public LogOperationAspect(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
        // 配置 Jackson 库的配置:当序列化一个空对象(没有任何 getter 方法或属性的对象)时,不要抛出异常,而是正常序列化为 {}
        this.objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    }

    /**
     * 环绕通知
     */
    @Around("@annotation(com.linzhixin.common.annotation.LogOperation)")
    public Object logOperation(ProceedingJoinPoint joinPoint) {
        // 1、访问的是哪个方法或者接口
        Long startTime = System.currentTimeMillis();
        // 获取签名
        // getSignature() 方法返回一个 Signature 对象
        //MethodSignature 是 Signature 的子接口,专门用于描述方法的签名信息。它包含了方法的所有静态元数据,是 AOP 中最常用的签名类型。
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //从 MethodSignature 中获取 Java 反射的 Method 对象,
        Method method = signature.getMethod();
        //从 Method 对象中提取 @LogOperation 注解的实例
        LogOperation logOperation = method.getAnnotation(LogOperation.class);

        // 通过 Java 反射获取当前执行方法的方法名
        String methodName = method.getName();
        //如果注解中指定了操作名称,就使用注解的值;否则使用方法名作为默认值。
        String operation = logOperation.value().isEmpty() ? methodName : logOperation.value();

        // 2、获取请求参数
        //从 Spring 的请求上下文 中获取当前 HTTP 请求的属性
        // RequestContextHolder(请求上下文持有者): Spring 提供的线程安全的请求上下文容器,一个全局访问点,存储当前线程的请求上下文。
        // ServletRequestAttributes(Servlet 请求属性): 请求属性的载体,包含了当前请求的所有 Servlet 相关信息。
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        // 获取HTTP 请求头 本身。它代表着一次具体的网络请求,包含传输层的数据(URL、IP、Header、Cookie、请求流)。
        HttpServletRequest request = attributes != null ? attributes.getRequest() : null;

        // 3、记录请求参数
        if(logOperation.logRequest()) {
            logRequest(operation, joinPoint, request);
        }

        // 4、记录响应参数
        try {
            Object result = joinPoint.proceed();
            if(logOperation.logResponse()) {
                logResponse(operation, result, startTime, logOperation.logExecutionTime());
            }
            // 5、响应的结果需要返回去
            return result;
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }



    /**
     * 记录请求参数
     * @param operation
     * @param joinPoint
     * @param request
     */
    private void logRequest(String operation, ProceedingJoinPoint joinPoint, HttpServletRequest request) {
        StringBuilder logBuilder = new StringBuilder();
        // 1、加上了请求描述
        logBuilder.append("[").append(operation).append("] 请求参数: ");

        // 2、加上访问路由和访问方法
        if(request != null) {
            logBuilder.append("method=").append(request.getMethod())
                    .append(", url=").append(request.getRequestURI());

            // 3、加上请求参数
            String queryString = request.getQueryString();
            if(queryString != null && !queryString.isEmpty()) {
                logBuilder.append(", query=").append(maskSensitiveData(queryString));
            }

            String authorization  = request.getHeader("Authorization");
            if(authorization != null) {
                logBuilder.append(", authorization=").append(maskToken(authorization));
            }
        }

        // 4、记录方法参数
        Object[] args = joinPoint.getArgs();
        if(args != null && args.length > 0) {
            logBuilder.append(", args=[");
            // 遍历参数列表
            for (int i = 0; i < args.length; i++) {
                if(i > 0) {
                    logBuilder.append(", ");
                }
                Object arg = args[i];
                if(arg ==null) {
                    logBuilder.append("null");
                } else if(isFile(arg)) {
                    // 处理文件
                    logBuilder.append("name=").append(getFileName(arg));
                } else if(isHttpServletResponse(arg)) {
                    logBuilder.append("下载内容,无法打印");
                } else {
                    try {
                        String argStr = maskSensitiveData(objectMapper.writeValueAsString(arg));
                        logBuilder.append(argStr);
                    } catch (JsonProcessingException e) {
                        logBuilder.append("获取参数异常");
                    }
                }
            }
            logBuilder.append("]");
        }
        log.info(logBuilder.toString());
    }

    /**
     * 记录响应参数的日志
     * @param operation 操作描述
     * @param result    执行结果
     * @param startTime 起始时间戳
     * @param logTime 是否记录耗时
     */
    private void logResponse(String operation, Object result, long startTime, boolean logTime) {
        StringBuilder logBuilder = new StringBuilder();
        logBuilder.append("[").append(operation).append("] 响应结果:");

        if(result == null) {
            logBuilder.append("null");
        } else {
            // 序列化响应结果
            try {
                String resultString = maskSensitiveData(objectMapper.writeValueAsString(result));
                logBuilder.append(resultString);
            } catch (JsonProcessingException e) {
                logBuilder.append("响应有异常");
            }
        }

        // 加上访问耗时
        if (logTime) {
            logBuilder.append(", 访问耗时=").append(System.currentTimeMillis() - startTime).append("ms");
        }
        log.info(logBuilder.toString());
    }

    /**
     *  关键字段脱敏操作
     * @param data 原始字符串
     * @return 脱敏后的字符串
     */
    private String maskSensitiveData(String data) {
        // 脱敏密码字段
        data = data.replaceAll("\"password\"\\s*:\\s*\"[^\"]*\"", "\"password\":\"***\"");
        data = data.replaceAll("\"passwordHash\"\\s*:\\s*\"[^\"]*\"", "\"passwordHash\":\"***\"");

        // 脱敏 token z字段
        data = data.replaceAll("\"token\"\\s*:\\s*\"[^\"]*\"", "\"token\":\"***\"");
        data = data.replaceAll("\"authorization\"\\s*:\\s*\"[^\"]*\"", "\"authorization\":\"***\"");

        // 脱敏邮箱
        data = data.replaceAll("\"email\"\\s*:\\s*\"([^\"]*@[^\"]*)\"", "\"email\":\"***\"");

        return data;
    }

    /**
     * 令牌脱敏
     * @param token 原始令牌
     * @return 脱敏后的 Token
     */
    private String maskToken(String token) {
        return "****";
    }

    /**
     * 根据反射判断对象是否是文件
     * @param object 文件对象
     * @return 是或者否
     */
    private boolean isFile(Object object) {
        //object.getClass() 得到的是该对象在运行时的真正类型信息,即 Class<T> 对象。
        return object != null && object.getClass().getName().contains("MultipartFile");
    }

    /**
     * 根据反射靠对象来复原文件名
     * @param object 文件对象
     * @return 文件名
     */
    private String getFileName(Object object) {
        try {
            //object.getClass():获取对象的运行时 Class
            //.getMethod("getOriginalFilename"):查找这个类中名为 getOriginalFilename 的公共方法
            Method method = object.getClass().getMethod("getOriginalFilename");
            // String fileName = (String) method.invoke(object);
            // method.invoke(object):执行 object 上的那个方法(即 getOriginalFilename)
            // 返回一个 Object 类型的结果
            return (String) method.invoke(object);
        } catch (Exception e) {
            return "文件未知!";
        }
    }

    /**
     * 根据反射来判断是否需要打印对象参数
     * @param object 入参
     * @return
     */
    private boolean isHttpServletResponse(Object object) {
        return object != null && object.getClass().getName().contains("HttpServletResponse");
    }
}

二、通用响应

java 复制代码
package com.linzhixin.common.util;

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

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result <T> {

    /**
     * 响应码
     */
    private Integer code;

    /**
     * 响应文案
     */
    private String message;

    /**
     * 响应数据
     */
    private T data;

    // 成功、客户端报错、服务端报错

    /**
     * 状态码枚举
     */
    public enum ResultCode {
        // 请求成功
        SUCCESS(200, "操作成功"),

        // 客户端报错
        BAD_REQUEST(400, "请求参数错误"),
        UNAUTHORIZED(401, "未授权"),
        FORBIDDEN(403, "禁止访问"),
        NOT_FOUND(404, "资源不存在"),

        // 服务端报错
        SERVER_ERROR(500, "服务报错"),
        GATEWAY_ERROR(502, "网关报错"),
        SERVER_UNAVAILABLE(503, "服务不可用")

        ;

        //枚举(enum)在 Java 中代表一组固定的、不可变的常量实例。既然枚举实例本身是固定的,那么它们的属性值也应该是固定的。
        private final Integer code;
        private final String message;

        ResultCode(Integer code, String message) {
            this.code = code;
            this.message = message;
        }

        public Integer getCode() {
            return code;
        }

        public String getMessage() {
            return message;
        }
    }


    public static <T> Result<T> success(String message, T data) {
        return new Result<>(ResultCode.SUCCESS.getCode(), message, data);
    }

    /**
     * 自定义失败响应
     * @param resultCode 错误枚举
     * @param message 响应文案
     * @return 通用响应对象
     * @param <T> 泛型
     */
    public static <T> Result<T> error(ResultCode resultCode, String message) {
        return new Result<>(resultCode.getCode(), message, null);
    }

    /**
     * 请求参数报错
     * @param message 响应文案
     * @return 通用响应对象
     * @param <T> 泛型
     */
    public static <T> Result<T> badRequest(String message) {
        return error(ResultCode.BAD_REQUEST, message);
    }

    public static <T> Result <T> serverError(String message) {
        return error(ResultCode.SERVER_ERROR, message);
    }

}

三、全局异常处理

java 复制代码
package com.linzhixin.common.config;

import com.linzhixin.common.util.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

/**
 * 全局异常处理器
 */

// @ControllerAdvice + @ResponseBody
// @ControllerAdvice 是 Spring 框架中的一个全局增强注解,用于为所有 Controller 提供统一的增强处理。可以把它理解为一个"全局拦截器/增强器",作用于整个应用的所有 Controller。
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler  {

    /**
     * 请求体不可读异常
     * @param e 异常内容
     * @return 客户端 400 报错
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public Result<Void> handleJsonException(HttpMessageNotReadableException e) {
        return Result.badRequest("请求格式错误");
    }

    /**
     * 专门处理参数校验失败异常
     * @param e 异常内容
     * @return 客户端 400 报错
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleMethodArgsNotValid(MethodArgumentNotValidException e) {
        return Result.badRequest("请求参数缺失");
    }

    /**
     * 非法参数异常
     * @param e 异常内容
     * @return 客户端 400 报错
     */
    @ExceptionHandler(IllegalArgumentException.class)
    public Result<Void> illegalArgumentException(IllegalArgumentException e) {
        return Result.badRequest(e.getMessage());
    }

    /**
     * 上传文件超大异常
     * @param e 异常内容
     * @return 客户端 400 报错
     */
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public Result<Void> uploadSizeExceeded(MaxUploadSizeExceededException e) {
        log.warn("文件有点大:{}", e.getMessage());
        return Result.badRequest("文件大小超过限制");
    }

    /**
     * Java堆内存溢出
     * @param e 错误
     * @return 服务端500报错
     */
    @ExceptionHandler(OutOfMemoryError.class)
    public Result<Void> oomException(OutOfMemoryError e) {
        log.error("Java堆内存溢出{}", e.getMessage());
        return Result.serverError("系统内存不足");
    }

    /**
     * 兜底异常
     * @param e 执行异常
     * @return 服务端500报错
     */
    @ExceptionHandler(RuntimeException.class)
    public Result<Void> handleRuntimeException(RuntimeException e) {
        log.error("业务运行异常:{}", e.getMessage());
        return Result.serverError(e.getMessage());
    }
}

四、设置统一分页配置

java 复制代码
package com.linzhixin.common.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;

/**
 * MybatisPlus 核心配置
 */
@Configuration
public class MybatisPlusConfig {
    /**
     * 分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();

        // 添加分页插件
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        // 单次返回最大记录数
        paginationInnerInterceptor.setMaxLimit(500L);
        // 请求页码超过总页数,返回空
        paginationInnerInterceptor.setOverflow(false);
        mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
        return mybatisPlusInterceptor;
    }
}
相关推荐
小裕哥略帅16 分钟前
Spring AOP 实现通用 Token 自动刷新重试工具包
java·后端·spring
Jazz_z19 分钟前
使用 Python 合并与取消合并 Excel 单元格
开发语言·python·excel
Lenyiin20 分钟前
第6篇_Python高级语法与标准库精髓:从会用到达精通
java·python·html
YHHLAI23 分钟前
TypeScript 面试题:type 与 interface 的区别与相同点
java·ubuntu·typescript
斑马13927 分钟前
Linux软件编程学习笔记(十)——网络(二)——TCP通信
开发语言·网络·php
咖啡八杯28 分钟前
PageHelper 分页封装:TableDataInfo 与 startPage() 的工作原理
java·spring boot·分页·若依·开源框架·pagehelper
多敲代码防脱发30 分钟前
Alibaba Sentinel(熔断、限流)(国内下载Sentinel)
java·开发语言·sentinel
艺杯羹33 分钟前
双轨大模型路由实战:从端侧Ollama边缘推理到云端大模型容灾降级架构深度拆解
开发语言·人工智能·ai·架构·php