日志框架系列講解文章
日志框架 - 基于spring-boot - 使用入門
日志框架 - 基于spring-boot - 設(shè)計(jì)
日志框架 - 基于spring-boot - 實(shí)現(xiàn)1 - 配置文件
日志框架 - 基于spring-boot - 實(shí)現(xiàn)2 - 消息定義及消息日志打印
日志框架 - 基于spring-boot - 實(shí)現(xiàn)3 - 關(guān)鍵字與三種消息解析器
日志框架 - 基于spring-boot - 實(shí)現(xiàn)4 - HTTP請求攔截
日志框架 - 基于spring-boot - 實(shí)現(xiàn)5 - 線程切換
日志框架 - 基于spring-boot - 實(shí)現(xiàn)6 - 自動(dòng)裝配
上一篇我們講了日志框架實(shí)現(xiàn)的第二部分:消息定義及消息日志打印
本篇我們主講框架實(shí)現(xiàn)的第三部分:如何自動(dòng)解析消息
在設(shè)計(jì)中是這樣描述的
根據(jù)關(guān)鍵字(Keyword),使用解析器(MessageResolver)提取消息(Message)中的值。關(guān)鍵字(Keyword)及其值保存于MDC之中。
下面是自動(dòng)消息解析器的實(shí)現(xiàn)
關(guān)鍵字(Keyword)定義
/**
* 關(guān)鍵字
*/
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允許進(jìn)行單詞的模糊匹配,例如Req-Sys可以指定模糊查找消息中可能包含的Req-Sys, Req_Sys, ReqSys, reqSys, req-sys, req_sys, reqsys, REQ-SYS, REQ_SYS, REQSYS等10種情況的內(nèi)容。
其中,之所以使用RelaxedNames,是為了實(shí)現(xiàn)關(guān)鍵字(Keyword)的模糊匹配(Relaxed binding)。
解析器(MessageResolver)定義
/**
* 從Message中根據(jù)Keywords解析得到關(guān)鍵信息
*/
public interface MessageResolver {
public Map<String, String> resolve(
Message message, MessageResolverChain chain);
}
根據(jù)使用入門文檔的說明,解析器必須支持Json、XML、KeyValue三種格式的消息。因此解析器會(huì)有三種實(shí)現(xiàn)。
由于無法提前確定請求消息會(huì)以何種格式發(fā)送,因此,采用責(zé)任鏈模式,將不同的解析器拼裝為責(zé)任鏈。下面是MessageResolverChain 的定義。
/**
* MessageResolver責(zé)任鏈
*/
public interface MessageResolverChain {
public Map<String, String> dispose(Message message);
}
XML解析器的實(shí)現(xiàn)
解析XML并從中查找關(guān)鍵字對應(yīng)的值,最簡單的辦法就是構(gòu)造XPath并在消息中查找對應(yīng)的值。本框架解析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 = "http://" + 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);
}
}
在查找關(guān)鍵字的值時(shí)使用了Java8新增的Stream編程,會(huì)使代碼看起來更簡潔一此,如果對Stream特性不熟悉的話,可以不使用。
Json解析器實(shí)現(xiàn)
類似于XML消息的解析,對Json消息使用JsonPath查找關(guān)鍵字最方便可行。本框架用了Github上一個(gè)開源的 JsonSurfer 組件,其與各種Json處理框架的結(jié)合都較好,具體代碼如下。
/**
* 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中同時(shí)查詢多個(gè)jsonPath的匹配值
* <p>
* 參考@{@link JsonSurfer#collectOne(String, Class, JsonPath...)}進(jìn)行自定義實(shí)現(xiàn)
*/
@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函數(shù)提供了JsonSurfer的自定義實(shí)現(xiàn)。
KeyValue解析器
所謂的KeyValue字符串,其格式相當(dāng)于HTTP請求中的QueryString,但不局限于此,也可以認(rèn)為是form表單提交的字符串請求。
KeyValue解析器的代碼實(shí)現(xiàn)如下:
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;
}
}
代碼中使用了一個(gè)KeyValueUtil的工具類,其代碼附在文章最后,有需要者自取。
責(zé)任鏈實(shí)現(xiàn)
前文定義了責(zé)任鏈的接口?,F(xiàn)需要將各種解析器拼成責(zé)任鏈,其代碼如下。
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();
}
}
至此,解析消息日志部分功能已經(jīng)實(shí)現(xiàn)。
附:KeyValueUtil工具類
提供了如下功能:
- 判斷字符串是否為KeyValue值字符串
- 實(shí)現(xiàn)KeyValue字符串與Map間的相互轉(zhuǎn)換
/**
* {@code KeyValutUtil}主要用于處理類似"key=value&key=value"的字符串
*
* <p>
* {@code KeyValueUtil}主要提供將key-value字符串轉(zhuǎn)換成{@link Map}的功能{@link #keyValueStringToMap(String)
* keyValueStringToMap} 和將{@link Map}轉(zhuǎn)換成key-value的功能{@link #mapToString(Map)
* mapToString}
* </p>
*
* <p>
* <Strong>設(shè)計(jì)思路:</Strong>Map轉(zhuǎn)換成key-value字符串時(shí),一次取出每個(gè)實(shí)體(Entity),將key與value用“=”連接,每個(gè)實(shí)體間用“&”連接,
* 組成如:key1=value1&key2=value2的字符串。 ?
* key-value轉(zhuǎn)字符串的時(shí)候需要區(qū)分含有value子串的形式:key1=value1&key2={key21=value21&key22=value22},
* 設(shè)計(jì)思路對key-value字符串逐個(gè)字符進(jìn)行處理,利用狀態(tài)機(jī)判斷當(dāng)前狀態(tài)為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();
}
/**
* 識(shí)別字符串狀態(tài)機(jī)轉(zhuǎn)換:<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形式的字符串轉(zhuǎn)轉(zhuǎn)換為一個(gè)排序的map<br>
* 此方法忽略字符串前后可能存在的"{}"字符<br>
* 樣例字符串:{accessType=0&bizType=000201¤cyCode=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);
// 狀態(tài)轉(zhuǎn)換
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中的數(shù)據(jù)轉(zhuǎn)換成按照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();
}
}