系统操作日志的实现
- springboot
- 自定义注解
- 切面
- 线程池
- 对请求参数、返回值、异常等操作
- nginx反向代理获取真实IP
- 操作内容自定义拼接
直接上代码
- 1、自定义操作日志记录注解
/**
* 自定义操作日志记录注解
*
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog {
/**
* 方法名称
*/
String MethodName() default SysOperationType.NO;
/**
* 操作类别
*/
String OperationType() default SysOperationType.OTHER;
/**
* 操作内容
*/
String OperationContent() default SysOperationType.NO;
}
- 自定义系统操作类型
/**
* 操作类型
*/
public interface SysOperationType {
String OTHER = "其他";
String NO = "无";
String UPDATE_MOBILE = "修改手机号";
String RECHARGE = "充值";
String DEDUCTION = "扣费";
String UPDATE_USER = "变更所属人";
String EXPORT_ORDER_DATA = "订单导出";
String EXPORT_FUND_DATA = "资金明细导出";
String PUBLISH = "发布";
}
- 操作内容实体
@Data
@TableName("sys_operation_log")
public class SysOperationLog {
@ApiModelProperty("主键id")
@TableId(type = IdType.AUTO)
private Long id;
@ApiModelProperty("操作类型")
private String operationType;
@ApiModelProperty("操作对象")
private String operationTarget;
@ApiModelProperty("方法名称")
private String methodName;
@ApiModelProperty("操作内容详情")
private String operationContent;
@ApiModelProperty("操作状态 0失败,1成功")
private Integer status;
@ApiModelProperty("操作者IP")
private String ip;
@ApiModelProperty("IP归属地")
private String ipAddress;
@ApiModelProperty("操作人名称")
private String operationName;
@ApiModelProperty("所属主体")
private Integer tenantCode;
@ApiModelProperty("创建人编码")
private Long createBy;
@ApiModelProperty("创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
public Date getCreateTime() {
if (createTime == null) {
return new Date();
}
return createTime;
}
}
- 注解解析
/**
* 注解解析
*/
@Aspect
@Component
@Slf4j
public class OperateLogAspect {
@Resource
private HandleLogAspect handleLogAspect;
@Pointcut(value = "@annotation(com.cloudapi.common.annotation.OperateLog)")
public void logPointCut() {
}
/**
* 处理完请求前执行
*
* @param joinPoint 切点
*/
@Before(value = "logPointCut()")
public void before(JoinPoint joinPoint) {
log.info("before");
}
/**
* 获取请求头中登录用户信息
*
* @param request 请求信息
* @return 登录用户信息
*/
private LoginUser getLoginUser(HttpServletRequest request) {
// 获取当前的用户
LoginUser loginUser = null;
String mobile = request.getHeader(AUTHORIZATION);
if (StrUtil.isNotBlank(mobile)) {
// 解析jwt
// 查询用户缓存信息
String key = Constants.WEB_USER_PREFIX + mobile;
UserVo userVo = JSONObject.parseObject(RedisUtils.StringOps.get(key), UserVo.class);
if (null != userVo) {
loginUser = userVo.getUser();
}
}
return loginUser;
}
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
log.info("doAfterReturning");
// 当前线程请求头信息
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert sra != null;
HttpServletRequest request = sra.getRequest();
// 获取用户信息
LoginUser loginUser = getLoginUser(request);
handleLogAspect.handleLog(joinPoint, null, jsonResult, loginUser, request);
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "logPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
log.info("doAfterThrowing");
// 当前线程请求头信息
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert sra != null;
HttpServletRequest request = sra.getRequest();
// 获取用户信息
LoginUser loginUser = getLoginUser(request);
handleLogAspect.handleLog(joinPoint, e, null, loginUser, request);
}
}
- 操作日志处理
/**
* @description: 操作日志处理类
* @date : 2023/3/29
*/
@Component
@Slf4j
public class HandleLogAspect {
@Resource
private SysOperationLogDao sysOperationLogDao;
@Async("operationLogThreadPoolExecutor")
public void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, LoginUser loginUser, HttpServletRequest request) {
try {
// 获得注解
OperateLog controllerLog = getAnnotationLog(joinPoint);
if (Objects.isNull(controllerLog)) {
return;
}
// 数据库日志
SysOperationLog sysOperationLog = new SysOperationLog();
// 处理注解上的参数、方法参数、登录信息,ip信息,异常信息,返回结果
getControllerMethodDescription(joinPoint, controllerLog, sysOperationLog, request, loginUser, e, jsonResult);
// 保存数据库
int insert = sysOperationLogDao.insert(sysOperationLog);
log.info("操作日志落库:{}", insert);
} catch (Exception exp) {
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
/**
* 处理注解上的参数、方法参数、登录信息,ip信息,异常信息,返回结果
*
* @param operateLog 日志
* @param sysOperationLog 操作日志
*/
public void getControllerMethodDescription(JoinPoint joinPoint,
OperateLog operateLog,
SysOperationLog sysOperationLog,
HttpServletRequest request,
LoginUser loginUser,
Exception e,
Object jsonResult) throws Exception {
// 初始状态
sysOperationLog.setStatus(CommonConstant.YES);
// 登录信息
if (Objects.nonNull(loginUser)) {
sysOperationLog.setOperationName(loginUser.getRealName() != null ? loginUser.getRealName() : loginUser.getId().toString());
sysOperationLog.setCreateBy(loginUser.getId());
sysOperationLog.setTenantCode(loginUser.getTenantCode());
} else {
sysOperationLog.setCreateBy(0L);
sysOperationLog.setOperationName("system");
sysOperationLog.setTenantCode(0);
}
if (Objects.nonNull(e)) {
sysOperationLog.setStatus(CommonConstant.NO);
}
// 设置action动作
sysOperationLog.setOperationType(operateLog.OperationType().trim());
// 获取请求参数
String nameAndValue = getNameAndValue(joinPoint);
// 操作对象
sysOperationLog.setOperationTarget(nameAndValue);
// 设置操作内容
String str = operateLog.OperationContent();
String format;
if (!StringUtils.isEmpty(nameAndValue) && str.contains("{")) {
format = MessageFormat.format(str, nameAndValue);
} else {
format = str;
}
sysOperationLog.setOperationContent(format);
// 方法名称
sysOperationLog.setMethodName(operateLog.MethodName());
// 获取ip
String realRequestIp = getRealRequestIp(request);
// 获取ip归属地
String ipAddress = getAddress(realRequestIp);
sysOperationLog.setIp(realRequestIp);
sysOperationLog.setIpAddress(ipAddress);
}
/**
* 是否存在注解,如果存在就获取
*/
private OperateLog getAnnotationLog(JoinPoint joinPoint) throws Exception {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
return method.getAnnotation(OperateLog.class);
}
return null;
}
/**
* 获取参数中操作者
*
* @param joinPoint 入参
* @return 请求参数
*/
private String getNameAndValue(JoinPoint joinPoint) {
Object[] paramValues = joinPoint.getArgs();
if (paramValues != null && paramValues.length > 0) {
String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
for (int i = 0; i < paramNames.length; i++) {
Object paramValue = paramValues[i];
if (paramValue != null) {
try {
String str = JSON.toJSONString(paramValue);
JSONObject jsonObject = JSON.parseObject(str);
Object operationTarget = jsonObject.get("operationTarget");
if (operationTarget != null) {
return operationTarget.toString();
}
} catch (Exception e) {
log.error("参数解析异常");
}
}
}
}
return "";
}
/**
* 处理完请求中执行
*
* @param point 切点
*/
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) {
log.info("around");
//获取方法名称
Signature methodName = point.getSignature();
long l1 = System.currentTimeMillis();
Object obj = null;
try {
obj = point.proceed(point.getArgs());
} catch (Throwable e) {
e.printStackTrace();
}
log.info(methodName + "bye" + "\t耗时 " + (System.currentTimeMillis() - l1));
return obj;
}
/**
* 真实请求IP
*
* @param request 请求
* @return 真实ip
*/
public static String getRealRequestIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (null != ip && ip.contains(",")) {
ip = ip.substring(0, ip.indexOf(","));
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
InetAddress address = null;
try {
address = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
log.info("获取本地IP异常");
}
if (address != null) {
log.info("本地IP:{}", address.getHostAddress());
ip = address.getHostAddress();
}
}
return ip;
}
/**
* 通过调用接口根据ip获取归属地
*/
public String getAddress(String ip) {
if (!StringUtils.isEmpty(ip)) {
try {
String encode = URLEncoder.encode(ip, "utf-8");
URL realUrl = new URL("https://whois.pconline.com.cn/ipJson.jsp?ip=" + encode + "&json=true");
HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection();
conn.setRequestProperty("contentType", "GBK");
conn.setRequestMethod("GET");
conn.setUseCaches(false);
conn.setReadTimeout(6000);
conn.setConnectTimeout(6000);
conn.setInstanceFollowRedirects(false);
int code = conn.getResponseCode();
String ipaddr = "";
if (code == 200) {
InputStream inputStream = conn.getInputStream();
StringBuilder sb = new StringBuilder();
String readLine;
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "GBK"));
BufferedReader responseReader = new BufferedReader(in);
while ((readLine = responseReader.readLine()) != null) {
sb.append(readLine);
}
ipaddr = sb.substring(sb.indexOf("addr") + 7, sb.indexOf("regionNames") - 3);
responseReader.close();
in.close();
inputStream.close();
}
// 断开连接,释放资源
conn.disconnect();
return ipaddr;
} catch (Exception e) {
log.error("获取ip归属地异常:{}", ip);
e.printStackTrace();
return null;
}
}
return null;
}
}
- 开启多线程
@Configuration
@Slf4j
public class ThreadPoolConfiguration {
/**
* 操作日志线程池
* @return 线程池
*/
@Bean(name = "operationLogThreadPoolExecutor", destroyMethod = "shutdown")
public ThreadPoolExecutor systemCheckPoolExecutorService() {
return new ThreadPoolExecutor(3, 10, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(10000),
new ThreadFactoryBuilder().setNameFormat("operationLog-executor-%d").build(),
(r, executor) -> log.error("操作日志线程已满! "));
}
}
- springboot启动类增加开启多线程注解
@EnableAsync
- 在方法上增加注解(依次:方法名,操作类型,自定义操作内容占位符{0})
@OperateLog(MethodName = "subtractBalance", OperationType = SysOperationType.DEDUCTION, OperationContent = "给【{0}】公司扣费")
- nginx代理获取真实IP增加配置
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 例:
server {
listen 80;
server_name localhost;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://xxxx;
}
- @Async注解失效原因:
1、注解@Async的方法不是public方法
2、注解@Async的返回值只能为void或者Future
3、注解@Async方法使用static修饰也会失效
4、spring无法扫描到异步类,没加注解@Async 或 @EnableAsync注解
5、调用方与被调方不能在同一个类