Skip to content

为什么要记录文件上传日志?

在项目中实现文件上传时,记录日志可以帮助我们跟踪文件上传的状态、排查问题以及监控系统行为。

记录内容

在文件上传接口中,记录以下关键信息:

请求信息:如客户端 IP、请求方法、请求 URL。
文件信息:如文件名、文件大小、文件类型。
上传结果:如上传成功或失败、OSS 返回的文件路径或错误信息。
异常信息:如文件大小超限、文件类型不支持、OSS 连接失败等。

AOP优势

在项目中使用 AOP(面向切面编程)注解实现文件上传日志具有以下优势:

1. 代码解耦:将日志记录逻辑与业务逻辑分离,避免代码重复,提升代码可读性和可维护性。
2. 灵活性与扩展性:通过注解可以灵活地控制哪些方法需要记录日志,便于扩展和修改日志记录逻辑。
3. 非侵入性:无需修改现有业务代码,只需在方法上添加注解即可实现日志记录。
4. 集中管理:日志记录逻辑集中在切面中,便于统一管理和维护。
5. 提高开发效率:通过注解方式快速实现日志功能,减少重复代码编写。

定义注解

xiaomayi-common/xiaomayi-file 模块中定义的文件上传日志的 AOP 切面 FileLog 文件,内容如下:

js
package com.xiaomayi.file.annotation;

import com.xiaomayi.file.enums.FileSource;
import com.xiaomayi.file.enums.FileType;

import java.lang.annotation.*;

/**
 * <p>
 * 文件日志注解
 * </p>
 *
 * @author 小蚂蚁云团队
 * @since 2024-06-16
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface FileLog {

    /**
     * 日志标题
     *
     * @return 返回结果
     */
    String title() default "未知";

    /**
     * 文件类型
     *
     * @return 返回结果
     */
    FileType type() default FileType.OTHER;

    /**
     * 文件来源
     *
     * @return 返回结果
     */
    FileSource source() default FileSource.SYSTEM;

}

注解实现

js
package com.xiaomayi.file.aspect;

import com.alibaba.fastjson2.JSON;
import com.xiaomayi.core.enums.HttpMethod;
import com.xiaomayi.core.utils.R;
import com.xiaomayi.core.utils.ServletUtils;
import com.xiaomayi.core.utils.StringUtils;
import com.xiaomayi.file.annotation.FileLog;
import com.xiaomayi.file.dto.UploadFileDTO;
import com.xiaomayi.file.vo.UploadFileLogVO;
import com.xiaomayi.file.vo.UploadFileVO;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * <p>
 * 文件上传AOP切面
 * </p>
 *
 * @author 小蚂蚁云团队
 * @since 2024-06-16
 */
@Slf4j
@Aspect
@Component
public abstract class FileAspect {

    /**
     * 计算耗时线程
     */
    ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("FileTime");

    /**
     * 扫描切入点注解
     */
    @Pointcut("@annotation(com.xiaomayi.file.annotation.FileLog)")
    public void doPointCut() {

    }

    /**
     * 处理请求前执行
     *
     * @param fileLog 文件日志
     */
    @Before("doPointCut() && @annotation(fileLog)")
    public void doBefore(FileLog fileLog) {
        log.info("发送文件AOP日志处理开始");
        // 设置请求开始时间
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }

    /**
     * 处理完请求后执行
     */
    @After("doPointCut()")
    public void doAfter() {
        log.info("发送文件AOP日志处理结束");
    }

    /**
     * 处理完请求后执行
     *
     * @param point      切点
     * @param fileLog    文件日志
     * @param jsonResult 响应结果
     */
    @AfterReturning(pointcut = "@annotation(fileLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint point, FileLog fileLog, Object jsonResult) {
        log.info("发送文件AOP日志结果已响应");
        smsLog(point, fileLog, jsonResult, null);
    }

    /**
     * 拦截异常处理
     *
     * @param point     切点
     * @param fileLog   文件日志
     * @param exception 异常处理
     */
    @AfterThrowing(value = "@annotation(fileLog)", throwing = "exception")
    public void doAfterThrowing(JoinPoint point, FileLog fileLog, Exception exception) {
        log.info("发送文件AOP日志处理异常");
        smsLog(point, fileLog, null, exception);
    }

