Статьи

Enterprise RIA с Spring 3, Flex 4 и GraniteDS

Adobe Flex — одна из наиболее широко используемых клиентских технологий для создания многофункциональных приложений, а Spring 3 — одна из самых популярных сред приложений Java. Эти две технологии представляют собой прекрасную комбинацию для создания корпоративных приложений с современным и богатым пользовательским интерфейсом.

Существуют различные варианты их интеграции, каждый из которых имеет свои плюсы и минусы, например, Web / REST-сервисы и проект Spring-Flex, продвигаемый Adobe и SpringSource. О них много статей и ресурсов, здесь я остановлюсь на альтернативном подходе с использованием открытого проекта GraniteDS .

GraniteDS основан на реализации в чистом помещении протоколов удаленного взаимодействия и обмена сообщениями AMF3 и исторически был первой реализацией протокола AMF3 в Java с открытым исходным кодом. Он очень рано предоставлял готовую интеграцию с Spring, и эта интеграция постоянно улучшалась с каждой новой версией GraniteDS, следуя нескольким основным принципам:

  • Предоставьте полностью интегрированную платформу Spring / Flex / GraniteDS RIA, которая делает код конфигурации и интеграции практически неиспользуемым. Платформа включает, в частности, инструменты и клиентские библиотеки Flex, необходимые для простого использования всех функций Spring и связанных с ней технологий (постоянство, безопасность …).
  • Обеспечьте максимально возможную безопасность типов в приложениях Java и AS3, гарантируя, что большинство проблем интеграции могут быть обнаружены на ранней стадии во время компиляции / сборки.

 

Эти основные варианты дизайна сильно отличают GraniteDS от, например, Adobe BlazeDS, который имеет только серверную часть. В этой статье я покажу эту концепцию платформы RIA в действии, создав простое приложение с использованием следующих функций:

  • Flex AMF удаленное взаимодействие с сервисами Spring.
  • Поддержка отдельных объектов Hibernate / JPA непосредственно в приложении Flex. Пока пока DTO и ленивые исключения инициализации.
  • Поддержка API Bean Validation (JSR-303) с соответствующими валидаторами Flex.
  • Поддержка компонентов Spring Security 3 и Flex, которые интегрируются с авторизацией на стороне сервера.
  • Поддержка данных в режиме реального времени .

 

В качестве дополнительного примечания, GraniteDS по-прежнему поддерживает классический Flex RemoteObject API и, таким образом, является близкой заменой BlazeDS с некоторыми полезными улучшениями, но предоставляет альтернативный Flex API, называемый Tide, который проще в использовании и обеспечивает всю мощь платформы. ,

Настройка проекта

Мы должны с чего-то начать, и первым шагом является создание приложения Spring. В этом нет ничего сложного, мы могли бы просто начать со стандартного приложения Spring MVC и просто добавить несколько элементов GraniteDS в контекст приложения Spring. Чтобы упростить задачу, я собираюсь использовать архетип Maven (рекомендуется Maven 3):

mvn archetype:generate    -DarchetypeGroupId=org.graniteds.archetypes    -DarchetypeArtifactId=graniteds-tide-spring-jpa-hibernate    -DgroupId=org.example     -DartifactId=gdsspringflex    -Dversion=1.0-SNAPSHOT

 

Это создает базовый скелет проекта, который включает в себя функции сохранения, безопасности и передачи данных в реальном времени. В качестве отправной точки вы можете просто построить проект и запустить его с помощью подключаемого модуля Maven:

cd gdsspringflexmvn installcd webappmvn jetty:run-war

Затем просмотрите http: // localhost: 8080 / gdsspringflex / gdsspringflex.swf и войдите в систему с помощью admin / admin или user / user.

 

Структура проекта представляет собой классический многомодульный проект Maven с модулем Flex, модулем Java и модулем веб-приложений. Он использует очень хороший плагин flexmojos для создания приложения Flex с Maven.

<b>gdsspringflex</b>|- pom.xml|- flex   |- pom.xml   |- src/main/flex      |- Main.mxml      |- Login.mxml      |- Home.mxml|- java   |- pom.xml   |- src/main/java      |- org/example/entities         |- AbstractEntity.java         |- Welcome.java      |- org/example/services         |- ObserveAllPublishAll.java         |- WelcomeService.java         |- WelcomeServiceImpl.java|- webapp   |- pom.xml   |- src/main/webapp      |- WEB-INF         |- web.xml         |- dispatcher-servlet.xml         |- spring            |- app-config.xml            |- app-jpa-config.xml            |- app-security-config.xml

