digester3 解析 xml 某字段首字母大写导致的 java.lang.NoSuchMethodException: Bean has no property named 错误解决办法

使用 Apache 的 digester3 解析 xml,API 提供商返回的 xml 格式如下:

<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
  <AssumeRoleResult>
    <AssumedRoleUser>
      <AssumedRoleId>AROBVWSTMMSAUKEYGUQYLx:AssumeRoleSessionx</AssumedRoleId>
      <Arn>arn:aws:sts::452277777756:assumed-role/AdminRole/AssumeRoleSession2</Arn>
    </AssumedRoleUser>
    <Credentials>
      <AccessKeyId>ASIAWSTMMSADFSUCVNVK</AccessKeyId>
      <SecretAccessKey>lf91PXA2Dy9wxCoAXRveg17pFFr4dMNkKoqdOc9Y</SecretAccessKey>
      <SessionToken>DDDGZXIvYXdzEH0aDJQXRFsToxhRbnYMSSK2AV9rtfcHNDNuX0jqxo+bEWCecOegp756cOoIPoqnHyv2LyNUwpGGA0mgNA5Sp+YxGf7eS6612TH9A8bU+tZ8Sb+cOKKSys8tWrbJ5VQQfJDlpjM2LJ5ZuO160eeDz3rxpnnKMI2WJhCghL3BEqj1/9p+hfnhOIFSPf+mjoH5TxpZxzCB3d0sICtrEe+u/vFQ5sx6nmQFuLcPvw8l0huC0FRiKLRWLYSkUhcCeP7gd8P+QuTsb8BHKI278YEGMi2VlXLEOzpLy2vN8tz+Jel3iFwf9oiUE8Xg4bH4YabfM4udePFk1PFkIOnVFc0=</SessionToken>
      <Expiration>2021-03-01T04:20:45Z</Expiration>
    </Credentials>
  </AssumeRoleResult>
  <ResponseMetadata>
    <RequestId>b7880ae6-59d3-4444-945c-bb9813543619</RequestId>
  </ResponseMetadata>
</AssumeRoleResponse>

根据 xml 自定义的数据模型 bean AssumeRoleResponse.java 如下:

package com.defonds.domain.model.aws.sts.assumerole;
public class AssumeRoleResponse {
    private String RequestId;
    private String AccessKeyId;
    private String SecretAccessKey;
    private String SessionToken;
    private String Expiration;
    public String getRequestId() {
        return RequestId;
    }
    public void setRequestId(String requestId) {
        RequestId = requestId;
    }
    public String getAccessKeyId() {
        return AccessKeyId;
    }
    public void setAccessKeyId(String accessKeyId) {
        AccessKeyId = accessKeyId;
    }
    public String getSecretAccessKey() {
        return SecretAccessKey;
    }
    public void setSecretAccessKey(String secretAccessKey) {
        SecretAccessKey = secretAccessKey;
    }
    public String getSessionToken() {
        return SessionToken;
    }
    public void setSessionToken(String sessionToken) {
        SessionToken = sessionToken;
    }
    public String getExpiration() {
        return Expiration;
    }
    public void setExpiration(String expiration) {
        Expiration = expiration;
    }
}

相应的 RulesModule AssumeRoleResponseModule.java 定义如下:

package com.defonds.domain.model.aws.sts.assumerole;
import org.apache.commons.digester3.binder.AbstractRulesModule;
public class AssumeRoleResponseModule extends AbstractRulesModule {
    @Override
    protected void configure() {
        forPattern("AssumeRoleResponse").createObject()
                .ofType("com.defonds.domain.model.aws.sts.assumerole.AssumeRoleResponse")
                .then().setProperties();
        forPattern("AssumeRoleResponse/ResponseMetadata/RequestId").setBeanProperty();
        forPattern("AssumeRoleResponse/AssumeRoleResult/Credentials/AccessKeyId").setBeanProperty();
        forPattern("AssumeRoleResponse/AssumeRoleResult/Credentials/SecretAccessKey").setBeanProperty();
        forPattern("AssumeRoleResponse/AssumeRoleResult/Credentials/SessionToken").setBeanProperty();
        forPattern("AssumeRoleResponse/AssumeRoleResult/Credentials/Expiration").setBeanProperty();
    }
}

