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 如何完成该指南
若要开始,请执行以下操作
-
下载并解压源代码
或
-
克隆 Git 仓库
git clone https://github.com/grails-guides/grails-soap.git
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 依赖项
compile 'com.github.groovy-wslite:groovy-wslite:1.1.3'
我们将 SOAP 代码封装到 Grails 服务中
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 测试框架对它进行测试
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 中添加一个国家/地区列表
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
package demo
import groovy.transform.CompileStatic
@CompileStatic
class Country {
String code
String name
}
在 Grails 服务中读取国家/地区
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 控制器和视图
此应用程序允许用户提交表单并检查增值税号对于特定国家/地区是否有效。
我们将请求封装到一个命令对象中
package demo
import grails.validation.Validateable
class VatCommand implements Validateable {
String code
String vatNumber
static constraints = {
code nullable: false
vatNumber nullable: false
}
}
创建一个 VatController 来处理该请求,与之前引入的服务协作,并通过flash 消息或错误生成响应。
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 消息或错误。
<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 功能测试以了解详情。
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 环境。
integrationTest {
systemProperties System.properties
}
创建一个 Geb 页面来封装表单标记。Geb 页面的使用使我们的测试更容易维护。
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()
}
}
创建一个带有不同值的表单的功能测试
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'
}
}