Если мы забудем о сгенерированных по умолчанию источниках приложений в модулях Flex и Java и сконцентрируемся только на конфигурации, наиболее интересными файлами будут web.xml и файлы конфигурации app * config.xml Spring.

web.xml в основном включает в себя прослушиватели Spring 3, сервлет-диспетчер Spring MVC, сопоставленный с / graniteamf / *, который будет обрабатывать запросы AMF, и сервлет Gravity для Jetty, сопоставленный с / gravityamf / * (Gravity — это название обмена сообщениями на основе GraniteDS Comet. реализация).

<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">    <display-name>GraniteDS Tide/Spring</display-name>    <description>GraniteDS Tide/Spring Archetype Application</description>        <context-param>        <param-name>contextConfigLocation</param-name>        <param-value>            /WEB-INF/spring/app-config.xml,            /WEB-INF/spring/app-*-config.xml        </param-value>    </context-param>        <!-- Spring listeners -->    <listener>        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>    </listener>    <listener>        <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>    </listener>    <!-- Spring MVC dispatcher servlet that handles incoming AMF requests on the /graniteamf endpoint --><servlet>    <servlet-name>dispatcher</servlet-name>    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>    <load-on-startup>1</load-on-startup></servlet><servlet-mapping>    <servlet-name>dispatcher</servlet-name>    <url-pattern>/graniteamf/*</url-pattern></servlet-mapping>         <!-- Gravity servlet that handles AMF asynchronous messaging request on the /gravityamf endpoint -->    <servlet>        <servlet-name>GravityServlet</servlet-name>        <servlet-class>org.granite.gravity.jetty.GravityJettyServlet</servlet-class>        <!--servlet-class>org.granite.gravity.tomcat.GravityTomcatServlet</servlet-class-->        <!--servlet-class>org.granite.gravity.jbossweb.GravityJBossWebServlet</servlet-class-->        <load-on-startup>1</load-on-startup>    </servlet>    <servlet-mapping>        <servlet-name>GravityServlet</servlet-name>        <url-pattern>/gravityamf/*</url-pattern>    </servlet-mapping></span>    <welcome-file-list>        <welcome-file>index.html</welcome-file>    </welcome-file-list></web-app>

 

Причина, по которой существует определенный сервлет для Gravity, заключается в том, что он оптимизирован для использования определенных асинхронных возможностей нижележащего контейнера сервлета, чтобы получить лучшую масштабируемость, и этого нельзя достичь с помощью сервлета диспетчера Spring MVC по умолчанию. Вот почему это необходимо для настройки различных реализаций сервлета в зависимости от целевого контейнера (Tomcat, JBossWeb …).

Далее приведена основная конфигурация Spring 3, которая в основном состоит из базовых компонентов Spring MVC:

<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:graniteds="http://www.graniteds.org/config"    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.graniteds.org/config http://www.graniteds.org/public/dtd/2.1.0/granite-config-2.1.xsd">    <!-- Annotation scan -->    <context:component-scan base-package="org.example"/>      <!-- Spring MVC configuration -->    <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/>    <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>    <!-- Configuration of GraniteDS -->    <graniteds:flex-filter url-pattern="/*" tide="true"/>        <!-- Simple messaging destination for data push -->    <graniteds:messaging-destination id="welcomeTopic" no-local="true" session-selector="true"/>    </beans>

Главное, что касается GraniteDS — это объявление flex-filter . Существует также пример темы сообщений, которая используется приложением Hello World по умолчанию. app-jpa-config.xml содержит конфигурацию JPA и не содержит ничего о GraniteDS. Наконец Spring Security:

<beansxmlns="http://www.springframework.org/schema/beans"xmlns:security="http://www.springframework.org/schema/security"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"xmlns:context="http://www.springframework.org/schema/context"  xmlns:graniteds="http://www.graniteds.org/config"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd          http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-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/security http://www.springframework.org/schema/security/spring-security-3.0.xsd          http://www.graniteds.org/config http://www.graniteds.org/public/dtd/2.1.0/granite-config-2.1.xsd"           default-autowire="byName"       default-lazy-init="true">              <security:authentication-manager alias="authenticationManager">     <security:authentication-provider>         <security:user-service>            <security:user name="admin" password="admin" authorities="ROLE_USER,ROLE_ADMIN" />                <security:user name="user" password="user" authorities="ROLE_USER" />        </security:user-service>        </security:authentication-provider>    </security:authentication-manager>         <security:global-method-security secured-annotations="enabled" jsr250-annotations="enabled"/>      <!-- Configuration for Tide/Spring authorization -->  <graniteds:tide-identity/>        <!-- Uncomment when there are more than one authentication-manager :    <graniteds:security-service authentication-manager="authenticationManager"/>    --></beans>

