最近因为做一套SSO框架,需要解析SAML报文,研究了一下openSAML发现其需要1.5以上的jdk,于是决定自己做解析程序。在网上比较了一番dom和sax后,决定用sax来解析,因为我们更关注的是报文中某些个属性值,而不是报文的结构。闲话少叙,下面开始SAX的学习。
首先我们先了解一下SAX的基本概念。SAX是Simple Api for XML的缩写,相比与DOM,SAX是一种轻量级的XML访问方法。DOM在处理xml文档时,我们需要读入整个文档,在内存中创建dom树。在文档比较小的时候这没有什么问题,但随着文档的增大,dom的访问效率以及内存占用都会出现问题,这时,SAX就是比较好的替代方案。SAX不同于DOM的文档驱动,它是事件驱动的,在解析过程中,它不需要读入整个文档,文档读入的过程就是解析过程。所谓的事件驱动,是一种基于回调机制的程序运行方法。在解析之前,先注册一个ContentHandler,相当于一个事件监听器,其中定义了很多方法,用以处理在读取文档各个部分时要做的事情,比如startDocument(),它定义了解析过程中,遇到文档开始时所作的事情。当XMLReader读取到合适的内容,就会抛出相应的事件,把内容交给ContentHander去处理。在网上搜索SAX资料时,找到了一个简单的例子,很清晰的说明了SAX解析过程。我们来看这个简单的xml文件
<POEM>
<AUTHOR>Ogden Nash</AUTHOR>
<TITLE>Fleas</TITLE>
<LINE>Adam</LINE>
</POEM>
SAX的解析过程:
遇到的项目 |
方法回调 |
{文档开始} |
startDocument() |
<POEM> |
startElement(null,"POEM",null,{Attributes}) |
"\n" |
characters("<POEM>\n...", 6, 1) |
<AUTHOR> |
startElement(null,"AUTHOR",null,{Attributes}) |
"Ogden Nash" |
characters("<POEM>\n...", 15, 10) |
</AUTHOR> |
endElement(null,"AUTHOR",null) |
"\n" |
characters("<POEM>\n...", 34, 1) |
<TITLE> |
startElement(null,"TITLE",null,{Attributes}) |
"Fleas" |
characters("<POEM>\n...", 42, 5) |
</TITLE> |
endElement(null,"TITLE",null) |
"\n" |
characters("<POEM>\n...", 55, 1) |
<LINE> |
startElement(null,"LINE",null,{Attributes}) |
"Adam" |
characters("<POEM>\n...", 62, 4) |
</LINE> |
endElement(null,"LINE",null) |
"\n" |
characters("<POEM>\n...", 67, 1) |
</POEM> |
endElement(null,"POEM",null) |
{文档结束} |
endDocument() |
SAX提供了一个DefaultHandler类,其中所有方法为空实现,我们要做的就是创建一个自己的Handeler类继承DefaultHandler,然后重载其中的startDocument()、startElement()、characters()、endElement()等方法来自己的处理。
现在我们来动手做个解析SAML AuthnRequest报文的例子,首先来看报文
<samlp:AuthnRequest
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="id_1"
Version="2.0"
IssueInstant="2007-12-05T09:21:59Z">
<saml:Issuer>https://www.chinamobile.com/SSO</saml:Issuer>
<samlp:NameIDPolicy
AllowCreate="true"
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"/>
</samlp:AuthnRequest>
我们要把其中的ID、IssueInstant、Issuer、AllowCreate、Format属性解析出来,放到一个java对象中。 首先我们定义一个AuthRequest对象,其中包含上面的属性以及get、set方法
static class AuthRequest {
private String id;
private String issueInstant;
private String issuer;
private String allowCreate;
private String format;
public void setID(String id){
this.id = id;
}
public void setIssueInstant(String issueInstant){
this.issueInstant = issueInstant;
}
public void setIssuer(String issuer){
this.issuer = issuer;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getAllowCreate() {
return allowCreate;
}
public void setAllowCreate(String allowCreate) {
this.allowCreate = allowCreate;
}
public String getFormat() {
return format;
}
public void setFormat(String format) {
this.format = format;
}
public String getIssueInstant() {
return issueInstant;
}
public String getIssuer() {
return issuer;
}
@Override
public String toString() {
return "id="+id+",issueInstant="+issueInstant+",issuer="+issuer;
}
}
然后我们创建自己的handler MyHandler
static class MyHandler extends DefaultHandler {
private AuthRequest request;
private Stack stack;
public MyHandler(AuthRequest request){
this.request = request;
this.stack = new Stack();
}
@Override
public void startElement(String uri, String localName, String name,
Attributes attributes) throws SAXException {
stack.push(name);
if ("samlp:AuthnRequest".equalsIgnoreCase(name)){
for (int i = 0;i < attributes.getLength();i ++){
if ("ID".equalsIgnoreCase(attributes.getQName(i))){
request.setID(attributes.getValue(i));
}else if ("IssueInstant".equalsIgnoreCase(attributes.getQName(i))){
request.setIssueInstant(attributes.getValue(i));
}
}
}else if ("samlp:NameIDPolicy".equalsIgnoreCase(name)){
for (int i = 0;i < attributes.getLength();i ++){
if ("AllowCreate".equalsIgnoreCase(attributes.getQName(i))){
request.setAllowCreate(attributes.getValue(i));
}else if ("Format".equalsIgnoreCase(attributes.getQName(i))){
request.setFormat(attributes.getValue(i));
}
}
}
}
@Override
public void endElement(String uri, String localName, String name)
throws SAXException {
stack.pop();
}
@Override
public void characters(char[] ch, int start, int length)
throws SAXException {
if ("saml:Issuer".equalsIgnoreCase((String)stack.peek())){
request.setIssuer(new String(ch,start,length));
}
}
}
这里我们用了一个stack来保存当前解析的节点信息。因为标准的xml每个标签都一定有一个对应的结束标签,我们在startElement时把标签名称push到stack中,在endElement时pop出来,这样就可以获取当前解析的标签在文档中的位置。
最后我们来执行代码
public static void main(String[] args) throws Exception {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
AuthRequest request = new AuthRequest();
parser.parse(new File("src/xml/req.xml"), new MyHandler(request));
System.out.println(request);
}
得到输出:
id=id_1,issueInstant=2007-12-05T09:21:59Z,issuer=https://www.chinamobile.com/SSO
这里我们完成了一个简单的将XML转成JAVA对象的例子。可以看出,SAX提供的是一套比DOM更为基础的XML解析API,通过自定义Handler,我们可以自己控制xml解析过程,只解析我们关心的信息。当然具体用DOM,还是SAX,还是要根据实际情况去选择。