All Articles ↓
3 месяца назад

CUBA: подготовка к эксплуатации

"Проект работает на моем компьютере!” Сегодня это практически мем, но проблема “среда разработки vs реальность” все еще актуальна. Будучи разработчиком, вы всегда должны помнить, что когда-нибудь ваше приложение перейдет в промышленную эксплуатацию. В этой статье мы поговорим о некоторых особенностях CUBA, которые помогут вам избежать многих проблем, связанных с запуском приложения “в прод”.

Как писать код: общие рекомендации

Используйте сервисы

Почти каждое приложение CUBA реализует определенные алгоритмы бизнес-логики. И наилучшее место для этих алгоритмов - сервисы CUBA, в которые нужно перенести бизнес-логику из всех остальных классов - контроллеров экрана, слушателей приложений и т.д. У этого подхода есть определенные преимущества:

  1. Будет только одна реализация бизнес-логики, которая находится в одном месте
  2. Эту бизнес-логику можно вызывать из разных мест приложения, а также публиковать ее как REST-сервис.

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

if (item.isOld()) {
   itemService.doPlanA(item);
} else {
   itemService.doPlanB(item);
}

Если вы видите такой код, возможно, его стоит переместить из контроллера экрана в "itemService" как отдельный метод "processItem (Item item)", поскольку он выглядит как часть бизнес-логики. После этого код в контроллере будет выглядеть так:

Item item = itemService.findItem(itemDate);
itemService.processItem(item);

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

Не храните состояние в сервисах

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

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

Если нужно дать доступ к каким-то данным, используйте базу данных или отдельное in-memory хранилище, например Redis.

Используйте логирование

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

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

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

CUBA использует проверенное сочетание библиотек slf4j и logback. Вам просто нужно инжектировать возможность логирования в код класса:

@Inject
private Logger log;

А затем просто вызываете сервис логирования в коде:

log.info("Transaction for the customer {} has succeeded at {}", customer, transaction.getDate());

Помните, что сообщения в логах должны быть осмысленными и содержать достаточно информации, чтобы понять, что произошло в приложении. В серии статей "Clean code, clean logs" можно найти больше информации по логированию в Java-приложениях. А еще рекомендуем взглянуть на статью “9 Logging Sins”.

Помимо “обычных” логов в CUBA есть логи со статистикой производительности, поэтому вы всегда видите, как приложение потребляет ресурсы сервера. Это здорово пригодится, когда техподдержка начнет получать жалобы пользователей на медленную работу приложения. С такими логами вы быстрее обнаружите узкое место.

Обрабатывайте исключения

Программные исключения очень важны, потому что они несут ценную информацию, когда в приложении что-то идет не так, как было задумано. Поэтому правило номер один - никогда не игнорировать ошибки. Используйте метод log.error(), создайте продуманное сообщение, добавьте контекст и стек вызовов. Эта информация - единственный источник данных, которые помогут вам определить, что произошло в программе. Если у вас есть какие-то соглашения по написанию для кода, обязательно добавьте к ним правила обработки ошибок.

Рассмотрим пример - загрузка аватара в приложение. Аватарка будет сохранена в хранилище файлов CUBA и API службы загрузки файлов.

Никогда не делайте так:

        try {
            fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd); 
        } catch (Exception e) {}

Если появляется ошибка, никто не узнает об этом, и пользователи очень сильно удивляются, когда не видят свою фотографию в профиле.

Так немного лучше, но далеко не идеально.

        try {
            fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd); 
        } catch (FileStorageException e) {
            log.error (e.getMessage)
        }

В логах появится сообщение об ошибке, и мы будем получать только определенные классы исключений. Но не будет никакой информации о контексте: как назывался файл, кто пытался его загрузить. Более того, не будет никакого стек-трейса, поэтому будет непросто найти, где выскочило исключение. И еще кое-что - пользователь не будет уведомлен о проблеме.

Такой вариант можно считать относительно хорошим подходом.

        try {
            fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd); 
        } catch (FileStorageException e) {
            throw new RuntimeException("Error saving file to FileStorage", e);
        }

Мы знаем ошибку, не теряем исходное исключение, добавляем информативное сообщение. Вызывающий метод будет уведомлен об исключении. Можно было бы добавить текущее имя пользователя и, возможно, имя файла к сообщению, чтобы было больше данных о контексте. Это пример web-модуля CUBA и тут предполагается, что исключение RuntimeException будет перехвачено в коде контроллера экрана.

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

Конфигурация для конкретной среды исполнения

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

Используйте подходящую реализацию сервисов

Любой сервис в CUBA состоит из двух частей: интерфейса (service API) и его реализации. Иногда реализация может зависеть от среды развертывания. В качестве примера используем службу хранения файлов.

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

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

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

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

<beans profile="prod">
   <bean name="cuba_FileStorage" class="com.haulmont.addon.cubaaws.s3.AmazonS3FileStorage"/>