    /**
     * 保存文件发送日志
     *
     * @param point      切点
     * @param annotation 日志注解
     * @param jsonResult 响应结果
     * @param exception  异常处理
     */
    private void smsLog(JoinPoint point, FileLog annotation, Object jsonResult, Exception exception) {
        // 获取日志标题
        String title = annotation.title();
        // 实例化文件VO
        UploadFileLogVO uploadFileLogVO = new UploadFileLogVO();
        // 日志标题
        uploadFileLogVO.setTitle(title);
        // 日志类型
        uploadFileLogVO.setType(annotation.type().ordinal());
        // 处理设置注解上的参数
        setRequestParam(point, uploadFileLogVO, jsonResult);
        // 设置请求耗时
        uploadFileLogVO.setConsumeTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());
        // 文件日志状态
        if (exception != null) {
            // 异常处理
            uploadFileLogVO.setStatus(1);
            uploadFileLogVO.setError(StringUtils.substring(exception.getMessage(), 0, 2000));
        } else {
            uploadFileLogVO.setStatus(0);
            uploadFileLogVO.setError(null);
        }
        // 响应处理结果
        try {
            // 实例化文件VO列表
            List<UploadFileVO> uploadFileVOList = new ArrayList<>();
            // 文件信息
            String result = uploadFileLogVO.getResult();
            if (StringUtils.isNotEmpty(result)) {
                R fileRes = JSON.parseObject(result, R.class);
                if (StringUtils.isNotNull(fileRes.getData())) {
                    if (fileRes.getData() instanceof List<?>) {
                        uploadFileVOList = JSON.parseArray(fileRes.getData().toString(), UploadFileVO.class);
                    } else {
                        UploadFileVO uploadFileVO = JSON.parseObject(fileRes.getData().toString(), UploadFileVO.class);
                        // 加入列表
                        uploadFileVOList.add(uploadFileVO);
                    }
                }
            }
            // 设置本地上传文件列表数据
            uploadFileLogVO.setUploadFileVOList(uploadFileVOList);
        } catch (Exception e) {
            log.error("文件信息解析失败:{}", e.getMessage());
        }

        // 请求参数处理
        try {
            UploadFileDTO uploadFileDTO = JSON.parseObject(uploadFileLogVO.getParam(), UploadFileDTO.class);
            if (StringUtils.isNotNull(uploadFileDTO)) {
                // 业务类型
                uploadFileLogVO.setBizType(uploadFileDTO.getBizId());
                // 业务ID
                uploadFileLogVO.setBizId(uploadFileDTO.getBizId());
                // 业务内容
                uploadFileLogVO.setBizContent(uploadFileDTO.getBizContent());
            }
        } catch (Exception e) {
            log.error("文件参数解释失败:{}", e.getMessage());
        }

