Русский
Русский
English
Статистика
Реклама

Spring boot

Перевод Разработка Spring Boot-приложений с применением архитектуры API First

10.04.2021 16:09:58 | Автор: admin
В этом материале я приведу практический пример реализации архитектуры API First с применением спецификации OpenAPI. А именно, сначала расскажу о том, как создал определение API, а затем о том, как, на основе этого определения, создал серверную и клиентскую части приложения. В процессе работы у меня возникли некоторые сложности, которых я тоже коснусь в этом материале.


Преимущества архитектуры API First


По мере того, как всё сильнее распространяется подход к разработке приложений, основанный на микросервисах, определённую популярность набирает и архитектура API First. У этой архитектуры имеется довольно много сильных сторон.

Чёткое определение контрактов


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

Хорошее документирование API


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

Создание благоприятных условий для распараллеливания работы над проектами


Это одна из моих любимых возможностей, получаемых тем, кто применяет архитектуру API First. Если в проекте имеется чёткое описание API, то тот, кто занимается разработкой самого API, и тот, кто пользуется возможностями этого API, могут работать параллельно. В частности, пользователь API, ещё до того, как сам API готов к работе, может, основываясь на уже согласованных контактах, создавать моки и заниматься своей частью работы над проектом.

Теперь, когда мы обсудили сильные стороны архитектуры API First, перейдём к практике.

Практика использования архитектуры API First


Я начал работу с посещения ресурса, дающего доступ к Swagger Editor, и с создания определения моего API. Я, при описании API, пользовался спецификацией OpenAPI 3. Определения API можно создавать и с применением многих других инструментов, но я выбрал именно Swagger Editor из-за того, что у меня есть опыт создания документации для Java-проектов с использованием этого редактора. Swagger Editor мне нравится и из-за того, что он поддерживает контекстно-зависимое автодополнение ввода (Ctrl + Пробел). Эта возможность очень помогла мне при создании определения API.

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

Для начала я создал весьма минималистичное определение API, описывающее создание учётной записи пользователя системы с использованием POST-запроса.


Определение API

Генерирование кода


После того, как подготовлено определение API, можно заняться созданием сервера и клиента для этого API. А именно, речь идёт о Spring Boot-приложении. Обычно я создаю такие приложения, пользуясь Spring Initializr. Единственной зависимостью, которую я добавил в проект, стал пакет Spring Web.

После этого я воспользовался Maven-плагином OpenAPI Generator, который позволяет создавать серверный и клиентский код.

Генерирование серверного кода


Для того, чтобы на основе определения API создать серверный код, я воспользовался соответствующим Maven-плагином, передав ему файл с описанием API. Так как мы создаём сервер, основанный на Spring, я, настраивая генератор, записал в параметр generatorName значение spring. Благодаря этому генератор будет знать о том, что он может, создавая серверный код, пользоваться классами Spring. Существует немало OpenAPI-генераторов серверного кода, их перечень можно найти здесь. Вот как выглядит конфигурационный файл плагина.


Содержимое конфигурационного файла для генерирования серверного кода

Среди настроек, которые я внёс в этот файл, хочу отметить указание имени пакета, которое будет использоваться при создании классов API и модели. Тут, в общем-то, имеется довольно много настроек, полное описание которых можно найти здесь. В частности, в подобном файле можно задать префикс имён классов модели

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

<dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>${springfox-version}</version></dependency><dependency><groupId>org.openapitools</groupId><artifactId>jackson-databind-nullable</artifactId><version>${jackson-databind-nullable}</version></dependency><dependency><groupId>io.swagger.core.v3</groupId><artifactId>swagger-annotations</artifactId><version>${swagger-annotations-version}</version></dependency>

А теперь самое интересное: генерирование кода. После выполнения команды mvn clean verify в нашем распоряжении оказываются несколько классов, сгенерированных плагином.


Результат работы генератора серверного кода

В пакете API имеются два основных интерфейса AccountApi и AccountApiDelegate. Интерфейс AccountApi содержит текущее определение API, оформленное с использованием аннотаций @postmapping. Там же имеется и необходимая документация API, представленная в виде аннотаций Spring Swagger. Классом, реализующим этот интерфейс, является AccountApiController. Он взаимодействует со службой, которой делегирована реализация возможностей, обеспечивающих работу API.

В нашей работе весьма важен интерфейс AccountApiDelegate. Благодаря ему мы можем пользоваться паттерном проектирования Delegate Service, который позволяет реализовывать процедуры обработки данных и механизмы выполнения неких действий, запускаемые при обращении к API. Именно в коде службы, созданной на основе этого интерфейса, и реализуется бизнес-логика приложения, ответственная за обработку запросов. После того, как будет реализована эта логика, сервер можно признать готовым к обработке запросов.

Теперь сервер готов к работе, а значит пришло время поговорить о клиенте.

Генерирование клиентского кода


Для генерирования клиентского кода я воспользовался тем же плагином, но теперь в параметр generatorName я записал значение java.


Содержимое конфигурационного файла для генерирования клиентского кода

Тут же я, в разделе configOptions, сделал некоторые настройки, в частности, касающиеся библиотеки, на которой должен быть основан REST-клиент. Здесь, для реализации механизма взаимодействия с сервером, я решил использовать библиотеку resttemplate. В разделе configOptions можно использовать и многие другие настройки.

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

<dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>${gson.version}</version></dependency><dependency><groupId>io.swagger.core.v3</groupId><artifactId>swagger-annotations</artifactId><version>${swagger-annotations-version}</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>${springfox-version}</version></dependency><dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId><version>${okhttp.version}</version></dependency><dependency><groupId>com.google.code.findbugs</groupId><artifactId>jsr305</artifactId><version>${jsr305.version}</version></dependency>


Результат работы генератора клиентского кода

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

Теперь надо создать экземпляр ApiClient. Он содержит параметры сервера, обмен данными с которым нужно наладить. Затем соответствующие возможности используются в классе AccountApi для выполнения запросов к серверу. Для того чтобы выполнить запрос, достаточно вызвать API-функцию из класса AccountApi.

На этом работа над клиентской частью проекта завершена. Теперь можно проверить совместную работу клиента и сервера.

Проблемы, выявленные в ходе реализации проекта


В последней версии плагина (5.0.1), которой я и пользовался, имеются некоторые проблемы.

  • При использовании генератора spring для создания контроллера в проект импортируются неиспользуемые зависимости из модуля Spring Data. В результате оказывается, что даже в том случае, если для организации работы соответствующей службы не нужна база данных, в проект, всё равно, попадают зависимости, предназначенные для работы с базами данных. Правда, если в проекте используется база данных, особых проблем вышеописанный недочёт не вызовет. Узнать о том, как продвигается работа по исправлению этой ошибки, можно здесь.
  • Обычно в разделе определения API components не описывают схемы запросов и ответов. Вместо этого в API используют ссылки на такие схемы, созданные с применением свойства $ref:. Сейчас этот механизм не работает так, как ожидается. Для того чтобы справиться с этой проблемой, можно описать встроенные (inline) схемы для каждого запроса и ответа. Это приводит к тому, что имена в модели генерируются с префиксом Inline*. Подробности об этой проблеме можно почитать здесь.

Если вы применяете более старую версию плагина, а именно 4.3.1, то знайте о том, что в ней этих проблем нет, работает она хорошо.

В этом репозитории вы можете найти код проекта, который мы обсуждали.

Применяете ли вы в своих проектах архитектуру API First?

Подробнее..

Опыт сопряжения Java, JavaScript, Ruby и Python в одном проекте посредством GraalVM

25.12.2020 04:13:23 | Автор: admin
В прошлом месяце вышла стабильная LTS-версия многоязычной среды выполнения GraalVM 20.3.0 от корпорации Oracle и мне захотелось испробовать её для решения какой-нибудь интересной практической задачи. Для тех кто не в курсе, приведу краткое описание этой новой платформы. GraalVM позволяет использовать в едином окружении различные популярные языки программирования и обеспечивает их разностороннее взаимодействие в рамках некоторой общей среды выполнения.


Схематическое изображение архитектуры GraalVM из официальной документации.

Добавление новых языков в GraalVM осуществляется с помощью специального фреймворка Truffle, выполненного в виде библиотеки Java. Фреймворк предназначен для создания реализаций языков программирования в качестве интерпретаторов для самомодифицируемых абстрактных синтаксических деревьев (AST). При желании на его основе можно создать собственный язык, в официальных репозиториях GraalVM подробно рассмотрен пример реализации такого проекта под названием SimpleLanguage. Интерпретаторы, которые были написаны с использованием фреймворка Truffle, будут автоматически использовать GraalVM как JIT-компилятор непосредственно для самой реализации языка запускаемой на JVM-платформе и, соответственно, иметь возможность взаимодействия и двустороннего обмена данными в одном и том же пространстве памяти посредством специально разработанного протокола и программного интерфейса Polyglot API.

Платформа GraalVM вместе с исполняемой программой на смеси самых разных языков может быть представлена в виде автономного и самодостаточного исполняемого файла, либо работать поверх OpenJDK, Node.js или даже внутри Oracle Database.

На текущий момент сразу из коробки поддерживаются следующие языки программирования и технологии:

  • Java, Kotlin, Scala и другие языки JVM-платформы.
  • JavaScript вкупе с платформой Node.js и сопутствующим инструментарием.
  • C, C++, Rust и другие языки, которые могут быть скомпилированы в LLVM bitcode.

Помимо этого, с помощью встроенного пакетного менеджера в дистрибутив GraalVM можно добавить поддержку:

  • Python
  • Ruby
  • R
  • WebAssembly

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

Кроме того, посредством ahead-of-time (AOT) компиляции имеется возможность создавать автономные исполняемые файлы, называемые нативными образами, которые используют не классическую Java VM, а более специализированную Substrate VM, реализующую компактную среду выполнения. В итоге программы запускаются значительно быстрее и расходуют гораздо меньше оперативной памяти. Но при этом теряются некоторые преимущества just-in-time (JIT) компиляции, доступной на классических платформах. Для формирования подобных нативных образов в большинстве случаев требуются значительные ресурсы CPU и десятки гигабайт оперативной памяти, поэтому их создание лучше всего производить на каких-нибудь мощных сборочных серверах или рабочих станциях.

Корпорация Oracle в настоящее время позиционирует GraalVM как единую и идеальную платформу для создания различных микросервисов. При этом она уже имеет влияние на развитие классического OpenJDK. Например, встраиваемый движок JavaScript под названием Nashorn уже удалён из JDK 15, а в качестве его замены предлагается попробовать именно GraalVM. Неизвестно, как дальше будут развиваться события и будет ли GraalVM в будущем предлагаться в качестве рекомендуемой JVM-платформы по умолчанию, но судя по весьма активному развитию и маркетинговому продвижению в последнее время, Oracle настроен вполне серьёзно. Так что время покажет.

Для конечного использования предлагаются две редакции: бесплатная GraalVM Community и платная GraalVM Enterprise, отличия между которыми описаны на этой страничке официального сайта GraalVM. В основном они сводятся к обеспечению лучшей производительности, меньшего потребления оперативной памяти и официальной поддержке от специалистов корпорации Oracle в платной версии. В этой статье я буду ориентироваться только на возможности GraalVM Community, распространяемой свободно.

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

Содержание:


1. Подсветка синтаксиса фрагментов кода на стороне сервера
2. Создание простейшего прототипа
3. Установка GraalVM и сопутствующих библиотек, запуск прототипа
4. От прототипа к готовому сервису
5. Подведём итоги

1. Подсветка синтаксиса фрагментов кода на стороне сервера


Когда-то давно мне стало жутко интересно, какими технологиями крупные хостеры IT-проектов вроде GitHub, Gitorious и Bitbucket подсвечивают наш исходный код на своих серверах. Проведя некоторое исследование, я пришёл к следующим результатам:

  1. Bitbucket использует библиотеку Pygments на языке программирования Python.
  2. GitHub использует Albino, обёртку библиотеки Pygments для языка программирования Ruby.
  3. Gitorious использует Makeup, тоже по своей сути обёртку библиотеки Pygments для Ruby.

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

Шло время и недавно я решил снова перепроверить чем же сейчас пользуются крупные хостеры исходного кода. За это время Gitorious был куплен GitLab'ом, который в итоге стал новым крупным и популярным сервисом. На сей раз получились такие результаты:

  1. Bitbucket как использовал библиотеку Pygments ранее, так и продолжает её использовать.
  2. GitHub отказался от использования обёртки Albino и перешёл на новую библиотеку Linguist. Однако, Linguist только детектирует язык в исходных файлах, а сам процесс подсветки выполняется какой-то другой библиотекой с закрытым исходным кодом, принадлежащей GitHub.
  3. GitLab применяет библиотеку Rouge на языке программирования Ruby, свободную от использования обёрток, но сохранившую некоторую совместимость с Pygments.

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

Особняком ещё стоит библиотека Highlight.js на языке программирования JavaScript, получившая огромную популярность и широкое распространение на самых разнообразных сайтах и сервисах. Её применяют в основном чтобы подсвечивать код на стороне клиента, но никто не запрещает использовать эту библиотеку и для подсветки на стороне сервера.

Если бы вы писали сайт на каком-либо языке JVM-стека вкупе с каким-нибудь популярным веб-фреймворком и перед вами бы стояла задача реализовать серверную подсветку синтаксиса различных фрагментов кода, то у вас бы испортилось настроение. К большому сожалению, JVM-платформа не обзавелась такими библиотеками, как Pygments, Rouge и Highlight.js, которые поддерживают сотни языков программирования. Все известные мне попытки портирования Pygments на Java на сегодня по сути заброшены и поэтому вам для выполнения этой задачи пришлось бы делать такие же обёртки над чужеродными библиотеками, которые были описаны выше.

Альтернатива видится в использовании Jython, JRuby или Nashorn, то бишь внешних реализаций Python, Ruby и JavaScript для платформы JVM. Но с ними не всё так гладко, как хотелось бы. Во-первых, размер вашего JAR-файла или WAR-файла и время его запуска существенно увеличится. Во-вторых, некоторые библиотеки предоставляют реализации версий языков далеко не первой свежести, например, Jython так и остался на Python 2, который уже устарел и новые версии Pygments на нём просто не работают. В-третьих, установка сторонних библиотек внутрь конечного файла для развёртывания в некоторых случаях далеко не так тривиальна и сопровождается грудой различных проблем и точек отказа.

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

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

<< Перейти к содержанию

2. Создание простейшего прототипа


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

  • Highlight.js на JavaScript, поддерживает ~190 языков программирования.
  • Rouge на Ruby, поддерживает ~205 языков программирования.
  • Pygments на Python, поддерживает ~500 языков программирования.

Их связующим звеном я решил выбрать Java 8, хотя с таким же успехом этот выбор мог быть сделан в сторону Kotlin, Scala или Java 11.

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

Исходный код Highlighter.java
// Highlighter.java, no comments, no checks.// $ javac Highlighter.java// $ jar -cvfe highlighter.jar Highlighter *.class// $ cat hello.py | java -jar highlighter.jar rouge pythonimport org.graalvm.polyglot.Context;import java.io.File;import java.io.FileNotFoundException;import java.util.Scanner;public class Highlighter {  private abstract class Highlight {    protected final Context polyglot =      Context.newBuilder("js", "python", "ruby").allowAllAccess(true).allowIO(true)        .build();    protected abstract String language();    protected abstract String renderHtml(String language, String rawCode);    protected String execute(String sourceCode) {      try {        return polyglot.eval(language(), sourceCode).asString();      } catch (RuntimeException re) { re.printStackTrace(); }      return sourceCode;    }    protected void importValue(String name, String value) {      try {        polyglot.getBindings(language()).putMember(name, value);      } catch (RuntimeException re) { re.printStackTrace(); }    }  }  private class Hjs extends Highlight {    @Override    protected String language() { return "js"; }    @Override    public String renderHtml(String language, String rawCode) {      importValue("source", rawCode);      String hjs = "";      try {        hjs = new Scanner(new File("highlight.min.js")).useDelimiter("\\A").next();      } catch (FileNotFoundException fnfe) { fnfe.printStackTrace(); }      final String renderLanguageSnippet =        hjs + "\n" +        "hljs.highlight('" + language + "', String(source)).value";      return execute(renderLanguageSnippet);    }  }  private class Rouge extends Highlight {    @Override    protected String language() { return "ruby"; }    @Override    public String renderHtml(String language, String rawCode) {      importValue("$source", rawCode);      final String renderLanguageSnippet =        "require 'rouge'" + "\n" +        "formatter = Rouge::Formatters::HTML.new" + "\n" +        "lexer = Rouge::Lexer::find('" + language + "')" + "\n" +        "formatter.format(lexer.lex($source.to_str))";      return execute(renderLanguageSnippet);    }  }  private class Pygments extends Highlight {    @Override    protected String language() { return "python"; }    @Override    public String renderHtml(String language, String rawCode) {      importValue("source", rawCode);      final String renderLanguageSnippet =        "import site" + "\n" +        "from pygments import highlight" + "\n" +        "from pygments.lexers import get_lexer_by_name" + "\n" +        "from pygments.formatters import HtmlFormatter" + "\n" +        "formatter = HtmlFormatter(nowrap=True)" + "\n" +        "lexer = get_lexer_by_name('" + language + "')" + "\n" +        "highlight(source, lexer, formatter)";      return execute(renderLanguageSnippet);    }  }  public String highlight(String library, String language, String code) {    switch (library) {      default:      case "hjs": return new Hjs().renderHtml(language, code);      case "rouge": return new Rouge().renderHtml(language, code);      case "pygments": return new Pygments().renderHtml(language, code);    }  }  public static void main(String[] args) {    Scanner scanner = new Scanner(System.in).useDelimiter("\\A");    if (scanner.hasNext()) {      String code = scanner.next();      if (!code.isEmpty()) {        System.out.println(new Highlighter().highlight(args[0], args[1], code));      }    }  }}


Как видно, в этом фрагменте смешаны четыре разных языка программирования. Утилита принимает на вход stdin в виде текста исходного файла, передаваемые аргументы определяют используемую библиотеку для подсветки и язык фрагмента, затем подсвечивается код и выводится готовый HTML на stdout терминала. Звучит просто и понятно, но для компиляции и запуска этой программы требуется установить GraalVM и сопутствующие пакеты нужных библиотек.

<< Перейти к содержанию

3. Установка GraalVM и сопутствующих библиотек, запуск прототипа


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

Рецепт установки платформы у меня получился следующим:

curl -LOJ https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-20.3.0/graalvm-ce-java8-linux-amd64-20.3.0.tar.gz# curl -LOJ https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-20.3.0/graalvm-ce-java11-linux-amd64-20.3.0.tar.gzcd /opt/sudo mkdir graalvmsudo chown `whoami`:`whoami` graalvmcd /opt/graalvm/tar -xvzf ~/graalvm-ce-java8-linux-amd64-20.3.0.tar.gzrm ~/graalvm-ce-java8-linux-amd64-20.3.0.tar.gz

Стоит заметить, что в настоящий момент времени GraalVM 20.3.0 поддерживает только LTS-версии Java 8 и Java 11, поэтому при создании проектов на этой платформе следует помнить об этом и пока забыть про вкусности новых версий Java и JVM-платформ. Итак, после выполнения этих команд в директории /opt/ у вас будет развёрнут GraalVM, но в него ещё нужно добавить поддержку дополнительных компонентов.

Рецепт установки языков программирования в GraalVM и требуемых библиотек подсветки кода таков:

export GRAALVM_HOME=/opt/graalvm/graalvm-ce-java8-20.3.0export JAVA_HOME=$GRAALVM_HOMEexport PATH=$GRAALVM_HOME/bin:$PATHgu install pythongu install ruby# /opt/graalvm/graalvm-ce-java8-20.3.0/jre/languages/ruby/lib/truffle/post_install_hook.shgraalpython -m ginstall install setuptoolscurl -LOJ https://github.com/pygments/pygments/archive/2.7.3.tar.gztar -xvzf pygments-2.7.3.tar.gzcd pygments-2.7.3/graalpython setup.py install --usercd ..rm -Rf pygments-2.7.3/ pygments-2.7.3.tar.gzgem install rougecurl -LOJ https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.4.1/highlight.min.js

Перед использованием инструментов из дистрибутива GraalVM следует подгрузить нужные нам переменные окружения. При желании их подгрузку можно сделать при старте сервера для определённого пользователя или для всей системы сразу. Утилита gu является неким пакетным менеджером и предназначена для установки, удаления и обновления компонентов GraalVM. Нативные образы я не стал пересобирать, поскольку это достаточно долгий и ресурсоёмкий процесс. Он потребуется только в том случае, если вы захотите сформировать автономный исполняемый файл вашей программы. Пост-установочный скрипт после процесса инсталляции поддержки языка Ruby я тоже не стал выполнять, поскольку библиотека Rouge никак не взаимодействует с сетью и интернетом. Для использования Highlight.js достаточно просто скачать минифицированный файл библиотеки, платформа Node.js здесь никак не задействуется.

Ситуацию с Pygments следует разобрать немного подробнее. В случае с Python, его менеджер пакетов pip доступен лишь в виртуальном окружении virtualenv и для упрощения я решил отказаться от изоляции и использовать способ установки библиотеки Pygments посредством инструмента setuptools и загрузки исходников. Стоит заметить, что постоянные холодные вызовы интерпретатора graalpython довольно ресурсоёмки и на выполнение этих команд потребуется некоторое время, однако после завершения процесса требуемая библиотека будет сразу нам доступна. Кстати, установку и удаление библиотек можно производить и обычным системным pip, который входит в поставку дистрибутива операционной системы. В моём случае использовался CentOS 7, где по умолчанию установлен лишь Python 2, а усложнять инструкцию установкой современного Python 3 мне не хотелось, тем более раз для GraalVM доступна его собственная реализация Python.

Рецепт сборки и запуска консольной программки-прототипа из предыдущего раздела:

export GRAALVM_HOME=/opt/graalvm/graalvm-ce-java8-20.3.0export JAVA_HOME=$GRAALVM_HOMEexport PATH=$GRAALVM_HOME/bin:$PATHjavac Highlighter.javajar -cvfe highlighter.jar Highlighter *.classcat hello.py#!/usr/bin/env pythonprint("Hello, World!")cat hello.py | java -jar highlighter.jar hjs python<span class="hljs-comment">#!/usr/bin/env python</span>print(<span class="hljs-string">"Hello, World!"</span>)cat hello.py | java -jar highlighter.jar rouge python<span class="c1">#!/usr/bin/env python</span><span class="k">print</span><span class="p">(</span><span class="s">"Hello, World!"</span><span class="p">)</span>cat hello.py | java -jar highlighter.jar pygments python<span class="ch">#!/usr/bin/env python</span><span class="nb">print</span><span class="p">(</span><span class="s2">"Hello, World!"</span><span class="p">)</span>

Как видно, всё прекрасно работает! Только не стоит забывать что для Highlight.js требуется положить файлик highlight.min.js рядом с нашей программкой. Использование внутренних ресурсов JAR-файла усложнило бы этот пример, поэтому я решил выбрать именно такой простой способ.

При желании все эти рецепты можно аккуратно завернуть в контейнеры вроде Docker или Podman, кому что нравится. Официальные образы от Oracle с GraalVM вполне себе доступны на том же Docker Hub.

<< Перейти к содержанию

4. От прототипа к готовому сервису


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

Для реализации этой задачи я взял известный в Java-экосистеме фреймворк Spring, который в настоящее время без особых нареканий работает на GraalVM и даже обозначен на официальном сайте платформы. В качестве проксирующего веб-сервера был выбран Nginx, а для базы данных я использовал PostgreSQL. Для front-end'а мной был выбран несколько устаревший в современном мире, но всё ещё использующийся подход с рендерингом HTML на стороне сервера при помощи шаблонизатора Thymeleaf. В общем, я остановился на одном из самых популярных фреймворков и его сопутствующих инструментах вроде Spring Boot 2.4.1 или Spring Data JPA, которые сильно ускоряют разработку и сокращают рутинный код. Но при этом с GraalVM вы вольны выбирать другие и более легковесные Java-фреймворки, которые больше ориентированы на создание микросервисов. Официальный сайт рекомендует посмотреть на Micronaut и Quarkus, преимущество которых состоит в возможности более лёгкого создания нативных образов из-за меньшего потребления оперативной памяти при их формировании.

Отправку фрагментов кода на веб-сайт я стилизовал под популярный ныне на многих IT-ресурсах язык разметки Markdown:

```python#!/usr/bin/env pythonprint("Hello, World!")```

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


Сервис Code Polyglot с тёмной темой.

Тёмная тема в хакерском стиле создавалась под впечатлением от интерфейса старых мобильных телефонов Motorola на P2K, вроде E398, которые оставили в моей юности приятные и тёплые воспоминания. Оболочка называлась Techno и выглядела следующим образом:

Скриншоты с Motorola E398

Оболочка Techno на Motorola E398.


Надеюсь, вы не станете меня проклинать за моё извращённое чувство прекрасного и безумные умения в вёрстке, ибо я не имею должного опыта веб-разработки. Но если всё-таки станете, то на сайте доступна более нейтральная светлая тема, простой дизайн которой я позаимствовал с похожего сервиса по обмену фрагментами кода: paste.org.ru:


Сервис Code Polyglot со светлой темой.

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

Фрагменты кода на веб-сайте я решил отображать в таблицах, как это делает GitHub, но оказалось что с этим не всё так гладко. Библиотека Highlight.js, например, позиционируется разработчиком как максимально простая и поэтому принципиально не умеет оборачивать фрагменты кода в табличные строки. С другой стороны, генерируемые таблицы Pygments и Rouge слабо совместимы между собой. Поэтому мне пришлось выключать табличное отображение вообще, просто подсвечивать фрагмент кода выбранной библиотекой и построчно оборачивать его в таблицу уже на стороне Java.

Подобный подход привёл ещё к одной проблеме, которую проще всего продемонстрировать на следующем примере:

/* * Многострочный комментарий! */<span class="comment">/* * Многострочный комментарий! */</span><tr><td>1</td><td><span class="comment">/*</td></tr><tr><td>2</td><td> * Многострочный комментарий!</td></tr><tr><td>3</td><td> */</span></td></tr>

Библиотеки Highlight.js и Rouge не оборачивали каждую линию кода в автономные HTML-теги, поэтому при генерации таблицы они разрывались и подсветка кода работала некорректно. Я постарался исправить эту проблему с помощью обычного стека из стандартной библиотеки структур данных в Java. Упрощённый алгоритм выглядит примерно следующим образом: проходим по строке, когда детектим открывающий тег подсветки, то кладём его на стек, а когда детектим закрывающий, то просто убираем последний элемент со стека. Если к концу строки стек не оказывается пустым, то закрываем все открытые теги в текущей строке, а в начале следующей строки открываем те теги, которые остались у нас на стеке. Решение получилось немного топорным, но вполне себе рабочим.

<tr><td>1</td><td><span class="comment">/*</span></td></tr><tr><td>2</td><td><span class="comment"> * Многострочный комментарий!</span></td></tr><tr><td>3</td><td><span class="comment"> */</span></td></tr>

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

С последним препятствием я столкнулся уже на слабенькой VPS от Oracle. Оказалось, что экспериментальная поддержка языка программирования Python в GraalVM вкупе с библиотекой Pygments не влазят в мои скромные 1 GB оперативной памяти на виртуальном сервере. Да и сама реализация Python показалась мне ещё несколько сыроватой и медленной, например, иногда в процессе подсветки кода кое-где теряются пробелы и переводы строк, хотя в классическом системном Python всё работает отлично. Поэтому мне пришлось поменять сервер на другой, с более мощным железом и 4 GB RAM на борту.

<< Перейти к содержанию

5. Подведём итоги


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

Все свои наработки, рецепты развёртывания и сборки, весь исходный код я разместил в репозитории на GitHub:

github.com/EXL/CodePolyglot

Сам демонстрационный сервис можно потыкать палочкой по этой ссылке:

code.exlmoto.ru

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

Отдельно стоит поговорить про несколько неудачный опыт создания нативных образов. В тех случаях, когда используются и смешиваются несколько языков программирования, их формирование превращается в пытку. Забегая вперёд сообщу, что для маленького прототипа, который был описан в статье выше, в его нативном образе удалось заставить работать лишь библиотеку Highlight.js вкупе с JavaScript. При попытке добавления других языков программирования автономный исполняемый файл раздувался совсем уж до неприличных размеров в 500 MB и отказывался видеть не только сторонние батарейки, но и стандартные библиотеки Python и Ruby. Копание в переменных окружения и флажках сборки исправило пару проблем, но удобоваримого результата у меня так и не получилось. К тому же, нельзя не упомянуть продолжительное время AOT-компиляции (порядка 10-15 минут), значительное потребление RAM (порядка 20 GB) и ресурсов CPU в процессе сборки.

Если с такими проблемами создания нативного образа я столкнулся на простейшем прототипе, то что уж говорить о моих тщетных попытках преобразования всего сервиса в автономный исполняемый файл. В экосистеме Spring совсем недавно появился экспериментальный проект Spring Native 0.8.5, позволяющий формировать нативные образы сервисов использующих библиотеки Spring. Оригинальные версии Java-библиотек, которые не работают должным образом под Substrate VM, вырезаются или подменяются на патченные, например, в их число входит встраиваемый веб-сервер Tomcat. При этом при сборке нужно ещё сделать некоторые ухищрения и телодвижения вроде правильной настройки Hibernate, подгрузки конфигурационного JSON-файла с описанием некоторой рефлексии и расстановки обязательных флажков для утилиты native-image, которая формирует нативные образы. В качестве системы сборки в Spring Native используется Maven, так как поддержка Gradle находится ещё на начальном этапе развития. В общем, всё выглядит слишком экспериментальным и сырым. В итоге мне удалось добиться корректной сборки нативного образа, но без поддержки каких-либо сторонних языков программирования кроме тех, что доступны на JVM-платформе. При попытках их добавления процесс зависал и ничего полезного так и не происходило в течении 30 минут, после чего я просто останавливал задачу. Далее, при запуске исполняемого файла сыпались ошибки встраиваемого языка SpEL в шаблонах Thymeleaf и я пока отступил от решения этой проблемы.

