设计原则和思路:

  • 元注解方式结合AOP,灵活记录操作日志
  • 能够记录详细错误日志为运营以及审计提供支持
  • 日志记录尽可能减少性能影响
  • 操作描述参数支持动态获取,其他参数自动记录。

1.定义日志记录元注解,

根据业务情况,要求description支持动态入参。例:新增应用{applicationName},其中applicationName是请求参数名。
/**
* 自定义注解 拦截Controller
*
* @author jianggy
*
*/
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface SystemControllerLog {
/**
* 描述业务操作 例:Xxx管理-执行Xxx操作
* 支持动态入参,例:新增应用{applicationName},其中applicationName是请求参数名
* @return
*/
String description() default "";
}

2.定义用于记录日志的实体类

package com.guahao.wcp.core.dal.dataobject;

import com.guahao.wcp.core.utils.StringUtils;
import java.io.Serializable;
import java.util.Date;
import java.util.Map; /**
* 日志类-记录用户操作行为
*
* @author lin.r.x
*/
public class OperateLogDO extends BaseDO implements Serializable {
private static final long serialVersionUID = -4000845735266995243L; private String userId; //用户ID
private String userName; //用户名
private String desc; //日志描述
private int isDeleted; //状态标识 private String menuName; //菜单名称
private String remoteAddr; //请求地址
private String requestUri; //URI
private String method; //请求方式
private String params; //提交参数
private String exception; //异常信息
private String type; //日志类型 public String getType() {
return StringUtils.isBlank(type) ? type : type.trim();
} public void setType(String type) {
this.type = type;
} public String getDesc() {
return StringUtils.isBlank(desc) ? desc : desc.trim();
} public void setDesc(String desc) {
this.desc = desc;
} public String getRemoteAddr() {
return StringUtils.isBlank(remoteAddr) ? remoteAddr : remoteAddr.trim();
} public void setRemoteAddr(String remoteAddr) {
this.remoteAddr = remoteAddr;
} public String getRequestUri() {
return StringUtils.isBlank(requestUri) ? requestUri : requestUri.trim();
} public void setRequestUri(String requestUri) {
this.requestUri = requestUri;
} public String getMethod() {
return StringUtils.isBlank(method) ? method : method.trim();
} public void setMethod(String method) {
this.method = method;
} public String getParams() {
return StringUtils.isBlank(params) ? params : params.trim();
} public void setParams(String params) {
this.params = params;
} /**
* 设置请求参数
*
* @param paramMap
*/
public void setMapToParams(Map<String, String[]> paramMap) {
if (paramMap == null) {
return;
}
StringBuilder params = new StringBuilder();
for (Map.Entry<String, String[]> param : ((Map<String, String[]>) paramMap).entrySet()) {
params.append(("".equals(params.toString()) ? "" : "&") + param.getKey() + "=");
String paramValue = (param.getValue() != null && param.getValue().length > 0 ? param.getValue()[0] : "");
params.append(StringUtils.abbr(StringUtils.endsWithIgnoreCase(param.getKey(), "password") ? "" : paramValue, 100));
}
this.params = params.toString();
} public String getException() {
return StringUtils.isBlank(exception) ? exception : exception.trim();
} public void setException(String exception) {
this.exception = exception;
} public String getUserName() {
return StringUtils.isBlank(userName) ? userName : userName.trim();
} public void setUserName(String userName) {
this.userName = userName;
} public String getUserId() {
return userId;
} public void setUserId(String userId) {
this.userId = userId;
} public String getMenuName() {
return menuName;
} public void setMenuName(String menuName) {
this.menuName = menuName;
} public int getIsDeleted() {
return isDeleted;
} public void setIsDeleted(int isDeleted) {
this.isDeleted = isDeleted;
} @Override
public String toString() {
return "OperateLogDO{" +
"userId='" + userId + '\'' +
", userName='" + userName + '\'' +
", desc='" + desc + '\'' +
", isDeleted=" + isDeleted +
", menuName='" + menuName + '\'' +
", remoteAddr='" + remoteAddr + '\'' +
", requestUri='" + requestUri + '\'' +
", method='" + method + '\'' +
", params='" + params + '\'' +
", exception='" + exception + '\'' +
", type='" + type + '\'' +
'}';
}
}