Опять же, в основном Spring, мы просто находим здесь bean компонент tide-identity, который используется для интеграции Spring Security с компонентом Tide Identity Flex.

 

Мы закончили для настройки на стороне сервера. GraniteDS автоматически обнаруживает большую часть конфигурации Spring при запуске и настраивает себя соответствующим образом, так что этих 10 строк XML обычно достаточно для большинства проектов. Если вы посмотрите на различные Maven POM, вы найдете зависимости на стороне сервера GraniteDS и клиентской стороне GraniteDS swcs. Вы также можете взглянуть на код Flex mxml примера приложения, сгенерированного архетипом, но сейчас я начну с нуля.

Remoting к сервисам Spring

Сначала традиционный Hello World и его воплощение в качестве сервиса Spring 3:

@RemoteDestinationpublic interface HelloService {public String hello(String name);}@Service("helloService")public class HelloServiceImpl implements HelloService {public String hello(String name) {return "Hello " + name;}}

Вы, вероятно, заметили аннотацию @RemoteDestination на интерфейсе, означающую, что службе разрешено вызывать удаленно из Flex. Теперь приложение Flex:

<s:Applicationxmlns:fx="http://ns.adobe.com/mxml/2009"xmlns:s="library://ns.adobe.com/flex/spark"xmlns:mx="library://ns.adobe.com/flex/mx"    xmlns="*"    preinitialize="init()">       <fx:Script>        <![CDATA[            import org.granite.tide.Component;            import org.granite.tide.spring.Spring;            import org.granite.tide.events.TideResultEvent;            import org.granite.tide.service.DefaultServiceInitializer;                        private function init():void {                Spring.getInstance().initApplication();                Spring.getInstance().getSpringContext().serviceInitializer = new DefaultServiceInitializer('/gdsspringflex');            }                        [In]            public var helloService:Component;                        private function hello(name:String):void {            helloService.hello(name,             function(event:TideResultEvent):void {            message.text = "Message: " + (event.result as String);            }            );            }        ]]>    </fx:Script>        <s:VGroup width="100%">    <s:Label text="Name"/>    <s:TextInput id="tiName"/>        <s:Button label="Hello" click="hello(tiName.text)"/>        <s:Label id="message"/></s:VGroup>    </s:Application>

Вы можете перестроить и перезапустить проект с помощью:

mvn clean installcd webappmvn jetty:run-war

 

Ну, это не совсем короткое приложение Hello World, но давайте посмотрим на интересные моменты:

  • Метод init () вызывается в обработчике preinitialize . Он делает две вещи: инициализирует инфраструктуру Tide с поддержкой Spring и объявляет инициализатор службы с корневым контекстом нашего приложения. Задача инициализатора службы состоит в том, чтобы настроить все компоненты удаленного взаимодействия / обмена сообщениями, такие как uris конечных точек сервера, каналы и т. Д. По сути, он заменяет традиционный файл Flex static services-config.xml . Другие реализации могут быть легко созданы, например, для динамического извлечения конфигурации каналов из удаленного файла (например, полезно с приложением AIR).
  • Клиентский прокси для bean- компонента helloService внедряется в mxml с помощью аннотации [In]. По умолчанию имя переменной должно совпадать с именем службы Spring, в противном случае нам придется указывать имя службы в аннотации [In («helloService»)] . Это может показаться «волшебной» инъекцией, но, как мы просили экземпляр Component, фреймворк точно знает, что вам нужен клиентский прокси для удаленного компонента.
  • Функция hello демонстрирует базовый API Tide Remoting. Он просто вызывает удаленный метод на клиентском прокси-сервере с необходимыми аргументами и предоставляет обратные вызовы для событий результата и ошибки, во многом аналогично jQuery , поэтому вам не нужно вручную обрабатывать прослушиватели событий, асинхронные токены, ответчики и все радости RemoteObject. ,

Теперь, когда у нас работают основы, мы можем немного улучшить это. POM проекта Flex настроен на автоматическую генерацию (с генератором GraniteDS Gas3, встроенным в flexmojos) прокси-серверов AS3 для всех интерфейсов Java с именем * Service и помеченных @RemoteDestination . Это означает, что мы могли бы просто написать это:

import org.example.services.HelloService;[In]public var helloService:HelloService;

 

Это выглядит как небольшое косметическое изменение, но теперь вы получаете выгоду от завершения кода в вашей IDE и от лучшей проверки ошибок компилятором Flex. Более того, вся инъекция может быть полностью безопасна для типов и больше не полагаться на имя службы, используя аннотацию [Inject] вместо [In] (обратите внимание, что теперь мы можем присвоить нашей внедренной переменной любое имя):

