代码实现

话不多说,直接上工具类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class XmlUtil {

/**
* @param xml xml字符串
* @param obj 要转换的实体类型
* @return : java.util.Map<java.lang.String,java.lang.String>
* @Name : xmlToObject
* @description : xml字符串内容并转化为对应实体
* @createTime : 2023/7/14 14:04
*/
static public <T> T xmlToObject(String xml, Class<T> obj) throws JAXBException {
StringReader stringReader = new StringReader(xml);
JAXBContext jaxbContext = JAXBContext.newInstance(obj);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
Object unmarshal = unmarshaller.unmarshal(stringReader);
return obj.cast(unmarshal);
}

/**
* @Name : objToXml
* @description : 将Java对象转换成为XML文本
* @createTime : 2023/7/20 10:47
* @param obj 要转换的实体类型
* @return : java.lang.String
*/
static public String objToXml(Object obj) throws JAXBException, IOException {
JAXBContext jaxbContext = JAXBContext.newInstance(obj.getClass());
Marshaller marshaller = jaxbContext.createMarshaller();
StringWriter sw = new StringWriter();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
marshaller.marshal(obj,sw);
String res = sw.toString();
sw.close();
// 尖括号修正
res = res.replace("&lt;","<");
res = res.replace("&gt;",">");
return res;
}

}

再上实体类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Data
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "xml")
public abstract class WXMessage implements Serializable {

@XmlElement(name = "ToUserName",required = true)
private String toUserName;

@XmlElement(name = "FromUserName",required = true)
private String fromUserName;

@XmlElement(name = "MsgType",required = true)
private String msgType;

@XmlElement(name = "CreateTime")
private String createTime;

@XmlElement(name = "MsgId")
private String msgId;

@XmlElement(name = "MsgDataId")
private String msgDataId;

@XmlElement(name = "Idx")
private String idx;

}

值得注意的是JAXB的那几个注解:

  • @XmlRootElement(name = "xml") 映射的根节点标签名,未使用则默认根节点是该实体的全类名。
  • @XmlAccessorType(XmlAccessType.FIELD) 映射属性形式,可映射字段(FIELD)、Setter/Getter方法(PROPERTY)等。
  • @XmlElement 属性值,映射着XML的具体标签名。

上面三个注解算是最基本的注解了。现在如过有个需求,需要对转换的XML字段进行具体的微调操作,比如我要给一些字段变成<![CDATA[原字段]]>的形式,那么怎么实现?

答曰,配合@XmlJavaTypeAdapter注解实现自定义转换器。

转换器实现:

1
2
3
4
5
6
7
8
9
10
11
12
public class XmlCDataAdapter extends XmlAdapter<String,String> {

@Override
public String unmarshal(String v) throws Exception {
return v;
}

@Override
public String marshal(String v) throws Exception {
return "<![CDATA[" + v + "]]>";
}
}

其实也就是重写两个转义的方法,对具体数据就可以实现微调了。

实体代码示例:

1
2
3
4
5
6
7
8
9
10
@Data
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "xml")
public abstract class WXMessage implements Serializable {

@XmlElement(name = "ToUserName",required = true)
@XmlJavaTypeAdapter(XmlCDataAdapter.class) // 对应要微调的字段添加该注解就行了
private String toUserName;

}

想法很不错,实际用起来存在一个很大的问题:尖括号<>在解析的时候会被JAXB自动转义成&lt/&gt!

这下咋整?博主当时研究了半天,网上的方法都是什么更换转码器啊,还要用代理的,怎么一个这样的小问题搞这么复杂啊。

其实你转义过去处理结果字符串的时候再替换回来不就行了?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @Name : objToXml
* @description : 将Java对象转换成为XML文本
* @createTime : 2023/7/20 10:47
* @param obj 要转换的实体类型
* @return : java.lang.String
*/
static public String objToXml(Object obj) throws JAXBException, IOException {
JAXBContext jaxbContext = JAXBContext.newInstance(obj.getClass());
Marshaller marshaller = jaxbContext.createMarshaller();
StringWriter sw = new StringWriter();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
marshaller.marshal(obj,sw);
String res = sw.toString();
sw.close();
// 尖括号修正
res = res.replace("&lt;","<");
res = res.replace("&gt;",">");
return res;
}

工具类里给结果的尖括号修正一下就好了。。。

深层次问题

代码实现简单,就是用了JAXB的转换工具。博主遇到的问题主要是在XML映射的对象上。

现在我有一个父类,若干继承了该父类的子类,父类的字段是子类共有的。

父类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Data
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "xml")
public class WXMessage implements Serializable {

@XmlElement(name = "ToUserName",required = true)
private String toUserName;

@XmlElement(name = "FromUserName",required = true)
private String fromUserName;

@XmlElement(name = "MsgType",required = true)
private String msgType;

@XmlElement(name = "CreateTime")
private String createTime;

@XmlElement(name = "MsgId")
private String msgId;

@XmlElement(name = "MsgDataId")
private String msgDataId;

@XmlElement(name = "Idx")
private String idx;

}

子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
@EqualsAndHashCode(callSuper = true)
@XmlAccessorType(XmlAccessType.FIELD)
public class WXTextMessage extends WXMessage {

@XmlElement(name = "Content")
private String content;

public WXTextMessage() {
super();
this.setMsgType(WXMessageType.TEXT.getType());
}
}

使用我自己写的工具类的时候报错:

image-20230720112053365

明显是根节点无法读取,本来我以为父类声明了@XmlRootElement(name = "xml"),他的子类应该都带有这个注解的效果了,结果就报错了。

那么子类加上这个注解吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@EqualsAndHashCode(callSuper = true)
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "xml") // 加上根节点标记
public class WXTextMessage extends WXMessage {

@XmlElement(name = "Content")
private String content;

public WXTextMessage() {
super();
this.setMsgType(WXMessageType.TEXT.getType());
}
}

结果发现出现了新的报错:

image-20230720112933842

类型转换错误?还是父类无法往子类转换。这怎么就变父类了呢,我都是直接把子类的Class传进去的啊,打上断点调试一波看看:

image-20230720115807447

这下更加糊涂了,可见传入的obj对象是子类类型,调用unmarshaller方法后转出来的竟然是它的父类!

博主思前想后,不懂其中的奥义,秉着排列组合的思想,既然父子类都标记,只有父类标记都用了,那就试试只有子类标记吧。好家伙,试一下就直接给我解决了。

image-20230720120540937

好好好,一下就给我解决了。

那么试着分析一波,JAXB解析的时候,读取@XmlRootElement(name = "xml")标签,当有父类有此标记的时候,优先读取父类的标签,并将其转换为父类对象。当然这也是我自己的分析,实际上最好对着源码分析一波,但是博主是在太懒了,有兴趣的研究看看。

其实到底这种手动解析XML的形式实在有点啰嗦,还有很多方法直接在Spring里面直接像解析Json一样,请求过来和返回都直接交给Spring的转换器来操作,这样才是最佳方案。