Простой вызов удалённых сервисных методов в одностраничных приложениях

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

    Сокращённо, я называю этот подход «Json Remote Service Procedure Call» — JRSPC. (Не очень благозвучно, возможно, но из песни слова не выкинешь.)

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

    Например, на сервере, определение бизнес-метода выглядит так:

    @Component("testService")
    public class TestService{
        @Remote
        public String testMethod(Long userId, String role, boolean test,
                List<User> users, User user){    
                ...
               return "ok"; 
        }   
    }    
    


    а его вызов на клиенте — так:

    var params = [userId, role, true, [{id:1, login:"111"},  {id:2, login:"222"} ], {id:3, login:"333"}]
    Server.call("testService", "testMethod",   params,  sucessCallback, errorCallback, controlWhichWillDisabledUntilResponse);	
    


    Больше, при определении метода, нигде, никакого кода не пишется.


    Как это работает


    На транспортном уровне, jrspc — использует json-rpc, с возможностью указывать в вызове не только метод, но и сервис. Поэтому, такой json-rpc можно было бы назвать json-rspc (s-service).

    Если бы на него существовала спецификация, то она была бы похожа на спецификацию json-rpc 2.0, за исключением того, что в объекте запроса было бы добавлено поле «service», а поле «id» — было бы не обязательным, и в ответе — необязателен errorCode.

    Для демонстрации, я написал простое демо-приложение, в котором реализуются функциональности регистрации, логина, и изменения данных и прав пользователя.

    Клиентская часть


    Клиентская часть этого приложения — написана на фреймворке AngularJS.
    предупреждение
    (Считаю своим долгом — предупредить тех, кто ещё не пробовал писать на нём:
    {{user.name}}, Ангуляр — тяжёлый наркотик!
    Для попадения в зависимость от него — достатчно словить кайф всего один раз.)

    Для оформления используется Bootstrap.

    В серверной части — Spring.

    В качестве реализации объекта json, используется JSONObject из библиотеки json-lib.

    Клиентская часть состоит из трёх файлов:

    ajax-connector.js.

    Реализация механизма запросов к серверу, инкапсулированная в объекте Server.
    (Префикс ajax — используется, чтобы отличать его от вебсокетного ws-connector.js, которым он может быть заменён, без изменения кода user-controller.js.)

    user-controller.js

    Здесь находится бизнес-логика приложения, инкапсулированная в функции userController.

    application.html


    Графический интерфейс приложения с логикой блокировки элементов.

    Как видим, в представлении скриптового кода, удалённый сервер — выглядит как объект Server, который должен быть проинициализирован url'ом.

    Через этот объект, мы можем обращаться к любому компоненту на сервере и вызывать любые его методы, таким способом:

    Server.call(serviceName, mathodName, [param1, param2, ...], successCallBack, errorCallback, control);

    Ответы или ошибки — приходят в соответствующие коллбэки.

    Добавление нового сервиса или метода на сервере — никак не затрагивает клиентский код, и мы можем вызывать эти сервисы и методы сразу, после того как они появились в серверном коде.

    Естественно, сказав «любому и любые» — я немного отошёл от истины.
    На самом деле, как удалённые сервисы, вызываться могут только классы, производные от AbstractService, а вызываемые удалённо методы, должны быть аннотированы @Remote.

    Для ограничения прав доступа к методам — используется аннотация @Secured(roleName).
    Так, например, метод, аннотированный @Secured("Admin") — не может быть вызван пользователем с ролью «User».

    Cерверная часть


    Весь серверный «фреймворк», если можно так выразиться, занимает меньше 9 кб., и состоит из шести классов, два из которых — уже знакомые нам аннотации: Remote и Secured, а также AbstractService
    абстрактный класс, от которого должны наследоваться все сервисы, и CommonServiceController

    В его метод processAjaxRequest приходят запросы из скриптового объекта Service.

    Далее, находится компонент, по имени сервиса, и на нём, после проверки прав доступа, рефлективно, вызвается указанный метод.

    User (entity), для хранения данных о пользователе, и UserManager, для операций с объектом User (тестовая реализация с эмуляцией персистентности).

    Бизнес-логика реализована в двух сервисах: TestUserService — сервис с методами для регистрации, логина, и редактирования данных, и TestAdminService — сервис с методами для удаления юзера, и изменения его роли.

    Код написан максимально self-explanatory, поэтому надеюсь, что разобраться в нём будет легко.

    Код демо-приложения на Гитхабе.

    Что дальше?


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

    Update2:
    Update1 — перемещён в тело статьи.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 20

      +1
      Ну, это решение в лоб, ничего вообще-то нового :)
      Я делаю на клиенте так-же, а на сервере без каких-либо объектов простой switch($nameFunction) и вызовы фактических функций.
        0
        Вот этот switch($nameFunction) — как раз и есть уровень конфигурации сервиса, который хочется упразднить.
        Дописав новую функцию в сервис, ещё нужно не забыть внести изменения в switch.
        Так что, это ещё вопрос, какое решение более лобовое).
          0
          Избавиться от этого switch очень просто.
          Имеем вызов '?method=user.login&data=...'
          Собираем входные параметры и смотрим method.
          Сплит по точке — даст 2 имени.

          moduleName = splitted[0];
          methodName = splitted[1];

          Вызов всего этого добра:

          modulesMap[moduleName][methodName](params, callback)

          modulesMap отдельно можно описать отдельно, даже подгружать json файл.
            0
            В скриптовых языках так можно делать, но в Java, для этого нужно использовать Reflection.
            (кстати, я бы заменил сплит на добавление параметра module).
        0
        не туда
          0
          Простите, я наверное туплю, но никак не могу понять в чем разница в сравнении со стандартными подходами (spring mvc, jax rs)?
            0
            Если в двух словах — меньше кода.
              0
              Разве там много кода? ну, кроме нужной логики.
                0
                Продемонстрируйте свою реализацию, и тогда можно будет сравнить.
                  +1
                  Что то вроде такого

                  @RequestMapping("/reports")
                  @Controller
                  public class ReportsController {
                  
                      @RequestMapping(value = "execute", method = RequestMethod.GET)
                      @PreAuthorize("hasRole('REPORT_BUTTON')")
                      @ResponseBody
                      public Report executeReport(@RequestParam("reportId") Long reportId){
                           return reportsRepository.findOne(reportId);
                      }
                  }
                  
                    +1
                    В таком подходе, есть, как минимум, три недостатка:

                    1. Бизнес-логика реализована в контроллере, т.е.слои контроллеров и бизнес-логики — смешаны.

                    Почему это плохо:

                    До тех пор, пока мы вызываем смешанный с контроллером сервис каким-то одним способом,
                    например из браузерного скрипта, через ajax, это будет работать.
                    Но, как только мы захотим вызывать сервисные методы как api, из ява-клиента, например,
                    или заменить транспорт с http на вебсокеты, нам придётся переписать заново
                    все методы в смешанном слое.

                    В подходе предлагаемом мной, в этом случае, будет нужно внести изменения
                    всего в один класс — CommonServiceController, а вся бизнес-логика, вынесенная
                    в классы сервисов, останется абсолютно без изменений.

                    2. Предполагаю(поправьте, если ошибаюсь), что нет возможности передать в параметрах объект.
                    Т.е. получить объект репорт мы можем, а сохранить — нет.

                    3. Нет возможности передать код ошибки по отдельному каналу.
                    В случае, если метод бросит исключение, на клиенте получим статус ответа 400,
                    и сообщение переданное в конструкторе исключения.

                    Если использовать такое решение, то мы не сможем предупредить клиента,
                    о том что метод или сервис не найдены, или что роль пользователя не валидна для данного метода.
                      0
                      Где должна быть реализована бизнес-логика? Что реализуют контроллеры?
                        0
                        1. В классах сервисов. 2. Связь транспортного слоя с сервисами.
                        0
                        Я уже мало помню о jax rs, поэтому буду говорить о спринге
                        1. Контроллеры находятся в spring контексте. Поэтому вызвать можно из любого места где он доступен.
                        2. Передать объект можно. Причем как json, так и автоматом замапить на какую нибудь сущность.
                        3. Вроде же можно вернуть статус ошибки вместе с сообщением, в которое можно всунуть код ошибки. Я на счет этого 100% не уверен, но могу поискать инфу.
                          0
                          Но в общем я вижу плюс, если есть большое желание отделить бизнес логику от контроллеров. На мой взгляд это может пригодиться только если требуется несколько разных внешних интерфейсов, если не хочется каждый из них описывать. У меня такой задачи пока не встречалось.
                            0
                            Вообще, эта статья — «подводящая», к статье об организации вебсокетного взаимодействия с сервером,
                            в которой преимущество отделения логики от контроллера будет очевидным, и без которого реализовать
                            такое взаимодействие — просто не получится.
                              0
                              > Вообще, эта статья — «подводящая», к статье об организации вебсокетного взаимодействия с сервером,

                              О, отлично, ждем. Буквально на днях размышлял над этим вопросом в контексте Java на сервере.
            0
            Код обновлён. Теперь, в сервисных методах можно указывать любое количество параметров,
            любых типов (JSONObject в том числе), кроме массивов ( вместо массивов — нужно использовать списки).

            Смотрите Update1 в конце статьи.

            Only users with full accounts can post comments. Log in, please.