Для себя я сделал вывод, что AOT-компиляция может быть интересной в том случае, если в проекте не задействован Polyglot API, не смешано множество языков программирования и сервис используется как back-end к чему либо. Вместо Spring для AOT пока лучше использовать более легковесный Quarkus, который как раз опирается на возможность формирования нативных образов в GraalVM. А для проектов, которые используют множество языков и серверные HTML-шаблонизаторы, неплохо работает традиционный подход с JIT-компиляцией и запуском на JVM.

Версии реализаций языков программирования и технологий доступных на платформе GraalVM 20.3.0:

  • Java 8 (1.8.0_272), Java 11 (11.0.9)
  • JavaScript ES2020 (ES11)
  • Ruby 2.6.6
  • Python 3.8.5
  • R 3.6.1
  • LLVM 10.0.0

Некоторые полезные ссылки:

  1. Платформа GraalVM.
  2. Документация GraalVM.
  3. Фреймворк Spring.
  4. Библиотека Pygments.
  5. Библиотека Rouge.
  6. Библиотека Highlight.js.

Надеюсь, мой опыт и эта статья будут полезны тем, кто когда-нибудь заинтересуется платформой GraalVM и её возможностями по использованию множества языков программирования в одном окружении.

P.S. Искренне поздравляю посетителей ресурса Хабр с наступающим 2021 годом, желаю вам ребята исполнения всех ваших желаний и крепкого сибирского здоровья!

P.P.S. Благодарю пользователя zorgrhrd за поддержку! Без него эта статья никогда бы не появилась на свет.

<< Перейти к содержанию
Подробнее..
Категории: Javascript , Ruby , Python , Java , Oracle , Spring , Graalvm , Spring boot , Graal , Polyglot

Мониторинг бизнес-процессов Camunda

07.01.2021 18:21:18 | Автор: admin

Привет, Хабр.

Меня зовут Антон и я техлид в компании ДомКлик. Создаю и поддерживаю микросервисы позволяющие обмениваться данными инфраструктуре ДомКлик с внутренними сервисами Сбербанка.

Это продолжение цикла статей о нашем опыте использования движка для работы с диаграммами бизнес-процессов Camunda. Предыдущая статья была посвящена разработке плагина для Bitbucket позволяющего просматривать изменения BPMN-схем. Сегодня я расскажу о мониторинге проектов, в которых используется Camunda, как с помощью сторонних инструментов (в нашем случае это стек Elasticsearch из Kibana и Grafana), так и родного для Camunda Cockpit. Опишу сложности, возникшие при использовании Cockpit, и наши решения.

Когда у тебя много микросервисов, то хочется знать об их работе и текущем статусе всё: чем больше мониторинга, тем увереннее ты себя чувствуешь как в штатных, так и внештатных ситуациях, во время релиза и так далее. В качестве средств мониторинга мы используем стек Elasticsearch: Kibana и Grafana. В Kibana смотрим логи, а в Grafana метрики. Также в БД имеются исторические данные по процессам Camunda. Казалось бы, этого должно хватать для понимания, работает ли сервис штатно, и если нет, то почему. Загвоздка в том, что данные приходится смотреть в трёх разных местах, и они далеко не всегда имеют четкую связь друг с другом. На разбор и анализ инцидента может уходить много времени. В частности, на анализ данных из БД: Camunda имеет далеко не очевидную схему данных, некоторые переменные хранит в сериализованном виде. По идее, облегчить задачу может Cockpit инструмент Camunda для мониторинга бизнес-процессов.


Интерфейс Cockpit.

Главная проблема в том, что Cockpit не может работать по кастомному URL. Об этом на их форуме есть множество реквестов, но пока такой функциональности из коробки нет. Единственный выход: сделать это самим. У Cockpit есть Sring Boot-автоконфигурация CamundaBpmWebappAutoConfiguration, вот её-то и надо заменить на свою. Нас интересует CamundaBpmWebappInitializer основной бин, который инициализирует веб-фильтры и сервлеты Cockpit.

Нам необходимо передать в основной фильтр (LazyProcessEnginesFilter) информацию об URL, по которому он будет работать, а в ResourceLoadingProcessEnginesFilter информацию о том, по каким URL он будет отдавать JS- и CSS-ресурсы.

Для этого в нашей реализации CamundaBpmWebappInitializer меняем строчку:

registerFilter("Engines Filter", LazyProcessEnginesFilter::class.java, "/api/*", "/app/*")

на:

registerFilter("Engines Filter", CustomLazyProcessEnginesFilter::class.java, singletonMap("servicePath", servicePath), *urlPatterns)

servicePath это наш кастомный URL. В самом же CustomLazyProcessEnginesFilter указываем нашу реализацию ResourceLoadingProcessEnginesFilter:

class CustomLazyProcessEnginesFilter:       LazyDelegateFilter<ResourceLoaderDependingFilter>       (CustomResourceLoadingProcessEnginesFilter::class.java)

В CustomResourceLoadingProcessEnginesFilter добавляем servicePath ко всем ссылкам на ресурсы, которые мы планируем отдавать клиентской стороне:

override fun replacePlaceholder(       data: String,       appName: String,       engineName: String,       contextPath: String,       request: HttpServletRequest,       response: HttpServletResponse) = data.replace(APP_ROOT_PLACEHOLDER, "$contextPath$servicePath")           .replace(BASE_PLACEHOLDER,                   String.format("%s$servicePath/app/%s/%s/", contextPath, appName, engineName))           .replace(PLUGIN_PACKAGES_PLACEHOLDER,                   createPluginPackagesString(appName, contextPath))           .replace(PLUGIN_DEPENDENCIES_PLACEHOLDER,                   createPluginDependenciesString(appName))

Теперь мы можем указывать нашему Cockpit, по какому URL он должен слушать запросы и отдавать ресурсы.

Но ведь не может быть всё так просто? В нашем случае Cockpit не способен работать из коробки на нескольких экземплярах приложения (например, в подах Kubernetes), так как вместо OAuth2 и JWT используется старый добрый jsessionid, который хранится в локальном кэше. Это значит, что если попытаться залогиниться в Cockpit, подключенный к Camunda, запущенной сразу в нескольких экземплярах, имея на руках ей же выданный jsessionid, то при каждом запросе ресурсов от клиента можно получить ошибку 401 с вероятностью х, где х = (1 1/количество_под). Что с этим можно сделать? У Cockpit во всё том же CamundaBpmWebappInitializer объявлен свой Authentication Filter, в котором и происходит вся работа с токенами; надо заменить его на свой. В нём из кеша сессии берём jsessionid, сохраняем его в базу данных, если это запрос на авторизацию, либо проверяем его валидность по базе данных в остальных случаях. Готово, теперь мы можем смотреть инциденты по бизнес-процессам через удобный графический интерфейс Cockpit, где сразу видно stacktrace-ошибки и переменные, которые были у процесса на момент инцидента.

И в тех случаях, когда причина инцидента ясна по stacktrace исключения, Cockpit позволяет сократить время разбора инцидента до 3-5 минут: зашел, посмотрел, какие есть инциденты по процессу, глянул stacktrace, переменные, и вуаля инцидент разобран, заводим баг в JIRA и погнали дальше. Но что если ситуация немного сложнее, stacktrace является лишь следствием более ранней ошибки или процесс вообще завершился без создания инцидента (то есть технически всё прошло хорошо, но, с точки зрения бизнес-логики, передались не те данные, либо процесс пошел не по той ветке схемы). В этом случае надо снова идти в Kibana, смотреть логи и пытаться связать их с процессами Camunda, на что опять-таки уходит много времени. Конечно, можно добавлять к каждому логу UUID текущего процесса и ID текущего элемента BPMN-схемы (activityId), но это требует много ручной работы, захламляет кодовую базу, усложняет рецензирование кода. Весь этот процесс можно автоматизировать.

Проект Sleuth позволяет трейсить логи уникальным идентификатором (в нашем случае UUID процесса). Настройка Sleuth-контекста подробно описана в документации, здесь я покажу лишь, как запустить его в Camunda.

Во-первых, необходимо зарегистрировать customPreBPMNParseListeners в текущем processEngine Camunda. В слушателе переопределить методы parseStartEvent (добавление слушателя на событие запуска верхнеуровневого процесса) и parseServiceTask (добавление слушателя на событие запуска ServiceTask).

В первом случае мы создаем Sleuth-контекст:

customContext[X_B_3_TRACE_ID] = businessKeycustomContext[X_B_3_SPAN_ID] = businessKeyHalfcustomContext[X_B_3_PARENT_SPAN_ID] = businessKeyHalfcustomContext[X_B_3_SAMPLED] = "0" val contextFlags: TraceContextOrSamplingFlags = tracing.propagation()       .extractor(OrcGetter())       .extract(customContext)val newSpan: Span = tracing.tracer().nextSpan(contextFlags)tracing.currentTraceContext().newScope(newSpan.context())

и сохраняем его в переменную бизнес-процесса:

execution.setVariable(TRACING_CONTEXT, sleuthService.tracingContextHeaders)

Во втором случае мы его из этой переменной восстанавливаем:

val storedContext = execution       .getVariableTyped<ObjectValue>(TRACING_CONTEXT)       .getValue(HashMap::class.java) as HashMap<String?, String?>val contextFlags: TraceContextOrSamplingFlags = tracing.propagation()       .extractor(OrcGetter())       .extract(storedContext)val newSpan: Span = tracing.tracer().nextSpan(contextFlags)tracing.currentTraceContext().newScope(newSpan.context())

Нам нужно трейсить логи вместе с дополнительными параметрами, такими как activityId (ID текущего BPMN-элемента), activityName (его бизнес-название) и scenarioId (ID схемы бизнес-процесса). Такая возможность появилась только с выходом Sleuth 3.

Для каждого параметра нужно объявить BaggageField:

companion object {   val HEADER_BUSINESS_KEY = BaggageField.create("HEADER_BUSINESS_KEY")   val HEADER_SCENARIO_ID = BaggageField.create("HEADER_SCENARIO_ID")   val HEADER_ACTIVITY_NAME = BaggageField.create("HEADER_ACTIVITY_NAME")   val HEADER_ACTIVITY_ID = BaggageField.create("HEADER_ACTIVITY_ID")}

Затем объявить три бина для обработки этих полей:

@Beanopen fun propagateBusinessProcessLocally(): BaggagePropagationCustomizer =       BaggagePropagationCustomizer { fb ->           fb.add(SingleBaggageField.local(HEADER_BUSINESS_KEY))           fb.add(SingleBaggageField.local(HEADER_SCENARIO_ID))           fb.add(SingleBaggageField.local(HEADER_ACTIVITY_NAME))           fb.add(SingleBaggageField.local(HEADER_ACTIVITY_ID))       }/** [BaggageField.updateValue] now flushes to MDC  */@Beanopen fun flushBusinessProcessToMDCOnUpdate(): CorrelationScopeCustomizer =       CorrelationScopeCustomizer { builder ->           builder.add(SingleCorrelationField.newBuilder(HEADER_BUSINESS_KEY).flushOnUpdate().build())           builder.add(SingleCorrelationField.newBuilder(HEADER_SCENARIO_ID).flushOnUpdate().build())           builder.add(SingleCorrelationField.newBuilder(HEADER_ACTIVITY_NAME).flushOnUpdate().build())           builder.add(SingleCorrelationField.newBuilder(HEADER_ACTIVITY_ID).flushOnUpdate().build())       }/** [.BUSINESS_PROCESS] is added as a tag only in the first span.  */@Beanopen fun tagBusinessProcessOncePerProcess(): SpanHandler =       object : SpanHandler() {           override fun end(context: TraceContext, span: MutableSpan, cause: Cause): Boolean {               if (context.isLocalRoot && cause == Cause.FINISHED) {                   Tags.BAGGAGE_FIELD.tag(HEADER_BUSINESS_KEY, context, span)                   Tags.BAGGAGE_FIELD.tag(HEADER_SCENARIO_ID, context, span)                   Tags.BAGGAGE_FIELD.tag(HEADER_ACTIVITY_NAME, context, span)                   Tags.BAGGAGE_FIELD.tag(HEADER_ACTIVITY_ID, context, span)               }               return true           }       }

После чего мы можем сохранять дополнительные поля в контекст Sleuth:

HEADER_BUSINESS_KEY.updateValue(businessKey)HEADER_SCENARIO_ID.updateValue(scenarioId)HEADER_ACTIVITY_NAME.updateValue(activityName)HEADER_ACTIVITY_ID.updateValue(activityId)

Когда мы можем видеть логи отдельно по каждому бизнес-процессу по его ключу, разбор инцидентов проходит гораздо быстрее. Правда, всё равно приходится переключаться между Kibana и Cockpit, вот бы их объединить в рамках одного UI.

И такая возможность имеется. Cockpit поддерживает пользовательские расширения плагины, в Kibana есть Rest API и две клиентские библиотеки для работы с ним: elasticsearch-rest-low-level-client и elasticsearch-rest-high-level-client.

Плагин представляет из себя проект на Maven, наследуемый от артефакта camunda-release-parent, с бэкендом на Jax-RS и фронтендом на AngularJS. Да-да, AngularJS, не Angular.

У Cockpit есть подробная документация о том, как писать для него плагины.

Уточню лишь, что для вывода логов на фронтенде нас интересует tab-панель на странице просмотра информации о Process Definition (cockpit.processDefinition.runtime.tab) и странице просмотра Process Instance (cockpit.processInstance.runtime.tab). Для них регистрируем наши компоненты:

ViewsProvider.registerDefaultView('cockpit.processDefinition.runtime.tab', {   id: 'process-definition-runtime-tab-log',   priority: 20,   label: 'Logs',   url: 'plugin://log-plugin/static/app/components/process-definition/processDefinitionTabView.html'});ViewsProvider.registerDefaultView('cockpit.processInstance.runtime.tab', {   id: 'process-instance-runtime-tab-log',   priority: 20,   label: 'Logs',   url: 'plugin://log-plugin/static/app/components/process-instance/processInstanceTabView.html'});

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

<div cam-searchable-area (1)    config="searchConfig" (2)    on-search-change="onSearchChange(query, pages)" (3)    loading-state="Loading..." (4)    text-empty="Not found"(5)    storage-group="'ANU'"    blocked="blocked">   <div class="col-lg-12 col-md-12 col-sm-12">       <table class="table table-hover cam-table">           <thead cam-sortable-table-header (6)                  default-sort-by="time"                  default-sort-order="asc" (7)                  sorting-id="admin-sorting-logs"                  on-sort-change="onSortChanged(sorting)"                  on-sort-initialized="onSortInitialized(sorting)" (8)>           <tr>               <!-- headers -->           </tr>           </thead>           <tbody>           <!-- table content -->           </tbody>       </table>   </div></div>

  1. Атрибут для объявления компонента поиска.
  2. Конфигурация компонента. Здесь имеем такую структуру:

    tooltips = { //здесь мы объявляем плейсхолдеры и сообщения,                    //которые будут выводиться в поле поиска в зависимости от результата   'inputPlaceholder': 'Add criteria',   'invalid': 'This search query is not valid',   'deleteSearch': 'Remove search',   'type': 'Type',   'name': 'Property',   'operator': 'Operator',   'value': 'Value'},operators =  { //операторы, используемые для поиска, нас интересует сравнение строк     'string': [       {'key': 'eq',  'value': '='},       {'key': 'like','value': 'like'}   ]},types = [// поля, по которым будет производится поиск, нас интересует поле businessKey   {       'id': {           'key': 'businessKey',           'value': 'Business Key'       },       'operators': [           {'key': 'eq', 'value': '='}       ],       enforceString: true   }]
    

  3. Функция поиска данных используется как при изменении параметров поиска, так и при первоначальной загрузке.
  4. Какое сообщение отображать во время загрузки данных.
  5. Какое сообщение отображать, если ничего не найдено.
  6. Атрибут для объявления таблицы отображения данных поиска.
  7. Поле и тип сортировки по умолчанию.
  8. Функции сортировок.

На бэкенде нужно настроить клиент для работы с Kibana API. Для этого достаточно воспользоваться RestHighLevelClient из библиотеки elasticsearch-rest-high-level-client. Там указать путь до Kibana, данные для аутентификации: логин и пароль, а если используется протокол шифрования, то надо указать подходящую реализацию X509TrustManager.

Для формирования запроса поиска используем QueryBuilders.boolQuery(), он позволяет составлять сложные запросы вида:

val boolQueryBuilder = QueryBuilders.boolQuery();KibanaConfiguration.ADDITIONAL_QUERY_PARAMS.forEach((key, value) ->       boolQueryBuilder.filter()               .add(QueryBuilders.matchPhraseQuery(key, value)));if (!StringUtils.isEmpty(businessKey)) {   boolQueryBuilder.filter()           .add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.BUSINESS_KEY, businessKey));}if (!StringUtils.isEmpty(procDefKey)) {   boolQueryBuilder.filter()           .add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.SCENARIO_ID, procDefKey));}if (!StringUtils.isEmpty(activityId)) {   boolQueryBuilder.filter()           .add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.ACTIVITY_ID, activityId));}

Теперь мы прямо из Cockpit можем просматривать логи отдельно по каждому процессу и по каждой activity. Выглядит это так:


Таб для просмотра логов в интерфейсе Cockpit.

Но нельзя останавливаться на достигнутом, в планах идеи о развитии проекта. Во-первых, расширить возможности поиска. Зачастую в начале разбора инцидента business key процесса на руках отсутствует, но имеется информация о других ключевых параметрах, и было бы неплохо добавить возможность настройки поиска по ним. Также таблица, в которую выводится информация о логах, не интерактивна: нет возможности перехода в нужный Process Instance по клику в соответствующей ему строке таблицы. Словом, развиваться есть куда. (Как только закончатся выходные, я опубликую ссылку на Github проекта, и приглашаю туда всех заинтересовавшихся.)
Подробнее..

Keycloak интеграция со Spring Boot и Vue.js для самых маленьких

10.05.2021 18:20:09 | Автор: admin

Вы больше не можете создать сервер авторизации с помощью @EnableAuthorizationServer, потому что Spring Security OAuth задеприкейтили, а проект Spring Authorization Serverвсё ещё экспериментальный? Выход есть! Напишем авторизацию своими руками... Что?.. Нет?.. Не хочется? И вообще получаются какие-то костыли и велосипеды? Ну ладно, тогда давайте возьмём уже что-то готовое. Например, Keycloak.

Что, зачем и почему?

Как-то сидя на карантине захотелось мне написать pet-проект, да не простой, а с использованием микросервисной архитектуры (ну или около того). На начальном этапе одного сервиса для фронта и одного для бэка, в принципе, будет достаточно. Если вдруг в будущем понадобятся ещё сервисы, то будем добавлять их по мере необходимости. Для бэка будем использовать Spring Boot. Для фронта - Vue.js, а точнее Vuetify, чтобы не писать свои компоненты, а использовать уже готовые.

Начнём с авторизации. В качестве протокола авторизации будем использовать OAuth2, т.к. стильно, модно, молодёжно, да и использовать токены для получения доступа к сервисам, одно удовольствие, особенно в микросервисной архитектуре.

Для авторизации пусть будет отдельный сервис. И раз уж мы решили использовать Spring Boot, то сможет ли он нам чем-то помочь в создании этого сервиса? Например, каким-нибудь готовым решением, таким как Authorization Server? Правильно, не сможет. Проект Spring Security OAuth в котором находился Authorization Server задеприкейтили, а сам проект Authorization Server стал эксперементальным и на данный момент находится в активной разработке. Что делать? Как быть? Можно написать свой сервис авторизации. Если подсматривать в исходники задеприкейченого Authorization Server, то, вероятно, задача будет не такой уж и страшной. Правда, при этом возможны ситуации когда реализацию каких-то интересных фич будет негде подсмотреть и решать вопросы о том "быть или не быть", "правильно ли так делать или чё-то фигня какая-то" придётся исходя из собственного опыта, что может привести к получению на выходе большого количества неприглядных костылей.

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

Keycloak

Keycloak представляет из себя сервис, который предназначен для идентификации и контроля доступа. Что он умеет:

  • SSO (Single-Sign On) - это когда вы логинитесь в одном едином месте входа, получаете идентификатор (например, токен), с которым можете получить доступ к различным вашим сервисам

  • Login Flows - различные процессы по регистрации, сбросу пароля, проверки почты и тому подобное, а так же соответствующие страницы для этих процессов

  • Темы - можно кастомизировать страницы для Login Flows

  • Social Login - можно логиниться через различные социальные сети

  • и много чего ещё

И всё это он умеет практически из коробки, достаточно просто настроить требуемое поведение из админки (Admin Console), которая у Keycloak тоже имеется. А если вам всего этого вдруг окажется мало, то Keycloak является open sourceпродуктом, который распространяется по лицензии Apache License 2.0. Так что можно взять исходники Keycloak и дописать требуемый функционал, если он вам, конечно, настолько сильно нужен.

А ещё у Keycloak имеются достаточно удобные интеграции со Spring Boot и Vue.js, что значительно упрощает разработку связанную с взаимодействием с ним.

Getting Started with Keycloak

Запускать локально сторонние сервисы, требуемые для разработки своих собственных, лично я предпочитаю с помощью Docker Compose, т.к. наглядно и достаточно удобно в yml-файле описывать как и с какими параметрами требуется осуществлять запуск. А посему, Keycloak локально будем запускать с помощью Docker Compose.

В качестве докер-образа возьмём jboss/keycloak. Чтобы иметь возможность обращаться к Keycloak прокинем из контейнера порт 8080. Так же, чтобы иметь возможность заходить в админку Keycloak, требуется установить логин и пароль от админской учётки. Сделать это можно установив переменные окружения KEYCLOAK_USER для логина и KEYCLOAK_PASSWORD для пароля. Итоговый файл приведен ниже.

# For developmentversion: "3.8"services:  keycloak:    image: jboss/keycloak:12.0.2    environment:      KEYCLOAK_USER: admin      KEYCLOAK_PASSWORD: admin    ports:      - 8080:8080

Создание своих realm и client

Для того чтобы иметь возможность из своего клиентского приложения обращаться к Keycloak, например, для аутентификации или авторизации, нужно в Keycloak создать клиента (client), который будет соответствовать этому приложению. Клиента в Keycloak можно создать в определённом realm. Realm - это независимая область в которую входят пользователи, клиенты, группы, роли и много чего ещё.

По умолчанию уже создан один realm и называется он master. В нём будет находится админская учётка логин и пароль от которой мы задали при запуске Keycloak с помощью Docker Compose. Данный realm предназначен для администрирования Keycloak и он не должен использоваться для ваших собственных приложений. Для своих приложений нужно создать свой realm.

Для начала нам нужно залогиниться в админке Keycloak, запустить который можно с помощью файла Docker Compose, описанного ранее. Для этого можно перейти по адресу http://localhost:8080/auth/ и выбрать Administration Console.

После этого мы попадаем на страницу авторизации админки Keycloak. Здесь можно ввести логин и пароль от админской учётки для входа в Keycloak.

После входа откроется страница настроек realm master.

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

На странице создания realm достаточно заполнить только поле Name.

После нажатия на кнопку Createмы попадём на страницу редактирования этого realm. Но пока дополнительно в нашем realm ничего менять не будем.

Теперь перейдём в раздел Clients. Как можно заметить, по умолчанию уже создано несколько технических клиентов, предназначенных для возможности администрирования через Keycloak, например, для того чтобы пользователи могли менять свои данные или чтобы можно было настраивать realm'ы с помощью REST API и много для чего ещё. Подробнее про этих клиентов можно почитать тут.

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

На странице создания клиента необходимо заполнить поля:

  • Client ID - идентификатор клиента, будет использоваться в различных запросах к Keycloak для идентификации приложения.

  • Root URL - адрес клиентского приложения.

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

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

В первую очередь давайте создадим проект на Spring Boot. Сделать это можно, например, с помощью Spring Initializr. В качестве системы автоматической сборки проекта будем использовать Gradle. В качестве языка пусть будет Java 15. Никаких дополнительных зависимостей в соответствующем блоке Dependencies добавлять не требуется.

Для того чтобы в Spring Boot проекте появилась поддержка Keycloak, необходимо добавить в него Spring Boot Adapter и добавить в конфиг приложения конфигурацию для Keycloak.

Для того чтобы добавить Spring Boot Adapter, необходимо в проект подключить зависимость org.keycloak:keycloak-spring-boot-starter и сам adapter org.keycloak.bom:keycloak-adapter-bom. Сделать это можно изменив файл build.gradle следующим образом:

...dependencyManagement {imports {mavenBom 'org.keycloak.bom:keycloak-adapter-bom:12.0.3'}}dependencies {implementation 'org.springframework.boot:spring-boot-starter-web'implementation 'org.keycloak:keycloak-spring-boot-starter'testImplementation 'org.springframework.boot:spring-boot-starter-test'}...
Проблемы в Java 14+

Если запустить Spring Boot приложение на Java 14 или выше, то при обращении к вашим методам API, закрытым ролями кейклока, будут возникать ошибки видаjava.lang.NoClassDefFoundError: java/security/acl/Group. Связано это с тем, что в Java 9 этот, а так же другие классы из этого пакета были задеприкейчины и удалены в Java 14. Исправить данную проблему, вроде как, собираются в 13-й версии Keycloak. Чтобы решить её сейчас, можно использовать Java 13 или ниже, либо, вместо сервера приложений Tomcat, который используется в Spring Boot по умолчанию, использовать, например, Undertow. Для того чтобы подключить в Spring Boot приложение Undertow, нужно добавить в build.gradle зависимость org.springframework.boot:spring-boot-starter-undertow и исключить зависимоситьspring-boot-starter-tomcat.

...dependencies {implementation('org.springframework.boot:spring-boot-starter-web') {exclude module: 'spring-boot-starter-tomcat'}implementation ('org.keycloak:keycloak-spring-boot-starter') {exclude module: 'spring-boot-starter-tomcat'}implementation 'org.springframework.boot:spring-boot-starter-undertow'testImplementation 'org.springframework.boot:spring-boot-starter-test'}...

Теперь перейдём к конфигурации приложения. Вместо properties файла конфигурации давайте будем использовать более удобный (на мой взгляд, конечно же) yml. А так же, чтобы подчеркнуть, что данный конфиг предназначен для разработки, профиль dev. Т.е. полное название файла конфигурации будет application-dev.yml.

server:  port: 8082keycloak:  auth-server-url: http://localhost:8080/auth  realm: "list-keep"  resource: "list-keep"  bearer-only: true  security-constraints:    - authRoles:        - uma_authorization      securityCollections:        - patterns:            - /api/*