3.定义日志AOP切面类,通过logManager.insert(log)往数据库写入日志。

项目pom.xml中增加spring-boot-starter-aop

        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

具体的日志切点类实现


package com.guahao.wcp.gops.home.aop;

import com.greenline.guser.biz.service.dto.UserInfoDTO;
import com.greenline.guser.client.utils.GuserCookieUtil;
import com.guahao.wcp.gops.home.annotation.SystemControllerLog;
import com.guahao.wcp.gops.home.service.DubboService;
import com.guahao.wcp.core.manager.operatelog.LogManager;
import com.guahao.wcp.core.dal.dataobject.OperateLogDO;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.NamedThreadLocal;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern; /**
* 系统日志切点类
*
* @author jianggy
*/
@Aspect
@Component
public class SystemLogAspect {
private static final Logger logger = LoggerFactory.getLogger(SystemLogAspect.class);
// private static final ThreadLocal<Date> beginTimeThreadLocal = new NamedThreadLocal<Date>("ThreadLocal beginTime");
private static final ThreadLocal<OperateLogDO> logThreadLocal = new NamedThreadLocal<OperateLogDO>("ThreadLocal log");
private static final ThreadLocal<UserInfoDTO> currentUserInfo = new NamedThreadLocal<UserInfoDTO>("ThreadLocal userInfo"); @Autowired(required = false)
private HttpServletRequest request;
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Autowired
private LogManager logManager;
@Autowired
private DubboService dubboService; /**
* Controller层切点 注解拦截
*/
@Pointcut("@annotation(com.guahao.wcp.gops.home.annotation.SystemControllerLog)")
public void controllerAspect() {
} /**
* 方法规则拦截
*/
@Pointcut("execution(* com.guahao.wcp.gops.home.controller.*.*(..))")
public void controllerPointerCut() {
} /**
* 前置通知 用于拦截Controller层记录用户的操作的开始时间
*
* @param joinPoint 切点
* @throws InterruptedException
*/
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint) throws InterruptedException {
// Date beginTime = new Date();
// beginTimeThreadLocal.set(beginTime);
//debug模式下 显式打印开始时间用于调试
// if (logger.isDebugEnabled()) {
// logger.debug("开始计时: {} URI: {}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
// .format(beginTime), request.getRequestURI());
// }
//读取GuserCookie中的用户信息
String loginId = GuserCookieUtil.getLoginId(request);
UserInfoDTO userInfo = dubboService.userInfoService.getUserInfoByLoginId(loginId).getDataResult();
currentUserInfo.set(userInfo);
} /**
* 后置通知 用于拦截Controller层记录用户的操作
*
* @param joinPoint 切点
*/
@After("controllerAspect()")
public void doAfter(JoinPoint joinPoint) {
UserInfoDTO userInfo = currentUserInfo.get();
//登入login操作 前置通知时用户未校验 所以session中不存在用户信息
if (userInfo == null) {
String loginId = GuserCookieUtil.getLoginId(request);
userInfo = dubboService.userInfoService.getUserInfoByLoginId(loginId).getDataResult();
if (userInfo == null) {
return;
}
}
Object[] args = joinPoint.getArgs();
System.out.println(args); String desc = "";
String type = "info"; //日志类型(info:入库,error:错误)
String remoteAddr = request.getRemoteAddr();//请求的IP
String requestUri = request.getRequestURI();//请求的Uri
String method = request.getMethod(); //请求的方法类型(post/get)
Map<String, String[]> paramsMap = request.getParameterMap(); //请求提交的参数
try {
desc = getControllerMethodDescription(request,joinPoint);
} catch (Exception e) {
e.printStackTrace();
}
// debug模式下打印JVM信息。
// long beginTime = beginTimeThreadLocal.get().getTime();//得到线程绑定的局部变量(开始时间)
// long endTime = System.currentTimeMillis(); //2、结束时间
// if (logger.isDebugEnabled()) {
// logger.debug("计时结束:{} URI: {} 耗时: {} 最大内存: {}m 已分配内存: {}m 已分配内存中的剩余空间: {}m 最大可用内存: {}m",
// new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(endTime),
// request.getRequestURI(),
// DateUtils.formatDateTime(endTime - beginTime),
// Runtime.getRuntime().maxMemory() / 1024 / 1024,
// Runtime.getRuntime().totalMemory() / 1024 / 1024,
// Runtime.getRuntime().freeMemory() / 1024 / 1024,
// (Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory() + Runtime.getRuntime().freeMemory()) / 1024 / 1024);
// } OperateLogDO log = new OperateLogDO();
log.setDesc(desc);
log.setType(type);
log.setRemoteAddr(remoteAddr);
log.setRequestUri(requestUri);
log.setMethod(method);
log.setMapToParams(paramsMap);
log.setUserName(userInfo.getName());
log.setUserId(userInfo.getLoginId());
// Date operateDate = beginTimeThreadLocal.get();
// log.setOperateDate(operateDate);
// log.setTimeout(DateUtils.formatDateTime(endTime - beginTime)); //1.直接执行保存操作
//this.logService.createSystemLog(log); //2.优化:异步保存日志
//new SaveLogThread(log, logService).start(); //3.再优化:通过线程池来执行日志保存
threadPoolTaskExecutor.execute(new SaveLogThread(log,logManager));
logThreadLocal.set(log);
} /**
* 异常通知
*
* @param joinPoint
* @param e
*/
@AfterThrowing(pointcut = "controllerAspect()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
OperateLogDO log = logThreadLocal.get();
if (log != null) {
log.setType("error");
log.setException(e.toString());
new UpdateLogThread(log,logManager).start();
}
} /**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param joinPoint 切点
* @return 方法描述
*/
public static String getControllerMethodDescription(HttpServletRequest request,JoinPoint joinPoint) throws IllegalAccessException, InstantiationException {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SystemControllerLog controllerLog = method
.getAnnotation(SystemControllerLog.class);
String desc = controllerLog.description();
List<String> list = descFormat(desc);
for (String s : list) {
//根据request的参数名获取到参数值,并对注解中的{}参数进行替换
String value=request.getParameter(s);
desc = desc.replace("{"+s+"}", value);
}
return desc;
} /**
* 获取日志信息中的动态参数
* @param desc
* @return
*/
private static List<String> descFormat(String desc){
List<String> list = new ArrayList<String>();
Pattern pattern = Pattern.compile("\\{([^\\}]+)\\}");
Matcher matcher = pattern.matcher(desc);
while(matcher.find()){
String t = matcher.group(1);
list.add(t);
}
return list;
}
/**
* 保存日志线程
*
* @author lin.r.x
*/
private static class SaveLogThread implements Runnable {
private OperateLogDO log;
private LogManager logManager; public SaveLogThread(OperateLogDO log, LogManager logManager) {
this.log = log;
this.logManager = logManager;
} @Override
public void run() {
logManager.insert(log);
}
} /**
* 日志更新线程
*
* @author lin.r.x
*/
private static class UpdateLogThread extends Thread {
private OperateLogDO log;
private LogManager logManager; public UpdateLogThread(OperateLogDO log, LogManager logManager) {
super(UpdateLogThread.class.getSimpleName());
this.log = log;
this.logManager = logManager;
} @Override
public void run() {
this.logManager.update(log);
}
}
}

