springboot项目脚手架搭建之二一文全解

11. springBoot集成AOP实现接口日志信息统一记录

为什么要记录接口日志

  • 用户登录记录统计
  • 重要的增删改操作留痕
  • 接口调用情况统计
  • 线上问题排查
  • 等等

使用spring的AOP使用场景,实现这个功能。

创建sql表

CREATE TABLE `sys_operate_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键',
  `title` varchar(50) DEFAULT '' COMMENT '模块标题',
  `business_type` int(2) DEFAULT '4' COMMENT '业务类型(0查询 1新增 2修改 3删除 4其他)',
  `method` varchar(100) DEFAULT '' COMMENT '方法名称',
  `resp_time` bigint(20) DEFAULT NULL COMMENT '响应时间',
  `request_method` varchar(10) DEFAULT '' COMMENT '请求方式',
  `browser` varchar(255) DEFAULT NULL COMMENT '浏览器类型',
  `operate_type` int(1) DEFAULT '3' COMMENT '操作类别(0网站用户 1后台用户 2小程序 3其他)',
  `operate_url` varchar(255) DEFAULT '' COMMENT '请求URL',
  `operate_ip` varchar(128) DEFAULT '' COMMENT '主机地址',
  `operate_location` varchar(255) DEFAULT '' COMMENT '操作地点',
  `operate_param` text COMMENT '请求参数',
  `json_result` text COMMENT '返回参数',
  `status` int(1) DEFAULT '0' COMMENT '操作状态(0正常 1异常)',
  `error_msg` text COMMENT '错误消息',
  `create_id` bigint(20) DEFAULT NULL COMMENT '操作人id',
  `create_name` varchar(50) DEFAULT '' COMMENT '操作人员',
  `create_time` datetime DEFAULT NULL COMMENT '操作时间',
  `update_id` bigint(20) NULL DEFAULT NULL COMMENT '更新人id',
  `update_name` varchar(64) NULL DEFAULT '' COMMENT '更新者',
  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统管理-操作日志记录';

使用mybatis plus自动生成代码

定义涉及到的枚举类

业务类型枚举类

/**
 * @author LH
 */
public enum BusinessTypeEnum {
    // 0查询 1新增 2修改 3删除 4其他
    SELECT,
    INSERT,
    UPDATE,
    DELETE,
    OTHER
}

操作类别枚举类

/**
 * @author LH
 */
public enum OperateTypeEnum {
    // 0网站用户 1后台用户 2小程序 3其他
    BLOG,
    ADMIN,
    APP,
    OTHER
}

定义切点的注解

定义一个自定义注解BwLog.java,后面哪些接口调用操作需要记录日志就靠它了。

/**
 * @author LH
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BwLog {

    // 0网站用户 1后台用户 2小程序 3其他
    OperateTypeEnum operateType() default OperateTypeEnum.OTHER;

    // 0查询 1新增 2修改 3删除 4其他
    BusinessTypeEnum businessType() default BusinessTypeEnum.SELECT;
    
    // 返回保存结果是否落库,没用的大结果可以不记录,比如分页查询等等,设为false即可
    boolean saveResult() default true;
}

AOP实现功能

使用Aop的环绕通知,配置切面类

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.bwss.aimanagementplatform.entity.BwLog;
import com.bwss.aimanagementplatform.entity.OperateLog;
import com.bwss.aimanagementplatform.mapper.OperateLogMapper;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Objects;

/**
 * @Description 配置操作日志切面类
 * @Author LH
 * @Date 2023/12/27 15:00
 **/
@Aspect
@Component
@Slf4j
@AllArgsConstructor
public class SystemLogAspect
{
    @Autowired
    private OperateLogMapper operateLogMapper;

    @Pointcut(value = "@annotation(com.bwss.aimanagementplatform.entity.BwLog)")
    public void systemLog()
    {
        // nothing
    }

    @Around(value = "systemLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable
    {
        int maxTextLength = 65000;
        Object obj;
        // 定义执行开始时间
        long startTime;
        // 定义执行结束时间
        long endTime;
        HttpServletRequest request = ((ServletRequestAttributes) Objects
                .requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        // 取swagger的描述信息
        ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
        BwLog bwLog = method.getAnnotation(BwLog.class);
        OperateLog operateLog = new OperateLog();

        try
        {
            operateLog.setBrowser(request.getHeader("USER-AGENT"));
            operateLog.setOperateUrl(request.getRequestURI());
            operateLog.setRequestMethod(request.getMethod());
            operateLog.setMethod(String.valueOf(joinPoint.getSignature()));
            operateLog.setCreateTime(new Date());
            operateLog.setOperateIp(getIpAddress(request));
            String operateParam = JSON.toJSONStringWithDateFormat(joinPoint.getArgs(), "yyyy-MM-dd HH:mm:ss",
                    SerializerFeature.WriteMapNullValue);
            if (operateParam.length() > maxTextLength)
            {
                operateParam = operateParam.substring(0, maxTextLength);
            }
            operateLog.setOperateParam(operateParam);

            if (apiOperation != null)
            {
                operateLog.setTitle(apiOperation.value() + "");
            }

            if (bwLog != null)
            {
                operateLog.setBusinessType(bwLog.businessType().ordinal());
                operateLog.setOperateType(bwLog.operateType().ordinal());
            }
        } catch (Exception e)
        {
            e.printStackTrace();
        }

        startTime = System.currentTimeMillis();
        try
        {
            obj = joinPoint.proceed();
            endTime = System.currentTimeMillis();
            operateLog.setRespTime(endTime - startTime);
            operateLog.setStatus(0);
            // 判断是否保存返回结果,列表页可以设为false
            if (Objects.nonNull(bwLog) && bwLog.saveResult())
            {
                String result = JSON.toJSONString(obj);
                if (result.length() > maxTextLength)
                {
                    result = result.substring(0, maxTextLength);
                }
                operateLog.setJsonResult(result);
            }
        } catch (Exception e)
        {
            // 记录异常信息
            operateLog.setStatus(1);
            operateLog.setErrorMsg(e.toString());
            throw e;
        } finally
        {
            endTime = System.currentTimeMillis();
            operateLog.setRespTime(endTime - startTime);
            operateLogMapper.insert(operateLog);
        }
        return obj;
    }

    /**
     * 获取Ip地址
     */
    private static String getIpAddress(HttpServletRequest request)
    {
        String xip = request.getHeader("X-Real-IP");
        String xFor = request.getHeader("X-Forwarded-For");
        String unknown = "unknown";
        if (StringUtils.isNotEmpty(xFor) && !unknown.equalsIgnoreCase(xFor))
        {
            //多次反向代理后会有多个ip值,第一个ip才是真实ip
            int index = xFor.indexOf(",");
            if (index != -1)
            {
                return xFor.substring(0, index);
            } else
            {
                return xFor;
            }
        }
        xFor = xip;
        if (StringUtils.isNotEmpty(xFor) && !unknown.equalsIgnoreCase(xFor))
        {
            return xFor;
        }
        if (StringUtils.isBlank(xFor) || unknown.equalsIgnoreCase(xFor))
        {
            xFor = request.getHeader("Proxy-Client-IP");
        }
        if (StringUtils.isBlank(xFor) || unknown.equalsIgnoreCase(xFor))
        {
            xFor = request.getHeader("WL-Proxy-Client-IP");
        }
        if (StringUtils.isBlank(xFor) || unknown.equalsIgnoreCase(xFor))
        {
            xFor = request.getHeader("HTTP_CLIENT_IP");
        }
        if (StringUtils.isBlank(xFor) || unknown.equalsIgnoreCase(xFor))
        {
            xFor = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (StringUtils.isBlank(xFor) || unknown.equalsIgnoreCase(xFor))
        {
            xFor = request.getRemoteAddr();
        }
        return xFor;
    }
}

编写测试类

import com.bwss.aimanagementplatform.entity.BwLog;
import com.bwss.aimanagementplatform.enums.BusinessTypeEnum;
import com.bwss.aimanagementplatform.enums.ErrorCode;
import com.bwss.aimanagementplatform.enums.OperateTypeEnum;
import com.bwss.aimanagementplatform.exception.CustomException;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.Data;
import org.springframework.web.bind.annotation.*;

/**
 * @Description
 * @Author LH
 **/
@RestController
@RequestMapping("/example")
@Api(tags = "实例演示-日志记录演示接口")
public class TestSystemLogController
{

    @ApiOperation(value = "测试带参数、有返回结果的get请求")
    @GetMapping("/testGetLog/{id}")
    @BwLog(businessType = BusinessTypeEnum.OTHER, operateType = OperateTypeEnum.OTHER)
    public Test testGetLog(@PathVariable Integer id)
    {
        Test test = new Test();
        test.setName("lh");
        test.setAge(18);
        test.setRemark("大家好,我是lh");
        return test;
    }

    @ApiOperation(value = "测试json参数、抛出异常的post请求")
    @PostMapping("/testPostLog")
    @BwLog(businessType = BusinessTypeEnum.OTHER, operateType = OperateTypeEnum.OTHER, saveResult = false)
    public Test testPostLog(@RequestBody Test param)
    {
        Test test = new Test();
        test.setName("lh");
        if (test.getAge() == null)
        {
            throw new CustomException(ErrorCode.COMMON_ERROR.getMsg());
        }
        test.setRemark("大家好,我是lh");
        return test;
    }

    @Data
    static class Test
    {
        private String name;

        private Integer age;

        private String remark;
    }
}

使用swagger调用测试

12. springBoot集成文件上传下载功能

在配置文件配置文件上传的路径

file:
  local:
    maxFileSize: 10485760
    imageFilePath: D:/test/image/
    docFilePath: D:/test/file/

编写读取配置的文件配置类

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

/**
 * @Description 文件上传下载配置
 * @Author LH
 **/
@Data
@Configuration
public class FileConfig
{
    /**
     * 图片存储路径
     */
    @Value("${file.local.imageFilePath}")
    private String imageFilePath;

    /**
     * 文档存储路径
     */
    @Value("${file.local.docFilePath}")
    private String docFilePath;

    /**
     * 文件限制大小
     */
    @Value("${file.local.maxFileSize}")
    private long maxFileSize;
}

WebMvcConfig实现WebMvcConfigurer,指向文件的存放路径

import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @Description 实现WebMvcConfigurer,指向我们文件的存放路径
 * @Author LH
 **/
@Configuration
@AllArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer
{
    @Autowired
    private FileConfig fileConfig;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry)
    {
        //重写方法
        //修改tomcat 虚拟映射
        //定义图片存放路径 这里加/example是为了测试避开系统的token校验,实际访问地址根据自己需求来
        registry.addResourceHandler("/example/images/**").addResourceLocations("file:" + fileConfig.getImageFilePath());
        //定义文档存放路径
        registry.addResourceHandler("/example/doc/**").addResourceLocations("file:" + fileConfig.getDocFilePath());
    }
}

文件操作工具类,后续可以改造,文件名称和存储路径,还有后缀等信息

import com.bwss.aimanagementplatform.config.FileConfig;
import com.bwss.aimanagementplatform.exception.CustomException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @Description 文件操作工具类
 * @Author LH
 **/
@Slf4j
@Component
@AllArgsConstructor
public class FileUtil
{
    @Autowired
    private FileConfig fileConfig;

    private static final List<String> FILE_TYPE_LIST_IMAGE = Arrays.asList("image/png", "image/jpg", "image/jpeg",
            "image/bmp");

    public String uploadImage(MultipartFile file)
    {
        // 检查图片类型
        String contentType = file.getContentType();
        if (!FILE_TYPE_LIST_IMAGE.contains(contentType))
        {
            throw new CustomException("上传失败,不允许的文件类型");
        }
        int size = (int) file.getSize();
        if (size > fileConfig.getMaxFileSize())
        {
            throw new CustomException("文件过大");
        }
        String fileName = file.getOriginalFilename();
        //获取文件后缀
        String afterName = StringUtils.substringAfterLast(fileName, ".");
        //获取文件前缀
        String prefName = StringUtils.substringBeforeLast(fileName, ".");
        //获取一个时间毫秒值作为文件名
//        fileName = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + "_" + prefName + "." + afterName;
        fileName = UUID.randomUUID().toString().replace("-", "");
        File filePath = new File(fileConfig.getImageFilePath(), fileName);

        //判断文件是否已经存在
        if (filePath.exists())
        {
            throw new CustomException("文件已经存在");
        }
        //判断文件父目录是否存在
        if (!filePath.getParentFile().exists())
        {
            filePath.getParentFile().mkdirs();
        }
        try
        {
            file.transferTo(filePath);
        } catch (IOException e)
        {
            log.error("图片上传失败", e);
            throw new CustomException("图片上传失败");
        }
        return fileName;
    }

    public List<Map<String, Object>> uploadFiles(MultipartFile[] files)
    {
        int size = 0;
        for (MultipartFile file : files)
        {
            size = (int) file.getSize() + size;
        }
        if (size > fileConfig.getMaxFileSize())
        {
            throw new CustomException("文件过大");
        }
        List<Map<String, Object>> fileInfoList = new ArrayList<>();
        for (int i = 0; i < files.length; i++)
        {
            Map<String, Object> map = new HashMap<>();
            String fileName = files[i].getOriginalFilename();
            //获取文件后缀
            String afterName = StringUtils.substringAfterLast(fileName, ".");
            //获取文件前缀
            String prefName = StringUtils.substringBeforeLast(fileName, ".");

//            String fileServiceName = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + i + "_" + prefName
//                    + "." + afterName;
            String fileServiceName = UUID.randomUUID().toString().replace("-", "");
            File filePath = new File(fileConfig.getDocFilePath(), fileServiceName);
            // 判断文件父目录是否存在
            if (!filePath.getParentFile().exists())
            {
                filePath.getParentFile().mkdirs();
            }
            try
            {
                files[i].transferTo(filePath);
            } catch (IOException e)
            {
                log.error("文件上传失败", e);
                throw new CustomException("文件上传失败");
            }
            map.put("fileName", fileName);
            map.put("filePath", filePath);
            map.put("fileServiceName", fileServiceName);
            fileInfoList.add(map);
        }
        return fileInfoList;
    }

    /**
     * 批量删除文件
     *
     * @param fileNameArr 服务端保存的文件的名数组
     */
    public void deleteFile(String[] fileNameArr)
    {
        for (String fileName : fileNameArr)
        {
            String filePath = fileConfig.getDocFilePath() + fileName;
            File file = new File(filePath);
            if (file.exists())
            {
                try
                {
                    Files.delete(file.toPath());
                } catch (IOException e)
                {
                    e.printStackTrace();
                    log.warn("文件删除失败", e);
                }
            } else
            {
                log.warn("文件: {} 删除失败,该文件不存在", fileName);
            }
        }
    }

    /**
     * 下载文件
     */
    public void downLoadFile(HttpServletResponse response, String fileName) throws UnsupportedEncodingException
    {
        String encodeFileName = URLDecoder.decode(fileName, "UTF-8");
        File file = new File(fileConfig.getDocFilePath() + encodeFileName);
        // 下载文件
        if (!file.exists())
        {
            throw new CustomException("文件不存在!");
        }
        try (FileInputStream inputStream = new FileInputStream(file);
                ServletOutputStream outputStream = response.getOutputStream())
        {
            response.reset();
            //设置响应类型	PDF文件为"application/pdf",WORD文件为:"application/msword", EXCEL文件为:"application/vnd.ms-excel"。
            response.setContentType("application/octet-stream;charset=utf-8");
            //设置响应的文件名称,并转换成中文编码
            String afterName = StringUtils.substringAfterLast(fileName, "_");
            //保存的文件名,必须和页面编码一致,否则乱码
            afterName = response.encodeURL(new String(afterName.getBytes(), StandardCharsets.ISO_8859_1.displayName()));
            response.setHeader("Content-type", "application-download");
            //attachment作为附件下载;inline客户端机器有安装匹配程序,则直接打开;注意改变配置,清除缓存,否则可能不能看到效果
            response.addHeader("Content-Disposition", "attachment;filename=" + afterName);
            response.addHeader("filename", afterName);
            //将文件读入响应流
            int length = 1024;
            byte[] buf = new byte[1024];
            int readLength = inputStream.read(buf, 0, length);
            while (readLength != -1)
            {
                outputStream.write(buf, 0, readLength);
                readLength = inputStream.read(buf, 0, length);
            }
            outputStream.flush();
        } catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}

编写测试类

import com.bwss.aimanagementplatform.exception.CustomException;
import com.bwss.aimanagementplatform.util.FileUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;

/**
 * @Description 文件相关操作接口
 * @Author LH
 **/
@Slf4j
@RestController
@AllArgsConstructor
@Api(tags = "文件相关操作接口")
@RequestMapping("/example")
public class TestFileController
{

    @Autowired
    private FileUtil fileUtil;

    @PostMapping("/uploadImage")
    @ApiOperation("图片上传")
    public String uploadImage(@RequestParam(value = "file") MultipartFile file)
    {
        if (file.isEmpty())
        {
            throw new CustomException("图片内容为空,上传失败!");
        }
        return fileUtil.uploadImage(file);
    }

    @PostMapping("/uploadFiles")
    @ApiOperation("文件批量上传")
    public List<Map<String, Object>> uploadFiles(@RequestParam(value = "file") MultipartFile[] files)
    {
        return fileUtil.uploadFiles(files);
    }

    @PostMapping("/deleteFiles")
    @ApiOperation("批量删除文件")
    public void deleteFiles(@RequestParam(value = "files") String[] files)
    {
        fileUtil.deleteFile(files);
    }

    @GetMapping(value = "/download/{fileName:.*}")
    @ApiOperation("文件下载功能")
    public void download(@PathVariable("fileName") String fileName, HttpServletResponse response)
    {
        try
        {
            fileUtil.downLoadFile(response, fileName);
        } catch (Exception e)
        {
            log.error("文件下载失败", e);
        }
    }
}

使用APIman调用测试

图片上传

图片下载

在WebMvcConfig文件中配置的

  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值