Давайте подробнее разберём данный конфиг:

  • server

    • port - порт на котором будет запущенно приложение

  • keycloak

    • auth-server-url - адрес на котором запущен Keycloak

    • realm - название нашего realm в Keycloak

    • resource - Client ID нашего клиента

    • bearer-only - если выставлено true, то приложение может только проверять токены, и в приложении нельзя будет залогиниться, например, с помощью логина и пароля из браузера

    • security-constraints - для описания ролевой политики

      • authRoles - список ролей Keycloak

      • securityCollections

        • patterns - URL-паттерны для методов REST API, которые требуется закрыть соответствующими ролями

      В данном конкретном случае мы закрываем ролью uma_authorization все методы, в начале которых присутствует путь /api/. Звёздочка в конце паттерна означает любое количество любых символов. Роль uma_authorization добавляется автоматически ко всем созданным пользователям, т.е. по сути данная ролевая политика означает что все методы /api/* доступны только авторизованным пользователям.

В общем-то, это все настройки которые нужно выполнить в Spring Boot приложении для интеграции с Keycloak. Давайте теперь добавим какой-нибудь тестовый контроллер.

@RestController@RequestMapping("/api/user")public class UserController {    @GetMapping("/current")    public User getCurrentUser(            KeycloakPrincipal<KeycloakSecurityContext> principal    ) {        return new User(principal.getKeycloakSecurityContext()                .getToken().getPreferredUsername()        );    }}
User.java
public class User {    private String name;    public User(String name) {        this.name = name;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }}

В данном контроллере есть лишь один метод /api/user/current, который возвращает информацию по текущему юзеру, а именно Preferred Username из токена. По умолчанию в Preferred Username находится username пользователя Keycloak.

Исходники проекта можно посмотреть тут.

Интеграция с Vue.js

Начнём с создания проекта. Создать проект можно, например, с помощью Vue CLI.

vue create list-keep-front

После ввода данной команды необходимо выбрать версию Vue. Т.к. в проекте будет использоваться библиотека Vuetify, которая на данный момент не поддерживает Vue 3, нужно выбрать Vue 2.

После этого нужно перейти в проект и добавить Vuetify.

vue add vuetify

После добавления Vuetify вместе с самой библиотекой в проект будут добавлены каталоги components и assets. В components будет компонент HelloWorld, с примером страницы на Vuetify, а в assets ресурсы, использующиеся в компоненте HelloWorld. Эти каталоги нам не пригодятся, поэтому можно их удалить.

Для удобства разработки сконфигурируем devServer следующим образом: запускать приложение будем на порту 8081, все запросы, которые начинаются с /api/ будем проксировать на адрес, на котором запущенно приложение на Spring Boot.

module.exports = {  devServer: {    port: 8081,    proxy: {      '^/api/': {        target: 'http://localhost:8082'      }    }  }}

Перейдём к добавлению в проект поддержки Keycloak. Для начала обратимся к официальной документации. Там нам рассказывают о том, что в проект нужно добавить Keycloak JS Adapter. Сделать это можно с помощью библиотеки keycloak-js. Добавим её в проект.

yarn add keycloak-js

Далее нам предлагают добавить в src/main.js код, который добавит в наш проект поддержку Keycloak.

// Параметры для подключения к Keycloaklet initOptions = {  url: 'http://127.0.0.1:8080/auth', // Адрес Keycloak  realm: 'keycloak-demo', // Имя нашего realm в Keycloak  clientId: 'app-vue', // Идентификатор клиента в Keycloak    // Перенаправлять неавторизованных пользователей на страницу входа  onLoad: 'login-required'}// Создать Keycloak JS Adapterlet keycloak = Keycloak(initOptions);// Инициализировать Keycloak JS Adapterkeycloak.init({ onLoad: initOptions.onLoad }).then((auth) => {  if (!auth) {    // Если пользователь не авторизован - перезагрузить страницу    window.location.reload();  } else {    Vue.$log.info("Authenticated");        // Если авторизован - инициализировать приложение Vue    new Vue({      el: '#app',      render: h => h(App, { props: { keycloak: keycloak } })    })  }  // Пытаемся обновить токен каждые 6 секунд  setInterval(() => {    // Обновляем токен, если срок его действия истекает в течении 70 секунд    keycloak.updateToken(70).then((refreshed) => {      if (refreshed) {        Vue.$log.info('Token refreshed' + refreshed);      } else {        Vue.$log.warn('Token not refreshed, valid for '          + Math.round(keycloak.tokenParsed.exp          + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds');      }    }).catch(() => {      Vue.$log.error('Failed to refresh token');    });  }, 6000)}).catch(() => {  Vue.$log.error("Authenticated Failed");});

С инициализацией Keycloak JS Adapter, вроде бы, всё понятно. А вот использование setInterval для обновления токенов мне показалось не очень практичным и красивым решением. Как минимум, кажется, что при бездействии пользователя на странице токены всё равно продолжат обновляться, хоть это и не требуется. На мой взгляд, обновление токенов лучше сделать так, как предлагает, например, автор данной статьи. Т.е. обновлять токены когда пользователь выполняет какое-либо действие в приложении. Автор указанной статьи выделяет три таких действия:

  • Взаимодействие с API (бэкендом)

  • Навигация (переход по страницам)

  • Переход на вкладку с нашим приложением, например, из другой вкладки

Приступим к реализации. Для того чтобы можно было обновлять токен из различных частей приложения, нам понадобится глобальный экземпляр Keycloak JS Adapter. Для этого во Vue.js существует функционал плагинов. Создадим свой плагин для Keycloak JS Adapter в файле /plugins/keycloak.js.

import Vue from 'vue'import Keycloak from 'keycloak-js'const initOptions = {    url: process.env.VUE_APP_KEYCLOAK_URL,    realm: 'list-keep',    clientId: 'list-keep'}const keycloak = Keycloak(initOptions)const KeycloakPlugin = {    install: Vue => {        Vue.$keycloak = keycloak    }}Vue.use(KeycloakPlugin)export default KeycloakPlugin

Значение адреса Keycloak, указанное в initOptions.url, может отличаться в зависимости от того где запущенно приложение (локально, на тесте, на проде), поэтому, чтобы иметь возможность указывать значения в зависимости от среды, будем использовать переменные окружения. Для локального запуска можно создать файл .env.local в корне проекта со следующим содержимым.

VUE_APP_KEYCLOAK_URL = http://localhost:8080/auth

Теперь нам достаточно импортировать файл с созданным нами плагином в main.js, и мы сможем из любого места приложения обратиться к нашему Keycloak JS Adapter с помощью Vue.$keycloak. Давайте это и сделаем, а так же создадим экземпляр Vue нашего приложения. Для этого изменим файл main.js следующим образом.

import Vue from 'vue'import App from './App.vue'import vuetify from './plugins/vuetify'import router from '@/router'import i18n from '@/plugins/i18n'import '@/plugins/keycloak'import { updateToken } from '@/plugins/keycloak-util'Vue.config.productionTip = falseVue.$keycloak.init({ onLoad: 'login-required' }).then((auth) => {  if (!auth) {    window.location.reload();  } else {    new Vue({      vuetify,      router,      i18n,      render: h => h(App)    }).$mount('#app')    window.onfocus = () => {      updateToken()    }  }})

Помимо инициализации Keycloak JS Adapter, здесь добавлен вызов функции updateToken() на событие window.onfocus, которое будет возникать при переходе пользователя на вкладку с нашим приложением. Наша функция updateToken() вызывает функцию updateToken() из Keycloak JS Adapter и, соответственно, обновляет токен, если срок жизни токена в секундах на данный момент меньше, чем значение TOKEN_MIN_VALIDITY_SECONDS, после чего возвращает актуальный токен.

import Vue from 'vue'const TOKEN_MIN_VALIDITY_SECONDS = 70export async function updateToken () {    await Vue.$keycloak.updateToken(TOKEN_MIN_VALIDITY_SECONDS)    return Vue.$keycloak.token}

Теперь добавим обновление токена на оставшиеся действия пользователя, а именно на взаимодействие с API и на навигацию. С API мы будем взаимодействовать с помощью axios. Помимо обновления токена нам в каждом запросе необходимо добавлять http-хидер Authorization: Bearer с нашим токеном для авторизации в нашем Spring Boot сервисе. Так же давайте будем перенаправлять на какую-нибудь страницу с ошибками, например, /error, если API будет возвращать нам ошибки. Для того чтобы выполнять какие-либо действие на любые запросы/ответы в axios существуют интерцепторы, добавить которые можно в App.vue.

<template>  <v-app>    <v-main>      <router-view></router-view>    </v-main>  </v-app></template><script>import Vue from 'vue'import axios from 'axios'import { updateToken } from '@/plugins/keycloak-util'const AUTHORIZATION_HEADER = 'Authorization'export default Vue.extend({  name: 'App',  created: function () {    axios.interceptors.request.use(async config => {      // Обновляем токен      const token = await updateToken()      // Добавляем токен в каждый запрос      config.headers.common[AUTHORIZATION_HEADER] = `Bearer ${token}`      return config    })        axios.interceptors.response.use( (response) => {      return response    }, error => {      return new Promise((resolve, reject) => {        // Если от API получена ошибка - отправляем на страницу /error        this.$router.push('/error')        reject(error)      })    })  },  // Обновляем токен при навигации  watch: {    $route() {      updateToken()    }  }})</script>

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

Интеграция с Keycloak закончена. Давайте теперь добавим тестовую страницу /pages/Home.vue, на которой будем вызывать с помощью axios тестовый метод /api/user/current, который мы ранее добавили в Spring Boot приложение, и выводить имя полученного пользователя.

<template>  <div>    <p>{{ user.name }}</p>  </div></template><script>import axios from 'axios'export default {  name: 'Home',  data() {    return {      user: {}    }  },  mounted() {    axios.get('/api/user/current')        .then(response => {          this.user = response.data        })  }}</script>

Для того чтобы можно было попасть на данную страницу в нашем приложении необходимо добавить её в router.js. Данная страница будет доступна по пути /.

import Vue from 'vue'import VueRouter from 'vue-router'import Home from '@/pages/Home'import Error from '@/pages/Error'import NotFound from '@/pages/NotFound'Vue.use(VueRouter)let router = new VueRouter({    mode: 'history',    routes: [        {            path: '/',            component: Home        },        {            path: '/error',            component: Error        },        {            path: '*',            component: NotFound        }    ]})export default router

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

И ещё немного о страницах

Помимо страницы /pages/Home.vue в роутере присутствуют страницы /pages/Error.vue и /pages/NotFound.vue. НаError , как уже упоминалось ранее, происходит переход из интерцептора при получении ошибок от API. На NotFound - если будет переход на неизвестную страницу.

Для примера давайте рассмотрим содержимое страницы Error.vue. Содержимое NotFound.vue практически ничем не отличается.

<template>  <v-container      class="text-center"      fill-height      style="height: calc(100vh - 58px);"  >    <v-row align="center">      <v-col>        <h1 class="display-2 primary--text">          {{ $t('oops.error.has.occurred') }}        </h1>        <p>{{ $t('please.try.again.later') }}</p>        <v-btn            href="http://personeltest.ru/aways/habr.com/"            color="primary"            outlined        >          {{ $t('go.to.main.page') }}        </v-btn>      </v-col>    </v-row>  </v-container></template><script>export default {  name: 'Error'}</script>

В шаблоне данной страницы используется локализация. Работает она с помощью плагина vue-i18n. Для того чтобы прикрутить локализацию своих текстовок нужно добавить переводы в виде json файлов в проект. Например, для русской локализации можно создать файл ru.json и положить его в каталог locales. Теперь эти текстовки необходимо загрузить в VueI18n. Сделать это можно, например, следующим образом. Давайте код по загрузке текстовок вынесем в/plugins/i18n.js.

import Vue from 'vue'import VueI18n from 'vue-i18n'Vue.use(VueI18n)function loadLocaleMessages () {    const locales = require.context('@/locales', true,                                    /[A-Za-z0-9-_,\s]+\.json$/i)    const messages = {}    locales.keys().forEach(key => {        const matched = key.match(/([A-Za-z0-9-_]+)\./i)        if (matched && matched.length > 1) {            const locale = matched[1]            messages[locale] = locales(key)        }    })    return messages}export default new VueI18n({    locale: 'ru',    fallbackLocale: 'ru',    messages: loadLocaleMessages()})

После этого к этим текстовкам можно будет обращаться из шаблона страницы с помощью $t.

Так же привожу содержимое /plugins/vuetify.js. В нём добавлена возможность использовать иконки Font Awesome на страницах нашего приложения.

import Vue from 'vue'import Vuetify from 'vuetify/lib/framework'import 'vuetify/dist/vuetify.min.css'import '@fortawesome/fontawesome-free/css/all.css'Vue.use(Vuetify);const opts = {    icons: {        iconfont: 'fa'    }}export default new Vuetify(opts)
Немного мыслей об обработке ошибок

Функции Keycloak JS Adapter init() и updateToken() возвращают объект KeycloakPromise, у которого есть возможность вызывать catch() и в нём обрабатывать ошибки. Но лично я не понял что именно в данном случае будет считаться ошибками и когда мы попадём в этот блок, т.к., например, если Keycloak не доступен, то в этот блок мы не попадаем. Поэтому в приведённом здесь приложении, я возможные ошибки от этих двух функций не обрабатываю. Возможно, если Keycloak не работает, то в продакшене стоит делать так, чтоб и наше приложение тоже становилось недоступным и не пытаться это как-то обработать. Ну или если всё-таки нужно такие ошибки понимать именно в Vue.js приложении, то, возможно, нужно как-то доработать keycloak-js.

Исходники проекта можно посмотреть тут.

Login Flows

Теперь давайте перейдём непосредственно к настройке процессов авторизации и регистрации, а так же соответствующих страниц. Т.к. возможностей по конфигурации Login Flows в Keycloak очень много, то давайте рассмотрим лишь некоторые из них, которые, на мой взгляд, являются наиболее важными. Предлагаю следующий список.

  • Авторизация и регистрация пользователей

  • Локализация страниц

  • Подтверждение email

  • Вход через социальные сети

Локализация страниц в Keycloak

Запустим наши Spring Boot и Vue.js приложения. При переходе в клиентское Vue.js приложение нас перенаправит на страницу логина Keycloak.

В первую очередь давайте добавим поддержку русского языка. Для этого в админке Keycloak, на вкладке Theams, в настройки realm включаем флаг Internationalization Enabled . В Supported Locales убираем все локали кроме ru, пусть наше приложение на Vue.js поддерживает только один язык. В Default Locale выставляем ru.

Нажимаем Save и возвращаемся в наше клиентское приложение.

Как видим, русский язык у нас появился, правда, не все текстовки были локализованы. Это можно исправить, добавив собственные варианты перевода. Сделать это можно на вкладке Localization, в настройках realm.

Здесь имеется возможность добавить текстовки вручную по одной, либо загрузить их из json файла. Давайте сделаем это вручную. Для начала требуется добавить локаль. Вводим ru и нажимаем Create. После чего попадаем на страницу Add localization text. На этой странице нам необходимо заполнить поля Key и Value. Если с value всё ясно, это будет просто значение нашей текстовки, то вот где взять Key не совсем понятно. В документации допустимые ключи нигде не описаны (либо я просто плохо искал), поэтому остаётся лишь найти их в исходниках Keycloak. Находим в ресурсах нужную нам базовую тему base и страницу login, а затем файл с текстовками в локали en - messages_en.properties. В этом файле по значению определяем нужный нам ключ текстовки, добавляем его в Key на странице Add localization text, а так же добавляем нужное нам Value и нажимаем Save.

После этого на вкладке Localization в настройках realm, при выборе локали ru, появляется таблица, в которой можно посмотреть, отредактировать или удалить нашу добавленную текстовку.

Вернёмся в наше клиентское приложение. Теперь все текстовки на странице логина локализованы.

Регистрация пользователей

Поддержку регистрации пользователей можно добавить, включив флаг User registration на вкладке Login в настройках realm.

После этого на странице логина появится кнопка Регистрация.

Нажимаем на кнопку Регистрация и попадаем на соответствующую страницу.

Давайте немного подкрутим эту страницу. Для начала добавим отсутствующий перевод текстовки, аналогично тому, как мы делали это ранее для страницы логина. Так же давайте уберём поле Имя пользователя. На самом деле совсем его убрать нельзя, т.к. это поля обязательно для заполнения у пользователя Keycloak, но можно сделать так, чтобы в качестве имени пользователя использовался email, при этом поле Имя пользователя исчезнет с формы регистрации. Сделать это можно, включив флаг Email as username на вкладке Login в настройках realm. После этого возвращаемся на страницу регистрации и видим что поле исчезло.

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

Подтверждение email

Давайте включим подтверждение email у пользователей, чтобы после регистрации они не могли зайти в наше приложение, пока не подтвердят свой email. Сделать это можно, включив флаг Verify email на вкладке Login в настройках realm. И нет, после этого волшебным образом всё не заработает, нужно ещё где-то добавить конфигурацию SMTP-сервера, с которого мы будем осуществлять рассылку. Сделать это можно на вкладке Email, в настройках realm. Ниже приведён пример настроек SMTP-сервера Gmail.

Нажимаем Test connection и получаем ошибку.

Ошибка возникает из-за того, что при нажатии на Test connection должно отправиться письмо на адрес пользователя, под которым мы сейчас залогинены в Keycloak, но этот адрес не задан. Соответственно, если вы заранее задали этот email, ошибки не будет.

Давайте зададим email нашему пользователю Keycloak. Для этого перейдём в realm master на страницу Users и нажмём View all users, чтобы отобразить всех пользователей.

Перейдём на страницу редактирования нашего пользователя и зададим ему email.

Возвращаемся на страницу конфигурации SMTP-сервера, снова пробуем Test connection и видим что всё рабо... Нет, мы снова видим ошибку. Правда, уже другую.

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

Снова жмём Test connection и, наконец-то, получаем Success.

Содержимое письма, которое будет ждать нас на почте, представлено ниже.

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

После нажатия на кнопку Регистрация. Мы попадём на страницу с предупреждением о том, что нужно подтвердить email. На эту страницу мы будем попадать каждый раз при логине в нашем приложении, до тех пор пока не подтвердим email.

На почту нам придёт письмо с ссылкой, по которой можно подтвердить email.

После перехода по ссылке мы попадём на нашу тестовую страницу /pages/Home.vue, на которой просто выводится имя пользователя. Т.к. в настройках нашего realm мы указали Email as username, то на данной странице мы увидим email нашего пользователя.

Social Login

Теперь добавим вход через социальные сети. В качестве примера давайте рассмотрим вход с помощью Google. Для того чтобы добавить данный функционал нужно в нашем realm создать соответствующий Identity Provider. Для этого нужно перейти на страницу Identity Providers и в списке Add provider... выбрать Google.

После этого мы попадём на страницу создания Identity Provider.

Здесь нам требуется задать два обязательных параметра - Client ID и Client Secret. Взять их можно из Google Cloud Platform.

Сказ о получении ключей из Google Cloud Platform

В первую очередь нам нужно создать в Google Cloud Platform проект.

Жмём CREATE PROJECT и попадаем на страницу создания проекта.

Задаём имя, жмём CREATE, ждём некоторое время, пока не будет создан наш проект, и после этого попадаем на DASHBOARD проекта.

Выбираем в меню APIs & Services -> Credentials. И попадаем на страницу на которой мы можем создавать различные ключи для нашего приложения.

Жмём Create credentials -> OAuth client ID и попадаем на очередную страницу.

Видим, что нам так просто не хотят давать возможность создавать ключи, а сначала просят создать некий OAuth consent screen. Что ж, хорошо, жмём CONFIGURE CONSENT SCREEN и снова новая страница.

Здесь давайте выберем External. Ну как выберем, выбора, на самом деле, у нас нет, т.к. Internal доступно только пользователямGoogle Workspace и эта штука платная и нужна, в общем-то, только организациям. Нажимаем Create и попадаем на страницу OAuth consent screen. Здесь заполняем название приложения и почты и жмём SAVE AND CONTINUE.

На следующей странице можно задать так называемые области действия OAuth 2.0 для API Google. Ничего задавать не будем, жмём SAVE AND CONTINUE.

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

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

Жмём BACK TO DASHBOARD, чтобы всё это уже закончить, и попадаем на страницу, на которой мы можем редактировать все те данные, которые мы вводили на предыдущих страницах.

Жмём Credentials, затем снова Create credentials -> OAuth client ID и попадаем на страницу создания OAuth client ID. И снова нужно что-то вводить. Google, ну сколько можно?! Ниже приведены поля, которые необходимо заполнить на этой странице.

  • Application type - выбираем Web application

  • Name - пишем имя нашего приложения

  • Authorized redirect URIs - сюда пишем значение из поля Redirect URI со страницы создания Identity Provider, чтобы Google редиректил пользователей на корректный адрес Keycloak после авторизации

Жмём CREATE и, наконец-то, получаем требуемые нам Client ID и Client Secret, которые нам нужно указать на странице создания Identity Provider в Keycloak.

Заполняем поля Client ID и Client Secret и жмём Save, чтобы создать Identity Provider. Теперь вернёмся на страницу логина нашего клиентского приложения. На этой странице появится нелокализованная текстовка, добавить её можно аналогично тому, как это было сделано ранее. Ниже на скрине ниже эта проблема уже устранена.

Итак, это всё что требовалось сделать, теперь мы можем входить в наше приложение с помощью Google.

Импорт и экспорт в Keycloak

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

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

После этого выгрузится файл realm-export.json с конфигурацией того realm в котором мы сейчас находимся. При этом различные пароли и секреты в этом файле будут в виде **********, поэтому, прежде чем куда-то импортировать этот файл, нужно заменить все такие значения на корректные. Либо сделать это после импорта через адиминку.

Импортировать данные можно на странице Import. Либо в yml-файле Docker Compose, если вы его используете. Для этого нужно указать в переменной окружения KEYCLOAK_IMPORT путь до ранее экспортированного файла и примонтировать этот файл в контейнер с помощью volumes. Итоговый файл приведен ниже.

# For developmentversion: "3.8"services:  keycloak:    image: jboss/keycloak:12.0.2    environment:      KEYCLOAK_USER: admin      KEYCLOAK_PASSWORD: admin      KEYCLOAK_IMPORT: "/tmp/realm-export.json"    volumes:      - "./keycloak/realm-export.json:/tmp/realm-export.json"    ports:      - 8080:8080
Импорт файлов локализации

Как уже упоминалось ранее, файлы локализации можно импортировать через админку. Помимо этого у Keycloak есть Admin REST API, а именно метод POST /{realm}/localization/{locale}, с помощью которого можно это сделать. В теории это можно использовать в Docker Compose, чтобы при запуске сразу загружать все текстовки в автоматическом режиме. На практике для этого можно написать bash-скрипт и вызвать его после того как в контейнере запустится Keycloak. Пример такого скрипта приведен ниже.

#!/bin/bashDIRECT_GRANT_RESPONSE=$(curl -i --request POST http://localhost:8080/auth/realms/master/protocol/openid-connect/token --header "Accept: application/json" --header "Content-Type: application/x-www-form-urlencoded" --data "grant_type=password&username=admin&password=admin&client_id=admin-cli");export DIRECT_GRANT_RESPONSEACCESS_TOKEN=$(echo $DIRECT_GRANT_RESPONSE | grep "access_token" | sed 's/.*\"access_token\":\"\([^\"]*\)\".*/\1/g');export ACCESS_TOKENcurl -i --request POST http://localhost:8080/auth/admin/realms/list-keep/localization/ru -F "file=@ru.json" --header "Content-Type: multipart/form-data" --header "Authorization: Bearer $ACCESS_TOKEN";

И в докер образе jboss/keycloak даже есть возможность запускать скрипты при старте (см. раздел Running custom scripts on startup на странице докер образа). Но запускаются они до фактического старта Keycloak. Поэтому пока я оставил данный вопрос не решенным. Если у кого-то есть идеи как это можно красиво сделать - оставляйте их в комментариях.

Заключение

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

Подробнее..

Серия вебинаров по серверной разработке на Kotlin

07.12.2020 14:17:59 | Автор: admin
Все больше инженеров выбирают Kotlin для разработки серверных приложений. Полная совместимость с Java, корутины и высокая безопасность делают Kotlin отличным инструментом для подобных задач.

Мы организуем серию вебинаров (на английском языке), где расскажем о разработке бэкенда на Kotlin в сочетании с технологиями Apache Kafka, Spring Boot и Google Cloud Platform. Вебинары подойдут для Kotlin- и Java-разработчиков любого уровня подготовленности, в том числе для разработчиков мобильных приложений без опыта серверной разработки.

Kotlin и Apache Kafka, 10 декабря 2020, 19:30 20:30 МСК
Kotlin и Google Cloud Platform, 17 декабря 2020, 19:30 20:30 МСК
Kotlin и Spring Boot, 14 января 2021, 19:30 20:30 МСК

Подробнее о каждом из вебинаров читайте ниже.


Kotlin и Apache Kafka


image

Зарегистрироваться

O чем этот вебинар?


Виктор и Антон покажут, как использовать Apache Kafka в сочетании с Kotlin для управления потоками данных. Презентация также даст обзор внутренней архитектуры Apache Kafka.

Спикеры:
  • Антон Архипов, Developer Advocate в команде Kotlin, JetBrains
  • Виктор Гамов, Developer Advocate, Confluent

Когда состоится вебинар?


10 декабря 2020
19:30 20:30 МСК
Подробная информация

Kotlin и Google Cloud Platform


image

Зарегистрироваться

О чем этот вебинар?


На вебинаре мы расскажем, как применить Kotlin в проекте с кодовой базой на Java, поговорим о таких возможностях Kotlin, как корутины, и продемонстрируем процесс развертывания приложения на Google Cloud.

Спикер: Джеймс Уорд, Developer Advocate, Google Cloud Platform

Когда состоится вебинар?


17 декабря 2020
19:30 20:30 МСК
Подробная информация

Kotlin и Spring Boot


image

Зарегистрироваться

О чем этот вебинар?


В ходе вебинара Рэй поможет вам создать бэкенд-сервис с использованием Spring Boot и Spring Cloud GCP и покажет полезные сервисы Google Cloud для баз данных, хранения и мониторинга состояния. Когда приложение будет готово, Рэй покажет, как с помощью Google Cloud Platform сделать из него бессерверный сервис, и продемонстрирует возможности автоматического масштабирования.

Спикер: Рэй Цанг, Developer Advocate в Google Cloud Platform и Java Champion

Когда состоится вебинар?


14 января 2021
19:30 20:30 МСК
Подробная информация

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

Все вебинары бесплатные. Записи будут доступны на канале JetBrains TV в Youtube. Подпишитесь на JetBrains TV, чтобы не пропустить появление записей.

Узнайте больше о бэкенд-разработке на Kotlin


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

Ваша команда Kotlin
The Drive to Develop
Подробнее..

Если у вас не работает Spring BootJar

28.12.2020 00:15:11 | Автор: admin

Проблема с загрузкой Spring Boot Jar


image


Сталкивались ли вы с проблемой запуска нового загрузочного архива Spring Boot?


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


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


Здесь я расскажу о часто встречающемся случае с потерей ресурсов. Конкретнее в моём случае с потерей JSP шаблонов. Почему JSP? Мне так привычнее, проект я по-быстрому начал с них, и не думал, что будут такие проблемы.


Итак структура проекта (стандартный веб):


/src/main/    java/    resources/        static/            some.html        public/    webapp/        WEB-INF/jsp            index.jsp

По заявлению создателей BootJar / BootWar, jsp не поддерживается толком в новом BootJar формате. Но совместимость должна быть в BootWar. На это я и надеялся, когда ваял код. Пока ваял, никаких проблем всё запускается, всё работает, как обычно, в общем. BootRun отрабатывает, только опции успевай подставлять.


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


Итак, дубль раз. Чуть ли не в первый раз запускаю задачу BootJar. Ярхив готов, деплоим Готово! Сигнала нет, сыпет ошибки 302 + 404 (это авторизация не находит вью). Но это пока не понятно.
Отключаем Секурити всё равно ничего не находит, кроме голимой статики, и то не всей, а только из webjars. ???


Дубль два. Догадываясь о несовместимости jsp и BootJar, пакуем BootWar. Деплоим Не работает. Не помню точно, но примерно то же самое в результате.


Хм, странно. Запускаем BootJar локально всё работает. Чудеса.


Выяснилось: Spring Boot слишком умный, он при запуске из каталога разработки даже релизного ярника всё равно все тащит из каталога разработки. Из другого запускаем перестаёт работать. Фух! Ну хоть отлаживать можно.


Что и делам. Выясняется ресурсы BootJar запакованные не из библиотек (webjars), а из проекта, не включаются в перечисление, и это, оказывается, по дизайну! Подробности здесь.


Вернее не так есть спец-ресурсы в каталогах типа static/, public/. И они вроде находятся, если объявить. Но jsp не видит в упор хоть тресни. И дело не в том, что не там лежат. Оказалось, что Томкат (в нашем плохом случае), грузит jsp особым механизмом после редиректа. И сами jsp можно загрузить без рендеринга, если правильно задать их положение в настройках
spring.resources.static-locations


Но это нас не устраивает.
Оказалось, что при использования встроенного томката, ресурсы вью он грузит отдельно и в первую очередь своей старой встроенной логикой, которую Спрингисты настраивать не научились. А этой логике нужен либо архив Вар, либо он же распакованный (почему кстати при разработке нормально отрабатывает расположение webapp/), либо ресурсы из библиотек, которые прекрасно видны, если правильно упакованы в изначальных либах нужно чтоб лежали в META-INF/resources, как в стандарте сервлетов. Последнее работает даже внутри BootJar. Удивительно.


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


Почему не работает способ с Вар-архивом? Ё-маё, Амазон решил, что лучше меня знает, что я буду грузить именно в яр-формате, хотя интерфейс заявлет о готовности съесть варник. Он этот варник переименовывает в ярник, умник такой. А Томкат не умничает, он смотрит расширение: не Вар ну тогда, извините, это не мой случай.


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


Ладно, проблема понятна. Решение?


Было три варианта.


  1. Сделать свой spring-загрузчик ресурсов. Вариант отпал, поскольку, как я уже сказал, Томкат отрабатывает jsp до их запуска.
  2. Прокинуть загрузку в Томкат. Стал прикидывать: надо расширить контекст spring-а, прокинуть пути в контекст Томката, там ещё раза два переложить непонятно, насколько сложно, и можно ли без изменения самого томката. Спрингисты не осилили, и я не хочу.
  3. Вариант попроще пакуем ресурсы в ресурсную либу в BootJar.

Вот о нём подробнее. Пихать всё в отдельный модуль, как предлагали здесь, не хотелось.
Делаем отдельной задачей в Gradle.
Создаём конфигурацию.


sourceSets {    jsp {        resources.source(sourceSets.main.resources);        resources.srcDirs += ['src/main/webapp'];    }    jmh {        .. ..    }}

Сама задача


task jsp(type: Jar, description: 'JSP Packaging') {    archiveBaseName = 'jsp'    group = "build"    def art = sourceSets.jsp.output    from(art) {        exclude('META-INF')        into('META-INF/resources/')    }    from(art) {        include('META-INF/*')        into('/')    }    dependsOn(processJspResources)}

Задача processJspResources уже создана, её не надо делать. Ставим всё в зависимость и пакуем:


bootJar {    dependsOn(jsp)    bootInf.with {        from(jsp.archiveFile) {            include('**/*.jar')        }        into('lib/')    }}

Как добавить другим способом (прямым), не нашёл подключить в зависимости конфиг jspImplementation самого же проекта нельзя, а хотелось бы. Но если все же из другого модуля забирать, то вот так ещё делаем:


artifacts {    jspImplementation jsp}

Всё, теперь имеем ресурсную либу, которую по всем стандартам томкат должен грузить, и он грузит. Запускаем, как BootJar.

Подробнее..

Перевод Spring Cloud и Spring Boot. Часть 1 использование Eureka Server

26.01.2021 20:09:19 | Автор: admin

Будущих студентов курса Разработчик на Spring Framework и всех желающих приглашаем на открытый онлайн-урок по теме Введение в облака, создание кластера в Mongo DB Atlas018. Участники вместе с преподавателем-экспертом поговорят о видах облаков и настроят бесплатный Mongo DB кластер для своих проектов.

А сейчас делимся с вами традиционным переводом статьи.


В этой статье мы поговорим о том, как установить и настроить службу обнаружения (service discovery) для Java-микросервисов.

Что такое Eureka Server?

Eureka Server это service discovery (обнаружение сервисов) для ваших микросервисов. Клиентские приложения могут самостоятельно регистрироваться в нем, а другие микросервисы могут обращаться к Eureka Server для поиска необходимых им микросервисов.

Eureka Server также известен как Discovery Server и содержит такую информацию как IP-адрес и порт микросервиса.

Для создания приложения с Eureka Server необходимо в pom.xml добавить указанную ниже зависимость.

<dependency>  <groupId>org.springframework.cloud</groupId>  <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency>

