当请求元素未以 'Request' 结尾时,spring-ws 生成的 wsdl 无效

Invalid wsdl generated by spring-ws when the request element doesn't end with 'Request'

我必须准备一个 web 服务来接受一个已经定义的 wsdl 结构。我按照教程 found here, with source code for the test project downloadable here.

对于 xsd 这样的:

<xs:element name="getCountryRequest">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="name" type="xs:string"/>
        </xs:sequence>
    </xs:complexType>
</xs:element>

应用返回请求的Wsdl操作正常,如下所示:

<wsdl:binding name="CountriesPortSoap11" type="tns:CountriesPort">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
    <wsdl:operation name="getCountry">
        <soap:operation soapAction=""/>
        <wsdl:input name="getCountryRequest">
            <soap:body use="literal"/>
        </wsdl:input>
        <wsdl:output name="getCountryResponse">
            <soap:body use="literal"/>
        </wsdl:output>
    </wsdl:operation>
</wsdl:binding>

但是当我将 xsd 更改为(元素名称中没有 'Request')时:

<xs:element name="getCountry">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="name" type="xs:string"/>
        </xs:sequence>
    </xs:complexType>
</xs:element>

wsdl 无效,未指定 <input>:

<wsdl:binding name="CountriesPortSoap11" type="tns:CountriesPort">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
    <wsdl:operation name="getCountry">
        <soap:operation soapAction=""/>
        <wsdl:output name="getCountryResponse">
            <soap:body use="literal"/>
        </wsdl:output>
    </wsdl:operation>
</wsdl:binding>

我该如何解决?如何使 Request-less 元素正确显示为 wsdl 中的 soap 操作输入?

根据 official Spring WS documentation,Request/Response 后缀是用于自动确定 request/response 并因此生成预期 WSDL 的默认后缀。

The DefaultWsdl11Definition which builds a WSDL from a XSD schema. This definition iterates over all element elements found in the schema, and creates a message for all elements. Next, it creates WSDL operation for all messages that end with the defined request or response suffix. The default request suffix is Request; the default response suffix is Response, though these can be changed by setting the requestSuffix and responseSuffix properties, respectively.

因此,您可以在上述示例代码中,更改 WebServiceConfig 配置 class、defaultWsdl11Definition 方法中的后缀,添加以下方法调用:

wsdl11Definition.setRequestSuffix("your-new-prefix-here");

例如,您可以将其设置为Req而不是Request,然后构建将自动生成一个新的GetCountryReq class,代码为[=然后需要手动调整 22=] 和 CountryEndpoint,消除编译错误(因为它们仍然指向先前存在的 GetCountryRequest class),但还要确保更改 [= CountryEndPoint class.

@PayloadRoot 注释的 25=] 属性

我试过了,构建很顺利,WSDL 也相应更新了。

这是关于将默认后缀更改为另一个后缀。 但是如果将其更改为空后缀呢?

wsdl11Definition.setRequestSuffix("");

异常:后缀不能为空。 Spring不支持。根据这个 thread:

Basically, the problem is this:
We have to differentiate which schema elements are wsdl messages, and which aren't.
Of all wsdl messages, we have to figure out which are input (request) messages.
Of all wsdl messages, we have to figure out which are output (response) messages.

The DefaultWsdl11Definition figures this out by suffixes. Or, more specifically, it delegates to a SuffixBasedMessagesProvider and SuffixBasedPortTypesProvider to do so.
So if you have some other preferred way of determining what makes an input/output message, you will have to write your own messagesprovider and or porttypesprovider.

Simply put: there is no generic way for Spring-WS to determine what makes up a request and a response, rather than using suffixes.

注意:此消息的发布者是 DefaultWsdl11Definition class(根据 javadocs)的作者 Arjen Poutsma,该组件处理基于这些后缀约定的自动映射。

但他留下了一扇敞开的门:写你自己的 SuffixBasedMessagesProviderSuffixBasedPortTypesProvider。但是,他还在 DefaultWsdl11Definition(实例化这些提供程序的地方)中将所有内容都保留为私有,因此您还需要编写(复制)自己的 WSDL11 定义映射器。

这是我当时遵循的过程:

  • 创建您自己的 CustomSuffixBasedMessagesProvider,覆盖 setRequestSuffix 方法并删除对空后缀的检查,在 isMessageElement 方法中您需要处理新映射
  • 创建您自己的 CustomSuffixBasedPortTypesProvider,覆盖 setRequestSuffix 方法并删除对空后缀的检查,在 getOperationNameisInputMessage 方法中您需要处理新映射
  • 创建您自己的 CustomWsdl11Definition 作为现有 DefaultWsdl11Definition 的副本并实例化上面创建的您自己的提供程序
  • 更改 WebServiceConfig class、defaultWsdl11Definition 方法,使用您自己的 CustomWsdl11Definition 以应用整个自定义。

