Статьи

Создание первых контрактных веб-сервисов с помощью Spring WS

1. Введение

В этой статье объясняется, как реализовать и протестировать веб-сервис SOAP с помощью
Проект Spring Web Services . Этот пример использует JAXB2 для (не) маршаллинга. Для разработки сервиса я буду использовать контракт-сначала, который заключается в том, чтобы сначала определить контракт на обслуживание, и на основе этого контракта внедрить сервис.

Статья состоит из следующих разделов:

  • 2 Объясняя приложение
  • 3 Реализация сервиса
  • 3.1 Создание договора
  • 3.2 Генерация классов Java
  • 3.3 Внедрение конечной точки SOAP
  • 3.4 Настройка приложения
  • 4 Тестирование сервиса
  • 5 Дополнительная информация
  • 5.1 Реализация клиента
  • 5.2 Как это работает внутри

2 Объясняя приложение

Пример приложения обрабатывает заказы. У нас есть фронт-контроллер (сервлет messageDispatcher), который будет обрабатывать запросы на заказ, вызывать сервис для обработки заказа и возвращать результат.

aplicacio

Вы можете получить исходный код на GitHub .

3 Реализация сервиса

3.1 Создание договора

Поскольку мы будем использовать подход, основанный на контракте, самый простой способ создать контракт — это сначала определить образцы XML-документов, и из этого мы сгенерируем контракт с помощью инструмента. Ниже приведены примеры XML-документов:

клиент-request.xml

1
2
3
4
    clientId="A-123"
    productId="C5FH"
    quantity="5" />

клиент-response.xml

1
2
3
4
    confirmationId="7890B"
    orderDate="2013-09-22"
    amount="15.50" />

Чтобы создать схему, мы можем использовать Trang, инструмент с открытым исходным кодом, который позволит нам генерировать схему xsd из документов xml. Я включил эту библиотеку в путь сборки проекта (вы можете получить этот jar с веб-сайта Trang) и создал задачу Ant для выполнения преобразования:

генерировать-schema.xml

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
<project name="Ant-Generate-Schema-With-Trang" default="generate" basedir=".">
    <property name="src.dir" location="src" />
    <property name="trang.dir" location="lib" />
    <property name="source.dir" location="${src.dir}/main/webapp/WEB-INF/schemas/samples" />
    <property name="schema.dir" location="${src.dir}/main/webapp/WEB-INF/schemas/xsd" />
     
    <target name="generate" description="generates order schema">
        <delete dir="${schema.dir}" />
        <mkdir dir="${schema.dir}" />
         
        <java jar="${trang.dir}/trang.jar" fork="true">
            <arg value="${source.dir}/client-request.xml" />
            <arg value="${schema.dir}/client-request.xsd" />
        </java>
         
        <java jar="${trang.dir}/trang.jar" fork="true">
            <arg value="${source.dir}/client-response.xml" />
            <arg value="${schema.dir}/client-response.xsd" />
        </java>
    </target>
</project>

Как только задача Ant будет выполнена, она сгенерирует схемы. Поскольку схемы были сгенерированы автоматически, возможно, нам необходимо внести некоторые изменения, чтобы адаптировать их к нашим потребностям. Давайте взглянем:

клиент-request.xsd

01
02
03
04
05
06
07
08
09
10
11
12
13
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
    elementFormDefault="qualified"
     
    <xs:element name="clientDataRequest">
        <xs:complexType>
            <xs:attribute name="clientId" use="required" type="xs:NCName"/>
            <xs:attribute name="productId" use="required" type="xs:NCName"/>
            <xs:attribute name="quantity" use="required" type="xs:integer"/>
        </xs:complexType>
    </xs:element>
</xs:schema>

клиент-response.xsd

01
02
03
04
05
06
07
08
09
10
11
12
13
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
    elementFormDefault="qualified"
     
    <xs:element name="clientDataResponse">
        <xs:complexType>
            <xs:attribute name="amount" use="required" type="xs:decimal"/>
            <xs:attribute name="confirmationId" use="required" type="xs:NMTOKEN"/>
            <xs:attribute name="orderDate" use="required" type="xs:NMTOKEN"/>
        </xs:complexType>
    </xs:element>