对应的 Creator 如下:

		AssumeRoleResponse result = null;
        DigesterLoader digesterLoader = DigesterLoader.newLoader(new AssumeRoleResponseModule());
        Digester digester = digesterLoader.newDigester();
        ByteArrayInputStream tInputStringStream = new ByteArrayInputStream(xmlStr.getBytes());
        result = digester.parse(tInputStringStream);
        return result;

以上代码工作的时候报错:

java.lang.NoSuchMethodException: Bean has no property named AccessKeyId

反复确认了 RulesModule 里对 AccessKeyId 路径配的没有问题。把 AccessKeyId 首字母大写小写都是同样的错误。
谷歌了一下,也有人先后遇到了同样的问题。比如 coderanch 上有帖子《Commons Digester — java.lang. No Such Method Exception》,
要解析的 xml 如下:

<CrdPatientServiceResponse>
<PatientList ListCount="1">
<PatientDetail CrdMpiiInternalIdentifier="XXXXXXX">
<PatientDemographics>
<MCNumber>XX-XXX-XXX</MCNumber>
<PatientSocialSecurityNumber>XXX-XX-XXXX</PatientSocialSecurityNumber>
<PatientName>
<Title>Dr.</Title>
<FirstName>FirstName</FirstName>
<MiddleName>K.</MiddleName>
<LastName>LastName</LastName>
</PatientName>
<PatientHomeAddress>
<Address1>1234 Front Rd</Address1>
<City>Jacksonville</City>
<State>FL</State>
<PostalCode>32257-4945</PostalCode>
<CountryCode>US</CountryCode>
</PatientHomeAddress>
<PatientBirthDate>1846-09-20</PatientBirthDate>
<PatientAge/>
<PatientDeathIndicator>N</PatientDeathIndicator>
<PatientGenderCode>F</PatientGenderCode>
<MaritalStatusCode>M</MaritalStatusCode>
<RestrictedPatientIndicator>N</RestrictedPatientIndicator>
</PatientDemographics>
</PatientDetail>
</PatientList>
</CrdPatientServiceResponse>

模型 bean:

public class PatientDemographics {
static Logger logger = Logger.getLogger(PatientDemographics.class);
private String MCNumber;
private String PatientSocialSecurityNumber;
private String PatientBirthDate;
private String PatientAge;
private String PatientDeathIndicator;
private String PatientGenderCode;
private String MaritalStatusCode;
private String RestrictedPatientIndicator;
private Vector PatientName;
private Vector PatientHomeAddress;
public PatientDemographics(){
logger.debug("inside the PatientDemographics");
PatientName = new Vector();
PatientHomeAddress = new Vector();
}

        /**
 * @param number The mCNumber to set.
 */
public void setMCNumber(String number) {
logger.debug("MCNumber: "+number);
MCNumber = number;
}
 
/**
 * @return Returns the patientSocialSecurityNumber.
 */
public String getPatientSocialSecurityNumber() {
return PatientSocialSecurityNumber;
}
/**
 * @param patientSocialSecurityNumber The patientSocialSecurityNumber to set.
 */
public void setPatientSocialSecurityNumber(
String patientSocialSecurityNumber) {
PatientSocialSecurityNumber = patientSocialSecurityNumber;
}  

报错信息:

java.lang.NoSuchMethodException: Bean has no property named PatientSocialSecurityNumber

可惜最终没人给出解决方案。
再比如 stackoverflow 上的帖子《java.lang.NoSuchMethodException: Bean has no property named》,
要解析的 xml:

<ECnetPO>
    <Header>
        <PONumber>PONUMB0116</PONumber>
                <CostCenter>Cost Center 20</CostCenter>
        </Header>