[Inject]public var myService:HelloService;private function hello(name:String):void {   myService.hello(name,    function(event:TideResultEvent):void {   message.text = "Message: " + (event.result as String);   }   );}

И поскольку инъекция в клиенте теперь использует имя интерфейса, нам больше не нужно давать имя сервису Spring:

@Servicepublic class HelloServiceImpl implements HelloService {public String hello(String name) {return "Hello " + name;}}

 

Теперь вы можете выполнить любой рефакторинг, который вы хотите на стороне Java, например, изменить сигнатуры методов, Gas3 затем сгенерирует прокси AS3, а компилятор Flex немедленно сообщит вам, что не так. Еще одна интересная вещь заключается в том, что Flex mxml теперь выглядит как bean-компонент Spring, что позволяет разработчикам Spring легко начать работу с Flex.

Постоянство и интеграция с Hibernate / JPA

Давайте пойдем немного дальше и посмотрим, как сделать простой CRUD с парой сущностей JPA:

@Entitypublic class Author extends AbstractEntity {        @Basic    private String name;    @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY, mappedBy="author", orphanRemoval=true)    private Set books = new HashSet();        // Getters/setters    ...}@Entitypublic class Book extends AbstractEntity {        @Basic    private String title;    @ManyToOne(optional=false)    private Author author;        // Getters/setters    ...}

Обе сущности расширяют AbstractEntity , но это совсем не обязательно, это просто вспомогательный класс, предоставляемый архетипом Maven.

 

Теперь мы создадим простой сервис Spring для обработки базового CRUD для этих объектов (очевидно, что политкорректный сервис Spring должен использовать DAO, но DAO ужасны и здесь ничего не изменится):

@RemoteDestinationpublic interface AuthorService {        public List<author> findAllAuthors();        public Author createAuthor(Author author);        public Author updateAuthor(Author author);        public void deleteAuthor(Long id);}@Servicepublic class AuthorServiceImpl implements AuthorService {    @PersistenceContext    private EntityManager entityManager;    @Transactional(readOnly=true)    public List<author> findAllAuthors() {        return entityManager.createQuery("select a from Author a order by a.name").getResultList();    }    @Transactional    public Author createAuthor(Author author) {        entityManager.persist(author);        entityManager.refresh(author);        return author;    }        @Transactional    public Author updateAuthor(Author author) {        return entityManager.merge(author);    }        @Transactional    public void deleteAuthor(Long id) {        Author author = entityManager.find(Author.class, id);        entityManager.remove(author);    }}</author></author>

И приложение Flex:

<s:Applicationxmlns:fx="http://ns.adobe.com/mxml/2009"xmlns:s="library://ns.adobe.com/flex/spark"xmlns:mx="library://ns.adobe.com/flex/mx"    xmlns="*"    preinitialize="init()">       <fx:Script>        <![CDATA[            import mx.controls.Alert;            import mx.collections.ArrayCollection;            import mx.data.utils.Managed;            import org.granite.tide.spring.Spring;            import org.granite.tide.service.DefaultServiceInitializer;            import org.granite.tide.events.TideResultEvent;            import org.granite.tide.events.TideFaultEvent;            import org.granite.tide.TideResponder;                        import org.example.entities.Author;            import org.example.services.AuthorService;                        private function init():void {                Spring.getInstance().initApplication();                Spring.getInstance().getSpringContext().serviceInitializer = new DefaultServiceInitializer('/gdsspringflex');            }                                    [Inject]            public var authorService:AuthorService;                                    [Bindable]            public var authors:ArrayCollection;                        private function findAllAuthors():void {                authorService.findAllAuthors(                    function(event:TideResultEvent):void {                        authors = ArrayCollection(event.result);                    }                );            }                        [Bindable]                        private var author:Author = new Author();                        private function createAuthor():void {            authorService.createAuthor(author,                     function(event:TideResultEvent):void {                        authors.addItem(author);                        author = new Author();                    },                    function(event:TideFaultEvent):void {                        Alert.show(event.fault.toString());                    }                );            }                        private function editAuthor():void {                currentState = 'edit';                author = Author(lAuthors.selectedItem);            }                        private function updateAuthor():void {            authorService.updateAuthor(lAuthors.selectedItem,                     function(event:TideResultEvent):void {                        lAuthors.selectedItem = null;                        author = new Author();                        currentState = 'create';                    },                    function(event:TideFaultEvent):void {                        Alert.show(event.fault.toString());                    }                );            }                        private function cancelAuthor():void {                            lAuthors.selectedItem = null;                 author = new Author();                currentState = 'create';             }                        private function deleteAuthor():void {                authorService.deleteAuthor(lAuthors.selectedItem.id,                    function(event:TideResultEvent):void {                        var idx:int = authors.getItemIndex(lAuthors.selectedItem);                        authors.removeItemAt(idx);                        lAuthors.selectedItem = null;                        author = new Author();                        currentState = 'create';                    },                    function(event:TideFaultEvent):void {                        Alert.show(event.fault.toString());                    }                );            }        ]]>    </fx:Script>        <s:states>        <s:State name="create"/>        <s:State name="edit"/>    </s:states>        <s:Group width="800">        <s:layout>            <s:VerticalLayout paddingLeft="10" paddingRight="10" paddingTop="10" paddingBottom="10"/>        </s:layout>                <mx:Form id="fAuthor">            <mx:FormHeading label.create="New author" label.edit="Edit author"/>                        <mx:FormItem label="Name">                <s:TextInput id="iName" text="@{author.name}"/>            </mx:FormItem>            <mx:FormItem>                <s:HGroup>                    <s:Button id="bSave" label.create="Create" label.edit="Update"                         click.create="createAuthor()"                        click.edit="updateAuthor()"/>                    <s:Button id="bDelete" label="Delete" visible.create="false" visible.edit="true"                        click.edit="deleteAuthor()"/>                    <s:Button id="bCancel" label="Cancel" visible.create="false" visible.edit="true"                        click.edit="cancelAuthor()"/>                </s:HGroup>            </mx:FormItem>        </mx:Form>            <s:Label fontWeight="bold" text="Authors List"/>    <s:List id="lAuthors" dataProvider="{authors}" labelField="name" width="100%"            change="editAuthor()"            creationComplete="findAllAuthors()"/>                <s:Button label="Refresh" click="findAllAuthors()"/>        </s:Group>    </s:Application>