4.实现AsyncConfigurer接口并重写AsyncConfigurer方法,并返回一个ThreadPoolTaskExecutor,这样我们就得到了一个基于线程池的TaskExecutor.

在Executor配置类中增加@EnableAsync注解,开启异步支持。

package com.guahao.wcp.gops.home.configuration;

import com.alibaba.dubbo.common.logger.Logger;
import com.alibaba.dubbo.common.logger.LoggerFactory;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.lang.reflect.Method;
import java.util.concurrent.Executor; /**
* @program: wcp
* @description: 配置类实现AsyncConfigurer接口并重写AsyncConfigurer方法,并返回一个ThreadPoolTaskExecutor
* @author: Cay.jiang
* @create: 2018-03-12 17:27
**/ //声明这是一个配置类
@Configuration
//开启注解:开启异步支持
@EnableAsync
public class TaskExecutorConfigurer implements AsyncConfigurer {
private static final Logger log = LoggerFactory.getLogger(TaskExecutorConfigurer.class);
@Bean
//配置类实现AsyncConfigurer接口并重写AsyncConfigurer方法,并返回一个ThreadPoolTaskExecutor
//这样我们就得到了一个基于线程池的TaskExecutor
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
//如果池中的实际线程数小于corePoolSize,无论是否其中有空闲的线程,都会给新的任务产生新的线程
taskExecutor.setCorePoolSize(5);
//连接池中保留的最大连接数。Default: 15 maxPoolSize
taskExecutor.setMaxPoolSize(10);
//线程池所使用的缓冲队列
taskExecutor.setQueueCapacity(25);
//等待所有线程执行完
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
taskExecutor.initialize();
return taskExecutor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new WcpAsyncExceptionHandler();
}
/**
* 自定义异常处理类
* @author hry
*
*/
class WcpAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
//手动处理捕获的异常
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
System.out.println("-------------》》》捕获到线程异常信息");
log.info("Exception message - " + throwable.getMessage());
log.info("Method name - " + method.getName());
for (Object param : obj) {
log.info("Parameter value - " + param);
}
} }
}