    <Header>
        <PONumber>PONUMB0116</PONumber>
                <CostCenter>Cost Center 20</CostCenter>
        </Header>
</ECnetPO>

对应的模型 bean YESBarePO.java:

private String  PONumber;
private String  CostCenter;
public String getPONumber()
{
    return PONumber;
}
public void setPONumber(String pONumber)
{
    PONumber = pONumber;
}
public String getCostCenter()
{
    return CostCenter;
}
public void setCostCenter(String costCenter)
{
    CostCenter = costCenter;
}

RulesModule DigestPO.java:

    Digester digester = new Digester();
    //Push the current object onto the stack
    digester.push(this);
    digester.addSetProperties("ECnetPO");
    //Creates a new instance of the YESBarePO class
    digester.addObjectCreate("ECnetPO/Header", YESBarePO.class );

    //Uses setName method of the YESBarePO instance
    //Uses tag name as the property name
    digester.addBeanPropertySetter("ECnetPO/Header/PONumber","PONumber");
    digester.addBeanPropertySetter("ECnetPO/Header/CostCenter","CostCenter");

报的错误:

Sep 13, 2013 4:25:23 PM org.apache.commons.digester3.Digester endElement
SEVERE: End event threw exception
java.lang.NoSuchMethodException: Bean has no property named CostCenter
at org.apache.commons.digester3.BeanPropertySetterRule.end(BeanPropertySetterRule.java:206)
at org.apache.commons.digester3.Digester.endElement(Digester.java:1097)
at com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.endElement(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanEndElement(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl F r a g m e n t C o n t e n t D r i v e r . n e x t ( U n k n o w n S o u r c e ) a t c o m . s u n . o r g . a p a c h e . x e r c e s . i n t e r n a l . i m p l . X M L D o c u m e n t S c a n n e r I m p l . n e x t ( U n k n o w n S o u r c e ) a t c o m . s u n . o r g . a p a c h e . x e r c e s . i n t e r n a l . i m p l . X M L D o c u m e n t F r a g m e n t S c a n n e r I m p l . s c a n D o c u m e n t ( U n k n o w n S o u r c e ) a t c o m . s u n . o r g . a p a c h e . x e r c e s . i n t e r n a l . p a r s e r s . X M L 11 C o n f i g u r a t i o n . p a r s e ( U n k n o w n S o u r c e ) a t c o m . s u n . o r g . a p a c h e . x e r c e s . i n t e r n a l . p a r s e r s . X M L 11 C o n f i g u r a t i o n . p a r s e ( U n k n o w n S o u r c e ) a t c o m . s u n . o r g . a p a c h e . x e r c e s . i n t e r n a l . p a r s e r s . X M L P a r s e r . p a r s e ( U n k n o w n S o u r c e ) a t c o m . s u n . o r g . a p a c h e . x e r c e s . i n t e r n a l . p a r s e r s . A b s t r a c t S A X P a r s e r . p a r s e ( U n k n o w n S o u r c e ) a t c o m . s u n . o r g . a p a c h e . x e r c e s . i n t e r n a l . j a x p . S A X P a r s e r I m p l FragmentContentDriver.next(Unknown Source) at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(Unknown Source) at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(Unknown Source) at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(Unknown Source) at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(Unknown Source) at com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(Unknown Source) at com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.parse(Unknown Source) at com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl FragmentContentDriver.next(UnknownSource)atcom.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(UnknownSource)atcom.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(UnknownSource)atcom.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(UnknownSource)atcom.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(UnknownSource)atcom.sun.org.apache.xerces.internal.parsers.XMLParser.parse(UnknownSource)atcom.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.parse(UnknownSource)atcom.sun.org.apache.xerces.internal.jaxp.SAXParserImplJAXPSAXParser.parse(Unknown Source)
at org.apache.commons.digester3.Digester.parse(Digester.java:1588)
at org.apache.commons.digester3.Digester.parse(Digester.java:1557)
at ecnet.yes.po.digester.DigestPO.digest(DigestPO.java:80)
at ecnet.yes.po.digester.DigestPO.main(DigestPO.java:16)
Sep 13, 2013 4:25:23 PM org.apache.commons.digester3.Digester parse
SEVERE: An error occurred while parsing XML from ‘file:/D:/workspace/Digester/src/ecnet/yes/po/digester/YES_PO.xml’, see nested exceptions

在这个帖子下边倒是有大拿指出了这种错误的原因:

I think the reason you didn’t observe the same problem with PONumber is because bean mappers generally tend to be a bit more lenient when the second character of a variable name is upper case.

给出的解决方案是把 xml 相应返回的字段首字母小写。
虽然没有尝试,但这个解决方案我认为是可行的。只是可惜,不是所有下游 API 使用者都有修改上游 API 提供商返回数据格式的话语权,尤其是 API 提供商是类似于 AWS 的这种大厂。所以这个解决方案对于只能“接受现状”的下游 API 消费者是不太实用的。
这个问题看上去是 digester3 不太完美的一个地方,也就是,它能解析出来大写开头的 xml 属性,但是在写入相应 bean 属性时就傻眼了——不管你相应 bean 属性是大写开头还是小写开头,一律 java.lang.NoSuchMethodException。
经过多番尝试,笔者终于找到了合适的解决方案,即放弃 RulesModule,使用注解方式来定义模型 bean 及属性。也就是说,我不管你返回的是什么东东,只要 digester3 能把你解析出来,我就让你强加到我指定的 bean 属性上边。
修改后的模型 bean AssumeRoleResponse.java 如下:

package com.defonds.domain.model.aws.sts.assumerole;
import org.apache.commons.digester3.annotations.rules.BeanPropertySetter;
import org.apache.commons.digester3.annotations.rules.ObjectCreate;
@ObjectCreate(pattern = "AssumeRoleResponse")
public class AssumeRoleResponse {
    @BeanPropertySetter(pattern = "AssumeRoleResponse/ResponseMetadata/RequestId")
    private String requestId;
    @BeanPropertySetter(pattern = "AssumeRoleResponse/AssumeRoleResult/Credentials/AccessKeyId")
    private String accessKeyId;
    @BeanPropertySetter(pattern = "AssumeRoleResponse/AssumeRoleResult/Credentials/SecretAccessKey")
    private String secretAccessKey;
    @BeanPropertySetter(pattern = "AssumeRoleResponse/AssumeRoleResult/Credentials/SessionToken")
    private String sessionToken;
    @BeanPropertySetter(pattern = "AssumeRoleResponse/AssumeRoleResult/Credentials/Expiration")
    private String expiration;
    public String getRequestId() {
        return requestId;
    }
    public void setRequestId(String requestId) {
        this.requestId = requestId;
    }
    public String getAccessKeyId() {
        return accessKeyId;
    }
    public void setAccessKeyId(String accessKeyId) {
        this.accessKeyId = accessKeyId;
    }
    public String getSecretAccessKey() {
        return secretAccessKey;
    }
    public void setSecretAccessKey(String secretAccessKey) {
        this.secretAccessKey = secretAccessKey;
    }
    public String getSessionToken() {
        return sessionToken;
    }
    public void setSessionToken(String sessionToken) {
        this.sessionToken = sessionToken;
    }
    public String getExpiration() {
        return expiration;
    }
    public void setExpiration(String expiration) {
        this.expiration = expiration;
    }
}

RulesModule 类 AssumeRoleResponseModule.java 弃用,对应的 Creator 相关代码修改如下:

        DigesterLoader loader = DigesterLoader.newLoader(new FromAnnotationsRuleModule() {
            @Override
            protected void configureRules() {
                bindRulesFrom(AssumeRoleResponse.class);
            }
        });
        AssumeRoleResponse result = null;
        Digester digester = loader.newDigester();
        ByteArrayInputStream tInputStringStream = new ByteArrayInputStream(xmlStr.getBytes());
        result = digester.parse(tInputStringStream);

执行返回 xml 数据解析,发现问题已解决。
参考文档

posted @ 2021-03-02 11:23  Defonds  阅读(135)  评论(0编辑  收藏  举报