但是,空后缀会带来一些挑战,因为它适用于任何元素(也就是说,每个元素都有一个空后缀)。这就是我提到 isMessageElementisInputMessagegetOperationName 处理的原因:在增长的 WSDL 上,您可能很容易最终对映射进行硬编码(对于 GetCountry 请求,GetCountryResponse 是响应)或传递一个properties/map(如上面提到的 thread 中所建议),但在 WebServiceConfig 配置 class 中再次重复大部分 XSD,使其难以维护,解决问题,分享。

所以,我真的建议不要踏上这段旅程,要么坚持使用默认后缀(如果可能),要么创建一个新后缀,但要避免空后缀(毕竟库不允许它们)。

但自从我踏上旅程后,这里是可行的解决方案:

MySuffixBasedMessagesProvider 自定义 class(为简洁起见删除了导入):

package hello;

public class MySuffixBasedMessagesProvider extends SuffixBasedMessagesProvider {

    protected String requestSuffix = DEFAULT_REQUEST_SUFFIX;

    public String getRequestSuffix() {
        return this.requestSuffix;
    }

    public void setRequestSuffix(String requestSuffix) {
        this.requestSuffix = requestSuffix;
    }

    @Override
    protected boolean isMessageElement(Element element) {
        if (isMessageElement0(element)) {
            String elementName = getElementName(element);
            Assert.hasText(elementName, "Element has no name");
            return elementName.endsWith(getResponseSuffix())
                    || (getRequestSuffix().isEmpty() ? true : elementName.endsWith(getRequestSuffix()))
                    || elementName.endsWith(getFaultSuffix());
        }
        return false;
    }

    protected boolean isMessageElement0(Element element) {
        return "element".equals(element.getLocalName())
                && "http://www.w3.org/2001/XMLSchema".equals(element.getNamespaceURI());
    }
}

MySuffixBasedPortTypesProvider 自定义 class(为简洁起见删除了导入):

package hello;

public class MySuffixBasedPortTypesProvider extends SuffixBasedPortTypesProvider {

    private String requestSuffix = DEFAULT_REQUEST_SUFFIX;

    public String getRequestSuffix() {
        return requestSuffix;
    }

    public void setRequestSuffix(String requestSuffix) {
        this.requestSuffix = requestSuffix;
    }

    @Override
    protected String getOperationName(Message message) {
        String messageName = getMessageName(message);
        String result = null;
        if (messageName != null) {
            if (messageName.endsWith(getResponseSuffix())) {
                result = messageName.substring(0, messageName.length() - getResponseSuffix().length());
            } else if (messageName.endsWith(getFaultSuffix())) {
                result = messageName.substring(0, messageName.length() - getFaultSuffix().length());
            } else if (messageName.endsWith(getRequestSuffix())) {
                result = messageName.substring(0, messageName.length() - getRequestSuffix().length());
            }
        }
        return result;
    }

    @Override
    protected boolean isInputMessage(Message message) {
        String messageName = getMessageName(message);

        return messageName != null && !messageName.endsWith(getResponseSuffix());
    }

    private String getMessageName(Message message) {
        return message.getQName().getLocalPart();
    }

}

MyWsdl11Definition 自定义 class(本质上是默认的副本,只是实例化上面的 classes,为简洁起见删除了导入):

package hello;

public class MyWsdl11Definition implements Wsdl11Definition, InitializingBean {

    private final InliningXsdSchemaTypesProvider typesProvider = new InliningXsdSchemaTypesProvider();

    private final SuffixBasedMessagesProvider messagesProvider = new MySuffixBasedMessagesProvider();

    private final SuffixBasedPortTypesProvider portTypesProvider = new MySuffixBasedPortTypesProvider();

    private final SoapProvider soapProvider = new SoapProvider();

    private final ProviderBasedWsdl4jDefinition delegate = new ProviderBasedWsdl4jDefinition();

    private String serviceName;

    public MyWsdl11Definition() {
        delegate.setTypesProvider(typesProvider);
        delegate.setMessagesProvider(messagesProvider);
        delegate.setPortTypesProvider(portTypesProvider);
        delegate.setBindingsProvider(soapProvider);
        delegate.setServicesProvider(soapProvider);
    }

    public void setTargetNamespace(String targetNamespace) {
        delegate.setTargetNamespace(targetNamespace);
    }

    public void setSchema(XsdSchema schema) {
        typesProvider.setSchema(schema);
    }

    public void setSchemaCollection(XsdSchemaCollection schemaCollection) {
        typesProvider.setSchemaCollection(schemaCollection);
    }

    public void setPortTypeName(String portTypeName) {
        portTypesProvider.setPortTypeName(portTypeName);
    }