</beans>

И реализация S3 будет автоматически включена при настройке свойства:

spring.profiles.active=prod

Поэтому при разработке приложения на CUBA постарайтесь определить, что зависит от среды развертывания приложения, и сделать для каждого случая соответствующую реализацию. Старайтесь не писать так:

If (“prod”.equals(getEnvironment())) {
   executeMethodA();
} else {
   executeMethodB();
}

Попробуйте реализовать отдельный сервис "myService, в котором определен один методexecuteMethod()" и создайте для него две реализации. После этого сконфигурируйте приложение с помощью профилей. Ваш код теперь будет выглядеть так:

myService.executeMethod();

Чище, проще и легче поддерживать.

Храните настройки во внешних файлах

Старайтесь по возможности переносить настройки приложения в файлы свойств. Если какой-то параметр может измениться в будущем (даже если эта вероятность невелика), всегда выносите его в настройки. Старайтесь не хранить URL-адресы подключений, имена хостов и т. д. как простые строки в коде приложения, и уж тем более никогда не разносите их по коду при помощи копирования. В этом случает, чтобы изменить такое жестко заданное значение в коде, придется приложить очень много усилий. Адрес почтового сервера, размер иконки фото пользователя, количество попыток соединения при отсутствии сетевого подключения - все это примеры свойств, которые необходимо переносить в настройки. Для использования свойств в коде приложения используйте интерфейсы настройки, инжектируя их в классы.

Используйте профили, чтобы хранить свойства, специфичные для среды исполнения, в отдельных файлах.

Допустим, в вашем приложении есть платежный шлюз. Конечно, во время разработки не стоит использовать настоящие деньги для тестирования реализуемой функциональности. Следовательно, нужно сделать заглушку шлюза для локальной среды, тестовый API на стороне шлюза для среды предварительного тестирования и использовать реальный шлюз для промышленной эксплуатации приложения. И адреса шлюзов для этих сред, очевидно, разные.

Не пишите код вот так:

If (“prod”.equals(getEnvironment())) {
      gatewayHost = “gateway.payments.com”;
} else if (“test”.equals(getEnvironment())) {
      gatewayHost = “testgw.payments.com”;
} else {
      gatewayHost = “localhost”;
}
connectToPaymentsGateway(gatewayHost);

Вместо этого определите три файла свойств: dev-app.properties, test-app.properties и prod-app.properties, и определите в них три разных значения для параметра database.host.name.

После этого опишите интерфейс конфигурации:

@Source(type = SourceType.DATABASE)
public interface PaymentGwConfig extends Config {

    @Property("payment.gateway.host.name")
    String getPaymentGwHost();
}

Затем инжектируйте интерфейс и используйте его в коде:

@Inject
PaymentGwConfig gwConfig;

//service code

connectToPaymentsGateway(gwConfig.getPaymentGwHost());

Этот код проще и не зависит от окружения, все настройки находятся в файлах свойств, не нужно искать их в коде, если что-то изменилось.

Обрабатывайте тайм-ауты при работе с сетью

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

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

Существует два типа тайм-аутов:

  1. Тайм-аут соединения
  2. Тайм-аут чтения

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

Значения тайм-аута - хороший пример того, что нужно переместить в файлы свойств. Всегда настраивайте их для всех сервисов, которые взаимодействуют через сеть. Ниже приведен пример определения сервиса:

<bean id="paymentGwConfig" class="com.global.api.serviceConfigs.GatewayConfig">
    <property name="connectionTimeout" value="${xxx.connectionTimeoutMillis}"/>
    <property name="readTimeout" value="${xxx.readTimeoutMillis}"/>
</bean>

Включайте в свой код специальные секции для обработки тайм-аутов.

Рекомендации по работе с базами данных

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

Создавайте скрипты специально для среды развертывания

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

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

Совет: всегда делайте резервное копирование базы данных перед обновлением. Это сэкономит вам массу времени и нервов при решении любых проблем.

Учитывайте Multitenancy

Если вы собираетесь делать ваш проект мультитенантным приложением, стоит принять это во внимание на ранних стадиях разработки.

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

Обратите внимание, что добавление функционала Multitenancy в приложение, которое уже работает в продакшене, может быть осложнено упомянутыми выше особенностями.

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

Безопасность

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

Пишите безопасный код

Безопасность начинается с кода, который предотвращает проблемы. Здесь вы найдете очень хороший сборник о безопасном кодировании от Oracle. Ниже приведены некоторые (возможно, очевидные) рекомендации из этого руководства.

Пункт 3-2 / INJECT-2: Избегайте динамического SQL

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

       try (Transaction tx = persistence.createTransaction()) {
            // get EntityManager for the current transaction
            EntityManager em = persistence.getEntityManager();
            // create and execute Query
            Query query = em.createQuery(
                    "select sum(o.amount) from sample_Order o where o.customer.id = :customerId");
            query.setParameter("customerId", customerId);
            result = (BigDecimal) query.getFirstResult();
            // commit transaction
            tx.commit();
        }

