设计原则和思路:
- 元注解方式结合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.日志结果展示
这个简单的。