显示导航

Grails 和 SOAP

了解如何使用 Grails 应用程序利用 SOAP 端点

作者:谢尔吉奥·德尔·阿莫

Grails 版本 4.0.1

1 帮助使用 Grails

Grails 培训 - 由创建并积极维护 Grails 框架的人员进行开发和交付!

2 开始使用

在此指南中,你将使用 Grails 应用程序利用 SOAP Web 服务。

2.1 你需要什么

若要完成此指南,你需要以下内容

  • 时间

  • 不错的文本编辑器或 IDE

  • JDK 1.8 或更高版本,已安装并正确配置 JAVA_HOME

2.2 如何完成该指南

若要开始,请执行以下操作

Grails 指南仓库包含两个文件夹

  • initial 初始项目。通常是包含一些附加代码的简单 Grails 应用程序,可为你节省时间。

  • complete 示例已完成。这是按照指南提出的步骤,然后对 initial 文件夹应用这些更改得到的结果。

若要完成此指南,请转到 initial 文件夹

  • 转到 grails-guides/grails-soap/initial

并按照后续部分中的说明操作。

如果你进入 grails-guides/grails-soap/complete,可以直接转到示例已完成

3 编写应用程序

VIES (增值税信息交换系统) 是一种电子方式,用于验证欧盟境内注册的经济经营者的增值税识别号码,用于跨境商品或服务交易。

例如,如果你在欧盟创建了一个电子商务 Web 应用程序,你会需要检查购买者的增值税号的有效性,以便创建正确的发票。

为了自动执行验证检查,欧盟不提供 REST 端点,而是 SOAP 服务。其 WSDL 文件可从此处 获得。

SOAP(最初为简单对象访问协议)是一个协议规范,用于在计算机网络中的 Web 服务实现中交换结构化的信息。其目的是诱导可扩展性、中立性和独立性

3.1 SOAP 库

为了使用 SOAP Web 服务,我们使用了 groovy-wslite。这是一个针对 Groovy 的库,可以提供无装饰的 SOAP 和 REST Web 服务客户端。

添加 wslite 依赖项

build.gradle
compile 'com.github.groovy-wslite:groovy-wslite:1.1.3'

我们将 SOAP 代码封装到 Grails 服务中

grails-app/services/demo/VatService.groovy
package demo

import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import wslite.soap.SOAPClient
import wslite.soap.SOAPResponse

@CompileStatic
class VatService {
    String url = 'http://ec.europa.eu/taxation_customs/vies/services/checkVatService'
    SOAPClient client = new SOAPClient("${url}.wsdl")

    @CompileDynamic
    Boolean validateVat(String memberStateCode, String vatNumberCode) {
        SOAPResponse response = client.send(SOAPAction: url) {
            body('xmlns': 'urn:ec.europa.eu:taxud:vies:services:checkVat:types') {
                checkVat {
                    countryCode(memberStateCode)
                    vatNumber(vatNumberCode)
                }
            }
        }
        response.checkVatResponse.valid.text() == 'true'
    }
}

我们使用Grails 测试框架对它进行测试

src/test/groovy/demo/VatServiceSpec.groovy
package demo

import grails.testing.services.ServiceUnitTest
import spock.lang.Specification
import spock.lang.Unroll

class VatServiceSpec extends Specification implements ServiceUnitTest<VatService> {

    @Unroll
    def "#memberState : #vatNumber #unrollDescription"(String memberState, String vatNumber, Boolean expected, String unrollDescription) {

        expect:
        expected == service.validateVat(memberState, vatNumber)

        where:
        memberState | vatNumber   || expected
        'es'        | 'B99286353' || true
        'es'        | 'B19280031' || true
        'es'        | 'XXXXXXXXX' || false

        unrollDescription = expected ? 'is valid' : ' is not valid'
    }
}

3.2 国家/地区

最好将我们希望支持的国家/地区封装到配置文件中。

这样,如果一个国家/地区离开欧盟(例如英国),那么在我们的应用程序中进行更改就非常轻松。

application.yml 中添加一个国家/地区列表