Пункт 5-1 / INPUT-1: Валидация входных данных

Нужно проверять входные данные из ненадежных источников перед использованием. Вредоносные входные данные могут вызывать проблемы независимо от того, поступают они через аргументы метода или внешние потоки ввода. Примеры тому - выход за пределы целых значений и атаки с обходом каталогов путем включения последовательности "../ " в именах файлов. CUBA позволяет использовать валидаторы в GUI помимо проверок в коде.

Это лишь несколько примеров принципов безопасного кодирования. Пожалуйста, внимательно прочитайте руководство, оно поможет во многом улучшить ваш код.

Думайте о защите персональных данных

Законодательство обязывает нас защищать определенные персональные данные. В Европе для этого есть стандарт GDPR, для применения в медицине в США есть требования HIPAA и т.д. Поэтому обязательно принимайте это во внимание при разработке приложения.

CUBA позволяет устанавливать различные права и ограничивать доступ к данным с помощью ролей и групп. Для групп можно задать различные ограничения для предотвращения несанкционированного доступа к персональным данным.

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

Изменяйте или отключайте пользователей и роли по умолчанию

Когда вы разрабатываете приложение на платформе CUBA, в системе создаются два пользователя: admin and anonymous. Всегда меняйте пароли по умолчанию до того, как приложением начнут пользоваться реальные пользователи. Это можно сделать вручную сразу после развертывания или добавить соответствующий оператор SQL в скрипт инициализации 30-....sql.

Используйте рекомендации из документации CUBA, которые помогут вам правильно настроить роли в рабочей среде.

Если у вас сложная организационная структура, подумайте о создании локальных администраторов для каждого отдела вместо нескольких “супер-администраторов” на уровне всей организации.

Экспортируйте роли в прод

Перед первым развертыванием обычно требуется скопировать роли и группы доступа с сервера разработки (или промежуточного) на рабочий. В CUBA есть встроенный административный интерфейс, чтобы не делать это вручную.

Для экспорта ролей и привилегий можно использовать экран Administration -> Roles. После скачивания файла вы можете загрузить его в рабочую версию приложения.

text

Для групп доступа существует аналогичный процесс, но используется экран Administration -> Access Groups.

text

Настройка приложения

Рабочая среда обычно отличается от среды разработки, и настройки приложения - тоже. Это означает, что нужно провести дополнительные проверки, чтобы убедиться, что приложение будет четко работать на проде.

Настройка логирования

Убедитесь, что вы правильно настроили подсистему логирования для продакшена: уровень логов установлен на нужный уровень (обычно это уровень INFO), и логи не удаляются при перезапуске приложения. За справкой по правильной настройке логирования обращайтесь к документации.

Если вы используете Docker, сконфигурируйте Docker volumes для хранения файлов с логами вне контейнера.

Для подробной аналитики можно развернуть специальные средства для сбора, хранения и анализа логов, например, ELK stack или Graylog. Рекомендуем установить инфраструктуру для логирования на отдельный сервер, чтобы это не влияло на производительность самого приложения.

Запуск в кластерной конфигурации

Приложения CUBA можно настроить для работы в кластерной конфигурации. Если вы ее используете, нужно внимательно отнестись к архитектуре приложения, иначе оно может повести себя непредсказуемо. Мы хотели бы обратить внимание на те свойства, которые нужно специально настраивать для кластерной среды:

Планировщик задач
Чтобы настроить в приложении регулярно выполняемые операции, например, ежедневную генерацию отчетов или еженедельную рассылку, можно использовать соответствующую встроенную функцию платформы. Но представьте себе, что вы получили три одинаковых рекламных письма. Раздражает, правда? Такое случается, если ваша задача выполняется на трех узлах кластера. Избежать таких ситуаций вам поможет планировщик задач CUBA, который позволяет создавать задачи в единственном экземпляре на весь кластер.

Распределенный кэш
Кэширование - это один из способов повысить производительность приложения. Иногда разработчики пытаются кэшировать все подряд, потому что сейчас память довольно дешевая. Но если приложение развернуто на нескольких серверах, кэш распределяется между серверами и должен синхронизироваться. Синхронизация происходит через довольно медленное сетевое соединение, что может увеличить время отклика. Совет - выполняйте нагрузочные тесты и измеряйте производительность, прежде чем изменять настройки кэширования, особенно в кластеризованной среде.

Заключение

CUBA Platform упрощает разработку, так что вы наверняка закончите проект и перейдете к развертыванию на проде раньше, чем ожидали. Но независимо от используемых вами средств разработки, развертывание - это непростая задача. И если начать задумываться о развертывании на ранней стадии разработки и следовать простым правилам, изложенным в этой статье, продакшен не доставит больших забот и вряд ли вызовет серьезные проблемы.

Андрей Беляев