Статьи

Веб-сервисы SOAP с Apache CXF и Spring Boot

Этот пост основан на том, что я написал несколько лет назад о первых контрактных веб-сервисах с 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но рекомендуется определить тип запроса как удобный способ оборачивания нескольких параметров и поддерживать интерфейс в чистоте.

AccountDetailsRequest Тип

Наконец, мы определим тип ответа, 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. A Busявляется основной точкой расширения CXF, которая позволяет добавлять перехватчики к любому клиенту CXF или конечной точке, которая использует шину. В приведенном выше примере мы добавляем инъекцию, LoggingFetaureчтобы включить ведение журнала.
  • LoggingFeature— A Feature— это то, что добавляет функциональность клиенту или серверу 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.