    public void setRequestSuffix(String requestSuffix) {
        portTypesProvider.setRequestSuffix(requestSuffix);
        messagesProvider.setRequestSuffix(requestSuffix);
    }

    public void setResponseSuffix(String responseSuffix) {
        portTypesProvider.setResponseSuffix(responseSuffix);
        messagesProvider.setResponseSuffix(responseSuffix);
    }

    public void setFaultSuffix(String faultSuffix) {
        portTypesProvider.setFaultSuffix(faultSuffix);
        messagesProvider.setFaultSuffix(faultSuffix);
    }

    public void setCreateSoap11Binding(boolean createSoap11Binding) {
        soapProvider.setCreateSoap11Binding(createSoap11Binding);
    }

    public void setCreateSoap12Binding(boolean createSoap12Binding) {
        soapProvider.setCreateSoap12Binding(createSoap12Binding);
    }

    public void setSoapActions(Properties soapActions) {
        soapProvider.setSoapActions(soapActions);
    }

    public void setTransportUri(String transportUri) {
        soapProvider.setTransportUri(transportUri);
    }

    public void setLocationUri(String locationUri) {
        soapProvider.setLocationUri(locationUri);
    }

    public void setServiceName(String serviceName) {
        soapProvider.setServiceName(serviceName);
        this.serviceName = serviceName;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        if (!StringUtils.hasText(delegate.getTargetNamespace()) && typesProvider.getSchemaCollection() != null &&
                typesProvider.getSchemaCollection().getXsdSchemas().length > 0) {
            XsdSchema schema = typesProvider.getSchemaCollection().getXsdSchemas()[0];
            setTargetNamespace(schema.getTargetNamespace());
        }
        if (!StringUtils.hasText(serviceName) && StringUtils.hasText(portTypesProvider.getPortTypeName())) {
            soapProvider.setServiceName(portTypesProvider.getPortTypeName() + "Service");
        }
        delegate.afterPropertiesSet();
    }

    @Override
    public Source getSource() {
        return delegate.getSource();
    }

}

此外,WebServiceConfig class 的 defaultWsdl11Definition 方法将更改如下(使用上面的自定义):

@Bean(name = "countries")
public Wsdl11Definition defaultWsdl11Definition(XsdSchema countriesSchema) {
    MyWsdl11Definition wsdl11Definition = new MyWsdl11Definition();
    wsdl11Definition.setPortTypeName("CountriesPort");
    wsdl11Definition.setLocationUri("/ws");
    wsdl11Definition.setRequestSuffix("");
    wsdl11Definition.setTargetNamespace("http://spring.io/guides/gs-producing-web-service");
    wsdl11Definition.setSchema(countriesSchema);
    return wsdl11Definition;
}

注意 wsdl11Definition.setRequestSuffix(""); 有效地将后缀设置为空。

自定义完成后,再修改XSD去掉Request后缀,会生成新的GetCountryclass,需要手动去掉之前存在的GetCountryRequest,修复编译如上所述的错误(测试和端点 class,只是不要忘记也更新 @PayloadRoot 注释)。

Build 然后 运行 没问题。 运行 Application main,生成的 WSDL 将按要求包含:

<wsdl:binding name="CountriesPortSoap11" type="tns:CountriesPort">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="getCountry">
  <soap:operation soapAction=""/>
  <wsdl:input name="getCountry">
    <soap:body use="literal"/>
  </wsdl:input>
  <wsdl:output name="getCountryResponse">
    <soap:body use="literal"/>
  </wsdl:output>
</wsdl:operation>
</wsdl:binding>

希望对您有所帮助。这是一个真实的例子,说明了配置约定极大地提供了什么,而框架中的一个小的不可预见的变化在编写代码和添加定制方面意味着什么。

Spring-WS 自动 wsdl 公开功能基于在 http://docs.spring.io/spring-ws/site/reference/html/server.html#server-automatic-wsdl-exposure

您用作起点的教程使用注释而不是名称空间,但应该有一种方法来指定文档中提到的 requestSuffix 和 responseSuffix 属性。但是,恐怕您不能将它们留空。

或者,您可以公开手动编写的 WSDL。我建议这样做,因为你从一开始就给出了 wsdl。

有更简单的方法。使用 ProviderBasedWsdl4jDefinition 代替 DefaultWsdl11Definition 并设置除一个以外的所有默认提供程序 - 代替 SuffixBasedPortTypesProvider 使用如下内容:

public class DelphiStylePortTypesProvider extends AbstractPortTypesProvider {

    @Override
    protected String getOperationName(Message msg) {
        if (isInputMessage(msg)) {
            return msg.getQName().getLocalPart();
        }
        if (isOutputMessage(msg)) {
            return msg.getQName().getLocalPart().replace("Response", "");
        }
        return "";
    }

