从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带你实现一个业务日志组件(一)需求分析篇

相关推荐
Ai 编码助手1 小时前
Go语言 实现将中文转化为拼音
开发语言·后端·golang
hummhumm1 小时前
第 12 章 - Go语言 方法
java·开发语言·javascript·后端·python·sql·golang
杜杜的man1 小时前
【go从零单排】Directories、Temporary Files and Directories目录和临时目录、临时文件
开发语言·后端·golang
wywcool1 小时前
JVM学习之路(5)垃圾回收
java·jvm·后端·学习
喜欢打篮球的普通人2 小时前
rust高级特征
开发语言·后端·rust
代码小鑫3 小时前
A032-基于Spring Boot的健康医院门诊在线挂号系统
java·开发语言·spring boot·后端·spring·毕业设计
豌豆花下猫3 小时前
REST API 已经 25 岁了:它是如何形成的,将来可能会怎样?
后端·python·ai
wclass-zhengge3 小时前
架构篇(04理解架构的演进)
架构
喔喔咿哈哈3 小时前
【手撕 Spring】 -- Bean 的创建以及获取
java·后端·spring·面试·开源·github
夏微凉.4 小时前
【JavaEE进阶】Spring AOP 原理
java·spring boot·后端·spring·java-ee·maven