日志框架 - 基于spring-boot - 实现3 - 关键字与三种消息解析器

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
日志服务 SLS,月写入数据量 50GB 1个月
简介: 日志框架系列讲解文章日志框架 - 基于spring-boot - 使用入门日志框架 - 基于spring-boot - 设计日志框架 - 基于spring-boot - 实现1 - 配置文件日志框架 - 基于spring-boot - 实现2 - 消...

日志框架系列讲解文章
日志框架 - 基于spring-boot - 使用入门
日志框架 - 基于spring-boot - 设计
日志框架 - 基于spring-boot - 实现1 - 配置文件
日志框架 - 基于spring-boot - 实现2 - 消息定义及消息日志打印
日志框架 - 基于spring-boot - 实现3 - 关键字与三种消息解析器
日志框架 - 基于spring-boot - 实现4 - HTTP请求拦截
日志框架 - 基于spring-boot - 实现5 - 线程切换
日志框架 - 基于spring-boot - 实现6 - 自动装配

上一篇我们讲了日志框架实现的第二部分:消息定义及消息日志打印
本篇我们主讲框架实现的第三部分:如何自动解析消息

设计中是这样描述的

根据关键字(Keyword),使用解析器(MessageResolver)提取消息(Message)中的值。关键字(Keyword)及其值保存于MDC之中。

下面是自动消息解析器的实现

关键字(Keyword)定义

/**
 * 关键字
 */
public class Keyword {
    
    private String key;
    
    private RelaxedNames relaxedNames;
    
    public Keyword(String key) {
        this.key = key;
        this.relaxedNames = new RelaxedNames(key);
    }
    
    public String getKey() {
        return key;
    }
    
    public RelaxedNames getRelaxedNames() {
        return relaxedNames;
    }
}

使用入门一文中提到,Relaxed binding允许进行单词的模糊匹配,例如Req-Sys可以指定模糊查找消息中可能包含的Req-Sys, Req_Sys, ReqSys, reqSys, req-sys, req_sys, reqsys, REQ-SYS, REQ_SYS, REQSYS等10种情况的内容。

其中,之所以使用RelaxedNames,是为了实现关键字(Keyword)的模糊匹配(Relaxed binding)。

解析器(MessageResolver)定义

/**
 * 从Message中根据Keywords解析得到关键信息
 */
public interface MessageResolver {
    
    public Map<String, String> resolve(
            Message message, MessageResolverChain chain);
    
}

根据使用入门文档的说明,解析器必须支持Json、XML、KeyValue三种格式的消息。因此解析器会有三种实现。
由于无法提前确定请求消息会以何种格式发送,因此,采用责任链模式,将不同的解析器拼装为责任链。下面是MessageResolverChain 的定义。

/**
 * MessageResolver责任链
 */
public interface MessageResolverChain {
    
    public Map<String, String> dispose(Message message);
}

XML解析器的实现

解析XML并从中查找关键字对应的值,最简单的办法就是构造XPath并在消息中查找对应的值。本框架解析XML使用Dom4j组件,代码如下。

/**
 * xml消息解析器,从消息中获取keyword值
 */
public class XmlMessageResolver implements MessageResolver {
    
    private Map<String, List<String>> xmlPathCache = new ConcurrentHashMap<>();
    
    public XmlMessageResolver(List<Keyword> keywordList) {
        keywordList.stream()
                   .forEach(keyword -> {
                       List<String> xmlPathList = new ArrayList<>();
                       for (String s : keyword.getRelaxedNames()) {
                           String xmlPath = "//" + s;
                           xmlPathList.add(xmlPath);
                       }
                       xmlPathCache.putIfAbsent(keyword.getKey(), xmlPathList);
                   });
    }
    
    @Override
    public Map<String, String> resolve(
            Message message, MessageResolverChain chain) {
        MessageType messageType = message.getType();
        Document document;
        if (MessageType.XML.equals(messageType) || MessageType.TEXT.equals
                (messageType)) {
            try {
                document = DocumentHelper.parseText((String) message
                        .getContent());
            } catch (DocumentException e) {
                if (MessageType.XML.equals(messageType)) {
                    return Collections.emptyMap();
                } else {
                    return chain.dispose(message);
                }
            }
        } else {
            return chain.dispose(message);
        }
        return doResolve(document, xmlPathCache);
    }
    