Это простое приложение CRUD, использующее некоторые очень удобные функции Flex 4, такие как состояния и двунаправленное связывание данных. Обратите внимание, как буквально исчез весь стандартный код для соединения клиента и сервера, сохраняя при этом чистое разделение между двумя уровнями. В реальном мире мы, вероятно, хотели бы применить некоторый шаблон MVC вместо монолитного mxml, но это не сильно изменится. Важно то, что части Flex и Java не содержат бесполезного или избыточного кода и поэтому намного проще в обслуживании. Даже использование генераторов кода на основе моделей для автоматического создания приложения Flex было бы проще, поскольку кода для генерации значительно меньше.

Теперь пересоберите и запустите приложение на Jetty и убедитесь, что вы можете создавать, обновлять и удалять авторов. В этом примере есть две незначительные проблемы, которые не слишком интересны сами по себе, но которые я буду использовать, чтобы показать две интересные особенности Tide.

Первая проблема: когда вы начинаете обновлять имя автора, изменение распространяется в список с помощью двунаправленной привязки, но предыдущее значение не восстанавливается при нажатии кнопки «Отмена», что приводит к несогласованному отображению. В основном это проблема двунаправленной привязки Flex 4, поскольку она распространяет все изменения немедленно, но не может откатить эти изменения. У нас было бы три варианта, чтобы исправить это: сохранить состояние объекта где-то перед редактированием и восстановить его после отмены, скопировать исходные данные и связать поля ввода на копии (но тогда список не будет обновлен путем связывания), или избежать использования двунаправленная привязка. Ни один из этих вариантов не является действительно привлекательным, к счастью, Tide предоставляет очень простую функцию для решения этой проблемы и делает двустороннее связывание действительно удобным:

private function cancelAuthor():void {    Managed.resetEntity(author);    lAuthors.selectedItem = null;     author = new Author();    currentState = 'create'; }

Managed.resetEntity () просто откатывает все изменения, сделанные на стороне Flex, и восстанавливает последнее стабильное состояние, полученное от сервера.

 

Вторая проблема: нажатие на кнопку обновления теряет текущий выбранный элемент. Это потому, что мы заменяем dataProvider списка каждый раз, когда получаем новую коллекцию. Это можно легко исправить с помощью функции слияния данных Tide:

public var authors:ArrayCollection = new ArrayCollection();private function findAllAuthors():void {    authorService.findAllAuthors(new TideResponder(null, null, null, authors));}

Этот относительно уродливый удаленный вызов с использованием TideResponder указывает Tide, что он должен объединить результат вызова с предоставленной переменной, вместо того, чтобы полностью заменить коллекцию на «author = event.result» в обработчике результата. Обратите внимание, что нам даже не нужен обработчик результатов, еще раз сохраняя несколько строк кода.

 

