设计通用日志审计方案
1.需求
1.用最少的代码,且不用改变业务逻辑代码,记录详细的日志信息
2.支持日志以文件形式输出或数据库存储,通过配置文件灵活切换。
2.实现
要实现肯定是用注解和 aop实现拦截记录日志,这样对业务代码改动最小。
要使用时只需一行代码:
@RequestMapping(value="/test", method = RequestMethod.GET)
@AuditLog(moduleName="系统模块",operation = ""测试" + #username")
public Result list(String username) { return Result.ok(username); }
Aop拦截类:
// 判断日志审计功能是否开启,spring.audit.log.enabled定义在application.properties中
@ConditionalOnProperty(name = "spring.audit.log.enabled", havingValue = "true")
// 表示当只工程中程中引用了HttpServletRequest 或RequestContextHolder 才会构建这个bean
@ConditionalOnClass({ HttpServletRequest.class, RequestContextHolder.class })
@Component@Slf4j
@Aspect
// 判断日志审计功能是否开启
@ConditionalOnProperty(name = "spring.audit.log.enabled", havingValue = "true")
// 表示当只工程中程中引用了HttpServletRequest 或RequestContextHolder 才会构建这个bean
@ConditionalOnClass({ HttpServletRequest.class, RequestContextHolder.class })
public class AuditLogAspect {
public static final String USERID_HEADER = "userid-header";
private static final String AUDIT_OBJ = "audit";
private static ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal<>();
private static final String START_TIME = "startTime";
@Autowired
private AuditService auditService;
/**
* 用于SpEL表达式解析.
*/
private SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
/**
* 用于获取方法参数定义名字.
*/
private DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
@Pointcut("@annotation(com.xxx.log.annoation.AuditLog)")
public void ServiceAspect() {
}
public AuditLogAspect() {
super();
}
@Before("ServiceAspect()")
public void beforeMethod(JoinPoint joinPoint) {
long startTime = System.currentTimeMillis();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
AuditLog auditLog = method.getAnnotation(AuditLog.class);
Audit audit = recordInfo(joinPoint, auditLog);
Map<String, Object> threadInfo = new HashMap<>();
threadInfo.put(START_TIME, startTime);
threadInfo.put(AUDIT_OBJ, audit);
threadLocal.set(threadInfo);
}
@AfterReturning(value = "ServiceAspect()", returning = "returnValue")
public void afterReturning(JoinPoint point, Object returnValue) {
Map<String, Object> threadInfo = threadLocal.get();
long takeTime = System.currentTimeMillis()
- (long) threadInfo.getOrDefault(START_TIME, System.currentTimeMillis());
Audit audit = (Audit) threadInfo.get(AUDIT_OBJ);
audit.setElapsedTime(takeTime);
audit.setError((byte) 0);
threadLocal.remove();
saveLogInfo(audit);
}
@AfterThrowing(value = "ServiceAspect()", throwing = "throwable")
public void doAfterThrowing(JoinPoint point, Throwable throwable) {
Map<String, Object> threadInfo = threadLocal.get();
long takeTime = System.currentTimeMillis()
- (long) threadInfo.getOrDefault(START_TIME, System.currentTimeMillis());
Audit audit = (Audit) threadInfo.get(AUDIT_OBJ);
audit.setElapsedTime(takeTime);
audit.setError((byte) 1);
/**
* 把堆栈信息转成string
*/
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
throwable.printStackTrace(pw);
/**
* 存入堆栈信息
*/
audit.setStatck(sw.toString());
threadLocal.remove();
saveLogInfo(audit);
}
private void saveLogInfo(Audit audit) {
auditService.save(audit);
}
/**
* 解析spEL表达式
*/
private String getValBySpEL(String spEL, MethodSignature methodSignature, Object[] args) {
// 获取方法形参名数组
String[] paramNames = nameDiscoverer.getParameterNames(methodSignature.getMethod());
if (paramNames != null && paramNames.length > 0) {
Expression expression = spelExpressionParser.parseExpression(spEL);
// spring的表达式上下文对象
EvaluationContext context = new StandardEvaluationContext();
// 给上下文赋值
for (int i = 0; i < args.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
return expression.getValue(context).toString();
}
return "";
}
/**
* 构建审计对象
*/
private Audit recordInfo(JoinPoint joinPoint, AuditLog auditLog) {
Audit audit = new Audit();
audit.setTimestamp(LocalDateTime.now());
audit.setModuleName(auditLog.moduleName());
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
audit.setClassName(methodSignature.getDeclaringTypeName());
audit.setMethodName(methodSignature.getName());
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
String queryStr = request.getQueryString();
// userId存放在request header 中
String userId = request.getHeader(USERID_HEADER);
audit.setUserId(userId);
if (StringUtils.isNotEmpty(queryStr)) {
audit.setRequestUrl(request.getRequestURI() + "?" + queryStr);
} else {
audit.setRequestUrl(request.getRequestURI());
}
String operation = auditLog.operation();
if (operation.contains("#")) {
// 获取方法参数值
Object[] args = joinPoint.getArgs();
if (!ObjectUtils.isEmpty(args)) {
operation = getValBySpEL(operation, methodSignature, args);
}
}
audit.setOperation(operation);
return audit;
}
}
数据库日志记录实现,只有当spring.audit.log.log-type=db时才执行此spring bean
@Service@Slf4j
//表示只有当spring.audit.log.log-type=db时才执行此spring bean
@ConditionalOnProperty(name = "spring.audit.log.log-type", havingValue = "db")
@ConditionalOnClass(JdbcTemplate.class)
public class DbAuditServiceImpl implements AuditService {
private static final String INSERT_SQL = " insert into sys_logger " +
" (module_name, class_name, method_name, elapsed_time, request_url, error, statck, operation, timestamp,user_id) " +
" values (?,?,?,?,?,?,?,?,?,?)";
@Autowired
private JdbcTemplate jdbcTemplate;
@PostConstruct
public void init() {
String sql = "CREATE TABLE IF NOT EXISTS `sys_logger` (
" +
" `id` int(11) NOT NULL AUTO_INCREMENT,
" +
" `module_name` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT "模块名",
" +
" `class_name` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT "类名",
" +
" `method_name` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT "方法名",
" +
" `elapsed_time` int(11) NULL COMMENT "耗时",
" +
" `request_url` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT "请求Url",
" +
" `error` int(11) NULL COMMENT "错误标志",
" +
" `statck` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT "错误信息",
" +
" `operation` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT "操作信息",
" +
" `timestamp` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT "创建时间",
" +
" `user_id` int(11) NULL COMMENT "用户ID",
" +
" PRIMARY KEY (`id`) USING BTREE
" +
") ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;";
this.jdbcTemplate.execute(sql);
}
/**
* @Async表示 异常插入
*/
@Async
@Override
public void save(Audit audit) {
this.jdbcTemplate.update(INSERT_SQL
, audit.getModuleName(), audit.getClassName(), audit.getMethodName(),audit.getElapsedTime()
, audit.getRequestUrl(), audit.getError(), audit.getStatck()
, audit.getOperation(), audit.getTimestamp(),audit.getUserId());
}
}
日志文件记录方式
@Slf4j@Service
@ConditionalOnProperty(name = "spring.audit.log.log-type", havingValue = "logger", matchIfMissing = true)
public class LogAuditServiceImpl implements AuditService {
private static final DateTimeFormatter OF_PATTERN = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
private static final String MSG_PATTERN = "{}|{}|{}|{}|{}|{}|{}";
@Override
public void save(Audit audit) {
if (1 == audit.getError()) {
log.error(MSG_PATTERN, audit.getTimestamp().format(OF_PATTERN),
audit.getModuleName(),audit.getUserId(), audit.getRequestUrl(), audit.getOperation(), audit.getElapsedTime(),
audit.getStatck());
} else {
log.debug(MSG_PATTERN, audit.getTimestamp().format(OF_PATTERN),
audit.getModuleName(), audit.getUserId(),audit.getRequestUrl(), audit.getOperation(), audit.getElapsedTime(),
audit.getError());
}
}
}
以上是 设计通用日志审计方案 的全部内容, 来源链接: utcz.com/z/516312.html