Запускаем Eureka Server

Перейдите на https://start.spring.io и создайте шаблон проекта. Укажите метаданные, такие как Group, Artifact, и добавьте указанные ниже зависимости / модули. Нажмите "Generate Project" и загрузите проект в zip-файле. Далее разархивируйте его и импортируйте в IDE как Maven-проект.

  • Eureka Server

  • Web

  • Actuator

Проверьте, что pom.xml выглядит следующим образом:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://personeltest.ru/away/maven.apache.org/POM/4.0.0" xmlns:xsi="http://personeltest.ru/away/www.w3.org/2001/XMLSchema-instance"        xsi:schemaLocation="http://personeltest.ru/away/maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">   <modelVersion>4.0.0</modelVersion>   <parent>       <groupId>org.springframework.boot</groupId>       <artifactId>spring-boot-starter-parent</artifactId>       <version>2.0.7.RELEASE</version>       <relativePath/> <!-- lookup parent from repository -->   </parent>   <groupId>com.example.eureka.server</groupId>   <artifactId>eureka-server</artifactId>   <version>0.0.1-SNAPSHOT</version>   <name>eureka-server</name>   <description>Demo project for Spring Boot</description>   <properties>       <java.version>1.8</java.version>       <spring-cloud.version>Finchley.SR2</spring-cloud.version>   </properties>   <dependencies>       <dependency>           <groupId>org.springframework.boot</groupId>           <artifactId>spring-boot-starter-actuator</artifactId>       </dependency>       <dependency>           <groupId>org.springframework.boot</groupId>           <artifactId>spring-boot-starter-web</artifactId>       </dependency>       <dependency>           <groupId>org.springframework.cloud</groupId>           <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>       </dependency>       <dependency>           <groupId>org.springframework.boot</groupId>           <artifactId>spring-boot-starter-test</artifactId>           <scope>test</scope>       </dependency>   </dependencies>   <dependencyManagement>       <dependencies>           <dependency>               <groupId>org.springframework.cloud</groupId>               <artifactId>spring-cloud-dependencies</artifactId>               <version>${spring-cloud.version}</version>               <type>pom</type>               <scope>import</scope>           </dependency>       </dependencies>   </dependencyManagement>   <build>       <plugins>           <plugin>               <groupId>org.springframework.boot</groupId>               <artifactId>spring-boot-maven-plugin</artifactId>           </plugin>       </plugins>   </build></project>

Теперь откройте файл EurekaServerApplication.java и добавьте для класса аннотацию @EnableEurekaServer, как показано ниже.

package com.example.eureka.server.eurekaserver;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@SpringBootApplication@EnableEurekaServerpublic class EurekaServerApplication {  public static void main(String[] args) {     SpringApplication.run(EurekaServerApplication.class, args);  }}

Добавьте в application.properties, расположенный в src/main/resources, следующие параметры.

spring.application.name=eureka-serverserver.port=8761eureka.client.register-with-eureka=falseeureka.client.fetch-registry=false
  • spring.application.name уникальное имя для вашего приложения.

  • server.port порт, на котором будет запущено ваше приложение, мы будем использовать порт по умолчанию (8761).

  • eureka.client.register-with-eureka определяет, регистрируется ли сервис как клиент на Eureka Server.

  • eureka.client.fetch-registry получать или нет информацию о зарегистрированных клиентах.

Запустите сервер Eureka как Java-приложение и перейдите по адресу http://localhost:8761/

Вы увидите, что Eureka Server запущен и работает, но в нем еще нет зарегистрированных приложений.

Регистрация клиентского приложения в Eureka Server

Перейдите на https://start.spring.io и создайте шаблон проекта. Укажите метаданные, такие как Group, Artifact, и добавьте указанные ниже зависимости / модули. Нажмите "Generate Project" и загрузите проект в zip-файле. Далее разархивируйте его и импортируйте в IDE как Maven-проект.

  • DevTools

  • Actuator

  • Discovery Client

Проверьте, что ваш pom.xml выглядит следующим образом:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://personeltest.ru/away/maven.apache.org/POM/4.0.0" xmlns:xsi="http://personeltest.ru/away/www.w3.org/2001/XMLSchema-instance"      xsi:schemaLocation="http://personeltest.ru/away/maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">  <modelVersion>4.0.0</modelVersion>  <parent>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-parent</artifactId>     <version>2.0.7.RELEASE</version>     <relativePath/> <!-- lookup parent from repository -->  </parent>  <groupId>com.example.eureka.client</groupId>  <artifactId>EurekaClientApplication</artifactId>  <version>0.0.1-SNAPSHOT</version>  <name>EurekaClientApplication</name>  <description>Demo project for Spring Boot</description>   <properties>     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>     <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>     <java.version>1.8</java.version>     <spring-cloud.version>Finchley.SR2</spring-cloud.version>  </properties>   <dependencies>     <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>     </dependency>     <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-actuator</artifactId>     </dependency>     <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-devtools</artifactId>        <scope>runtime</scope>     </dependency>     <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-test</artifactId>        <scope>test</scope>     </dependency>     <dependency>        <groupId>org.springframework.cloud</groupId>        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>     </dependency>  </dependencies>   <dependencyManagement>     <dependencies>        <dependency>           <groupId>org.springframework.cloud</groupId>           <artifactId>spring-cloud-dependencies</artifactId>           <version>${spring-cloud.version}</version>           <type>pom</type>           <scope>import</scope>        </dependency>     </dependencies>  </dependencyManagement>  <build>     <plugins>        <plugin>           <groupId>org.springframework.boot</groupId>           <artifactId>spring-boot-maven-plugin</artifactId>        </plugin>     </plugins>  </build></project>

Откройте файл EurekaClientApplication.java и добавьте для класса аннотацию @EnableDiscoveryClient, как показано ниже.

package com.example.eureka.client.application;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication@EnableDiscoveryClientpublic class EurekaClientApplication {  public static void main(String[] args) {     SpringApplication.run(EurekaClientApplication.class, args);  }}

Добавление REST-контроллера

Создайте класс HelloWorldController в пакете com.example.eureka.client.application и добавьте GET-метод в этом классе.

package com.example.eureka.client.application;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class HelloWorldController {   @GetMapping("/hello-worlds/{name}")   public String getHelloWorld(@PathVariable String name) {       return "Hello World " + name;   }}

В application.properties, расположенный в src/main/resources, добавьте следующие параметры.

spring.application.name=eureka-client-serviceserver.port=8081eureka.client.service-url.defaultZone=http://localhost:8761/eureka/

Параметр eureka.client.service-url.defaultZone определяет адрес Eureka Server, чтобы клиентское приложение могло там зарегистрироваться.

Запуск клиентского приложения

Перед запуском приложения необходимо убедиться, что Eureka Server запущен и работает. Запустите нашего клиента как Java-приложение и перейдите в Eureka Server по адресу http://localhost:8761/. На этот раз вы должны увидеть, что наше клиентское приложение зарегистрировалось на Eureka Server.

Теперь вы знаете как использовать Eureka Server для своих микросервисов. В следующей статье посмотрим на распределенную трассировку (Distributed Tracing) для Spring Boot-микросервисов.


Узнать подробнее о курсе Разработчик на Spring Framework.

Записаться на открытый онлайн-урок по теме
Введение в облака, создание кластера в Mongo DB Atlas018.

Подробнее..

Тривиальная и неправильная облачная компиляция

28.01.2021 00:21:03 | Автор: admin


Введение


Данная статья не история успеха, а скорее руководство как не надо делать. Весной 2020 для поддержания спортивного тонуса участвовал в студенческом хакатоне (спойлер: заняли 2-е место). Удивительно, но задача из полуфинала оказалась более интересной и сложной чем финальная. Как вы поняли, о ней и своём решении расскажу под катом.


Задача


Данный кейс был предложен Deutsche Bank в направлении WEB-разработка.
Необходимо было разработать онлайн-редактор для проекта Алгосимулятор тестового стенда для проверки работы алгоритмов электронной торговли на языке Java. Каждый алгоритм реализуется в виде наследника класса AbstractTradingAlgorythm.


AbstractTradingAlgorythm.java
public abstract class AbstractTradingAlgorithm {    abstract void handleTicker(Ticker ticker) throws Exception;    public void receiveTick(String tick) throws Exception {        handleTicker(Ticker.parse(tick));    }    static class Ticker {        String pair;        double price;       static Ticker parse(String tick) {           Ticker ticker = new Ticker();           String[] tickerSplit = tick.split(",");           ticker.pair = tickerSplit[0];           ticker.price = Double.valueOf(tickerSplit[1]);           return ticker;       }    }}

Сам же редактор во время работы говорит тебе три вещи:


  1. Наследуешь ли ты правильный класс
  2. Будут ли ошибки на этапе компиляции
  3. Успешен ли тестовый прогон алгоритма. В данном случае подразумевается, что "В результате вызова new <ClassName>().receiveTick(RUBHGD,100.1) отсутствуют runtime exceptions".


Ну окей, скелет веб-сервиса через spring накидать дело на 5-10 минут. Пункт 1 работа для регулярных выражений, поэтому даже думать об этом сейчас не буду. Для пункта 2 можно конечно написать синтаксический анализатор, но зачем, когда это уже сделали за меня. Может и пункт 3 получится сделать, использовав наработки по пункту 2. В общем, дело за малым, уместить в один метод, ну например, компиляцию исходного кода программы на Java, переданного в контроллер строкой.


Решение


Здесь и начинается самое интересное. Забегая вперёд, как сделали другие ребята: установили на машину джаву, отдавали команды на ось и грепали stdout. Конечно, это более универсальный метод, но во-первых, нам сказали слово Java, а во-вторых...



у каждого свой путь.


Естественно, Java окружение устанавливать и настраивать всё же придётся. Правда компилировать и исполнять код мы будем не в терминале, а, как бы это ни звучало, в коде. Начиная с 6 версии, в Java SE присутствует пакет javax.tools, добавленный в стандартный API для компиляции исходного кода Java.
Теперь привычные понятия такие, как файлы с исходным кодом, параметры компилятора, каталоги с выходными файлами, сообщения компилятора, превратились в абстракции, используемые при работе с интерфейсом JavaCompiler, через реализации которого ведётся основная работа с задачами компиляции. Подробней о нём можно прочитать в официальной документации. Главное, что оттуда сейчас перейдёт моментально в текст статьи, это класс JavaSourceFromString. Дело в том, что, по умолчанию, исходный код загружается из файловой системы. В нашем же случае исходный код будет приходить строкой извне.


JavaSourceFromString.java
import javax.tools.SimpleJavaFileObject;import java.net.URI;public class JavaSourceFromString extends SimpleJavaFileObject {    final String code;    public JavaSourceFromString(String name, String code) {        super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);        this.code = code;    }    @Override    public CharSequence getCharContent(boolean ignoreEncodingErrors) {        return code;    }}

Далее, в принципе уже ничего сложного нет. Получаем строку, имя класса и преобразуем их в объект JavaFileObject. Этот объект передаём в компилятор, компилируем и собираем вывод, который и возвращаем на клиент.
Сделаем класс Validator, в котором инкапсулируем процесс компиляции и тестового прогона некоторого исходника.


public class Validator {    private JavaSourceFromString sourceObject;    public Validator(String className, String source) {        sourceObject = new JavaSourceFromString(className, source);    }}

Далее добавим компиляцию.


public class Validator {    ...    public List<Diagnostic<? extends JavaFileObject>> compile() {        // получаем компилятор, установленный в системе        var compiler = ToolProvider.getSystemJavaCompiler();        // компилируем        var compilationUnits = Collections.singletonList(sourceObject);        var diagnostics = new DiagnosticCollector<JavaFileObject>();        compiler.getTask(null, null, diagnostics, null, null, compilationUnits).call();        // возворащаем диагностику        return diagnostics.getDiagnostics();    }}

Пользоваться этим можно как-то так.


public void MyMethod() {        var className = "TradeAlgo";        var sourceString = "public class TradeAlgo extends AbstractTradingAlgorithm{\n" +                "@Override\n" +                "    void handleTicker(Ticker ticker) throws Exception {\n" +                "       System.out.println(\"TradeAlgo::handleTicker\");\n" +                "    }\n" +                "}\n";        var validator = new Validator(className, sourceString);        for (var message : validator.compile()) {            System.out.println(message);        }    }

При этом, если компиляция прошла успешно, то возвращённый методом compile список будет пуст. Что интересно? А вот что.

На приведённом изображении вы можете видеть директорию проекта после завершения выполнения программы, во время выполнения которой была осуществлена компиляция. Красным прямоугольником обведены .class файлы, сгенерированные компилятором. Куда их девать, и как это чистить, не знаю жду в комментариях. Но что это значит? Что скомпилированные классы присоединяются в runtime, и там их можно использовать. А значит, следующий пункт задачи решается тривиально с помощью средств рефлексии.
Создадим вспомогательный POJO для хранения результата прогона.


TestResult.java
public class TestResult {    private boolean success;    private String comment;    public TestResult(boolean success, String comment) {        this.success = success;        this.comment = comment;    }    public boolean success() {        return success;    }    public String getComment() {        return comment;    }}

Теперь модифицируем класс Validator с учётом новых обстоятельств.


public class Validator {    ...    private String className;    private boolean compiled = false;    public Validator(String className, String source) {        this.className = className;        ...    }    ...    public TestResult testRun(String arg) {        var result = new TestResult(false, "Failed to compile");        if (compiled) {            try {                // загружаем класс                var classLoader = URLClassLoader.newInstance(new URL[]{new File("").toURI().toURL()});                var c = Class.forName(className, true, classLoader);                // создаём объект класса                var constructor = c.getConstructor();                var instance = constructor.newInstance();                // выполняем целевой метод                c.getDeclaredMethod("receiveTick", String.class).invoke(instance, arg);                result = new TestResult(true, "Success");            } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException | RuntimeException | MalformedURLException | InstantiationException e) {                var sw = new StringWriter();                e.printStackTrace(new PrintWriter(sw));                result = new TestResult(false, sw.toString());            }        }        return result;    }}

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


public void MyMethod() {        ...        var result = validator.testRun("RUBHGD,100.1");        System.out.println(result.success() + " " + result.getComment());    }

Вставить этот код в реализацию API контроллера задача нетрудная, поэтому подробности её решения можно опустить.


Какие проблемы?


  1. Ещё раз напомню про кучу .class файлов.


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


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



Поэтому делать в точности как я не надо)


P.S. Ссылка на гитхаб с исходным кодом из статьи.

Подробнее..

Перевод Шпаргалка по Spring Boot WebClient

09.02.2021 02:22:17 | Автор: admin

В преддверии старта курса Разработчик на Spring Framework подготовили традиционный перевод полезного материала.

Также абсолютно бесплатно предлагаем посмотреть запись демо-урока на тему
Введение в облака, создание кластера в Mongo DB Atlas.


WebClient это неблокирующий, реактивный клиент для выполнения HTTP-запросов.

Время RestTemplate подошло к концу

Возможно, вы слышали, что время RestTemplate на исходе. Теперь это указано и в официальной документации:

NOTE: As of 5.0 this class is in maintenance mode, with only minor requests for changes and bugs to be accepted going forward. Please, consider using the org.springframework.web.reactive.client.WebClient which has a more modern API and supports sync, async, and streaming scenarios.

ПРИМЕЧАНИЕ: Начиная с версии 5.0, этот класс законсервирован и в дальнейшем будут приниматься только минорные запросы на изменения и на исправления багов. Пожалуйста, подумайте об использовании org.springframework.web.reactive.client.WebClient, который имеет более современный API и поддерживает синхронную, асинхронную и потоковую передачи.

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

Отличия между WebClient и RestTemplate

Если в двух словах, то основное различие между этими технологиями заключается в том, что RestTemplate работает синхронно (блокируя), а WebClient работает асинхронно (не блокируя).

RestTemplate это синхронный клиент для выполнения HTTP-запросов, он предоставляет простой API с шаблонным методом поверх базовых HTTP-библиотек, таких как HttpURLConnection (JDK), HttpComponents (Apache) и другими.

Spring WebClient это асинхронный, реактивный клиент для выполнения HTTP-запросов, часть Spring WebFlux.

Вам, вероятно, интересно, как можно заменить синхронного клиента на асинхронный. У WebClient есть решение этой задачи. Мы рассмотрим несколько примеров использования WebClient.

А сейчас настало время попрощаться с RestTemplate , сказать ему спасибо и продолжить изучение WebClient.

Начало работы с WebClient

Предварительные условия

Подготовка проекта

Давайте создадим базовый проект с зависимостями, используя Spring Initializr.

Теперь взглянем на зависимости нашего проекта. Самая важная для нас зависимость spring-boot-starter-webflux.

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId> </dependency>

Spring WebFlux является частью Spring 5 и обеспечивает поддержку реактивного программирования для веб-приложений.

Пришло время настроить WebClient.

Настройка WebClient

Есть несколько способов настройки WebClient. Первый и самый простой создать его с настройками по умолчанию.

WebClient client = WebClient.create();

Можно также указать базовый URL:

WebClient client = WebClient.create("http://base-url.com");

Третий и самый продвинутый вариант создать WebClient с помощью билдера. Мы будем использовать конфигурацию, которая включает базовый URL и таймауты.

@Configurationpublic class WebClientConfiguration {    private static final String BASE_URL = "https://jsonplaceholder.typicode.com";    public static final int TIMEOUT = 1000;    @Bean    public WebClient webClientWithTimeout() {        final var tcpClient = TcpClient                .create()                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, TIMEOUT)                .doOnConnected(connection -> {                    connection.addHandlerLast(new ReadTimeoutHandler(TIMEOUT, TimeUnit.MILLISECONDS));                    connection.addHandlerLast(new WriteTimeoutHandler(TIMEOUT, TimeUnit.MILLISECONDS));                });        return WebClient.builder()                .baseUrl(BASE_URL)                .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient)))                .build();    }}

Параметры, поддерживаемые WebClient.Builder можно посмотреть здесь.

Подготовка запроса с помощью Spring WebClient

WebClient поддерживает методы: get(), post(), put(), patch(), delete(), options() и head().

Также можно указать следующие параметры:

  • Переменные пути (path variables) и параметры запроса с помощью метода uri().

  • Заголовки запроса с помощью метода headers().

  • Куки с помощью метода cookies().

После указания параметров можно выполнить запрос с помощью retrieve() или exchange(). Далее мы преобразуем результат в Mono с помощью bodyToMono() или во Flux с помощью bodyToFlux().

Асинхронный запрос

Давайте создадим сервис, который использует бин WebClient и создает асинхронный запрос.

@Service@AllArgsConstructorpublic class UserService {    private final WebClient webClient;    public Mono<User> getUserByIdAsync(final String id) {        return webClient                .get()                .uri(String.join("", "/users/", id))                .retrieve()                .bodyToMono(User.class);    }}

Как вы видите, мы не сразу получаем модель User. Вместо User мы получаем Mono-обертку, с которой выполняем различные действия. Давайте подпишемся неблокирующим способом, используя subscribe().

userService  .getUserByIdAsync("1")  .subscribe(user -> log.info("Get user async: {}", user));

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

Синхронный запрос

Если вам нужен старый добрый синхронный вызов, то это легко сделать с помощью метода block().

public User getUserByIdSync(final String id) {    return webClient            .get()            .uri(String.join("", "/users/", id))            .retrieve()            .bodyToMono(User.class)            .block();}

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

Повторные попытки

Мы все знаем, что сетевой вызов не всегда может быть успешным. Но мы можем перестраховаться и в некоторых случаях выполнить его повторно. Для этого используется метод retryWhen(), который принимает в качестве аргумента класс response.util.retry.Retry.

public User getUserWithRetry(final String id) {    return webClient            .get()            .uri(String.join("", "/users/", id))            .retrieve()            .bodyToMono(User.class)            .retryWhen(Retry.fixedDelay(3, Duration.ofMillis(100)))            .block();}

С помощью билдера можно настроить параметры и различные стратегии повтора (например, экспоненциальную). Если вам нужно повторить успешную попытку, то используйте repeatWhen() или repeatWhenEmpty() вместо retryWhen().

Обработка ошибок

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

  • doOnError() срабатывает, когда Mono завершается с ошибкой.

  • onErrorResume() при возникновении ошибки подписывается на резервного издателя, используя функцию для выбора действия в зависимости от ошибки.

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

public User getUserWithFallback(final String id) {    return webClient            .get()            .uri(String.join("", "/broken-url/", id))            .retrieve()            .bodyToMono(User.class)            .doOnError(error -> log.error("An error has occurred {}", error.getMessage()))            .onErrorResume(error -> Mono.just(new User()))            .block();}

В некоторых ситуациях может быть полезно отреагировать на конкретный код ошибки. Для этого можно использовать метод onStatus().

public User getUserWithErrorHandling(final String id) {  return webClient          .get()          .uri(String.join("", "/broken-url/", id))          .retrieve()              .onStatus(HttpStatus::is4xxClientError,                      error -> Mono.error(new RuntimeException("API not found")))              .onStatus(HttpStatus::is5xxServerError,                      error -> Mono.error(new RuntimeException("Server is not responding")))          .bodyToMono(User.class)          .block();}

Клиентские фильтры

Для перехвата и изменения запроса можно настроить фильтры через билдер WebClient .

WebClient.builder()  .baseUrl(BASE_URL)  .filter((request, next) -> next          .exchange(ClientRequest.from(request)                  .header("foo", "bar")                  .build()))  .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient)))  .build();

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

WebClient.builder()  .baseUrl(BASE_URL)  .filter(basicAuthentication("user", "password")) // org.springframework.web.reactive.function.client.basicAuthentication()  .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient)))  .build();

Заключение

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

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

Удачи с новым Spring WebClient!


Узнать подробнее о курсе Разработчик на Spring Framework.

Посмотреть запись демо-урока на тему Введение в облака, создание кластера в Mongo DB Atlas.

Подробнее..

Перевод Okta безопасный доступ к приложениям на Angular Spring Boot

30.04.2021 20:15:00 | Автор: admin

Перевод статьи подготовлен в рамках набора учащихся на курс Разработчик на Spring Framework.

Приглашаем также всех желающих на открытый демо-урок Конфигурация Spring-приложений. На занятии рассмотрим, как можно конфигурировать Spring Boot приложения с помощью свойств:
- properties vs. YAML
- @Value + SpEL
- @ConfigurationProperties
- Externalized конфигурация.
Присоединяйтесь!


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

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

Для авторизации пользователей в Okta используется протокол OAuth2. Подробнее о протоколе OAuth2 и стандарте Open ID Connect (OIDC) можно почитать в блоге Okta.

В этой статье мы рассмотрим демонстрационное приложение Контакты и настройку разрешений на просмотр, создание, редактирование и удаление контактов. Это вторая часть цикла статей о приложении Контакты, посвященная его защите с помощью Okta. О базовом приложении подробно написано в первой статье цикла: Разработка веб-приложения на Spring Boot + Angular.

Полный код, фрагменты которого мы рассматриваем в этой статье, доступен на GitHub.

В итоге наше приложение будет иметь следующую архитектуру:

Компонентная архитектура приложения Контакты, использующего протокол OAuthКомпонентная архитектура приложения Контакты, использующего протокол OAuth

О том, как контейнеризировать это приложение и развернуть его в кластере Kubernetes, можно узнать по ссылкам:

Kubernetes: развертывание приложения на Angular + Java/ Spring Boot в Google Kubernetes Engine (GKE)

Kubernetes: развертывание приложения на Angular + Spring Boot в облаке Microsoft Azure

Конфигурация на портале разработчика Okta

Для начала нам нужно настроить учетную запись Okta и добавить приложение в Okta. Здесь можно создать учетную запись разработчика бесплатно.

После создания учетной записи мы можем добавить наше приложение в консоль администрирования Okta. Для этого выберем вкладку Applications (Приложения) и щелкнем Add Application (Добавить приложение).

Выберем в качестве платформы Single Page App (Одностраничное приложение) и настроим его так, как показано на скриншоте ниже. Здесь мы отметим только опцию Authorization code (Код авторизации), поскольку нам не нужно, чтобы токен возвращался непосредственно в URL-адресе (о связанных с этим рисках безопасности написано в этой статье в блоге Okta).

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

OAuth предлагает различные потоки авторизации (authorization code flows) выбор конкретного потока зависит от сценария использования приложения. О том, какой поток OAuth выбрать, можно прочитать в этой статье на Medium или в документации Okta. Пользовательский интерфейс нашего демонстрационного приложения представляет собой одностраничное приложение на Angular, поэтому выберем неявный поток (implicit flow) с дополнительной проверкой для обмена кодом (Proof Key Code Exchange, PKCE).

Авторизация запросов через сервер авторизации

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

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

Добавление заявлений о группах (groups claims) в приложение Контакты

Для демонстрации создадим группу Admin. Администратор будет обладать правами на создание/редактирование/удаление.

О том, как создать группу и идентифицировать ее с помощью токена доступа, можно узнать в блоге Okta. В официальном блоге Okta описана автономная конфигурация приложения на Spring Boot в качестве веб-проекта Okta. В нашем случае менять тип проекта не нужно. Мы просто создадим группы и включим заявления о группах в токен доступа, выдаваемый сервером авторизации.

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

Управление группамиУправление группамиНазначение групп/пользователей для приложенияНазначение групп/пользователей для приложенияДобавление заявления о группах в токен доступа, выдаваемый сервером авторизацииДобавление заявления о группах в токен доступа, выдаваемый сервером авторизации

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

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

Для защиты клиентской части приложения Контакты воспользуемся SDK-библиотекой Okta для Angular. Мы будем авторизовывать пользователя, перенаправляя его на конечную точку авторизации, настроенную для нашей организации в Okta. Библиотеку можно установить с помощью npm:

npm install @okta/okta-angular --save

Чтобы реализовать этот промежуточный слой, после установки нужно будет указать значения из клиента OIDC, полученные на предыдущем этапе. Также понадобится URL-адрес организации в Okta его можно узнать на домашней странице консоли разработчика Okta.

В файле app.module.ts создадим объект конфигурации и в нем установим true для параметра PKCE.

const oktaConfig = {issuer: 'https://{yourOktaDomain}/oauth2/default',redirectUri: window.location.origin + '/implicit/callback',clientId: 'your clientId',pkce: true};

Этот объект нужно добавить в раздел providers. Также импортируем OktaAuthModule:

import { OktaAuthModule, OktaCallbackComponent } from '@okta/okta-angular';import {OKTA_CONFIG} from '@okta/okta-angular';....imports:[...OktaAuthModule...]providers: [...{ provide: OKTA_CONFIG, useValue: oktaConfig }...]

В модуле обработки маршрутов приложения зададим защиту маршрута, воспользовавшись Route Guard из Angular. Вот фрагмент кода из app-routing-module.ts:

import { OktaCallbackComponent } from '@okta/okta-angular';...const routes: Routes = [{ path: 'contact-list',canActivate: [ OktaAuthGuard ],component: ContactListComponent },{ path: 'contact',canActivate: [ OktaAuthGuard, AdminGuard ],component: EditContactComponent },{path: 'implicit/callback',component: OktaCallbackComponent},{ path: '',redirectTo: 'contact-list',pathMatch: 'full'},];

Здесь мы защищаем просмотр контактов с помощью AuthGuard, а создание, обновление, удаление контактов с помощью AdminGuard и OktaAuthGuard.

Метод canActivate в OktaAuthGuard используется для проверки того, прошел ли пользователь процедуру аутентификации. Если нет, он перенаправляется на страницу входа Okta; если да, мы позволяем ему просматривать страницу, на которую ведет маршрут.

