前言:
某些业务场景,需要传输xml文件,本案例基于jackson-xml实现了javaBean向XML的转化,同时自定义了某些注解和DIY序列化方法;支持自定义xml-header信息;支持对list数组的特殊处理,如将list序列化为<list><object><prop></prop></object><object><prop></prop></object></list> 或者 <list><prop></prop></list><list><prop></prop></list>
XmlEncoder:本来是用于openfeign序列化的,现在单独提出来当作工具类使用,主要基于注解实现的序列化功能定制
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import feign.codec.EncodeException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Slf4j
public class XmlEncoder {
private static final String XML_ELEMENT_SPLIT_SYMBOL = ";";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
//解析为 xml格式数据
public static String encode(Object object) throws EncodeException {
if (Objects.isNull(object)) {
return null;
}
ObjectMapper objectMapper = new XmlMapper();
String body = null;
try {
body = objectMapper.writeValueAsString(object);
} catch (JsonProcessingException ex) {
throw new RuntimeException(ex);
}
//fix hide list sub table ---- 不带头部信息 ---document.asXML默认会使用utf-8 encoding
body = hideListSubTable(body, object);
//append xml header ---- 自定义头部信息
body = appendHeader(body, object);
return body;
}
private static String hideListSubTable(String body, Object object) {
//判断是否需要对List列表进行特殊处理
if (StringUtils.isNotBlank(body) && object.getClass().isAnnotationPresent(XmlListHideSubTableEnabled.class)) {
XmlListHideSubTableEnabled xmlListHideSubTableEnabled = object.getClass().getAnnotation(XmlListHideSubTableEnabled.class);
if (!xmlListHideSubTableEnabled.value()) {
return body;
}
//读取Body信息文档
try {
//装载Body为Document文档
Document document = DocumentHelper.parseText(body);
//查询该文档下需要处理的XML List数组,递归平铺----key: 从根节点开始到当前对象(节点)链路 v: 当前对象(节点)的多个List属性
Map<String, Set<Field>> nameFieldsMap = findHideListSubTableFields(document.getRootElement().getName(), object);
//处理List数据
for (String elePath : nameFieldsMap.keySet()) {
Set<Field> fields = nameFieldsMap.get(elePath);
for (Field field : fields) {
doHideListSubTable(document,elePath, field);
}
}
//去除头部信息
body = document.getRootElement().asXML();
} catch (DocumentException ex) {
throw new RuntimeException(ex);
}
}
return body;
}
/*
* 源数据结构
* <root>
* <model>
* <products>
* <product>
* <id>1111</id>
* </product>
* <product>
* <id>2222</id>
* </product>
* </products>
* </model>
* <root/>
*
* 目标结构
* <root>
* <model>
* <products>
* <id>1111</id>
* </products>
* <products>
* <id>2222</id>
* </products>
* </model>
* <root/>
*
* PS:暂不支持List中再次嵌套List的数据处理
*/
/**
* @param document 文档
* @param listTablePath list标签的上级地址,如有数组标签 root;model;products --> 此处则为 root;model ---> /表示为从根节点开始
* @param field products
*/
private static void doHideListSubTable(Document document, String listTablePath, Field field) {
Element parent = findElement(document, listTablePath, true);
Element listEle = parent.element(getXmlName(field));
List<List<Element>> listEleContents = findListEleContents(listEle, field);
if (CollectionUtils.isNotEmpty(listEleContents)) {
//删除原有的list
parent.remove(listEle);
//追加新的list
for (List<Element> listEleContent : listEleContents) {
//新增list节点
Element newListEle = parent.addElement(listEle.getName());
if (CollectionUtils.isNotEmpty(listEleContent)) {
for (Element element : listEleContent) {
//为list节点赋值内容
newListEle.add(element);
}
}
}
}
}
private static List<List<Element>> findListEleContents(Element listEle, Field field) {
if (Objects.isNull(listEle) || Objects.isNull(field) || CollectionUtils.isEmpty(listEle.elements())) {
return Collections.emptyList();
}
String name = field.getName();
if (field.isAnnotationPresent(JacksonXmlProperty.class)) {
JacksonXmlProperty annotation = field.getAnnotation(JacksonXmlProperty.class);
if (StringUtils.isNotBlank(annotation.localName())) {
name = annotation.localName();
}
}
List<List<Element>> partions = new ArrayList<>();
for (Element element : listEle.elements()) {
List<Element> properties = new ArrayList<>();
if (StringUtils.equals(name, element.getName()) && CollectionUtils.isNotEmpty(element.elements())) {
for (Element property : element.elements()) {
properties.add((Element) property.clone());
}
}
if (CollectionUtils.isNotEmpty(properties)) {
partions.add(properties);
}
}
return partions;
}
private static Element findElement(Document document, String listTablePath, boolean pathIncludeRoot) {
Element element = document.getRootElement();
if (StringUtils.isBlank(listTablePath)) {
return element;
}
List<String> eleNames = Arrays.stream(listTablePath.split(XML_ELEMENT_SPLIT_SYMBOL)).collect(Collectors.toList());
if (pathIncludeRoot) {
eleNames = eleNames.stream().skip(1).collect(Collectors.toList());
}
for (String eleName : eleNames) {
element = element.element(eleName);
if (Objects.isNull(element)) {
throw new RuntimeException("can not find xml element from path: " + listTablePath + ", error ele name: " + eleName);
}
}
return element;
}
private static Map<String, Set<Field>> findHideListSubTableFields(String name, Object object) {
//root name
if (StringUtils.isBlank(name)) {
name = getXmlName(object);
}
Map<String, Set<Field>> nameFieldsMap = new HashMap<>();
Set<Field> fieldSet = new HashSet<>();
Field[] fields = object.getClass().getDeclaredFields();
BeanWrapper beanWrapper = new BeanWrapperImpl(object);
for (Field field : fields) {
if (!isJavaClass(field.getType())) {
String eleName = getXmlName(field);
nameFieldsMap.putAll(findHideListSubTableFields(name + XML_ELEMENT_SPLIT_SYMBOL + eleName,
beanWrapper.getPropertyValue(field.getName())));
}
if (field.isAnnotationPresent(XmlListHideSubTable.class) && field.getAnnotation(XmlListHideSubTable.class).value()) {
fieldSet.add(field);
}
}
if (CollectionUtils.isNotEmpty(fieldSet)) {
nameFieldsMap.put(name, fieldSet);
}
return nameFieldsMap;
}
private static String getXmlName(Object object) {
if (object.getClass().isAnnotationPresent(JacksonXmlRootElement.class)) {
return object.getClass().getAnnotation(JacksonXmlRootElement.class).localName();
}
String name = object.getClass().toString();
if (object instanceof Field) {
Field field = (Field) object;
if (field.isAnnotationPresent(JacksonXmlElementWrapper.class)) {
JacksonXmlElementWrapper anno = field.getAnnotation(JacksonXmlElementWrapper.class);
if (StringUtils.isNotBlank(anno.localName())) {
name = anno.localName();
}
} else if (field.isAnnotationPresent(JacksonXmlProperty.class)) {
JacksonXmlProperty anno = field.getAnnotation(JacksonXmlProperty.class);
if (StringUtils.isNotBlank(anno.localName())) {
name = anno.localName();
}
}
}
return name;
}
//判断是否为内置对象,false-->则为用户自定义对象
private static boolean isJavaClass(Class<?> clz) {
return clz != null && clz.getClassLoader() == null;
}
//判断是否为数组--集合--map类型
private static boolean isMultiClass(Class<?> clazz) {
return null != clazz && (clazz.isArray()
|| Collection.class.isAssignableFrom(clazz)
|| Map.class.isAssignableFrom(clazz));
}
private static String appendHeader(String body, Object object) {
if (object.getClass().isAnnotationPresent(XmlHeader.class)) {
String xmlHeader = object.getClass().getAnnotation(XmlHeader.class).value();
body = xmlHeader + body;
}
return body;
}
public static String parseJsonPayload(EmailRequest emailRequest) {
String xmlStr = encode(emailRequest);
if (StringUtils.isBlank(xmlStr)) {
return null;
}
// 匹配规则
//提取xml中的标签
String xml = extractXml(xmlStr, emailRequest.getEmailBodyNodeNames());
//转义xml 为 str给到notification
return StringEscapeUtils.escapeXml(xml);
}
private static String extractXml(String xmlStr, List<String> xmlNames) {
if (CollectionUtils.isEmpty(xmlNames)) {
return xmlStr;
}
StringBuilder sb = new StringBuilder();
for (Object xmlName : xmlNames) {
String startNode = "<" + xmlName;
String endNode = "</" + xmlName + ">";
String reg = startNode + "(.*?)" + endNode;
Pattern pattern = Pattern.compile(reg);
Matcher matcher = pattern.matcher(xmlStr);
while (matcher.find()) {
sb.append(matcher.group());
}
}
return sb.toString();
}
}
PS:以上代码使用encode方法就好,其他的方法是基于encode之后的数据做了一些xml标签的提取工作
相关注解:
//JacksonXmlElementWrapper: jackson的标记list的注解,encode的时候会获取该注解信息
//JacksonXmlProperty:jackson的标记对象标签或者值标签的注解
//JacksonXmlRootElement: jackson的标记根标签的注解
//XmlHeader: 自定义注解,标记在最外层的javaBean上,和xml最外层跟标签一个层级, encode的时候获取该注解信息---> xml头信息
//XmlListHideSubTable: 自定义注解, 标记在List<Model>的javabean属性上,用于判定是否要对list进行特殊处理
//XmlListHideSubTableEnabled: 自定义注解, 标记在最外层的javaBean上,和xml最外层跟标签一个层级
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface XmlHeader {
String value() default "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
}
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface XmlListHideSubTable {
boolean value() default true;
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface XmlListHideSubTableEnabled {
boolean value() default true;
}
JavaBean写法
@XmlListHideSubTableEnabled
@XmlHeader
@JsonIgnoreProperties(ignoreUnknown = true)
@JacksonXmlRootElement(localName = "Root")
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class EmailContentLeaTempRequest extends EmailRequest {
@Builder.Default
@JacksonXmlProperty(localName = "xmlns", isAttribute = true)
private String ns = "ns1";
@Builder.Default
@JacksonXmlProperty(localName = "xmlns:xsi", isAttribute = true)
private String otherNs = "ns2";
@JacksonXmlProperty(localName = "Model")
private Model model;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class Model {
@JacksonXmlProperty(localName = "Detail")
private Detail detail;
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class Detail {
@Builder.Default
@JacksonXmlProperty(localName = "xmlns", isAttribute = true)
private String namespace = "http://xxx.detail/1.0";
@XmlListHideSubTable
@JacksonXmlProperty(localName = "Product")
@JacksonXmlElementWrapper(localName = "Products")
private List<Product> products;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class Product {
@JacksonXmlProperty(localName = "Name")
private String productName;
}
}
}
}