    private Map<String, String> doResolve(
            Document document, Map<String, List<String>> xmlPathCache) {
        HashMap<String, String> resultMap = new HashMap<>();
        xmlPathCache.forEach((key, paths) -> {
            String value = queryStringInDocument(document, paths);
            if (!StringUtils.isEmpty(value)) {
                resultMap.putIfAbsent(key, value);
            }
        });
        return CollectionUtils.isEmpty(resultMap) ? Collections.emptyMap() :
               resultMap;
    }
    
    public String queryStringInDocument(
            Document document, List<String> xmlPathList) {
        return xmlPathList.stream()
                          .map(document::selectSingleNode)
                          .filter(node -> !ObjectUtils.isEmpty(node))
                          .map(node -> node.getText())
                          .findAny().orElse(null);
    }
}

在查找关键字的值时使用了Java8新增的Stream编程,会使代码看起来更简洁一此,如果对Stream特性不熟悉的话,可以不使用。

Json解析器实现

类似于XML消息的解析,对Json消息使用JsonPath查找关键字最方便可行。本框架用了Github上一个开源的 JsonSurfer 组件,其与各种Json处理框架的结合都较好,具体代码如下。

/**
 * json消息解析器,从消息中获取keyword值
 */
public class JsonMessageResolver implements MessageResolver {
    
    private Map<String, List<JsonPath>> jsonPathCache = new
            ConcurrentSkipListMap<>();
    
    private ObjectMapper objectMapper = new ObjectMapper();
    
    public JsonMessageResolver(List<Keyword> keywordList) {
        keywordList.stream()
                   .forEach(keyword -> {
                       List<JsonPath> jsonPathList = new ArrayList<>();
                       for (String s : keyword.getRelaxedNames()) {
                           String jsonPathStr = "$.." + s;
                           JsonPath jsonPath = JsonPathCompiler.compile
                                   (jsonPathStr);
                           jsonPathList.add(jsonPath);
                       }
                       jsonPathCache
                               .putIfAbsent(keyword.getKey(), jsonPathList);
                   });
    }
    
    @Override
    public Map<String, String> resolve(
            Message message, MessageResolverChain chain) {
        MessageType messageType = message.getType();
        
        if (MessageType.JSON.equals(messageType)) {
            return doResolve((String) message.getContent(), jsonPathCache);
        } else if (MessageType.TEXT.equals(messageType)) {
            if (canResolveAsJson((String) message.getContent())) {
                return doResolve((String) message.getContent(), jsonPathCache);
            } else {
                return chain.dispose(message);
            }
        }
        
        return chain.dispose(message);
    }
    
    public Map<String, String> doResolve(
            String content, Map<String, List<JsonPath>> jsonPathCache) {
        Map<String, String> resultMap = new HashMap<>();
        for (Map.Entry<String, List<JsonPath>> entry : jsonPathCache
                .entrySet()) {
            String value = queryStringInJsonMessage(content, entry.getValue());
            if (!StringUtils.isEmpty(value)) {
                resultMap.putIfAbsent(entry.getKey(), value);
            }
        }
        return CollectionUtils.isEmpty(resultMap) ? Collections.emptyMap() :
               resultMap;
    }
    
    public boolean canResolveAsJson(String maybeJson) {
        try {
            objectMapper.readTree(maybeJson);
        } catch (IOException e) {
            return false;
        }
        return true;
    }
    