async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {this.authenticated = await this.okta.isAuthenticated();if (this.authenticated) { return true; }// Redirect to login flow.this.oktaAuth.loginRedirect();return false;}

Аналогичным образом метод canActivate в AdminGuard проверяет, принадлежат ли вошедшие в систему пользователи к группе Admin; если нет, запрос на доступ к маршруту отклоняется и выводится предупреждение.

async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {this.user = await this.okta.getUser();const result = this.user.groups.find((group) => group === 'Admin');if (result) {return true;} else {alert('User is not authorized to perform this operation');return false; }}

Ниже показана последовательность операций, которые выполняются, когда пользователь пытается обратиться к маршруту /contact-list для просмотра списка контактов.

Последовательность операций при выводе контактов на экранПоследовательность операций при выводе контактов на экран

Ниже представлена последовательность операций, демонстрирующая принцип работы PKCE. В случае одностраничного приложения, если токен возвращается непосредственно как часть URL-адреса перенаправления, любое расширение браузера может получить доступ к токену еще до того, как его увидит приложение. Это представляет потенциальную угрозу безопасности. Для снижения этого риска в PKCE вычисляется хэш SHA256 от случайной строки верификатора кода (code verifier), и этот хэш включается в запрос кода доступа как контрольное значение (code challenge). Когда поступает запрос на обмен токена доступа на код доступа, сервер авторизации может снова вычислить хэш SHA256 от верификатора кода и сопоставить результат с сохраненным контрольным значением.

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

Неявное разрешение на доступ (implicit grant) с использованием PKCE в приложении КонтактыНеявное разрешение на доступ (implicit grant) с использованием PKCE в приложении Контакты

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

Для того чтобы приложение Spring Boot поддерживало Okta, нужно установить следующие зависимости:

<dependency><groupId>com.okta.spring</groupId><artifactId>okta-spring-boot-starter</artifactId><version>1.2.1</version><exclusions><exclusion><groupId>org.springframework.security.oauth</groupId><artifactId>spring-security-oauth2</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.security.oauth</groupId><artifactId>spring-security-oauth2</artifactId><version>2.4.0.RELEASE</version></dependency>

Spring Security OAuth2 и Okta Spring Boot Starter следует добавить в classpath, тогда Spring настроит поддержку Okta во время запуска.

Укажем следующие значения конфигурации для настройки и подключения Spring к приложению Okta:

okta.oauth2.client-id=<clientId>okta.oauth2.issuer=https://{yourOktaDomain}/oauth2/defaultokta.oauth2.groups-claim=groups

В классе SpringSecurity нужно указать, что приложение Spring Boot является сервером ресурсов:

@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)@Profile("dev")@Slf4jpublic class SecurityDevConfiguration extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception{http.cors().and().csrf().disable();http.cors().configurationSource(request -> new CorsConfiguration(corsConfiguratione()));// http.authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll()// .and().http.antMatcher("/**").authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll().antMatchers("/").permitAll().anyRequest().authenticated();http.oauth2ResourceServer();}

При такой конфигурации конечные точки нашего приложения защищены. Если попробовать обратиться к конечной точке /contacts по адресу http://localhost:8080/api/contacts, будет возвращена ошибка 401, поскольку запрос не пройдет через фильтр аутентификации.

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

@EnableGlobalMethodSecurity(prePostEnabled = true)

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

@PreAuthorize("hasAuthority('Admin')")public Contact saveContact(@RequestBody Contact contact,@AuthenticationPrincipal Principal userInfo) {

Для получения сведений о вошедшем в систему пользователе можно использовать объект Principal (@AuthenticationPrincipal).

Защита доступа к API из клиентской части приложения с помощью токена доступа

Теперь Okta защищает и клиентскую, и серверную часть нашего приложения. Когда клиентская часть направляет запрос к API, она должна передавать токен доступа в заголовке авторизации. В Angular для этого можно использовать перехватчик HttpInterceptor. В классе перехватчика авторизации можно задать токен доступа для любого HTTP-запроса следующим образом:

private async handleAccess(request: HttpRequest<any>, next: HttpHandler): Promise<HttpEvent<any>> {const accessToken = await this.oktaAuth.getAccessToken();if ( accessToken) {request = request.clone({setHeaders: {Authorization: 'Bearer ' + accessToken}});}return next.handle(request).toPromise();}}

Прямой доступ к API приложения через внешний сервер API

Если все настроено правильно, пользовательский интерфейс нашего приложения сможет получать доступ к API серверной части. В корпоративных приложениях часто присутствует несколько микросервисов, к которым обращается пользовательский интерфейс приложения, а также ряд микросервисов, доступ к которым открыт непосредственно для других приложений или пользователей. Так как наше приложение настроено как одностраничное, в настоящее время оно поддерживает доступ только из JavaScript-кода. Чтобы открыть доступ к микросервисам как к API для других потребителей API, нужно создать в Okta серверное приложение с типом веб-приложение, а затем создать микросервисы как одностраничные приложения и разрешить единый вход для доступа к ним (технически мы имеем дело с двумя разными приложениями в Okta).

Я разработал небольшое решение, позволяющее обойти эту проблему. Мы по-прежнему будем использовать неявный поток с PKCE, но на этот раз не станем перенаправлять пользователя на страницу входа (при направлении запроса к API войти в Okta в ручном режиме невозможно, поскольку в этом случае пользователем будет пользователь API). Вместо этого будем следовать следующему алгоритму.

Последовательность операций при прямом доступе к серверному APIПоследовательность операций при прямом доступе к серверному API

Я создал сервер API, тоесть микросервис-обертку, предоставляющий список API, к которым мы хотим открыть доступ как к серверным API.

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

Она выступает в роли прокси-сервера для основного серверного API нашего приложения и открывает доступ к необходимым сервисам. В ней используется библиотека Spring Cloud OpenFeign, предназначенная для написания декларативных REST-клиентов. Такая обертка представляет собой очень простой способ создания клиентов для веб-сервисов. Она позволяет сократить количество кода при написании потребителей веб-сервисов на базе REST, а также поддерживает перехватчики, позволяющие реализовать любые промежуточные операции, которые должны выполняться до направления запроса к API. В нашем случае мы будем использовать перехватчик для задания токена доступа и распространения прав доступа пользователей. Дополнительные сведения о Feign доступны по этой ссылке.

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

Kubernetes: развертывание приложения на Angular + Java/ Spring Boot в Google Kubernetes Engine (GKE)

Kubernetes: развертывание приложения на Angular + Spring Boot в облаке Microsoft Azure


Узнать подробнее о курсе Разработчик на Spring Framework

Смотреть вебинар Конфигурация Spring-приложений

Подробнее..

Перевод Пример развертывания Spring Boot-приложения в Kubernetes

10.11.2020 20:15:48 | Автор: admin

Перевод статьи подготовлен специально для студентов курса Разработчик на Spring Framework.


Давайте создадим простейшее Spring Boot-приложение, которое будет запускаться в кластере Kubernetes.




Структура проекта


 Dockerfile build.gradle gradle    wrapper        gradle-wrapper.jar        gradle-wrapper.properties gradlew k8s    depl.yaml settings.gradle src    main        java            hello                App.java                HelloWorldCtrl.java

App.java это точка входа в приложение:


package hello;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class App {    public static void main(String[] args) {        SpringApplication.run(App.class, args);    }}

Приведенный выше код представляет собой минимальное Spring Boot приложение.
Файл HelloWorldCtrl.java содержит простой контроллер, который мапит корень (/) на метод index, возвращающий строку приветствия:


package hello;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.bind.annotation.RequestMapping;@RestControllerpublic class HelloWorldCtrl {    @RequestMapping("/")    public String index() {        return "Greetings from Spring Boot!";    }}

Сборка приложения


Для сборки я использую Gradle. Файл build.gradle также минималистичен:


plugins {   id 'org.springframework.boot' version '2.3.3.RELEASE'   id 'io.spring.dependency-management' version '1.0.8.RELEASE'   id 'java'}group = 'com.test'version = '0.0.1-SNAPSHOT'sourceCompatibility = '1.8'repositories {   mavenCentral()}dependencies {   implementation 'org.springframework.boot:spring-boot-starter-web'}

Создаем ресурсы K8s


Для развертывания в K8s нам понадобится Docker-образ. Давайте добавим в Dockerfile следующие строки:


FROM gradle:jdk10COPY --chown=gradle:gradle . /appWORKDIR /appRUN gradle buildEXPOSE 8080WORKDIR /appCMD java -jar build/libs/gs-spring-boot-0.1.0.jar

Шаги в нашем Dockerfile:


  • копирование проекта в /app
  • сборка проекта с помощью Gradle
  • запуск приложения, используя результат предыдущего шага

docker build -t marounbassam/hello-spring .docker push marounbassam/hello-spring

Файл манифеста K8s тоже простой. Он состоит из развертывания (Deployment) и сервиса (Service):


apiVersion: extensions/v1beta1kind: Deploymentmetadata:  name: hello-worldspec:  replicas: 2  template:    metadata:      labels:        app: hello-world        visualize: "true"    spec:      containers:      - name: hello-world-pod        image: marounbassam/hello-spring        ports:        - containerPort: 8080---apiVersion: v1kind: Servicemetadata:  labels:    visualize: "true"  name: hello-world-servicespec:  selector:    app: hello-world  ports:  - name: http    protocol: TCP    port: 8080    targetPort: 8080  type: ClusterIP

Deployment определяет две реплики пода, в которых будет выполняться контейнер, созданный из образа, указанного в атрибуте image.


У сервиса (Service) тип ClusterIP (тип сервиса по умолчанию). Он предоставляет внутри кластера возможность подключаться к нам другим приложениям.


Создаем ресурсы в кластере:


kubectl create -f <yaml_file>

Визуально ресурсы можно представить следующим образом:



Внутри кластера


$ kubectl get podsNAME                         READY     STATUS    RESTARTS   AGEhello-world-5bb87c95-6h4kh   1/1       Running   0          7hhello-world-5bb87c95-bz64v   1/1       Running   0          7h$ kubectl get svcNAME                  TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGEhello-world-service   ClusterIP   10.15.242.210   <none>        8080/TCP   5skubernetes            ClusterIP   10.15.240.1     <none>        443/TCP    7h$ kubectl exec -it hello-world-5bb87c95-6h4kh bash$ (inside the pod) curl 10.15.242.210:8080$ (inside the pod) Greetings from Spring Boot!

Мы видим, что сервер запустился и работает внутри подов. Вы также можете настроить службу типа LoadBalancer (зависит от вашего облачного провайдера) и получить доступ к приложению из-за пределов кластера.


Заключение


Мы создали простое приложение на Spring Boot, запустили его в Docker-контейнере в поде K8s, который управляется через K8s deployment и доступен через сервис.


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


Разработчик на Spring Framework
Подробнее..

Сервисы с Apache Kafka и тестирование

09.01.2021 18:16:01 | Автор: admin

Когда сервисы интегрируются при помощи Kafka очень удобно использовать REST API, как универсальный и стандартный способ обмена сообщениями. При увеличении количества сервисов сложность коммуникаций увеличивается. Для контроля можно и нужно использовать интеграционное тестирование. Такие библиотеки как testcontainers или EmbeddedServer прекрасно помогают организовать такое тестирование. Существуют много примеров для micronaut, Spring Boot и т.д. Но в этих примерах опущены некоторые детали, которые не позволяют с первого раза запустить код. В статье приводятся примеры с подробным описанием и ссылками на код.


Пример


Для простоты можно принять такой REST API.


/runs POST-метод. Инициализирует запрос в канал связи. Принимает данные и возвращает ключ запроса.
/runs/{key}/status GET-метод. По ключу возвращает статус запроса. Может принимать следующие значения: UNKNOWN, RUNNING, DONE.
/runs /{key} GET-метод. По ключу возвращает результат запроса.


Подобный API реализован у livy, хотя и для других задач.


Реализация


Будут использоваться: micronaut, Spring Boot.


micronaut


Контроллер для API.


import io.micronaut.http.annotation.Body;import io.micronaut.http.annotation.Controller;import io.micronaut.http.annotation.Get;import io.micronaut.http.annotation.Post;import io.reactivex.Maybe;import io.reactivex.schedulers.Schedulers;import javax.inject.Inject;import java.util.UUID;@Controller("/runs")public class RunController {    @Inject    RunClient runClient;    @Inject    RunCache runCache;    @Post    public String runs(@Body String body) {        String key = UUID.randomUUID().toString();        runCache.statuses.put(key, RunStatus.RUNNING);        runCache.responses.put(key, "");        runClient.sendRun(key, new Run(key, RunType.REQUEST, "", body));        return key;    }    @Get("/{key}/status")    public Maybe<RunStatus> getRunStatus(String key) {        return Maybe.just(key)                .subscribeOn(Schedulers.io())                .map(it -> runCache.statuses.getOrDefault(it, RunStatus.UNKNOWN));    }    @Get("/{key}")    public Maybe<String> getRunResponse(String key) {        return Maybe.just(key)                .subscribeOn(Schedulers.io())                .map(it -> runCache.responses.getOrDefault(it, ""));    }}

Отправка сообщений в kafka.


import io.micronaut.configuration.kafka.annotation.*;import io.micronaut.messaging.annotation.Body;@KafkaClientpublic interface RunClient {    @Topic("runs")    void sendRun(@KafkaKey String key, @Body Run run);}

Получение сообщений из kafka.


import io.micronaut.configuration.kafka.annotation.*;import io.micronaut.messaging.annotation.Body;import javax.inject.Inject;@KafkaListener(offsetReset = OffsetReset.EARLIEST)public class RunListener {    @Inject    RunCalculator runCalculator;    @Topic("runs")    public void receive(@KafkaKey String key, @Body Run run) {        runCalculator.run(key, run);    }}

Обработка сообщений происходит в RunCalculator. Для тестов используется особая реализация, в которой происходит переброска сообщений.


import io.micronaut.context.annotation.Replaces;import javax.inject.Inject;import javax.inject.Singleton;import java.util.UUID;@Replaces(RunCalculatorImpl.class)@Singletonpublic class RunCalculatorWithWork implements RunCalculator {    @Inject    RunClient runClient;    @Inject    RunCache runCache;    @Override    public void run(String key, Run run) {        if (RunType.REQUEST.equals(run.getType())) {            String runKey = run.getKey();            String newKey = UUID.randomUUID().toString();            String runBody = run.getBody();            runClient.sendRun(newKey, new Run(newKey, RunType.RESPONSE, runKey, runBody + "_calculated"));        } else if (RunType.RESPONSE.equals(run.getType())) {            runCache.statuses.replace(run.getResponseKey(), RunStatus.DONE);            runCache.responses.replace(run.getResponseKey(), run.getBody());        }    }}

Тест.


import io.micronaut.http.HttpRequest;import io.micronaut.http.client.HttpClient;import static org.junit.jupiter.api.Assertions.assertEquals;public abstract class RunBase {    void run(HttpClient client) {        String key = client.toBlocking().retrieve(HttpRequest.POST("/runs", "body"));        RunStatus runStatus = RunStatus.UNKNOWN;        while (runStatus != RunStatus.DONE) {            runStatus = client.toBlocking().retrieve(HttpRequest.GET("/runs/" + key + "/status"), RunStatus.class);            try {                Thread.sleep(500);            } catch (InterruptedException e) {                e.printStackTrace();            }        }        String response = client.toBlocking().retrieve(HttpRequest.GET("/runs/" + key), String.class);        assertEquals("body_calculated", response);    }}

Для использования EmbeddedServer необходимо.


Подключить библиотеки:


testImplementation("org.apache.kafka:kafka-clients:2.6.0:test")testImplementation("org.apache.kafka:kafka_2.12:2.6.0")testImplementation("org.apache.kafka:kafka_2.12:2.6.0:test")

Тест может выглядеть так.


import io.micronaut.context.ApplicationContext;import io.micronaut.http.client.HttpClient;import io.micronaut.runtime.server.EmbeddedServer;import org.junit.jupiter.api.Test;import java.util.HashMap;import java.util.Map;public class RunKeTest extends RunBase {    @Test    void test() {        Map<String, Object> properties = new HashMap<>();        properties.put("kafka.bootstrap.servers", "localhost:9092");        properties.put("kafka.embedded.enabled", "true");        try (EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, properties)) {            ApplicationContext applicationContext = embeddedServer.getApplicationContext();            HttpClient client = applicationContext.createBean(HttpClient.class, embeddedServer.getURI());            run(client);        }    }}

Для использования testcontainers необходимо.


Подключить библиотеки:


implementation("org.testcontainers:kafka:1.14.3")

Тест может выглядеть так.


import io.micronaut.context.ApplicationContext;import io.micronaut.http.client.HttpClient;import io.micronaut.runtime.server.EmbeddedServer;import org.junit.jupiter.api.Test;import org.testcontainers.containers.KafkaContainer;import org.testcontainers.utility.DockerImageName;import java.util.HashMap;import java.util.Map;public class RunTcTest extends RunBase {    @Test    public void test() {        try (KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:5.5.3"))) {            kafka.start();            Map<String, Object> properties = new HashMap<>();            properties.put("kafka.bootstrap.servers", kafka.getBootstrapServers());            try (EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, properties)) {                ApplicationContext applicationContext = embeddedServer.getApplicationContext();                HttpClient client = applicationContext.createBean(HttpClient.class, embeddedServer.getURI());                run(client);            }        }    }}

Spring Boot


Контроллер для API.


import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import java.util.UUID;@RestController@RequestMapping("/runs")public class RunController {    @Autowired    private RunClient runClient;    @Autowired    private RunCache runCache;    @PostMapping()    public String runs(@RequestBody String body) {        String key = UUID.randomUUID().toString();        runCache.statuses.put(key, RunStatus.RUNNING);        runCache.responses.put(key, "");        runClient.sendRun(key, new Run(key, RunType.REQUEST, "", body));        return key;    }    @GetMapping("/{key}/status")    public RunStatus getRunStatus(@PathVariable String key) {        return runCache.statuses.getOrDefault(key, RunStatus.UNKNOWN);    }    @GetMapping("/{key}")    public String getRunResponse(@PathVariable String key) {        return runCache.responses.getOrDefault(key, "");    }}

Отправка сообщений в kafka.


import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.kafka.core.KafkaTemplate;import org.springframework.stereotype.Component;@Componentpublic class RunClient {    @Autowired    private KafkaTemplate<String, String> kafkaTemplate;    @Autowired    private ObjectMapper objectMapper;    public void sendRun(String key, Run run) {        String data = "";        try {            data = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(run);        } catch (JsonProcessingException e) {            e.printStackTrace();        }        kafkaTemplate.send("runs", key, data);    }}

Получение сообщений из kafka.


import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import org.apache.kafka.clients.consumer.ConsumerRecord;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.kafka.annotation.KafkaListener;import org.springframework.stereotype.Component;@Componentpublic class RunListener {    @Autowired    private ObjectMapper objectMapper;    @Autowired    private RunCalculator runCalculator;    @KafkaListener(topics = "runs", groupId = "m-group")    public void receive(ConsumerRecord<?, ?> consumerRecord) {        String key = consumerRecord.key().toString();        Run run = null;        try {            run = objectMapper.readValue(consumerRecord.value().toString(), Run.class);        } catch (JsonProcessingException e) {            e.printStackTrace();        }        runCalculator.run(key, run);    }}

Обработка сообщений происходит в RunCalculator. Для тестов используется особая реализация, в которой происходит переброска сообщений.


import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.util.UUID;@Componentpublic class RunCalculatorWithWork implements RunCalculator {    @Autowired    RunClient runClient;    @Autowired    RunCache runCache;    @Override    public void run(String key, Run run) {        if (RunType.REQUEST.equals(run.getType())) {            String runKey = run.getKey();            String newKey = UUID.randomUUID().toString();            String runBody = run.getBody();            runClient.sendRun(newKey, new Run(newKey, RunType.RESPONSE, runKey, runBody + "_calculated"));        } else if (RunType.RESPONSE.equals(run.getType())) {            runCache.statuses.replace(run.getResponseKey(), RunStatus.DONE);            runCache.responses.replace(run.getResponseKey(), run.getBody());        }    }}

Тест.


import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.http.MediaType;import org.springframework.test.web.servlet.MockMvc;import org.springframework.test.web.servlet.MvcResult;import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;import static org.junit.jupiter.api.Assertions.assertEquals;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;public abstract class RunBase {    void run(MockMvc mockMvc, ObjectMapper objectMapper) throws Exception {        MvcResult keyResult = mockMvc.perform(MockMvcRequestBuilders.post("/runs")                .content("body")                .contentType(MediaType.APPLICATION_JSON)                .accept(MediaType.APPLICATION_JSON))                .andExpect(status().isOk())                .andReturn();        String key = keyResult.getResponse().getContentAsString();        RunStatus runStatus = RunStatus.UNKNOWN;        while (runStatus != RunStatus.DONE) {            MvcResult statusResult = mockMvc.perform(MockMvcRequestBuilders.get("/runs/" + key + "/status")                    .contentType(MediaType.APPLICATION_JSON)                    .accept(MediaType.APPLICATION_JSON))                    .andExpect(status().isOk())                    .andReturn();            runStatus = objectMapper.readValue(statusResult.getResponse().getContentAsString(), RunStatus.class);            try {                Thread.sleep(500);            } catch (InterruptedException e) {                e.printStackTrace();            }        }        String response = mockMvc.perform(MockMvcRequestBuilders.get("/runs/" + key)                .contentType(MediaType.APPLICATION_JSON)                .accept(MediaType.APPLICATION_JSON))                .andExpect(status().isOk())                .andReturn().getResponse().getContentAsString();        assertEquals("body_calculated", response);    }}

Для использования EmbeddedServer необходимо.


Подключить библиотеки:


<dependency>    <groupId>org.springframework.kafka</groupId>    <artifactId>spring-kafka</artifactId>    <version>2.5.10.RELEASE</version></dependency><dependency>    <groupId>org.springframework.kafka</groupId>    <artifactId>spring-kafka-test</artifactId>    <version>2.5.10.RELEASE</version>    <scope>test</scope></dependency>

Тест может выглядеть так.


import com.fasterxml.jackson.databind.ObjectMapper;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.boot.test.context.TestConfiguration;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Import;import org.springframework.kafka.test.context.EmbeddedKafka;import org.springframework.test.web.servlet.MockMvc;@AutoConfigureMockMvc@SpringBootTest@EmbeddedKafka(partitions = 1, brokerProperties = {"listeners=PLAINTEXT://localhost:9092", "port=9092"})@Import(RunKeTest.RunKeTestConfiguration.class)public class RunKeTest extends RunBase {    @Autowired    private MockMvc mockMvc;    @Autowired    private ObjectMapper objectMapper;    @Test    void test() throws Exception {        run(mockMvc, objectMapper);    }    @TestConfiguration    static class RunKeTestConfiguration {        @Autowired        private RunCache runCache;        @Autowired        private RunClient runClient;        @Bean        public RunCalculator runCalculator() {            RunCalculatorWithWork runCalculatorWithWork = new RunCalculatorWithWork();            runCalculatorWithWork.runCache = runCache;            runCalculatorWithWork.runClient = runClient;            return runCalculatorWithWork;        }    }}

Для использования testcontainers необходимо.


Подключить библиотеки:


<dependency>    <groupId>org.testcontainers</groupId>    <artifactId>kafka</artifactId>    <version>1.14.3</version>    <scope>test</scope></dependency>

Тест может выглядеть так.


import com.fasterxml.jackson.databind.ObjectMapper;import org.apache.kafka.clients.consumer.ConsumerConfig;import org.apache.kafka.clients.producer.ProducerConfig;import org.apache.kafka.common.serialization.StringDeserializer;import org.apache.kafka.common.serialization.StringSerializer;import org.junit.ClassRule;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.boot.test.context.TestConfiguration;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Import;import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;import org.springframework.kafka.core.*;import org.springframework.test.web.servlet.MockMvc;import org.testcontainers.containers.KafkaContainer;import org.testcontainers.utility.DockerImageName;import java.util.HashMap;import java.util.Map;@AutoConfigureMockMvc@SpringBootTest@Import(RunTcTest.RunTcTestConfiguration.class)public class RunTcTest extends RunBase {    @ClassRule    public static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:5.5.3"));    static {        kafka.start();    }    @Autowired    private MockMvc mockMvc;    @Autowired    private ObjectMapper objectMapper;    @Test    void test() throws Exception {        run(mockMvc, objectMapper);    }    @TestConfiguration    static class RunTcTestConfiguration {        @Autowired        private RunCache runCache;        @Autowired        private RunClient runClient;        @Bean        ConcurrentKafkaListenerContainerFactory<Integer, String> kafkaListenerContainerFactory() {            ConcurrentKafkaListenerContainerFactory<Integer, String> factory = new ConcurrentKafkaListenerContainerFactory<>();            factory.setConsumerFactory(consumerFactory());            return factory;        }        @Bean        public ConsumerFactory<Integer, String> consumerFactory() {            return new DefaultKafkaConsumerFactory<>(consumerConfigs());        }        @Bean        public Map<String, Object> consumerConfigs() {            Map<String, Object> props = new HashMap<>();            props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());            props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");            props.put(ConsumerConfig.GROUP_ID_CONFIG, "m-group");            props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);            props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);            return props;        }        @Bean        public ProducerFactory<String, String> producerFactory() {            Map<String, Object> configProps = new HashMap<>();            configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());            configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);            configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);            return new DefaultKafkaProducerFactory<>(configProps);        }        @Bean        public KafkaTemplate<String, String> kafkaTemplate() {            return new KafkaTemplate<>(producerFactory());        }        @Bean        public RunCalculator runCalculator() {            RunCalculatorWithWork runCalculatorWithWork = new RunCalculatorWithWork();            runCalculatorWithWork.runCache = runCache;            runCalculatorWithWork.runClient = runClient;            return runCalculatorWithWork;        }    }}

Перед всеми тестами необходимо стартовать kafka. Это делается вот таким вот образом:


kafka.start();

Дополнительные свойства для kafka в тестах можно задать в ресурсном файле.


application.yml

spring:  kafka:    consumer:      auto-offset-reset: earliest

Ресурсы и ссылки


Код для micronaut


Код для Spring Boot


PART 1: TESTING KAFKA MICROSERVICES WITH MICRONAUT


Testing Kafka and Spring Boot


Micronaut Kafka


Spring for Apache Kafka

Подробнее..

Перевод Асинхронное выполнение задач с использованием Redis и Spring Boot

02.01.2021 16:06:38 | Автор: admin

В этой статье мы рассмотрим, как использовать Spring Boot 2.x и Redis для выполнения асинхронных задач, а полный код продемонстрирует шаги, описанные в этом посте.

Spring/Spring Boot

Spring самый популярный фреймворк для разработки Java приложений.Таким образом, Spring имеет одно из крупнейших сообществ с открытым исходным кодом.Кроме того, Spring предоставляет обширную и актуальную документацию, которая охватывает внутреннюю работу фреймворка и примеры проектов в своем блоге, а наStackOverflowболее 100 тысяч вопросови ответов.

Вначале Spring поддерживал только конфигурацию на основе XML, и из-за этого был подвержен множеству критических замечаний.Позже Spring представила конфигурацию на основе аннотаций, которая изменила все.Spring 3.0 была первой версией, которая поддерживала конфигурацию на основе аннотаций.В 2014 годубыла выпущенаSpring Boot1.0, полностью изменившая наш взгляд на экосистему фреймворка Spring.Более подробное описание истории Spring можно найтиздесь.

Redis

Redis одна из самых популярных NoSQL баз данных в памяти.Redis поддерживает разные типы структур данных. Redis поддерживает различные типы структур данных, например Set, Hash table, List, простую пару ключ-значение это лишь некоторые из них.Задержка вызова Redis составляет менее миллисекунд, поддержка набора реплик и т. д. Задержка операции Redis составляет менее миллисекунд, что делает ее еще более привлекательной для сообщества разработчиков.

Почему асинхронное выполнение задачи

Типичный вызов API состоит из пяти этапов:

  1. Выполние одного или нескольких запросов к базе данных (RDBMS / NoSQL)

  2. Одна или несколько операций системы кэширования (In-Memory, Distributed и т. д.)

  3. Некоторые вычисления (это может быть обработка данных при выполнении некоторых математических операций)

  4. Вызов других служб (внутренних / внешних)

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

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

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

Отложенная очередь

Каждый раз, когда мы планируем запуск задачи в определенное время или через определенный интервал, мы используем задания cron, которые запланированы на определенное время или интервал.Мы можем запускать задачи по расписанию, используя различные инструменты, такие как crontab в стиле UNIX,Chronos, если мы используем фреймворки Spring, тогда речь идетоб аннотацииScheduled.

Большинство заданий cron просматривают записи о том, когда должно быть предпринято определенное действие, например, поиск всех поставок по истечении семи дней, по которым не были созданы счета.Большинство таких механизмов планирования страдаютпроблемами масштабирования, когда мы сканируем базы данных, чтобы найти соответствующие строки/записи.Во многих случаях это приводит кполному сканированию таблицы,которое работает очень медленно.Представьте себе случай, когда одна и та же база данных используется приложением реального времени и этой системой пакетной обработки.Поскольку она не является масштабируемый, нам понадобится какая-то масштабируемая система, которая может выполнять задачи в заданное время или интервал без каких-либо проблем с производительностью.Есть много способов масштабирования таким образом, например, запускать задачи в пакетном режиме или управлять задачами для определенного подмножества пользователей/регионов.Другой способ запустить конкретную задачу в определенное время без зависимости от других задач, например безсерверной функции.Отложенная очередьможет использоваться в тех случаях, как только таймер достигнет запланированного времени работа будет вызвана. Имеется множествосистем/программного обеспечения для организации очередей, но очень немногие из них, например SQS, предоставляют функцию, которая обеспечивает задержку на 15 минут, а не произвольную задержку, такую как 7 часов или 7 дней и т. д.

Rqueue

Rqueue это брокер сообщений, созданный для платформыSpring,который хранит данные в Redis и предоставляет механизм для выполнения задачи с любой указанной задержкой.Rqueue поддерживается Redis, поскольку Redis имеет некоторые преимущества перед широко используемыми системами очередей, такими как Kafka, SQS.В большинстве серверных приложений веб-приложений Redis используется для хранения данных кеша или для других целей.В настоящее время8,4% веб-приложений используют базу данных Redis.

Как правило, для очереди мы используем либо Kafka/SQS, либо некоторые другие системы, эти системы приносят дополнительные накладные расходы в разных измерениях, например, финансовые затраты, которые можно уменьшить до нуля с помощью Rqueue и Redis.

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

Доставка сообщений

Rqueue гарантирует доставку сообщения хотя бы раз, так как длинные данные не теряются в базе данных.Подробнее об этом читайте настранице Введение в Rqueue.

Инструменты, которые нам понадобятся:

  1. Любая IDE

  2. Gradle

  3. Java

  4. Redis

Мы собираемся использоватьSpring Boot для простоты.Мы создадим проект Gradle с помощью инициализатора Spring Boot по адресуhttps://start.spring.io/.

Из зависимостей нам понадобятся:

  1. Spring Data Redis

  2. Spring Web

  3. Lombok и некоторые другие

Структура каталогов/папок показана ниже:

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

Мы добавим зависимость spring boot starter дляRqueue com.github.sonus21:rqueue-spring-boot-starter:2.0.0-RELEASE с помощью кода:

dependencies {    implementation 'org.springframework.boot:spring-boot-starter-data-redis'  implementation 'org.springframework.boot:spring-boot-starter-web'  implementation 'com.github.sonus21:rqueue-spring-boot-starter:2.0.0-RELEASE'  compileOnly 'org.projectlombok:lombok'     annotationProcessor 'org.projectlombok:lombok'  providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'  testImplementation('org.springframework.boot:spring-boot-starter-test') {    exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'    }}

Нам нужно включить функции Redis Spring Boot.В целях тестирования мы также включим WEB MVC.

Обновите файл application как:

@SpringBootApplication@EnableRedisRepositories@EnableWebMvcpublic class AsynchronousTaskExecutorApplication {   public static void main(String[] args) {     SpringApplication.run(AsynchronousTaskExecutorApplication.class, args);  }}

