[编码实践]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),然后设置通知在该切入点上执行的方式(前置.后置.环绕等 ...
随机推荐
- FlappyBird开发帮助文档
FlappyBird开发帮助文档 项目需求 完成FlappyBird游戏. 功能说明: 游戏开始后,间歇性的点击鼠标,让小鸟向上飞,不会掉下来,并且要穿过柱子的空隙,不能碰到柱子,碰到就dead了,穿 ...
- impdp如何杀掉job
今天刚执行一个impdp操作发现,硬盘空间可能不够了,赶紧按了Ctrl+C,结果发现磁盘空间仍然在减少.问了一下部门oracle专家,Ctrl+C是无法停止impdp的job的.需要将job进行att ...
- Python 3 教程
http://www.runoob.com/python3/python3-tutorial.html https://www.jianshu.com/p/f1332c58ca86
- javaScript中的redirect
<script type="text/javascript"> <!-- window.location = "http://www.google.co ...
- AR图像识别 AR识别图像 AR摄像头识别 外包开发 AR识别应用开发就找北京动点软件
当绝大多数手机厂商还在追求后置双摄拍照的时候,已经有人开始潜心研究AR手机了.刚刚结束的美国消费电子展上,华硕发布了全新的ZenFone AR手机,配备5.7英寸2K屏.骁龙821处理器.8GB内存, ...
- 使用密钥登录CentOS系统(基于密钥的认证)
在Window客户端有多种软件可以登陆ssh,比如putty,xshelll,secureCRT,我就以xshell为例设置使用公钥和私钥验证登陆服务器. 使用Xshell密钥认证机制远程登录Linu ...
- 360或者金山毒霸可能会导致HP网络打印机驱动安装失败“数据无效”的解决办法
360或者金山毒霸可能会导致HP网络打印机驱动安装失败“数据无效”的解决办法 同事办公室的打印机是网线接口的那种网络打印机,不是直接连到电脑的那种,他电脑安装了360和金山毒霸,WIN10下安 ...
- maven项目pom.xml添加main启动类
pom.xml配置添加main启动类: <build> <finalName>MyApp</finalName> <!-- 最终package打包的jar名称 ...
- 利用redis 漏洞入侵挖矿临时解决办法
top 看到一个bashd的进程占据了cpu ps aux |grep bashd cd /tmp 发现ddg.2011 的文件.root dump.rdb 在/root/.ssh 也有奇怪的文件 ...
- Nginx 流量和连接数限制
1.Nginx流量限制 实现流量限制由两个指令 limit_rate 和 limit_rate_after 共同完成: limit_rate 语法:limit_rate rate; 默认值:limit ...