    /**
     * 从json中同时查询多个jsonPath的匹配值
     * <p>
     * 参考@{@link JsonSurfer#collectOne(String, Class, JsonPath...)}进行自定义实现
     */
    @SuppressWarnings("unchecked")
    public String queryStringInJsonMessage(
            String json, List<JsonPath> pathList) {
        JsonSurfer surfer = JsonSurferJackson.INSTANCE;
        CollectOneListener listener = new CollectOneListener(true);
        SurfingConfiguration.Builder builder = surfer.configBuilder()
                                                     .skipOverlappedPath();
        pathList.stream()
                .forEach(jsonPath -> builder.bind(jsonPath, listener));
        surfer.surf(json, builder.build());
        Object value = listener.getValue();
        JsonProvider provider = JacksonProvider.INSTANCE;
        if (value == null) {
            return null;
        } else {
            return (String)  provider.cast(value, String.class);
        }
    }
}

由于JsonSurfer组件缺少我需要的API, 因此queryStringInJsonMessage函数提供了JsonSurfer的自定义实现。

KeyValue解析器

所谓的KeyValue字符串,其格式相当于HTTP请求中的QueryString,但不局限于此,也可以认为是form表单提交的字符串请求。

KeyValue解析器的代码实现如下:

public class KeyValueMessageResolver implements MessageResolver {
    
    private List<Keyword> keywordList;
    
    public KeyValueMessageResolver(List<Keyword> keywordList) {
        this.keywordList = keywordList;
    }
    
    @Override
    @SuppressWarnings("unchecked")
    public Map<String, String> resolve(
            Message message, MessageResolverChain chain) {
        MessageType messageType = message.getType();
        final Map<String, String> contentMap;
        if (MessageType.KEY_VALUE.equals(messageType)) {
            Object messageContent = message.getContent();
            if (messageContent instanceof Map) {
                contentMap = (Map<String, String>) messageContent;
            } else {
                contentMap = KeyValueUtil
                        .keyValueStringToMap(messageContent.toString());
            }
        } else if (MessageType.TEXT.equals(messageType)) {
            String content = (String) message.getContent();
            if (KeyValueUtil.isKeyValueString(content)) {
                contentMap = KeyValueUtil.keyValueStringToMap(content);
            } else {
                contentMap = Collections.EMPTY_MAP;
            }
        } else {
            contentMap = null;
        }
        
        if (CollectionUtils.isEmpty(contentMap)) {
            return chain.dispose(message);
        }
        
        Map<String, String> resultMap = new HashMap<>();
        keywordList.forEach(keyword -> {
            for (String s : keyword.getRelaxedNames()) {
                if (contentMap.containsKey(s)) {
                    resultMap.putIfAbsent(keyword.getKey(), contentMap.get(s));
                    return;
                }
            }
        });
        return CollectionUtils.isEmpty(resultMap) ? Collections.emptyMap()
                                                  : resultMap;
    }
}

代码中使用了一个KeyValueUtil的工具类,其代码附在文章最后,有需要者自取。

责任链实现

前文定义了责任链的接口。现需要将各种解析器拼成责任链,其代码如下。

public class MessageResolverChainImpl implements MessageResolverChain {
    
    private List<MessageResolver> chain;
    
    public int currentPosition = 0;
    
    public MessageResolverChainImpl(List<MessageResolver> messageResolvers) {
        chain = messageResolvers;
    }
    
    @Override
    public Map<String, String> dispose(Message message) {
        int pos = currentPosition;
        currentPosition++;
        if (pos < chain.size()) {
            MessageResolver resolver = chain.get(pos);
            return resolver.resolve(message, this);
        }
        return Collections.emptyMap();
    }
}

至此,解析消息日志部分功能已经实现。

附:KeyValueUtil工具类

提供了如下功能:

  1. 判断字符串是否为KeyValue值字符串
  2. 实现KeyValue字符串与Map间的相互转换
