美文网首页
日志框架 - 基于spring-boot - 实现3 - 关键字

日志框架 - 基于spring-boot - 实现3 - 关键字

作者: 船前几度寄芳心 | 来源:发表于2018-04-28 00:27 被阅读0次

    日志框架系列讲解文章
    日志框架 - 基于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();
        }
    
    }
    
    

    相关文章

      网友评论

          本文标题:日志框架 - 基于spring-boot - 实现3 - 关键字

          本文链接:https://www.haomeiwen.com/subject/vdevlftx.html