К основному контенту

JAX-WS + Spring-WS в одном флаконе


Описание проблемы и основные понятия

 Для реализации классических SOAP веб-сервисов spring предлагает хороший фреймворк spring-ws. Он использует так называемую схему "contract-first", т.е. сначала необходимо определить схему используемых сообщений, а затем уже создать под нее требуемые java-классы. Обычно этим занимаются кодогенераторы, которые создадут классы по схеме.
Однако существуют и другие фреймворки, которые позволяют создавать веб-сервисы. В том числе и исторически более ранний стандарт JAX-WS. Это именно стандарт, т.е. по факту набор апи, реализацию которого предоставляют различные платформы. Например, реализация Metro, которая есть в Glassfish. В самом проекте эта реализация в качестве зависимости не притягивается - нужен только апи. Более того, это апи содержится в поставке JDK, поэтому объявлять что-то дополнительно не нужно совсем. Деплоить такой проект нужно в такой контейнер, в котором имеется эта реализация. Контейнер подцепляет объявленные веб-сервисы и они становятся доступными. Томкат такой реализации в совем составе не имеет, поэтому такой проект задеплоить в нем не получится.
 Тем не менее проблема даже не в том, что томкат нельзя использовать, а в том, что из-за своего особого жизненного цикла, которым рулит контейнер, spring не управляет этими бинами и соответственно недоступна инжекция и прочие радости жизни спринга.
JAX-WS (и вариант для REST JAX-RS) сам по себе не так уж плох - в нем хватает аннотаций, чтоб разметить веб-сервис любой сложности, и они достаточно удобны. Проблем с ними не возникнет, если весь проект построен на JavaEE со своим DI, EJB и прочими плюшками, идущими из коробки. Тем не менее сейчас гораздо более популярным решением является spring со своим не менее большим стеком проектов на все случаи жизни и неудивительно, если захочется перейти на него.  Однако мигрировать на spring-ws для крупного проекта может быть затратным по времени, особенно если использовался подход code-first, т.е. по имеющимся доменам генерируется wsdl. Один из вариантов оставить JAX-WS в строю и при этом пользоваться spring-ws с новыми веб-сервисами уже на новом фреймфорке описывается дальше в этой статье.

JAX-WS и Spring

Основной задачей интеграции JAX-WS сервисов в spring-ws-проект является именнно добавление классов сервисов в контекст спринга. Сделать это можно с помощью отдельной либы jaxws-spring.Этот вариант был взят из статьи вездесущего mkyong: https://www.mkyong.com/webservices/jax-ws/jax-ws-spring-integration-example. Там достаточно подробно все описывается и можно для простого случая делать именно по ней, но здесь я приведу еще некоторые комментарии к этой реализации для большего понимания, что и зачем вообще делается.
В статье используется xml-конфигурация веб-приложения и спринга, т.к. конфигурацию веб-сервисов в либе проще производить именно в xml-формате. Но если есть желание, то всегда эту часть можно изолировать и сконфигурировать все остальное в java-конфиге.
Итак, пусть для примера у нас есть интерфейс JAX-WS веб-сервиса:
@WebService(name = "IncomeWS")
@SOAPBinding(parameterStyle = SOAPBinding.ParameterStyle.BARE)
public interface IncomeWS {
    @WebMethod
    GetYearIncomeResponse getYearIncome(GetYearIncomeRequest request);
}
Содержание реквеста и респонса здесь не играет никакой роли.
Реализация этого сервиса такая:
@WebService(endpointInterface = "org.salamansar.common.ws.IncomeWS",
            serviceName = "IncomeWS",
            portName = "IncomeWSPort")