grails-app/conf/application.yml
eu:
    countries:
        -
            code: AT
            name: Austria
        -
            code: BE
            name: Belgium
        -
            code: BG
            name: Bulgaria
        -
            code: CY
            name: Cyprus
        -
            code: CZ
            name: Czech Republic
        -
            code: DE
            name: Germany
        -
            code: DK
            name: Denmark
        -
            code: EE
            name: Estonia
        -
            code: EL
            name: Greece
        -
            code: ES
            name: Spain
        -
            code: FI
            name: Finland
        -
            code: FR
            name: France
        -
            code: GB
            name: United Kingdom
        -
            code: HR
            name: Croatia
        -
            code: HU
            name: Hungary
        -
            code: IE
            name: Ireland
        -
            code: IT
            name: Italy
        -
            code: LT
            name: Lithuania
        -
            code: LU
            name: Luxembourg
        -
            code: LV
            name: Latvia
        -
            code: MT
            name: Malta
        -
            code: NL
            name: The Netherlands
        -
            code: PL
            name: Poland
        -
            code: PT
            name: Portugal
        -
            code: RO
            name: Romania
        -
            code: SE
            name: Sweden
        -
            code: SI
            name: Slovenia
        -
            code: SK
            name: Slovakia

创建一个 POGO

src/main/groovy/demo/Country.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class Country {
    String code
    String name
}

在 Grails 服务中读取国家/地区

grails-app/services/demo/CountryService.groovy
package demo

import grails.config.Config
import grails.core.support.GrailsConfigurationAware
import groovy.transform.CompileStatic

@CompileStatic
class CountryService implements GrailsConfigurationAware {

    List<Country> countries = [] as List<Country>

    @Override
    void setConfiguration(Config co) {
        List<Map> l = co.getProperty('eu.countries', List)
        for ( Map m : l ) {
            countries << new Country(name: m.get('name') as String, code: m.get('code') as String)
        }
    }

    List<Country> findAll() {
        countries
    }


}

3.3 控制器和视图

此应用程序允许用户提交表单并检查增值税号对于特定国家/地区是否有效。

我们将请求封装到一个命令对象中

grails-app/controllers/demo/VatCommand.groovy
package demo

import grails.validation.Validateable

class VatCommand implements Validateable {

    String code
    String vatNumber

    static constraints = {
        code nullable: false
        vatNumber nullable: false
    }
}

创建一个 VatController 来处理该请求,与之前引入的服务协作,并通过flash 消息或错误生成响应。

grails-app/controllers/demo/VatController.groovy
package demo

import groovy.transform.CompileStatic
import org.springframework.context.MessageSource

@CompileStatic
class VatController {

    static allowedMethods = [index: 'GET', validate: 'GET']

    VatService vatService

    CountryService countryService

    MessageSource messageSource

    def index() {
        [countries: countryService.findAll()]
    }

    def validate(VatCommand cmd) {

        if ( cmd.hasErrors() ) {
            render view: 'index', model: [cmd: cmd, countries: countryService.findAll()]
            return
        }
        boolean isValid = vatService.validateVat(cmd.code, cmd.vatNumber)
        if ( isValid ) {
            flash.message = messageSource.getMessage('vat.valid',
                    [cmd.code, cmd.vatNumber] as Object[],
                    "${cmd.code} : ${cmd.vatNumber} is valid",
                    request.locale)

        } else {
            flash.error = messageSource.getMessage('vat.valid',
                    [cmd.code, cmd.vatNumber] as Object[],
                    "${cmd.code} : ${cmd.vatNumber} is NOT valid",
                    request.locale)
        }
        render view: 'index', model: [cmd: cmd, countries: countryService.findAll()]


    }
}

创建一个 GSP 来呈现表单并呈现flash 消息或错误。

grails-app/views/vat/index.gsp
<html>
<head>
    <title>VAT Validator</title>
    <meta name="layout" content="main" />
    <style type="text/css">
        form ol li { list-style-type: none; }
    </style>