/**
 * {@code KeyValutUtil}主要用于处理类似"key=value&key=value"的字符串
 * 
 * <p>
 * {@code KeyValueUtil}主要提供将key-value字符串转换成{@link Map}的功能{@link #keyValueStringToMap(String)
 * keyValueStringToMap} 和将{@link Map}转换成key-value的功能{@link #mapToString(Map)
 * mapToString}
 * </p>
 * 
 * <p>
 * <Strong>设计思路:</Strong>Map转换成key-value字符串时,一次取出每个实体(Entity),将key与value用“=”连接,每个实体间用“&”连接,
 * 组成如:key1=value1&key2=value2的字符串。 ​
 * key-value转字符串的时候需要区分含有value子串的形式:key1=value1&key2={key21=value21&key22=value22},
 * 设计思路对key-value字符串逐个字符进行处理,利用状态机判断当前状态为key还是value。
 * </p>
 * 
 * @author fonoisrev(Java大坑)
 * @since xpay-common 0.0.1
 */
public class KeyValueUtil {

    private static final Pattern PATTERN = Pattern.compile("^(\\S+?=(.|\\n)*&)+\\S+=(.|\\n)*$");

    public static boolean isKeyValueString(String str) {
        return  PATTERN.matcher(str).matches();
    }

    /**
     * 识别字符串状态机转换:<br/>
     * STATUS_KEY --[=]--> STATUS_SIMPLEVALUE <br/>
     * STATUS_SIMPLEVALUE --[&]--> STATUS_KEY <br/>
     * STATUS_SIMPLEVALUE --[{]--> STATUS_COMPLEXVALUE <br/>
     * STATUS_COMPLEXVALUE --[}]--> STATUS_SIMPLEVALUE <br/>
     * STATUS_COMPLEXVALUE --[=]--> STATUS_COMPLEXVALUE <br/>
     * STATUS_COMPLEXVALUE --[&]--> STATUS_COMPLEXVALUE
     */
    private static int STATUS_KEY = 1;
    private static int STATUS_SIMPLEVALUE = 2;
    private static int STATUS_COMPLEXVALUE = 4;

    /**
     * 将key1=value1&key2=value2形式的字符串转转换为一个排序的map<br>
     * 此方法忽略字符串前后可能存在的"{}"字符<br>
     * 样例字符串:{accessType=0&bizType=000201&currencyCode=156&encoding=UTF-8&
     * issuerIdentifyMode=0&merId=777290058110048&orderId=20160317150838&
     * origRespCode=00&origRespMsg=成功[0000000]&payCardType=01&queryId=
     * 201603171508382661928&reqReserved={a=aaa&b=bbb&c=ccc}&respCode=00&respMsg
     * =成功[0000000]&settleAmt=10000&settleCurrencyCode=156&settleDate=0317&
     * signMethod=01&traceNo=266192&traceTime=0317150838&txnAmt=10000&txnSubType
     * =01&txnTime=20160317150838&txnType=01&version=5.0.0&certId=68759585097&
     * signature=EpwPj3OIQgCmr9FfdJIs/dYG+
     * CVnYOm9JwoC4dyaEjtgdSCzRNyWGOCbToHs5sAbVfjqSUi/o3ctqAaOJEyMEIdbZt+
     * hVQcWDmUovQs6ruQM5VN0tNdRsR+QANo1f1LYNs6q89UhGo+OIpFMMB+jdb2Sg54XFH++
     * ywqXoL0WCWWwtzeu2Haqq8LM5P1j4p0FqrAYuEI58zy40g/T4S+
     * eTBrZZx8MGGNcAQDMsk2IEsuEa1IVzzAIW5ZvsG2Ypf74DJpPEGMgzInKUyC1+BblJ/
     * oYGIRQyeYan0jd/7nZuvTB5nmoTdSgSsPZlnuSsPvHP+BK48MyrvsWRJXH983VFw==}
     * 
     * @param keyValueString
     * @return
     */
    public static SortedMap<String, String> keyValueStringToMap(String keyValueString) {
        if (!StringUtils.hasText(keyValueString)) {
            return null;
        }

        StringBuilder sb = new StringBuilder(keyValueString.trim());
        if (sb.charAt(0) == '{') {
            sb.deleteCharAt(0);
        }
        if (sb.charAt(sb.length() - 1) == '}') {
            sb.deleteCharAt(sb.length() - 1);
        }

        SortedMap<String, String> map = new TreeMap<String, String>();

        int currentIndex = 0;
        String key = null;
        String value = null;

        int status = STATUS_KEY;

        for (int i = 0; i < sb.length(); ++i) {
            char c = sb.charAt(i);
            // 状态转换
            if (status == STATUS_KEY && c == '=') {
                status = STATUS_SIMPLEVALUE;
                key = sb.substring(currentIndex, i);
                currentIndex = i + 1;
            } else if (status == STATUS_SIMPLEVALUE && c == '&') {
                status = STATUS_KEY;
                value = sb.substring(currentIndex, i);
                map.put(key, value);
                currentIndex = i + 1;
            } else if (status == STATUS_SIMPLEVALUE && c == '{') {
                status = STATUS_COMPLEXVALUE;
            } else if (status == STATUS_COMPLEXVALUE && c == '}') {
                status = STATUS_SIMPLEVALUE;
            }
        }
        value = sb.substring(currentIndex, sb.length());
        map.put(key, value);

        return map;
    }