public class IncomesWSEndpoint implements IncomeWS {
    @Autowired
    private IncomeService service; //спринговый сервис, который мы хотим использовать
        @Override
        public GetYearIncomeResponse getYearIncome(GetYearIncomeRequest request) {
            ...//использование сервиса и формирование респонса
    }

}
Реализация спрингового сервиса так же тут не имеет значения.
Чтобы это все заработало, необходимо во-первых добавить бин реализации веб-сервиса в спринг-контекст, а во-вторых обеспечить работу веб-сервиса, чтобы он мог обрабатывать сообщения.
Для 1-го пункта создаем конфиг applicationContext.xml, в котором перечисляем нужные бины (естественно можно воспользоваться автосканом, но тогда не забудьте пометить эндпоинт какой-нибудь аннотацией):
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context" 
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <context:annotation-config/>

    <bean name="incomeService" class="org.salamansar.common.ws.IncomeService"/>
    <bean name="incomeEndpoint" class="org.salamansar.spring.IncomesWSEndpoint"/>

</beans>

Примечание: не збываем включать <context:annotation-config/>, чтобы работала инжекция через @Autowired. Если об этом забыть, при старте не будет ошибок, но при обращении к сервису вылетит NPE.
После чего добавляем листенер в web.xml для загрузки контекста:
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
  version="3.1">
 <listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 </listener>
</web-app>

Пункт 2 реализуется как раз через сервлет диспатчера библиотеки jaxws-spring. Для этого подключаем ее к проекту (актуальную версию можно взять с сайта maven-репозитория):
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.4</version>
</dependency> 
Добавляем конфигурацию для веб-сервиса в контекст applicationContext.xml (изменения выделены жирным):
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context" 
    xmlns:wss="http://jax-ws.dev.java.net/spring/servlet"
    xmlns:ws="http://jax-ws.dev.java.net/spring/core"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
                        http://jax-ws.dev.java.net/spring/core http://jax-ws.dev.java.net/spring/core.xsd
   http://jax-ws.dev.java.net/spring/servlet http://jax-ws.dev.java.net/spring/servlet.xsd">

    <context:annotation-config/>

    <wss:binding url="/incomes-jaxws/IncomesWS">
        <wss:service>
            <ws:service bean="#incomeEndpoint" />
        </wss:service>
    </wss:binding>

    <bean name="incomeService" class="org.salamansar.common.ws.IncomeService"/>
    <bean name="incomeEndpoint" class="org.salamansar.spring.IncomesWSEndpoint"/>