Добавлять задачи с помощью Rqueue очень просто.Нам нужно аннотировать метод с помощьюRqueueListenerRqueuListenerаннотации есть несколько полей, которые можно настроить в зависимости от варианта использования.УстановитеdeadLetterQueueдля отправки задач в другую очередь.В противном случае задача будет отброшена в случае неудачи.Мы также можем установить, сколько раз задача должна быть повторена, используяполе.numRetries

Создайте файл Java с именемMessageListenerи добавьте несколько методов для выполнения задач:

@Component@Slf4jpublic class MessageListener {  @RqueueListener(value = "${email.queue.name}") (1)  public void sendEmail(Email email) {    log.info("Email {}", email);  }  @RqueueListener(value = "${invoice.queue.name}") (2)  public void generateInvoice(Invoice invoice) {    log.info("Invoice {}", invoice);  }}

Нам понадобится классы Email и Invoiceдля хранения данных электронной почты и счетов-фактур соответственно.Для простоты у классов будет только небольшое количество полей.

Invoice.java:

import lombok.Data;@Data@AllArgsConstructor@NoArgsConstructorpublic class Invoice {  private String id;  private String type;}

Email.java:

import lombok.Data;@Data@AllArgsConstructor@NoArgsConstructorpublic class Email {  private String email;  private String subject;  private String content;}

Отправка задач в очередь

Задачу можно отправить в очередь с помощьюRqueueMessageSenderbean-компонента. У которого есть несколько методов для постановки задачи в очередь в зависимости от сценария использования, используйте один из доступных методов.Для простых задач используйте enqueue, для отложенных задач используйте enqueueIn.

Нам нужно автоматически подключитьRqueueMessageSenderили использовать внедрение на основе конструктора для внедрения этих bean-компонентов.

Воткак создать контроллер для тестирования.

Мы планируем создать счет-фактуру, который нужно будет выполнить через 30 секунд.Для этого мы отправим задачу с задержкой 30000 (миллисекунд) в очереди счетов.Кроме того, мы постараемся отправить электронное письмо, которое может выполняться в фоновом режиме.Для этого мы добавим два метода GET,sendEmailи generateInvoice, мы также можем использовать POST.

@RestController@RequiredArgsConstructor(onConstructor = @__(@Autowired))@Slf4jpublic class Controller {  private @NonNull RqueueMessageSender rqueueMessageSender;  @Value("${email.queue.name}")  private String emailQueueName;  @Value("${invoice.queue.name}")  private String invoiceQueueName;  @Value("${invoice.queue.delay}")  private Long invoiceDelay;  @GetMapping("email")  public String sendEmail(      @RequestParam String email, @RequestParam String subject, @RequestParam String content) {    log.info("Sending email");    rqueueMessageSender.enqueu(emailQueueName, new Email(email, subject, content));    return "Please check your inbox!";  }  @GetMapping("invoice")  public String generateInvoice(@RequestParam String id, @RequestParam String type) {    log.info("Generate invoice");    rqueueMessageSender.enqueueIn(invoiceQueueName, new Invoice(id, type), invoiceDelay);    return "Invoice would be generated in " + invoiceDelay + " milliseconds";  }}

Добавим в файл application.properties следующие строки:

email.queue.name=email-queueinvoice.queue.name=invoice-queue# 30 seconds delay for invoiceinvoice.queue.delay=300000

Теперь мы можем запустить приложение.После успешного запуска приложения вы можете просмотреть результат по этойссылке.

В журнале мы видим, что задача электронной почты выполняется в фоновом режиме:

Ниже приведено расписание выставления счетов через 30 секунд:

http://localhost:8080/invoice?id=INV-1234&type=PROFORMA

Заключение

Теперь мы можем планировать задачи с помощью Rqueue без большого объёма вспомогательного кода!Были приведены основные соображения по настройке и использованию библиотеки Rqueue.Следует иметь в виду одну важную вещь: независимо от того, является ли задача отложенной задачей или нет, по умолчанию предполагается, что задачи необходимо выполнить как можно скорее.

Полный код этого поста можно найти в репозиториинаGitHub.

Дополнительноечтение

Spring Boot: Creating Asynchronous Methods Using @Async Annotation

Spring and Threads: Async

Distributed Tasks Execution and Scheduling in Java, Powered by Redis

Подробнее..
Категории: Redis , Java , Spring , Spring boot , Asynchronous task

Перевод Реализация мультиарендности с использованием Spring Boot, MongoDB и Redis

28.02.2021 18:05:07 | Автор: admin

В этом руководстве мы рассмотрим, как реализовать мультиарендность в Spring Boot приложении с использованием MongoDB и Redis.

Используются:

  • Spring Boot 2.4

  • Maven 3.6. +

  • JAVA 8+

  • Монго 4.4

  • Redis 5

Что такое мультиарендность?

Мультиарендность (англ. multitenancy множественная аренда) это программная архитектура, в которой один экземпляр программного приложения обслуживает нескольких клиентов.Все должно быть общим, за исключением данных разных клиентов, которые должны быть должным образом разделены.Несмотря на то, что они совместно используют ресурсы, арендаторы не знают друг друга, и их данные хранятся совершенно отдельно.Каждый покупатель называется арендатором.

Предложение программное обеспечение как услуга (SaaS) является примером мультиарендной архитектуры.Более подробно.

Модели с несколькими арендаторами

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

  1. База данных для каждого арендатора: каждый арендатор имеет свою собственную базу данных и изолирован от других арендаторов.

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

  3. Общая база данных, отдельная схема: все арендаторы совместно используют базу данных, но имеют свои собственные схемы и таблицы базы данных.

Начнем

В этом руководстве мы реализуем мультиарендность на основе базы данных для каждого клиента.

Мы начнем с создания простого проекта Spring Boot наstart.spring.ioсо следующими зависимостями:

<dependencies>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-data-mongodb</artifactId>    </dependency>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>    </dependency>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-data-redis</artifactId>    </dependency>    <dependency>        <groupId>redis.clients</groupId>        <artifactId>jedis</artifactId>    </dependency>    <dependency>        <groupId>org.projectlombok</groupId>        <artifactId>lombok</artifactId>        <optional>true</optional>    </dependency></dependencies>

Определение текущего идентификатора клиента

Идентификатор клиента необходимо определить для каждого клиентского запроса.Для этого мы включим поле идентификатора клиента в заголовок HTTP-запроса.

Давайте добавим перехватчик, который получает идентификатор клиента из http заголовкаX-Tenant.

@Slf4j@Componentpublic class TenantInterceptor implements WebRequestInterceptor {    private static final String TENANT_HEADER = "X-Tenant";    @Override    public void preHandle(WebRequest request) {        String tenantId = request.getHeader(TENANT_HEADER);        if (tenantId != null && !tenantId.isEmpty()) {            TenantContext.setTenantId(tenantId);            log.info("Tenant header get: {}", tenantId);        } else {            log.error("Tenant header not found.");            throw new TenantAliasNotFoundException("Tenant header not found.");        }    }    @Override    public void postHandle(WebRequest webRequest, ModelMap modelMap) {        TenantContext.clear();    }    @Override    public void afterCompletion(WebRequest webRequest, Exception e) {    }}

TenantContextэто хранилище, содержащее переменную ThreadLocal.ThreadLocal можно рассматривать как область доступа (scope of access), такую как область запроса (request scope) или область сеанса (session scope).

Сохраняя tenantId в ThreadLocal, мы можем быть уверены, что каждый поток имеет свою собственную копию этой переменной и что текущий поток не имеет доступа к другому tenantId:

@Slf4jpublic class TenantContext {    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();    public static void setTenantId(String tenantId) {        log.debug("Setting tenantId to " + tenantId);        CONTEXT.set(tenantId);    }    public static String getTenantId() {        return CONTEXT.get();    }    public static void clear() {        CONTEXT.remove();    }}

Настройка источников данных клиента (Tenant Datasources)

В нашей архитектуре у нас есть экземпляр Redis, представляющий базу метаданных (master database), в которой централизована вся информация о базе данных клиента.Таким образом, из каждого предоставленного идентификатора клиента информация о подключении к базе данных извлекается из базы метаданных .

RedisDatasourceService.java это класс, отвечающий за управление всеми взаимодействиями с базой метаданных .

