среда, 12 марта 2008 г.

Интероперабельный веб-сервис с использованием XFire, Spring и Hibernate

В рамках проходящей в Южно-Уральском государственном университете конференции "ITFest" был проведен эксперимент по интеграции приложений написаных на Java и .NET. Единственный способ интеграции столь разнородных платформ - веб-сервисы. Соответственно был написан веб-сервис на Java и клиент к нему на .NET. Собственно о веб-сервисе, написаном на Java и хочется поговорить.

Чем хороша Java как платформа, так это тем, что в ней удобно писать правильно, в отличие от дельфи и того-же дотнета, который своей организацией подталкивает программиста к неправильным архитектурным решениям и антипаттернам (в частности "Волшебная кнопка"). В Java же наоборот, чем грамотнее организовано приложение, тем удобнее и проще писать. Вот пример такого, на мой взгляд, грамотного приложения хотелось бы привести.


Задание:
Итак, необходимо написать веб-сервис, который является RSS-агрегатором. Пользователь добавляет категории, фиды, а сервис парсит фиды и выдает новости пользователю по запросу. Ну и соответственно поддерживает добавление разделов и фидов.

Используемые технологии:
Для создания уровня представления (собственно веб-сервиса) будем использовать XFire. XFire мне понравился своими богатейшими возможностями кастомизации, простотой использования и интеграцией со Spring (давно хотел поковырять Spring). Ну и конечно основное - XFire создает веб-сервис, полностью удовлетворяющий стандартам w3.org.

Для организации уровня доступа к данным выбран Hibernate. Hibernate - наиболее популярное на сегодняшний день решение для организации ORM. Использование ORM позволит нам наиболее просто манипулировать сущностями в нашем сервисе.

Для связи этих двух уровней используется Spring.  Spring представляет собой IoC-контейнер с набором дополнительных возможностей. Данные возможности касаются организации уровня доступа к данным, уровня бизнес-логики, уровня представления. Собственно об этих возможностях и хочется поговорить.
Пожалуй начнем.

Конфигурируем веб-приложение.
Любой веб-сервис - это прежде всего веб-приложение, обрабатывающее запросы по протоколу HTTP. Чтобы поднять веб-приложение нам необходим application server, например Apache Tomcat. Указания серверу находятся в дескрипторе развертывания веб-приложения - web.xml.

Первое, что необходимо сделать - подключить Spring. Для организации веб-приложений Spring содержит базовый сервлет: org.springframework.web.servlet.DispatcherServlet

Базовый сервлет получает запросы и отправляет их Spring'у, далее запросы уже диспетчиризуются в соответствии с настройками Spring'а. Конфигурирование Spring осуществляется с помощью так называемых бинов. Bean - это описание создания объекта - основное понятие Spring IoC-контейнера. С точки зрения синтаксиса бин - запись в xml-файле конфигурации Spring. Не стоит путать спринговские бины с Java Bean и Enterprise Java Bean, названия похожи, но суть совершенно разная. Итак, чтобы Spring смог прочитать свой файл конфигурации и загрузить описание бинов необходимо их указать, делается это директивой:

<context-param>

    <param-name>contextConfigLocation</param-name>

    <param-value>/WEB-INF/applicationContext.xml</param-value>

</context-param>



Непосредственно загрузкой файла конфигурации Spring занимается ContextLoadListener, который подключается директивой:

<listener>

    <listener-class>

        org.springframework.web.context.ContextLoaderListener

    </listener-class>

</listener>


Итак, теперь у нас настроен спринговский сервлет, который будет перехватывать все запросы к нашему веб-приложению. В том числе и SOAP-запросы от клиента веб-сервиса. Приведу полную версию файла web.xml:

<?xml version="1.0"?>

<!DOCTYPE web-app

   PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"

   "http://java.sun.com/dtd/web-app_2_3.dtd">

   