    @Override
    protected boolean isInputMessage(Message msg) {
        return !msg.getQName().getLocalPart().endsWith("Response");
    }

    @Override
    protected boolean isOutputMessage(Message msg) {
        return msg.getQName().getLocalPart().endsWith("Response");
    }

    @Override
    protected boolean isFaultMessage(Message msg) {
        return false;
    }

}

我想最好的解决方案是编写一个您需要的 WSDL 文件。在我看来,使用 Spring 约定以外的请求名称和响应名称从 XSD 文件创建自定义 WSDL 是不可路由的。因此最好创建 WSDL 并将其导入项目,然后从中生成您的 class。

我有同样的错误。 我的设置如下:

使用 Maven jaxb2-maven-plugin

从现有 XSD 文件生成 类
<!-- SOAP -->
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>jaxb2-maven-plugin</artifactId>
    <version>2.5.0</version>
    <executions>
        <execution>
            <id>xjc</id>
            <goals>
                <goal>xjc</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <!-- The package of your generated sources -->
        <packageName>com.company</packageName>
        <addGeneratedAnnotation>true</addGeneratedAnnotation>
        <quiet>true</quiet>
        <sources>
            <source>${project.basedir}/src/main/resources/file.xsd</source>
        </sources>
    </configuration>
</plugin>

处理方法:

@PayloadRoot(namespace = "URI", localPart = "LOCAL_PART")
@ResponsePayload
public JAXBElement<StatusResponse> retrieveNextStatesRequest(@RequestPayload Request request) {
    // Logic
    return new Response();
}

但是 jaxb2-maven-plugin 不可能添加 XmlRoot 注释。但是,它提供了一个 ObjectFactory 来生成包装在 JAXBElement 个实例中的实例。

因此,当相应地更改方法时,SOAP 请求现在可以按预期工作:

@PayloadRoot(namespace = "URI", localPart = "LOCAL_PART")
@ResponsePayload
public Response retrieveNextStatesRequest(@RequestPayload Request request) {
    // Logic
    return new ObjectFactory.createResponse(new Response());
}

@EnableWs配置中class:

@Bean(WSDL_SCHEMA_NAME)
public ProviderBasedWsdl4jDefinition providerBasedWsdl4jDefinition(XsdSchema xsdSchema) {
    var wsdl4jDefinition = new ProviderBasedWsdl4jDefinition();
    wsdl4jDefinition.setTargetNamespace(WSDL_TARGET_NAMESPACE);

    var suffixBasedPortTypesProvider = new CustomPortTypesProvider();
    suffixBasedPortTypesProvider.setPortTypeName(WSDL_PORT_TYPE_NAME);
    wsdl4jDefinition.setPortTypesProvider(suffixBasedPortTypesProvider);

    var inliningXsdSchemaTypesProvider = new InliningXsdSchemaTypesProvider();
    inliningXsdSchemaTypesProvider.setSchema(xsdSchema);
    wsdl4jDefinition.setTypesProvider(inliningXsdSchemaTypesProvider);

    wsdl4jDefinition.setMessagesProvider(new CustomMessagesProvider());

    var soapProvider = new SoapProvider();
    soapProvider.setLocationUri(WSDL_SCHEMA_URI);
    soapProvider.setServiceName(WSDL_SCHEMA_NAME);
    wsdl4jDefinition.setBindingsProvider(soapProvider);
    wsdl4jDefinition.setServicesProvider(soapProvider);

    return wsdl4jDefinition;
}

private static class CustomMessagesProvider extends SuffixBasedMessagesProvider {
    private static final String[] REQUEST_ELEMENTS =
            new String[] {"requestOne", "requestTwo"};

    @Override
    protected boolean isMessageElement(Element element) {
        if (super.isMessageElement(element)) {
            return true;
        } else {
            boolean isElement = "element".equals(element.getLocalName()) &&
                    "http://www.w3.org/2001/XMLSchema".equals(element.getNamespaceURI());
            String elementName = getElementName(element);
            return isElement && Arrays.asList(REQUEST_ELEMENTS).contains(elementName);
        }
    }
}

private static class CustomPortTypesProvider extends SuffixBasedPortTypesProvider {
    @Override
    protected String getOperationName(Message message) {
        String messageName = message.getQName().getLocalPart();
        if (isInputMessage(message)) {
            return messageName;
        } else if (isOutputMessage(message)) {
            return messageName.substring(0, messageName.length() - getResponseSuffix().length());
        } else if (isFaultMessage(message)) {
            return messageName.substring(0, messageName.length() - getFaultSuffix().length());
        }
        return null;
    }

    @Override
    protected boolean isInputMessage(Message message) {
        String messageName = message.getQName().getLocalPart();
        return !messageName.endsWith(getResponseSuffix()) && !messageName.endsWith(getFaultSuffix());
    }
}