我使用的是springboot框架,写系统日志之前需要先引入pom依赖:
<!--aop用于插入操作日志 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
1.日志表 (我使用的时postgre,其他数据库需要修改脚本)
-- public.sys_log definition
-- Drop table
-- DROP TABLE sys_log;
CREATE TABLE sys_log (
id varchar(38) NOT NULL,
username varchar(30) NOT NULL,
"method" varchar(255) NULL, -- 方法名
params varchar(1000) NULL, -- 参数
ip varchar(20) NULL,
create_date timestamp(0) NULL DEFAULT now(), --默认设置为now,插入日志时就不需要设置时间了
"type" varchar(20) NULL, -- 类型 : 查询、新增等等
model varchar(50) NULL, -- 模块
"result" varchar(50) NULL, -- 操作结果
description varchar(255) NULL,
url varchar(400) NULL, -- 请求url
CONSTRAINT log_pk PRIMARY KEY (id)
);
-- Column comments
COMMENT ON COLUMN public.sys_log."method" IS '方法名';
COMMENT ON COLUMN public.sys_log.params IS '参数';
COMMENT ON COLUMN public.sys_log."type" IS '类型 : 查询、新增等等';
COMMENT ON COLUMN public.sys_log.model IS '模块';
COMMENT ON COLUMN public.sys_log."result" IS '操作结果';
COMMENT ON COLUMN public.sys_log.url IS '请求url';
2. 日志实体
import java.util.Date;
import javax.persistence.*;
import lombok.Data;
@Data
@Table(name = "sys_log")
public class SysLog {
/**
* 权限角色ID
*/
@Id
private String id;
@Column(name = "username")
private String username; //用户名
@Column(name = "method")
private String method; //方法名
@Column(name = "params")
private String params; //参数
@Column(name = "ip")
private String ip; //ip地址
@Column(name = "url")
private String url; //请求url
@Column(name = "type")
private String type; //操作类型 :新增、删除等等
@Column(name = "model")
private String model; //模块
@Column(name = "create_date")
private Date createDate; //操作时间
@Column(name = "result")
private String result; //操作结果
@Column(name = "description")
private String description;//描述
}
3. dao (继承了tk.mapper中的Mymapper,所以没有写插入日志方法),因没有业务相关代码,所以没有写service以及的impl
import org.springframework.stereotype.Repository;
import com.album.manager.pojo.SysLog;
import tk.mapper.MyMapper;
@Repository
public interface LogMapper extends MyMapper<SysLog>{
}
4. 自定义注解类(用于拦截操作方法并插入日志)
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author hewenjun
* @title: OperationLog
* @date 2021/9/23
* @description: 自定义操作日志注解
*/
@Target(ElementType.METHOD)//注解放置的目标位置即方法级别
@Retention(RetentionPolicy.RUNTIME)//注解在哪个阶段执行
@Documented
public @interface SysLogAnnotation {
String operModul() default ""; // 操作模块
String operType() default ""; // 操作类型
String operDesc() default ""; // 操作说明
}
5. AOP拦截插入日志类(返回值使用的是Map接收):
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
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.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import com.album.manager.common.utils.IPUtils;
import com.album.manager.common.utils.SysLogAnnotation;
import com.album.manager.dao.LogMapper;
import com.album.manager.pojo.SysLog;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import cn.hutool.core.lang.id.NanoId;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author hewenjun
* @title: OperationAspect
* @date 2021/9/23
* @description: 操作日志切面处理类
*/
@Aspect
@Component
@Slf4j
public class SysLogAspect {
@Autowired
LogMapper logDao;
/**
* 设置操作日志切入点 在注解的位置切入代码
*/
@Pointcut("@annotation(com.album.manager.common.utils.SysLogAnnotation)")
public void operLogPoinCut() {
}
/**
* 记录操作日志
* @param joinPoint 方法的执行点
* @param result 方法返回值
* @throws Throwable
*/
@AfterReturning(returning = "result", value = "operLogPoinCut()")
public void saveOperLog(JoinPoint joinPoint, Object result) throws Throwable {
// 获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
try {
SysLog sysLog = new SysLog();
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取切入点所在的方法
Method method = signature.getMethod();
//获取操作
SysLogAnnotation annotation = method.getAnnotation(SysLogAnnotation.class);
if (annotation != null) {
sysLog.setModel(annotation.operModul());
sysLog.setType(annotation.operType());
sysLog.setDescription(annotation.operDesc());
}
// 获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
// 获取请求的方法名
String methodName = method.getName();
methodName = className + "." + methodName;
sysLog.setMethod(methodName); // 类名.请求方法
sysLog.setCreateDate(new Date()); //操作时间
//操作用户 --登录时有把用户的信息保存在session中,可以直接取出
String user = (String)request.getSession().getAttribute("user");
sysLog.setUsername(user);
sysLog.setIp(IPUtils.getIpAddr(request)); //操作IP IPUtils工具类网上大把的,比如工具类集锦的hutool.jar
sysLog.setUrl(request.getRequestURI()); // 请求URI
// 方法请求的参数
Map<String, String> rtnMap = converMap(request.getParameterMap());
// 将参数所在的数组转换成json
String params = JSON.toJSONString(rtnMap);
//获取json的请求参数
if (rtnMap == null || rtnMap.size() == 0) {
params = getJsonStrByRequest(request);
}
sysLog.setParams(params); // 请求参数
Map <String, Object> dataResult = (Map <String, Object>)result; //返回值信息
//需要先判断返回值是不是Map <String, Object>,如果不是會拋異常,需要控制层的接口返回数据格式统一
//如果嫌返回格式统一太麻烦建议日志保存时去掉操作结果
sysLog.setResult(dataResult.get("msg").toString()); //獲取方法返回值中的msg,如果上面的類型錯誤就拿不到msg就會拋異常
//保存日志
sysLog.setId(NanoId.randomNanoId());
logDao.insert(sysLog);
} catch (Exception e) {
e.printStackTrace();
log.error("日誌記錄異常,請檢查返回值是否是Map <String, Object>類型");
}
}
/**
* 转换request 请求参数
*
* @param paramMap request获取的参数数组
*/
public Map<String, String> converMap(Map<String, String[]> paramMap) {
Map<String, String> rtnMap = new HashMap<String, String>();
for (String key : paramMap.keySet()) {
rtnMap.put(key, paramMap.get(key)[0]);
}
return rtnMap;
}
/**
* 获取json格式 请求参数
*/
public String getJsonStrByRequest(HttpServletRequest request) {
String param = null;
try {
BufferedReader streamReader = new BufferedReader(new InputStreamReader(request.getInputStream(), "UTF-8"));
StringBuilder responseStrBuilder = new StringBuilder();
String inputStr;
while ((inputStr = streamReader.readLine()) != null) {
responseStrBuilder.append(inputStr);
}
JSONObject jsonObject = JSONObject.parseObject(responseStrBuilder.toString());
param = jsonObject.toJSONString();
System.out.println(param);
} catch (Exception e) {
e.printStackTrace();
}
return param;
}
/**
* 转换异常信息为字符串
*
* @param exceptionName 异常名称
* @param exceptionMessage 异常信息
* @param elements 堆栈信息
*/
public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
StringBuffer strbuff = new StringBuffer();
for (StackTraceElement stet : elements) {
strbuff.append(stet + "\n");
}
String message = exceptionName + ":" + exceptionMessage + "\n\t" + strbuff.toString();
return message;
}
}
5.1 AOP拦截插入日志类(返回值统一使用ReturnT.java --20230720更新版):
package com.epson.Aspect;
import com.alibaba.druid.support.json.JSONUtils;
import com.epson.entity.ReturnT;
import com.epson.entity.SysLog;
import com.epson.mapper.LogMapper;
import com.epson.service.LogService;
import com.epson.utils.IPUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
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.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import cn.hutool.core.lang.id.NanoId;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author hewenjun
* @title: OperationAspect
* @date 2021/9/23
* @description: 操作日志切面处理类
*/
@Aspect
@Component
@Slf4j
public class SysLogAspect {
@Autowired
LogService logService;
/**
* 设置操作日志切入点 在注解的位置切入代码
*/
@Pointcut("@annotation(com.epson.Aspect.SysLogAnnotation)")
public void operLogPoinCut() {
}
/**
* 记录操作日志
* @param joinPoint 方法的执行点
* @param result 方法返回值
*/
@AfterReturning(returning = "result", value = "operLogPoinCut()")
public void saveOperLog(JoinPoint joinPoint, ReturnT result) { //必须统一返回值,否则拿不到返回的数据
// 获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
try {
SysLog sysLog = new SysLog();
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取切入点所在的方法
Method method = signature.getMethod();
//获取操作
SysLogAnnotation annotation = method.getAnnotation(SysLogAnnotation.class);
if (annotation != null) {
sysLog.setModel(annotation.operModul());
sysLog.setType(annotation.operType());
sysLog.setDescription(annotation.operDesc());
}
// 获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
// 获取请求的方法名
String methodName = method.getName();
methodName = className + "." + methodName;
sysLog.setMethod(methodName); // 类名.请求方法
//操作用户 --登录时有把用户的信息保存在session中,可以直接取出
String loginUserName = (String)request.getSession().getAttribute("loginUserNo");
String loginUserNo = (String)request.getSession().getAttribute("loginUserNo");
sysLog.setUsername(loginUserName+ "(" + loginUserNo + ")");
sysLog.setIp(IPUtils.getIpAddr(request)); //操作IP IPUtils工具类网上大把的,比如工具类集锦的hutool.jar
sysLog.setUrl(request.getRequestURI()); // 请求URI
// 方法请求的参数
Map<String, String> rtnMap = converMap(request.getParameterMap());
// 将参数所在的数组转换成json
String params = JSONUtils.toJSONString(rtnMap);
//获取json的请求参数
if (rtnMap == null || rtnMap.size() == 0) {
params = getJsonStrByRequest(request);
}
sysLog.setParams(params); // 请求参数
String updateDataCount = annotation.operType()+"了" + (null!= result.getCount()?result.getCount():0)+"条数据";
sysLog.setResult(result.getMsg()+"("+updateDataCount+")"); //獲取方法返回值中的msg,如果上面的類型錯誤就拿不到msg就會拋異常
//保存日志
sysLog.setId(NanoId.randomNanoId());
logService.insert(sysLog);
} catch (Exception e) {
e.printStackTrace();
log.error("日誌記錄異常,請檢查返回值是否是Map <String, Object>類型");
}
}
/**
* 转换request 请求参数
*
* @param paramMap request获取的参数数组
*/
public Map<String, String> converMap(Map<String, String[]> paramMap) {
Map<String, String> rtnMap = new HashMap<>();
for (String key : paramMap.keySet()) {
rtnMap.put(key, paramMap.get(key)[0]);
}
return rtnMap;
}
/**
* 获取json格式 请求参数
*/
public String getJsonStrByRequest(HttpServletRequest request) {
String param = null;
try {
BufferedReader streamReader = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8));
StringBuilder responseStrBuilder = new StringBuilder();
String inputStr;
while ((inputStr = streamReader.readLine()) != null) {
responseStrBuilder.append(inputStr);
}
param = JSONUtils.toJSONString(responseStrBuilder);
} catch (Exception e) {
e.printStackTrace();
}
return param;
}
/**
* 转换异常信息为字符串
*
* @param exceptionName 异常名称
* @param exceptionMessage 异常信息
* @param elements 堆栈信息
*/
public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
StringBuilder strbuff = new StringBuilder();
for (StackTraceElement stet : elements) {
strbuff.append(stet).append("\n");
}
return exceptionName + ":" + exceptionMessage + "\n\t" + strbuff.toString();
}
}
6. Controller层接口(也是操作被拦截的地方):
@RequestMapping(value = "/setUser", method = RequestMethod.POST)
@ResponseBody
@SysLogAnnotation(operModul = "系统管理>>用户管理", operType = "新增", operDesc = "新增用户") //operType尽量统一为 查询/新增/更新/删除 ,这样返回值就可以拼接得到更完善的结果
public Map<String,Object> setUser(BaseAdminUser user) {
Map<String,Object> data = new HashMap();
if(user.getId().isBlank()){
data = adminUserService.addUser(user);
}
return data;
}
7. 特别特别特别需要注意,如果系统日志中需要插入操作结果,那么就一定需要统一返回格式,比如上面代码中返回结果 Map<String,Object> data = new HashMap(),service代码:
@Transactional(rollbackFor = Exception.class)
public Map<String,Object> addUser(BaseAdminUser user) {
Map<String,Object> data = new HashMap();
try {
BaseAdminUser old = baseAdminUserMapper.getUserByUserName(user.getSysUserName(),null);
if(old != null){
data.put("code",0);
data.put("msg","用户名已存在!");
log.error("用户[新增],结果=用户名已存在!");
return data;
}
String phone = user.getUserPhone();
if(phone.length() != 11){
data.put("code",0);
data.put("msg","手机号位数不对!");
log.error("设置用户[新增或更新],结果=手机号位数不对!");
return data;
}
String username = user.getSysUserName();
if(user.getSysUserPwd() == null){
String password = DigestUtils.Md5(username,"123456");
user.setSysUserPwd(password);
}else{
String password = DigestUtils.Md5(username,user.getSysUserPwd());
user.setSysUserPwd(password);
}
user.setRegTime(DateUtils.getCurrentDate());
user.setUserStatus(1);
user.setId(UUID.randomUUID().toString());
int result = baseAdminUserMapper.insert(user);
if(result == 0){
data.put("code",0);
data.put("msg","新增失败!");
log.error("用户[新增],结果=新增失败!");
return data;
}
data.put("code",1);
data.put("msg","新增成功!");
log.info("用户[新增],结果=新增成功!");
} catch (Exception e) {
e.printStackTrace();
data.put("code",0);
data.put("msg","新增异常失败!");
log.error("用户[新增]异常!", e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();//捕捉的异常需要手动回滚
}
return data;
}
上面service代码中返回的map格式定义有没有code都可以,视页面处理而定,如果系统操作日志需要保存结果,那么这个msg就必须要有,步骤5中的 AOP拦截插入日志类中获取操作结果时获取的就是map中的msg
分享个工具类jar 的依赖(官网:Hutool参考文档 , API文档:hutool-码云(gitee.com)):
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.19</version>
</dependency>