Этот пост основан на том, что я написал несколько лет назад о первых контрактных веб-сервисах с Apache CXF и Spring. В предыдущем посте Spring Boot не использовалась, и большинство настроек Spring и CXF были через XML. Этот пост немного продвигает вперед, используя последнюю версию CXF и Spring Boot.
Образец приложения
Мы собираемся создать простое приложение Spring Boot, которое предоставляет веб-сервис SOAP с использованием Apache CXF. Служба будет иметь одну операцию, которая берет номер счета и возвращает реквизиты банковского счета. Если вы нетерпеливы и хотите выйти вперед, вы можете получить полный исходный код из GitHub .
Определение модели данных
При создании веб-сервисов я всегда использую контракт в первую очередь. Это означает определение контракта на обслуживание как WSDL перед написанием реализации сервиса. Мы начнем этот процесс с создания файла XSD с типами, которые будет использовать служба учетных записей. Первый тип, который мы создадим, Account
— диаграмма ниже показывает фрагмент XSD для, Account
а также визуальное представление, взятое из XML Spy. Примечание : вам не нужен XML Spy для определения XSD, но визуальный редактор может быть очень полезен, если вы разрабатываете сложные доменные модели.
Далее мы определим тип запроса для сервиса, который инкапсулирует параметры сервиса. В этом случае у нас есть только один параметр, accountNumber
но рекомендуется определить тип запроса как удобный способ оборачивания нескольких параметров и поддерживать интерфейс в чистоте.
Наконец, мы определим тип ответа, AccountDetailsResponse
который представляет собой простую оболочку, которую Account
мы создали ранее. Опять же, мы могли бы просто вернуться, Account
но я думаю, что в общем случае рекомендуется использовать оболочки для типа ответа, поскольку это означает, что в будущем мы можем легко добавить другие данные в оболочку ответа.
Полное определение схемы показано ниже.
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://com/blog/samples/webservices/accountservice" xmlns:account="http://webservices.samples.blog.com" targetNamespace="http://com/blog/samples/webservices/accountservice" elementFormDefault="qualified">
<xsd:complexType name="Account">
<xsd:sequence>
<xsd:element name="AccountNumber" type="xsd:string"/>
<xsd:element name="AccountName" type="xsd:string"/>
<xsd:element name="AccountBalance" type="xsd:double"/>
<xsd:element name="AccountStatus" type="EnumAccountStatus"/>
</xsd:sequence>
</xsd:complexType>
<xsd:simpleType name="EnumAccountStatus">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="Active"/>
<xsd:enumeration value="Inactive"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:element name="AccountDetailsRequest">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="accountNumber" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="AccountDetailsResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="AccountDetails" type="Account"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
Определение службы WSDL
Теперь, когда мы определили типы сервисов через XSD, пришло время создать WSDL для определения открытого контракта на обслуживание. WSDL — это XML-документ, описывающий веб-службу SOAP и способы взаимодействия с ней клиентов. WSDL службы учетных записей определяется следующим образом.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<wsdl:definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap12/"
xmlns:tns="http://www.briansjavablog.com/Accounts/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="Accounts"
targetNamespace="http://www.briansjavablog.com/Accounts/"
xmlns:accounts="http://com/blog/samples/webservices/accountservice">
<wsdl:types>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:import namespace="http://com/blog/samples/webservices/accountservice"
schemaLocation="../schema/AccountsService.xsd">
</xsd:import>
</xsd:schema>
</wsdl:types>
<wsdl:message name="AccountDetailsRequest">
<wsdl:part element="accounts:AccountDetailsRequest" name="parameters" />
</wsdl:message>
<wsdl:message name="AccountDetailsResponse">
<wsdl:part element="accounts:AccountDetailsResponse" name="parameters" />
</wsdl:message>
<wsdl:portType name="Accounts">
<wsdl:operation name="GetAccountDetails">
<wsdl:input message="tns:AccountDetailsRequest" />
<wsdl:output message="tns:AccountDetailsResponse" />
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="AccountsServiceSoapBinding" type="tns:Accounts">
<soap:binding style="document"
transport="http://schemas.xmlsoap.org/soap/http" />
<wsdl:operation name="GetAccountDetails">
<soap:operation
soapAction="http://www.briansjavablog.com/Accounts/GetAccountDetails" />
<wsdl:input>
<soap:body use="literal" />
</wsdl:input>
<wsdl:output>
<soap:body use="literal" />
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="AccountsService">
<wsdl:port binding="tns:AccountsServiceSoapBinding" name="AccountsPort">
<soap:address
location="http://localhost:8080/apache-cfx-demo/services/accounts" />
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
WSDL содержит 5 ключевых частей информации:
- Типы — <wsdl: types> определяет модель домена, используемую службой. Модель определяется с помощью XSD и может быть встроена в WSDL или импортирована из отдельного XSD. Строка 9 выше импортирует файл XSD, который мы создали ранее.
- Message — <wsdl: message> определяет сообщения запроса и ответа, используемые сервисом. Вложенный раздел <wsdl: part> определяет типы доменов сообщений запроса и ответа.
- PortType — <wsdl: portType> определяет операции службы, параметры и типы ответов, предоставляемые клиентам.
- Binding — <wsdl: binding> определяет протокол и формат данных.
- Атрибут типа привязки ссылается на portType, определенный ранее в WSDL.
- Стиль связывания мыла может быть либо RPC, либо документом.
- Атрибут транспорта указывает, что сервис будет доступен через HTTP. Другие варианты (менее распространенные) включают JMS и SMTP.
- Элемент операции определяет каждую операцию, которую мы представили через portType.
- Binding — <wsdl: binding> определяет протокол и формат данных.
- Service — <wsdl: service> определяет предоставляемый сервис, используя
portType
привязку и, которую мы определили выше.
Генерация сервисного интерфейса и модели предметной области
На данный момент у нас есть WSDL, который определяет контракт для сервиса, который мы собираемся построить. Следующим шагом является использование WSDL для генерации классов Java для модели домена и интерфейса службы. Мы собираемся использовать плагин codegen CXF для запуска задания WSDL2Java как части сборки. Плагин codegen определяется в проекте POM следующим образом.
<plugin>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-codegen-plugin</artifactId>
<executions>
<execution>
<id>generate-sources</id>
<phase>generate-sources</phase>
<configuration>
<sourceRoot>src/generated/java</sourceRoot>
<wsdlOptions>
<wsdlOption>
<wsdl>${basedir}/src/main/resources/wsdl/Accounts.wsdl</wsdl>
</wsdlOption>
</wsdlOptions>
</configuration>
<goals>
<goal>wsdl2java</goal>
</goals>
</execution>
</executions>
</plugin>
sourceRoot
Элемент говорит плагин , чтобы поместить сгенерированные классы Java в новом каталоге источник Src / сгенерированного / Java. Это отделяет генерирующие классы от классов, которые мы напишем сами. В wsdl
точках элемента в файл WSDL мы создали ранее.
Для запуска генерации кода откройте командное окно и запустите mvn generate-sources
.
Обновите рабочее пространство IDE, и вы увидите два новых пакета. По умолчанию имена пакетов основаны на пространствах имен в WSDL. Содержимое обоих пакетов описано ниже.
- com.blog.demo.webservices.accountservice — содержит четыре объекта домена,
Account
,AccountDetailsRequest
,AccountDetailsResponse
иEnumAccountStatus
. Это основные типы, которые мы определили в XSD ранее, и они станут основными строительными блоками службы. - com.briansdevblog.accounts — содержит интерфейс конечной точки службы
AccountService
. Этот интерфейс является представлением Java операции сервиса, определенной в WSDL.AccountService_Service.
является клиентом веб-службы и может использоваться для вызова службы.
Интерфейс конечной точки сервиса
Сгенерированный интерфейс конечной точки службы AccountService
содержит метод для каждой операции, определенной в WSDL. Как и следовало ожидать, параметр метода и тип возвращаемого значения определены в XSD и указаны в WSDL.
@WebService(targetNamespace = "http://www.briansdevblog.com/Accounts/", name = "AccountService")
@XmlSeeAlso({com.blog.demo.webservices.accountservice.ObjectFactory.class})
@SOAPBinding(parameterStyle = SOAPBinding.ParameterStyle.BARE)
public interface AccountService {
@WebMethod(operationName = "GetAccountDetails", action = "http://www.briansjavablog.com/Accounts/GetAccountDetails")
@WebResult(name = "AccountDetailsResponse", targetNamespace = "http://com/blog/demo/webservices/accountservice", partName = "parameters")
public com.blog.demo.webservices.accountservice.AccountDetailsResponse getAccountDetails(
@WebParam(partName = "parameters", name = "AccountDetailsRequest", targetNamespace = "http://com/blog/demo/webservices/accountservice")
com.blog.demo.webservices.accountservice.AccountDetailsRequest parameters
);
}
- @WebService — помечает класс как определяющий интерфейс веб-службы из WSDL. Пространство имен должно соответствовать пространству имен, определенному в WSDL, а имя должно соответствовать типу порта WSDL.
- @XmlSeeAlso — позволяет JAXB знать, какие другие классы необходимо зарегистрировать в контексте JAXB для сериализации и десериализации.
- @SoapBinding — описывает сопоставление операций веб-службы протоколу SOAP.
- @WebMethod — сопоставляет операцию службы с методом Java. Имя операции ссылается на операцию, определенную в WSDL, а целевое пространство имен использует пространство имен, связанное с операцией WSDL.
- @WebResult — сопоставляет ответное сообщение операции службы с типом возврата Java. Имя относится к имени ответного сообщения, определенного в WSDL. Целевое пространство имен использует пространство имен, связанное с сообщением WSDL, и
partName
ссылку наwsdl:part
имя в WSDL. - @WebParam — сопоставляет сообщение запроса операции службы с типом параметра Java. Имя относится к имени сообщения запроса, определенного в WSDL. Целевое пространство имен использует пространство имен, связанное с сообщением WSDL, и
partName
ссылку наwsdl:part
имя в WSDL.
Написание конечной точки службы
Далее мы собираемся создать конечную точку службы путем реализации AccountService
интерфейса.
@Service
public class AccountServiceEndpoint implements AccountService {
@Override
public AccountDetailsResponse getAccountDetails(AccountDetailsRequest parameters) {
ObjectFactory factory = new ObjectFactory();
AccountDetailsResponse response = factory.createAccountDetailsResponse();
Account account = factory.createAccount();
account.setAccountNumber("12345");
account.setAccountStatus(EnumAccountStatus.ACTIVE);
account.setAccountName("Joe Bloggs");
account.setAccountBalance(3400);
response.setAccountDetails(account);
return response;
}
}
Конечная точка реализует getAccountDetails
метод на AccountService
интерфейсе. Для простоты тело метода возвращает некоторые жестко закодированные Account
данные, помещенные в AccountDetailsResponse
.
Настройка сервиса с помощью Spring
Раньше мы настраивали конечную точку службы через XML, но теперь, когда мы используем Spring Boot, мы можем переместить всю нашу конфигурацию на Java. Конфигурация XML была в порядке, но трудно превзойти настройку Spring с Java. ApplicationConfig
Класс ниже содержит все настройки , необходимые для запуска службы. Я опишу каждый боб ниже.
@Configuration
public class ApplicationConfig {
@Bean
public ServletRegistrationBean<CXFServlet> dispatcherServlet() {
return new ServletRegistrationBean<CXFServlet>(new CXFServlet(), "/soap-api/*");
}
@Bean
@Primary
public DispatcherServletPath dispatcherServletPathProvider() {
return () -> "";
}
@Bean(name=Bus.DEFAULT_BUS_ID)
public SpringBus springBus(LoggingFeature loggingFeature) {
SpringBus cxfBus = new SpringBus();
cxfBus.getFeatures().add(loggingFeature);
return cxfBus;
}
@Bean
public LoggingFeature loggingFeature() {
LoggingFeature loggingFeature = new LoggingFeature();
loggingFeature.setPrettyLogging(true);
return loggingFeature;
}
@Bean
public Endpoint endpoint(Bus bus, AccountServiceEndpoint accountServiceEndpoint) {
EndpointImpl endpoint = new EndpointImpl(bus, accountServiceEndpoint);
endpoint.publish("/service/accounts");
return endpoint;
}
}
ServletRegistrationBean<CXFServlet>
— регистрирует сервлет диспетчера CXF для обработки входящих HTTP-запросов к / soap-api / *. Сервлет-диспетчер по существу направляет запросы в конечную точку для обработки.SpringBus
— это CXF со вкусом весныBus
. ABus
является основной точкой расширения CXF, которая позволяет добавлять перехватчики к любому клиенту CXF или конечной точке, которая использует шину. В приведенном выше примере мы добавляем инъекцию,LoggingFetaure
чтобы включить ведение журнала.LoggingFeature
— AFeature
— это то, что добавляет функциональность клиенту или серверу CXF. В этом случаеLoggingFeature
выполняется регистрация входящей и исходящей полезной нагрузки SOAP. Я включил красивое ведение журнала, чтобы сделать сообщения SOAP немного более читабельными.Endpoint
— предоставляет конечную точку HTTP для обработки входящих запросов SOAP. Метод publish говорит CXF опубликовать конечную точку в / service / accounts. Этот путь будет добавлен к шаблону / soap-api / *, используемому для настройкиCXFServlet
ранее. Это означает, что конечная точка выставлена в CONTEXT_ROOT / soap-api / service / account.
Написание интеграционного теста
Далее мы собираемся написать интеграционный тест, чтобы убедиться, что все работает как положено. Тест будет делать следующее:
- Используйте Jetty, чтобы встать, и экземпляр конечной точки по адресу http: // localhost: 8080 / services / accounts.
- Создайте клиент веб-службы / SOAP-прокси для обработки сериализации запроса и десериализации ответа.
- Создайте
AccountDetailsRequest
, отправьте его в конечную точку SOAP и проверьте содержимоеAccountDetailsResponse
- Сорвите конечную точку теста.
Вы заметите, что тест не использует тот, который ApplicationConfig
мы определили ранее, а использует следующий специфичный для теста конфиг.
@Configuration
@ComponentScan("com.blog.demo.service")
public class TestConfig {
private static final String SERVICE_URL = "http://localhost:8080/services/accounts";
@Bean("accountServiceClient")
public AccountService accountServiceClient() {
JaxWsProxyFactoryBean jaxWsProxyFactoryBean = new JaxWsProxyFactoryBean();
jaxWsProxyFactoryBean.setServiceClass(AccountService.class);
jaxWsProxyFactoryBean.setAddress(SERVICE_URL);
return (AccountService) jaxWsProxyFactoryBean.create();
}
@Bean(name=Bus.DEFAULT_BUS_ID)
public SpringBus springBus(LoggingFeature loggingFeature) {
SpringBus bus = new SpringBus();
bus.getFeatures().add(loggingFeature);
return bus;
}
@Bean
public LoggingFeature loggingFeature() {
LoggingFeature loggingFeature = new LoggingFeature();
loggingFeature.setPrettyLogging(true);
return loggingFeature;
}
@Bean
public Endpoint endpoint(Bus bus, LoggingFeature loggingFeature, AccountServiceEndpoint accountServiceEndpoint) {
EndpointImpl endpoint = new EndpointImpl(bus, accountServiceEndpoint);
endpoint.publish(SERVICE_URL);
return endpoint;
}
accountServiceClient
Метод использует JaxWsProxyFactoryBean
для создания клиента веб — службы для AccountService
службы Endpoint Interface. Клиент настроен для вызова конечной точки по адресу http: // localhost: 8080 / services / accounts . Для того , чтобы встать на тестовый экземпляр в конечной точке, мы также сконфигурируйте SpringBus
, LoggingFeature
и Endpoint
подобно тому, как мы это делали в ApplicationConfig
.
AccountServiceEndpointTest
Ниже , использует впрыскивается accountServiceClient
из TestConfig
.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { TestConfig.class })
public class AccountServiceEndpointTest {
@Autowired
@Qualifier("accountServiceClient")
private AccountService accountsServiceClient;
private AccountDetailsRequest accountDetailsRequest;
@Before
public void setUp() throws Exception {
ObjectFactory objectFactory = new ObjectFactory();
accountDetailsRequest = objectFactory.createAccountDetailsRequest();
accountDetailsRequest.setAccountNumber("12345");
}
@Test
public void testGetAccountDetails() throws Exception {
AccountDetailsResponse response = accountsServiceClient.getAccountDetails(accountDetailsRequest);
assertTrue(response.getAccountDetails()!= null);
assertTrue(response.getAccountDetails().getAccountNumber().equals("12345"));
assertTrue(response.getAccountDetails().getAccountName().equals("Joe Bloggs"));
assertTrue(response.getAccountDetails().getAccountBalance() == 3400);
assertTrue(response.getAccountDetails().getAccountStatus().equals(EnumAccountStatus.ACTIVE));
}
}
Выполнение теста
Вы можете запустить интеграционный тест в вашей IDE или запустить командную строку и запустить mvn test
. Вы увидите запуск Jetty на порту 8080 и полезные нагрузки запросов / ответов SOAP, записанные в журнал при вызове конечной точки.
Запуск в качестве автономной службы
At this point, we’ve run the integration test and everything is behaving as expected. The only thing left to do is to fire up the service with Spring Boot. On the command line run mvn spring-boot:run
. The service should start on port 8090 as shown below.
If you browse to http://localhost:8090/soap-api you’ll see the standard CXF service page with the Account Service listed and a link to the WSDL.
You can now test the service using any standard HTTP client. Below I use Postman to send a POST request to http://localhost:8090/soap-api/service/accounts. The SOAP response is displayed as expected.
Wrapping Up
In this post, we built a SOAP Web Service from scratch using Apache CXF and Spring Boot. We began by defining the data model and WSDL (contract first) and then moved on to implement the service endpoint. We looked at how a CXF endpoint is configured in Spring Boot and also put together a simple integration test. The full source code for this post is available on GitHub. As always, feel free to post comments or questions below.