✨这里是第七人格的博客✨小七,欢迎您的到来~✨
🍅系列专栏:【架构思想】🍅
✈️本篇内容: 从0到1带你实现一个业务日志组件(二)✈️
🍱本篇收录完整代码地址:gitee.com/diqirenge/b...
楔子
上一篇(从0到1带你实现一个业务日志组件(一)需求分析篇)小七对需求进行了拆分,并且实现了业务日志的基本架构,这一篇小七将会继续带着大家,实现相关核心代码,废话少说,开干!
设计思路
再次回顾一下我们的设计思路
定义组件
从以上设计思路,我们完成了对注解的定义,并编写了一个待实现逻辑的aop切面。这一章我们先完成组件的核心代码------解析,为了更好地实现功能,小七提供了以下思维导图
1、parser-解析模块
该模块下主要是对业务日志进行解析。
小七这里计划参考org.springframework.cache.interceptor.CacheOperationExpressionEvaluator实现。
2、model-实体模块
抽象实体,这里放一些,组件自己内部使用的实体,比如业务日志实体、业务操作日志实体、方法执行结果实体。
3、config-配置模块
初始化一些组件必须要用到的bean,单独写这么一个配置类,是为了方便以后抽取成spring-boot-starter。
4、exception-统一异常模块
封装的组件统一异常。
5、constant-常量模块
组件用到的一些常量,比如"#"就可以定义到里面。
6、interface-接口模块
我们需要暴露的一些接口就要写到这里。说白了,就是我们提供给组件使用方的一些拓展点,暂时定义两个:
(1)用于创建自定义函数的接口
(2)用于创建自定义日志记录的接口
分支名称
231013-52javaee.com-BizLogDefinition
仓库地址
分支描述
编写业务日志组件相关定义代码。
代码实现
constant模块
BizLogConsts
java
/**
* 业务日志常量
* 关注公众号【奔跑的码畜】,一起进步不迷路
*
* @author 第七人格
* @date 2023/10/13
*/
public final class BizLogConsts {
/**
* #号键值,用于替换参数
*/
public static final String POUND_KEY = "#";
/**
* 内置参数:错误信息
*/
public static final String ERR_MSG = "_errMsg";
/**
* 内置参数:结果
*/
public static final String RESULT = "_result";
}
model模块
BizLogInfo
java
/**
* 日志信息
* 关注公众号【奔跑的码畜】,一起进步不迷路
*
* @author 第七人格
* @date 2023/10/13
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BizLogInfo {
/**
* 系统
*/
private String system;
/**
* 操作者
*/
private String operator;
/**
* 业务id
*/
private String bizNo;
/**
* 模块
*/
private String module;
/**
* 操作类型
*/
private String type;
/**
* 成功操作内容
*/
private String content;
/**
* 操作时间 时间戳单位:ms
*/
private Long operateTime;
/**
* 操作花费的时间 单位:ms
*/
private Long executeTime;
/**
* 是否调用成功
*/
private Boolean success;
/**
* 执行后返回的json字符串
*/
private String result;
/**
* 错误信息
*/
private String errorMsg;
/**
* 详细
*/
private String details;
}
BizLogOps
java
/**
* 业务日志选项
* 关注公众号【奔跑的码畜】,一起进步不迷路
*
* @author 第七人格
* @date 2023/10/13
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BizLogOps {
private String system;
private String operator;
private String bizNo;
private String module;
private String type;
private String success;
private String fail;
private String details;
private String condition;
}
MethodExecuteResult
java
/**
* 方法的执行结果
* 关注公众号【奔跑的码畜】,一起进步不迷路
*
* @author 第七人格
* @date 2023/10/13
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MethodExecuteResult {
private boolean success;
private Throwable throwable;
private String errMsg;
private Long operateTime;
private Long executeTime;
public MethodExecuteResult(boolean success) {
this.success = success;
this.operateTime = System.currentTimeMillis();
}
public void exception(Throwable throwable) {
this.success = false;
this.executeTime = System.currentTimeMillis() - this.operateTime;
this.throwable = throwable;
this.errMsg = throwable.getMessage();
}
}
exception模块
BizLogException
java
/**
* 业务日志异常
* 关注公众号【奔跑的码畜】,一起进步不迷路
*
* @author 第七人格
* @date 2023/10/13
*/
public class BizLogException extends RuntimeException {
public BizLogException(String message) {
super(message);
}
}
interface模块
ICustomFunctionService
java
/**
* 用于创建自定义函数的接口
* 关注公众号【奔跑的码畜】,一起进步不迷路
*
* @author 第七人格
* @date 2023/10/13
*/
public interface ICustomFunctionService {
/**
* true:前置函数,false:后置函数
*
* 该拓展点主要是为了解决取数时机问题,比如:
* 有些数据在目标方法执行之前就可以拿到,那么自定义函数这个值就应该为true
* 有些数据要目标方法执行之后才能拿得到,那么自定义函数这个值就应该为false
*
* @return 是否执行前的函数
*/
boolean executeBefore();
/**
* 获取自定义函数名称
*
* @return 自定义函数名
*/
String functionName();
/**
* 应用自定义函数
*
* @param param 参数
* @return 执行结果
*/
String apply(Object param);
}
ILogRecordService
java
/**
* 日志记录接口
* 关注公众号【奔跑的码畜】,一起进步不迷路
*
* @author 第七人格
* @date 2023/10/13
*/
public interface ILogRecordService {
/**
* 保存 log
*
* @param bizLogInfo 日志实体
*/
void record(BizLogInfo bizLogInfo);
}
为了方便获取自定义函数,我们可以搞一个工厂把自定义函数缓存起来
java
/**
* 自定义函数工厂
* 关注公众号【奔跑的码畜】,一起进步不迷路
*
* @author 第七人格
* @date 2023/10/13
*/
public class CustomFunctionFactory {
/**
* 自定义函数map
* 项目启动时,将自定义函数注册到map中(因为启动时就添加好了,所以不需要考虑线程安全问题,这里使用HashMap就可以了)
*/
private static final Map<String, ICustomFunctionService> CUSTOM_FUNCTION_MAP = new HashMap<>();
public CustomFunctionFactory(List<ICustomFunctionService> customFunctions) {
for (ICustomFunctionService customFunction : customFunctions) {
CUSTOM_FUNCTION_MAP.put(customFunction.functionName(), customFunction);
}
}
/**
* 通过函数名获取对应自定义函数
*
* @param functionName 函数名
* @return 自定义函数
*/
public ICustomFunctionService getFunction(String functionName) {
return CUSTOM_FUNCTION_MAP.get(functionName);
}
}
然后再抽象一层IFunctionService,针对内部使用。与ICustomFunctionService区分开来,ICustomFunctionService给组件外部使用。
java
/**
* 函数服务
* 关注公众号【奔跑的码畜】,一起进步不迷路
*
* @author 第七人格
* @date 2023/10/13
*/
public interface IFunctionService {
/**
* 执行函数
*
* @param functionName 函数名
* @param value 参数
* @return 执行结果
*/
String apply(String functionName, Object value);
/**
* 是否在拦截的方法执行前执行
*
* @param functionName 函数名
* @return boolean
*/
boolean executeBefore(String functionName);
}
parser模块
BizLogEvaluationContext
java
/**
* 基于方法的上下文,主要作用:将方法参数放入到上下文中
* 关注公众号【奔跑的码畜】,一起进步不迷路
*
* @author 第七人格
* @date 2023/10/13
*/
public class BizLogEvaluationContext extends MethodBasedEvaluationContext {
public BizLogEvaluationContext(Method method, Object[] arguments, ParameterNameDiscoverer parameterNameDiscoverer) {
super(null, method, arguments, parameterNameDiscoverer);
}
/**
* 将方法执行结果放入上下文中
*
* @param errMsg 错误信息
* @param result 返回结果
*/
public void putResult(String errMsg, Object result) {
super.setVariable(BizLogConsts.ERR_MSG, errMsg);
super.setVariable(BizLogConsts.RESULT, result);
}
}
BizLogCachedExpressionEvaluator
java
/**
* 缓存表达式求值器
* 关注公众号【奔跑的码畜】,一起进步不迷路
* 参考 {@link org.springframework.cache.interceptor.CacheOperationExpressionEvaluator}
*
* @author 第七人格
* @date 2023/10/13
*/
public class BizLogCachedExpressionEvaluator extends CachedExpressionEvaluator {
/**
* 缓存key
*/
private final Map<ExpressionKey, Expression> keyCache = new ConcurrentHashMap<>(64);
public BizLogCachedExpressionEvaluator() {
}
/**
* 创建上下文
*
* @param method 方法
* @param args 参数
* @param beanFactory bean工厂
* @param errMsg 错误信息
* @param result 结果
* @return {@link EvaluationContext} 返回结果
*/
public EvaluationContext createEvaluationContext(Method method, Object[] args, BeanFactory beanFactory, String errMsg, Object result) {
BizLogEvaluationContext evaluationContext = new BizLogEvaluationContext(method, args, this.getParameterNameDiscoverer());
evaluationContext.putResult(errMsg, result);
if (beanFactory != null) {
// setBeanResolver 主要用于支持SpEL模板中调用指定类的方法,如:@XXService.x(#root)
evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
}
return evaluationContext;
}
public Object parseExpression(String expression, AnnotatedElementKey methodKey, EvaluationContext evalContext) {
return this.getExpression(this.keyCache, methodKey, expression).getValue(evalContext);
}
void clear() {
this.keyCache.clear();
}
}
BizLogParser
java
/**
* 业务日志解析器
* 关注公众号【奔跑的码畜】,一起进步不迷路
*
* @author 第七人格
* @date 2023/10/13
*/
public class BizLogParser implements BeanFactoryAware {
/**
* 实现BeanFactoryAware以获取容器中的 beanFactory对象,
* 拿到beanFactory后便可以获取容器中的bean,用于SpEl表达式的解析
*/
private BeanFactory beanFactory;
/**
* 这个正则表达式的含义为:
* 匹配一个包含在花括号中的字符串,其中花括号中可以包含任意数量的空白字符(包括空格、制表符、换行符等),
* 并且花括号中至少包含一个单词字符(字母、数字或下划线)。
* =================================================
* 具体来说,该正则表达式由两部分组成:
* {s*(\w*)\s*}:表示匹配一个左花括号,后面跟随零个或多个空白字符,然后是一个单词字符(字母、数字或下划线)零个或多个空白字符,最后是一个右花括号。这部分用括号括起来,以便提取匹配到的内容。
* (.*?):表示匹配任意数量的任意字符,但尽可能少地匹配。这部分用括号括起来,以便提取匹配到的内容。
* =================================================
* 因此,整个正则表达式的意思是:
* 匹配一个包含在花括号中的字符串,
* 其中花括号中可以包含任意数量的空白字符(包括空格、制表符、换行符等),
* 并且花括号中至少包含一个单词字符(字母、数字或下划线),并提取出花括号中的内容。
* =================================================
*/
private static final Pattern PATTERN = Pattern.compile("\\{\\s*(\\w*)\\s*\\{(.*?)}}");
/**
* 自定义函数服务
*/
@Resource
private IFunctionService customFunctionService;
/**
* 缓存表达式求值器
*/
private final BizLogCachedExpressionEvaluator cachedExpressionEvaluator = new BizLogCachedExpressionEvaluator();
/**
* 处理前置函数
*
* @param templates 模版
* @param method 方法
* @param args 参数
* @param targetClass 目标类
* @return {@link Map}<{@link String}, {@link String}> 返回结果
*/
public Map<String, String> processBeforeExec(List<String> templates, Method method, Object[] args, Class<?> targetClass) {
HashMap<String, String> map = new HashMap<>();
AnnotatedElementKey elementKey = new AnnotatedElementKey(method, targetClass);
EvaluationContext evaluationContext = cachedExpressionEvaluator.createEvaluationContext(method, args, beanFactory, null, null);
for (String template : templates) {
if (!template.contains("{")) {
continue;
}
Matcher matcher = PATTERN.matcher(template);
while (matcher.find()) {
String paramName = matcher.group(2);
if (paramName.contains(BizLogConsts.POUND_KEY + BizLogConsts.ERR_MSG) || paramName.contains(BizLogConsts.POUND_KEY + BizLogConsts.RESULT)) {
continue;
}
String funcName = matcher.group(1);
if (customFunctionService.executeBefore(funcName)) {
Object value = cachedExpressionEvaluator.parseExpression(paramName, elementKey, evaluationContext);
String apply = customFunctionService.apply(funcName, value == null ? null : value.toString());
map.put(getFunctionMapKey(funcName, paramName), apply);
}
}
}
return map;
}
/**
* 处理后置函数
*
* @param expressTemplate 待解析的模板
* @param funcValBeforeExecMap 自定义前置函数
* @param method 方法
* @param args 参数
* @param targetClass 目标类
* @param errMsg 错误信息
* @param result 结果
* @return {@link Map}<{@link String}, {@link String}> 返回结果
*/
public Map<String, String> processAfterExec(List<String> expressTemplate, Map<String, String> funcValBeforeExecMap, Method method, Object[] args, Class<?> targetClass, String errMsg, Object result) {
HashMap<String, String> map = new HashMap<>();
AnnotatedElementKey elementKey = new AnnotatedElementKey(method, targetClass);
EvaluationContext evaluationContext = cachedExpressionEvaluator.createEvaluationContext(method, args, beanFactory, errMsg, result);
for (String template : expressTemplate) {
if (template.contains("{")) {
Matcher matcher = PATTERN.matcher(template);
StringBuffer parsedStr = new StringBuffer();
while (matcher.find()) {
String paramName = matcher.group(2);
Object value = cachedExpressionEvaluator.parseExpression(paramName, elementKey, evaluationContext);
String funcName = matcher.group(1);
String param = value == null ? "" : value.toString();
String functionVal = ObjectUtils.isEmpty(funcName) ? param : getFuncVal(funcValBeforeExecMap, funcName, paramName, param);
matcher.appendReplacement(parsedStr, functionVal);
}
matcher.appendTail(parsedStr);
map.put(template, parsedStr.toString());
} else {
Object value;
try {
value = cachedExpressionEvaluator.parseExpression(template, elementKey, evaluationContext);
} catch (Exception e) {
throw new BizLogException(method.getDeclaringClass().getName() + "." + method.getName() + "下 BizLog 解析失败: [" + template + "], 请检查是否符合SpEl表达式规范!");
}
map.put(template, value == null ? "" : value.toString());
}
}
return map;
}
/**
* 获取前置函数映射的 key
*
* @param funcName 方法名
* @param param 参数
* @return {@link String} 返回结果
*/
private String getFunctionMapKey(String funcName, String param) {
return funcName + param;
}
/**
* 获取自定义函数值
*
* @param funcValBeforeExecutionMap 执行之前的函数值
* @param funcName 函数名
* @param paramName 函数参数名称
* @param param 函数参数
* @return {@link String} 返回结果
*/
public String getFuncVal(Map<String, String> funcValBeforeExecutionMap, String funcName, String paramName, String param) {
String val = null;
if (!CollectionUtils.isEmpty(funcValBeforeExecutionMap)) {
val = funcValBeforeExecutionMap.get(getFunctionMapKey(funcName, paramName));
}
if (ObjectUtils.isEmpty(val)) {
val = customFunctionService.apply(funcName, param);
}
return val;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
}
config模块
java
/**
* 日志基础配置,这个类初始化了一些组件必须要用到的bean
* <p>
* 为什么要单独写一个配置类呢?
* 1、以后可以引入业务日志组件的全局配置
* 2、方便抽取成start
* 关注公众号【奔跑的码畜】,一起进步不迷路
*
* @author 第七人格
* @date 2023/10/13
*/
@Configuration
public class BizLogAutoConfiguration {
/**
* 自定义函数拓展点
*
* @return {@link ICustomFunctionService} 注入ICustomFunctionService
*/
@Bean
@ConditionalOnMissingBean(ICustomFunctionService.class)
@Role(BeanDefinition.ROLE_APPLICATION)
public ICustomFunctionService customFunction() {
// todo 这里需要组件实现一下默认的处理器
return new DefaultCustomFunctionHandler();
}
/**
* 自定义函数工厂,项目启动时Spring会自动注入这些bean
*
* @param iCustomFunctionServiceList 实现了{@link ICustomFunctionService}的bean的集合
* @return {@link CustomFunctionFactory} 注入CustomFunctionFactory
*/
@Bean
public CustomFunctionFactory CustomFunctionRegistrar(List<ICustomFunctionService> iCustomFunctionServiceList) {
return new CustomFunctionFactory(iCustomFunctionServiceList);
}
/**
* 自定义函数
*
* @param customFunctionFactory 自定义函数工厂
* @return {@link IFunctionService} 注入IFunctionService
*/
@Bean
public IFunctionService customFunctionService(CustomFunctionFactory customFunctionFactory) {
// todo 这里需要组件实现一下默认的处理器
return new DefaultFunctionHandler(customFunctionFactory);
}
/**
* 日志解析器
*
* @return {@link BizLogParser} 注入BizLogParser
*/
@Bean
public BizLogParser bizLogParser() {
return new BizLogParser();
}
/**
* 日志记录拓展点,如果需要拓展日志记录,可以实现该接口
*
* @return {@link ILogRecordService} 注入ILogRecordService
*/
@Bean
@ConditionalOnMissingBean(ILogRecordService.class)
@Role(BeanDefinition.ROLE_APPLICATION)
public ILogRecordService recordService() {
// todo 这里需要组件实现一下默认的处理器
return new DefaultLogRecordHandler();
}
}
java
// 如果实现缺失才会走这里
@ConditionalOnMissingBean
// 用户定义的Bean的优先级需要高于默认的Bean,所以这里需要加上这个注解
@Role(BeanDefinition.ROLE_APPLICATION)
默认handler实现
java
/**
* 自定义函数的默认实现,增加一层是为了屏蔽底层与上层直接接触(也就是说这个类是给组件内部玩的)
* 日志组件都操作IFunctionService的基础服务对象
* 关注公众号【奔跑的码畜】,一起进步不迷路
*
* @author 第七人格
* @date 2023/10/13
*/
public class DefaultFunctionHandler implements IFunctionService {
private final CustomFunctionFactory customFunctionFactory;
public DefaultFunctionHandler(CustomFunctionFactory customFunctionFactory) {
this.customFunctionFactory = customFunctionFactory;
}
@Override
public String apply(String functionName, Object value) {
ICustomFunctionService function = customFunctionFactory.getFunction(functionName);
if (function == null) {
return value.toString();
}
return function.apply(value);
}
@Override
public boolean executeBefore(String functionName) {
ICustomFunctionService function = customFunctionFactory.getFunction(functionName);
return function != null && function.executeBefore();
}
}
java
/**
* 默认实现类,用于创建自定义函数
* 关注公众号【奔跑的码畜】,一起进步不迷路
*
* @author 第七人格
* @date 2023/10/13
*/
public class DefaultCustomFunctionHandler implements ICustomFunctionService {
@Override
public boolean executeBefore() {
return false;
}
@Override
public String functionName() {
return "defaultName";
}
@Override
public String apply(Object value) {
return null;
}
}
java
/**
* 默认业务日志记录处理器
* 注:自己实现的过滤器需要加上@Service等注解,并实现ILogRecordService接口
* 关注公众号【奔跑的码畜】,一起进步不迷路
*
* @author 第七人格
* @date 2023/10/13
*/
public class DefaultLogRecordHandler implements ILogRecordService {
public static Logger log = LoggerFactory.getLogger(DefaultLogRecordHandler.class);
@Override
public void record(BizLogInfo bizLogInfo) {
log.info("[触发默认业务日志记录]=====>log={}", JSON.toJSONString(bizLogInfo));
}
}
最终整理代码目录如下:
编写切面
如果说上一章定义组件是给业务日志组件塑造身体的话,那么这一章编写切面就是给业务组件注入灵魂了。
还记得这一张图吗?照着他写就可以了~
分支名称
231013-52javaee.com-BizLogAOP
仓库地址
分支描述
编写业务日志组件相关定义代码。
代码实现
java
/**
* 业务日志切面
* 关注公众号【奔跑的码畜】,一起进步不迷路
*
* @author 第七人格
* @date 2023/10/13
*/
@Aspect
@Component
public class BizLogAspect {
/**
* 系统日志记录器
*/
public static Logger log = LoggerFactory.getLogger(BizLogAspect.class);
private final ILogRecordService logRecordService;
private final BizLogParser bizLogParser;
@Resource
private Environment environment;
// 参数的2个类,已经在启动时通过框架的配置类,交予了Spring管理
public BizLogAspect(ILogRecordService logRecordService, BizLogParser bizLogParser) {
this.logRecordService = logRecordService;
this.bizLogParser = bizLogParser;
}
/**
* 定义切点
* 切入包含BizLog和BizLogs注解的方法
*/
@Pointcut("@annotation(com.run2code.log.annotation.BizLog) || @annotation(com.run2code.log.annotation.BizLogs)")
public void pointCut() {
}
/**
* 环绕通知
*
* @param joinPoint 切点
* @return {@link Object} 返回结果
*/
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取方法签名
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 获取方法
Method method = methodSignature.getMethod();
// 获取方法参数
Object[] args = joinPoint.getArgs();
Object target = joinPoint.getTarget();
Class<?> targetClass = AopUtils.getTargetClass(target);
// 获取所有的日志注解
BizLog[] bizLogs = method.getAnnotationsByType(BizLog.class);
// 将注解参数解析之后放入到实体内,这里是个集合
List<BizLogOps> bizLogOpsList = new ArrayList<>();
for (BizLog bizLog : bizLogs) {
bizLogOpsList.add(parseLogAnnotation(bizLog));
}
// 获取不为空的待解析模板
List<String> expressTemplate = getExpressTemplate(bizLogOpsList);
// 获取前置自定义函数值
Map<String, String> customFunctionExecResultMap = bizLogParser.processBeforeExec(expressTemplate, method, args, targetClass);
Object result = null;
MethodExecuteResult executeResult = new MethodExecuteResult(true);
// 执行目标方法
try {
result = joinPoint.proceed();
} catch (Throwable e) {
executeResult.exception(e);
}
boolean existsNoFailTemp = bizLogOpsList.stream().anyMatch(bizLogOps -> ObjectUtils.isEmpty(bizLogOps.getFail()));
if (!executeResult.isSuccess() && existsNoFailTemp) {
log.warn("[{}] 方法执行失败,@BizLog注解中 失败模板没有配置", method.getName());
} else {
// 解析SpEl表达式
Map<String, String> templateMap = bizLogParser.processAfterExec(expressTemplate, customFunctionExecResultMap, method, args, targetClass, executeResult.getErrMsg(), result);
// 发送日志
sendLog(bizLogOpsList, result, executeResult, templateMap);
}
// 这里要把目标方法的结果抛出来,不然会吞掉异常
if (!executeResult.isSuccess()) {
throw executeResult.getThrowable();
}
return result;
}
/**
* 发送日志
*
* @param bizLogOps
* @param result
* @param executeResult
* @param templateMap
*/
private void sendLog(List<BizLogOps> bizLogOps, Object result, MethodExecuteResult executeResult, Map<String, String> templateMap) {
List<BizLogInfo> bizLogInfos = createBizLogInfo(templateMap, bizLogOps, executeResult);
if (!CollectionUtils.isEmpty(bizLogInfos)) {
bizLogInfos.forEach(bizLogInfo -> {
bizLogInfo.setResult(JSON.toJSONString(result));
// 发送日志(这里其实可以追加参数,判断是否多线程执行,目前是交由子类判断)
logRecordService.record(bizLogInfo);
});
}
}
/**
* 创建操作日志实体
*
* @param templateMap
* @param bizLogOpsList
* @return
*/
private List<BizLogInfo> createBizLogInfo(Map<String, String> templateMap, List<BizLogOps> bizLogOpsList, MethodExecuteResult executeResult) {
return Optional.ofNullable(bizLogOpsList)
.orElse(new ArrayList<>())
.stream()
.filter(bizLogOps -> !"false".equalsIgnoreCase(templateMap.get(bizLogOps.getCondition())))
.map(bizLogOps -> BizLogInfo.builder()
.system(bizLogOps.getSystem() == null || bizLogOps.getSystem().isEmpty() ? environment.getProperty("spring.application.name") : bizLogOps.getSystem())
.module(bizLogOps.getModule())
.type(bizLogOps.getType())
.operator(templateMap.get(bizLogOps.getOperator()))
.bizNo(templateMap.get(bizLogOps.getBizNo()))
.details(templateMap.get(bizLogOps.getDetails()))
.content(executeResult.isSuccess() ? templateMap.get(bizLogOps.getSuccess()) : templateMap.get(bizLogOps.getFail()))
.success(executeResult.isSuccess())
.errorMsg(executeResult.getErrMsg())
.executeTime(executeResult.getExecuteTime())
.operateTime(executeResult.getOperateTime())
.build())
.collect(Collectors.toList());
}
/**
* 将注解转为实体
*
* @param bizLog bizlog注解
* @return {@link BizLogOps} BizLogOps-日志操作对象
*/
private BizLogOps parseLogAnnotation(BizLog bizLog) {
return BizLogOps.builder()
.system(bizLog.system())
.module(bizLog.module())
.type(bizLog.type())
.operator(bizLog.operator())
.bizNo(bizLog.bizNo())
.success(bizLog.success())
.details(bizLog.detail())
.fail(bizLog.fail())
.condition(bizLog.condition())
.build();
}
/**
* 获取不为空的待解析模板
* 从这个List里面我们也可以知道,哪些参数需要符合SpEl表达式
*
* @param bizLogOpsList
* @return
*/
private List<String> getExpressTemplate(List<BizLogOps> bizLogOpsList) {
Set<String> set = new HashSet<>();
for (BizLogOps bizLogOps : bizLogOpsList) {
set.addAll(Arrays.asList(bizLogOps.getBizNo(), bizLogOps.getDetails(),
bizLogOps.getOperator(), bizLogOps.getSuccess(), bizLogOps.getFail(),
bizLogOps.getCondition()));
}
return set.stream().filter(s -> !ObjectUtils.isEmpty(s)).collect(Collectors.toList());
}
}
测试
编写测试的web例子
BizController
java
@RestController
@RequestMapping("/http")
@Slf4j
public class BizController {
@GetMapping("/testBizGet/{param}")
@BizLog(
module = "测试模块",
type = "查询",
operator = "{{#_result}}",
success = "[{{#_result}}]请求get测试接口,参数内容为[{{#param}}],请求后的内容参数为[{getAfterResultDemo{#param}}]"
)
public String testBizGet(@PathVariable(value = "param") String param){
log.info("入参:{}",param);
return "第七人格";
}
@PostMapping("/testBizPost")
@BizLog(
module = "测试模块",
type = "查询",
bizNo = "{{#param.param}}",
operator = "{{#param.operator}}",
success = "用户:[{{#param.operator}}-{{#param.operator}}],请求post测试接口,参数内容为[{{#param.param}}]",
condition = "{{#param.operator == '第七人格'}}"
)
public String testBizPost(@RequestBody BizTestParamDto param){
String result = JSON.toJSONString(param);
log.info("入参:{}", result);
return "这是post的返回结果:"+result;
}
}
BizTestParamDto
java
@Data
public class BizTestParamDto {
private String operator;
private String param;
}
GetAfterResultDemo
这个类实现了ICustomFunctionService表明他是biz-log的自定义函数。executeBefore的false,表明他是后置函数;他的函数名称为getAfterResultDemo;他执行的操作是,返回一个" after result demo"的字符串。
java
@Service
public class GetAfterResultDemo implements ICustomFunctionService {
@Override
public boolean executeBefore() {
return false;
}
@Override
public String functionName() {
return "getAfterResultDemo";
}
@Override
public String apply(Object param) {
return " after result demo";
}
}
http-test-api.http
bash
#### 测试get请求 * 关注公众号【奔跑的码畜】,一起进步不迷路
GET http://localhost:8089/http/testBizGet/测试get请求-公众号【奔跑的码畜】
### 测试post请求,会纪录日志 * 关注公众号【奔跑的码畜】,一起进步不迷路
POST http://localhost:8089/http/testBizPost
Accept: application/json
Content-Type: application/json
{
"operator": "第七人格",
"param": "业务编号"
}
执行Get方法,输出:
java
[触发默认业务日志记录]=====>log=
{
"content": "[第七人格]请求get测试接口,参数内容为[测试get请求-公众号【奔跑的码畜】],请求后的内容参数为[ after result demo]",
"module": "测试模块",
"operateTime": 1697442822021,
"operator": "第七人格",
"result": "\"第七人格\"",
"success": true,
"type": "查询"
}
执行Post方法,输出
java
[触发默认业务日志记录]=====>log=
{
"bizNo": "业务编号",
"content": "用户:[第七人格-第七人格],请求post测试接口,参数内容为[业务编号]",
"module": "测试模块",
"operateTime": 1697443561724,
"operator": "第七人格",
"result": "\"这是post的返回结果:{\\\"operator\\\":\\\"第七人格\\\",\\\"param\\\":\\\"业务编号\\\"}\"",
"success": true,
"type": "查询"
}
通过以上结果简单测试,表明我们的@BizLog已经实现了基本功能。
总结
本文通过需求分析和设计思路,完成了业务日志组件核心代码的编写,并且通过测试小例表明该组件已经可以实现,我们所需要的功能。