5.logManager调用日志DAO操作,具体的mybatis实现就不写了。

package com.guahao.wcp.core.manager.operatelog.impl;

import com.guahao.wcp.core.dal.dataobject.OperateLogDO;
import com.guahao.wcp.core.dal.mapper.OperateLogMapper;
import com.guahao.wcp.core.manager.operatelog.LogManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; @Service("logManager")
public class LogManagerImpl implements LogManager { @Autowired
private OperateLogMapper operateLogDAO; @Override
public int insert(OperateLogDO log) { System.out.println("新增操作日志:"+log);
return operateLogDAO.insert(log);
} @Override
public int update(OperateLogDO log) {
//暂不实现
//return this.logDao.updateByPrimaryKeySelective(log);
System.out.println("更新操作日志:"+log);
return 1;
} }

6.使用范例ApplicationController方法中添加日志注解

    @RequestMapping(value = "/add.json", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})
@ResponseBody
@SystemControllerLog (description = "【应用管理】新增应用{applicationName}")
public BaseJson add(@ModelAttribute("application") ApplicationDO applicationDO, @ModelAttribute("team") TeamDO teamDO) { .......
}

7.日志数据入库结果

8.日志结果展示

这个简单的。

[编码实践]SpringBoot实战:利用Spring AOP实现操作日志审计管理的更多相关文章

  1. Spring aop 记录操作日志 Aspect

    前几天做系统日志记录的功能,一个操作调一次记录方法,每次还得去收集参数等等,太尼玛烦了.在程序员的世界里,当你的一个功能重复出现多次,就应该想想肯定有更简单的实现方法.于是果断搜索各种资料,终于搞定了 ...

  2. Spring aop 记录操作日志 Aspect 自定义注解

    时间过的真快,转眼就一年了,没想到随手写的笔记会被这么多人浏览,不想误人子弟,于是整理了一个优化版,在这里感谢智斌哥提供的建议和帮助,话不多说,进入正题 所需jar包 :spring4.3相关联以及a ...

  3. 化繁就简,如何利用Spring AOP快速实现系统日志

    1.引言 有关Spring AOP的概念就不细讲了,网上这样的文章一大堆,要讲我也不会比别人讲得更好,所以就不啰嗦了. 为什么要用Spring AOP呢?少写代码.专注自身业务逻辑实现(关注本身的业务 ...

  4. 【Spring Boot】利用 Spring Boot Admin 进行项目监控管理

    利用 Spring Boot Admin 进行项目监控管理 一.Spring Boot Admin 是什么 Spring Boot Admin (SBA) 是一个社区开源项目,用于管理和监视 Spri ...

  5. 利用Spring AOP自定义注解解决日志和签名校验

    转载:http://www.cnblogs.com/shipengzhi/articles/2716004.html 一.需解决的问题 部分API有签名参数(signature),Passport首先 ...

  6. (转)利用Spring AOP自定义注解解决日志和签名校验

    一.需解决的问题 部分API有签名参数(signature),Passport首先对签名进行校验,校验通过才会执行实现方法. 第一种实现方式(Origin):在需要签名校验的接口里写校验的代码,例如: ...

  7. 利用spring AOP 和注解实现方法中查cache-我们到底能走多远系列(46)

    主题:这份代码是开发中常见的代码,查询数据库某个主表的数据,为了提高性能,做一次缓存,每次调用时先拿缓存数据,有则直接返回,没有才向数据库查数据,降低数据库压力. public Merchant lo ...

  8. 利用Spring AOP切面对用户访问进行监控

    开发系统时往往需要考虑记录用户访问系统查询了那些数据.进行了什么操作,尤其是访问重要的数据和执行重要的操作的时候将数记录下来尤显的有意义.有了这些用户行为数据,事后可以以用户为条件对用户在系统的访问和 ...

  9. 利用Spring AOP和自定义注解实现日志功能

    Spring AOP的主要功能相信大家都知道,日志记录.权限校验等等. 用法就是定义一个切入点(Pointcut),定义一个通知(Advice),然后设置通知在该切入点上执行的方式(前置.后置.环绕等 ...

随机推荐

  1. SQL中Union和Union All

    工作中,看到大佬写的一段SQL,查询了五个表中的数据,最后求某个收入的总和,其中使用了Union All,因此在这里记录一下我从中学到的东西 先上语法 Union:   [ Select语句1 ] U ...

  2. js 实现仿 淘宝 五星评价 demo

    <style> @font-face { font-family: 'iconfont'; /* project id 247957 */ src: url('//at.alicdn.co ...

  3. SOCKET.IO 的用法 系统API,

    原文:http://www.cnblogs.com/xiezhengcai/p/3956401.html 1. 服务端 io.on('connection',function(socket)); 监听 ...

  4. [工作积累] Tricks with UE4 PerInstanceRandom

    最近在用UE4的Instancing, 发现限制很多. Unity有instancing的attribute array (uniform/constant buffer),通过InstanceID来 ...

  5. Mysql基本代码操作

    Mysql的基本代码生成操作 创建一个数据库   (myschool是数据库名) create database myschool; 删除数据库 drop database myschool 创建一个 ...

  6. 【druid 】数据库连接池-sql解析

    https://segmentfault.com/a/1190000008120254?utm_source=tuicool&utm_medium=referral sql解析 Druid 的 ...

  7. 1、IT人思维之投资开篇 - IT人思维之投资

    在IT圈中,很多人的认识就只在于工作中获取收益,这个观点是狭隘的,本身就不符合投资领域的内容.所以,在工作之余,笔者就对投资领域的内容进行的分析和收集整理相关的投资方面的内容,一方面对笔者自己的投资方 ...

  8. 友善RK3399/NanoPC-T4开发板wiringPi的C语言访问GPIO外设实例讲解 -【申嵌视频】

    1 wiringPi简介 wiringPi库最早是由Gordon Henderson所编写并维护的一个用C语言写成的类库,除了GPIO库,还包括了I2C库.SPI库.UART库和软件PWM库等,由于w ...

  9. 详解Asp.Net Core 2.1+的视图缓存(响应缓存)

    响应缓存Razor 页与 ASP.NET 核心 2.0 中不支持. 此功能将支持ASP.NET 核心 2.1 版本. 在老的版本的MVC里面,有一种可以缓存视图的特性(OutputCache),可以保 ...

  10. 指定的经纬度是否落在多边形内 java版

    这个想法算法就是判断一个点向左的射线跟一个多边形的交叉点有几个,如果结果为奇数的话那么说明这个点落在多边形中,反之则不在. A: B: C: D: E: no1: no2: y1: y2: 以上的AB ...