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

в 9:39, , рубрики: actionscript, dto, flash, Flash-платформа, flex, generator, java, метки: , , , , ,

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

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

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

Генерация 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 и достаточно объемные, поэтому они не представлены в статье, желающие могут посмотреть шаблоны в репозитории для этого примера тут. Для модификации шаблонов нужно добавить в конфигурацию 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-приложение, работающее с этими сущностями. Исходный код можно посмотреть тут.

Материалы

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

Автор: dmmm


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js