- PVSM.RU - https://www.pvsm.ru -

Генерация DTO и remote интерфейсов из Java в ActionScript

Дано web приложение на Java и Flex. Для связи используется Blaze DS или подобная технология, использующая AMF сериализацию. На стороне сервера и на стороне клиента явно или неявно присутствуют DTO (data transfer objects) и интерфейсы remote сервисов. В подобных приложениях стоит проблема синхронизации кода DTO между клиентом и сервером. Конечно, если приложение полностью покрыто тестами, рассинхронизация между Java и ActionScript исходниками выявится во время тестирования, но есть возможность получить feedback еще раньше – уже во время компиляции.

Для этого нужно генерировать DTO и remote интерфейсы каждый раз при сборке. В случае изменения интерфейсов DTO или remote интерфейсов на сервере они соответствующим образом изменятся и на клиенте. Это позволит узнать о проблемах уже на этапе компиляции, не дожидаясь тестов, кроме того, нельзя исключать вероятности, что тесты не полностью покрывают исходный код.
Для генерации AS DTO есть множество библиотек, например clear toolkit [1], pimento [2] или Gas3 [3] из Granite DS.

Библиотека Gas3 интегрирована в flexmojos [4], она позволяет редактировать шаблоны для генерации классов и выглядит более предпочтительно. Далее в примерах используется именно эта библиотека.

Генерация DTO

Для примера генерации DTO возьмем java класс SampleDto.

public class SampleDto {
    public String field1;
    public String field2;

    public String getField1() {
        return field1;
    }

    public void setField1(String field1) {
        this.field1 = field1;
    }

    public String getField2() {
        return field2;
    }

    public void setField2(String field2) {
        this.field2 = field2;
    }
}

Разместим этот класс в maven модуле git.example.com:jar:jar.
Генерировать ActionScript код будем в модуль git.example.com:flex:swf. Для того чтобы сгенерировать AS код из SampleDto.java нужно в настройке flexmojos добавить следующее:

<plugin>
    <groupId>org.sonatype.flexmojos</groupId>
    <artifactId>flexmojos-maven-plugin</artifactId>
    <version>3.9</version>
    <extensions>true</extensions>
    <configuration>
        <includeJavaClasses>
            <includeJavaClass>com.example.*</includeJavaClass>
        </includeJavaClasses>
    </configuration>

    <executions>
        <execution>
            <id>generate</id>
            <goals>
                <goal>generate</goal>
            </goals>
        </execution>
    </executions>
</plugin> 

Все генерируемые классы состоят из двух частей: самого класса и предка (с постфиксом Base). В нашем случае после генерации мы получим классы SampleDto и SampleDtoBase. Основной класс генерируется в случае, если класса с таким именем не существует. Предок генерируется всегда и перезаписывает старый файл, если он есть. Всю дополнительную функциональность нужно добавлять только в основной класс, если он есть, в этом случае генератор его не тронет. Если добавить новое поведение в Base класс, тогда генератор все удалит.

[Bindable]
[RemoteClass(alias="com.example.SampleDto")]
public class SampleDto extends SampleDtoBase {
}

[Bindable]
public class SampleDtoBase implements IExternalizable {

    public var _field1:String;
    public var _field2:String;

    public function set field1(value:String):void {
        _field1 = value;
    }

    public function get field1():String {
        return _field1;
    }

    public function set field2(value:String):void {
        _field2 = value;
    }

    public function get field2():String {
        return _field2;
    }

    public function readExternal(input:IDataInput):void {
        _field1 = input.readObject() as String;
        _field2 = input.readObject() as String;
    }

    public function writeExternal(output:IDataOutput):void {
        output.writeObject(_field1);
        output.writeObject(_field2);
    }
}

Генерация remote интерфейса

Теперь сгенерируем remote интерфейс. Создадим модуль git.example.com:jar:jar java класс SampleService и проаннотируем его.

@RemoteDestination(id = "sampleService", channel = "amf")
public class SampleService {

    public final String sampleMethod1() {
        return "test output";
    }

    public final String getSampleField1(@Param("sampleDto") final SampleDto sampleDto) {
        return sampleDto.getField1();
    }

    public final String getSampleField2(@Param("sampleDto") final SampleDto sampleDto) {
        return sampleDto.getField2();
    }

    @IgnoredMethod
    public final void ignoredMethod() {
    }
} 

Аннотация @RemoteDestination описывает канал и имя end point’а для сервиса и вешается на класс. @Param задает имя параметра в сгенерированном методе, но она не обязательна. @IgnoredMethod говорит генератору игнорировать помеченный метод.
Для генерации используем те же настройки, что и для DTO. На выходе получаем два класса:

[RemoteClass(alias="com.example.SampleService")]
public class SampleService extends SampleServiceBase {
}

public class SampleServiceBase extends RemoteObject {

    private var _initRemote:Boolean = false;

    private function initRemote():void {
        destination = "sampleService";
        channelSet = new ChannelSet();
        channelSet.addChannel(ServerConfig.getChannel("amf"));
        _initRemote = true;
    }

    public function sampleMethod1():void {
        if (!_initRemote)
            initRemote();
        getOperation("sampleMethod1").send();
    }

    public function getSampleField2(sampleDto:SampleDto):void {
        if (!_initRemote)
            initRemote();
        getOperation("getSampleField2").send(sampleDto);
    }

    public function getSampleField1(sampleDto:SampleDto):void {
        if (!_initRemote)
            initRemote();
        getOperation("getSampleField1").send(sampleDto);
    }