    /**
     * 将Map中的数据转换成按照Key的ascii码排序后的key1=value1&key2=value2的形式
     * 
     * @param map
     * @return
     */
    public static String mapToString(Map<String, String> map) {
        SortedMap<String, String> sortedMap = new TreeMap<String, String>(map);

        StringBuilder sb = new StringBuilder();

        for (Map.Entry<String, String> entry : sortedMap.entrySet()) {
            if (!StringUtils.hasText(entry.getValue())) {
                continue;
            }
            sb.append(entry.getKey()).append('=').append(entry.getValue()).append('&');
        }
        sb.deleteCharAt(sb.length() - 1);

        return sb.length() == 0 ? "" : sb.toString();
    }

}

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
10天前
|
XML 安全 Java
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
110 30
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
|
17天前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
|
30天前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
40 4
|
27天前
|
Java API 数据库
Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐
本文通过在线图书管理系统案例,详细介绍如何使用Spring Boot构建RESTful API。从项目基础环境搭建、实体类与数据访问层定义,到业务逻辑实现和控制器编写,逐步展示了Spring Boot的简洁配置和强大功能。最后,通过Postman测试API,并介绍了如何添加安全性和异常处理,确保API的稳定性和安全性。
34 0
|
20天前
|
搜索推荐 Java Spring
Spring Filter深度解析
【10月更文挑战第21天】Spring Filter 是 Spring 框架中非常重要的一部分,它为请求处理提供了灵活的控制和扩展机制。通过合理配置和使用 Filter,可以实现各种个性化的功能,提升应用的安全性、可靠性和性能。还可以结合具体的代码示例和实际应用案例,进一步深入探讨 Spring Filter 的具体应用和优化技巧,使对它的理解更加全面和深入。
|
1天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
9 2
|
21天前
|
前端开发 Java 数据库连接
Spring 框架:Java 开发者的春天
Spring 框架是一个功能强大的开源框架,主要用于简化 Java 企业级应用的开发,由被称为“Spring 之父”的 Rod Johnson 于 2002 年提出并创立,并由Pivotal团队维护。
41 1
Spring 框架:Java 开发者的春天
|
14天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,帮助开发者提高开发效率和应用的可维护性。
32 2
|
13天前
|
消息中间件 NoSQL Java
springboot整合常用中间件框架案例
该项目是Spring Boot集成整合案例,涵盖多种中间件的使用示例,每个案例项目使用最小依赖,便于直接应用到自己的项目中。包括MyBatis、Redis、MongoDB、MQ、ES等的整合示例。
61 1
|
21天前
|
Java 数据库连接 开发者
Spring 框架:Java 开发者的春天
【10月更文挑战第27天】Spring 框架由 Rod Johnson 在 2002 年创建,旨在解决 Java 企业级开发中的复杂性问题。它通过控制反转(IOC)和面向切面的编程(AOP)等核心机制,提供了轻量级的容器和丰富的功能,支持 Web 开发、数据访问等领域,显著提高了开发效率和应用的可维护性。Spring 拥有强大的社区支持和丰富的生态系统,是 Java 开发不可或缺的工具。

推荐镜像

更多