设计原理
在我们开始编写开发日志starter之前,我们需要先整理好这个starter的应用方式,首先我们需要对日志工具类做一个需求设计,考虑到代码的侵入性、封装性,我们需要使用到AOP、注解,然后通过日志记录的方式(例如:存入数据库、记录到日志文件中),我们可以在配置文件中配置使用的方式,然后通过 @ConditionalOnProperty注解来获取配置文件中的值来判断记录的日志方式。
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
注解开发
首先我们需要开发一个注解类,该注解主要传入的参数是opera,及用户对应的操作
package com.fengchuang.log.annotation;
import java.lang.annotation.*;
/**
* @author fengchuang
* @date 2023/11/3
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BussinessLog {
/**
* 操作信息
*/
String opera();
}
starter配置信息
package com.fengchuang.log.properties;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
/**
* 审计日志配置
*
* @author fengchuang
* @date 2023/11/3
*/
@Setter
@Getter
@ConfigurationProperties(prefix = "fc.business-log")
@RefreshScope
public class BusinessLogProperties {
/**
* 是否开启审计日志
*/
private Boolean enabled = false;
/**
* 日志记录类型(log/db)
*/
private String logType;
}
@RefreshScope:微服务体系中刷新远程配置 @ConfigurationProperties(prefix = "fc.bussiness-log"):获取配置文件中fc.bussiness-log的配置
创建日志实体类
package com.fengchuang.pojo;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
/**
* 审计日志
*
* @author fengchuang
* @date 2023/11/3
*/
@Setter
@Getter
public class Business {
/**
* 操作时间
*/
private LocalDateTime timestamp;
/**
* 应用名
*/
private String applicationName;
/**
* 类名
*/
private String className;
/**
* 方法名
*/
private String methodName;
/**
* 用户id
*/
private String userId;
/**
* 用户名
*/
private String userName;
/**
* 租户id
*/
private String tenantId;
/**
* 操作信息
*/
private String opera;
}
AOP日志切面
接下来我们需要开发的是AOP日志切面类
package com.fengchuang.aspect;
import com.fengchuang.annotation.BusinessLog;
import com.fengchuang.pojo.Business;
import com.fengchuang.properties.BusinessLogProperties;
import com.fengchuang.service.ILogService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* 业务日志切面
*
* @author fengchuang
* @date 2023/11/3
*/
@Slf4j
@Aspect
@Component
public class BusinessLogAspect {
@Value("${spring.application.name}")
private String applicationName;
private BusinessLogProperties businessLogProperties;
private ILogService logService;
public BusinessLogAspect(BusinessLogProperties businessLogProperties, ILogService logService) {
this.businessLogProperties = businessLogProperties;
this.logService = logService;
}
@Before("@within(businessLog) || @annotation(businessLog)")
public void beforeMethod(JoinPoint joinPoint, BusinessLog businessLog) {
//判断功能是否开启
if (businessLogProperties.getEnabled()) {
if (logService == null) {
log.warn("AuditLogAspect - auditService is null");
return;
}
if (businessLog == null) {
// 获取类上的注解
businessLog = joinPoint.getTarget().getClass().getDeclaredAnnotation(BusinessLog.class);
}
Business business = getAudit(businessLog, joinPoint);
logService.save(business);
}
}
/**
* 构建审计对象
*/
private Business getAudit(BusinessLog businessLog, JoinPoint joinPoint) {
Business business = new Business();
business.setTimestamp(LocalDateTime.now());
business.setApplicationName(applicationName);
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
business.setClassName(methodSignature.getDeclaringTypeName());
business.setMethodName(methodSignature.getName());
String operation = businessLog.opera();
business.setOpera(operation);
return business;
}
}
@Aspect:定义切面类 @Value("${spring.application.name}"):获取配置类中的应用名 @Before("@within(bussinessLog) || @annotation(bussinessLog)"):@Before表示的是方法执行前 @within和@annotation主要是在切面前判断方法是否有@BussinessLog的注解,如果有则在方法执行前切面。
实现LogService实现类
1.我们可以通过@ConditionalOnProperty来判断我们配置文件中设置的文件类型选择的日志记录方式 name表示获取配置文件中fc.business-log.type属性值,借助于hadingValue属性,清楚地表明,仅当fc.business-log.type设置为log时,才希望加载LogServiceImpl。同时我们也可以通过该方法拓展出别的日志记录方法(比如存储数据库等)
package com.fengchuang.service.impl;
import com.fengchuang.pojo.Business;
import com.fengchuang.service.ILogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import java.time.format.DateTimeFormatter;
/**
* 审计日志实现类-打印日志
*
* @author fengchuang
* @date 2023/11/3
*/
@Slf4j
@Service
@ConditionalOnProperty(name = "fc.business-log.log-type", havingValue = "log", matchIfMissing = true)
public class LogServiceImpl implements ILogService {
private static final String MSG_PATTERN = "{}|{}|{}|{}|{}|{}|{}|{}";
/**
* 格式为:{时间}|{应用名}|{类名}|{方法名}|{用户id}|{用户名}|{租户id}|{操作信息}
*/
@Override
public void save(Business businessMsg) {
log.debug(MSG_PATTERN
, businessMsg.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"))
, businessMsg.getApplicationName(), businessMsg.getClassName(), businessMsg.getMethodName()
, businessMsg.getUserId(), businessMsg.getUserName(), businessMsg.getTenantId()
, businessMsg.getOpera());
}
}
springboot自动配置类
/**
* 日志自动配置
*
* @author fengchuang
* @date 2023/11/3
*/
@ComponentScan
@EnableConfigurationProperties({AuditLogProperties.class})
public class LogAutoConfigure {
}
@EnableConfigurationProperties:让使用@ConfigurationProperties 注解的类生效。
在resources目录下新建META-INF文件夹 创建spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.fengchuang.log.LogAutoConfigure
至此我们制作的基于AOP日志切面的stater就已经做好了,我们可以在别的项目中直接依赖他或者微服务的形式将stater放入公共方法中,供别的模块插拔式使用,当别的模块依赖我们的starter后
fc:
#日志
business-log:
enabled: true
logType: log
在对应的方法加上
@Business(opera = "对应操作")
这样我们就封装好了一个日志的starter。 git源码地址