</beans>
Здесь атрибут url, как можно догадаться по его названию, определяет адрес от рута приложения в контейнере. Далее необходим диспатчер, который доставит сообщения к компоненту. Для этого необходимо добавить сервлет:
<servlet>
    <servlet-name>jaxws-spring</servlet-name>
    <servlet-class>com.sun.xml.ws.transport.http.servlet.WSSpringServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>jaxws-spring</servlet-name>
    <url-pattern>/incomes-jaxws/*</url-pattern>
</servlet-mapping>
Тут следует обратить внимание как раз на этот путь "/incomes-jaxws/*". Нужно следить, чтобы все веб-сервисы, объявленые в контексте выше, принадлежали этому пути.

После этого веб-сервис можно деплоить на томкат и он станет доступен по указанному относительному адресу
/incomes-jaxws/IncomesWS. Скачать wsdl можно через url /incomes-jaxws/IncomesWS?wsdl.

Использование JAX-WS и Spring-WS в одном проекте

Итак, предположим появилась потребность добавить в проект веб-сервисы, но уже на spring-ws. Делается это ровным счетом так же, как и для проекта без JAX-WS конфига с единственным исключением - диспатчер должен обслуживать свой путь и соответственно в клиентском урле так же должен быть этот путь Так же рекомендую использовать схему с рутовым и дочерними конфигами. В этом случае бины, относящиеся только к spring-ws, будут в отдельном контексте и зависеть от рутового контекста, который в свою очередь так же используется и для JAX-WS веб-сервисов.
Для начала добавляем необходимые зависимости:
<dependency>
    <groupId>org.springframework.ws</groupId>
    <artifactId>spring-ws-core</artifactId>
    <version>3.0.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>wsdl4j</groupId>
    <artifactId>wsdl4j</artifactId>
    <version>1.6.2</version>
    <scope>runtime</scope>
</dependency>
Далее следует создать xsd, которая содержит схему используемых сообщений. Содержание ее так же не имеет здесь значения. В данном примере я поместил ее в папку src/main/webapp/WEB-INF/xsd и назвал IncomesWS.xsd.  
Примечание: Хранить xsd не обязательно в WEB-INF - это может быть отдельная папка, но при сборке нужно будет перенести в WEB-INF, чтобы у конфига wsdl, который мы добавим дальше, был к ним доступ. 
Для генерации нужных классов, добавляем плагин xjc:
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>jaxb2-maven-plugin</artifactId>
    <version>2.3.1</version>
    <executions>
        <execution>
            <goals>
                <goal>xjc</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <sources>
            <source>src/main/webapp/WEB-INF/xsd</source>
        </sources>
    </configuration>
</plugin>
Создаем эндпоинт с использованием сгенерированных классов:
@Endpoint
public class SpringIncomesWSEndpoint {
    @Autowired
    private IncomeService service;

    @PayloadRoot(localPart = "getYearIncomeRequest", namespace = "http://salamansar.org/springws/incomes")
    @ResponsePayload
    public GetYearIncomeResponse getYearIncome(@RequestPayload GetYearIncomeRequest request) {
        ... //реализация эндпоинта
    }
 
}
IncomeService здесь тот же самый, который используется для jax-ws веб-сервиса. Этот сервис располагается в рутовом контексте, а для spring-ws предлагаю создать новый incomes-ws-servlet.xml, в котором будет объявлен наш эндпоинт и сконфигурирована wsdl.
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:sws="http://www.springframework.org/schema/web-services"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
   http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
   http://www.springframework.org/schema/web-services http://www.springframework.org/schema/web-services/web-services-2.0.xsd">

    <context:annotation-config/>
    <sws:annotation-driven/>
    
    <sws:dynamic-wsdl id="IncomesWS" 
                         portTypeName="IncomesWS" 
    locationUri="/incomes-spring/IncomesWS">
        <sws:xsd location="WEB-INF/xsd/IncomesWS.xsd" />
    </sws:dynamic-wsdl>
 
    <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory" />
    <bean id="springIncomesWSEndpoint" class="org.salamansar.spring.SpringIncomesWSEndpoint" />
</beans>
Элемент dynamic-wsdl по сути нужен только для того, чтобы сконструировать wsdl и ее можно было бы скачать. Так что если этого не требуется, то весь этот блок можно не добавлять.
Примечание: бин messageFactory нужно определять обязательно - он делает доступным маршаллинг из POJO-объекта и обратно. Сам маршаллер определять не нужно - по дефолту создастся Jaxb2Marshaller.
Обратите внимание - сам IncomeService не определяется в этом конфиге, а так же нет никаких импортов конфигов. Но этот сервис определен в рутовом конфиге и все корректно подтянется при загрузке иерархичного контекста.
Осталось добавить спринговый диспатчер в web.xml, который доставит сообщение к эндпоинту.
<servlet>
    <servlet-name>incomes-ws</servlet-name>
    <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class>
    <init-param>
        <param-name>transformWsdlLocations</param-name>
        <param-value>true</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>incomes-ws</servlet-name>
    <url-pattern>/incomes-spring/*</url-pattern>
</servlet-mapping>
Название конфига (incomes-ws-servlet.xml) и сервлета не случайно частично совпадают. По умолчанию spring ищет конфиг по шаблону <имя_сервлета>-servlet.xml, что и было сделано в этом примере. При желании, конечно, можно явно задать путь к конфигу для диспатчера.

 После проведения всех указанных выше манипуляций, можно деплоить приложение в томкате, где будут доступны обе версии веб-сервисов по своим определенным урлам. Скачать WSDL для спрингового сервиса можно через урл incomes-spring/IncomesWS.wsdl. Реквесты же будут отрабатывать по любому урлу под incomes-spring/*, т. к. был настроен диспатчеринг по телу сообщения (через @PayloadRoot).

Комментарии