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文件中配置的