如果这个项目让你有所收获,记得 Star 关注哦,这对我是非常不错的鼓励与支持。
源码地址(后端):mingyue: 🎉 基于 Spring Boot、Spring Cloud & Alibaba 的分布式微服务架构基础服务中心
源码地址(前端):mingyue-ui: 🎉 基于 Vue3 + TS + Vite + Element plus 等技术,适配 MingYue 后台微服务
文档地址:Wiki - Gitee.com
在Java应用程序中记录日志是一种良好的实践,它为开发、运维和支持团队提供了很多好处。以下是一些主要的理由:
故障排除和调试:
日志是定位和解决问题的重要工具。通过在关键代码路径和操作中插入日志语句,开发人员可以追踪应用程序的执行流程,快速定位潜在的错误和异常。
性能分析:
记录关键操作的执行时间、资源使用情况等信息,有助于性能分析和优化。通过分析日志,可以确定应用程序的瓶颈并改进性能。
安全审计:
记录关键的安全事件和用户活动,以便进行审计和检测潜在的安全威胁。登录失败、访问敏感信息等事件的记录对于安全监控至关重要。
系统状态监控:
通过记录系统状态和关键指标,可以实时监控应用程序的运行状况。这有助于及时发现和解决潜在的问题,以提高系统的稳定性和可用性。
版本追踪和审计:
在代码中记录版本信息、变更历史和代码提交信息,有助于追踪应用程序的演变过程。审计日志还可以用于追溯特定功能或问题的起源。
用户行为分析:
对于包含用户交互的应用程序,记录用户活动可以帮助了解他们的使用模式、偏好和行为。这对于改进用户体验和调整产品设计非常有帮助。
合规性和法规要求:
许多行业和法规要求记录关键事件和操作,以确保企业的合规性。通过日志记录,可以满足这些法规的要求,并提供审计证据。
持久化数据:
将日志存储在持久化介质中,例如文件或数据库,以便在应用程序重新启动后仍然可以访问日志。这有助于在系统故障或应用程序崩溃时还原状态并进行故障排除。
系统操作日志和用户登录日志是两种不同类型的日志,它们记录了系统中不同方面的活动
系统操作日志:记录系统的各种操作,包括但不限于增删改查、上传与下载文件等。
用户登录日志:记录用户登录和注销的信息。
系统操作日志:采用注解(非侵入)方式记录;
用户登录日志:采用显式(侵入)方式记录;
com.csp.mingyue mingyue-common-security
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 模块
*/
String module() default "";
/**
* 功能
*/
BusinessType businessType() default BusinessType.OTHER;
/**
* 操作人类别
*/
OperatorUserType operatorUserType() default OperatorUserType.MANAGE;
/**
* 是否保存请求的参数
*/
boolean isSaveRequestData() default true;
/**
* 是否保存响应的参数
*/
boolean isSaveResponseData() default true;
/**
* 排除指定的请求参数
*/
String[] excludeParamNames() default {};
}
操作日志记录核心类
@Slf4j
@Aspect
@RequiredArgsConstructor
@AutoConfiguration
public class LogAspect {
private final ServiceInstance serviceInstance;
/**
* 排除敏感属性字段
*/
public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
/**
* 处理完请求后执行
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
handleLog(joinPoint, controllerLog, null, jsonResult);
}
/**
* 拦截异常操作
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
handleLog(joinPoint, controllerLog, e, null);
}
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
// 日志记录开始时间
Long startTime = System.currentTimeMillis();
// ========数据库日志========
OperateLogEvent operateLog = new OperateLogEvent();
try {
// 请求信息
String ip = ServletUtils.getClientIP();
operateLog.setReqIp(ip);
operateLog.setServiceId(serviceInstance.getServiceId());
operateLog.setReqAddress(AddressUtils.getRealAddressByIP(ip));
operateLog
.setReqUrl(StrUtil.sub(Objects.requireNonNull(ServletUtils.getRequest()).getRequestURI(), 0, 255));
operateLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 用户信息
LoginUser loginUser = LoginHelper.getLoginUser();
operateLog.setUserId(loginUser.getUserId());
operateLog.setUserName(loginUser.getUsername());
if (e != null) {
operateLog.setStatus(BusinessStatus.FAIL.ordinal());
operateLog.setException(StrUtil.sub(e.getMessage(), 0, 2000));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operateLog.setMethod(className + "." + methodName + "()");
// 设置User-Agent
operateLog.setUserAgent(ServletUtils.getRequest().getHeader(HttpHeaders.USER_AGENT));
// 设置请求方式
operateLog.setReqMethod(ServletUtils.getRequest().getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operateLog, jsonResult);
}
catch (Exception exp) {
// 记录本地异常日志
log.error("异常信息:{}", exp.getMessage());
}
finally {
Long endTime = System.currentTimeMillis();
operateLog.setDuration(endTime - startTime);
// 发布事件保存数据库
SpringUtils.context().publishEvent(operateLog);
}
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
* @param log 日志
* @param operateLog 操作日志
*/
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, OperateLogEvent operateLog,
Object jsonResult) throws Exception {
// 设置标题
operateLog.setModule(log.module());
// 设置 action 动作
operateLog.setBusinessType(log.businessType().ordinal());
// 设置操作人类别
operateLog.setUserType(log.operatorUserType().ordinal());
// 是否需要保存 request,参数和值
if (log.isSaveRequestData()) {
// 获取参数的信息,传入到数据库中。
setRequestValue(joinPoint, operateLog, log.excludeParamNames());
}
// 是否需要保存 response,参数和值
if (log.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) {
R resp = JSONUtil.toBean(JSONUtil.toJsonStr(jsonResult), R.class);
operateLog.setRespMsg(resp.getMsg());
operateLog.setRespCode(resp.getCode());
operateLog.setRespResult(StrUtil.sub(JSONUtil.toJsonStr(jsonResult), 0, 2000));
}
}
/**
* 获取请求的参数,放到log中
* @param operLog 操作日志
* @throws Exception 异常
*/
private void setRequestValue(JoinPoint joinPoint, OperateLogEvent operLog, String[] excludeParamNames)
throws Exception {
Map paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
String requestMethod = operLog.getReqMethod();
if (MapUtil.isEmpty(paramsMap) && HttpMethod.PUT.name().equals(requestMethod)
|| HttpMethod.POST.name().equals(requestMethod)) {
String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
operLog.setReqParams(StrUtil.sub(params, 0, 2000));
}
else {
MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);
MapUtil.removeAny(paramsMap, excludeParamNames);
operLog.setReqParams(StrUtil.sub(JSONUtil.toJsonStr(paramsMap), 0, 2000));
}
}
/**
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
StringJoiner params = new StringJoiner(" ");
if (ArrayUtil.isEmpty(paramsArray)) {
return params.toString();
}
for (Object o : paramsArray) {
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
String str = JSONUtil.toJsonStr(o);
Dict dict = JsonUtils.parseMap(str);
if (MapUtil.isNotEmpty(dict)) {
MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
MapUtil.removeAny(dict, excludeParamNames);
str = JSONUtil.toJsonStr(dict);
}
params.add(str);
}
}
return params.toString();
}
/**
* 判断是否需要过滤的对象。
* @param o 对象信息。
* @return 如果是需要过滤的对象,则返回true;否则返回false。
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o) {
Class> clazz = o.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
}
else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection) {
return value instanceof MultipartFile;
}
}
else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
for (Object value : map.values()) {
return value instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
}
}
@Slf4j
@Component
@RequiredArgsConstructor
public class LogEventListener {
private final RemoteLogService remoteLogService;
/**
* 保存系统日志记录
*/
@Async
@EventListener
public void saveLog(OperateLogEvent operateLog) {
log.info("保存系统日志记录落库「{}」", JSONUtil.toJsonStr(operateLog));
remoteLogService.saveSysOperateLog(BeanUtil.copyProperties(operateLog, SysOperateLog.class));
}
}
org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.csp.mingyue.common.log.event.LogEventListener com.csp.mingyue.common.log.aspect.LogAspect
DROP TABLE IF EXISTS sys_operate_log; CREATE TABLE sys_operate_log ( operate_log_id BIGINT(20) NOT NULL COMMENT '操作日志ID', module VARCHAR(50) DEFAULT '' COMMENT '模块', business_type INT(2) DEFAULT 0 COMMENT '业务类型(0其它 1新增 2修改 3删除)', method VARCHAR(100) DEFAULT '' COMMENT '方法名称', service_id VARCHAR(32) DEFAULT NULL COMMENT '服务ID', user_id BIGINT(20) NOT NULL COMMENT '用户ID', user_name VARCHAR(50) NOT NULL COMMENT '用户账号', user_type TINYINT(1) DEFAULT 0 COMMENT '用户类型(0其它 1系统用户)', user_agent VARCHAR(1000) DEFAULT NULL COMMENT '用户代理', req_ip VARCHAR(128) DEFAULT '' COMMENT '请求IP', req_address VARCHAR(255) DEFAULT '' COMMENT '请求地点', req_url VARCHAR(255) DEFAULT '' COMMENT '请求URL', req_method VARCHAR(20) DEFAULT NULL COMMENT '请求方式', req_params TEXT DEFAULT NULL COMMENT '请求参数', duration BIGINT NOT NULL COMMENT '执行时长,单位(ms)', resp_code INT DEFAULT NULL COMMENT '结果码', resp_msg VARCHAR(512) NULL DEFAULT '' COMMENT '结果提示', resp_result VARCHAR(2000) DEFAULT '' COMMENT '返回参数', status CHAR(1) DEFAULT 0 COMMENT '操作状态(0正常 1异常)', exception TEXT DEFAULT NULL COMMENT '异常信息', operate_time DATETIME NOT NULL COMMENT '操作时间', is_deleted CHAR(1) DEFAULT '0' COMMENT '删除标志(0正常,1删除)', create_by VARCHAR(64) DEFAULT '' COMMENT '创建者', create_time DATETIME DEFAULT NULL COMMENT '创建时间', update_by VARCHAR(64) DEFAULT '' COMMENT '更新者', update_time DATETIME DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (operate_log_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC COMMENT='系统操作日志';
@Log(module = "用户管理", businessType = BusinessType.DELETE)
@DeleteMapping("{userId}")
@Log(module = "用户管理", businessType = BusinessType.DELETE)
@Operation(summary = "删除用户", parameters = { @Parameter(name = "userId", description = "用户ID", required = true) })
public R delUser(@PathVariable Long userId) {
return R.ok(sysUserService.delUser(userId));
}
调用完成后查看数据库是否存在该操作记录即可
curl -X 'DELETE' 'http://192.168.63.114:7100/system/sysUser/111111111' -H 'accept: */*' -H 'Authorization: UWapduuggQcNSqg1oQZ17ZyfPHDxxt8Q'