В рамках проходящей в Южно-Уральском государственном университете конференции "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 смог прочитать свой файл конфигурации и загрузить описание бинов необходимо их указать, делается это директивой:
Непосредственно загрузкой файла конфигурации Spring занимается ContextLoadListener, который подключается директивой:
Итак, теперь у нас настроен спринговский сервлет, который будет перехватывать все запросы к нашему веб-приложению. В том числе и SOAP-запросы от клиента веб-сервиса. Приведу полную версию файла web.xml:
Конфигурируем Spring
Теперь займемся конфигурированием Spring'а. В этом кстати прелесть Spring'а и как IoC-контейнера и как фреймворка - очень многие вещи не нужно писать в коде, а достаточно сконфигурировать в xml-файле. Именно поэтому в данном проекте я решил использовать Spring - мне было лень писать servlet-фильтры, которые бы открывали/закрывали hibernate-транзакции, иницализировать hibernate с помощью servlet-листенеров, создавать свой сервлет, в котором инициализировать веб-сервис. Все это руками делать не нужно, ведь у нас есть Spring!
Создадим заглушку для файла applicationContext.xml:
Именно в этот файл мы будем помещать описание бинов. Начнем с уровня представления - ведь нам нужно как можно быстрее сделать простой веб-сервис, который бы отдавал корректный WSDL, чтобы другая команда могла писать клиента. Логико;й веб-сервис наполним позднее.
Итак, первое, что нам необходимо сделать - перенаправить все запросы, приходящие к сервису на XFire, который уже и будет вызывать нужный класс сервиса. Напомню, что сейчас у нас все запросы приходят на org.springframework.web.servlet.DispatcherServlet. Для этого нам необходимо настроить urlMapping - отображение контроллера по адресу ресурса. Сделаем так, что все запросы приходящие на http://oursite.com/services/... перенаправлялись на XFire:
simpleUrlMapping - предопределенное имя бина, используется DispatcherServlet для диспетчиризации запросов. Собственно в данном бине строится карта урлов и обрабатывающих их контроллеров. В нашем случае удобно использовать контроллер org.codehaus.xfire.spring.remoting.XFireExporter входящий в поставку XFire и обеспечивающий интеграцию XFire и Spring. В бине simpleUrlMapping мы видем ссылку на servicesController - это и есть бин, описывающий наш контроллер:
Данный код как раз и содержит все самое интересное. Бин servicesController содержит ссылки на бин serviceBean - это как раз и есть наш класс веб-сервиса! Параметр serviceClass указывает интерфейс, который должен реализовывать наш веб-сервис. Бин serviceFactory описывает фабрику, с помощью которой будет строится сервис. XFire поддерживает фабрики на основе аннотаций (jsr181), JAXB, XMLBeans и многое другое (см официальную страничку проекта). В данном случае я выбрал фабрику на основе аннотаций, как самый простой способ построить веб-сервис. Фабрика принимает одним из параметров бин config. В данном случае конфигурирование осуществляется с помощью aegis - xml-файлов специального формата, имя которых совпадает с именем конфигурируемого параметра. Собственно информацию об aegis также можно найти на сайте XFire, но думаю данная технология еще составит тему для нашего разговора. Вообще строго говоря, в данном веб-сервисе конфигурировать нечего, в реальных проектах aegis удобно использовать для того, чтобы задавать свои типы данных (в частности Map например).
Теперь рассмотрим то, о чем так долго говорили большевики - собственно интерфейс нашего веб-сервиса и класс-заглушку (пока еще) его реализующий:
IGoodNewsService:
Как видим данный интерфейс содержит ряд аннотаций, по которым собственно и будет строится сервис. Каждая аннотация непосредственно влияет на 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). Файл мэппинга приведу полностью, т.е. уже с находящимися в нем именованными запросами, они нам в дальнейшем пригодятся:
С помощью hibtools по файлу мэппинга можно сгенерировать классы-сущности. Впрочем данные классы можно написать и руками, в данном случае это не имеет значения. Переходим к конфигурированию Spring:
Рассмотрим бины подробнее. Бин dataSource - задает параметры подключения к БД: Класс драйвера СУБД, урл подключения, имя пользователя и пароль. В результате мы получаем источник данных. Бин sessionFactory реализуется одним из наиболее важных спринговских классов: LocalSessionFactoryBean. Собственно данный бин описывает взаимодействие с Hibernate. В качестве параметров он принимает созданный ранее источник данных, mappingResource - список файлов мэппинга (в нашем случае - 1 файл) и некоторые параметры Hibernate (в частности показывать или нет генерящийся SQL, использовать юникод и т.д.). Бин transactionManager - необходим для управления транзакциями. Позволяет вдальнейшем через аннотации или здесь же в конфигфайле описывать управление транзакциями при выполнении операций над БД (в частности при чтении данных ставить readonly, что предотвратит запись данных при их чтении).
И, наконец, hibernateTemplate - основной механизм обращения к хибернейту из кода программы. Именно посредством hibernateTemplate производятся CRUD-операции над данными и выполняются HQL-запросы.
Пишем DAO
Единственный вопрос, который остался - как из сервиса взаимодействовать с hibernateTemplate. Я решил вопрос следующим образом - ввел так называемый слой DAO, написал DAO-класс, через который реализовал взаимодействие с уровнем хранения. Данный класс описал как бин и заинъектил в класс веб-сервиса. Ну как говорится, а теперь слайды:
IGoodNewsDao:
HibernateGoodNewsDao:
Прошу обратить внимание, что в данном классе продемонстрировано, как работать с HibernateTemplate для обработки запросов. Также важен следующий код:
Именно с его помощью в DAO инъектится HibernateTemplate. Правда для инъекции необходимо описать соответствующий бин:
Теперь созданное DAO можно передавать в классы, реализующие бизнес-логику. Данные классы также необходимо описывать с помощью бинов. Я думаю преимущества Spring'а как фреймворка, который организует все приложения становятся понятны. У нас есть четкое разделение слоев: уровень доступа к данным (например через Hibernate), уровень DAO, уровень бизнес-логики и уровень представления (например XFire). Заметьте, что в коде данные уровни напрямую друг с другом не связаны, а связаны через наборы интерфейсов, предоставляемых каждым уровнем. Реальные же связи (выражающиеся в том, какие именно классы будут реализовывать те или иные интерфейсы) описаны лишь в одном месте - файлах конфигурации Spring как бины. И если нам будет необходимо расширить приложение, изменить бизнес-логику или отказаться от использования хибернейт - изменения придется внести только в файлах конфигурации. Кстати файлов может быть несколько, не обязательно всю конфигурацию помещать в один файл.
Буду рад вашим коментариям, вопросам, а также сообщениям о найденых ошибках и неточностях.
Понравилось сообщение - подпишись на блог через RSS
Чем хороша 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>
<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>
<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>
<!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>
<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>
<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"/>
<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();
}
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>
<!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>
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);
}
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);
}
}
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;
}
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>
<property name="hibernateTemplate">
<ref bean="hibernateTemplate" />
</property>
</bean>
Теперь созданное DAO можно передавать в классы, реализующие бизнес-логику. Данные классы также необходимо описывать с помощью бинов. Я думаю преимущества Spring'а как фреймворка, который организует все приложения становятся понятны. У нас есть четкое разделение слоев: уровень доступа к данным (например через Hibernate), уровень DAO, уровень бизнес-логики и уровень представления (например XFire). Заметьте, что в коде данные уровни напрямую друг с другом не связаны, а связаны через наборы интерфейсов, предоставляемых каждым уровнем. Реальные же связи (выражающиеся в том, какие именно классы будут реализовывать те или иные интерфейсы) описаны лишь в одном месте - файлах конфигурации Spring как бины. И если нам будет необходимо расширить приложение, изменить бизнес-логику или отказаться от использования хибернейт - изменения придется внести только в файлах конфигурации. Кстати файлов может быть несколько, не обязательно всю конфигурацию помещать в один файл.
Буду рад вашим коментариям, вопросам, а также сообщениям о найденых ошибках и неточностях.
Понравилось сообщение - подпишись на блог через RSS
2 комментария:
А как, собственно, прошел эксперимент по интеграции?
Все прошло довольно успешно - коллекции записей довольно бодро ходили туда-сюда. И это радует.
Отправить комментарий
Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!