    public function addOperationListener(op:Function, type:String, handler:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void {
        if (op == this.sampleMethod1)
            this.getOperation("sampleMethod1").addEventListener(type, handler, useCapture, priority, useWeakReference);
        if (op == this.getSampleField2)
            this.getOperation("getSampleField2").addEventListener(type, handler, useCapture, priority, useWeakReference);
        if (op == this.getSampleField1)
            this.getOperation("getSampleField1").addEventListener(type, handler, useCapture, priority, useWeakReference);
    }

    public function removeOperationListener(op:Function, event:String, handler:Function):void {
        if (op == this.sampleMethod1)
            this.getOperation("sampleMethod1").removeEventListener(event, handler);
        if (op == this.getSampleField2)
            this.getOperation("getSampleField2").removeEventListener(event, handler);
        if (op == this.getSampleField1)
            this.getOperation("getSampleField1").removeEventListener(event, handler);
    }
}

Шаблоны для генерации

Gas3 позволяет модифицировать шаблоны для генерации. Изменим шаблон для генерации remote интерфейсов, чтобы иметь возможность вешать Responder’ы на методы. Шаблоны пишутся на groovy и достаточно объемные, поэтому они не представлены в статье, желающие могут посмотреть шаблоны в репозитории для этого примера тут [5]. Для модификации шаблонов нужно добавить в конфигурацию flexmojos секцию templates:

<templates>
    <base-remote-template>
        ${project.basedir}/src/main/generator-templates/remoteBase.gsp
    </base-remote-template>
</templates>

В результате получаем такой код сервиса:

public class SampleServiceBase extends RemoteObject {
    private static const logger:ILogger = Log.getLogger(getQualifiedClassName(SampleService).replace("::", "."));
    private var _initRemote:Boolean = false;

    public function SampleServiceBase() {
        super();
    }

    private function initRemote():void {
        destination = "sampleService";
        channelSet = new ChannelSet();
        channelSet.addChannel(ServerConfig.getChannel("amf"));
        _initRemote = true;
    }

    public function getSampleField1(sampleDto:SampleDto, responder:IResponder = null):AsyncToken {
        if (!_initRemote) {
            initRemote();
        }

        var asyncToken:AsyncToken = getOperation("getSampleField1").send(sampleDto);

        if (responder) {
            asyncToken.addResponder(responder);
        }

        if (Log.isDebug()) {
            logger.debug("Method <getSampleField1> invoked with parameters <{0}>", sampleDto);
        }

        return asyncToken;
    }

    public function sampleMethod1(responder:IResponder = null):AsyncToken {
        if (!_initRemote) {
            initRemote();
        }

        var asyncToken:AsyncToken = getOperation("sampleMethod1").send();

        if (responder) {
            asyncToken.addResponder(responder);
        }

        if (Log.isDebug()) {
            logger.debug("Method <sampleMethod1> invoked with parameters ");
        }

        return asyncToken;
    }

    public function getSampleField2(sampleDto:SampleDto, responder:IResponder = null):AsyncToken {
        if (!_initRemote) {
            initRemote();
        }

        var asyncToken:AsyncToken = getOperation("getSampleField2").send(sampleDto);

        if (responder) {
            asyncToken.addResponder(responder);
        }

        if (Log.isDebug()) {
            logger.debug("Method <getSampleField2> invoked with parameters <{0}>", sampleDto);
        }

        return asyncToken;
    }

    public function addOperationListener(op:Function, type:String, handler:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void {
        if (op == this.getSampleField1) {
            this.getOperation("getSampleField1").addEventListener(type, handler, useCapture, priority, useWeakReference);
        }
        if (op == this.sampleMethod1) {
            this.getOperation("sampleMethod1").addEventListener(type, handler, useCapture, priority, useWeakReference);
        }
        if (op == this.getSampleField2) {
            this.getOperation("getSampleField2").addEventListener(type, handler, useCapture, priority, useWeakReference);
        }
    }

    public function removeOperationListener(op:Function, event:String, handler:Function):void {
        if (op == this.getSampleField1) {
            this.getOperation("getSampleField1").removeEventListener(event, handler);
        }
        if (op == this.sampleMethod1) {
            this.getOperation("sampleMethod1").removeEventListener(event, handler);
        }
        if (op == this.getSampleField2) {
            this.getOperation("getSampleField2").removeEventListener(event, handler);
        }
    }
}

Кроме remote интерфейсов Gas3 дает возможность менять шаблоны для java интерфейсов, для обычных bean’ов и для JPA entity bean’ов.

Итог

Как результат:

  1. Мы получили возможность узнавать о несоответствии java и actionscript кода. В случае изменения java кода, actionscript изменится автоматически, и все участки кода, работающие со старой версией классов, не смогут скомпилироваться.
  2. Мы избавились от необходимости генерировать AS DTO и remote интерфейсы вручную.

Для удобства мною создано web-приложение, работающее с этими сущностями. Исходный код можно посмотреть тут [6].

Материалы

p.s. Я в курсе нехороших тенденций в области flash/flex, но может быть кому-то будет полезен мой опыт.

Автор: dmmm


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/8565

Ссылки в тексте:

[1] clear toolkit: http://sourceforge.net/projects/cleartoolkit/

[2] pimento: http://www.spicefactory.org/

[3] Gas3: http://www.graniteds.org/public/docs/2.3.0/docs/reference/en-US/html/graniteds.validation.html#validation.gas3

[4] flexmojos: http://flexmojos.sonatype.org/

[5] тут: https://github.com/dmalch/sample-generate-flex-dto-and-services/blob/master/flex/src/main/generator-templates/remoteBase.gsp

[6] тут: https://github.com/dmalch/sample-generate-flex-dto-and-services

[7] http://www.graniteds.org/confluence/display/DOC/3.+Gas3+Code+Generator: http://www.graniteds.org/confluence/display/DOC/3.+Gas3+Code+Generator