        // 调用抽象类保存请求地址
        if (saveFileLog(uploadFileLogVO)) {
            log.info("发送文件AOP日志已存储");
        }
    }

    /**
     * 设置请求参数
     *
     * @param point      切点
     * @param smsLogVO   文件日志VO
     * @param jsonResult 返回结果
     */
    private void setRequestParam(JoinPoint point, UploadFileLogVO smsLogVO, Object jsonResult) {
        // 获取所有请求参数
        Map<?, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
        // 获取请求方式
        String requestMethod = ServletUtils.getRequest().getMethod();
        // 请求参数判空
        if (StringUtils.isEmpty(paramsMap) && (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))) {
            // 格式化请求参数
            String params = getJSONParam(point.getArgs());
            // 设置文件日志参数
            smsLogVO.setParam(StringUtils.substring(params, 0, 2000));
        } else {
            // 设置文件日志参数
            smsLogVO.setParam(StringUtils.substring(JSON.toJSONString(paramsMap), 0, 2000));
        }
        // 设置网络响应
        if (StringUtils.isNotNull(jsonResult)) {
            smsLogVO.setResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
        }
    }

    /**
     * 请求参数转JSON字符串
     *
     * @param objects 参数对象
     * @return 返回结果
     */
    private String getJSONParam(Object[] objects) {
        // 参数对象判空
        if (StringUtils.isEmpty(objects)) {
            return "";
        }
        // 实例化参数列表
        List<String> paramList = new ArrayList<>();
        // 遍历属性
        for (Object obj : objects) {
            // 对象判空
            if (StringUtils.isNull(obj) || isExcludeObject(obj)) {
                continue;
            }
            // 对象转JSON,排除敏感属性字段
            String jsonObj = JSON.toJSONString(obj);
            if (StringUtils.isEmpty(jsonObj)) {
                continue;
            }
            // 加入列表
            paramList.add(jsonObj);
        }
        // 参数列表转拼接字符串
        String params = StringUtils.join(paramList.toArray(), " ");
        // 返回结果
        return params;
    }

    /**
     * 判断是否需要排除的对象
     *
     * @param obj 对象
     * @return 返回结果
     */
    @SuppressWarnings("rawtypes")
    public boolean isExcludeObject(final Object obj) {
        // 获取类
        Class<?> clazz = obj.getClass();
        // 判断是否数组
        if (clazz.isArray()) {
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        }
        // 判断是否集合
        else if (Collection.class.isAssignableFrom(clazz)) {
            Collection collection = (Collection) obj;
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        }
        // 判断是否Map
        else if (Map.class.isAssignableFrom(clazz)) {
            Map map = (Map) obj;
            for (Object value : map.entrySet()) {
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        // 返回结果
        return obj instanceof MultipartFile
                || obj instanceof HttpServletRequest
                || obj instanceof HttpServletResponse
                || obj instanceof BindingResult;
    }

    /**
     * 文件日志抽象类
     *
     * @param uploadFileLogVO 文件日志VO
     * @return 返回结果
     */
    public abstract boolean saveFileLog(UploadFileLogVO uploadFileLogVO);

}

添加依赖

pom.xml 配置文件中引入以下依赖:

js
<!-- 文件日志模块 -->
<dependency>
    <groupId>com.xiaomayi</groupId>
    <artifactId>xiaomayi-file</artifactId>
</dependency>

注解使用

在需要记录文件上传日志的方法上添加以下注解:

js
@FileLog(title = "上传文件", type = FileType.FILE)

使用案例:

js
package com.xiaomayi.admin.controller;

import com.xiaomayi.core.config.AppConfig;
import com.xiaomayi.core.utils.R;
import com.xiaomayi.file.annotation.FileLog;
import com.xiaomayi.file.annotation.FileValidation;
import com.xiaomayi.file.annotation.FilesValidation;
import com.xiaomayi.file.dto.UploadFileDTO;
import com.xiaomayi.file.enums.FileType;
import com.xiaomayi.file.vo.UploadFileVO;
import com.xiaomayi.system.service.UploadService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

/**
 * <p>
 * 文件上传 前端控制器
 * </p>
 *
 * @author 小蚂蚁云团队
 * @since 2024-05-21
 */
@RestController
@RequestMapping("/upload")
@Tag(name = "文件上传", description = "文件上传")
@AllArgsConstructor
public class UploadController {

    private final UploadService uploadService;

    /**
     * 上传文件
     *
     * @param file          文件对象
     * @param uploadFileDTO 文件参数
     * @return 返回结果
     */
    @Operation(summary = "上传文件", description = "上传文件")
    @PreAuthorize("@pms.hasAuthority('sys:upload:file')")
    @FileLog(title = "上传文件", type = FileType.FILE)
    @PostMapping("/uploadFile")
    public R<UploadFileVO> uploadFile(@FileValidation MultipartFile file, @Validated UploadFileDTO uploadFileDTO) {
        return uploadService.uploadFile(file, uploadFileDTO);
    }

    /**
     * 批量上传文件
     *
     * @param file          文件对象列表
     * @param uploadFileDTO 文件参数
     * @return 返回结果
     */
    @Operation(summary = "批量上传文件", description = "批量上传文件")
    @PreAuthorize("@pms.hasAuthority('sys:upload:files')")
    @FileLog(title = "批量上传文件", type = FileType.FILES)
    @PostMapping("/uploadFiles")
    public R<List<UploadFileVO>> uploadFiles(@FilesValidation MultipartFile[] file, @Validated UploadFileDTO uploadFileDTO) {
        return uploadService.uploadFiles(file, uploadFileDTO);
    }

}

总结

通过以上方式,可以清晰地记录文件上传的整个过程,便于后续的监控和问题排查。

小蚂蚁云团队 · 提供技术支持

小蚂蚁云 新品首发
新品首发,限时特惠,抢购从速! 全场95折
赋能开发者,助理企业发展,提供全方位数据中台解决方案。
获取官方授权