</xs:schema>

Мы можем добавить различные проверки к этим схемам, но в этом примере я просто изменю несколько типов, таких как clientId, productId и translationId (xs: string) и orderDate (xs: date). Преобразование типов данных XML в типы Java выполняется JAXB. Вы можете проверить, какие сопоставления предоставляются
здесь

Чтобы закончить со схемой, мы скопируем элемент response в схему запроса. Я создал третью схему с ответом и запросом:

клиент-service.xsd

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
    elementFormDefault="qualified" targetNamespace="http://www.xpadro.spring.samples.com/orders"
     
    <xs:element name="clientDataRequest">
        <xs:complexType>
            <xs:attribute name="clientId" use="required" type="xs:string" />
            <xs:attribute name="productId" use="required" type="xs:string" />
            <xs:attribute name="quantity" use="required" type="xs:integer" />
        </xs:complexType>
    </xs:element>
     
    <xs:element name="clientDataResponse">
        <xs:complexType>
            <xs:attribute name="amount" use="required" type="xs:decimal" />
            <xs:attribute name="confirmationId" use="required" type="xs:string" />
            <xs:attribute name="orderDate" use="required" type="xs:date" />
        </xs:complexType>
    </xs:element>
</xs:schema>

Последний шаг состоит в написании контракта, обычно выражаемого в виде файла WSDL. Если вы не хотите создавать его вручную, проект Spring-ws предоставляет нам способ создания этого файла из схемы XSD. Мы будем использовать этот второй подход, как вы увидите в разделе настройки приложения .

3.2 Генерация классов Java

Мы будем использовать JAXB2 для генерации объектов запроса и ответа. Компилятор XJC из JAXB будет отвечать за преобразование этих объектов из схемы XSD, которую мы создали ранее. Это будет выполнено как задача Ant:

01
02
03
04
05
06
07
08
09
10
11
<project name="Ant-Generate-Classes-With-JAXB2" default="generate" basedir=".">
    <property name="src.dir" location="src" />
    <property name="java.dir" location="src/main/java" />
    <property name="schema.dir" location="${src.dir}/main/webapp/WEB-INF/schemas/xsd" />
     
    <target name="generate">
        <exec executable="xjc">
            <arg line=" -d ${java.dir} -p xpadro.spring.ws.types ${schema.dir}/client-service.xsd" />
        </exec>
    </target>
</project>

Эта задача создаст классы Java в пакете xpadro.spring.ws.types (может потребоваться обновить проект).

типы

3.3 Внедрение конечной точки SOAP

Конечная точка получает недопустимую полезную нагрузку сообщения и использует эти данные для вызова службы заказа. Затем он вернет ответ службы, который будет передан адаптером конечной точки:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
@Endpoint
public class OrderEndpoint {
    @Autowired
    private OrderService orderService;
     
    @PayloadRoot(localPart="clientDataRequest", namespace="http://www.xpadro.spring.samples.com/orders")
    public @ResponsePayload ClientDataResponse order(@RequestPayload ClientDataRequest orderData) {
        OrderConfirmation confirmation =
            orderService.order(orderData.getClientId(), orderData.getProductId(), orderData.getQuantity().intValue());
         
        ClientDataResponse response = new ClientDataResponse();
        response.setConfirmationId(confirmation.getConfirmationId());
        BigDecimal amount = new BigDecimal(Float.toString(confirmation.getAmount()));
        response.setAmount(amount);
        response.setOrderDate(convertDate(confirmation.getOrderDate()));
         
        return response;
    }
     
    //date conversion
}

Вот краткое описание аннотаций, используемых конечной точкой:

@Endpoint : регистрирует класс как компонент. Таким образом, класс будет обнаружен при сканировании компонента.