<web-app>

    <servlet>

        <servlet-name>XFireServlet</servlet-name>    

        <servlet-class>

            org.springframework.web.servlet.DispatcherServlet

        </servlet-class>    

        <load-on-startup>1</load-on-startup>

    </servlet>



    <servlet-mapping>

        <servlet-name>XFireServlet</servlet-name>

        <url-pattern>/*</url-pattern>

    </servlet-mapping>

 

    <context-param>

        <param-name>contextConfigLocation</param-name>

        <param-value>/WEB-INF/applicationContext.xml</param-value>

    </context-param>



    <listener>

        <listener-class>

            org.springframework.web.context.ContextLoaderListener

        </listener-class>

    </listener>

</web-app>

Конфигурируем Spring

Теперь займемся конфигурированием Spring'а. В этом кстати прелесть Spring'а и как IoC-контейнера и как фреймворка - очень многие вещи не нужно писать в коде, а достаточно сконфигурировать в xml-файле. Именно поэтому в данном проекте я решил использовать Spring - мне было лень писать servlet-фильтры, которые бы открывали/закрывали hibernate-транзакции, иницализировать hibernate с помощью servlet-листенеров, создавать свой сервлет, в котором инициализировать веб-сервис. Все это руками делать не нужно, ведь у нас есть Spring!

Создадим заглушку для файла applicationContext.xml:

<?xml version="1.0"?>

<beans

    xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:aop="http://www.springframework.org/schema/aop"

    xmlns:tx="http://www.springframework.org/schema/tx"

    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd

        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd

        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd"


    >






</beans>


Именно в этот файл мы будем помещать описание бинов. Начнем с уровня представления - ведь нам нужно как можно быстрее сделать простой веб-сервис, который бы отдавал корректный WSDL, чтобы другая команда могла писать клиента. Логико;й веб-сервис наполним позднее.

Итак, первое, что нам необходимо сделать - перенаправить все запросы, приходящие к сервису на XFire, который уже и будет вызывать нужный класс сервиса. Напомню, что сейчас у нас все запросы приходят на org.springframework.web.servlet.DispatcherServlet. Для этого нам необходимо настроить urlMapping - отображение контроллера по адресу ресурса. Сделаем так, что все запросы приходящие на http://oursite.com/services/... перенаправлялись на XFire:

    <bean id="simpleUrlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">

        <property name="order"><value>0</value></property>

        <property name="mappings">

            <props>

                <prop key="/services/*">servicesController</prop>

            </props>

        </property>

    </bean>


simpleUrlMapping - предопределенное имя бина, используется DispatcherServlet для диспетчиризации запросов. Собственно в данном бине строится карта урлов и обрабатывающих их контроллеров. В нашем случае удобно использовать контроллер org.codehaus.xfire.spring.remoting.XFireExporter входящий в поставку XFire и обеспечивающий интеграцию XFire и Spring. В бине simpleUrlMapping мы видем ссылку на servicesController - это и есть бин, описывающий наш контроллер:
    <import resource="classpath:org/codehaus/xfire/spring/xfire.xml" />

   

    <bean id="config" class="org.codehaus.xfire.aegis.type.Configuration">

        <property name="defaultExtensibleElements" value="false" />

        <property name="defaultExtensibleAttributes" value="false" />

        <property name="defaultNillable" value="false" />

        <property name="defaultMinOccurs" value="1" />

    </bean>

   

    <bean name="jsr181ServiceFactory" class="org.codehaus.xfire.annotations.AnnotationServiceFactory">

        <constructor-arg ref="xfire.transportManager" index="0"/>

        <constructor-arg ref="config" index="1" type="org.codehaus.xfire.aegis.type.Configuration" />

    </bean>

   

    <bean id="servicesController" class="org.codehaus.xfire.spring.remoting.XFireExporter">

        <property name="serviceBean" ref="xFireServiceBean"/>

        <property name="serviceClass" value="org.evm.integration.goodnews.service.IGoodNewsService" />     

        <property name="serviceFactory" ref="jsr181ServiceFactory" />

    </bean>

   

    <bean id="xFireServiceBean" class="org.evm.integration.goodnews.service.GoodNewsService"/>

 


Данный код как раз и содержит все самое интересное. Бин servicesController содержит ссылки на бин serviceBean - это как раз и есть наш класс веб-сервиса! Параметр serviceClass указывает интерфейс, который должен реализовывать наш веб-сервис. Бин serviceFactory описывает фабрику, с помощью которой будет строится сервис. XFire поддерживает фабрики на основе аннотаций (jsr181), JAXB, XMLBeans и многое другое (см официальную страничку проекта). В данном случае я выбрал фабрику на основе аннотаций, как самый простой способ построить веб-сервис. Фабрика принимает одним из параметров бин config. В данном случае конфигурирование осуществляется с помощью aegis - xml-файлов специального формата, имя которых совпадает с именем конфигурируемого параметра. Собственно информацию об aegis также можно найти на сайте XFire, но думаю данная технология еще составит тему для нашего разговора. Вообще строго говоря, в данном веб-сервисе конфигурировать нечего, в реальных проектах aegis удобно использовать для того, чтобы задавать свои типы данных (в частности Map например).

Теперь рассмотрим то, о чем так долго говорили большевики - собственно интерфейс нашего веб-сервиса и класс-заглушку (пока еще) его реализующий:

IGoodNewsService:
package org.evm.integration.goodnews.service;



import java.util.Collection;

import java.util.Date;



import javax.jws.WebMethod;

import javax.jws.WebResult;

import javax.jws.WebService;



import org.evm.integration.goodnews.model.entities.New;

import org.evm.integration.goodnews.model.entities.Topic;



@WebService(name="SimpleService", targetNamespace="http://service.goodnews.ru")

public interface IGoodNewsService {

    @WebMethod

    @WebResult(name="news")    

    public Collection<New> getAllNews();

   

    @WebMethod

    public void addTopic(String topicName);

   

    @WebMethod

    public void addFeed(String topicName, String feedName, String feedUrl);

   

    @WebMethod

    @WebResult(name="news")

    public Collection<New> getNewsByTopDateTitle(Topic top, Date date, String title);

   

    @WebMethod

    @WebResult(name="msg")

    public String helloWorld();

}

 


Как видим данный интерфейс содержит ряд аннотаций, по которым собственно и будет строится сервис. Каждая аннотация непосредственно влияет на WSDL, а значит и способы взаимодействия с сервисом.

@WebService(name="SimpleService", targetNamespace="http://service.goodnews.ru")
Задает название сервиса и таргет-нэймспейс. Тарегт неймспейс - пространство имен (XML Namespace) в котором будут находится методы сервиса. Вообще XFire очень гибкая вещь и позволяет помещать каждый сервис в свое пространство имен, более того, можно даже каждую используемую сервисом сущность либо каждый тип данных поместить в отдельное пространство имен. Это очень удобно при создании больших веб-сервисов, оперирующих сложными иерархиями сущностей. Замечу, что с помощью JAXB можно сконфигурировать процесс генерации клиента таким образом, что сущности, относящиеся к разным пространствам имен будут помещены в разные пакеты Java.

Каждый метод, доступный для сервиса должен быть аннотирован @WebMethod. Аннотация @WebResult(name="news") задает как будет называться возвращаемое методом значение в WSDL-сервиса.

Заглушку класса GoodNewsService приводить нет необходимости - это просто реализация интерфейса IGoodNewsService с пустыми методами. Обращу лишь внимание, что в самом классе аннотации использовать уже не нужно.

Отмечу одну очень важную особенность! Для каждого используемого контроллера необходимо создать свой файл бинов, можно даже пустой. Главное чтобы был файл к примеру XFireServlet-servlet.xml, содержание которого как минимум равно приведенной выше заглушке.

Собственно даже все, теперь можно запускать томкат, стартовать веб-приложение и обратится к сервису по адресу: http://oursite.com/services/GoodNewsService?wsdl, браузер покажет вам корректный WSDL сервиса. Его можно скачивать и генерировать стабы для клиента.

А мы продолжим, пришла пора добавить работу с БД и кое-какую логику.

Подключаем Hibernate

Прежде, чем подключать Hibernate необходимо создать файл мэппинга, в котором настроить отображение используемых нами сущностей на таблицы в БД. В данном проекте мы будем использовать 3 сущности: раздел новостей (Topic), RSS-feed с которого парсятся новости (Feed) и собственно новость (New). Файл мэппинга приведу полностью, т.е. уже с находящимися в нем именованными запросами, они нам в дальнейшем пригодятся:

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="org.evm.integration.goodnews.model.entities">                                             

    <class name="Topic" table="T_TOPICS" lazy="true">

        <id name="id" column="topic_id" type="java.lang.Long">

            <generator class="native" />           

        </id>

       

        <property name="name" column="name" type="string" length="255"/>



        <bag

            name="feeds"

            inverse="true"

            cascade="all">


           

            <key column="topic_id" />            

            <one-to-many class="Feed" />

        </bag>

        <bag

            name="news"

            inverse="true"

            cascade="all">


           

            <key column="topic_id" />            

            <one-to-many class="New" />

        </bag>        

    </class>        

   

    <class name="Feed" table="T_FEEDS" lazy="true">

        <id name="id" column="feed_id" type="java.lang.Long">

            <generator class="native" />           

        </id>

       

        <property name="name" column="name" type="string" length="255"/>

        <property name="url" column="url" type="string" length="255"/>

       

        <many-to-one name="topic" class="Topic">

            <column name="topic_id" not-null="true"/>

        </many-to-one>

       

        <bag

            name="news"

            inverse="true"

            cascade="all">


           

            <key column="feed_id" />            

            <one-to-many class="New" />

        </bag>

    </class>

   

    <class name="New" table="T_NEWS" lazy="true">

        <id name="id" column="new_id" type="java.lang.Long">

            <generator class="native" />           

        </id>

       

        <property name="name" column="name" type="string" length="255"/>

        <property name="text" column="text" type="text" />

        <property name="date" column="date" type="java.util.Date"/>

       

        <many-to-one name="topic" class="Topic">

            <column name="topic_id" not-null="true"/>

        </many-to-one>

               

        <many-to-one name="feed" class="Feed">

            <column name="feed_id" not-null="true"/>

        </many-to-one>

    </class>

   

    <query name="newsDao-getAllTopics">

    <![CDATA[

        from Topic as t order by t.name

   ]]></query>

   

    <query name="newsDao-getAllNews">

    <![CDATA[

        from New as n order by n.name

   ]]></query>

   

    <query name="newsDao-getFeedByName">

    <![CDATA[

        from Feed as f where f.name=:name

  ]]></query>

   

   <query name="newsDao-getTopicByName">

    <![CDATA[

        from Topic as t where t.name=:name

   ]]></query>

   

    <query name="newsDao-getNewsByTopDateTitle">

    <![CDATA[

        from New as n where n.topic.id = :topId and name=:name and date>=date

   ]]></query>

</hibernate-mapping>


С помощью hibtools по файлу мэппинга можно сгенерировать классы-сущности. Впрочем данные классы можно написать и руками, в данном случае это не имеет значения. Переходим к конфигурированию Spring:

     <bean id="dataSource"

        class="org.apache.commons.dbcp.BasicDataSource"

        destroy-method="close">


        <property name="driverClassName" value="com.mysql.jdbc.Driver" />

        <property name="url"

            value="jdbc:mysql://localhost:3306/goodnews?useUnicode=true" />


        <property name="username" value="root" />

        <property name="password" value="" />

    </bean>



    <bean id="sessionFactory"

        class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">


        <property name="dataSource" ref="dataSource" />

        <property name="mappingResources">

            <list>

                <value>mapping.hbm.xml</value>

            </list>

        </property>

        <property name="hibernateProperties">

            <value>

                hibernate.dialect=org.hibernate.dialect.MySQLDialect

                hibernate.show_sql=false hibernate.format_sql=false

                hibernate.use_sql_comments=true

                hibernate.connection.charSet=utf8

            </value>

        </property>

    </bean>



    <bean id="transactionManager"

        class="org.springframework.orm.hibernate3.HibernateTransactionManager">


        <property name="sessionFactory">

            <ref local="sessionFactory" />

        </property>

    </bean>



    <bean id="hibernateTemplate"

        class="org.springframework.orm.hibernate3.HibernateTemplate">


        <property name="sessionFactory">

            <ref bean="sessionFactory" />

        </property>

    </bean>

 


Рассмотрим бины подробнее. Бин dataSource - задает параметры подключения к БД: Класс драйвера СУБД, урл подключения, имя пользователя и пароль. В результате мы получаем источник данных. Бин sessionFactory реализуется одним из наиболее важных спринговских классов: LocalSessionFactoryBean. Собственно данный бин описывает взаимодействие с Hibernate. В качестве параметров он принимает созданный ранее источник данных, mappingResource - список файлов мэппинга (в нашем случае - 1 файл) и некоторые параметры Hibernate (в частности показывать или нет генерящийся SQL, использовать юникод и т.д.). Бин transactionManager - необходим для управления транзакциями. Позволяет вдальнейшем через аннотации или здесь же в конфигфайле описывать управление транзакциями при выполнении операций над БД (в частности при чтении данных ставить readonly, что предотвратит запись данных при их чтении).

И, наконец, hibernateTemplate - основной механизм обращения к хибернейту из кода программы. Именно посредством hibernateTemplate производятся CRUD-операции над данными и выполняются HQL-запросы.

Пишем DAO

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

IGoodNewsDao:
package org.evm.integration.goodnews.model.dao;



import java.util.Collection;



import org.evm.integration.goodnews.model.entities.Feed;

import org.evm.integration.goodnews.model.entities.New;

import org.evm.integration.goodnews.model.entities.Topic;



public interface IGoodNewsDao {

   

    public void addFeed(Feed feed);

   

    public void addTopic(Topic topic);

   

    public void addNew(New n, Topic topic, Feed feed);

   

    public Feed getFeedByName(String name);

   

    public Topic getTopicByName(String name);

   

    public Collection<New> getAllNews();

   

    public Collection<Topic> getAllTopics();

   

    public Object load(Long id, Class<?> clazz);

}


HibernateGoodNewsDao:
package org.evm.integration.goodnews.model.dao.hibernate;



import java.util.Collection;

import java.util.Date;



import org.evm.integration.goodnews.model.dao.IGoodNewsDao;

import org.evm.integration.goodnews.model.entities.Feed;

import org.evm.integration.goodnews.model.entities.New;

import org.evm.integration.goodnews.model.entities.Topic;

import org.springframework.orm.hibernate3.HibernateTemplate;



public class HibernateGoodNewsDao implements IGoodNewsDao {



    private HibernateTemplate hibTempl;

   

    public void setHibernateTemplate(HibernateTemplate hibTempl) {

        this.hibTempl = hibTempl;

    }

   

    public void addFeed(Feed feed) {

        hibTempl.save(feed);       

    }



    public void addNew(New n, Topic topic, Feed feed) {

        n.setTopic(topic);

        n.setFeed(feed);

        hibTempl.save(n);

    }



    public void addTopic(Topic topic) {

        hibTempl.save(topic);      

    }



    @SuppressWarnings("unchecked")

    public Collection<New> getAllNews() {

        return hibTempl.findByNamedQuery("newsDao-getAllNews");

    }

       

    @SuppressWarnings("unchecked")

    public Collection<Topic> getAllTopics() {

        return hibTempl.findByNamedQuery("newsDao-getAllTopics");

    }



    public Feed getFeedByName(String name) throws IndexOutOfBoundsException {      

        return (Feed) hibTempl.findByNamedQueryAndNamedParam("newsDao-getFeedByName", "name", name).get(1);

    }



    public Topic getTopicByName(String name) throws IndexOutOfBoundsException {

        return (Topic) hibTempl.findByNamedQueryAndNamedParam("newsDao-getTopicByName", "name", name).get(1);

    }

   

    @SuppressWarnings("unchecked")

    public Collection<New> getNewByTopDateTitle(Topic top, Date date, String title) throws IndexOutOfBoundsException {

        return hibTempl.findByNamedQueryAndNamedParam("newsDao-getNewsByTopDateTitle", new String[]{"topId", "name", "date"},

                new Object[]{top.getId(), title, date});

    }

   

    public Object load(Long id, Class<?> clazz) {

        return hibTempl.get(clazz, id);

    }

}

 


Прошу обратить внимание, что в данном классе продемонстрировано, как работать с HibernateTemplate для обработки запросов. Также важен следующий код:

    private HibernateTemplate hibTempl;

   

    public void setHibernateTemplate(HibernateTemplate hibTempl) {

        this.hibTempl = hibTempl;

    }

 

Именно с его помощью в DAO инъектится HibernateTemplate. Правда для инъекции необходимо описать соответствующий бин:

    <bean id="newsDAO" class="org.evm.integration.goodnews.model.dao.hibernate.HibernateGoodNewsDao">

        <property name="hibernateTemplate">

            <ref bean="hibernateTemplate" />

        </property>

    </bean> 

 


Теперь созданное DAO можно передавать в классы, реализующие бизнес-логику. Данные классы также необходимо описывать с помощью бинов. Я думаю преимущества Spring'а как фреймворка, который организует все приложения становятся понятны. У нас есть четкое разделение слоев: уровень доступа к данным (например через Hibernate), уровень DAO, уровень бизнес-логики и уровень представления (например XFire). Заметьте, что в коде данные уровни напрямую друг с другом не связаны, а связаны через наборы интерфейсов, предоставляемых каждым уровнем. Реальные же связи (выражающиеся в том, какие именно классы будут реализовывать те или иные интерфейсы) описаны лишь в одном месте - файлах конфигурации Spring как бины. И если нам будет необходимо расширить приложение, изменить бизнес-логику или отказаться от использования хибернейт - изменения придется внести только в файлах конфигурации. Кстати файлов может быть несколько, не обязательно всю конфигурацию помещать в один файл.

Буду рад вашим коментариям, вопросам, а также сообщениям о найденых ошибках и неточностях.

Понравилось сообщение - подпишись на блог через RSS

2 комментария:

_arh комментирует...

А как, собственно, прошел эксперимент по интеграции?

Pavel Samolisov комментирует...

Все прошло довольно успешно - коллекции записей довольно бодро ходили туда-сюда. И это радует.

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!