</head>
<body>
<div id="content" role="main">
    <section class="row colset-2-its">
        <g:if test="${flash.message}">
            <p class="message">${flash.message}</p>
        </g:if>
        <g:if test="${flash.error}">
            <p class="errors">${flash.error}</p>
        </g:if>
        <g:if test="${cmd}">
            <g:hasErrors bean="${cmd}">
                <div class="errors">
                    <g:eachError><p><g:message error="${it}"/></p></g:eachError>
                </div>
            </g:hasErrors>
        </g:if>
        <g:form controller="vat" method="GET">
            <ol>
                <li>
                    <label for="code"><g:message code="vat.country" default="Country"/></label>
                    <g:select id="code" name='code' value="${cmd?.code}"
                              noSelection="${['null':'Select One...']}"
                              from='${countries}'
                              optionKey="code" optionValue="name"></g:select>
                </li>
                <li>
                    <label for="code"><g:message code="vat.vatNumber" default="VAT Number"/></label>
                    <g:textField name="vatNumber" id="vatNumber" vatNumber="${cmd?.vatNumber}"/>
                </li>
                <li>
                    <g:actionSubmit id="submit" value="${message(code:'vat.check', default: 'Check')}" action="validate"/>
                </li>
            </ol>
        </g:form>
    </section>
</div>
</body>
</html>

3.4 功能测试

添加几个 Geb 依赖项。阅读我们的指南使用多个浏览器运行 Grails Geb 功能测试以了解详情。

build.gradle
testCompile "org.grails.plugins:geb"
testCompile "org.mockito:mockito-core"
testCompile "org.seleniumhq.selenium:htmlunit-driver:2.35.1"
testRuntime 'net.sourceforge.htmlunit:htmlunit:2.35.0'
testCompile "org.seleniumhq.selenium:selenium-remote-driver:3.141.59"
testCompile "org.seleniumhq.selenium:selenium-api:3.141.59"
testCompile "org.seleniumhq.selenium:selenium-support:3.141.59"
testRuntime "org.seleniumhq.selenium:selenium-chrome-driver:3.141.59"
testRuntime "org.seleniumhq.selenium:selenium-firefox-driver:3.141.59"

确保您可以将系统属性传递给 Gradle 任务 integrationTest。您可以通过系统属性 geb.env 提供 geb 环境。

build.gradle
integrationTest {
    systemProperties System.properties
}

创建一个 Geb 页面来封装表单标记。Geb 页面的使用使我们的测试更容易维护。

src/integration-test/groovy/demo/VatFormPage.groovy
package demo

import geb.Page

class VatFormPage extends Page {

    static url = '/vat/index'

    static content = {
        vatNumberInput { $('input#vatNumber', 0) }
        codeSelect { $('select#code', 0) }
        submitButton { $('input#submit', 0) }
    }

    void validate(String code, String vatNumber ) {
        vatNumberInput = vatNumber
        codeSelect = code.toUpperCase()
        submitButton.click()
    }
}

创建一个带有不同值的表单的功能测试

src/integration-test/groovy/demo/VatControllerSpec.groovy
package demo

import geb.spock.GebSpec
import grails.testing.mixin.integration.Integration
import spock.lang.Unroll

@Integration
class VatControllerSpec extends GebSpec {

    @Unroll
    def "#memberState : #vatNumber validation #unrollDescription when you submit the form"(String memberState, String vatNumber, Boolean expected, String unrollDescription) {

        when:
        VatFormPage page = browser.to VatFormPage
        page.validate(memberState, vatNumber)

        then:
        if ( expected) {
            browser.driver.pageSource.contains("${memberState} : ${vatNumber} is valid")
        } else {
            browser.driver.pageSource.contains("${memberState} : ${vatNumber} is NOT valid")
        }

        where:
        memberState | vatNumber   || expected
        'ES'        | 'B99286353' || true
        'ES'        | 'B19280031' || true
        'ES'        | 'XXXXXXXXX' || false

        unrollDescription = expected ? 'is successful' : ' fails'
    }
}

4 Grails 帮助

Object Computing, Inc.(OCI)赞助本指南的创建。它提供了各种咨询和支持服务。

OCI 是 Grails 的家

认识团队