Эта проблема с выбором элементов иллюстрирует, почему сохранение одинаковых коллекций и экземпляров сущностей для удаленных вызовов очень важно, если вы хотите настроить управляемые данными визуальные эффекты или анимации (помните, R of RIA). Все визуальные функции Flex сильно зависят от экземпляра объекта, который управляет эффектом, и от событий, которые он отправляет. Именно здесь кеширование и слияние сущностей Tide очень помогают, гарантируя, что каждый экземпляр сущности будет существовать только один раз и отправлять только необходимые события, когда он обновляется с сервера.

Я закончу эту часть, показывая, как отображать и обновлять коллекцию книг. На данный момент вы, возможно, заметили, что существование этой коллекции вообще не вызывает никаких проблем, хотя для сущности JPA она отмечена как ленивая. Нет исключения LazyInitializationException и особых проблем при объединении сущности, измененной в Flex, в контексте постоянства JPA. GraniteDS прозрачно сериализовал и десериализовал внутреннее состояние Hibernate коллекции назад и вперед, благодаря чему данные, поступающие из Flex, выглядят точно так же, как если бы они пришли из клиента Java.

Итак, давайте попробуем осуществить редактирование списка книг в форме обновления. Нам не нужно ничего менять в сервисе, Hibernate позаботится о сохранении коллекции из-за выбранной нами каскадной опции. Что касается Flex, мы можем использовать редактируемый список (обратите внимание, что в Flex 4 нет встроенного редактируемого списка Spark , поэтому мы используем пользовательский ItemRenderer, вдохновленный этим постом в блоге , см. Полные прилагаемые источники), и добавьте его:

<mx:FormItem label="Books" includeIn="edit">    <s:HGroup>        <s:List id="lBooks" dataProvider="{author.books}" labelField="title" width="300" itemRenderer="BookItemRenderer"/>        <s:VGroup>            <s:Button label="Add" click="addBook()"/>            <s:Button label="Remove" enabled="{Boolean(lBooks.selectedItem)}" click="removeBook()"/>        </s:VGroup>    </s:HGroup></mx:FormItem>

И соответствующие действия скрипта:

private function addBook():void {    var book:Book = new Book();    book.author = author;    author.books.addItem(book);    lBooks.selectedIndex = author.books.length-1;}private function removeBook():void {    author.books.removeItemAt(lBooks.selectedIndex);}

Среда Tide автоматически выполняет инициализацию коллекции, когда это необходимо, вам просто нужно привязать ее к любому компоненту данных Flex, например List . Еще раз весь обычный стандартный код, необходимый для работы с данными и коллекциями, полностью исчез.

 

Интеграция с проверкой бина

В нашем приложении все еще отсутствует критическая часть: проверка данных. В качестве первого шага мы можем использовать интеграцию Hibernate с Bean Validation и просто аннотировать наши объекты, чтобы сервер мог выполнять проверку:

@Entitypublic class Author extends AbstractEntity {        @Basic    @Size(min=2, max=25)    private String name;    @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY, mappedBy="author", orphanRemoval=true)    @Valid    private Set books = new HashSet();        // Getters/setters    ...}@Entitypublic class Author extends AbstractEntity {    @Basic    @Size(min=2, max=100)    private String title;        ...}

 

После того, как вы измените это и повторно развернете, создание недействительного автора теперь стало невозможным, но есть сообщение об ошибке — беспорядок, который не может быть понят реальным пользователем. Мы могли бы просто добавить определенное поведение в обработчик ошибок:

function(event:TideFaultEvent):void {    if (event.fault.faultCode == 'Validation.Failed') {        // Do something interesting, for example show the first error message        Alert.show(event.fault.extendedData.invalidValues[0].message);    }    else        Alert.show(event.fault.toString());}

 

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

К счастью, с GraniteDS 2.2 это может быть намного проще. Если вы посмотрите на сгенерированную сущность ActionScript 3 для Author (на самом деле ее родительский класс AuthorBase.as находится в flex / target / generate-sources), вы заметите, что сгенерированы аннотации, соответствующие аннотациям Java Bean Validation.

Компонент FormValidator может использовать эти аннотации и автоматически обрабатывать проверку на стороне клиента, но сначала мы должны указать компилятору Flex, что он должен хранить эти аннотации в скомпилированных классах, что по умолчанию не имеет места. В модуле Flex pom.xml вы можете найти раздел, просто добавьте аннотации проверки, которые мы используем здесь:

<keepAs3Metadata>NotNull</keepAs3Metadata><keepAs3Metadata>Size</keepAs3Metadata><keepAs3Metadata>Valid</keepAs3Metadata>

 

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

private function updateAuthor():void {    if (!fvAuthor.validateEntity())        return;        authorService.updateAuthor(...);}

 

Интеграция с Spring Security

Все хорошо, но любой может изменить что угодно в нашей сверхкритической базе данных книг, так что пришло время добавить немного безопасности. Архетип Maven включает в себя простую установку Spring Security 3 с двумя пользователями admin / admin и user / user, которая явно не подходит для любого реального приложения, но которую мы можем просто использовать в качестве примера. Первым шагом является добавление аутентификации, и мы можем, например, повторно использовать простую форму входа Login.mxml, предоставленную архетипом. Нам просто нужна логика для переключения между формой входа и приложением, поэтому мы создаем новый основной mxml, переименовывая существующий Main.mxml в Home.mxml и создавая новый Main.mxml :

<s:Applicationxmlns:fx="http://ns.adobe.com/mxml/2009"xmlns:s="library://ns.adobe.com/flex/spark"xmlns:mx="library://ns.adobe.com/flex/mx"    xmlns="*"    controlBarVisible="{identity.loggedIn}"    preinitialize="Spring.getInstance().initApplication()"    currentState="identity.loggedIn ? 'loggedIn' : ''"    creationComplete="init()">       <fx:Script>        <![CDATA[            import org.granite.tide.spring.Spring;            import org.granite.tide.spring.Identity;            import org.granite.tide.service.DefaultServiceInitializer;                        [Bindable] [Inject]            public var identity:Identity;                        private function init():void {                // Define service endpoint resolver                Spring.getInstance().getSpringContext().serviceInitializer = new DefaultServiceInitializer('/gdsspringflex');                                // Check current authentication state                identity.isLoggedIn();            }        ]]>    </fx:Script>        <s:states>        <s:State name=""/>        <s:State name="loggedIn"/>    </s:states>        <s:controlBarContent>        <s:Label text="Spring Flex GraniteDS example" fontSize="18" fontWeight="bold" width="100%"/>        <s:Button label="Logout" click="identity.logout();"/>    </s:controlBarContent>     <Login id="loginView" excludeFrom="loggedIn"/>    <Home id="homeView" includeIn="loggedIn"/></s:Application>

 

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

  1. Мы снова используем удобные состояния Flex 4 для отображения формы входа или приложения и привязываем текущее состояние к свойству loggedIn компонента Tide Identity, которое представляет текущее состояние аутентификации. Если вы напоминаете о конфигурации безопасности Spring, то это клиентский аналог компонента идентификации приливов, который мы там объявили.
  2. Мы вызываем identity.isLoggedIn () при запуске приложения, чтобы определить, аутентифицирован ли уже пользователь, поэтому, например, при обновлении браузера не будет повторно отображаться форма входа. Это также может быть полезно, когда аутентификация выполняется через простую веб-страницу, и вы просто хотите получить состояние аутентификации вместо отображения формы входа Flex.

Кроме удаления инициализации Tide, нам также нужно внести небольшое изменение в Home.mxml, так как он больше не является основным mxml:

<s:VGroupxmlns:fx="http://ns.adobe.com/mxml/2009"xmlns:s="library://ns.adobe.com/flex/spark"xmlns:mx="library://ns.adobe.com/flex/mx"    xmlns="*">        <fx:Metadata>[Name]</fx:Metadata>       <fx:Script>        <![CDATA[            import mx.controls.Alert;            import mx.collections.ArrayCollection;            import org.granite.tide.events.TideResultEvent;            import org.granite.tide.events.TideFaultEvent;                        import org.example.entities.Author;            import org.example.services.AuthorService;                        [Inject]            public var authorService:AuthorService;        ]]>    ...</s:VGroup>

Метаданные [Имя] указывают, что этот mxml должен управляться Tide (т. Е. Внедрение, наблюдатели …). Без этого ничто больше не будет работать.

 

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

@Transactional@Secured("ROLE_ADMIN")public void deleteAuthor(Long id) {    Author author = entityManager.find(Author.class, id);    entityManager.remove(author);}

 

Вы можете проверить, что вы не можете удалить автора, войдя в систему как пользователь. Сообщение об ошибке обрабатывается нашим обработчиком ошибок и отображается как предупреждение. Немного утомительно обрабатывать это в каждом обработчике сбоев, поэтому вы можете определить собственный обработчик исключений, который будет глобально перехватывать такие ошибки безопасности, которые всегда имеют код неисправности Server.Security.AccessDenied:

public class AccessDeniedExceptionHandler implements IExceptionHandler {        public function accepts(emsg:ErrorMessage):Boolean {        return emsg.faultCode == 'Server.Security.AccessDenied';    }    public function handle(context:BaseContext, emsg:ErrorMessage):void {        // Do whatever you want here, for example a simple alert        Alert.show(emsg.faultString);    }}

И зарегистрируйте этот обработчик в основном mxml с помощью:

Spring.getInstance().addExceptionHandler(AccessDeniedExceptionHandler);

Теперь ошибки авторизации будут корректно обрабатываться и отображаться для всех удаленных вызовов.

 

Было бы еще лучше, если бы мы даже не отображали кнопку «Удалить» нашему пользователю, если ему не разрешено ее использовать. Очень легко скрыть или отключить части пользовательского интерфейса в зависимости от прав доступа пользователя, используя компонент Identity, который имеет несколько методов, похожих на теги jsp Spring Security:

<s:Button label="Delete" visible="{identity.ifAllGranted('ROLE_ADMIN')}" includeInLayout="{identity.ifAllGranted('ROLE_ADMIN')}" click="..."/>

 

Наконец, если вам удастся настроить Spring Security ACL (я даже не буду пытаться показать это здесь, для этого потребуется полная статья), вы можете использовать защиту объекта домена и защитить каждый экземпляр автора отдельно (8 — битовая маска Spring Security ACL для «удаления»):

<s:Button label="Delete" visible="{identity.hasPermission(author, '8')}" includeInLayout="{identity.hasPermission(author, '8')}" click="..."/>

 

Как и в случае с проверкой, вы можете видеть, что большая часть значения GraniteDS находится в библиотеках Flex, которые он предоставляет. Этот уровень интеграции не может быть достигнут с помощью серверной структуры.

Передача данных

Последнее, что я продемонстрирую, — это возможность рассылать обновления сущности всем подключенным клиентам. Это может быть полезно для часто обновляемых данных, поэтому у каждого пользователя есть актуальное представление без необходимости нажимать какую-либо кнопку «Обновить». Включение этого включает несколько шагов:

  1. Определите тему обмена сообщениями GraniteDS в конфигурации Spring. Архетип уже определяет тему с именем welcomeTopic , мы можем просто повторно использовать ее и, например, переименовать в authorTopic .
  2. Добавьте прослушиватель сущности DataPublishListener к нашим сущностям Author и Book . В нашем примере они уже расширяют класс AbstractEntity, предоставляемый архетипом, так что это уже так.
  3. Сконфигурируйте клиент DataObserver для темы в основном mxml и привяжите его подписку / отписку к событиям входа / выхода, чтобы публикация могла зависеть от безопасности:
    Spring.getInstance().addComponent("authorTopic", DataObserver);Spring.getInstance().addEventObserver("org.granite.tide.login", "authorTopic", "subscribe");Spring.getInstance().addEventObserver("org.granite.tide.logout", "authorTopic", "unsubscribe");
  4. Аннотируйте все сервисные интерфейсы (или все реализации) с помощью @DataEnabled, даже если они доступны только для чтения:
    @RemoteDestination@DataEnabled(topic="authorTopic", params=ObserveAllPublishAll.class, publishMode=PublishMode.ON_SUCCESS)public interface AuthorService {    ...}

 

Класс ObserveAllPublishAll по умолчанию происходит из архетипа и определяет политику публикации, где все получают все. Альтернативные стратегии диспетчеризации могут быть определены, если необходимо ограничить набор получателей в зависимости от самих данных по соображениям безопасности, функциональности или производительности.

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

Новые и удаленные авторы не распространяются автоматически, мы должны обработать эти два случая вручную. Это не очень сложно, мы просто должны наблюдать некоторые встроенные события, отправляемые Tide:

[Observer("org.granite.tide.data.persist.Author")]public function persistAuthorHandler(author:Author):void {    authors.addItem(author);}[Observer("org.granite.tide.data.remove.Author")]public function removeAuthorHandler(author:Author):void {    var idx:int = authors.getItemIndex(author);    if (idx >= 0)        authors.removeItemAt(idx);}

 

Хорошо, теперь список корректно обновляется новыми и удаленными авторами, но вы заметите, что новые авторы добавляются дважды в текущем пользовательском приложении. Действительно, мы добавляем его как из наблюдателя данных, так и из обработчика результата. Мы можем безопасно хранить только глобального наблюдателя, поскольку он всегда будет вызываться и удалять addItem из обработчика. Такие наблюдатели могут быть представлены во многих точках зрения одновременно, и все они будут обновлены.

Вывод

Это была длинная статья, но, по крайней мере, я надеюсь, что она дала вам хорошее представление о концепции GraniteDS для платформы клиент-сервер RIA. Приложения могут быть написаны быстрее с гораздо меньшим количеством кода и при этом имеют чистую архитектуру с четко разделенными слоями.