[编码实践]SpringBoot实战:利用Spring AOP实现操作日志审计管理
设计原则和思路:
- 元注解方式结合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实现操作日志审计管理的更多相关文章
- Spring aop 记录操作日志 Aspect
前几天做系统日志记录的功能,一个操作调一次记录方法,每次还得去收集参数等等,太尼玛烦了.在程序员的世界里,当你的一个功能重复出现多次,就应该想想肯定有更简单的实现方法.于是果断搜索各种资料,终于搞定了 ...
- Spring aop 记录操作日志 Aspect 自定义注解
时间过的真快,转眼就一年了,没想到随手写的笔记会被这么多人浏览,不想误人子弟,于是整理了一个优化版,在这里感谢智斌哥提供的建议和帮助,话不多说,进入正题 所需jar包 :spring4.3相关联以及a ...
- 化繁就简,如何利用Spring AOP快速实现系统日志
1.引言 有关Spring AOP的概念就不细讲了,网上这样的文章一大堆,要讲我也不会比别人讲得更好,所以就不啰嗦了. 为什么要用Spring AOP呢?少写代码.专注自身业务逻辑实现(关注本身的业务 ...
- 【Spring Boot】利用 Spring Boot Admin 进行项目监控管理
利用 Spring Boot Admin 进行项目监控管理 一.Spring Boot Admin 是什么 Spring Boot Admin (SBA) 是一个社区开源项目,用于管理和监视 Spri ...
- 利用Spring AOP自定义注解解决日志和签名校验
转载:http://www.cnblogs.com/shipengzhi/articles/2716004.html 一.需解决的问题 部分API有签名参数(signature),Passport首先 ...
- (转)利用Spring AOP自定义注解解决日志和签名校验
一.需解决的问题 部分API有签名参数(signature),Passport首先对签名进行校验,校验通过才会执行实现方法. 第一种实现方式(Origin):在需要签名校验的接口里写校验的代码,例如: ...
- 利用spring AOP 和注解实现方法中查cache-我们到底能走多远系列(46)
主题:这份代码是开发中常见的代码,查询数据库某个主表的数据,为了提高性能,做一次缓存,每次调用时先拿缓存数据,有则直接返回,没有才向数据库查数据,降低数据库压力. public Merchant lo ...
- 利用Spring AOP切面对用户访问进行监控
开发系统时往往需要考虑记录用户访问系统查询了那些数据.进行了什么操作,尤其是访问重要的数据和执行重要的操作的时候将数记录下来尤显的有意义.有了这些用户行为数据,事后可以以用户为条件对用户在系统的访问和 ...
- 利用Spring AOP和自定义注解实现日志功能
Spring AOP的主要功能相信大家都知道,日志记录.权限校验等等. 用法就是定义一个切入点(Pointcut),定义一个通知(Advice),然后设置通知在该切入点上执行的方式(前置.后置.环绕等 ...
随机推荐
- 前端---js02
主要内容 1.数组 2.字符串 3.Date日期对象 4.内置对象 5.定时器 6.DOM 7.伪数组 内置对象: 1 数组(列表) Array (1) 数组的创建 <script>//字 ...
- 服务器A制定计划任务,BAT脚本自动备份oracle数据文件,拷贝至服务器B的共享目录。
运行环境:windows server 2008 R2 目的:在数据库服务器A进行数据库自动备份,并且保留5天. 为了安全,需要在web应用服务器B进行数据库的冗余备份,建立双保险.(保留15天) A ...
- Web前端学习第一天
1.SQL注入攻击的发生 select username, email ,descl from users where id=1; 可能会被伪造成 1 union select password,1, ...
- Java I/O输入输出流
IO流的复习总结 ------注:蓝色背景段落是例子:红色背景的字段IO流的功能类. 编码问题 String s = "威力锅ABC"; //utf-8编码中文占用三个字节,英文 ...
- 《DSP using MATLAB》Problem 7.16
使用一种固定窗函数法设计带通滤波器. 代码: %% ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ...
- Python基础:十一、流程控制(if语句、while循环)
一.流程控制——if循环 1.第一种语法: if条件: #引号是将条件与结果分开 结果1 #四个空格,或者一个tab键,这个是告诉程序满足条件的话,运行这个结果 结果2 #如果条件是真(True)执行 ...
- pip install torch on windows, and the 'from torch._C import * ImportError: DLL load failed:' solution
通过pip安装PyTorch 0.4.0成功(cpu, not gpu; python3.5; pip): pip3 install http://download.pytorch.org/whl/c ...
- Android Studio学习NO.1 了解项目资源
2018.3.1 12:40:51 阅读书籍:第一行代码 1. res目录 drawable 图片 mipmap 图标 values 字符串.样式.颜色 layout 布局 2. 引用(可在Andro ...
- Linux内核笔记:内存管理
逻辑地址由16位segment selector和offset组成 根据segment selector到GDT或LDT中去查找segment descriptor 32位base,20位limit, ...
- NRF51800 空中升级DFU
下面是基础软件的安装:[抄袭他人所得] 1.安装Python软件,建议版本2.7.9及以上,不超过3.0版本[以下安装步骤需要联网]2.安装Python的pip模块,通过命令提示符进入到Python的 ...