[编码实践]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 paramMap) {
if (paramMap == null) {
return;
}
StringBuilder params = new StringBuilder();
for (Map.Entry param : ((Map) 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.bootgroupId> <artifactId>spring-boot-starter-aopartifactId> 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 ThreadLocalbeginTimeThreadLocal = new NamedThreadLocal ("ThreadLocal beginTime");
private static final ThreadLocallogThreadLocal = new NamedThreadLocal ("ThreadLocal log");
private static final ThreadLocalcurrentUserInfo = new NamedThreadLocal ("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)
MapparamsMap = 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();
Listlist = descFormat(desc);
for (String s : list) {
//根据request的参数名获取到参数值,并对注解中的{}参数进行替换
String value=request.getParameter(s);
desc = desc.replace("{"+s+"}", value);
}
return desc;
}
/**
* 获取日志信息中的动态参数
* @param desc
* @return
*/
private static ListdescFormat(String desc){
Listlist = new ArrayList ();
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.日志结果展示
这个简单的。
posted on
2018-03-15 16:23 蒋刚毅 阅读(
...) 评论(
...) 编辑 收藏