@PayloadRoot : регистрирует метод конечной точки в качестве обработчика для запроса. Эта аннотация будет определять, какой тип сообщения запроса может быть обработан методом. В нашем примере он будет получать сообщения, в которых его корневой элемент полезной нагрузки имеет то же пространство имен, которое определено в созданной нами схеме XSD, и его локальное имя — это то, что определено для запроса (clientDataRequest).

@RequestPayload : указывает полезную нагрузку сообщения запроса, которое будет передано в качестве параметра методу.

@ResponsePayload , указывает, что возвращаемое значение используется в качестве полезной нагрузки ответного сообщения.

3.4 Настройка приложения

web.xml
Конфигурация приложения (например, источник данных, TransactionManager …)

1
2
3
4
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:xpadro/spring/ws/config/root-config.xml</param-value>
</context-param>

Загружает контекст приложения

1
2
3
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

Это сервлет, который будет выполнять роль фронт-контроллера для обработки всех вызовов SOAP. Его функция состоит в том, чтобы выводить входящие XML-сообщения на конечные точки, во многом как DispatcherServlet Spring MVC.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<servlet>
    <servlet-name>orders</servlet-name>
    <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:xpadro/spring/ws/config/servlet-config.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
 
<servlet-mapping>
    <servlet-name>orders</servlet-name>
    <url-pattern>/orders/*</url-pattern>
</servlet-mapping>

сервлет-config.xml
Эта конфигурация содержит компоненты инфраструктуры веб-службы.

1
2
3
4
5
6
7
8
9
<!-- Detects @Endpoint since it is a specialization of @Component -->
<context:component-scan base-package="xpadro.spring.ws"/>
 
<!-- detects @PayloadRoot -->
<ws:annotation-driven/>
 
<ws:dynamic-wsdl id="orderDefinition" portTypeName="Orders" locationUri="http://localhost:8081/spring-ws">
    <ws:xsd location="/WEB-INF/schemas/xsd/client-service.xsd"/>
</ws:dynamic-wsdl>

В динамическом wsdl не имеет значения, какое значение вы указали в атрибуте locationUri, потому что он будет обрабатываться MessageDispatcherServlet. Следовательно, wsdl будет доступен по адресу:

HTTP: // локальный: 8081 / весна-WS / заказы / все / orderDefinition.wsdl

4 Тестирование сервиса

В следующем примере создается фиктивный клиент, который будет обращаться к веб-службе:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@ContextConfiguration("classpath:xpadro/spring/ws/test/config/test-server-config.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class TestWebService {
    @Autowired
    ApplicationContext context;
     
    private MockWebServiceClient mockClient;
     
    @Test
    public void testValidOrderRequest() {
        Source requestPayload = new StringSource(
            "<clientDataRequest xmlns='http://www.xpadro.spring.samples.com/orders' " +
            "clientId='123' productId='XA-55' quantity='5'/>");
         
        Source responsePayload = new StringSource(
            "<clientDataResponse xmlns='http://www.xpadro.spring.samples.com/orders' " +
            "amount='55.99' confirmationId='GHKG34L' orderDate='2013-10-26+02:00'/>");
         
        RequestCreator creator = RequestCreators.withPayload(requestPayload);
        mockClient = MockWebServiceClient.createClient(context);
        mockClient.sendRequest(creator).andExpect(ResponseMatchers.payload(responsePayload));
    }
     
    @Test
    public void testInvalidOrderRequest() {
        Source requestPayload = new StringSource(
            "<clientDataRequest xmlns='http://www.xpadro.spring.samples.com/orders' " +
            "clientId='456' productId='XA-55' quantity='5'/>");
         
        Source responsePayload = new StringSource(
            "<SOAP-ENV:Fault xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/'>" +
            "<faultcode>SOAP-ENV:Server</faultcode><faultstring xml:lang='en'>Client [456] not found</faultstring></SOAP-ENV:Fault>");
         
        RequestCreator creator = RequestCreators.withPayload(requestPayload);
        mockClient = MockWebServiceClient.createClient(context);
        mockClient.sendRequest(creator).andExpect(ResponseMatchers.payload(responsePayload));
    }
}

Конфигурационный файл, используемый в этом тесте, довольно прост, содержит только сканирование компонентов службы:

1
2
<context:component-scan base-package="xpadro.spring.ws"/>
<ws:annotation-driven/>

5 Дополнительная информация

5.1 Реализация клиента

Чтобы облегчить клиенту доступ к веб-службе, Spring предоставляет нам класс WebServiceTemplate. Этот класс содержит методы для отправки и получения сообщений, а также использует конвертеры в (не) маршальные объекты.

Я создал тест, который действует как клиент сервиса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
@ContextConfiguration("classpath:xpadro/spring/ws/test/config/client-config.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class TestClient {
    @Autowired
    WebServiceTemplate wsTemplate;
     
    @Test
    public void invokeOrderService() throws Exception {
        ClientDataRequest request = new ClientDataRequest();
        request.setClientId("123");
        request.setProductId("XA-55");
        request.setQuantity(new BigInteger("5", 10));
         
        ClientDataResponse response = (ClientDataResponse) wsTemplate.marshalSendAndReceive(request);
         
        assertNotNull(response);
        assertEquals(new BigDecimal("55.99"), response.getAmount());
        assertEquals("GHKG34L", response.getConfirmationId());
    }
}

Файл теста конфигурации содержит конфигурацию WebServiceTemplate:

1
2
3
4
5
6
7
<oxm:jaxb2-marshaller id="marshaller" contextPath="xpadro.spring.ws.types"/>
 
<bean class="org.springframework.ws.client.core.WebServiceTemplate">
    <property name="marshaller" ref="marshaller" />
    <property name="unmarshaller" ref="marshaller" />
    <property name="defaultUri" value="http://localhost:8081/spring-ws/orders" />
</bean>

Просто не забудьте запустить сервер с развернутым приложением веб-службы перед выполнением этого теста.

5.2 Как это работает внутри

Если вы просто хотите внедрить веб-сервис, статья заканчивается в предыдущем разделе. Для тех, кому интересно, как это действительно работает, я попытаюсь объяснить, как запрос отображается на конечную точку, чуть более низкоуровнево, чем объяснялось до этого момента.

Когда запрос приходит к MessageDispatcher, он опирается на два компонента:

  1. Он запрашивает EndpointMapping, который является подходящей конечной точкой.
  2. С информацией, полученной из сопоставления, он использует адаптер конечной точки для вызова конечной точки. Адаптер также поддерживает преобразователи аргументов и обработчики возвращаемых типов.

Отображение конечной точки

MessageDispatcher содержит список отображений конечных точек, каждое из которых содержит карту ранее зарегистрированных конечных точек метода. В нашем случае отображение JAXB PayloadRootAnnotationMethodEndpointMapping зарегистрировало все методы, аннотированные @PayloadRoot. Если полное имя полезной нагрузки сообщения разрешается как зарегистрированный метод, оно будет возвращено MessageDispatcher. Если бы мы не аннотировали наш метод, он не смог бы обработать запрос.
sequencia1
Конечный адаптер

Затем MessageDispatcher спросит каждый из своих адаптеров конечных точек, поддерживает ли он текущий запрос. В нашем случае адаптер проверяет, выполняются ли оба следующих условия:

  • По крайней мере один из аргументов, переданных методу, аннотирован @RequestPayload
  • Если метод конечной точки возвращает ответ, он должен быть аннотирован @ResponsePayload

Если адаптер возвращается, он вызывает конечную точку, демаршируя параметр перед передачей его методу. Когда метод возвращает ответ, адаптер выполняет его маршализацию.

Следующая диаграмма — значительно сокращенная версия этого шага, чтобы сделать его простым:

sequencia2

Вывод

Мы увидели введение о том, как реализовать простой веб-сервис и затем протестировать его. Если вам интересно, вы также можете посмотреть, как протестировать клиентскую часть с помощью MockWebServiceServer.