从0到1带你实现一个业务日志组件(二)核心代码篇

✨这里是第七人格的博客✨小七,欢迎您的到来~✨

🍅系列专栏:【架构思想】🍅

✈️本篇内容: 从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

仓库地址

gitee.com/diqirenge/b...

分支描述

编写业务日志组件相关定义代码。

代码实现

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

仓库地址

gitee.com/diqirenge/b...

分支描述

编写业务日志组件相关定义代码。

代码实现

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已经实现了基本功能。

总结

本文通过需求分析和设计思路,完成了业务日志组件核心代码的编写,并且通过测试小例表明该组件已经可以实现,我们所需要的功能。

附录

从0到1带你实现一个业务日志组件(一)需求分析篇

相关推荐
weifont2 小时前
聊一聊Electron中Chromium多进程架构
javascript·架构·electron
热河暖男2 小时前
【实战解决方案】Spring Boot+Redisson构建高并发Excel导出服务,彻底解决系统阻塞难题
spring boot·后端·excel
国际云,接待5 小时前
云服务器的运用自如
服务器·架构·云计算·腾讯云·量子计算
noravinsc6 小时前
redis是内存级缓存吗
后端·python·django
noravinsc8 小时前
django中用 InforSuite RDS 替代memcache
后端·python·django
好吃的肘子8 小时前
Elasticsearch架构原理
开发语言·算法·elasticsearch·架构·jenkins
喝醉的小喵8 小时前
【mysql】并发 Insert 的死锁问题 第二弹
数据库·后端·mysql·死锁
编程星空8 小时前
架构与UML4+1视图
架构
kaixin_learn_qt_ing8 小时前
Golang
开发语言·后端·golang
炒空心菜菜9 小时前
MapReduce 实现 WordCount
java·开发语言·ide·后端·spark·eclipse·mapreduce