@Servicepublic class RedisDatasourceService {    private final RedisTemplate redisTemplate;    private final ApplicationProperties applicationProperties;    private final DataSourceProperties dataSourceProperties;public RedisDatasourceService(RedisTemplate redisTemplate, ApplicationProperties applicationProperties, DataSourceProperties dataSourceProperties) {        this.redisTemplate = redisTemplate;        this.applicationProperties = applicationProperties;        this.dataSourceProperties = dataSourceProperties;    }        /**     * Save tenant datasource infos     *     * @param tenantDatasource data of datasource     * @return status if true save successfully , false error     */        public boolean save(TenantDatasource tenantDatasource) {        try {            Map ruleHash = new ObjectMapper().convertValue(tenantDatasource, Map.class);            redisTemplate.opsForHash().put(applicationProperties.getServiceKey(), String.format("%s_%s", applicationProperties.getTenantKey(), tenantDatasource.getAlias()), ruleHash);            return true;        } catch (Exception e) {            return false;        }    }        /**     * Get all of keys     *     * @return list of datasource     */         public List findAll() {        return redisTemplate.opsForHash().values(applicationProperties.getServiceKey());    }        /**     * Get datasource     *     * @return map key and datasource infos     */         public Map<String, TenantDatasource> loadServiceDatasources() {        List<Map<String, Object>> datasourceConfigList = findAll();        // Save datasource credentials first time        // In production mode, this part can be skip        if (datasourceConfigList.isEmpty()) {            List<DataSourceProperties.Tenant> tenants = dataSourceProperties.getDatasources();            tenants.forEach(d -> {                TenantDatasource tenant = TenantDatasource.builder()                        .alias(d.getAlias())                        .database(d.getDatabase())                        .host(d.getHost())                        .port(d.getPort())                        .username(d.getUsername())                        .password(d.getPassword())                        .build();                save(tenant);            });        }        return getDataSourceHashMap();    }        /**     * Get all tenant alias     *     * @return list of alias     */         public List<String> getTenantsAlias() {        // get list all datasource for this microservice        List<Map<String, Object>> datasourceConfigList = findAll();        return datasourceConfigList.stream().map(data -> (String) data.get("alias")).collect(Collectors.toList());    }        /**     * Fill the data sources list.     *     * @return Map<String, TenantDatasource>     */         private Map<String, TenantDatasource> getDataSourceHashMap() {        Map<String, TenantDatasource> datasourceMap = new HashMap<>();        // get list all datasource for this microservice        List<Map<String, Object>> datasourceConfigList = findAll();        datasourceConfigList.forEach(data -> datasourceMap.put(String.format("%s_%s", applicationProperties.getTenantKey(), (String) data.get("alias")), new TenantDatasource((String) data.get("alias"), (String) data.get("host"), (int) data.get("port"), (String) data.get("database"), (String) data.get("username"), (String) data.get("password"))));        return datasourceMap;    }}

В этом руководстве мы заполнили информацию о клиенте из yml-файла (tenants.yml).В производственном режиме можно создать конечные точки для сохранения информации о клиенте в базе метаданных.

Чтобы иметь возможность динамически переключаться на подключение к базе данных mongo, мы создаемкласс MultiTenantMongoDBFactory, расширяющий класс SimpleMongoClientDatabaseFactory из org.springframework.data.mongodb.core.Он вернетэкземплярMongoDatabase, связанный с текущим арендатором.

@Configurationpublic class MultiTenantMongoDBFactory extends SimpleMongoClientDatabaseFactory {@Autowired    MongoDataSources mongoDataSources;public MultiTenantMongoDBFactory(@Qualifier("getMongoClient") MongoClient mongoClient, String databaseName) {        super(mongoClient, databaseName);    }    @Override    protected MongoDatabase doGetMongoDatabase(String dbName) {        return mongoDataSources.mongoDatabaseCurrentTenantResolver();    }}

Нам нужно инициализироватьконструктор MongoDBFactoryMultiTenantс параметрами по умолчанию (MongoClientиdatabaseName).

Это реализует прозрачный механизм для получения текущего клиента.

@Component@Slf4jpublic class MongoDataSources {    /**     * Key: String tenant alias     * Value: TenantDatasource     */    private Map<String, TenantDatasource> tenantClients;    private final ApplicationProperties applicationProperties;    private final RedisDatasourceService redisDatasourceService;    public MongoDataSources(ApplicationProperties applicationProperties, RedisDatasourceService redisDatasourceService) {        this.applicationProperties = applicationProperties;        this.redisDatasourceService = redisDatasourceService;    }    /**     * Initialize all mongo datasource     */    @PostConstruct    @Lazy    public void initTenant() {        tenantClients = new HashMap<>();        tenantClients = redisDatasourceService.loadServiceDatasources();    }    /**     * Default Database name for spring initialization. It is used to be injected into the constructor of MultiTenantMongoDBFactory.     *     * @return String of default database.     */    @Bean    public String databaseName() {        return applicationProperties.getDatasourceDefault().getDatabase();    }    /**     * Default Mongo Connection for spring initialization.     * It is used to be injected into the constructor of MultiTenantMongoDBFactory.     */    @Bean    public MongoClient getMongoClient() {        MongoCredential credential = MongoCredential.createCredential(applicationProperties.getDatasourceDefault().getUsername(), applicationProperties.getDatasourceDefault().getDatabase(), applicationProperties.getDatasourceDefault().getPassword().toCharArray());        return MongoClients.create(MongoClientSettings.builder()                .applyToClusterSettings(builder ->                        builder.hosts(Collections.singletonList(new ServerAddress(applicationProperties.getDatasourceDefault().getHost(), Integer.parseInt(applicationProperties.getDatasourceDefault().getPort())))))                .credential(credential)                .build());    }    /**     * This will get called for each DB operations     *     * @return MongoDatabase     */    public MongoDatabase mongoDatabaseCurrentTenantResolver() {        try {            final String tenantId = TenantContext.getTenantId();            // Compose tenant alias. (tenantAlias = key + tenantId)            String tenantAlias = String.format("%s_%s", applicationProperties.getTenantKey(), tenantId);            return tenantClients.get(tenantAlias).getClient().                    getDatabase(tenantClients.get(tenantAlias).getDatabase());        } catch (NullPointerException exception) {            throw new TenantAliasNotFoundException("Tenant Datasource alias not found.");        }    }73}

Тест

Давайте создадим CRUD пример с документом Employee.

@Builder@Data@AllArgsConstructor@NoArgsConstructor@Accessors(chain = true)@Document(collection = "employee")public class Employee  {    @Id    private String id;    private String firstName;    private String lastName;    private String email;}

Также нам нужно создать классы EmployeeRepository, EmployeeServiceиEmployeeController.Для тестирования при запуске приложения мы загружаем фиктивные данные в каждую базу данных клиента.

@Overridepublic void run(String... args) throws Exception {    List<String> aliasList = redisDatasourceService.getTenantsAlias();    if (!aliasList.isEmpty()) {        //perform actions for each tenant        aliasList.forEach(alias -> {            TenantContext.setTenantId(alias);            employeeRepository.deleteAll();            Employee employee = Employee.builder()                    .firstName(alias)                    .lastName(alias)                    .email(String.format("%s%s", alias, "@localhost.com" ))                    .build();            employeeRepository.save(employee);            TenantContext.clear();        });    }}

Теперь мы можем запустить наше приложение и протестировать его.

Итак, мы все сделали. Надеюсь, это руководство поможет вам понять, что такое мультиарендность и как она может быть реализована в Spring Boot проекте с использованием MongoDB и Redis.

Полный исходный код примера можно найти наGitHub.

Подробнее..

Перевод Запись событий Spring при тестировании приложений Spring Boot

24.02.2021 18:11:35 | Автор: admin

Одна из основных функций Spring - функция публикации событий.Мы можем использовать события для разделения частей нашего приложения и реализации шаблона публикации-подписки.Одна часть нашего приложения может публиковать событие, на которое реагируют несколько слушателей (даже асинхронно).В рамкахSpring Framework 5.3.3(Spring Boot 2.4.2) теперь мы можем записывать и проверять все опубликованные события (ApplicationEvent) при тестировании приложений Spring Boot с использованием@RecrodApplicationEvents.

Настройка для записи ApplicationEvent с помощью Spring Boot

Чтобы использовать эту функцию, нам нужен толькоSpring Boot Starter Test,который является частью каждого проекта Spring Boot, который вы загружаете наstart.spring.io.

<dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-test</artifactId>  <scope>test</scope></dependency>

Обязательно используйте версию Spring Boot >= 2.4.2, так как нам нужна версия Spring Framework >= 5.3.3.

Для наших тестов есть одно дополнительное требование: нам нужно работать со SpringTestContextпоскольку публикация событий является основной функциональностью платформыApplicationContext.

Следовательно, она не работает для модульного теста, где неиспользуется поддержка инфраструктуры Spring TestContext.Есть несколькоаннотаций тестовых срезов Spring Boot,которые удобно загружают контекст для нашего теста.

Введение в публикацию событий Spring

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

public class UserCreationEvent extends ApplicationEvent {   private final String username;  private final Long id;   public UserCreationEvent(Object source, String username, Long id) {    super(source);    this.username = username;    this.id = id;  }   // getters}

Начиная со Spring Framework 4.2, нам не нужно расширять абстрактныйкласс ApplicationEventи мы можем использовать любой POJO в качестве нашего класса событий.В следующий статье привеленоотличное введение в события приложенийс помощью Spring Boot.

НашUserServiceсоздает и хранит наших новых пользователей.Мы можем создать как одного пользователя, так и группу пользователей:

@Servicepublic class UserService {   private final ApplicationEventPublisher eventPublisher;   public UserService(ApplicationEventPublisher eventPublisher) {    this.eventPublisher = eventPublisher;  }   public Long createUser(String username) {    // logic to create a user and store it in a database    Long primaryKey = ThreadLocalRandom.current().nextLong(1, 1000);     this.eventPublisher.publishEvent(new UserCreationEvent(this, username, primaryKey));     return primaryKey;  }   public List<Long> createUser(List<String> usernames) {    List<Long> resultIds = new ArrayList<>();     for (String username : usernames) {      resultIds.add(createUser(username));    }     return resultIds;  }}

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

Например, наше приложение выполняет две дополнительные операции всякий раз, когда мы запускаем такоеUserCreationEvent:

@Componentpublic class ReportingListener {   @EventListener(UserCreationEvent.class)  public void reportUserCreation(UserCreationEvent event) {    // e.g. increment a counter to report the total amount of new users    System.out.println("Increment counter as new user was created: " + event);  }   @EventListener(UserCreationEvent.class)  public void syncUserToExternalSystem(UserCreationEvent event) {    // e.g. send a message to a messaging queue to inform other systems    System.out.println("informing other systems about new user: " + event);  }}

Запись и проверка событий приложения с помощью Spring Boot

Давайте напишем наш первый тест, который проверяет,UserServiceгенерирует событие всякий раз, когда мы создаем нового пользователя.Мы инструктируем Spring фиксировать наши события с помощью@RecordApplicationEventsаннотации поверх нашего тестового класса:

@SpringBootTest@RecordApplicationEventsclass UserServiceFullContextTest {   @Autowired  private ApplicationEvents applicationEvents;   @Autowired  private UserService userService;   @Test  void userCreationShouldPublishEvent() {     this.userService.createUser("duke");     assertEquals(1, applicationEvents      .stream(UserCreationEvent.class)      .filter(event -> event.getUsername().equals("duke"))      .count());     // There are multiple events recorded    // PrepareInstanceEvent    // BeforeTestMethodEvent    // BeforeTestExecutionEvent    // UserCreationEvent    applicationEvents.stream().forEach(System.out::println);  }}

После того, какмы выполняем публичный метод нашего класса испытываемый (createUserизUserServiceв этом примере), мы можем запросить все захваченные события из биновApplicationEvents,которые мы внедряем в наш тест.

Открытый.stream()методкласса ApplicationEventsпозволяет просмотреть все события, записанные для теста.Есть перегруженная версия .stream(),вкоторой мы запрашиваем поток только определенных событий.

Несмотря на то, что мы генерируем только одно событие из нашего приложения, Spring захватывает четыре события для теста выше.Остальные три события относятся к Spring, как иPrepareInstanceEventв среде TestContext.

Поскольку мы используем JUnit Jupiter иSpringExtension(зарегистрированный для нас при использовании@SpringBootTest), мы также можем внедритьbean-компонент ApplicationEventsв метод жизненного цикла JUnit или непосредственно в тест:

@Testvoid batchUserCreationShouldPublishEvents(@Autowired ApplicationEvents events) {  List<Long> result = this.userService.createUser(List.of("duke", "mike", "alice"));   assertEquals(3, result.size());  assertEquals(3, events.stream(UserCreationEvent.class).count());}

ЭкземплярApplicationEventsсоздается до и удаляется после каждого теста как часть текущего потока.Следовательно, вы даже можете использовать внедрение поля и@TestInstance(TestInstance.Lifecycle.PER_CLASS)делить тестовый экземпляр между несколькими тестами (PER_METHODпо умолчанию).

Обратите внимание, что запуск всего контекста Spring@SpringBootTestдля такого тестаможет быть излишним.Мы также могли бы написать тест, который заполняет минимальный SpringTestContextтолько нашимbean-компонентом UserService, чтобы убедиться, чтоUserCreationEvent опубликован:

@RecordApplicationEvents@ExtendWith(SpringExtension.class)@ContextConfiguration(classes = UserService.class)class UserServicePerClassTest {   @Autowired  private ApplicationEvents applicationEvents;   @Autowired  private UserService userService;   @Test  void userCreationShouldPublishEvent() {     this.userService.createUser("duke");     assertEquals(1, applicationEvents      .stream(UserCreationEvent.class)      .filter(event -> event.getUsername().equals("duke"))      .count());     applicationEvents.stream().forEach(System.out::println);  }}

Или используйте альтернативный подход к тестированию.

Альтернативы тестированию весенних событий

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

@ExtendWith(MockitoExtension.class)class UserServiceUnitTest {   @Mock  private ApplicationEventPublisher applicationEventPublisher;   @Captor  private ArgumentCaptor<UserCreationEvent> eventArgumentCaptor;   @InjectMocks  private UserService userService;   @Test  void userCreationShouldPublishEvent() {     Long result = this.userService.createUser("duke");     Mockito.verify(applicationEventPublisher).publishEvent(eventArgumentCaptor.capture());     assertEquals("duke", eventArgumentCaptor.getValue().getUsername());  }   @Test  void batchUserCreationShouldPublishEvents() {    List<Long> result = this.userService.createUser(List.of("duke", "mike", "alice"));     Mockito      .verify(applicationEventPublisher, Mockito.times(3))      .publishEvent(any(UserCreationEvent.class));  }}

Обратите внимание, что здесь мы не используем никакой поддержки Spring Test иполагаемсяисключительно наMockitoи JUnit Jupiter.

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

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)class ApplicationIT {   @Autowired  private TestRestTemplate testRestTemplate;   @Test  void shouldCreateUserAndPerformReporting() {     ResponseEntity<Void> result = this.testRestTemplate      .postForEntity("/api/users", "duke", Void.class);     assertEquals(201, result.getStatusCodeValue());    assertTrue(result.getHeaders().containsKey("Location"),      "Response doesn't contain Location header");     // additional assertion to verify the counter was incremented    // additional assertion that a new message is part of the queue  }}

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

Резюме тестирования событий Spring с помощью Spring Boot

Все различные подходы сводятся к тестированию поведения и состояния.Благодаря новойфункции @RecordApplicationEvents вSpring Test у насможетвозникнуть соблазн провести больше поведенческих тестов и проверить внутреннюю часть нашей реализации.В общем, мы должны сосредоточиться на тестировании состояния (также известном как результат), поскольку оно поддерживает беспроблемный рефакторинг.

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

Помните об этом и не перегружайте свои тесты деталями реализации (если вы хотите провести рефакторинг в будущем :).Тем не менее, есть определенные тестовые сценарии, когда функция@RecordApplicationEvents очень помогает.

Исходный код со всеми альтернативными вариантами для тестирования Spring Event с помощью Spring Bootдоступен на GitHub.

Подробнее..
Категории: Events , Java , Testing , Spring boot

Как расширить Spring своим типом Repository на примере Infinispan

27.12.2020 16:14:22 | Автор: admin

Зачем об этом писать?

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

Программирование для Infinispan я не буду рассматривать подробно, детали реализации всегда можно посмотреть в исходниках, указанных в конце статьи. Основной упор сделан именно на сопряжение механизма Spring Boot Repository и нового типа репозитория.

С чего всё начиналось

В ходе работы на одном из проектов у одного из архитектора возникла идея, что можно написать свои типы репозиториев по аналогии, как это сделано в разных модулях Spring (например, JPARepository, KeyValueRepository, CassandraRepository и т.п.). В качестве пробной реализации решили выбрать работу с данными через Infinispan.

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

Когда я начал прорабатывать тему в интернете, то Google упорно выдавал почти одни статьи про то, как замечательно использовать JPARepository во всех видах на тривиальных примерах. По KeyValueRepository информации было ещё меньше. На StackOverFlow печальные никем не отвеченные вопросы по подобной теме. Делать нечего, пришлось лезть в исходники Spring.

Infinispan

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

Было решено, что наиболее подходящий кандидат для исследования - KeyValueRepository, как самый близкий к данной области, уже реализованный в Spring. Вся разница только в том, что вместо Infinispan (на его месте мог быть и Hazelcast, например), как хранилища данных, в KeyValueRepository обычный ConcurrentHashMap.

Реализация

Чтобы в Spring проекте подключить возможность пользоваться репозиторием для хранилища ключ-значение пользуются аннотацией EnableMapRepositories.

@SpringBootApplication@EnableMapRepositories("my.person.package.for.entities")public class Application {    public static void main(String[] args) {        SpringApplication.run(Application.class, args);    }}

Можем практически полностью скопировать содержимое кода данной аннотации и создать свою EnableInfinispanRepositories.

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

Код аннотации EnableInfinispanRepositories
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@Import(InfinispanRepositoriesRegistrar.class)public @interface EnableInfinispanRepositories {    String[] value() default {};    String[] basePackages() default {};    Class<?>[] basePackageClasses() default {};    ComponentScan.Filter[] excludeFilters() default {};    ComponentScan.Filter[] includeFilters() default {};    String repositoryImplementationPostfix() default "Impl";    String namedQueriesLocation() default "";    QueryLookupStrategy.Key queryLookupStrategy() default QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND;    Class<?> repositoryFactoryBeanClass() default KeyValueRepositoryFactoryBean.class;    Class<?> repositoryBaseClass() default DefaultRepositoryBaseClass.class;    String keyValueTemplateRef() default "infinispanKeyValueTemplate";    boolean considerNestedRepositories() default false;}

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

@Import(MapRepositoriesRegistrar.class)public @interface EnableMapRepositories {}

Ниже код MapRepositoriesRegistar.

public class MapRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport {@Overrideprotected Class<? extends Annotation> getAnnotation() {return EnableMapRepositories.class;}@Overrideprotected RepositoryConfigurationExtension getExtension() {return new MapRepositoryConfigurationExtension();}}

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

Сделаем по аналогии свой InfinispaRepositoriesRegistar.
@NoArgsConstructorpublic class InfinispanRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport {    @Override    protected Class<? extends Annotation> getAnnotation() {        return EnableInfinispanRepositories.class;    }    @Override    protected RepositoryConfigurationExtension getExtension() {        return new InfinispanRepositoryConfigurationExtension();    }}

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

public class MapRepositoryConfigurationExtension extends KeyValueRepositoryConfigurationExtension {@Overridepublic String getModuleName() {return "Map";}@Overrideprotected String getModulePrefix() {return "map";}@Overrideprotected String getDefaultKeyValueTemplateRef() {return "mapKeyValueTemplate";}@Overrideprotected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition(RepositoryConfigurationSource configurationSource) {BeanDefinitionBuilder adapterBuilder = BeanDefinitionBuilder.rootBeanDefinition(MapKeyValueAdapter.class);adapterBuilder.addConstructorArgValue(getMapTypeToUse(configurationSource));BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(KeyValueTemplate.class);    ...  }  ...}

В MapKeyValueAdapter будет реализована самая специфическая часть, характерная именно для локального хранения кэша в HashMap. А вот KeyValueTemplate оборачивает методы адаптера довольно общим кодом.

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

Реализация InfinispanRepositoriesConfigurationExtension
@NoArgsConstructorpublic class InfinispanRepositoryConfigurationExtension extends KeyValueRepositoryConfigurationExtension {    @Override    public String getModuleName() {        return "Infinispan";    }    @Override    protected String getModulePrefix() {        return "infinispan";    }    @Override    protected String getDefaultKeyValueTemplateRef() {        return "infinispanKeyValueTemplate";    }    @Override    protected Collection<Class<?>> getIdentifyingTypes() {        return Collections.singleton(InfinispanRepository.class);    }    @Override    protected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition(RepositoryConfigurationSource configurationSource) {        RootBeanDefinition infinispanKeyValueAdapterDefinition = new RootBeanDefinition(InfinispanKeyValueAdapter.class);        RootBeanDefinition keyValueTemplateDefinition = new RootBeanDefinition(KeyValueTemplate.class);        ConstructorArgumentValues constructorArgumentValuesForKeyValueTemplate = new ConstructorArgumentValues();        constructorArgumentValuesForKeyValueTemplate.addGenericArgumentValue(infinispanKeyValueAdapterDefinition);        keyValueTemplateDefinition.setConstructorArgumentValues(constructorArgumentValuesForKeyValueTemplate);        return keyValueTemplateDefinition;    }}

Нужно обязательно в нашем ConfigurationExtension ещё перегрузить метод getIdentifyingTypes(), чтобы в нём сослаться на наш новый тип репозитория (см. реализацию выше).

@NoRepositoryBeanpublic interface InfinispanRepository <T, ID> extends PagingAndSortingRepository<T, ID> {}

Чтобы окончательно всё заработало, нужно сконфигурировать KeyValueTemplate, подсунув ему наш адаптер.

@Configurationpublic class InfinispanConfiguration extends CachingConfigurerSupport {    @Autowired    private ApplicationContext applicationContext;    @Bean    public InfinispanKeyValueAdapter getInfinispanAdapter() {        return new InfinispanKeyValueAdapter(                applicationContext.getBean(CacheManager.class)        );    }    @Bean("infinispanKeyValueTemplate")    public KeyValueTemplate getInfinispanKeyValueTemplate() {        return new KeyValueTemplate(getInfinispanAdapter());    }}

На этом всё.

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

Резюме

Написав всего 6 своих классов, мы получили новый тип репозитория, который может работать с Infinispan в качестве хранилища данных. И работает этот новый тип репозитория очень похоже на стандартные Spring репозитории.

Полный комплект исходников можно найти на моём github.

Исходники Spring Data KeyValue можно увидеть также на github.

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

Подробнее..

Перевод Мониторинг и профилирование Spring Boot приложения

02.01.2021 00:07:07 | Автор: admin

Мониторинг очень важен для современных приложений, современные приложения по своей природе сильно распределены и имеют разные зависимости, такие как база данных, службы, кеширование и многое другое.Поэтому все более важны отслеживание и мониторинг этих служб, чтобы приложение придерживалось условий SLA(ServiceLevelAgreement).SLA это соглашение между клиентом и поставщиком услуг, в нем учитываются надежность, скорость отклика и другие показатели уровня обслуживания.

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

Какие показатели нужно отслеживать?

  • Доступность услуги:время, в течение которого услуга доступна для использования.Это может быть измерено с точки зрения времени отклика, например, процентиль X, сокращенно обозначаемый как pX, например, p95, p99, p99.999.Не для всех сервисов требуется p99,999, системы с гарантированной высокой доступностью, такие как электронная коммерция, поиск, оплата и т. д., должны иметь более высокое SLA.

  • Уровень дефектов:несмотря на все усилия по разработке системы, ни одна система не является идеальной на 100%.Мы должны подсчитывать процент ошибок в основных потоках.В эту категорию могут быть включены производственные сбои, такие как ошибка сервера, ошибка запроса базы данных, ошибка соединения, сетевые ошибки и пропущенные сроки.

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

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

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

Настройка мониторинга

Типичная система настройки мониторинга будет состоять из трех компонентов

  1. Хранилища метрик (какправило,база данных временных рядов), такое как InfluxDB, TimescaleDB, Prometheusи т. д.

  2. Панель инструментов (панель будет использоваться для визуализации данных, хранящихся в хранилище метрик).

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

У нас могут быть и другие компоненты, например, оповещение, где каналами оповещения могут быть электронная почта, Slack или любые другие.Компонент оповещения будет отправлять оповещения владельцам приложений или подписчикам событий.Мы собираемся использоватьGrafanaкак панель управления и систему оповещений,Prometheusкак систему хранения метрик.

Нам понадобятся:

  1. Любая IDE

  2. Платформа Java

  3. Gradle

Создайте проект с помощью инициализатора Spring Boot, добавьте столько зависимостей, сколько нам нужно.Мы собираемся использовать библиотеку Micrometer, это инструментальный фасад, который обеспечивает привязки для многих хранилищ метрик, таких как Prometheus, Datadog и New Relic, и это лишь некоторые из них.

Из коробки Micrometer обеспечивает

  1. HTTP-запрос

  2. JVM

  3. База данных

  4. Метрики, относящиеся к системе кэширования и т. д.

Некоторые метрики включены по умолчанию, тогда как другие можно включить, отключить или настроить.Мы будем использовать файл application.properties для включения, отключения и настройки метрик. Нам также нужно использовать Spring boot actuator, так как он откроет доступ к конечной точкеPrometheus.

Добавьте эти зависимости в файл build.gradle:

  1. io.micrometer: micrometer-registry-prometheus

  2. org.springframework.boot: spring-boot-starter-actuator

dependencies {  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'  implementation 'org.springframework.boot:spring-boot-starter-web'  compileOnly 'org.projectlombok:lombok'  annotationProcessor 'org.projectlombok:lombok'  providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'  implementation 'io.micrometer:micrometer-registry-prometheus'  implementation 'org.springframework.boot:spring-boot-starter-actuator'  // https://mvnrepository.com/artifact/com.h2database/h2  compile group: 'com.h2database', name: 'h2', version: '1.4.200'  testImplementation('org.springframework.boot:spring-boot-starter-test') {  exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'  }}

Мы можем включить экспорт Prometheus, добавив следующую строку в файл свойств.

management.metrics.export.prometheus.enabled = true

После добавления этой строки Micrometer начнет накапливать данные о приложении, и эти данные можно будет просмотреть, перейдя на конечную точку actuator/Prometheus.Эта конечная точка будет использоваться в скрипте Prometheus для получения данных с наших серверов приложений.

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

management.endpoints.web.exposure.include=prometheus

ПРИМЕЧАНИЕ. Не включайте все конечные точки actuator, так как это может открыть лазейку в системе безопасности.Мы должны выбирать их выборочно, особенно в производственной системе, даже если мы хотим, не раскрывать конечную точку для всего мира, так как она может раскрыть большой объем данных о приложении, использовать какой-то прокси или какое-то правило, чтобы скрыть данные от внешнего мира.

Различные частиHTTP-запросовнастраиваются, например SLA, настройка признака должна быть вычислена или нет гистограмма процентилей делается спомощьюсвойствmetrics.distribution.

В примере application.properties могут быть такие строки

# Включить экспорт prometheusmanagement.metrics.export.prometheus.enabled = true# Включить конечную точку Prometheusmanagement.endpoints.web.exposure.include = Прометей# включить гистограмму на основе процентилей для http запросовmanagement.metrics.distribution.percentiles-histogram.http.server.requests = true# сегментов гистограммы http SLAmanagement.metrics.distribution.sla.http.server.requests = 100 мс, 150 мс, 250 мс, 500 мс, 1 с# включить метрики JVMmanagement.metrics.enable.jvm = true

Теперь, если мы запустим приложение и перейдем на страницу http://locahost:8080/actator/prometheus, будет отображаться чертовски много данных.

Приведенные выше данные отображают детали HTTP-запроса, exception=None означает, что исключение не произошло, если оно есть, мы можем использовать это для фильтрации количества запросов, которые не удалось выполнить из-за этого исключения,method=GETимя метода HTTP.status=200HTTP статус равен 200,uri=/actator/prometheusотображает путь URL,le=xyzотображает время обработки,N.0отображает количествовызововэтой конечной точки.

Эти данные представляют собой гистограмму, которую можно построить в Grafana, например, чтобы построить график p95 за 5 минут, мы можем использовать следующий запрос.

histogram_quantile(0.95,sum(rate(http_server_requests_seconds_bucke[5m])) by (le))

В Grafana можно построить и другие графики показателей, например круговую диаграмму и т. д.

Пользовательские показатели

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

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

  1. Добавить товары

  2. Получить товары

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

@Componentpublic class StockManager {  @Autowired private MeterRegistry meterRegistry;  private List<String> orders = new Vector<>();  private Counter counter;  private Gauge gauge;@PostConstruct  public void init() {    counter =        Counter.builder("order_created")            .description("number of orders created")            .register(meterRegistry);    gauge =        Gauge.builder("stock.size", this, StockManager::getNumberOfItems)            .description("Number of items in stocks")            .register(meterRegistry);  }public int getNumberOfItems() {    return orders.size();  }public void addItems(List<String> items) {    orders.addAll(items);    // measure gauge    gauge.measure();  }public List<String> getItem(int count) {    List<String> items = new ArrayList<>(count);    while (count < 0) {      try {        items.add(orders.remove(0));      } catch (ArrayIndexOutOfBoundsException e) {        break;      }      count -= 1;    }    // increase counter    counter.increment();    //  measure gauge    gauge.measure();    return items;  }}

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

@RestController@RequestMapping(path = "stocks")@RequiredArgsConstructor(onConstructor = @__(@Autowired))public class StockController {  @NonNull private StockManager stockManager;  @GetMapping  @ResponseBody  public List<String> getItems(@RequestParam int size) {    return stockManager.getItem(size);  }  @PostMapping  @ResponseBody  public int addItems(@RequestParam List<String> items) {    stockManager.addItems(items);    return stockManager.getNumberOfItems();  }}  @PostMapping  @ResponseBody  public int addItems(@RequestParam List<String> items) {    stockManager.addItems(items);    return stockManager.getNumberOfItems();  }}

Давайте сначала добавим десять товаров, используя два вызова API:

  1. Curl -X POST http://localhost:8080/stocks?Items = 1,2,3,4

  2. Curl -X POST http://localhost:8080/stocks?Items = 5,6,7,8,9,10

Теперь, если мы перейдем к конечным точкам Prometheus, то увидим следующие данные, которые показывают, что в настоящее время у нас есть 10 товаров на складе.

# HELP stock_size Количество товаров на складе# TYPE stock_size gaugestock_size 10.0

Теперь мы собираемся разместить заказ на 3 товара:

http://localhost:8080/stocks?size=3

Теперь, если мы посмотрим конечную точку Prometheus, мы получим следующие данные, которые показывают, что размер запаса был изменен до семи.

# HELP stock_size Количество товаров на складе# TYPE stock_size gaugestock_size 7.0

Кроме того, мы видим, что на счетчике добавлено значение 1, это означает, что размещен один ордер.

# HELP order_created_total количество созданных заказов# TYPE order_created_total counterorder_created_total 1.0ordercreated_total 1.0

Профилирование

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

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

Профилирование очень полезно при диагностике системных проблем, например, сколько времени занимает HTTP-вызов, если он занимает N секунд, а затем, где все это время было потрачено, каково распределение N секунд между различными запросами к базе данных, вызовами последующих служб и т. д. Мы можем использовать гистограмму для построения графика распределения на панели инструментов, также мы можем использовать счетчик для измерения количества запросов к БД и т. д. Для профилирования нам необходимо внедрить код во многие функции, которые будут выполняться как часть выполнения метода.

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

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

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

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

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

Аннотация Timed бесполезна без бина TimedAspect, поскольку мы переопределяем аннотациюTimed, поэтому мы также определим класс TimedAspect в соответствии с потребностями, такими как ведение журнала, массовое профилирование (профилируйте все методы в пакете без добавления каких-либо аннотаций к любому методу или классу), повторить попытку и т. д. В этой истории мы рассмотрим три сценария использования:

  1. Массовое профилирование.

  2. Логирование.

  3. Специфический для профиля метод.

Созадим файл java MonitoringTimed.java, в который мы добавим новое поле с именемloggingEnabled,это поле будет использоваться для проверки, включено ли ведение журнала или нет, если оно включено, а затем регистрировать аргументы метода и возвращаемые значения.

@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public @interface MonitoringTimed {  /** All fields are same as in {@link io.micrometer.core.annotation.Timed} */  String value() default "";  String[] extraTags() default {};  boolean longTask() default false;  double[] percentiles() default {};  boolean histogram() default false;  String description() default "";  // NEW fields starts here  boolean loggingEnabled() default false;}

Эта аннотация бесполезна без класса аспекта Timed, поэтомубудет определенновый классMonitoringTimedAspectсо всеми необходимыми деталями, этот класс будет иметь метод для профилирования любого метода на основе объединенного объекта обработки иобъектаMonitoringTimed,а другой - для профилирования метода на основе в аннотации MonitoringTimed.

@Around("execution (@com.gitbub.sonus21.monitoring.aop.MonitoringTimed * *.*(..))")public Object timedMethod(ProceedingJoinPoint pjp) throws Throwable {  Method method = ((MethodSignature) pjp.getSignature()).getMethod();  MonitoringTimed timed = method.getAnnotation(MonitoringTimed.class);  if (timed == null) {    method = pjp.getTarget().getClass().getMethod(method.getName(), method.getParameterTypes());    timed = method.getAnnotation(MonitoringTimed.class);  }  final String metricName = generateMetricName(pjp, timed);  return timeThisMethod(pjp, timed, metricName);}public Object timeThisMethod(ProceedingJoinPoint pjp, MonitoringTimed timed) throws Throwable {  final String metricName = generateMetricName(pjp, timed);  return timeThisMethod(pjp, timed, metricName);}

МетодTimedMethodсаннотациейAroundиспользуется для фильтрации всех вызовов методов, аннотированных с помощьюMonitoringTimed.

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

@Aspect@Componentpublic class ControllerProfiler {  private static Map<String, Object> timedAnnotationData = new HashMap<>();  static {    // use percentile data of p90, p95, p99.99    double[] percentiles = {0.90, 0.95, 0.9999};    // set histogram to true    timedAnnotationData.put("histogram", true);    // set percentile    timedAnnotationData.put("percentiles", percentiles);  }  @Autowired private MonitoringTimedAspect timedAspect;  private static final MonitoringTimed timed = Javanna.createAnnotation(MonitoringTimed.class, timedAnnotationData);  private static final Logger logger = LoggerFactory.getLogger(ControllerProfiler.class);  @Pointcut("execution(* com.gitbub.sonus21.monitoring.controller..*.*(..))")  public void controller() {}  @Around("controller()")  public Object profile(ProceedingJoinPoint pjp) throws Throwable {    // here add other logic like error happen then log parameters etc    return timedAspect.timeThisMethod(pjp, timed);  }}

Интересной строкой в приведенном выше коде является @Pointcut(execution(com.gitbub.sonus21.monitoring.controller...*(..))), которая определяет pointcut, выражение pointcut может быть определено с использованием логических операторов вродеnot (!), or (||), and (&&). После того, как метод квалифицирован согласно выражению pointcut, он может вызвать соответствующий метод, определенный с помощью аннотации [at]Around.Поскольку мы определилиметодprofile,который будет вызываться, мы также можем определить другие методы, используя аннотации [at]After, [at]Before и т. д.

После добавления нескольких элементов с помощью метода POST мы можем увидеть следующие данные в конечной точке Prometheus.

method_timed_seconds {class = "com.gitbub.sonus21.monitoring.controller.StockController", exception = "none", method = "addItems", quantile = "0.9",} 0.0method_timed_seconds_bucket {class = "com.gitbub.sonus21.monitoring.controller.StockController", exception = "none", method = "addItems", le = "0.001",} 3.0method_timed_seconds_bucket {class = "com.gitbub.sonus21.monitoring.controller.StockController", exception = "none", method = "addItems", le = "0.002446676",} 3.0

Мы можем напрямую использоватьаннотациюMonitoringTimedтакже для любого метода для измерения времени выполнения, например, давайтеизмерим,сколько времениStockManagerметод addItemsтратит на добавление элементов.

@MonitoringTimedpublic void addItems(List<String> items) {  orders.addAll(items);  // measure gauge  gauge.measure();}

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

method_timed_seconds_count{class="com.gitbub.sonus21.monitoring.service.StockManager",exception="none",method="addItems",} 4.0method_timed_seconds_sum{class="com.gitbub.sonus21.monitoring.service.StockManager",exception="none",method="addItems",} 0.005457965method_timed_seconds_max{class="com.gitbub.sonus21.monitoring.service.StockManager",exception="none",method="addItems",} 0.00615316

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

Полный код доступен наGitHub.

Дополнительное чтение

Magic With the Spring Boot Actuator

Spring Boot Actuator in Spring Boot 2.0

Spring Boot Admin Client Configuration Using Basic HTTP Authentication

Подробнее..

Валидация данных в Spring Boot

10.01.2021 12:14:24 | Автор: admin

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


Эту задачу решает Bean Validation. Он интегрирован со Spring и Spring Boot. Hibernate Validator считается эталонной реализацией Bean Validation.


Основы валидации Bean


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


При передаче размеченного таким образом объекта класса в валидатор, происходит проверка на ограничения.


Настройка


Добавьте следующие зависимости в проект:


<dependency>    <groupId>javax.validation</groupId>    <artifactId>validation-api</artifactId>    <version>2.0.1.Final</version></dependency><dependency>    <groupId>org.hibernate</groupId>    <artifactId>hibernate-validator</artifactId>    <version>7.0.0.Final</version></dependency>

dependencies {    compile group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final'    compile group: 'org.hibernate', name: 'hibernate-validator', version: '7.0.0.Final'}

Валидация в Spring MVC Controller


Сначала данные попадают в контроллер. У входящего HTTP-запроса возможно проверить следующие параметры:


  • тело запроса
  • переменные пути (например, id в /foos/{id})
  • параметры запроса

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


Валидация тела запроса


Тело запроса POST и PUT обычно содержит данные в формате JSON. Spring автоматически сопоставляет входящий JSON с объектом Java.


Проверяем соответствует ли входящий Java объект нашим требованиям.


class Input {     @Min(1)     @Max(10)     private int numberBetweenOneAndTen;     @Pattern(regexp = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$")     private String ipAddress;     // ...}

  • Поле numberBetweenOneAndTen должно быть от 1 до 10, включительно.
  • Поле ipAddress должно содержать строку в формате IP-адреса.

Контроллер REST принимает объект Input и выполняет проверку:


@RestControllerclass ValidateRequestBodyController {  @PostMapping("/validateBody")  public ResponseEntity<String> validateBody(@Valid @RequestBody Input input) {    return ResponseEntity.ok("valid");  }}

Достаточно добавить в параметр input аннотацию @Valid, чтобы сообщить спрингу передать объект Валидатору, прежде чем делать с ним что-либо еще.


Если класс содержит поле с другим классом, который тоже необходимо проверить это поле необходимо пометить аннотацией Valid.


Исключение MethodArgumentNotValidException выбрасывается, когда объект не проходит проверку. По умолчанию, Spring переведет это исключение в HTTP статус 400.


Проверка переменных пути и параметров запроса


Проверка переменных пути и параметров запроса работает по-другому.


Не проверяются сложные Java-объекты, так как path-переменные и параметры запроса являются примитивными типами, такими как int, или их аналогами: Integer или String.


Вместо аннотации поля класса, как описано выше, добавляют аннотацию ограничения (в данном случае @Min) непосредственно к параметру метода в контроллере Spring:


@Validated@RestControllerclass ValidateParametersController {  @GetMapping("/validatePathVariable/{id}")  ResponseEntity<String> validatePathVariable(      @PathVariable("id") @Min(5) int id  ) {    return ResponseEntity.ok("valid");  }  @GetMapping("/validateRequestParameter")  ResponseEntity<String> validateRequestParameter(      @RequestParam("param") @Min(5) int param  ) {     return ResponseEntity.ok("valid");  }}

Обратите внимание, что необходимо добавить @Validated Spring в контроллер на уровне класса, чтобы сказать Spring проверять ограничения на параметрах метода.


В этом случае аннотация @Validated устанавливается на уровне класса, даже если она присутствует на методах.


В отличии валидации тела запроса, при неудачной проверки параметра вместо метода MethodArgumentNotValidException будет выброшен ConstraintViolationException. По умолчанию последует ответ со статусом HTTP 500 (Internal Server Error), так как Spring не регистрирует обработчик для этого исключения.


Вернем HTTP статус 400, так как клиент предоставил недействительный параметр. Для этого добавляем пользовательский обработчик исключений в контоллер:


@RestController@Validatedclass ValidateParametersController {  // request mapping method omitted  @ExceptionHandler(ConstraintViolationException.class)  @ResponseStatus(HttpStatus.BAD_REQUEST)  public ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {    return new ResponseEntity<>("not valid due to validation error: " + e.getMessage(), HttpStatus.BAD_REQUEST);  }}

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


Валидация в сервисном слое


Можно проверять данные на любых компонентах Spring. Для этого используется комбинация аннотаций @Validated и @Valid.


@Service@Validatedclass ValidatingService{    void validateInput(@Valid Input input){      // do something    }}

Аннотация @Validated устанавливается только на уровне класса, так что не ставьте ее на метод в данном случае.


Валидация сущностей JPA


Persistence Layer это последняя линия проверки данных. По умолчанию Spring Data использует Hibernate, который поддерживает Bean Validation из коробки.


Обычно мы не хотим делать проверку так поздно, поскольку это означает, что бизнес-код работал с потенциально невалидными объектами, что может привести к непредвиденным ошибкам.


Допустим, необходимо хранить объекты нашего класса Input в базе данных. Сначала добавляем нужную JPA аннотацию @Entity, а так же поле id:


@Entitypublic class Input {  @Id  @GeneratedValue  private Long id;  @Min(1)  @Max(10)  private int numberBetweenOneAndTen;  @Pattern(regexp = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$")  private String ipAddress;  // ...}

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


Bean Validation запускается Hibernate только после того как EntityManager вызовет flush.


Чтобы отключить Bean Validation в репозиториях Spring, достаточно установить свойство Spring Boot spring.jpa.properties.javax.persistence.validation.mode равным null.


Валидация конфигурации приложения


Spring Boot аннотация @ConfigurationProperties используется для связывания свойств из application.properties с Java объектом.


Данные из application необходимы для стабильной работы приложения. Bean Validation поможет обнаружить ошибку в этих данных при старте приложения.


Допустим имеется следующий конфигурационный класс:


@Validated@ConfigurationProperties(prefix="app.properties")class AppProperties {  @NotEmpty  private String name;  @Min(value = 7)  @Max(value = 30)  private Integer reportIntervalInDays;  @Email  private String reportEmailAddress;  // getters and setters}

При попытке запуска с недействительным адресом электронной почты получаем ошибку:


***************************APPLICATION FAILED TO START***************************Description:Binding to target org.springframework.boot.context.properties.bind.BindException:  Failed to bind properties under 'app.properties' to  io.reflectoring.validation.AppProperties failed:    Property: app.properties.reportEmailAddress    Value: manager.analysisapp.com    Reason: must be a well-formed email addressAction:Update your application's configuration

Стандартные ограничения


Библиотека javax.validation имеет множество аннотаций для валидации.


Каждая аннотация имеет следующие поля:


  • message указывает на ключ свойства в ValidationMessages.properties, который используется для отправки сообщения в случае нарушения ограничения.
  • groups позволяет определить, при каких обстоятельствах будет срабатывать эта проверка (о группах проверки поговорим позже).
  • payload позволяет определить полезную нагрузку, которая будет передаваться сс проверкой.
  • @Constraint указывает на реализацию интерфейса ConstraintValidator.

Рассмотрим популярные ограничения.


@NotNull и @Null


@NotNull аннотированный элемент не должен быть null. Принимает любой тип.
@Null аннотированный элемент должен быть null. Принимает любой тип.


@NotBlank и @NotEmpty


@NotBlank аннотированный элемент не должен быть null и должен содержать хотя бы один непробельный символ. Принимает CharSequence.
@NotEmpty аннотированный элемент **не** должен быть null или пустым. Поддерживаемые типы:


  • CharSequence
  • Collection. Оценивается размер коллекции
  • Map. Оценивается размер мапы
  • Array. Оценивается длина массива

@NotBlank применяется только к строкам и проверяет, что строка не пуста и не состоит только из пробелов.


@NotNull применяется к CharSequence, Collection, Map или Array и проверяет, что объект не равен null. Но при этом он может быть пуст.


@NotEmpty применяется к CharSequence, Collection, Map или Array и проверяет, что он не null имеет размер больше 0.


Аннотация @Size(min=6) пропустит строку состоящую из 6 пробелов и/или символов переноса строки, а @NotBlank не пропустит.


@Size


Размер аннотированного элемента должен быть между указанными границами, включая сами границы. null элементы считаются валидными.


Поддерживаемые типы:


  • CharSequence. Оценивается длина последовательности символов
  • Collection. Оценивается размер коллекции
  • Map. Оценивается размер мапы
  • Array. Оценивается длина массива

Добавление пользовательского валидатора


Если имеющихся аннотаций ограничений недостаточно, то создайте новые.


В классе Input использовалось регулярное выражение для проверки того, что строка является IP адресом. Регулярное выражение не является полным: оно позволяет сокеты со значениями больше 255, таким образом "111.111.111.333" будет считаться действительным.


Давайте напишем валидатор, который реализует эту проверку на Java. Потому что как говорится, до решения проблемы регулярным выражением у вас была одна проблема, а теперь стало двe :smile:


Сначала создаем пользовательскую аннотацию @IpAddress:


@Target({ FIELD })@Retention(RUNTIME)@Constraint(validatedBy = IpAddressValidator.class)@Documentedpublic @interface IpAddress {  String message() default "{IpAddress.invalid}";  Class<?>[] groups() default { };  Class<? extends Payload>[] payload() default { };}

Реализация валидатора выглядит следующим образом:


class IpAddressValidator implements ConstraintValidator<IpAddress, String> {  @Override  public boolean isValid(String value, ConstraintValidatorContext context) {    Pattern pattern =       Pattern.compile("^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$");    Matcher matcher = pattern.matcher(value);    try {      if (!matcher.matches()) {        return false;      } else {        for (int i = 1; i <= 4; i++) {          int octet = Integer.valueOf(matcher.group(i));          if (octet > 255) {            return false;          }        }        return true;      }    } catch (Exception e) {      return false;    }  }}

Теперь можно использовать аннотацию @IpAddress, как и любую другую аннотацию ограничения.


class InputWithCustomValidator {  @IpAddress  private String ipAddress;  // ...}

Принудительный вызов валидации


Для принудительного вызова проверки, без использования Spring Boot, создайте валидатор вручную.


class ProgrammaticallyValidatingService {  void validateInput(Input input) {    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();    Validator validator = factory.getValidator();    Set<ConstraintViolation<Input>> violations = validator.validate(input);    if (!violations.isEmpty()) {      throw new ConstraintViolationException(violations);    }  }}

Тем не менее, Spring Boot предоставляет предварительно сконфигурированный экземпляр валидатора. Внедрив этот экземпляр в сервис не придется создавать его вручную.


@Serviceclass ProgrammaticallyValidatingService {  private Validator validator;  public ProgrammaticallyValidatingService(Validator validator) {    this.validator = validator;  }  public void validateInputWithInjectedValidator(Input input) {    Set<ConstraintViolation<Input>> violations = validator.validate(input);    if (!violations.isEmpty()) {      throw new ConstraintViolationException(violations);    }  }}

Когда этот сервис внедряется Spring, в конструктор автоматически вставляется экземпляр валидатора.


Группы валидаций {#validation-groups}


Некоторые объекты участвуют в разных вариантах использования.


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


  • только перед созданием
  • только перед обновлением
  • или в обоих случаях

Функция Bean Validation, которая позволяет нам внедрять такие правила проверки, называется "Validation Groups".


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


Для нашего примера CRUD определим два маркерных интерфейса OnCreate и OnUpdate:


interface OnCreate {}interface OnUpdate {}

Затем используем эти интерфейсы с любой аннотацией ограничения:


class InputWithGroups {  @Null(groups = OnCreate.class)  @NotNull(groups = OnUpdate.class)  private Long id;  // ...}

Это позволит убедиться, что id пуст при создании и заполнен при обновлении.


{{< admonition type=warning title="" open=true >}}
Spring поддерживает группы проверки только с аннотацией @Validated
{{< /admonition >}}


@Service@Validatedclass ValidatingServiceWithGroups {    @Validated(OnCreate.class)    void validateForCreate(@Valid InputWithGroups input){      // do something    }    @Validated(OnUpdate.class)    void validateForUpdate(@Valid InputWithGroups input){      // do something    }}

Обратите внимание, что аннотация @Validated применяется ко всему классу. Чтобы определить, какая группа проверки активна, она также применяется на уровне метода.


Использование групп проверки может легко стать анти-паттерном. При использовании групп валидации сущность должна знать правила валидации для всех случаев использования (групп), в которых она используется.


Возвращение структурных ответов на ошибки


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


Сначала нужно определить эту структуру данных. Назовем ее ValidationErrorResponse и она содержит список объектов Violation:


public class ValidationErrorResponse {  private List<Violation> violations = new ArrayList<>();  // ...}public class Violation {  private final String fieldName;  private final String message;  // ...}

Затем создадим глобальный ControllerAdvice, который обрабатывает все ConstraintViolationExventions, которые пробрасываются до уровня контроллера. Чтобы отлавливать ошибки валидации и для тел запросов, мы также будем работать с MethodArgumentNotValidExceptions:


@ControllerAdviceclass ErrorHandlingControllerAdvice {  @ExceptionHandler(ConstraintViolationException.class)  @ResponseStatus(HttpStatus.BAD_REQUEST)  @ResponseBody  ValidationErrorResponse onConstraintValidationException(      ConstraintViolationException e) {    ValidationErrorResponse error = new ValidationErrorResponse();    for (ConstraintViolation violation : e.getConstraintViolations()) {      error.getViolations().add(        new Violation(violation.getPropertyPath().toString(), violation.getMessage()));    }    return error;  }  @ExceptionHandler(MethodArgumentNotValidException.class)  @ResponseStatus(HttpStatus.BAD_REQUEST)  @ResponseBody  ValidationErrorResponse onMethodArgumentNotValidException(      MethodArgumentNotValidException e) {    ValidationErrorResponse error = new ValidationErrorResponse();    for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {      error.getViolations().add(        new Violation(fieldError.getField(), fieldError.getDefaultMessage()));    }    return error;  }}

Здесь информацию о нарушениях из исключений переводится в нашу структуру данных ValidationErrorResponse.


Обратите внимание на аннотацию @ControllerAdvice, которая делает методы обработки исключений глобально доступными для всех контроллеров в контексте приложения.

Подробнее..
Категории: Java , Spring , Validation , Spring boot , Spring framework

Перевод Настройка Spring Data JPA с помощью Spring Boot

28.03.2021 14:14:21 | Автор: admin

До Spring Boot разработки вам нужно сделать несколько вещей, чтобы настроитьSpring Data JPA.Вам не только нужно было аннотировать свои классы сущностей с помощью аннотаций преобразования, добавить Spring Data JPA зависимость и настроить соединение с базой данных.Вам также нужно включить репозитории и управление транзакциями и настроить EntityManagerFactory.Это - повторяющаяся и раздражающая задача.

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

В этой статье объясняется конфигурация Spring Boot по умолчанию для Spring Data JPA, какие параметры конфигурации вы можете использовать для ее изменения, а также конфигурацию, которую вы, возможно, захотите добавить.

Обязательные зависимости

Прежде чем вы сможете приступить к настройке Spring Data JPA, вам необходимо добавить его в свое приложение.В приложении Spring Boot это обычно означает, что вам нужно добавить правильный стартер в зависимости вашего проекта.Самый простой способ сделать это для нового проекта - использоватьSpring Initializrдля настройки процесса сборки и добавления всех необходимых зависимостей.Для всех проектов Spring Boot вам необходимо добавитьмодульspring-boot-starter-data-jpa.

<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-data-jpa</artifactId></dependency>

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

<dependency>    <groupId>org.postgresql</groupId>    <artifactId>postgresql</artifactId>    <version>${postgresql.version}</version></dependency>

Конфигурация по умолчанию

Как упоминалось ранее, интеграция Spring Boot со Spring Data JPA обеспечивает обширную конфигурацию по умолчанию и добавляет большинство необходимых зависимостей в ваш проект.Она включает в себя:

  • зависимость отпуласоединенийHikariCPи базовой конфигурации по умолчанию.Вы можете установить все параметры конфигурации HikariCP вфайлеapplication.properties, добавив префиксspring.datasource.hikariк имени параметра.

  • создание базы данных в памяти H2, HSQL или Derby, если ваш путь к классам содержит соответствующий драйвер JDBC.

  • зависимость от Hibernate в качестве вашей реализации JPA и требуемую конфигурацию для создания экземпляраEntityManagerFactory.

  • зависимость и необходимая конфигурация для управления вашими транзакциями с помощью встроенного менеджера транзакций Atomikos.

  • необходимая конфигурация для использования репозиториев Spring Data JPA.

Примечание: поскольку Spring Data JPA использует Hibernate в качестве реализации JPA по-умолчанию, вы можете использоватьвсе, что вы знаете о Hibernate, со Spring Data JPA.

Как видите, это в основном все, что вам ранее приходилось настраивать в своем классе конфигурации.Вот почему большинство проектов предпочитают использовать Spring Boot классическому Spring.

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

Большинство корпоративных приложений используют автономную базу данных, например,сервер базы данныхPostgreSQLили Oracle.В этом случае вам нужно только предоставить URL-адрес, имя пользователя и пароль для подключения к этой базе данных.Вы можете сделать это, установив следующие 3 свойства конфигурации вфайлеapplication.properties.

spring.datasource.url=jdbc:postgresql://localhost:5432/testspring.datasource.username=postgresspring.datasource.password=postgres

Настройка конфигурации по умолчанию

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

Использование другого пула подключений

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

<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-data-jpa</artifactId>    <exclusions>        <exclusion>            <groupId>com.zaxxer</groupId>            <artifactId>HikariCP</artifactId>        </exclusion>    </exclusions></dependency>

Затем Spring Boot пытается найти следующие реализации пула соединений в описанном порядке в пути к классам и использует первую из найденных:

  • Пул соединений Tomcat,

  • Commons DBCP2,

  • Oracle UCP.

Если вы не хотите полагаться на сканирование пути к классам вашего приложения, вы также можете явно указать пул соединений, настроив свойство spring.datasource.type.

spring.datasource.type=org.apache.tomcat.jdbc.pool.DataSource

После того, каквы изменили пул соединений, вы можете установить все свои стандартные параметры конфигурации вapplication.propertiesфайл путем добавления префикса spring.datasource.tomcat, spring.datasource.dbcp2 или spring.datasource.oracleucp с именем параметра.

Использование Bitronix Transaction Manager

В прошлом Bitronix был популярным менеджером транзакций в приложениях Spring.Поддержка Spring Boot для него устарела и будет удалена в будущем.

Если вы все еще хотите его использовать, вы можете добавить в свое приложение зависимость отspring-boot-starter-jta-bitronix.Затем Spring Boot будет включать все необходимые зависимости и настраивать Bitronix для управления вашими транзакциями.

Деактивация репозиториев Spring Data JPA

Я рекомендую использовать репозитории Spring Data JPA.Они значительно упрощают реализацию вашей персистентности, предоставляя набор стандартных методов для сохранения, чтения и удаления сущностей.Они также предоставляют такие функции, какпроизводныеинастраиваемые запросы.

Если вы решите не использовать эти функции, вы можете деактивировать все репозитории JPA в своей конфигурации.

spring.data.jpa.repositories.enabled=false

Дополнительный параметр конфигурации, который вы должны знать

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

Настройка ведения журнала

Как объясняется в Руководстве по ведению журнала Hibernate рекомендовано использовать 2 разные конфигурации ведения журнала для среды разработки и рабочей среды.Это, конечно, меняется не при настройке Spring Data JPA.Используя Spring Boot, вы можете настроить уровни журнала для всех категорий Hibernate вфайлеapplication.properties, добавив префиксlogging.levelк имени категории журнала.

Конфигурация среды разработки

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

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

logging.level.org.hibernate=INFOlogging.level.org.hibernate.SQL=DEBUGlogging.level.org.hibernate.cache=DEBUGlogging.level.org.hibernate.stat=DEBUG

Она активирует статистический компонент Hibernate.Он предоставляет вам сводную информацию о количестве и времени, затраченном Hibernate на выполнение наиболее важных операций во время каждого сеанса.Он также добавляет все выполненные операторы SQL в файл журнала и показывает, как Hibernate использовал кеш 2-го уровня.

Spring Boot также поддерживает параметр spring.jpa.show-sql, чтобы включить ведение журнала операторов SQL.Но вам лучше избегать этого, по2тому что он игнорирует вашу структуру ведения журнала и записывает операторы SQL непосредственно в стандартный вывод.

Конфигурация рабочей среды

В рабочей среде вы должны установить уровень журнала Hibernate на ERROR, чтобы сократить накладные расходы как можно меньше.

logging.level.org.hibernate=ERROR

Настройка свойств JPA и Hibernate

Когда вы используете JPA и Hibernate без Spring Data JPA, вы обычно настраиваете его с помощьюфайлаpersistence.xmlэлементе свойствэтого XML-файла вы можете указать параметры конфигурации, зависящие от поставщика.

Вы можете установить все эти параметры вфайлеapplication.properties, добавив префиксspring.jpa.propertiesк имени свойства конфигурации.

spring.jpa.properties.hibernate.generate_statistics=true

Настройка создания базы данных

По умолчанию Spring Boot автоматически создает для вас базы данных в памяти.Эта функция отключена для всех остальных баз данных.Вы можете активировать ее, установив для свойстваspring.jpa.hibernate.ddl-auto значения: none,validate,updateилиcreate-drop.

Рекомендуетсявместо этогоиспользоватьSpring Boot's Flyway или интеграцию с Liquibase.Они более мощные и дают вам полный контроль над определением структур ваших таблиц.

Вывод

Spring Boot стартер для Spring Data JPA добавляет в ваше приложение наиболее распространенные зависимости и разумную конфигурацию по умолчанию.Единственное, что вам нужно добавить, это информацию о подключении к вашей базе данных.

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

Подробнее..
Категории: Java , Spring boot , Spring data jpa

Перевод Изучение и анализ Spring Boot приложения с помощью Actuator и jq

10.06.2021 10:07:14 | Автор: admin

Spring Boot Actuator помогает нам отслеживать и управлять нашими приложениями в производственной среде.Он предоставляет конечные точки, которые публикуются показатели работоспособности и другая информация о запущенном приложении.Мы также можем использовать его для изменения уровня ведения журнала приложения, создания дампа потока и т. д. - короче говоря, возможностей, которые упрощают работу в производственной среде.

Хотя Actuator в основном используется в производственной среде, он также может помочь нам во время разработки и сопровождения.Мы можем использовать его для изучения и анализа нового приложения Spring Boot.

В этой статье мы увидим, как использовать некоторые из его конечных точек для изучения нового приложения, с которым мы не знакомы.Мы будем работать в командной строке и использоватьcurl иjq, с изящным и мощным JSON процессором командной строки.

Пример кода

Эта статья сопровождается примером рабочего кодана GitHub.

Зачем использовать Actuator для анализа и изучения приложения?

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

Хотя это важные шаги, они дают нам только статичное представление о приложении.Мы не можем получить полную картину, не понимая, что происходит во время выполнения.Например, что представляют собой все создаваемые Spring Beans?Какие конечные точки API доступны?Каковы все фильтры, через которые проходит запрос?

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

Общий обзор Spring Boot Actuator

Начнем с краткого обзора по Spring Boot Actuator.

На верхнем уровне, когда мы работаем с Actuator, мы делаем следующие шаги:

  1. Добавляем Actuator как зависимость к нашему проекту

  2. Включаем и открываем конечные точки

  3. Защищаем и настраиваем конечные точки

Давайте кратко рассмотрим каждый из этих шагов.

Шаг 1. Добавьте Actuator зависимость

Добавление Actuator в наш проект похоже на добавление любой другой зависимости.Вот фрагмент для Mavenpom.xml:

<dependencies>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-actuator</artifactId>    </dependency></dependencies>

Если бы мы использовали Gradle, мы бы добавили вфайл build.gradle следующийфрагмент:

dependencies {    implementation 'org.springframework.boot:spring-boot-starter-actuator'}

Простое добавление указанной выше зависимости в приложение Spring Boot предоставляет некоторые готовые конечные точки, такие как/actuator/health, которые могут использоваться, например, для поверхностной проверки работоспособности с помощью балансировщика нагрузки.

$ curl http://localhost:8080/actuator/health{"status":"UP"}

Мы можем перейти на конечную точку/actuator, чтобы просмотреть другие конечные точки, доступные по умолчанию.Конечная точка /actuator открывает обзорную страницу со всеми доступными конечными точками:

$ curl http://localhost:8080/actuator{"_links":{"self":{"href":"http://localhost:8080/actuator","templated":false},"health":{"href":"http://localhost:8080/actuator/health","templated":false},"health-path":{"href":"http://localhost:8080/actuator/health/{*path}","templated":true},"info":{"href":"http://localhost:8080/actuator/info","templated":false}}}

Шаг 2. Включите и откройте конечные точки

Конечные точки идентифицируются идентификаторами,такими как health, info, metrics и так далее.Включение и открытие конечной точки делает ее доступной для использования попути /actuator URL-адреса приложения, напримерhttp://your-service.com/actuator/health,http://your-service.com/actuator/metrics и т. д.

Большинство конечных точек, за исключениемshutdown, включены по умолчанию.Мы можем отключить конечную точку, установив для свойства management.endpoint.<id>.enabled значениеfalseвapplication.propertiesфайле.Например, вот как мы отключимmetrics конечную точку:

management.endpoint.metrics.enabled=false

Доступ к отключенной конечной точке возвращает ошибку HTTP 404:

$ curl http://localhost:8080/actuator/metrics{"timestamp":"2021-04-24T12:55:40.688+00:00","status":404,"error":"Not Found","message":"","path":"/actuator/metrics"}

Мы можем предоставить доступ к конечным точкам через HTTP и / или JMX.Хотя обычно используется HTTP, для некоторых приложений может быть предпочтительнее JMX.

Мы можем раскрыть конечные точки, установивmanagement.endpoints.[web|jmx].exposure.include для списка идентификаторов конечных точек, которые мы хотим раскрыть.Вот как мы можем открытьmetrics конечную точку, например:

management.endpoints.web.exposure.include=metrics

Чтобы конечная точка была доступна, она должна быть включена и доступна.

Шаг 3. Защитите и настройте конечные точки

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

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

Краткое введение вjq

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

Предположим, у нас в файле есть следующий JSONsample.json:

{  "students": [    {      "name": "John",      "age": 10,      "grade": 3,      "subjects": ["math", "english"]          },    {      "name": "Jack",      "age": 10,      "grade": 3,      "subjects": ["math", "social science", "painting"]    },    {      "name": "James",      "age": 11,      "grade": 5,      "subjects": ["math", "environmental science", "english"]    },    .... other student objects omitted ...  ]}

Это объект, содержащий массив объектов student с некоторыми деталями для каждого ученика.

Давайте рассмотрим несколько примеров обработки и преобразования этого JSON с помощьюjq.

$ cat sample.json | jq '.students[] | .name'"John""Jack""James"

Рассмотримjq команду, чтобы понять, что происходит:

Выражение

Эффект

.students[]

перебиратьмассив students

|

вывод каждогоstudent для следующего фильтра

.name

выборка атрибутаnameиз объектаstudent

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

$ cat sample.json | jq '.students[] | select(.subjects[] | contains("science"))'{  "name": "Jack",  "age": 10,  "grade": 3,  "subjects": [    "math",    "social science",    "painting"  ]}{  "name": "James",  "age": 11,  "grade": 5,  "subjects": [    "math",    "environmental science",    "english"  ]}

Рассмотримкоманду еще раз:

Выражение

Эффект

.students[]

перебирать массивstudents

|

вывод каждогоstudent для следующего фильтра

select(.subjects[] | contains("science"))

выберите студента, если его массивsubjects содержит элемент со строкой наука

С одним небольшим изменением мы можем снова собрать эти элементы в массив:

$ cat sample.json | jq '[.students[] | select(.subjects[] | contains("science"))]'[  {    "name": "Jack",    "age": 10,    "grade": 3,    "subjects": [      "math",      "social science",      "painting"    ]  },  {    "name": "James",    "age": 11,    "grade": 5,    "subjects": [      "math",      "environmental science",      "english"    ]  }]

Все, что нам нужно было сделать, это заключить все выражение в квадратные скобки.

Мы можем использоватьjq как для фильтрации, так и для изменения формы JSON:

$ cat sample.json | jq '[.students[] | {"studentName": .name, "favoriteSubject": .subjects[0]}]'[  {    "studentName": "John",    "favoriteSubject": "math"  },  {    "studentName": "Jack",    "favoriteSubject": "math"  },  {    "studentName": "James",    "favoriteSubject": "math"  }]

Мы, выполнив итерацию по массивуstudents, создали новый объект,содержащий свойствоstudentName иfavoriteSubject со значениями устанавленными из атрибутаname и первогоsubject из исходногообъекта student .В результате мы собрали все новые объекты в массив.

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

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

Изучение приложения Spring Boot

В оставшейся части этой статьи мы будем использовать Actuator для изучения работающего приложения Spring Boot.Само приложение представляет собой очень упрощенный пример приложения для обработки заказов электронной коммерции.В нем есть только скелетный код, необходимый для иллюстрации идей.

Хотя доступно множество конечных точек Actuator, мы сосредоточимся только на тех, которые помогают нам понять структуру приложения во время выполнения.

се конечные точки, которые мы увидим, включены по умолчанию. Давйте откроем их:

management.endpoints.web.exposure.include=mappings,beans,startup,env,scheduledtasks,caches,metrics

Использование конечной точки отображения

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

Давайте перейдем на конечную точку с помощью команды curl и направим ответ в jq, чтобы красиво его распечатать:

$ curl http://localhost:8080/actuator/mappings | jq

Вот ответ:

{  "contexts": {    "application": {      "mappings": {        "dispatcherServlets": {          "dispatcherServlet": [            {              "handler": "Actuator web endpoint 'metrics'",              "predicate": "{GET [/actuator/metrics], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}",              "details": {                "handlerMethod": {                  "className": "org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler",                  "name": "handle",                  "descriptor": "(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;"                },                "requestMappingConditions": {                  ... properties omitted ...                  ],                  "params": [],                  "patterns": [                    "/actuator/metrics"                  ],                  "produces": [                    ... properties omitted ...                  ]                }              }            },          ... 20+ more handlers omitted ...          ]        },        "servletFilters": [          {            "servletNameMappings": [],            "urlPatternMappings": [              "/*"            ],            "name": "webMvcMetricsFilter",            "className": "org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter"          },          ... other filters omitted ...        ],        "servlets": [          {            "mappings": [              "/"            ],            "name": "dispatcherServlet",            "className": "org.springframework.web.servlet.DispatcherServlet"          }        ]      },      "parentId": null    }  }}

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

jq для дальнейшей фильтрации этой информации. Поскольку мы знаем имена пакетов из нашей службы, jq будет выбирать только те обработчики, которые содержат имя нашего пакета io.reflectoring.springboot.actuator:

Давайте воспользуемся jq для дальнейшей фильтрации этой информации. Поскольку мы знаем имена пакетов из нашей службы, jqselectбудет выбирать только те обработчики, которые содержат имя нашего пакета io.reflectoring.springboot.actuator:

$ curl http://localhost:8080/actuator/mappings | jq '.contexts.application.mappings.dispatcherServlets.dispatcherServlet[] | select(.handler | contains("io.reflectoring.springboot.actuator"))'{  "handler": "io.reflectoring.springboot.actuator.controllers.PaymentController#processPayments(String, PaymentRequest)",  "predicate": "{POST [/{orderId}/payment]}",  "details": {    "handlerMethod": {      "className": "io.reflectoring.springboot.actuator.controllers.PaymentController",      "name": "processPayments",      "descriptor": "(Ljava/lang/String;Lio/reflectoring/springboot/actuator/model/PaymentRequest;)Lio/reflectoring/springboot/actuator/model/PaymentResponse;"    },    "requestMappingConditions": {      "consumes": [],      "headers": [],      "methods": [        "POST"      ],      "params": [],      "patterns": [        "/{orderId}/payment"      ],      "produces": []    }  }}{  "handler": "io.reflectoring.springboot.actuator.controllers.OrderController#getOrders(String)",  "predicate": "{GET [/{customerId}/orders]}",  "details": {    "handlerMethod": {      "className": "io.reflectoring.springboot.actuator.controllers.OrderController",      "name": "getOrders",      "descriptor": "(Ljava/lang/String;)Ljava/util/List;"    },    "requestMappingConditions": {      "consumes": [],      "headers": [],      "methods": [        "GET"      ],      "params": [],      "patterns": [        "/{customerId}/orders"      ],      "produces": []    }  }}{  "handler": "io.reflectoring.springboot.actuator.controllers.OrderController#placeOrder(String, Order)",  "predicate": "{POST [/{customerId}/orders]}",  "details": {    "handlerMethod": {      "className": "io.reflectoring.springboot.actuator.controllers.OrderController",      "name": "placeOrder",      "descriptor": "(Ljava/lang/String;Lio/reflectoring/springboot/actuator/model/Order;)Lio/reflectoring/springboot/actuator/model/OrderCreatedResponse;"    },    "requestMappingConditions": {      "consumes": [],      "headers": [],      "methods": [        "POST"      ],      "params": [],      "patterns": [        "/{customerId}/orders"      ],      "produces": []    }  }}

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

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

$ curl http://localhost:8080/actuator/mappings | jq '.contexts.application.mappings.servletFilters'[  {    "servletNameMappings": [],    "urlPatternMappings": [      "/*"    ],    "name": "webMvcMetricsFilter",    "className": "org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter"  },  ... other filters omitted ...]

Использование конечной точкиbeans

Теперь давайте посмотрим на список созданных bean-компонентов:

$ curl http://localhost:8080/actuator/beans | jq{  "contexts": {    "application": {      "beans": {        "endpointCachingOperationInvokerAdvisor": {          "aliases": [],          "scope": "singleton",          "type": "org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor",          "resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.class]",          "dependencies": [            "org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration",            "environment"          ]        },               .... other beans omitted ...    }  }}

Это дает общее представление обо всех компонентах вApplicationContext. Промотр его дает нам некоторое представление о структуре приложения во время выполнения - какие внутренние bean-компоненты Spring, каковы bean-компоненты приложения, каковы их области действия, каковы зависимости каждого bean-компонента и т. д.

Опять же, мы можем использоватьjq для фильтрации ответов и сосредоточиться на тех частях ответа, которые нам интересны:

$ curl http://localhost:8080/actuator/beans | jq '.contexts.application.beans | with_entries(select(.value.type | contains("io.reflectoring.springboot.actuator")))'{  "orderController": {    "aliases": [],    "scope": "singleton",    "type": "io.reflectoring.springboot.actuator.controllers.OrderController",    "resource": "file [/code-examples/spring-boot/spring-boot-actuator/target/classes/io/reflectoring/springboot/actuator/controllers/OrderController.class]",    "dependencies": [      "orderService",      "simpleMeterRegistry"    ]  },  "orderService": {    "aliases": [],    "scope": "singleton",    "type": "io.reflectoring.springboot.actuator.services.OrderService",    "resource": "file [/code-examples/spring-boot/spring-boot-actuator/target/classes/io/reflectoring/springboot/actuator/services/OrderService.class]",    "dependencies": [      "orderRepository"    ]  },  ... other beans omitted ...  "cleanUpAbandonedBaskets": {    "aliases": [],    "scope": "singleton",    "type": "io.reflectoring.springboot.actuator.services.tasks.CleanUpAbandonedBaskets",    "resource": "file [/code-examples/spring-boot/spring-boot-actuator/target/classes/io/reflectoring/springboot/actuator/services/tasks/CleanUpAbandonedBaskets.class]",    "dependencies": []  }}

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

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

Использование конечной точкиstartup

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

SpringApplication app = new SpringApplication(DemoApplication.class);app.setApplicationStartup(new BufferingApplicationStartup(2048));app.run(args);

Здесь мы установили для нашего приложенияApplicationStartup значение a,BufferingApplicationStartup которое является структурой в памяти, которая фиксирует события в сложном процессе запуска Spring.Внутренний буфер будет иметь указанную нами емкость - 2048.

Теперь перейдем конечной точкеstartup.В отличие от других конечных точек startup поддерживаетPOST метод:

$ curl -XPOST 'http://localhost:8080/actuator/startup' | jq{  "springBootVersion": "2.4.4",  "timeline": {    "startTime": "2021-04-24T12:58:06.947320Z",    "events": [      {        "startupStep": {          "name": "spring.boot.application.starting",          "id": 1,          "parentId": 0,          "tags": [            {              "key": "mainApplicationClass",              "value": "io.reflectoring.springboot.actuator.DemoApplication"            }          ]        },        "startTime": "2021-04-24T12:58:06.956665337Z",        "endTime": "2021-04-24T12:58:06.998894390Z",        "duration": "PT0.042229053S"      },      {        "startupStep": {          "name": "spring.boot.application.environment-prepared",          "id": 2,          "parentId": 0,          "tags": []        },        "startTime": "2021-04-24T12:58:07.114646769Z",        "endTime": "2021-04-24T12:58:07.324207009Z",        "duration": "PT0.20956024S"      },         .... other steps omitted ....      {        "startupStep": {          "name": "spring.boot.application.started",          "id": 277,          "parentId": 0,          "tags": []        },        "startTime": "2021-04-24T12:58:11.169267550Z",        "endTime": "2021-04-24T12:58:11.212604248Z",        "duration": "PT0.043336698S"      },      {        "startupStep": {          "name": "spring.boot.application.running",          "id": 278,          "parentId": 0,          "tags": []        },        "startTime": "2021-04-24T12:58:11.213585420Z",        "endTime": "2021-04-24T12:58:11.214002336Z",        "duration": "PT0.000416916S"      }    ]  }}

Ответ представляет собой массив с подробной информацией о событиях: name, startTime, endTimeиduration.

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

Поскольку приведенный выше ответ содержит много деталей, давайте сузим его, отфильтровав поspring.beans.instantiate шагу, а также отсортируем события по продолжительности в порядке убывания:

$ curl -XPOST 'http://localhost:8080/actuator/startup' | jq '.timeline.events | sort_by(.duration) | reverse[] | select(.startupStep.name | contains("instantiate"))'$ 

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

$ curl -XPOST 'http://localhost:8080/actuator/startup' | jq '[.timeline.events | sort_by(.duration) | reverse[] | select(.startupStep.name | contains("instantiate")) | {beanName: .startupStep.tags[0].value, duration: .duration}]' [  {    "beanName": "orderController",    "duration": "PT1.010878035S"  },  {    "beanName": "orderService",    "duration": "PT1.005529559S"  },  {    "beanName": "requestMappingHandlerAdapter",    "duration": "PT0.11549366S"  },  {    "beanName": "tomcatServletWebServerFactory",    "duration": "PT0.108340094S"  },  ... other beans omitted ...]

Таким образом, на созданиеbean-компонентовorderController иуходит больше секундыorderService!Это интересно - теперь у нас есть конкретная область приложения, на которой мы можем сосредоточиться, чтобы понять больше.

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

jq '[.timeline.events \  | sort_by(.duration) \  | reverse[] \  | select(.startupStep.name \  | contains("instantiate")) \  | {beanName: .startupStep.tags[0].value, duration: .duration}]'

Выражение

Эффект

.timeline.events | sort_by(.duration) | reverse

отсортируйтемассив timeline.events по свойству duration и реверсируйте результат, чтобы он был отсортирован в порядке убывания

[]

перебирать результирующий массив

select(.startupStep.name | contains("instantiate"))

выберите элемент объекта только в том случае, еслисвойствоstartupStep элементаname содержит текст instantiate

{beanName: .startupStep.tags[0].value, duration: .duration}

создать новый объект JSON со свойствами beanName и duration

Скобки над всем выражением указывают на то, что мы хотим собрать все созданные объекты JSON в массив.

Использование конечной точкиenv

Конечная точка env дает обобщенное представление всех свойств конфигурации приложения.Сюда входят конфигурации изapplication.properties файла, системные свойства JVM, переменные среды и т. д.

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

$ curl http://localhost:8080/actuator/env | jq{  "activeProfiles": [],  "propertySources": [    {      "name": "server.ports",      "properties": {        "local.server.port": {          "value": 8080        }      }    },    {      "name": "servletContextInitParams",      "properties": {}    },    {      "name": "systemProperties",      "properties": {        "gopherProxySet": {          "value": "false"        },        "java.class.path": {          "value": "/target/test-classes:/target/classes:/Users/reflectoring/.m2/repository/org/springframework/boot/spring-boot-starter-actuator/2.4.4/spring-boot-starter-actuator-2.4.4.jar:/Users/reflectoring/.m2/repository/org/springframework/boot/spring-boot-starter/2.4.4/spring-boot-starter-2.4.4.jar: ... other jars omitted ... "        },       ... other properties omitted ...      }    },    {      "name": "systemEnvironment",      "properties": {        "USER": {          "value": "reflectoring",          "origin": "System Environment Property \"USER\""        },        "HOME": {          "value": "/Users/reflectoring",          "origin": "System Environment Property \"HOME\""        }        ... other environment variables omitted ...      }    },    {      "name": "Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'",      "properties": {        "management.endpoint.logfile.enabled": {          "value": "true",          "origin": "class path resource [application.properties] - 2:37"        },        "management.endpoints.web.exposure.include": {          "value": "metrics,beans,mappings,startup,env, info,loggers",          "origin": "class path resource [application.properties] - 5:43"        }      }    }  ]}

Использование конечной точкиscheduledtasks

Эта конечная точка позволяет нам проверять, выполняет ли приложение какую-либо задачу периодически, используя@Scheduled аннотациюSpring:

$ curl http://localhost:8080/actuator/scheduledtasks | jq{  "cron": [    {      "runnable": {        "target": "io.reflectoring.springboot.actuator.services.tasks.ReportGenerator.generateReports"      },      "expression": "0 0 12 * * *"    }  ],  "fixedDelay": [    {      "runnable": {        "target": "io.reflectoring.springboot.actuator.services.tasks.CleanUpAbandonedBaskets.process"      },      "initialDelay": 0,      "interval": 900000    }  ],  "fixedRate": [],  "custom": []}

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

Использование конечной точкиcaches

Эта конечная точка перечисляет все кэши приложений:

$ curl http://localhost:8080/actuator/caches | jq{  "cacheManagers": {    "cacheManager": {      "caches": {        "states": {          "target": "java.util.concurrent.ConcurrentHashMap"        },        "shippingPrice": {          "target": "java.util.concurrent.ConcurrentHashMap"        }      }    }  }}

Мы можем сказать,что приложение кэширует некоторые данные: states и shippingPrice. Это дает нам еще одну область приложения, которую нужно изучить и узнать больше: как создаются кеши, когда удаляются записи кеша и т. д.

Использование конечной точкиhealth

Конечная точка health показывает информацию оздоровье приложения:

$ curl http://localhost:8080/actuator/health{"status":"UP"}

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

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

Прочтитеэтустатьюна Reflectoring,чтобы узнать больше о реализации проверок работоспособности с помощью Actuator.

Использование конечной точкиmetrics

Эта конечная точка перечисляет все метрики, сгенерированные приложением:

$ curl http://localhost:8080/actuator/metrics | jq{  "names": [    "http.server.requests",    "jvm.buffer.count",    "jvm.buffer.memory.used",    "jvm.buffer.total.capacity",    "jvm.threads.states",    "logback.events",    "orders.placed.counter",    "process.cpu.usage",    ... other metrics omitted ...  ]}

Затем мы можем получить данные отдельных показателей:

$ curl http://localhost:8080/actuator/metrics/jvm.memory.used | jq{  "name": "jvm.memory.used",  "description": "The amount of used memory",  "baseUnit": "bytes",  "measurements": [    {      "statistic": "VALUE",      "value": 148044128    }  ],  "availableTags": [    {      "tag": "area",      "values": [        "heap",        "nonheap"      ]    },    {      "tag": "id",      "values": [        "CodeHeap 'profiled nmethods'",        "G1 Old Gen",                ... other tags omitted ...      ]    }  ]}

Особенно полезна проверка доступных пользовательских метрик API.Это может дать нам некоторое представление о том, что важно в этом приложении с точки зрения бизнеса.Например, из списка показателей мы можем видеть, что есть индикатор, orders.placed.counter который, вероятно, сообщает нам, сколько заказов было размещено за определенный период времени.

Заключение

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

Вы можете поиграть с полным приложением, иллюстрирующим эти идеи, используя кодна GitHub.

Подробнее..
Категории: Java , Spring boot , Actuator

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru