Tune ITTune IT2024-03-19T07:40:10Z2024-03-19T07:40:10ZIntellij idea - как сделать, чтобы файлы открывались всегда в новой вкладке.Marina Pashninahttps://www.tune-it.ru/c/blogs/find_entry?entryId=197467722024-03-01T15:19:27Z2024-03-01T15:09:00Z<p>В Intellij idea появилась новая интерактивная логика вкладок - разработчики системы предложили новый подход к навигации и редактированию файлов. Теперь все файлы могут открываться в двух разных режимах: обычный и режим предварительного просмотра (preview tab). И если в обычном режиме все работает стандартно, то режим предварительного просмотра открывает все следующие файлы повторно в той же вкладке.</p>
<p>Этот инновационный подход к управлению файлами предоставляет пользователям больший контроль и гибкость при навигации по нескольким файлам в рабочем пространстве. Используя визуальные подсказки, такие как стили шрифтов, для обозначения статуса файла, пользователи могут легко отличать файлы, открытые на текущей вкладке, от тех, которые откроются на новой вкладке.</p>
<p>Таким образом Файл, открытый с использованием CTRL + ЛКМ и отображаемый обычным латинским шрифтом, находится в обычном режиме. При переходе между файлами с помощью этой комбинации клавиш следующий файл открывается в соседней вкладке. </p>
<p>С другой стороны, если название файла отображается курсивом, это означает, что файл открыт в режиме предварительного просмотра (preview tab). При использовании CTRL + ЛКМ для переключения между файлами новый файл также откроется в текущей вкладке.</p>
<p>Вот так это выглядит в самой среде разработки:<br />
Слева - обычный режим, справа (курсивом) - режим предварительного просмотра.<br />
</p>
<p><b id="docs-internal-guid-f7d9d338-7fff-335b-1f0a-4592cda9f2d4"><img height="120" src="https://lh7-us.googleusercontent.com/2ytRy-AiPWG2aC5JM-ko-ujhNuezfLR_06mmDKi2alhWZC9PtdtI3o3FonUcmBpZnk5r3sI9q1ouaLLQvJbuIcG400HQSI1m9-8h2neg9P5ijCHV3RO-2zojmOKyW_oqX2xUkpFOZumL9aiYAp1SAnA" width="624" /></b></p>
<p>В целом, новая интерактивная логика вкладок, представленная в этом тесте, предлагает удобный и интуитивно понятный способ оптимизации процессов навигации и редактирования файлов. Используя сочетания клавиш и визуальные подсказки, пользователи могут повысить эффективность и производительность рабочего процесса при одновременной работе с несколькими файлами.</p>
<p>По крайней мере так думают разработчики. Только на самом деле это оказалось абсолютно неудобно! </p>
<p>Если вы, как и я, считаете, что новый подход к навигации и редактированию файлов неудобен, есть возможность отключить этот режим. Для этого вам нужно будет выполнить определенные действия в настройках вашей среды разработки. <br />
</p>
<h1>Как отключить режим предварительного просмотра в вашей среде разработки:</h1>
<p>В современных средах разработки, таких как IDE или текстовых редакторах, предварительный просмотр файлов может быть полезным инструментом. Однако, есть случаи, когда пользователи предпочитают работать без этой функции. Если вы хотите отключить режим предварительного просмотра в вашей среде разработки, следуйте этим простым шагам.<br />
</p>
<p>1. Откройте настройки (Settings) вашей среды разработки.<br />
2. Перейдите в раздел Editor - Editor tabs.<br />
3. Найдите опцию "Opening policy".<br />
4. В открывшемся окне отключите опцию "preview tab".<br />
</p>
<p>После выполнения этих действий режим предварительного просмотра будет отключен, и вы сможете работать в вашей среде разработки без этой функции. Не забудьте сохранить изменения, чтобы они вступили в силу.<br />
</p>
<p><b id="docs-internal-guid-f8048580-7fff-13a6-0907-efe98615045c"><img height="121" src="https://lh7-us.googleusercontent.com/QpQBWm9pQ4fw9U6_Mv1ggNyrz8llqPKX1gOIn39rmvYOggT83cyHoswVxoXx0v7z76-iY5OxE_na3kzCkBdu-raF-UJfYQwfTuemk_XdbWz36KTENdhq8diWWlrfeBouxHr79kTzUTACe7uP_WlgHwA" width="624" /></b></p>
<p>Вот тут:</p>
<p><b id="docs-internal-guid-2dd73cdd-7fff-5373-54ba-fedfffec51c7"><img height="463" src="https://lh7-us.googleusercontent.com/k_tEjnlw29zHDjuFUgv0kJMwyeorDuGnx1eLi3dezzyyfZvBmUrO_UGnFO2RggSuqU5Gp1uCUL2Xao-4J5LpsP9KQ2u5C7Zm5QV0sHuORbVgAFxbGUSuwEXc8P3bNzFE4DgcWAkNvRWO7HVuZxMLRAU" width="624" /></b></p>
<p> </p>
<p><br />
Теперь вы знаете, как отключить режим предварительного просмотра в вашей среде разработки. Надеемся, что эта информация будет полезной для вас при работе над вашими проектами.<br />
</p>Marina Pashnina2024-03-01T15:09:00ZКомплексная сортировка в Kotlin: Смешанный порядок сортировкиRomo Fedoroffhttps://www.tune-it.ru/c/blogs/find_entry?entryId=197461052024-03-01T12:16:40Z2024-03-01T12:05:00Z<style type="text/css">article p {
font-size:11pt;
font-family:Verdana, sans-serif;
text-align:justify;
color:#6a6a6a;
}
article img {
width: 90%;
}
.centered {
text-align:center;
}
</style>
<p>Представьте, что у вас есть список идентификационных номеров или номеров продуктов в формате "A-123", где каждый идентификатор состоит из буквы и цифры. Теперь предположим, что вам нужно отсортировать эти идентификаторы таким образом, чтобы алфавитная часть была упорядочена по возрастанию, а числовая - по убыванию. Как бы вы решили эту задачу на языке Kotlin?</p>
<p>В этой статье мы рассмотрим, как добиться желаемых результатов сортировки, используя эффективные возможности языка Kotlin.</p>
<p> </p>
<p class="centered"><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/sorting-2.jpg/b1c6c0a4-9643-f64a-150f-19cce8034959?imagePreview=1" /></p>
<p>Оригинал статьи: Advanced Sorting Techniques in Kotlin: Mixed Ascending and Descending Order by Michihiro Iwasaki</p>
<p> </p>
<p><strong>Для начала рассмотрим подход, который не дает ожидаемого результата сортировки.</strong></p>
<p>Предположим, что исходно у нас имеется следующий список:</p>
<pre class="brush:java;">
val itemList = listOf(
"A-123",
"B-032",
"A-025",
"C-112",
"C-054",
"A-372",
"B-004",
"C-010",
"B-538"
)</pre>
<p>И нам нужно отсортировать этот список следующим образом:</p>
<p>1) Отсортировать алфавитную часть по возрастанию.</p>
<p>2) Отсортировать числовую часть по убыванию.</p>
<pre class="brush:java;">
val expectedList = listOf(
"A-372",
"A-123",
"A-025",
"B-538",
"B-032",
"B-004",
"C-112",
"C-054",
"C-010"
)</pre>
<p>Сортировка только по возрастанию или убыванию не представляет сложности. Например, сортировка по первому символу (алфавиту) в порядке возрастания может быть выполнена следующим образом:</p>
<pre class="brush:java;">
fun main() {
val sortedList = itemList.sortedBy {
it.first()
}
for (item in sortedList) {
println(item)
}
/**
* Вывод:
*
* A-123
* A-025
* A-372
* B-032
* B-004
* B-538
* C-112
* C-054
* C-010
*
*/
}
</pre>
<p>Функция <strong>sortedBy </strong>правильно сортирует алфавитную часть. Однако ей не удается отсортировать числовую часть в порядке убывания. Попытка добавить сортировку по убыванию для чисел приводит к неожиданному результату:</p>
<pre class="brush:java;">
fun main() {
val sortedList = itemList.sortedBy {
it.first()
}.sortedByDescending {
it.slice(2..4)
}
for (item in sortedList) {
println(item)
}
/**
* Вывод:
*
* B-538
* A-372
* A-123
* C-112
* C-054
* B-032
* A-025
* C-010
* B-004
*
*/
}</pre>
<p>Эта проблема возникает потому, что вторая операция сортировки отменяет первую, нарушая первоначальный алфавитный порядок сортировки.</p>
<p> </p>
<p><strong>Давайте теперь рассмотрим подход, который успешно достигает нужного нам результата.</strong></p>
<p>Чтобы одновременно применить условия сортировки по возрастанию и убыванию, можно использовать функцию<strong> sortedWith()</strong>. В этой функции для первичного условия сортировки используется compareBy() или compareByDescending(), а для вторичного - thenBy() или thenByDescending().</p>
<p>Вот пример кода, который позволяет достичь желаемого результата:</p>
<pre class="brush:java;">
val itemList = listOf(
"A-123",
"B-032",
"A-025",
"C-112",
"C-054",
"A-372",
"B-004",
"C-010",
"B-538"
)
val expectedList = listOf(
"A-372",
"A-123",
"A-025",
"B-538",
"B-032",
"B-004",
"C-112",
"C-054",
"C-010"
)
fun main() {
val sortedList = itemList.sortedWith(
compareBy<String> {
it.first()
}.thenByDescending {
it.slice(2..4)
}
)
for (item in sortedList) {
println(item)
}
/**
* Вывод:
*
* A-372
* A-123
* A-025
* B-538
* B-032
* B-004
* C-112
* C-054
* C-010
*
*/
println(expectedList == sortedList) // Вывод: true
}</pre>
<p>Отлично! Мы успешно получили ожидаемый результат.</p>
<p>Однако важно явно указывать тип в compareBy() или compareByDescending(). Невыполнение этого требования может привести к ошибке.</p>
<p>Например:</p>
<pre class="brush:java;">
val sortedList = itemList.sortedWith(
compareBy { // Этот код приведет к ошибке
it.first()
}.thenByDescending {
it.slice(2..4)
}
)</pre>
<p> </p>
<p><strong>Давайте теперь рассмотрим как реализовать аналогичную сортировку для списка объектов.</strong></p>
<p>Подход к применению нескольких условий сортировки к списку объектов данных аналогичен тому, что используется при работе с обычными списками.</p>
<p>Рассмотрим пример класса данных и соответствующего списка:</p>
<pre class="brush:java;">
data class Item (
val group: String,
val num: Int
)
val itemList = listOf(
Item("A", 123),
Item("B", 32),
Item("A", 25),
Item("C", 112),
Item("C", 54),
Item("A", 372),
Item("B", 4),
Item("C", 10),
Item("B", 538),
)</pre>
<p>На этот раз отсортируем список в порядке убывания по алфавиту (group) и в порядке возрастания по числам (num). Для алфавитной части используем compareByDescending(), а для числовой части - thenBy().</p>
<p>Ниже приведен код и результат выполнения этой сортировки:</p>
<pre class="brush:java;">
fun main() {
val sortedList = itemList.sortedWith(
compareByDescending<Item> {
it.group
}.thenBy {
it.num
}
)
for (item in sortedList) {
println("${item.group}: ${item.num}")
}
/**
* Вывод:
*
* C: 10
* C: 54
* C: 112
* B: 4
* B: 32
* B: 538
* A: 25
* A: 123
* A: 372
*/
}</pre>
<p>Превосходно! Мы успешно справились и со списком объектов.</p>
<p> </p>
<p><strong>Конец статьи.</strong></p>Romo Fedoroff2024-03-01T12:05:00ZПовышение эффективности Spring Boot REST API приложения с помощью сжатия GzipRomo Fedoroffhttps://www.tune-it.ru/c/blogs/find_entry?entryId=196679052024-02-26T10:08:30Z2024-02-26T09:59:00Z<style type="text/css">article p {
font-size:11pt;
font-family:Verdana, sans-serif;
text-align:justify;
color:#6a6a6a;
}
article img {
width: 90%;
}
.centered {
text-align:center;
}
</style>
<p class="centered"><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/gzip2.jpg/b5ce7d5b-ed67-db90-f403-d5a3dbb25f7e?imagePreview=1" /></p>
<p>Статья вдохновлена трудом "Supercharge Your Spring Boot REST API with Gzip Compression" автора JackyNote.</p>
<h2>Немного о сжатии Gzip</h2>
<p>Gzip-сжатие - это популярный метод сжатия данных, который позволяет уменьшить размер данных, передаваемых через Интернет. Он достигается за счет сжатия данных в более компактный формат, что приводит к ускорению передачи данных и снижению потребления полосы пропускания. Gzip - это алгоритм сжатия без потерь, что означает, что данные могут быть распакованы без потери качества.</p>
<h2>Зачем использовать сжатие Gzip в вашем Spring Boot REST API приложении?</h2>
<p>1. Улучшенное время отклика: Благодаря меньшему объему данных клиенты быстрее получают ответы, что обеспечивает более высокую скорость отклика приложения.</p>
<p>2. Экономия полосы пропускания: Gzip-сжатие снижает потребление полосы пропускания, что приводит к экономии средств и обеспечивает более плавное взаимодействие с пользователями.</p>
<p>3. Сокращение задержек: Благодаря меньшему количеству передаваемых данных уменьшается задержка API, что приводит к повышению эффективности работы приложения и удовлетворенности пользователей.</p>
<p>4. Улучшенная масштабируемость: Меньшая полезная нагрузка означает, что ваш API может обрабатывать больше запросов без существенного увеличения ресурсов сервера, что повышает масштабируемость.</p>
<p>5. Улучшение SEO: Ускоренная загрузка API может положительно сказаться на рейтинге вашего сайта в поисковых системах. Это может косвенно повлиять на ваш Spring Boot REST API, особенно если он является частью более крупного веб-приложения.</p>
<h2>Как реализовать Gzip-сжатие в Spring Boot REST API?</h2>
<p>Реализация Gzip-сжатия в Spring Boot REST API проста и может быть выполнена в несколько шагов:</p>
<p>1) <strong>Добавьте зависимость Gzip:</strong> Чтобы использовать сжатие Gzip в вашем приложении Spring Boot, вам нужно добавить зависимость Gzip.</p>
<p>Откройте ваш pom.xml и включите в него следующее:</p>
<pre class="brush:xml;">
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency></pre>
<p>2) <strong>Включите сжатие Gzip:</strong> В конфигурации вашего приложения Spring Boot включите сжатие Gzip.</p>
<p>Вы можете сделать это в файле application.properties или application.yml:</p>
<pre class="brush:jscript;">
server.compression.enabled=true
server.compression.min-response-size=2048
server.compression.mime-types=text/html,text/xml,text/plain,text/css,application/json</pre>
<p>К примеру, настройки выше, позволят сжимать ответы размером более 2 КБ для указанных типов MIME.</p>
<p>Чтобы включить сжатие для определенных контроллеров, аннотируйте их с помощью @EnableCompression:</p>
<pre class="brush:java;">
@RestController
@EnableCompression
public class MyController {
// ...
}</pre>
<p>Чтобы сжать ответы для конкретной конечной точки, используйте @EnableCompression для метода обработки запроса:</p>
<pre class="brush:java;">
@RequestMapping("/compress")
@EnableCompression
public String compress() {
return "Response with compression";
}</pre>
<p> </p>
<p>Конец статьи.</p>
<p><strong>Благодарим Вас за чтение.</strong></p>Romo Fedoroff2024-02-26T09:59:00ZВалидация по условию в приложении на KotlinRomo Fedoroffhttps://www.tune-it.ru/c/blogs/find_entry?entryId=196656322024-02-25T20:17:32Z2024-02-25T19:52:00Z<style type="text/css">article p {
font-size:11pt;
font-family:Verdana, sans-serif;
text-align:justify;
color:#6a6a6a;
}
article img {
width: 90%;
}
.centered {
text-align:center;
}
</style>
<p class="centered"><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/valid-1.jpg/b620907a-6735-3eff-e801-6adf1730b9ef?imagePreview=1" /></p>
<p>В этой статье мы рассмотрим как реализовать валидацию модели данных по условию - валидацию, которая зависит от связи между несколькими свойствами объекта.</p>
<p>В качестве примера кодовой базы для этой статьи возьмем существующий проект "Реактивное АПИ онлайн магазина обучающих курсов", который мы создавали с вами в предыдущих статьях: <a href="https://www.tune-it.ru/web/romo/blog/-/blogs/reactive-rest-api-kotlin-webflux-r2dbc" target="_blank">здесь</a> и <a href="https://www.tune-it.ru/web/romo/blog/-/blogs/kotlin-bucket4k-api-rate-limiting" target="_blank">здесь</a>.</p>
<p>И так, приступим.</p>
<p>Предположим, что класс CourseRequest является тем доменным классом, к которому должны быть применены некоторые проверки:</p>
<pre class="brush:java;">
data class CourseRequest(
@field:NotBlank(message = "name should be specified") val name : String,
@field:PositiveOrZero(message = "price must be >= 0") val price : Float,
@field:NotBlank(message = "author should be specified") val author : String,
@field:NotBlank(message = "direction should be specified") val direction : String,
val startDate : LocalDate,
val endDate: LocalDate?=null
)</pre>
<p>Как видно, к некоторым полям класса уже применены готовые проверки из фреймворка, такие как: NotBlank, PositiveOrZero.</p>
<p>Но, что если нам необходимо, чтобы одно из полей класса к примеру было обязательным, только если не заполнено другое и наоборот. Или как быть, если мы хотим, чтобы в зависимости от значения определенного поля изменялся и набор других связанных с ним полей. Стандартными проверками, которые предоставляет нам фреймворк Jakarta Validation, такую задачу не решить.</p>
<p>На помощь приходит создание нашей собственной пользовательской проверки.</p>
<p>Давайте добавим в наш класс два поля <strong>conditional1</strong> и <strong>conditional2</strong>:</p>
<pre class="brush:java;">
data class CourseRequest(
@field:NotBlank(message = "name should be specified") val name : String,
@field:PositiveOrZero(message = "price must be >= 0") val price : Float,
@field:NotBlank(message = "author should be specified") val author : String,
@field:NotBlank(message = "direction should be specified") val direction : String,
val startDate : LocalDate,
val endDate: LocalDate?=null,
val conditional1: String?,
val conditional2: String?
)</pre>
<p>Эти поля мы добавили, что на них наглядно показать как будет работать наша кастомная валидация.</p>
<p>Логика проверки будет следующей: должно быть заполнено хотя бы одно поле: conditional1 или conditional2. Если оба поля не заполнены, выдаем ошибку валидации. Если заполнено первое поле, то заполненность второго уже не проверяем и наоборот. Таким образом, мы реализуем зависимость двух полей друг от друга, а также обязательность заполнения какого-либо одного из полей.</p>
<p>Давайте введем пользовательскую аннотацию на уровне класса, которая определяет условия этой логики. Назовем её - <strong>@RequiredByCondition</strong>:</p>
<pre class="brush:java;">
@RequiredByCondition(
conditionalProperty = "conditional1",
triggerValues = ["NULL", "null"],
requiredProperties = ["conditional2"],
message = "'conditional1' or 'conditional2' must be specified"
)
@RequiredByCondition(
conditionalProperty = "conditional2",
triggerValues = ["NULL", "null", "EMPTY", "empty"],
requiredProperties = ["conditional1"],
message = "'conditional1' or 'conditional2' must be specified"
)
data class CourseRequest(
@field:NotBlank(message = "name should be specified") val name : String,
@field:PositiveOrZero(message = "price must be >= 0") val price : Float,
@field:NotBlank(message = "author should be specified") val author : String,
@field:NotBlank(message = "direction should be specified") val direction : String,
val startDate : LocalDate,
val endDate: LocalDate?=null,
val conditional1: String?,
val conditional2: String?
)</pre>
<p>Как читать вышеприведенный код? Все очень просто. Мы добавили к классу две аннотации @RequiredByCondition, одну для поля "conditional1", другую для - "conditional2".</p>
<p>Первая аннотация говорит нам о том, что если поле класса "conditional1" (определено как conditionalProperty = "conditional1") содержит в себе значения "NULL" или "null" (triggerValues), что также в нашей реализации означает пустую строку, то мы ожидаем что поле класса "conditional2" (определено как requiredProperties = ["conditional2"]) будет содержать в себе непустое значение и не будет равно "null". В обратном случае, мы выдадим ошибку валидации с сообщением, определенным свойством message = "'conditional1' or 'conditional2' must be specified".</p>
<p>Абсолютно похожее условие содержится во второй аннотации, только применяется оно уже к полю класса "conditional2".</p>
<p>Мы могли задать свойства аннотаций и по другому. Например так:</p>
<pre class="brush:java;">
@RequiredByCondition(
conditionalProperty = "conditional1",
triggerValues = ["Some good value"],
requiredProperties = ["name", "price"],
message = "'name' and 'price' must be specified"
)
@RequiredByCondition(
conditionalProperty = "author",
triggerValues = ["Some famous author"],
requiredProperties = ["conditional1", "conditional2"],
message = "'conditional1' and 'conditional2' must be specified"
)
data class CourseRequest(
@field:NotBlank(message = "name should be specified") val name : String,
@field:PositiveOrZero(message = "price must be >= 0") val price : Float,
@field:NotBlank(message = "author should be specified") val author : String,
@field:NotBlank(message = "direction should be specified") val direction : String,
val startDate : LocalDate,
val endDate: LocalDate?=null,
val conditional1: String?,
val conditional2: String?
)</pre>
<p>В этом случае мы достигли бы того, что в зависимости от значения определенного поля изменялся бы набор обязательных других связанных с ним полей.</p>
<p>Например, если поле "conditional1" имеет значение "Some good value", то мы ожидаем, что поля "name" и "price" будут заполнены.</p>
<p>А если поле "author" равно "Some famous author", то мы ожидаем, что оба поля "conditional1" и "conditional2" не будут пустыми или равными null.</p>
<p> </p>
<p>И так, мы рассмотрели как добавить нашу аннотацию к классу и как задать ее свойства.</p>
<p>Теперь давайте рассмотрим как наша аннотация реализована "под капотом".</p>
<p>Добавим файл Validatrs.kt в пакет model со следующим содержимым:</p>
<pre class="brush:java;">
@Constraint(validatedBy = [RequiredByConditionValidator::class])
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@JvmRepeatable(
RequiredByCondition.List::class
)
annotation class RequiredByCondition(
val message: String = "must be specified",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = [],
val conditionalProperty: String,
val triggerValues: Array<String>,
val requiredProperties: Array<String>
) {
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class List(vararg val value: RequiredByCondition)
}
class RequiredByConditionValidator : ConstraintValidator<RequiredByCondition, Any?> {
private var conditionalProperty: String? = null
private lateinit var triggerValues: Array<String>
private lateinit var requiredProperties: Array<String>
private var message: String? = null
override fun initialize(constraint: RequiredByCondition) {
conditionalProperty = constraint.conditionalProperty
triggerValues = constraint.triggerValues
requiredProperties = constraint.requiredProperties
message = constraint.message
}
override fun isValid(o: Any?, context: ConstraintValidatorContext): Boolean {
try {
val conditionalPropertyValue: Any? = o?.getField(conditionalProperty!!)
if (isValidationNeeded(conditionalPropertyValue)) {
return validateRequiredProperties(o, context)
}
} catch (e: Exception) {
return false
}
return true
}
private fun isValidationNeeded(conditionalPropertyValue: Any?): Boolean {
val values = listOf(*triggerValues)
val isEmpty: Boolean = ObjectUtils.isEmpty(conditionalPropertyValue)
return if (isEmpty && values.stream().anyMatch {
it: String ->
it.equals("NULL", ignoreCase = true) || it.equals("EMPTY", ignoreCase = true) })
true
else values.contains(conditionalPropertyValue)
}
private fun validateRequiredProperties(o: Any?, context: ConstraintValidatorContext): Boolean {
var isValid = true
listOf(*requiredProperties).forEach(Consumer { it: String? ->
val requiredPropertyValue: Any? = o?.getField(it!!)
val isNotSpecified: Boolean = ObjectUtils.isEmpty(requiredPropertyValue)
if (isNotSpecified) {
isValid = false
context.disableDefaultConstraintViolation()
context.buildConstraintViolationWithTemplate(message)
.addPropertyNode(it)
.addConstraintViolation()
}
})
return isValid
}
@Throws(IllegalAccessException::class, ClassCastException::class)
inline fun <reified T> Any.getField(fieldName: String): T? {
this::class.memberProperties.forEach { kCallable ->
if (fieldName == kCallable.name) {
return kCallable.getter.call(this) as T?
}
}
return null
}
}</pre>
<p>Для начала рассмотрим саму аннотацию:</p>
<pre class="brush:java;">
@Constraint(validatedBy = [RequiredByConditionValidator::class])
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@JvmRepeatable(
RequiredByCondition.List::class
)
annotation class RequiredByCondition(
val message: String = "must be specified",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = [],
val conditionalProperty: String,
val triggerValues: Array<String>,
val requiredProperties: Array<String>
) {
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class List(vararg val value: RequiredByCondition)
}</pre>
<p>Здесь важно отметить несколько моментов:</p>
<p>С помощью @Constraint(validatedBy = [RequiredByConditionValidator::class]) мы указываем, что наша аннотация будет ограничивающей аннотацией и для проверки элементов, которые мы пометим ей, будет использоваться валидатор RequiredByConditionValidator.</p>
<p>С помощью @Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) мы указываем область применения нашей аннотации, - классы.</p>
<p>С помощью @JvmRepeatable(RequiredByCondition.List::class) мы указываем, что наша аннотация будет повторяющейся, то есть ее можно будет применить несколько раз к одному и тому же элементу. Место хранения повторений указываем как RequiredByCondition.List</p>
<p>Теперь перейдем к классу RequiredByConditionValidator, который выполняет фактическую логику валидации. Этот валидатор реализует интерфейс ConstraintValidator из пакета jakarta.validation.</p>
<pre class="brush:java;">
class RequiredByConditionValidator : ConstraintValidator<RequiredByCondition, Any?> {
private var conditionalProperty: String? = null
private lateinit var triggerValues: Array<String>
private lateinit var requiredProperties: Array<String>
private var message: String? = null
override fun initialize(constraint: RequiredByCondition) {
conditionalProperty = constraint.conditionalProperty
triggerValues = constraint.triggerValues
requiredProperties = constraint.requiredProperties
message = constraint.message
}
override fun isValid(o: Any?, context: ConstraintValidatorContext): Boolean {
try {
val conditionalPropertyValue: Any? = o?.getField(conditionalProperty!!)
if (isValidationNeeded(conditionalPropertyValue)) {
return validateRequiredProperties(o, context)
}
} catch (e: Exception) {
return false
}
return true
}
private fun isValidationNeeded(conditionalPropertyValue: Any?): Boolean {
val values = listOf(*triggerValues)
val isEmpty: Boolean = ObjectUtils.isEmpty(conditionalPropertyValue)
return if (isEmpty && values.stream().anyMatch {
it: String ->
it.equals("NULL", ignoreCase = true) || it.equals("EMPTY", ignoreCase = true) })
true
else values.contains(conditionalPropertyValue)
}
private fun validateRequiredProperties(o: Any?, context: ConstraintValidatorContext): Boolean {
var isValid = true
listOf(*requiredProperties).forEach(Consumer { it: String? ->
val requiredPropertyValue: Any? = o?.getField(it!!)
val isNotSpecified: Boolean = ObjectUtils.isEmpty(requiredPropertyValue)
if (isNotSpecified) {
isValid = false
context.disableDefaultConstraintViolation()
context.buildConstraintViolationWithTemplate(message)
.addPropertyNode(it)
.addConstraintViolation()
}
})
return isValid
}
@Throws(IllegalAccessException::class, ClassCastException::class)
inline fun <reified T> Any.getField(fieldName: String): T? {
this::class.memberProperties.forEach { kCallable ->
if (fieldName == kCallable.name) {
return kCallable.getter.call(this) as T?
}
}
return null
}
}</pre>
<p>Функция initialize() предоставляет нам доступ к значениям атрибутов, используемых в нашей аннотации, и позволяет хранить их в полях валидатора.</p>
<p>Функция isValid() содержит конкретную логику валидации.</p>
<p>В ней вначале мы получаем с помощью рефлексии (реализовано нами как inline функция getField) значение поля класса, название которого мы указали в свойстве conditionalProperty нашей аннотации.</p>
<p>Далее с помощью функции isValidationNeeded проверяем нужно ли валидировать это значение или нет. Если валидировать не нужно, функция isValid() просто возвращает true, что означает что валидатор отработал и результат проверки положительный. В обратном случае, когда валидация значения требуется, мы выполняем функцию validateRequiredProperties и возвращаем ее результат. В случае если результат отрицательный, то мы генерируем ошибку валидации ConstraintViolation с сообщением, которое мы указали в свойстве message аннотации.</p>
<p>Теперь давайте протестируем нашу аннотацию и валидатор и проверим, ведут ли они себя так, как ожидалось.</p>
<p>Для начала напишем, а затем прогоним тесты:</p>
<pre class="brush:as3;">
@SpringBootTest
class CoursesShopReactiveApplicationTests {
private var request: CourseRequest? = null
var validator: Validator = Validation.buildDefaultValidatorFactory().validator
@Test
fun GIVEN_course_request_without_conditional1_or_conditional2_WHEN_validate_THEN_constraintViolations() {
request = CourseRequest("test-name",
200f,
"test-author",
"test-direction",
LocalDate.now(),
null,
null,
null)
val messages = messages(validator.validate(request))
Assertions.assertEquals(messages.size, 1)
Assertions.assertTrue(messages.contains("'conditional1' or 'conditional2' must be specified"))
}
@Test
fun GIVEN_course_request_with_conditional1_WHEN_validate_THEN_success() {
request = CourseRequest("test-name",
200f,
"test-author",
"test-direction",
LocalDate.now(),
null,
"conditional-1",
null)
val messages = messages(validator.validate(request))
Assertions.assertEquals(messages.size, 0)
}
private fun messages(constraintViolations: Set<ConstraintViolation<Any>>): Set<String> {
return constraintViolations
.stream()
.map { obj: ConstraintViolation<Any> -> obj.message }
.collect(Collectors.toSet())
}
}</pre>
<p>Результат прогона тестов:</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/Screenshot+2024-02-25+at+22.39.11.png/dbf5e923-f704-e60e-b1aa-85615c9acc4e?imagePreview=1" /></p>
<p><strong>Tests passed: 2 of 2</strong>. Perfect. Как видно, все работает как ожидалось.</p>
<p> </p>
<p>Конец статьи.</p>
<p>Благодарю вас за чтение.</p>
<p>Исходный код, приведенный в этой статье, Вы сможете найти по ссылке <a href="https://github.com/romo-it/courses-shop-reactive/tree/main" target="_blank">здесь</a></p>Romo Fedoroff2024-02-25T19:52:00ZOpen edX: отправка писем через django manage.pyТимофей Перцевhttps://www.tune-it.ru/c/blogs/find_entry?entryId=195374532024-02-05T18:19:34Z2024-02-05T17:58:00Z<div class="portlet-msg-info">Примеры приведены для платформы edX версии Maple.</div>
<p>Для отправки письма мы хотим использовать готовый шаблон.<br />
Шаблоны писем размазаны по всему проекту, что, конечно, <em>очень удобно</em>. Тем не менее, их можно найти:</p>
<pre class="brush:bash;">
cd edx-platform
find . -type d -name email</pre>
<p>Вот несколько примеров:</p>
<ul>
<li>./common/templates/student/edx_ace/accountactivation/email</li>
<li>./lms/templates/instructor/edx_ace/allowedenroll/email</li>
<li>./openedx/core/djangoapps/user_authn/templates/user_authn/edx_ace/passwordreset/email</li>
</ul>
<p>Остановимся на первом из них - accountactivation - и рассмотрим как его отправить. Дальнейшие действия мы будем выполнять в интерактивной оболочке django:</p>
<pre class="brush:bash;">
# cd edx-platform
./manage.py lms shell</pre>
<p>Нужно импортировать класс, соответствующий письму активации. Его определение располагается в файле <code>message_types.py</code>:</p>
<pre class="brush:python;">
from common.djangoapps.student.message_types import AccountActivation</pre>
<p>Здесь хочется обратить внимание на два момента, которые позволят лучше понять, что происходит:</p>
<ul>
<li>Определение класса "живёт" в том же приложении, что и шаблон письма. В данном случае, в приложении <code>student</code>.</li>
<li>Имя поддиректории <code>accountactivation</code> совпадает с именем класса в нижнем регистре.</li>
</ul>
<p>Итак, мы разобрались с шаблоном письма. Далее нам потребуется адресат:</p>
<pre class="brush:python;">
from django.contrib.auth.models import User
from edx_ace.recipient import Recipient
user = User.objects.get(email='test@example.com')
recipient = Recipient(user, user.email)</pre>
<p>Теперь мы можем подготовить контекст. Контекст представляет собой словарь данных (dict), значения из которого подставляются в шаблон, а на выходе мы получаем готовый текст.</p>
<p>Описать контекст можно полностью вручную или воспользоваться вспомогательной функцией, при наличии. Для письма регистрации такая функция есть и мы ей воспользуемся.</p>
<p>На вход функция принимает два объекта: <code>User</code> и <code>Registration</code>. Пользователя мы уже получили ранее, второй объект получить просто:</p>
<pre class="brush:python;">
registration = user.registration</pre>
<p>А теперь подробней рассмотрим определение функции:</p>
<pre class="brush:python;">
def generate_activation_email_context(user, registration):
"""
Constructs a dictionary for use in activation email contexts
Arguments:
user (User): Currently logged-in user
registration (Registration): Registration object for the currently logged-in user
"""
context = get_base_template_context(None)
context.update({
'name': user.profile.name,
'key': registration.activation_key,
'lms_url': configuration_helpers.get_value('LMS_ROOT_URL', settings.LMS_ROOT_URL),
'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
'contact_mailing_address': configuration_helpers.get_value(
'contact_mailing_address',
settings.CONTACT_MAILING_ADDRESS
),
'support_url': configuration_helpers.get_value(
'ACTIVATION_EMAIL_SUPPORT_LINK', settings.ACTIVATION_EMAIL_SUPPORT_LINK
) or settings.SUPPORT_SITE_LINK,
'support_email': configuration_helpers.get_value('CONTACT_EMAIL', settings.CONTACT_EMAIL),
'site_configuration_values': configuration_helpers.get_current_site_configuration_values(),
})
return context</pre>
<p>Невооруженным взглядом можно заметить, что контекст зависит от конфигурации сайта, а та в свою очередь зависит от домена, на который поступил HTTP-запрос.</p>
<blockquote>
<p>— Запрос? Какой запрос?</p>
</blockquote>
<p>Верно подмечено! Мы работаем в интерактивной оболочке, поэтому запрос придётся эмулировать:</p>
<pre class="brush:python;">
from openedx.core.lib.celery.task_utils import emulate_http_request
from django.contrib.sites.models import Site
from edx_django_utils.cache import RequestCache
RequestCache(namespace="site_config").clear()
site = Site.objects.get(domain='education.example.com')
with emulate_http_request(site=site, user=user):
context = generate_activation_email_context(user, registration)
...</pre>
<p>Здесь можно заметить вызов метода <code>clear()</code> у объекта <code>RequestCache</code>, что связано с особенностями реализации. Если этого не сделать, то конфигурация сайта не применится.</p>
<p>Остаётся лишь сформировать и отправить письмо внутри того же блока <code>with ...</code> для работы встроенной аналитики:</p>
<pre class="brush:python;">
...
from edx_ace import ace
msg = AccountActivation().personalize(
recipient=recipient,
language='RU',
user_context=context,
)
ace.send(msg)</pre>
<p>Всё, можно ловить письмо!</p>
<p>Ну и напоследок соберём весь код вместе:</p>
<pre class="brush:python;">
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from edx_ace import ace
from edx_ace.recipient import Recipient
from edx_django_utils.cache import RequestCache
from common.djangoapps.student.email_helpers import generate_activation_email_context
from common.djangoapps.student.message_types import AccountActivation
from openedx.core.lib.celery.task_utils import emulate_http_request
user = User.objects.get(email='test@example.com')
recipient = Recipient(user, user.email)
registration = user.registration
RequestCache(namespace="site_config").clear()
site = Site.objects.get(domain='education.example.com')
with emulate_http_request(site=site, user=user):
context = generate_activation_email_context(user, registration)
msg = AccountActivation().personalize(
recipient=recipient,
language='RU',
user_context=context,
)
ace.send(msg)
</pre>Тимофей Перцев2024-02-05T17:58:00ZПривязка корпоративных пользователей к TelegramDmitry Afanasievhttps://www.tune-it.ru/c/blogs/find_entry?entryId=191946592023-12-29T16:08:30Z2023-12-29T14:44:00Z<p>Предположим, что в Вашей компании доступ в глобальный Internet не ограничен и использование, например, Telegram не запрещено. Для того, чтобы сотрудники могли найти в telegram своих коллег, и для того, чтобы Ваш бот, при отправке сообщений в какие-то общие чаты, мог использовать @username для уведомления заинтересованных людей необходимо, чтобы в Вашей корпоративной системе каким-то образом хранились имена этих пользователей. А для того, чтобы Ваш бот мог напрямую отправлять сообщение конкретным людям, боту должен быть известен числовой идентификатор этого пользователя. И если с указанием @username всё относительно просто, так как эта информация видна в настройках пользователя, то с получением идентификатора могут возникнуть проблемы, так как для этого надо или использовать сторонние сервисы (боты), или писать своего бота, который будет иметь доступ и к внутренней корпоративной систему, и к сервисам telegram. Также при этом могут возникнуть проблемы с валидацией вводимой пользователем информации.</p>
<p>Всего этого можно избежать, если использовать предлагаемый Telegram вариант с аутентификацией через Login Widget. Чисто формально для этого Вам всё еще необходим <a href="https://core.telegram.org/bots">telegram бот</a>, но фактически он не будет ничего ни принимать, ни отправлять, нужны только его идентификатор и токен, который будет использовать для валидации полученных данных.</p>
<p>Для этого Вам потребуется на официальную страницу <a href="https://core.telegram.org/widgets/login">login widget</a>, где Вы увидите инструкцию как привязать Вашего бота к некоторому корпоративному доменному имени, которое никак не валидируется, и будет предложено ввести Bot Username, выбрать Authorization Type и некоторые параметры, связанные с оформлением. Если с Bot Username всё понятно, то для Authorization Type предлагается два значения:</p>
<ul>
<li>Callback</li>
<li>Redirect to URL</li>
</ul>
<p>При выборе Callback необходимо будет написать на Javascript функцию, которая будет обрабатывать результат аутентификации. При выборе второго варианта после успешной аутентификации пользователь будет перенаправлен на указанный Вами URL, а в параметрах метода GET будут указаны интересующие нас id и username. Что характерно, никакой валидации вводимого URL нет, и это вполне может быть ссылкой на внутренний, недоступный из интернет, портал.</p>
<p>Также будет передан параметр hash, содержащий цифровую подпись <a href="https://en.wikipedia.org/wiki/Hash-based_message_authentication_code">HMAC-SHA-256</a>, которая формируется с использованием токена Вашего бота, и с помощью которой можно проверить, что данную информацию Вы действительно получили от Telegram, гарантируя тем самым консистентность используемых значений идентификатора и имени telegram пользователя.</p>
<p>Обработка такого запроса довольно проста и не требует глубокого погружения в Bot API Telegram или использования <a href="https://core.telegram.org/bots/samples">готовых библиотек</a>, и, сопряженных с этим, мук выбора этим самых библиотек, и может быть реализована на таких архаичных языках как PHP.</p>
<p>Если по какой-то причине был выбран PHP, то обработка параметров с информацией о пользователе не должна вызвать каких либо проблем, но, если Вы ранее не имели опыта работа с функциями hash() и hash_hmac(), то могут возникнуть проблемы с подбором правильных параметров для этих функций. Поэтому процитирую полностью код до момента завершения проверки цифровой подписи:</p>
<pre class="brush:php;">
$fields = [
"id", "first_name", "last_name", "username", "photo_url", "auth_date",
];
sort($fields);
$tginfo = [];
$tginfo4chk = [];
foreach ($fields as $field)
if (isset($_GET[$field])) {
$tginfo[$field] = $_GET[$field];
$tginfo4chk[] = $field . "=" . $_GET[$field];
}
$hmac1 = $_GET["hash"];
$hmac2 = hash_hmac("sha256",
implode("\n", $tginfo4chk),
hash("sha256", TG_TOKEN, $binary = true)
);
if ($hmac1 != $hmac2) {
http_response_code(400);
die("Invalid hash received");
}</pre>
<p>Некоторые комментарии к коду:</p>
<ol>
<li>В массиве $fields перечислены имена параметров, которые, согласно документации, могут быть переданы. Если какие-то из перечисленных полей у пользователя не заполнены, их в запросе не будет.</li>
<li>Сортировка массива $fields нужна для корректного вычисления цифровой подписи, так как, согласно документации, она должна вычисляться для всех переданных параметров, перечисленных в алфавитном порядке. При этом в самом запросе эти параметры указаны в другом порядке.</li>
<li>В массив $tginfo сохраняется полученная информация, которую можно использовать далее.</li>
<li>Массив $tginfo4chk используется только для проверки цифровой подписи.</li>
<li>При вызове функции hash() обязательно должен быть установлен параметр $binary в значение true, так как функция hash_hmac() в качестве инициализирующего ключа использует именно двоичные данные, а у самой функции hash_hmac() этот же параметр должен быть установлен в false, так как нас интересует шестнадцатеричное представление контрольной суммы, которое сравнивается со значением переданного параметра hash.</li>
</ol>
<p>Реализация обработчика на других языках также не должна вызывать проблем. При этом сам способ достаточно простой как с точки зрения обработчика, так и для самих пользователей, и не требует наличия на компьютере пользователя клиента telegram, написания полноценного telegram bot, обработки запросов из небезопасного интернета на внутренних сервисах и других сложностей.</p>Dmitry Afanasiev2023-12-29T14:44:00ZАвтоматические повторные попытки с помощью Spring-аннотации @RetryableRomo Fedoroffhttps://www.tune-it.ru/c/blogs/find_entry?entryId=191289512023-12-22T12:28:35Z2023-12-22T11:45:00Z<style type="text/css">article p {
font-size:11pt;
font-family:Verdana, sans-serif;
text-align:justify;
color:#6a6a6a;
}
article img {
width: 95%;
}
.centered {
text-align:center;
}
</style>
<p class="centered"><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/art2.jpg/c98eb910-9acd-6ebf-0daa-a36295a15491?imagePreview=1" /></p>
<p>Статья основана на труде "Using Spring’s @Retryable Annotation for Automatic Retries" автора Alexander Obregon</p>
<h1>Введение</h1>
<p>В сегодняшнем переплетенном мире, приложения зачастую взаимодействуют друг с другом, а также с внешними сервисами, базами данных и другими ресурсами. Во время такого взаимодействия, могут возникать временные неполадки, сетевые задержки, тайм-ауты, сбои в работе сторонних сервисов. Все вышеперечисленное привносит неопределенность в гарантию получения ожидаемых результатов. Если в вашем приложении есть критический участок кода, который подвержен подобным сценариям сбоя, то естественное желание - чтобы код был устойчивым и способным к самовосстановлению, по крайней мере, для проблем, которые непостоянные и временные. Именно здесь на помощь приходит аннотация Spring @Retryable, которая добавляет уровень отказоустойчивости в разрабатываемые приложения.</p>
<h1>Когда нам необходимо повторять попытки?</h1>
<p>Представьте себе какой-нибудь сервис, который запрашивает и получает данные из стороннего АПИ. Например, сервис который проверяет доступность номеров в отеле.</p>
<p>В идеальных условиях, сервис выполняет HTTP-запрос и ему возвращаются данные. Но в реальности так может быть не всегда, иногда возникают различные неполадки. К примеру, сервер, на котором расположено АПИ, может быть сильно загружен, или же ваше приложение, само, может сталкиваться с сетевыми проблемами, задержками и т.д. Если не обрабатывать подобные случаи и оставлять все на самотек, то в итоге будет страдать качество, быстродействие и надежность ваших приложений, что может плохо отразиться на удовлетворенности пользователей от использования ваших программных продуктов и соответственно привести к потерям в бизнесе.</p>
<p>Повторное выполнение операций может показаться простым в плане реализации решением. Однако программирование его вручную и применение во всех проблемных местах в коде, может сделать этот самый код чрезмерно раздутым и трудно поддерживаемым.</p>
<p>Вот простейший пример:</p>
<pre class="brush:java;">
public class ManualRetryService {
public String fetchDataFromRemote() {
int attempts = 0;
while(attempts < 3) {
try {
// Make the API call
return "Успешно";
} catch (MyNetworkException e) {
attempts++;
}
}
throw new MyCustomException("Так и не удалось вызвать АПИ даже после 3 попыток повторения");
}
}</pre>
<p>В этом примере мы вручную реализуем логику повторных попыток с помощью цикла while, что усложняет наш код. Поддерживать, читать и применять этот код будет еще сложнее, если мы захотим в последствии добавить к нему дополнительные возможности, например, такие как: настройку интервалов повторных попыток, перехват и обработку различных видов исключений и многое многое другое.</p>
<h1>Как аннотация @Retryable упрощает весь процесс?</h1>
<p>Spring Framework упрощает задачу выполнения повторений с помощью аннотации @Retryable. Мы просто добавляем ее в нужные нам компоненты и фреймворк делает за нас всю "грязную работу". Вот как будет выглядеть наш предыдущий пример, если мы перепишем его с @Retryable:</p>
<pre class="brush:java;">
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
@Service
public class MyService {
@Retryable(MyNetworkException.class)
public String fetchDataFromRemote() {
// Make the API call that might fail
return "Успешно";
}
}</pre>
<p>В приведенном выше коде, Spring будет автоматически повторять метод fetchDataFromRemote, в случае если при его выполнении возникнет исключение MyNetworkException.</p>
<p>Очевидно, что код стал намного чище и проще для понимания и поддержки. По мимо этого, мы можем легко дополнить его другими опциями. О них мы поговорим далее в этой статье.</p>
<h1>Что скрывается под капотом?</h1>
<p>Когда вы аннотируете метод с помощью @Retryable, Spring создает прокси вокруг этого метода. Это позволяет фреймворку перехватывать вызовы метода и прозрачно добавлять к нему логику повторных попыток. Принцип работы схож с другими возможностями Spring, которые используют прокси, например, с управлением транзакциями с помощью аннотации @Transactional.</p>
<p> </p>
<p>Так почему же стоит попробовать @Retryable, а не реализовывать все вручную?</p>
<p>1) Чистота кода: Бизнес-логика остается отделенной от логики отказоустойчивости (не смешивается).</p>
<p>2) Удобство: Легче расширять или изменять конфигурацию логики повторных попыток, не затрагивая при этом бизнес-код.</p>
<p>3) Понятный код: С помощью аннотаций легко понять замысел разработчика, легко понять ожидаемое поведения метода.</p>
<p> </p>
<p>Используя аннотацию @Retryable, вы можете добавить мощную и гибкую логику повторных попыток в свои методы, не усложняя кодовую базу. Это позволит вам сосредоточиться на бизнес-логике, в то время как фреймворк будет ответственен за реализацию отказоустойчивости.</p>
<h1>Настройка @Retryable</h1>
<p>Аннотация @Retryable - это не универсальное решение, а скорее - настраиваемая функция, которая может адаптироваться к множеству сценариев. Ее гибкость достигается благодаря богатому набору параметров конфигурации, которые позволяют точно настроить управление повторными попытками.</p>
<h2>Задание типов отслеживаемых исключений</h2>
<p>Ваш метод может выбрасывать разные типы исключений, и скорее всего нет нужды выполнять повторные попытки для каждого из таких случаев. Например, повторное выполнение операции, которая завершилась неудачей из-за NullPointerException, скорее всего, будет бессмысленным, поскольку исключение, вероятно, вызвано ошибкой программирования. С другой стороны, повторное выполнение неудачной сетевой операции имеет смысл.</p>
<p>С помощью атрибута value аннотации @Retryable вы можете указать типы исключений, которые должны инициировать повторную попытку.</p>
<p>К примеру:</p>
<pre class="brush:java;">
@Retryable(value = { MyNetworkException.class, TimeoutException.class })
public String fetchRemoteData() {
// Во время сетевого вызова может произойти ошибка
return "Данные";
}</pre>
<h2>Настройка количества попыток</h2>
<p>По умолчанию @Retryable будет осуществлять максимум три попытки повторения, однако можно легко настроить это поведение, задав атрибут maxAttempts:</p>
<pre class="brush:java;">
@Retryable(value = MyNetworkException.class, maxAttempts = 5)
public String fetchRemoteData() {
// Во время сетевого вызова может произойти ошибка
return "Данные";
}</pre>
<h2>Задержки между попытками</h2>
<p>Часто бывает полезно ввести задержку между повторными попытками. Это может помочь в ситуациях, когда внешний сервис может быть временно перегружен. Задержка настраивается с помощью атрибута backoff аннотации @Backoff.</p>
<p>Вот пример, в котором задается двухсекундная задержка между попытками повторения:</p>
<pre class="brush:java;">
@Retryable(value = MyNetworkException.class, backoff = @Backoff(delay = 2000))
public String fetchRemoteData() {
// Во время сетевого вызова может произойти ошибка
return "Данные";
}</pre>
<h2>Экспоненциальная задержка</h2>
<p>В некоторых случаях вам может понадобиться стратегия экспоненциальной задержки, суть которой заключается в увеличении временных интервалов между каждой попыткой повтора. Это может быть полезно, когда вы взаимодействуете с сервисами, которым требуется время на восстановление или масштабирование. За настройку экспоненциальной задержки отвечает атрибут multiplier:</p>
<pre class="brush:java;">
@Retryable(value = MyNetworkException.class,
backoff = @Backoff(delay = 1000, multiplier = 2))
public String fetchRemoteData() {
// Во время сетевого вызова может произойти ошибка
return "Данные";
}</pre>
<h2>Соединяем воедино несколько параметров</h2>
<p>Практическая польза @Retryable раскрывается в полной мере, когда вы начинаете комбинировать ее доступные атрибуты.</p>
<p>Например:</p>
<pre class="brush:java;">
@Retryable(value = { MyNetworkException.class, TimeoutException.class },
maxAttempts = 5,
backoff = @Backoff(delay = 1000, multiplier = 2))
public String fetchRemoteData() {
// Во время сетевого вызова может произойти ошибка
return "Данные";
}</pre>
<p>В примере выше метод будет повторно вызываться максимум до 5 раз, в случаях если будут происходить исключения MyNetworkException и TimeoutException. При этом, повторные вызовы будут происходить с задержкой между друг другом, начиная с задержки в 1000 миллисекунд и с дальнейшим ее удвоением с каждой последующей попыткой.</p>
<h2>Добавляем условия</h2>
<p>Могут возникнуть ситуации, когда необходимо динамически управлять выполнением повторной попытки, основываясь на каком-либо условии или возникшем исключении. Для этого можно использовать атрибут condition, который принимает выражение на синтаксисе SpEL.</p>
<pre class="brush:java;">
@Retryable(value = MyNetworkException.class,
condition = "#{#root.args[0] != 'no-retry'}")
public String fetchRemoteData(String controlFlag) {
// Во время сетевого вызова может произойти ошибка
return "Данные";
}</pre>
<p>В этом примере повторная попытка не будет выполняться, если аргумент controlFlag равен 'no-retry'.</p>
<h1>Разбираемся в параметрах</h1>
<p>Аннотация @Retryable имеет много различных параметров для настройки логики повторного выполнения. Эти параметры спроектированы так, чтобы работать в гармонии и чтобы обеспечить надежный механизм повторных попыток прямо из коробки. Нужны ли вам простые повторные попытки с фиксированными интервалами или сложные механизмы вроде экспоненциального отката с повторными попытками на основе условий, понимание этих параметров поможет вам все реализовать без особых усилий.</p>
<h2>параметр value</h2>
<p>Параметр value задает какие исключения должны инициализировать повторную попытку. В качестве значения он может принимать массив классов Throwable. По умолчанию, если value не задан явно, повторная попытка инициализируется для всех исключений, расширяющих Throwable.</p>
<pre class="brush:java;">
@Retryable(value = { MyNetworkException.class, TimeoutException.class })
public String execute() {
// Code
}</pre>
<h2>параметр include</h2>
<p>Как и параметр value, параметр include позволяет указать исключения, которые должны вызвать повторную попытку. Разница в том, что include позволяет указывать исключения в дополнение к тем, которые уже определены параметром value.</p>
<pre class="brush:java;">
@Retryable(value = MyNetworkException.class, include = TimeoutException.class)
public String execute() {
// Code
}</pre>
<h2>параметр exclude</h2>
<p>В противоположность include, параметр exclude позволяет определить какие исключения не должны вызывать повторную попытку.</p>
<pre class="brush:java;">
@Retryable(value = Exception.class, exclude = IllegalArgumentException.class)
public String execute() {
// Code
}</pre>
<h2>параметр maxAttempts</h2>
<p>Параметр maxAttempts определяет максимальное количество попыток повторения вызова метода. Значение по умолчанию равно 3.</p>
<pre class="brush:java;">
@Retryable(maxAttempts = 5)
public String execute() {
// Code
}</pre>
<h2>параметр backoff</h2>
<p>Параметр backoff позволяет реализовать задержку между повторными попытками. В качестве значения он принимает аннотацию @Backoff, в которой вы можете указать задержку в миллисекундах и необязательный множитель для экспоненциальной задержки.</p>
<pre class="brush:java;">
@Retryable(backoff = @Backoff(delay = 2000, multiplier = 2))
public String execute() {
// Code
}</pre>
<h2>параметр condition</h2>
<p>Параметр condition позволяет указать булево условное выражение на синтаксисе SpEL (Spring Expression Language). Логика повторной попытки будет активирована только в том случае, если это выражение будет равно true.</p>
<pre class="brush:java;">
@Retryable(condition = "#{#arg > 100}")
public String execute(int arg) {
// Code
}</pre>
<h2>параметр stateful</h2>
<p>Параметр stateful указывает должны ли повторные попытки выполняться с учетом состояния или без учета состояния. При повторных попытках с сохранением состояния запоминается состояние первой неудачной попытки, и последующие повторные попытки выполняются на основе этого состояния. Повторные попытки без состояния, с другой стороны, не зависят друг от друга.</p>
<pre class="brush:java;">
@Retryable(stateful = true)
public String execute() {
// Code
}</pre>
<h2>параметр listeners</h2>
<p>Параметр listeners позволяет указать бин, который будет уведомляться при каждой попытке повтора. Этот бин должен реализовывать интерфейс RetryListener. Это может быть полезно для ведения журнала, метрик и т.п.</p>
<pre class="brush:java;">
@Retryable(listeners = "myRetryListenerBean")
public String execute() {
// Code
}</pre>
<h2>Соединяем все воедино</h2>
<pre class="brush:java;">
@Retryable(value = { MyNetworkException.class, TimeoutException.class },
maxAttempts = 5,
backoff = @Backoff(delay = 2000, multiplier = 2),
condition = "#{#arg != 'no-retry'}")
public String execute(String arg) {
// Code
}</pre>
<p>В этом примере метод будет повторен до 5 раз и только в случае возникновения MyNetworkException или TimeoutException. Повторные попытки будут происходить с начальной задержкой в 2000 миллисекунд, с удвоением задержки с каждой попыткой, и будут выполняться только в том случае, если аргумент arg не равен 'no-retry'.</p>
<h1>Соединяем аннотацию @Retryable с аннотацией @Recover</h1>
<p>При использовании аннотации @Retryable необходимо продумать что произойдет, если все повторные попытки окажутся неудачными. Хотя повторные попытки могут увеличить шансы на успешное выполнение операции, они не могут гарантировать его. Именно здесь на помощь приходит аннотация @Recover.</p>
<p>Аннотация @Recover позволяет определить fallback метод, который будет вызван, когда все попытки повторных попыток, настроенные с помощью @Retryable, будут исчерпаны. Fallback метод предназначен для выполнения альтернативной логики, такой как: отправка сообщения об ошибке, попытка подключения к резервной службе или обновление состояния приложения для отражения сбоя.</p>
<p>Вот простой пример, иллюстрирующий его использование:</p>
<pre class="brush:java;">
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
@Service
public class MyService {
@Retryable(MyNetworkException.class)
public String fetchDataFromRemote() {
// Во время сетевого вызова может произойти ошибка
return "Данные";
}
@Recover
public String recover(MyNetworkException e) {
// Логика запасного варианта
return "Данные по умолчанию";
}
}</pre>
<p>В этом примере, если метод fetchDataFromRemote вызовет исключение MyNetworkException и исчерпает все попытки восстановления, будет вызван метод recover, возвращающий "Данные по умолчанию" в качестве запасного варианта.</p>
<h2>Соответствие типов исключений</h2>
<p>Список параметров метода @Recover должен совпадать со списком параметров метода @Retryable. Однако список параметров @Recover должен также содержать еще один параметр и этот параметр должен быть первым в списке. Речь идет о параметре, указывающим тип исключения, для которого мы хотим инициализировать запасной вариант.</p>
<p>Например, если метод @Retryable принимает два параметра следующим образом:</p>
<pre class="brush:java;">
@Retryable(MyNetworkException.class)
public String fetchData(String param1, int param2) {
// Сетевой вызов
}</pre>
<p>Тогда сигнатура метода @Recover должна быть следующей:</p>
<pre class="brush:java;">
@Recover
public String recover(MyNetworkException e, String param1, int param2) {
// Логика запасного варианта
}</pre>
<h2>Задание нескольких запасных вариантов</h2>
<p>Вы можете задать несколько методов @Recover для разных типов исключений. Таким образом, вы можете выполнять различную логику восстановления в зависимости от типа исключения, которое привело к неудаче всех повторных попыток.</p>
<p>Вот пример того, как это можно настроить:</p>
<pre class="brush:java;">
@Retryable(value = { MyNetworkException.class, TimeoutException.class })
public String fetchDataFromRemote() {
// Сетевой вызов
}
@Recover
public String recover(MyNetworkException e) {
return "Данные по умолчанию для исключения MyNetworkException";
}
@Recover
public String recover(TimeoutException e) {
return "Данные по умолчанию для исключения TimeoutException";
}</pre>
<p>В этом примере есть два метода @Recover: один для MyNetworkException и другой для TimeoutException. Соответствующий метод @Recover будет вызван в зависимости от исключения, которое привело к исчерпанию повторных попыток.</p>
<h2>Добавляем условия</h2>
<p>Подобно @Retryable, вы также можете добавлять условия в методы @Recover:</p>
<pre class="brush:java;">
@Recover
public String recover(MyNetworkException e, String param1) {
if ("special_case".equals(param1)) {
return "Special Recovery Logic";
}
return "General Recovery Logic";
}</pre>
<h2>Когда следует использовать @Recover</h2>
<p>В то время как @Retryable может помочь восстановиться после преходящих сбоев, @Recover вступает в игру, когда вам приходится иметь дело с более постоянными проблемами или когда вы хотите выполнить "план Б" после того, как все попытки повторных попыток не увенчались успехом.</p>
<p>Комбинируя @Retryable с @Recover, вы можете создать надежную самовосстанавливающуюся систему, способную справляться как с преходящими, так и с более постоянными проблемами, обеспечивая более высокий уровень отказоустойчивости и улучшая общий пользовательский опыт.</p>
<h1>Примеры использования @Retryable</h1>
<h2>Вызовы удаленных сервисов</h2>
<p>Если ваше приложение зависит от удаленного сервиса, который может быть временно недоступен или испытывать периодические проблемы, использование @Retryable может повысить вероятность успешного завершения операции.</p>
<pre class="brush:java;">
@Retryable(MyNetworkException.class)
public String fetchFromRemoteService() {
// HTTP request to an external API
return "Data";
}</pre>
<h2>Распределенные системы</h2>
<p>В микросервисах или распределенных архитектурах часто случаются сбои в работе сети или временная недоступность сервисов. @Retryable может обеспечить устойчивость вашей системы к таким сбоям.</p>
<pre class="brush:java;">
@Retryable(TimeoutException.class)
public void sendMessageToQueue(String message) {
// Send message to a message queue
}</pre>
<h2>Базы данных</h2>
<p>Иногда операции с базой данных могут завершиться неудачей из-за блокировок или временных проблем с соединением. Повторное выполнение транзакции часто позволяет решить эти проблемы.</p>
<pre class="brush:java;">
@Retryable(DatabaseException.class)
public void updateDatabaseRecord() {
// Update database record
}</pre>
<h2>Операции с файлами</h2>
<p>Операции с файлами могут не выполняться по разным причинам, например из-за отсутствия разрешений или свободного места на диске. Повторная попытка может быть эффективной после устранения конкретной проблемы, которая привела к сбою.</p>
<pre class="brush:java;">
@Retryable(IOException.class)
public void writeFile() {
// Write to a file
}</pre>
<h2>Повторные попытки, основанные на составных, сложных условиях</h2>
<p>Используя параметр condition, мы можете добиться реализации комплексной логики повторных попыток на основе самых различных условий любой сложности, что делает этот инструмент невероятно гибким.</p>
<pre class="brush:java;">
@Retryable(value = CustomException.class, condition = "#{#someArg > 100}")
public void complexConditionMethod(int someArg) {
// Do something
}</pre>
<h1>Ограничения использования @Retryable</h1>
<h2>Влияние на производительность</h2>
<p>Каждая повторная попытка потребляет ресурсы, будь то циклы процессора, память или даже пропускная способность сети. Чрезмерное количество повторных попыток может привести к падению производительности.</p>
<h2>Подходит не для всех ошибок</h2>
<p>Не все типы ошибок можно успешно преодолеть повторением попыток. Например, повторная попытка выполнить неудачную операцию из-за исключения "файл не найден", скорее всего, приведет к повторному сбою.</p>
<h2>Каскадирование сбоев</h2>
<p>Слишком большое количество повторных попыток в архитектуре микросервисов может привести к каскадным отказам, когда один отказавший сервис приводит к отказу других.</p>
<h2>Потенциальные сложности в системах с состоянием</h2>
<p>В системах, поддерживающих состояние, неудачная операция, изменяющая состояние, может усложнить повторные попытки.</p>
<h2>Обработка ошибок</h2>
<p>Использование методов @Recover может привести к разрозненной логике обработки ошибок, что может привести к затруднениям в проектах с большой кодовой базой.</p>
<h1>Заключение</h1>
<p>Аннотации @Retryable и @Recover в Spring предлагают элегантный, декларативный подход к добавлению логики повторных попыток и отказоустойчивости в ваши приложения. Несмотря на богатый набор настраиваемых опций, важно использовать их с умом, помня как о преимуществах их применения, так и об ограничениях. Понимая глубину их возможностей и применяя их с умом, вы сможете значительно повысить отказоустойчивость и надежность вашего приложения.</p>Romo Fedoroff2023-12-22T11:45:00ZДоверительные отношения между ALD Pro и MS AD.Денис Серянкинhttps://www.tune-it.ru/c/blogs/find_entry?entryId=190542102023-12-20T08:15:23Z2023-12-14T10:37:00Z<p>Сначала было слово. Потом появилось MS Active Directory, а чуть позже ALD Pro на Astra Linux. И настало время подружить их. Первым шагом к этому будет настройка доверительных отношений, благодаря которым пользователи одного домена смогут авторизовываться в другом. В статье описываются работы на стенде с Astra Linux 1.7.4, ALD Pro 2.0.1 и MS AD на Windows Server 2022 (уровень леса 2016).</p>
<p>Со стороны Windos для начала надо озаботиться видимостью домена ALD Pro. Для этого добавим DNS зону условной пересылки. Для этого открываем DNS Manager -> правой кнопкой по "Conditional Forwarders" -> "New Conditional Forwarder...". В открывшемся окне указываем имя домена, запросы к которому хотим пересылать:</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/14.jpg/8ab2ec5d-1e5a-81d2-13a5-5963c333a469?t=1702903460732&imagePreview=1" /></p>
<p>Поначалу напротив IP-адреса может показываться крестик, но если вы настроили всё правильно, можно не обращать на него внимания. Далее нажимаем "OK".</p>
<p>Должно получиться нечто следующее:</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/15.jpg/c356b053-417b-27af-2391-0d645419f24f?t=1702903610881&imagePreview=1" /></p>
<p>Если теперь зайти в свойства новой записи и нажать "Edit", то можно увидеть, что теперь напротив IP-адреса стоит зеленая галочка и значит всё настроено правильно:</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/16.jpg/3b471952-156a-3678-b6a1-a2ae466057e5?t=1702903708257&imagePreview=1" /></p>
<p> </p>
<p>Теперь переходим к настройке доверительных отношений. Открываем Active Directory Domains and Trusts. Правой кнопкой по конфигурируемому домену и открыть свойства:</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/11.jpg/ec07f173-174c-e1bf-cfc7-d12ff4d09b71?t=1702902226306&imagePreview=1" /></p>
<p>Открываем вкладку "Trusts" и нажимаем "New Trust...". Откроется визард по настройке доверительных отношений:</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/12.jpg/082067f5-3ef9-792b-1434-e086a61c3913?t=1702902353162&imagePreview=1" /></p>
<p>Нажимаем "Next" и в поле Name указываем имя линуксового домена:</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/13.jpg/eafb67d8-77f2-0b32-a59e-472a67f99f36?t=1702902430303&imagePreview=1" /></p>
<p>Далее указываем тип отношений, тут надо выбирать под задачи. В примере выбираем "Forest Trust":</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/17+%281%29.jpg/5c3ca0d4-76dc-8012-b559-c175124ea30b?t=1702904086046&imagePreview=1" /></p>
<p>Здесь указвыаем направление доверия, опять же надо исходить из своих задач. В примере будут двусторонние:</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/18.jpg/da0cb069-2207-bd51-2e0e-af2527c4d061?t=1702904170807&imagePreview=1" /></p>
<p>Далее указываем, что доверие настроить только на текущем домене:</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/19.jpg/1e2c2b1b-cb10-2368-2cff-90d5b42a038c?t=1702905606985&imagePreview=1" /></p>
<p>Выбираем "Forest-wide authentication":</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/20.jpg/bec198e6-06d1-7355-070b-7443e5f7b4d5?t=1702905904069&imagePreview=1" /></p>
<p>Указываем пароль для настройки доверия:</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/21.jpg/4bd573e9-a490-b1b5-d986-dbe3b641da7f?t=1702908956286&imagePreview=1" /></p>
<p>Потом везде нажимаем далее и выбираем не подтверждать исходящее доверие и не подтверждать входящее доверие.</p>
<p>Далее настраиваем доверие со стороны ALD Pro.</p>
<p>Первым делом выключаем dnssec-validation на КД ALD Pro:</p>
<pre class="brush:bash;">
sudo nano /etc/bind/ipa-options-ext.conf</pre>
<p>Находим строку:</p>
<pre class="brush:bash;">
dnssec-validation yes;</pre>
<p>И исправляем её на:</p>
<pre class="brush:bash;">
dnssec-validation no;</pre>
<p>Сохраняем и перезапускаем FreeIPA (ALD Pro работает поверх FreeIPA)</p>
<pre class="brush:bash;">
sudo ipactl restart</pre>
<p>Далее приступаем к настройке доверительных отношений.</p>
<p>Для этого открываем веб-панель управления ALD Pro и переходим в раздел "Управление доменом" -> "Интеграция с MS AD"</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/01.jpg/bf5efb48-1b8a-ab2f-4af4-b39f49955f24?t=1702550636696&imagePreview=1" /></p>
<p>Там нажимаем "Новое подключение к Active Directory"</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/02.jpg/fb2a2ef8-4ea0-6a8a-3cdc-ba5d76475d03?t=1702550758256&imagePreview=1" /></p>
<p>Заполняем поля:</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/03.jpg/68abd0f6-4a7e-4637-bcde-3d53aa9d973d?t=1702556820633&imagePreview=1" /></p>
<p>"Домен" - имя домена, с которым настраиваем доверительные отношения</p>
<p>Влючаем "Перенаправление зоны DNS", чтобы имя windows-домена могло резолвиться</p>
<p>"IP-адрес DNS сервера" - IP-адрес DNS сервера, который может резолвить имя windows-домена</p>
<p>Ставим галку на "Доверительные отношения" и если надо, то и на "Двусторонние доверительные отношения"</p>
<p>и нажимаем сохранить</p>
<p>Если возникает ошибка </p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/04.jpg/2336f3ec-3629-dd48-58c7-27c1ca3514fa?t=1702556886400&imagePreview=1" /></p>
<p>Далее идём в интерфейс управления FreeIPA (ALD Pro работает на её основе) по адресу</p>
<p>https://adc01.astra.lab/ipa/ui/ (только укажите имя вашего домена)</p>
<p>изначально появится окошко с предложением ввести логин и пароль</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/05.jpg/a76bdb2b-fb8b-75df-0087-8102c5602842?t=1702557487812&imagePreview=1" /></p>
<p>нажимаем Cancel пару раз и загрузится уже веб-интерфейс FreeIPA с предложением логина и пароля</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/06.jpg/5657af1b-02e2-6208-3b61-c1efee974670?t=1702557603486&imagePreview=1" /></p>
<p>Здесь уже вводим учётные данные администратора домена (имя admin и пароль, указанный при настройке ALD Pro)</p>
<p>Идём в "IPA Server" -> "Trusts" -> "Trusts"</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/09.jpg/04776e9f-fc9f-2226-91e8-0077ecd5b739?t=1702559286646&imagePreview=1" /></p>
<p>И нажимаем кнопку Add</p>
<p>Появится окно настройки, которое заполняем следующим образом</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/07.jpg/c5659e10-b8b2-3c7a-31cc-b3ac92945669?t=1702559358602&imagePreview=1" /></p>
<p>нажимаем Add, он какое-то время думает и выдает следующую ошибку:</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/08.jpg/0e7e5d8f-2d9b-8ee5-d7ff-8f6bd8079d1a?t=1702559789114&imagePreview=1" /></p>
<p>нажимаем Cancel, закрываем окно добавления доверия. Нажимаем Refresh и видим созданное доверие:</p>
<p><img src="https://www.tune-it.ru/documents/18325185/0/10.jpg/67554695-2425-3237-8b83-c3c9abf5f737?t=1702890554950&imagePreview=1" /></p>
<p>после данной операции доверительные отношения в ALD Pro будут создаваться без ошибок.</p>Денис Серянкин2023-12-14T10:37:00ZУстановка ALD Pro 2.1.0 на Astra Linux 1.7.4Денис Серянкинhttps://www.tune-it.ru/c/blogs/find_entry?entryId=190169152023-12-13T08:20:16Z2023-12-08T10:47:00Z<p>ALD Pro - это служба каталога для Astra Linux. С поддержкой доверительных отношений и возможностью миграции объектов с MS Active Directory. В этой статье рассмотрим, как пошагово настроить ALD Pro 2.1.0.</p>
<p>Для работы первого контроллера домена требуется 8 ядер процессора, 16ГБ оперативной памяти и 50ГБ свободного дискового пространства.</p>
<p>Так же потребуется установленная Astra Linux версии не выше 1.7.4 в режиме защищённости Смоленск и графической оболочкой Fly.</p>
<p>Первым делом надо настроить сеть. Во избежание конфликтов настройки сети выключаем network-manager:</p>
<pre class="brush:bash;ruler:true;">
sudo systemctl stop network-manager
sudo systemctl disable network-manager</pre>
<p>И настраиваем статический IP-адрес вручную. Для этого открываем файл конфигурации сетевых интерфейсов:</p>
<pre>
<code>sudo nano /etc/network/interfaces</code></pre>
<p>Добавляем следующие строки (только измените IP-адреса и имена интерфесов на свои):</p>
<pre>
<code>auto eth0
iface eth0 inet static
address 192.168.0.100
netmask 255.255.255.0
gateway 192.168.0.1
dns-nameservers 8.8.8.8
dns-search astra.lab</code></pre>
<p>Так же добавим информацию о днс сервере в resolv.conf:</p>
<pre>
<code>sudo nano /etc/resolv.conf</code></pre>
<pre>
<code>nameserver 8.8.8.8
search astra.lab</code></pre>
<p>настраиваем имя контроллера домена</p>
<pre>
<code>sudo hostnamectl set-hostname adc01.astra.lab</code></pre>
<p>Поправим файл hosts</p>
<pre>
sudo nano /etc/hosts</pre>
<pre>
127.0.0.1 localhost.localdomain localhost
192.168.0.100 adc01.astra.lab adc01
127.0.1.1 adc01</pre>
<p>На этом настройка сети закончена. Далее настраиваются репозитории.</p>
<p>Для начала открываем sources.list:</p>
<pre>
<code>sudo nano /etc/apt/sources.list</code></pre>
<p>В нём надо закоментировать все записи (по умолчанию не закоментирован только cdrom) и добавить следующие записи:</p>
<p>deb http://dl.astralinux.ru/astra/frozen/1.7_x86-64/1.7.4/repository-base 1.7_x86-64 main non-free contrib<br />
deb http://dl.astralinux.ru/astra/frozen/1.7_x86-64/1.7.4/repository-extended 1.7_x86-64 main contrib non-free</p>
<p>Это замороженные версии репозиториев, где пакеты не обновляются. Если версии пакетов обновятся, то с очень высокой вероятностью ALD Pro не установится.</p>
<p>Далее создаем файл настроек репозитория ALD Pro:</p>
<pre>
<code>sudo nano /etc/apt/sources.list.d/aldpro.list</code></pre>
<p>И пишем в него:</p>
<pre>
<code>deb https://dl.astralinux.ru/aldpro/stable/repository-main/ 2.1.0 main
deb https://dl.astralinux.ru/aldpro/stable/repository-extended/ generic main</code></pre>
<p>Создаем файл приоритета apt:</p>
<pre>
<code>sudo nano /etc/apt/preferences.d/aldpro</code></pre>
<pre>
<code>Package: *
Pin: release n=generic
Pin-Priority: 900</code></pre>
<p>После этого нужно обновить систему следующей командой:</p>
<pre>
<code>sudo apt update && sudo apt install astra-update -y && sudo astra-update -A -r -T</code></pre>
<p>Если пошли какие-то ошибки, что не может найти какого репозитория, то проблема скорее всего либо в отсутствии интернета, либо опечатка в файле с репозиториями.</p>
<p>Перезагружаемся:</p>
<pre>
<code>sudo reboot</code></pre>
<p>Следующей командой устанаваливаем сам ALD Pro с модулями глобального каталога и синхронизации:</p>
<p>sudo DEBIAN_FRONTEND=noninteractive apt-get install -q -y aldpro-mp aldpro-gc aldpro-syncer</p>
<p>Далее важный шаг - это исправить в сетевых настройках DNS-сервер на локальный:</p>
<pre>
<code>sudo nano /etc/resolv.conf</code></pre>
<pre>
<code>nameserver 127.0.0.1
search astra.lab</code></pre>
<pre>
<code>sudo nano /etc/network/interfaces</code></pre>
<pre>
<code>auto eth0
iface eth0 inet static
address 192.168.50.166
netmask 255.255.255.0
gateway 192.168.50.254
dns-nameservers 127.0.0.1
dns-search astra.lab</code></pre>
<p>Перезапускаем службу сети:</p>
<pre>
<code>sudo systemctl restart networking</code></pre>
<p>Далее остаётся только продвинуть сервер до роли контроллера домена следующей командой:</p>
<pre>
<code>sudo aldpro-server-install -d astra.lab -n adc01 -p 123456 --ip 192.168.50.166 --no-reboot --setup_syncer --setup_gc</code></pre>
<p>где -d - указываем имя домена</p>
<p>-n - имя сервера</p>
<p>-p - пароль будущего админа контроллера домена (не используйте простые пароли, как в этом примере)</p>
<p>-ip - IP-адрес контроллера домена</p>
<p>--setup_syncer - настроить модуль синхронизации</p>
<p>--setup_gc - настроить модуль глобального каталога</p>
<p>после завершения работы скрипта остается только перезагрузить сервер</p>
<pre>
<code>sudo reboot</code></pre>
<p>Теперь можно зайти в веб-интерфейс ALD Pro по HTTPS с самого КД:</p>
<pre>
<code>https://adc01</code></pre>
<p>Либо по IP-адресу с другого компьютера:</p>
<pre>
<code>https://192.168.50.166</code></pre>
<p>Логин по умолчанию admin. А пароль тот, что указывали при настройке КД.</p>
<p>На этом установка контроллера домена на основе ALD Pro закончена.</p>Денис Серянкин2023-12-08T10:47:00ZКак собрать Slurm и не сойти с умаDanil Khanalainenhttps://www.tune-it.ru/c/blogs/find_entry?entryId=183551012023-11-30T16:10:45Z2023-11-30T16:10:00Z<p>Slurm - это бесплатный планировщик задач с открытым исходным кодом, который можно использовать в HPC (High-performance computing). Данная статья призвана минимизировать количество проблем, возникающих у пользователя при сборке данного ПО.</p>
<h2>Последовательность действий</h2>
<p>Автор использовал ОС Ubuntu Server 22.04. Стоит упомянуть, что сборка/установка пакетов должна быть одинакова на всех нодах кластера.</p>
<h3>Munge</h3>
<p>Нельзя просто взять и установить Slurm. Для начала нужно установить Munge (<a href="https://dun.github.io/munge">https://dun.github.io/munge</a>) - сервис аутентификации. Он нужен для того, чтобы ноды в кластере могли опознать друг друга с помощью ключа. Рекомендуется создать для него отдельного пользователя без права входа для максимальной безопасности. Для удобства запуск демона будет производиться от root, и только сборка от отдельного пользователя "munge". Собирается так:</p>
<pre class="brush:bash;">
sudo su
addgroup munge
adduser --gecos "" --ingroup munge --disabled-login --uid 1001 munge
apt-get install libssl-dev
cd /home/munge
wget https://github.com/dun/munge/releases/download/munge-0.5.15/munge-0.5.15.tar.xz
tar xJf munge-0.5.15.tar.xz
chown -R munge munge-0.5.15
cd munge-0.5.15
runuser -l munge -c '/home/munge/munge-0.5.15/configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --runstatedir=/run'
runuser -l munge -c 'make'
runuser -l munge -c 'make check'
cd ..
make install</pre>
<p>Важно, чтобы GID и UID munge-пользователя совпадали на всех нодах кластера! </p>
<p>После этого можно указать пользователя, от которого будет производиться запуск, в файле /lib/systemd/system/munge.service:</p>
<pre>
User=munge -> User=root
Group=munge -> Group=root</pre>
<p> </p>
<p>Далее нужно сгенерировать ключ, который будет находиться на всех машинах кластера, использующих Slurm:</p>
<pre class="brush:bash;">
cd /etc/munge
sudo -u munge mungekey</pre>
<p>Нужен только один ключ на кластер, поэтому он генерируется один раз. Поскольку для примера используем пользвателя root для запуска, меняем права и владельца ключа:</p>
<pre class="brush:bash;">
sudo chmod 0600 /etc/munge/munge.key
sudo chown root /etc/munge/munge.key</pre>
<p>После этого munge-демон должен успешно запуститься:</p>
<pre class="brush:bash;">
systemctl daemon-reload
systemctl enable munge.service
systemctl start munge.service
systemctl status munge.service</pre>
<h3>Slurm</h3>
<p>Если есть необходимость использовать MPI, то нужно произвести еще несколько действий. Соберем и установим OpenMPI:</p>
<pre class="brush:bash;">
wget https://download.open-mpi.org/release/open-mpi/v4.1/openmpi-4.1.5.tar.gz
tar xfz openmpi-4.1.5.tar.gz
cd openmpi-4.1.5
./configure --with-cuda=/usr/local/cuda
make -j 32 all 2>&1 | tee make.out
sudo make install 2>&1 | tee install.out
sudo ldconfig</pre>
<p>И установим пару необходимых пакетов:</p>
<pre class="brush:bash;">
sudo apt install libpmix-dev
sudo apt install libdbus-1-dev</pre>
<p>Далее можно приступить к сборке и установке самого Slurm:</p>
<pre class="brush:bash;">
wget https://download.schedmd.com/slurm/slurm-23.02.3.tar.bz2
tar --bzip -x -f slurm-23.02.3.tar.bz2
cd slurm-23.02.3
./configure --with-pmix=/usr/lib/x86_64-linux-gnu/pmix2
make
sudo make install
</pre>
<h3>Баг: отсутствуют slurmd.service slurmctld.service</h3>
<p>После make install сервисы могут не скопироваться в /lib/systemd/system. Можно сделать это вручную:</p>
<pre class="brush:bash;">
cd slurm-23.02.3
sudo cp ./etc/slurmd.service /lib/systemd/system
sudo cp ./etc/slurmctld.service /lib/systemd/system
sudo systemctl daemon-reload
sudo systemctl enable slurmd
sudo systemctl enable slurmctld</pre>
<h3>Запуск</h3>
<p>Сначала нужно создать файл конфигурации. Для этого есть специальная страница - <a href="https://slurm.schedmd.com/configurator.html">https://slurm.schedmd.com/configurator.html</a>. Также существует упрощенный конфигуратор, можно использовать для тестового запуска - <a href="https://slurm.schedmd.com/configurator.easy.html">https://slurm.schedmd.com/configurator.easy.html</a>.</p>
<p>После выбора всех опций и нажатия кнопки "submit" получим текст конфига - его записываем в /usr/local/etc/slurm.conf. Он также должен быть одинаковым на всех машинах в кластере.</p>
<p>slurmctld (управляющий демон) и slurmd (исполняющий демон) запускаются просто:</p>
<pre class="brush:bash;">
systemctl start slurmctld
systemctl start slurmd</pre>
<p>Можно запускать оба на одной машине.</p>
<p> </p>
<p>Вот так, разобравшись с зависимостями в виде Munge, OpenMPI и нескольких неочевидных пакетов, можно самостоятельно собрать, установить и сконфигурировать Slurm.</p>Danil Khanalainen2023-11-30T16:10:00ZKotlin + WebFlux + Bucket4K/Bucket4J + Lettuce. Усовершенствуем API с помощью Rate Limiting.Romo Fedoroffhttps://www.tune-it.ru/c/blogs/find_entry?entryId=187731672023-11-27T20:41:52Z2023-11-27T18:26:00Z<style type="text/css">article p {
font-size:11pt;
font-family:Verdana, sans-serif;
text-align:justify;
color:#6a6a6a;
}
article img {
width: 90%;
}
.centered {
text-align:center;
}
</style>
<p class="centered"><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/bucket-2.jpg/478180e2-c964-4195-3b3d-323ca73a4b9d?imagePreview=1" /></p>
<h2>Введение</h2>
<p>Rate Limiting (Ограничение скорости) - это важная стратегия ограничения доступа к API (и не только). Она ограничивает количество вызовов API, которые клиент может совершить за определенный промежуток времени. Это позволяет защитить API от чрезмерного использования, как непреднамеренного, так и злонамеренного.</p>
<p>В мире Java существует несколько различных готовых решений, реализующих стратегию ограничения скорости.</p>
<p>В этой статье мы рассмотрим одно из таких решений - популярную, надежную и производительную библиотеку Bucket4J.</p>
<p>Мы не будет создавать проект с нуля. Мы за основу возьмем проект, который реализовывали в рамках предыдущей статьи. Ее вы можете прочитать <a href="https://www.tune-it.ru/web/romo/blog/-/blogs/reactive-rest-api-kotlin-webflux-r2dbc" target="_blank">здесь</a>.</p>
<p>Кратко напомню, что в ней мы настраивали проект в IntelliJ Idea для создания реактивного REST API приложения на Kotlin и WebFlux.</p>
<p>Также мы создавали демонстрационное приложение "АПИ интернет магазина обучающих курсов онлайн", используя реактивное программирование.</p>
<p>В этой статье, для краткости, я буду приводить лишь код, который отличается от кода, реализованного в прошлый раз. Однако, при необходимости, полный исходный код из этой статьи вы сможете найти в моем репозитории на гитхабе (<a href="https://github.com/romo-it/courses-shop-reactive/tree/main" target="_blank">здесь</a>).</p>
<h2>1 Добавление зависимостей</h2>
<p>Первым делом давайте добавим в проект нужные зависимости.</p>
<p>Нам понадобятся следующие зависимости:</p>
<p>a) Bucket4k (<a href="https://github.com/ksletmoe/Bucket4k" target="_blank">https://github.com/ksletmoe/Bucket4k</a>) - Kotlin-обертка для Bucket4j, которая может приостанавливать свою работу и отлично дружит с coroutines. Внутри себя также содержит код классического Bucket4J.</p>
<p>b) Kotlinx-coroutines-core - необходима нам для того, чтобы работать с coroutines</p>
<p>c) Bucket4j-redis и Lettuce-core - нужны для того, чтобы реализовать работу Bucket4j в распределенных системах</p>
<p>Файл build.gradle.kts со всеми нужными зависимостями выглядит так:</p>
<pre class="brush:jscript;">
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "3.1.4"
id("io.spring.dependency-management") version "1.1.3"
kotlin("jvm") version "1.8.22"
kotlin("plugin.spring") version "1.8.22"
}
group = "com.tuneit"
version = "0.0.1-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("org.springframework.boot:spring-boot-starter-validation:3.1.4")
implementation("com.sletmoe.bucket4k:bucket4k:1.0.0")
implementation("com.bucket4j:bucket4j-redis:8.2.0")
implementation("io.lettuce:lettuce-core:6.2.6.RELEASE")
runtimeOnly("org.postgresql:postgresql")
runtimeOnly("org.postgresql:r2dbc-postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.projectreactor:reactor-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "17"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}</pre>
<h2>2 Ограничение скорости с помощью Bucket4k и поддержкой coroutines (Bucket4K + Coroutines)</h2>
<p>Функционал Rate Limiting можно реализовать на уровне контроллера или даже эндпоинта. Однако при каждом добавлении нового контроллера и/или эндпоинта возникнет необходимость снова и снова писать однотипный повторяющийся код для подключения и настройки Bucket4J. Лучшим вариантом, который лишен вышеуказанной проблемы, является использование интерфейса Filter.</p>
<p>В классическом Spring им может стать OncePerRequestFilter, в реактивном Spring обычно используется WebFilter или его разновидность CoWebFilter, которая специально создана для Kotlin и поддерживает coroutines.</p>
<p>Так как мы используем реактивный Spring и Kotlin, то неплохой идеей будет попробовать реализовать асинхронную работу Bucket4J с помощью фичи Kotlin - coroutines.</p>
<p>Поэтому создадим package с названием filters и добавим в него класс "CoRateLimitFilter" со следующим содержимым:</p>
<pre class="brush:java;">
package com.tuneit.coursesshopreactive.filters
import com.sletmoe.bucket4k.SuspendingBucket
import com.tuneit.coursesshopreactive.model.RateLimitException
import io.github.bucket4j.Bandwidth
import io.github.bucket4j.Refill
import org.springframework.http.HttpMethod
import org.springframework.stereotype.Component
import org.springframework.web.util.pattern.PathPattern
import org.springframework.web.util.pattern.PathPatternParser
import java.time.Duration
import kotlinx.coroutines.*
import org.springframework.core.Ordered
import org.springframework.http.server.reactive.ServerHttpRequest
import org.springframework.web.server.*
import java.security.MessageDigest
import kotlin.collections.HashMap
import kotlin.time.DurationUnit
import kotlin.time.toDuration
@Component
class CoRateLimitFilter : CoWebFilter(), Ordered {
//A Kotlin wrapper around Bucket4j which suspends and plays nicely with coroutines.
//https://github.com/ksletmoe/Bucket4k
val localBuckets = HashMap<String, SuspendingBucket>()
private fun createSuspendingBucket() : SuspendingBucket = SuspendingBucket.build {
addLimit(Bandwidth.classic(50, Refill.intervally(50, Duration.ofHours(1))))
addLimit(Bandwidth.classic(5, Refill.greedy(5, Duration.ofMinutes(1))))
}
private fun getBucketKeyForRemoteAddr(request: ServerHttpRequest): String {
val ipFromHeader: String? = request.headers.getFirst("X-FORWARDED-FOR")
val ip = if (ipFromHeader.isNullOrBlank()) request.remoteAddress.toString() else ipFromHeader
return MessageDigest.getInstance("SHA-256")
.digest(ip.toByteArray())
.fold(StringBuilder()) { sb, it -> sb.append("%02x".format(it)) }.toString()
}
override suspend fun filter(exchange: ServerWebExchange, chain: CoWebFilterChain) {
val request = exchange.request
val response = exchange.response
val bucketKey = getBucketKeyForRemoteAddr(request)
val localBucket = localBuckets.getOrPut(bucketKey) { createSuspendingBucket() }
if(urlMatches(request)) {
val isConsumed = CoroutineScope(Dispatchers.IO).async {
localBucket.tryConsume(1, 1.toDuration(DurationUnit.SECONDS))
}.await()
val bucketInfo = localBucket.toString().split("[", "]")[1].split(", ")
val availableTokensInLongPeriod = bucketInfo[1].toInt().coerceAtLeast(0)
val availableTokensInShortPeriod = bucketInfo[4].toInt().coerceAtLeast(0)
if(isConsumed) {
response.headers.set("X-Rate-Limit-Remaining",
"$availableTokensInShortPeriod/$availableTokensInLongPeriod"
)
return chain.filter(exchange)
}
throw RateLimitException("Too many requests")
}
return chain.filter(exchange)
}
val pathsToFilter: List<PathPattern> =
listOf(PathPatternParser.defaultInstance.parse("/courses"),
PathPatternParser.defaultInstance.parse("/courses/{id}"))
private fun urlMatches(request: ServerHttpRequest) : Boolean {
return pathsToFilter.any { it.matches(request.path.pathWithinApplication())}
&& request.method.matches(HttpMethod.GET.name())
}
override fun getOrder(): Int {
return 1;
}
}</pre>
<p>Давайте рассмотрим основные моменты в реализации фильтра.</p>
<pre class="brush:java;">
val localBuckets = HashMap<String, SuspendingBucket>()</pre>
<p>Здесь мы объявляем карту, где будем хранить все Bucket (ведра), которые будут создаваться в ходе работы приложения.</p>
<p>Информацию о Buckets можно хранить также в сессии сервера, использовать внешние хранилища, такие как реляционные базы данных, или же другие продукты, предназначенные для реализации распределенных систем: Infinispan, Hazelcast, Coherence, Ignite, Redis.</p>
<p>Немного позже в этой статье мы рассмотрим как сконфигурировать Bucket4J для работы в распределенной системе с помощью Redis библиотеки - Lettuce.</p>
<p>Возвращаемся к фильтру.</p>
<p>Далее идет функция</p>
<pre class="brush:java;">
private fun createSuspendingBucket() : SuspendingBucket = SuspendingBucket.build {
addLimit(Bandwidth.classic(50, Refill.intervally(50, Duration.ofHours(1))))
addLimit(Bandwidth.classic(5, Refill.greedy(5, Duration.ofMinutes(1))))
}</pre>
<p>В ней создается и возвращается SuspendingBucket - Bucket, который хорошо работает с coroutines.</p>
<p>При создании Bucket мы указываем его лимиты и стратегию пополнения.</p>
<p>Перед тем как пойти дальше, хочется (и даже возможно нужно ;) ) немного коснуться теории.</p>
<p>Rate Limiting может быть реализовано с помощью множества различных алгоритмов: Token bucket, Leaky bucket, Fixed window counter, Sliding window log и др.</p>
<p>В сердце библиотеки Bucket4J лежит алгоритм Token bucket. Здесь, по ссылке, немного wiki про это (<a href="https://ru.wikipedia.org/wiki/Алгоритм_текущего_ведра" target="_blank">https://ru.wikipedia.org/wiki/Алгоритм_текущего_ведра</a>)</p>
<p>Основы этого алгоритма понять несложно. Предположим, что у нас есть ведро (Bucket), в которое можно поместить какое-то максимальное количеством жетонов (токенов). Каждый раз, когда клиент (или Потребитель) хочет обратиться к сервису или запросить ресурс, он должен вынуть из ведра один или несколько жетонов. Потребитель (Consumer) может воспользоваться сервисом только в том случае, если он может извлечь необходимое количество жетонов. Если в ведре нет необходимого количества жетонов, ему нужно подождать, пока в ведре не будет достаточного количества.</p>
<p>Потребитель, тот кто пользуется нашим сервисом, он забирает жетоны.. А кто же их тогда кладет в ведро? В алгоритме существует также Пополнитель (Refiller), который периодически создает новые жетоны и кладет их в ведро.</p>
<p>Библиотека Bucket4J поддерживает два типа Пополнителя:</p>
<pre class="brush:java;">
Refill.intervally(50, Duration.ofHours(1))</pre>
<p>"Интервальный" Пополнитель "intervally" ждет, пока пройдет заданное количество времени, и затем кладет в ведро все жетоны сразу. В нашем примере он добавляет по 50 жетонов каждый час.</p>
<pre class="brush:java;">
Refill.greedy(5, Duration.ofMinutes(1))</pre>
<p>"Жадный" Пополнитель "greedy" добавляет жетоны более торопливо. В данном примере Refiller делит одну минуту на пять периодов. Затем в каждый из этих периодов он помещает в ведро по одному жетону. Другими словами он кладет в ведро по одному жетону каждые 12 секунд.</p>
<p>Важно еще раз отметить, что ведра имеют максимальную емкость. Поэтому Refiller кладет жетоны в ведро только до тех пор, пока не упрется в максимум. Если ведро заполнено (количество жетонов = вместимости), он больше не кладет в него жетоны.</p>
<p>Емкость задается при добавлении лимита:</p>
<pre class="brush:java;">
addLimit(Bandwidth.classic(50, Refill.intervally(50, Duration.ofHours(1))))</pre>
<p>Таким образом, максимальная емкость ведра в нашем случае составляет 50 жетонов.</p>
<p>И если эти жетоны израсходовать в течении часа, то нужно будет ждать окончание этого часа, чтобы заполучить новые 50 жетонов.</p>
<p>А зачем необходим второй лимит?</p>
<pre class="brush:java;">
addLimit(Bandwidth.classic(5, Refill.greedy(5, Duration.ofMinutes(1))))</pre>
<p>Вторым лимитом мы ограничиваем потребление жетонов в более коротком промежутке времени. Так, в течении минуты мы не можем потребить сразу более 5 жетонов.</p>
<p>Если соединить эти два лимита воедино, то получим: что на час нам дается "потолок" в размере 50 жетонов, которые пополняются единожды каждый час (вновь до 50-ти), при этом в течении минуты максимум мы единоразово может потребить только 5 жетонов и не более. Лимит в 5 жетонов/мин пополняется каждые 12 секунд по одному жетону. Все это значит, что клиенты нашего АПИ смогут обратиться к нему не более 50-ти раз в течении часа (затем им нужно будет ждать окончание этого часа) и смогут совершить не более 5-ти одновременных вызовов в течении минуты или не более 5 последовательных вызовов в течении 12 секунд, а потом им придется ждать каждые 12 секунд, чтобы сделать еще одни вызов (уверен суть вы поняли).</p>
<p>P.S. теорию я кстати взял из классной статьи вот здесь (<a href="https://golb.hplar.ch/2019/08/rate-limit-bucket4j.html" target="_blank">https://golb.hplar.ch/2019/08/rate-limit-bucket4j.html</a>)</p>
<p>Разобравшись с теорией и лимитами, продолжим разбор исходного кода нашего фильтра.</p>
<p>Рассмотрим следующую функцию:</p>
<pre class="brush:java;">
private fun getBucketKeyForRemoteAddr(request: ServerHttpRequest): String {
val ipFromHeader: String? = request.headers.getFirst("X-FORWARDED-FOR")
val ip = if (ipFromHeader.isNullOrBlank()) request.remoteAddress.toString() else ipFromHeader
return MessageDigest.getInstance("SHA-256")
.digest(ip.toByteArray())
.fold(StringBuilder()) { sb, it -> sb.append("%02x".format(it)) }.toString()
}</pre>
<p>Ограничения в API часто делаются по IP-адресу или более специфичным для бизнеса способом, например, с помощью ключей API или маркеров доступа.</p>
<p>В нашем примере, мы делаем ограничение по IP-адресу. Для каждого уникального пользователя API (уникальность определяется по ip), мы создаем и храним свой Bucket.</p>
<p>Функция выше определяет с какого ip приходит запрос к АПИ, вычисляет и возвращает hash от этого ip (чтобы не хранить ip в явном виде; забота о личных данных как говорится :)). Этот hash мы используем в качестве ключа для карты, в которой храним все наши Buckets.</p>
<p>Далее идет главная функция фильтра - filter (), где разыгрывается основное действие:</p>
<p>по данным http-запроса, определяем ip адрес пользователя АПИ. По этому адресу вычисляем ключ для карты. Далее смотрим есть ли в карте данные по этому ключу. Если нет, то создаем новый Bucket и помещаем в карту, если данные уже есть - то достаем существующий Bucket из карты.</p>
<p>Затем смотрим соответствует ли адрес запроса определенному нами шаблону:</p>
<pre class="brush:java;">
val pathsToFilter: List<PathPattern> =
listOf(PathPatternParser.defaultInstance.parse("/courses"),
PathPatternParser.defaultInstance.parse("/courses/{id}"))
private fun urlMatches(request: ServerHttpRequest) : Boolean {
return pathsToFilter.any { it.matches(request.path.pathWithinApplication())}
&& request.method.matches(HttpMethod.GET.name())
}</pre>
<p>В примере мы сделали так, что ограничения Rate Limiting накладываются только на GET-эндпоинты courses и courses/{id}</p>
<p>Если адрес запроса соответствует шаблону, то выполняем все необходимые мероприятия по Rate Limiting, иначе - фильтр ничего не делает и просто передает эстафету дальше по цепи.</p>
<pre class="brush:java;">
val isConsumed = CoroutineScope(Dispatchers.IO).async {
localBucket.tryConsume(1, 1.toDuration(DurationUnit.SECONDS))
}.await()
</pre>
<p>Здесь мы с помощью coroutine асинхронно пытаемся вытащить из Bucket один жетон.</p>
<p>Если попытка успешная, то есть isConsumed = true, то мы разрешаем фильтру передать управление дальше по цепи, то есть разрешаем обычный цикл запроса к АПИ.</p>
<p>Пользователь при этом получит желаемый ответ на свой запрос.</p>
<p>При этом в ответ мы добавляем заголовок X-Rate-Limit-Remaining, в котором содержится кол-во оставшихся доступных токенов.</p>
<p>Так как почему-то в реализации Bucket4K не работает нормально расчет оставшегося кол-ва жетонов, мне пришлось доставать эти данные из мета-данных Bucket:</p>
<pre class="brush:java;">
val bucketInfo = localBucket.toString().split("[", "]")[1].split(", ")
val availableTokensInLongPeriod = bucketInfo[1].toInt().coerceAtLeast(0)
val availableTokensInShortPeriod = bucketInfo[4].toInt().coerceAtLeast(0)</pre>
<p>Если же в Bucket недостаточно жетонов, то мы выбрасываем ошибку RateLimitException и прерываем цикл выполнения запроса к АПИ. Пользователь увидит ошибку: "Слишком много запросов".</p>
<p>Класс ошибки RateLimitException находится в файле Errors.kt и реализован следующим образом:</p>
<pre class="brush:java;">
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
data class RateLimitException(val msg: String) : RuntimeException(msg)</pre>
<p>Вот что видит пользователь API, когда жетонов в ведре достаточно и доступ разрешен:</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/Screenshot+2023-11-27+at+20.20.32.png/1b49f718-55b1-b56d-af79-2f99f29b7368?imagePreview=1" /></p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/Screenshot+2023-11-27+at+20.20.53.png/adc35e15-d658-d5fc-2d16-4b9f17cb213a?imagePreview=1" /></p>
<p>А вот, что он видит когда все жетоны израсходованы:</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/Screenshot+2023-11-27+at+20.21.13.png/7bb7595c-8507-59ec-d8b6-0816ec6753db?imagePreview=1" /></p>
<h2>3 Ограничение скорости с помощью Bucket4J в распределенной системе (Bucket4J + WebFlux + Redis Lettuce)</h2>
<p>В предыдущем примере, информация о Buckets хранилась в памяти приложения. Это неплохо когда кол-во пользователей АПИ небольшое и нагрузки невелики. Однако обычно в реальных приложения дела обстоят с точностью наоборот. Для компенсации нагрузки и обеспечения стабильности работы сервисов, в таких случаях, обычно одновременно работают несколько экземпляров приложения на разных серверах, а балансировщик равномерно распределяет нагрузку между этими серверами. Такую схему работы обычно называют распределенной или distributed. Локальное хранение Buckets в памяти каждого из экземпляров приложения будет ошибочным при такой схеме, ведь нам нужно чтобы все экземпляры имели общее хранилище Buckets, а не каждый свое. Другими словами нам нужен единый лимит токенов для всех экземпляров приложения в распределенной системе. Как упоминалось в статье выше, к счастью, Bucket4J поддерживает разные продукты для работы в распределенных средах. Мне как то по душе больше Redis и поэтому в примере я решил использовать его, а именно одну из библиотек на его основе - Lettuce. Эта библиотека прекрасно дружит с WebFlux, асинхронна, поэтому я выбрал ее.</p>
<p>Однако есть и ложка дегтя во всей этой истории.. как оказалось Bucket4K пока еще не поддерживает Redis... (ну или я не смог разобраться, но все же кажется что не поддерживает). Поэтому придется попрощаться с coroutines... и вернуться к webflux да и только. Но не беда! Как оказалось, после сравнения производительности Bucket4K + coroutines vs Bucket4J + WebFlux... оказалось, что последний союз работает быстрее, а иногда и в 10-ки раз быстрее.</p>
<p>И так приступим!</p>
<p>Для начала, запустим экземпляр Redis в докере <strong>(в терминале: docker-compose up -d)</strong>.</p>
<pre class="brush:jscript;">
docker-compose.yml:
version: '3.8'
services:
postgres:
container_name: "reactive-postgres"
image: postgres
ports:
- "127.0.0.1:5432:5432"
environment:
POSTGRES_USER: demo
POSTGRES_PASSWORD: demo
POSTGRES_DB: coursesshopreactive
redis-local:
container_name: "reactive-redis"
image: redis
ports:
- "6379:6379" </pre>
<p>Готово. Супер.</p>
<p>Далее, надо в файле application.yml добавить несколько строк, связанных с Redis:</p>
<pre class="brush:jscript;">
spring:
r2dbc:
url: "r2dbc:postgresql://localhost:5432/coursesshopreactive"
username: demo
password: demo
data:
redis:
host: localhost
port: 6379
server:
port: 8080
error:
include-message: always</pre>
<p>И наконец, нужно создать конфигурационный класс, посвященный Lettuce.</p>
<p>Для этого, создадим package "configuration" и в нем создадим класс "LettuceConfig":</p>
<pre class="brush:java;">
package com.tuneit.coursesshopreactive.configuration;
import io.github.bucket4j.distributed.ExpirationAfterWriteStrategy
import io.github.bucket4j.redis.lettuce.cas.LettuceBasedProxyManager
import io.lettuce.core.RedisClient
import io.lettuce.core.RedisURI
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.Duration
@Configuration
class LettuceConfig (@Value("\${spring.data.redis.host}") val host : String,
@Value("\${spring.data.redis.port}") val port: Int) {
@Bean
fun redisClient(): RedisClient {
return RedisClient.create(RedisURI.builder().withHost(host).withPort(port).build())
}
@Bean
fun lettuceProxyManager(): LettuceBasedProxyManager {
return LettuceBasedProxyManager.builderFor(redisClient())
.withExpirationStrategy(ExpirationAfterWriteStrategy
.basedOnTimeForRefillingBucketUpToMax(Duration.ofSeconds(10)))
.build()
}
}</pre>
<p>Этот класс я не буду разбирать подробнее, он вполне стандартный и все нюансы настройки RedisClient и LettuceBasedProxyManager вы сможете легко найти в интернете сами.</p>
<p>Поэтому, продолжим.</p>
<p>Как вы могли наверное догадаться, тот CoRateLimitFilter, который мы использовали в предыдущем кейсе, нам не подходит. Во-первых, в текущем кейсе мы уже не можем использовать coroutines и, во-вторых, нам нужно реализовать взаимодействие с Lettuce.</p>
<p>Вместо CoRateLimitFilter, мы напишем новый другой фильтр.</p>
<p>В package "filters" добавим класс "RateLimitFilter":</p>
<pre class="brush:java;">
package com.tuneit.coursesshopreactive.filters
import com.tuneit.coursesshopreactive.model.RateLimitException
import io.github.bucket4j.Bandwidth
import io.github.bucket4j.BucketConfiguration
import io.github.bucket4j.ConsumptionProbe
import io.github.bucket4j.Refill
import io.github.bucket4j.distributed.AsyncBucketProxy
import io.github.bucket4j.redis.lettuce.cas.LettuceBasedProxyManager
import org.springframework.boot.web.reactive.filter.OrderedWebFilter
import org.springframework.http.HttpMethod
import org.springframework.http.server.reactive.ServerHttpRequest
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilterChain
import org.springframework.web.util.pattern.PathPattern
import org.springframework.web.util.pattern.PathPatternParser
import reactor.core.publisher.Mono
import java.security.MessageDigest
import java.time.Duration
import java.util.concurrent.TimeUnit
@Component
class RateLimitFilter (val lettuceProxyManager: LettuceBasedProxyManager): OrderedWebFilter {
private fun getBucketConfig(): BucketConfiguration {
val conf = BucketConfiguration.builder()
conf.addLimit(Bandwidth.classic(50, Refill.intervally(50, Duration.ofHours(1))))
conf.addLimit(Bandwidth.classic(5, Refill.greedy(5, Duration.ofMinutes(1))))
return conf.build()
}
private fun getBucketKeyForRemoteAddr(request: ServerHttpRequest): ByteArray {
val ipFromHeader: String? = request.headers.getFirst("X-FORWARDED-FOR")
val ip = if (ipFromHeader.isNullOrBlank()) request.remoteAddress.toString() else ipFromHeader
return MessageDigest.getInstance("SHA-256").digest(ip.toByteArray())
}
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val request = exchange.request
val bucketKey = getBucketKeyForRemoteAddr(request)
val bucket: AsyncBucketProxy = lettuceProxyManager.asAsync()
.builder()
.build(bucketKey, getBucketConfig())
if(urlMatches(request)) {
return Mono
.fromFuture(bucket.tryConsumeAndReturnRemaining(1))
.flatMap { handleConsumptionProbe(exchange, chain, it) }
}
return chain.filter(exchange)
}
val pathsToFilter: List<PathPattern> =
listOf(PathPatternParser.defaultInstance.parse("/courses"),
PathPatternParser.defaultInstance.parse("/courses/{id}"))
private fun urlMatches(request: ServerHttpRequest) : Boolean {
return pathsToFilter.any { it.matches(request.path.pathWithinApplication())}
&& request.method.matches(HttpMethod.GET.name())
}
private fun handleConsumptionProbe(exchange: ServerWebExchange,
chain: WebFilterChain,
probe: ConsumptionProbe) : Mono<Void> {
val response = exchange.response
if(probe.isConsumed) {
response.headers.set("X-Rate-Limit-Remaining", probe.remainingTokens.toString())
return chain.filter(exchange)
}
response.headers.set("X-Rate-Limit-Retry-After-Seconds",
TimeUnit.NANOSECONDS.toSeconds(probe.nanosToWaitForRefill).toString())
return Mono.error(RateLimitException("Too many requests"))
}
override fun getOrder(): Int {
return 1;
}
}</pre>
<p>В нем реализован практически аналогичный функционал, что и в предыдущем примере. За тем исключением, что в этот раз данные о Buckets хранятся и берутся из Redis.</p>
<pre class="brush:java;">
val bucket: AsyncBucketProxy = lettuceProxyManager.asAsync()
.builder()
.build(bucketKey, getBucketConfig())</pre>
<p>А также то, что асинхронность вместо coroutines достигается с помощью реактивного WebFlux.</p>
<pre class="brush:java;">
return Mono
.fromFuture(bucket.tryConsumeAndReturnRemaining(1))
.flatMap { handleConsumptionProbe(exchange, chain, it) }</pre>
<p>Ну и в данном случае у нас нормально работают подсчеты оставшегося кол-ва токенов и есть возможность вычислить кол-во секунд до ближайшего заполнения.</p>
<pre class="brush:java;">
response.headers.set("X-Rate-Limit-Remaining", probe.remainingTokens.toString())
response.headers.set("X-Rate-Limit-Retry-After-Seconds",
TimeUnit.NANOSECONDS.toSeconds(probe.nanosToWaitForRefill).toString())</pre>
<p>Вот что увидит пользователь API, когда жетонов в ведре достаточно и доступ разрешен:</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/Screenshot+2023-11-27+at+20.20.32+%281%29.png/ed25369e-c4fd-6688-fb1c-1e157d1f5afe?imagePreview=1" /></p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/Screenshot+2023-11-27+at+20.26.02.png/58e0f374-3010-1832-8b2a-cf1d79d05687?imagePreview=1" /></p>
<p>И вот, что он увидит когда все жетоны будут израсходованы:</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/Screenshot+2023-11-27+at+20.26.37.png/07b41548-8093-5e7e-d786-a6a552962dbc?imagePreview=1" /></p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/Screenshot+2023-11-27+at+20.26.10.png/5faf8baa-312b-036f-174b-a4bd5308592f?imagePreview=1" /></p>
<p>P.S. Также приведу итоговую структуру проекта, чтобы вы могли сравнить и убедиться, что все нужные классы у вас на месте:</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/Screenshot+2023-11-27+at+15.50.39+%281%29.png/0b9bd929-2879-c954-0f28-b8b897d767df?imagePreview=1" /></p>
<h2>Заключение (которое мне почти помог написать ChatGPT)</h2>
<p>Bucket4j является мощным и универсальным инструментом для реализации стратегии Ограничения скорости в Java/Kotlin-приложениях. Независимо от того, стремитесь ли вы повысить безопасность, защитить ресурсы или обеспечить сбалансированное использование вашего приложения, Bucket4j предоставляет надежное решение. Его простота, гибкость и применимость в реальных условиях делают его ценным дополнением к инструментарию Java/Kotlin-разработчиков, стремящихся оптимизировать производительность и надежность своих приложений. Используя Bucket4j, разработчики могут достичь гармоничного баланса между эффективной обработкой запросов, стабильностью системы и улучшенным пользовательским опытом.</p>
<p><strong>Спасибо за чтение! Оставайтесь с нами, stay tune-it :)</strong></p>Romo Fedoroff2023-11-27T18:26:00ZПереадресация писем в Exchange с помощью PowerShellДмитрий Сазоновhttps://www.tune-it.ru/c/blogs/find_entry?entryId=186577612023-11-19T17:27:39Z2023-11-19T17:26:00Z<p>Настройка переадресации писем возможна через Exchange Admin Center, однако иногда бывает удобнее использовать PowerShell. Ниже я вкратце опишу, как это можно сделать.</p>
<p>Существует два атрибута, позволяющих настроить переадресацию - это<strong> ForwardingAddress</strong> и <strong>ForwardingSmtpAddress </strong>.</p>
<p><strong>ForwardingSmtpAddress</strong> позволяет настроить пересылку на любой внутренний или внешний SMTP адрес, однако имеются следующие ограничения: если это внешний SMTP адрес, то такая пересылка будет работать <u>только для доверенных внешних доменов</u>, которые администратор добавил в Remote Domains и разрешил на них пересылку. Проверить можно с помощью</p>
<div>
<div class="syntaxhighlighter as3" id="highlighter_903762">
<div class="toolbar"><span><a class="toolbar_item command_help help" href="https://www.tune-it.ru/web/fev/home/-/blogs/pereadresacia-pisem-v-exchange-s-pomos-u-powershell#">?</a></span></div>
<table border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td class="gutter">
<div class="line number1 index0 alt2">1</div>
</td>
<td class="code">
<div class="container">
<div class="line number1 index0 alt2"><code class="as3 plain">Get-RemoteDomain | fl DomainName,AutoForwardEnabled</code></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<p><strong>ForwardingAddress</strong> позволяет настроить пересылку почты на любой обьект в организации, при этом необходимы административные права. Аттрибут<strong> </strong>же <strong>ForwardingSmtpAddress </strong>может быть изменен пользователями через графический интерфейс Outlook/OWA.</p>
<p>Следует отметить, что аттрибуты эти неравнозначны - если включены и <strong>ForwardingAddress</strong> и <strong>ForwardingSMTPAddress</strong>, то значение последнего будет игнорироваться как менее приоритетное.</p>
<p> </p>
<p>Для настройки подключаемся к серверу Exchange, далее выполняем команду -</p>
<div>
<div class="syntaxhighlighter as3" id="highlighter_254401">
<div class="toolbar"><span><a class="toolbar_item command_help help" href="https://www.tune-it.ru/web/fev/home/-/blogs/pereadresacia-pisem-v-exchange-s-pomos-u-powershell#">?</a></span></div>
<table border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td class="gutter">
<div class="line number1 index0 alt2">1</div>
</td>
<td class="code">
<div class="container">
<div class="line number1 index0 alt2"><code class="as3 plain">Set-Mailbox i.ivanov@name.ru -ForwardingAddress v.pupkin@name.ru -DeliverToMailboxAndForward $</code><code class="as3 keyword">true</code></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<p>Результатом выполнения будет - все письма, направляемые на i.ivanov@name.ru будут автоматически пересылаться на v.pupkin@name.ru.</p>
<p>За сохранение писем в ящике первоначального адресата ( i.ivanov@name.ru ) отвечает опция <strong>DeliverToMailboxAndForward</strong>. Если указать <strong>DeliverToMailboxAndForward<em> $false</em></strong> , письма не будут сохраняться в почтовом ящике первоначального адресата.</p>
<p>Для проверки, включена ли эта функция, можно выполнить:</p>
<div>
<div class="syntaxhighlighter as3" id="highlighter_522976">
<div class="toolbar"><span><a class="toolbar_item command_help help" href="https://www.tune-it.ru/web/fev/home/-/blogs/pereadresacia-pisem-v-exchange-s-pomos-u-powershell#">?</a></span></div>
<table border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td class="gutter">
<div class="line number1 index0 alt2">1</div>
</td>
<td class="code">
<div class="container">
<div class="line number1 index0 alt2"><code class="as3 plain">Get-Mailbox -Identity i.ivanov@name.ru |fl ForwardingAddress, ForwardingSmtpAddress, DeliverToMailboxAndForward</code></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<p>Для отключения:</p>
<div>
<div class="syntaxhighlighter as3" id="highlighter_258105">
<div class="toolbar"><span><a class="toolbar_item command_help help" href="https://www.tune-it.ru/web/fev/home/-/blogs/pereadresacia-pisem-v-exchange-s-pomos-u-powershell#">?</a></span></div>
<table border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td class="gutter">
<div class="line number1 index0 alt2">1</div>
</td>
<td class="code">
<div class="container">
<div class="line number1 index0 alt2"><code class="as3 plain">Set-Mailbox -Identity i.ivanov@name.ru -DeliverToMailboxAndForward $False -ForwardingAddress $</code><code class="as3 keyword">null</code> <code class="as3 plain">-ForwardingSmtpAddress $</code><code class="as3 keyword">null</code></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<p>Поиск всех ящиков, для которых включена функция автоматической пересылки -</p>
<div>
<div class="syntaxhighlighter as3" id="highlighter_687776">
<div class="toolbar"><span><a class="toolbar_item command_help help" href="https://www.tune-it.ru/web/fev/home/-/blogs/pereadresacia-pisem-v-exchange-s-pomos-u-powershell#">?</a></span></div>
<table border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td class="gutter">
<div class="line number1 index0 alt2">1</div>
</td>
<td class="code">
<div class="container">
<div class="line number1 index0 alt2"><code class="as3 plain">Get-Mailbox -ResultSize Unlimited -Filter </code><code class="as3 string">"ForwardingAddress -like '*' -or ForwardingSmtpAddress -like '*'"</code> <code class="as3 plain">| Select-</code><code class="as3 keyword">Object</code> <code class="as3 plain">Name,ForwardingAddress,ForwardingSmtpAddress</code></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<p>Обратите внимание, что выше при использовании <strong>ForwardingAddress</strong> мы говорили про любой объект в организации. Если же нужно организовать пересылку на внешний адрес, следует сначала создать контакт для этого адреса:</p>
<div>
<div class="syntaxhighlighter as3" id="highlighter_438849">
<div class="toolbar"><span><a class="toolbar_item command_help help" href="https://www.tune-it.ru/web/fev/home/-/blogs/pereadresacia-pisem-v-exchange-s-pomos-u-powershell#">?</a></span></div>
<table border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td class="gutter">
<div class="line number1 index0 alt2">1</div>
</td>
<td class="code">
<div class="container">
<div class="line number1 index0 alt2"><code class="as3 plain">New-MailContact -Name </code><code class="as3 string">"ext. Vasya Petrov"</code> <code class="as3 plain">-ExternalEmailAddress </code><code class="as3 string">"v.petrov@extname.com"</code></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<p>Настраиваем для контакта внутренний email адрес:</p>
<div>
<div class="syntaxhighlighter as3" id="highlighter_784034">
<div class="toolbar"><span><a class="toolbar_item command_help help" href="https://www.tune-it.ru/web/fev/home/-/blogs/pereadresacia-pisem-v-exchange-s-pomos-u-powershell#">?</a></span></div>
<table border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td class="gutter">
<div class="line number1 index0 alt2">1</div>
</td>
<td class="code">
<div class="container">
<div class="line number1 index0 alt2"><code class="as3 plain">Set-MailContact </code><code class="as3 string">"ext. Vasya Petrov"</code> <code class="as3 plain">-EmailAddresses </code><code class="as3 string">"SMTP:ext_v.petrov@name.ru, v.petrov@extnamel.com"</code></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<p>, где ext. Vasya Petrov - имя контакта в нашей организации, ext_v.petrov@name.ru - внутренний адрес, v.petrov@extnamel.com - внешний адрес.</p>
<p> </p>
<p>Далее настройка пересылки выполняется как и описано выше, через <em> Set-Mailbox </em>.</p>Дмитрий Сазонов2023-11-19T17:26:00ZКак собрать GROMACS для использования с CUDADanil Khanalainenhttps://www.tune-it.ru/c/blogs/find_entry?entryId=182926522023-11-09T10:49:14Z2023-11-09T10:30:00Z<h2>Вместо вступления</h2>
<p>GROMACS - свободно распространяемый софт для моделирования молекулярных и физических процессов. Чтобы получить максимальную производительность от GROMACS, нужно собирать его самостоятельно, выбирая из множества флагов и опций. Статья призвана показать усреднённый вариант сборки для видеокарт Nvidia, а также проверку софта на работоспособность и производительность.</p>
<h2>Системные требования</h2>
<ol>
<li>Видеокарта Nvidia с поддержкой архитектуры CUDA</li>
<li>ОС Linux (в примере рассмотрена Ubuntu Server 22.04)</li>
</ol>
<h2>Сборка</h2>
<p>Для сборки понадобятся gcc, cmake:</p>
<pre class="brush:bash;">
sudo apt install build-essential
sudo apt install cmake</pre>
<p>CUDA устанавливается разными способами, можно воспользоваться инструкцией с официального сайта: <a href="https://developer.nvidia.com/cuda-downloads">https://developer.nvidia.com/cuda-downloads</a></p>
<p>Далее нужен MPI. Он необходим для пареллелизации задач при вычислениях на GPU. Воспользуемся Open MPI, который соберём самостоятельно: <a href="https://www.open-mpi.org/software/ompi/v4.1/">https://www.open-mpi.org/software/ompi/v4.1/</a>. При конфигурации обязательно указываем флаг --with-cuda, а также путь до этой самой CUDA: </p>
<pre class="brush:bash;">
wget https://download.open-mpi.org/release/open-mpi/v4.1/openmpi-4.1.5.tar.gz
tar xfz openmpi-4.1.5.tar.gz
cd openmpi-4.1.5
./configure --with-cuda=/usr/local/cuda
make -j 32 all 2>&1 | tee make.out
sudo make install 2>&1 | tee install.out
sudo ldconfig
</pre>
<p>Теперь можно приступить к сборке и установке GROMACS. В данном случае при конфигурации нужно лишь указать о необходимости поддержки MPI и CUDA, пути до них будут найдены автоматически:</p>
<pre class="brush:bash;">
wget https://ftp.gromacs.org/gromacs/gromacs-2023.2.tar.gz
tar xfz gromacs-2023.2.tar.gz
cd gromacs-2023.2
mkdir build && cd build
cmake .. -DGMX_GPU=CUDA -DGMX_MPI=on -DGMX_BUILD_OWN_FFTW=on
make -j 32
make check -j 32
sudo make install
echo "source /usr/local/gromacs/bin/GMXRC" >> ~/.bashrc
. ~/.bashrc</pre>
<p>make check позволяет проверить, было ли всё собрано правильно. Сначала тесты <em>долго</em> собираются, а только потом начинают прогоняться, из-за этого некоторые могут не отработать корректно в первый раз. Если валятся 1-2 из них, можно попробовать написать make check ещё раз, это запустит тесты без пересборки.</p>
<h2>Оно работает!</h2>
<p>Вот так можно быстро собрать GROMACS и начать им пользоваться. Чтобы проверить программу в деле и замерить производительность, запустим бенчмарк. Взять их можно например отсюда: <a href="https://www.mpinat.mpg.de/grubmueller/bench">https://www.mpinat.mpg.de/grubmueller/bench</a>. Перед запуском рекомендуется создать отдельную директорию и запускать GROMACS из неё, чтобы все файлы вывода оказались собраны в одном месте.</p>
<p>При установке с MPI бинарный файл будет называться именно gmx_mpi! Запустить бенчмарки можно разными способами:</p>
<pre class="brush:bash;">
gmx_mpi mdrun -pme gpu -update gpu -bonded gpu -s /path/to/benchmark.tpr
gmx_mpi mdrun -s /path/to/benchmark.tpr</pre>
<p>После (возможно длительных) вычислений получаем посчитанную производительность:</p>
<pre class="brush:bash;">
Core t (s) Wall t (s) (%)
Time: 660.339 27.529 2398.7
(ns/day) (hour/ns)
Performance: 62.777 0.382
</pre>Danil Khanalainen2023-11-09T10:30:00ZНастройка NFSAnna Ershovahttps://www.tune-it.ru/c/blogs/find_entry?entryId=183225632023-11-02T12:38:58Z2023-10-30T07:38:00Z<p>При настройке кластеров или других систем, включающих несколько серверов, бывает очень удобно иметь некоторую "шару", куда каждое устройство может класть и извлекать общие файлы, предназначенные для всех устройств в данной системе. С этим вам может помочь очень простое решение - Network File System или NFS.</p>
<p>NFS - это сетевая файловая система. На одном из серверов можно разрешить доступ к некоторой директории, другие сервера могут смонтировать эту директорию себе и пользоваться ей, как если бы она была локальной.</p>
<p>Основное преимущество такого решения в том, что отдельные устройства могут использовать меньше собственного дискового пространства, так как часть файлов хранится на удаленном сервере.</p>
<p>Для настройки NFS необходим</p>
<p>1. NFS server - предоставляет свое дисковое пространство, и</p>
<p>2. NFS Clients - монтируют сетевую директорию как обычный диск в системе.</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/18242054/nfs-server.jpg/4d01d06d-7c50-3ee6-7ba0-0d79cf1711bc?imagePreview=1" /></p>
<p> </p>
<h2> </h2>
<h2>NFS Server</h2>
<p>На устройстве, которое вы решили выбрать в качестве NFS Сервера, произведите следующие действия.</p>
<p>1. Устанавливаем пакет nfs-kernel-server</p>
<p><code>sudo apt install nfs-kernel-server</code></p>
<p>2. Проверим корректность установки. </p>
<p>- Выполните команду</p>
<p><code> rpcinfo -p | grep nfs</code></p>
<p> и убедитесь, что nfs-kernel-server слушает TCP/UDP на порту 2049.</p>
<p>- Проверьте поддерживается ли NFS на уровне ядра</p>
<p><code> cat /proc/filesystems | grep nfs</code></p>
<p> Вывод должен быть таким:<br />
<code> nodev nfsd</code></p>
<p> Если ваш вывод выглядит не так, то стоит вручную загрузить модуль nfs. Это можно сделать, выполнив команду</p>
<p><code> modprobe nfs</code></p>
<p>3. Добавьте NFS в автозагрузку, чтобы сервер NFS автоматически запускался при каждом включении или перезагрузки системы.<br />
<code>sudo systemctl enable nfs-server</code></p>
<p>Проверьте, что все корректно добавилось. Статус должен быть active:<br />
<code>sudo systemctl status nfs-server</code></p>
<p> </p>
<h2>NFS Client</h2>
<p>На устройствах NFS Пользователей произведите следующие действия:</p>
<p>1. Установите пакет nfs-common</p>
<p><code>sudo apt install nfs-common</code></p>
<p>2. Проверьте наличие nfs-client:</p>
<p><code>showmount</code></p>
<p>Также это можно проверить, набрав <code>mount.</code> и нажав 2 раза на TAB, должен найтись mount.nfs</p>
<p> </p>
<h2>Конфигурация NFS Server</h2>
<p>1. Выбираем директорию на nfs-сервере, к которой мы откроем доступ<br />
Пусть это будет <code>/nfs</code></p>
<p><code>mkdir /nfs -p<br />
chown nobody:nogroup /nfs</code></p>
<p>2. Редактируем конфиг /etc/exports, подставляя IP NFS клиентов</p>
<p><code>/nfs 10.0.0.1(rw,sync,no_subtree_check)<br />
/nfs 10.0.0.2(rw,sync,no_subtree_check)<br />
/nfs 10.0.0.3(rw,sync,no_subtree_check)<br />
/nfs 10.0.0.4(rw,sync,no_subtree_check)<br />
/nfs 10.0.0.5(rw,sync,no_subtree_check)</code></p>
<p><code>/nfs</code> - путь к каталогу в файловой системе сервера, который будет экспортирован и станет доступен для клиентов NFS</p>
<p><code>10.0.0.1 - 10.0.0.5</code> - IP NFS клиентов</p>
<p><code>(rw,sync,no_subtree_check) </code>- опции доступа и настройки для данного экспорта. Рассмотрим значения некоторых возможных опций:</p>
<p><strong>rw</strong> - разрешает чтение/запись в указанную директорию (в данном случае /nfs)</p>
<p><strong>ro</strong> - разрешает только чтение</p>
<p><strong>sync </strong>- означает, что сервер NFS будет синхронизировать записи прежде чем отвечать на следующие запросы</p>
<p><strong>async</strong> - не блокирует подключения, пока данные записываются на диск</p>
<p><strong>no_subtree_check</strong> - позволяет клиентам обращаться ко всем файлам и подкаталогам внутри указанного каталога (в данном случае /nfs)</p>
<p><strong>subtree_check</strong> - проверяет имеет ли клиент права доступа к каждому подкаталогу. Таким образом, обеспечивается более строгий контроль доступа</p>
<p><strong>secure </strong>- для соединения используются только порты ниже 1024</p>
<p><strong>insecure</strong> - для подключения используются любые порты</p>
<p><strong>nohide </strong>- не скрывать поддиректории при открытии доступа к нескольким директориям</p>
<p><strong>all_squash </strong>- превращать все запросы в анонимные</p>
<p><strong>root_squash</strong> - подменять запросы от root на анонимные</p>
<p><strong>no_root_squash</strong> - не подменять запросы от root на анонимные</p>
<p> </p>
<p>3. После этого необходимо рестартнуть сервер</p>
<p><code>systemctl restart nfs-kernel-server</code></p>
<p>4. Если включен брандмауэр, надо открыть необходимые порты<br />
Проверим, что он включен</p>
<p><code>sudo ufw status</code></p>
<p>Если статус active, то открываем порты для клиентов данной командой, подставляя IP клиентов, </p>
<p><code>sudo ufw allow from 10.0.0.1 to any port nfs</code></p>
<p> </p>
<h2>Примонтировать NFS клиента к серверу</h2>
<p>1. На клиенте создаем директорию, к которой будем монтировать директорию с сервера</p>
<p><code>sudo mkdir -p /nfs</code></p>
<p>2. Монтируем клиента к серверу</p>
<p>Вставляем IP NFS сервера</p>
<p><code>sudo mount 10.0.0.10:/nfs /nfs</code></p>
<p>3. Проверяем, что все примонтировалось командой</p>
<p><code>df -h</code></p>
<p> </p>
<p>После этого настройка NFS закончена, вы можете пользоваться общей директорией /nfs!</p>
<p> </p>
<p>Чтобы отмонтировать эту директорию достаточно выполнить команду:</p>
<p><code>sudo umount /nfs</code></p>
<p> </p>
<h3>Сколько места используется в точке монтирования?</h3>
<p><code>du -sh </code></p>
<p><code>/nfs 4.0K /nfs</code></p>
<p> </p>
<h2>Настройка автоматического монтирования fstab на клиенте</h2>
<p>Будет хорошо также настроить автоматическое монтирование fstab на клиенте. Это позволит обеспечить постоянный доступ к ресурсам после каждой загрузки системы без необходимости ручного вмешательства.</p>
<p>Для данной настройки достаточно изменить 1 конфигурационный файл:</p>
<p>В конец файла <code>/etc/fstab</code> добавляем строку:</p>
<p><code>10.0.0.10:/nfs /nfs nfs defaults 0 0</code></p>
<p> </p>
<p>Разберем содержимое этой строки:</p>
<p><code>10.0.0.10:/nfs </code>- <IP-адрес NFS сервера>:<экспортированный каталог></p>
<p><code>/nfs</code> - локальная точка монтирования</p>
<p><code>nfs</code> - тип файловой системы, который будет использоваться для монтирования</p>
<p><code>defaults</code> - набор параметров монтирования, который включает стандартные настройки, такие как чтение и запись. Вы можете настроить дополнительные параметры, если это необходимо.</p>
<p><code>0 0</code> - эти 2 числа обозначают, будут ли выполняться проверки при загрузке и выключении системы. В данной конфигурации "0 0" проверки не выполняются. Они могут быть настроены, чтобы включить автоматическую проверку файловой системы при загрузке или выключении, если это необходимо.</p>Anna Ershova2023-10-30T07:38:00ZНастройка статических IP-адресов на UbuntuAnna Ershovahttps://www.tune-it.ru/c/blogs/find_entry?entryId=183290112024-03-05T20:30:34Z2023-10-26T15:52:00Z<p>В большинстве сетевых конфигураций DHCP-сервер по умолчанию назначает IP-адрес динамически. Но в некоторых ситуациях удобнее, когда IP-адрес остается неизменным всегда. В этом вам помогут статические IP.</p>
<p> </p>
<h2>Шаг 1</h2>
<p>Для начала необходимо узнать имя интерфейса, которому мы будем присваивать статический IP. Это можно сделать с помощью команды</p>
<p><code>ip a</code></p>
<p>Пример вывода:</p>
<p><code>1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000<br />
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00<br />
inet 127.0.0.1/8 scope host lo<br />
valid_lft forever preferred_lft forever<br />
inet6 ::1/128 scope host <br />
valid_lft forever preferred_lft forever<br />
2: enp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000<br />
link/ether 50:50:50:5e:53:5e brd ff:ff:ff:ff:ff:ff<br />
inet 192.168.122.213/24 brd 192.168.122.255 scope global dynamic noprefixroute enp1s0<br />
valid_lft 3547sec preferred_lft 3547sec<br />
inet6 fe80::20b7:e59b:9d09:b9a1/64 scope link noprefixroute <br />
valid_lft forever preferred_lft forever</code></p>
<p>Интерфейс = enp1s0, текущий IP = 192.168.122.213/24</p>
<p> </p>
<h2>Шаг 2</h2>
<p>Netplan - это инструмент управления сетью по умолчанию в последних версиях Ubuntu. Конфигурация Netplan хранится в YAML-файлах.</p>
<div class="portlet-msg-alert">Пробелы являются частью синтаксиса в конфигурационных файлах Netplan, поэтому будьте осторожны с ними.</div>
<p>Конфигурация хранится в директории <code>/etc/netplan</code>. Зайдите в нее и посмотрите, какие .yaml файлы там храняться. Дефолтное название файла конфигурации - <code>01-network-manager-all.yaml</code>, но название может быть и другим.</p>
<p>Если ни одного .yaml файла там нет, то вы можете создать его сами. Имя может быть любым, но по соглашению, оно должно начинаться с цифры, например 01.</p>
<p>Я напишу такой <code>01-network-manager-all.yaml </code>файл:</p>
<p><code>network:<br />
version: 2<br />
renderer: NetworkManager<br />
ethernets:<br />
enp1s0:<br />
dhcp4: no<br />
addresses: [10.0.0.11/20]<br />
routes:<br />
- to: default<br />
via: 10.0.0.1<br />
nameservers:<br />
addresses: [10.0.0.1]</code></p>
<p> </p>
<p><code>network: </code>- блок начала конфигурационного файла</p>
<p><code>renderer: NetworkManager </code>- указали сетевого менеджера, который будем использовать. Это может быть NetworkManager или networkd.</p>
<p><code>enp1s0: </code>- указываем интерфейс, которому будем присваивать статический IP и который мы определили на шаге 1.</p>
<p><code>dhcp4: no</code> - отключаем DHCP IPv4. Если нужно отключить для IPv6, то используем dhcp6:</p>
<p><code>addresses: [10.0.0.11/20] </code>- статический IP, который задаем на интерфейс.</p>
<p>В блоке <code>routes: </code>определяем адрес gateway. В более ранних версиях Ubuntu gateway задается по-другому: <code>gateway4: 10.0.0.1</code></p>
<p>В блоке <code>nameservers: </code>определяем адрес DNS-сервера.</p>
<p> </p>
<h2>Шаг 3</h2>
<p>Применяем изменения.</p>
<p><code>sudo netplan try</code></p>
<p>При вводе этой команды изменения сначала будут протестированы. Если ошибок не будет, то вас спросят хотите ли вы принять изменения.</p>
<p> </p>
<h2>Шаг 4</h2>
<p>Остается только проверить, что изменения вошли в силу. Это можно сделать командой с первого шага:</p>
<p><code>ip a</code></p>Anna Ershova2023-10-26T15:52:00ZОрганизация резервного копирования баз данных средствами MS SQLДмитрий Сазоновhttps://www.tune-it.ru/c/blogs/find_entry?entryId=42228662023-10-20T05:42:07Z2023-10-19T17:22:00Z<p>В данной заметке кратко опишем процесс организации бэкапа баз данных средствами MS SQL и последующее удаление бэкапов с истекшим сроком хранения.</p>
<p>Для начала посмотрим, как организовать суточное\недельное\ежемесячное резевное копирование средствами самого MS SQL.</p>
<p>Запускаем Management Studio и создаем maintenance plan для каждого типа бэкапа(ежедневный, еженедельный и тп) следующим образом:</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/1199236/%D0%91%D0%B5%D0%B7%D1%8B%D0%BC%D1%8F%D0%BD%D0%BD%D1%8B%D0%B9.jpg/53521bd0-2407-010b-3216-c51f145a1318?imagePreview=1" /></p>
<p>Выбираем имя и нажимем на Chenge для выбора плана выполнения процедуры</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/1199236/2.jpg/fd94dfc5-b398-bed6-5d8a-bef00ca28bc4?imagePreview=1" /></p>
<p>в данном случае пусть будет ежедневное:</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/1199236/3.jpg/3c56b605-3ca8-8350-9f0d-cff3730a0d27?imagePreview=1" /></p>
<p>Обратите внимание, чтобы Shedule type был типа Recurring и стояла галочка Enable.</p>
<p>Далее нажимаем OK, Next и выбираем тип резервного копирования(пусть будет Full в данном случае) :</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/1199236/4.jpg/b237c759-d94e-ad58-e71a-fd1592f1b355?imagePreview=1" /></p>
<p>Жмем Next, далее выбираем какие базы данных хотим подвергать резервному копированию</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/1199236/5.jpg/0a313678-a38c-ae79-9a53-42ddeff51e73?imagePreview=1" /></p>
<p>И обязательно переходим во вкладку Destination для указания места хранения наших бэкапов(я рекомендую разделить конечные каталоги для каждого типа, т.е. создать структуру вида .<strong>../Backup/Daily, .../Backup/Weekly</strong> и тд) . Также обратите внимание, чтобы расширение файлов было написано без точки, т.е. просто как <strong>bak :</strong></p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/1199236/6.jpg/09b67a2d-46ec-d016-7f2c-586af858de98?imagePreview=1" /></p>
<p>Дальнейший процесс трудностей не вызывает и заключается в согласии или отказе в логировании, уведомлении по почте и тп, в простейшем случае просто жмем "Next" и завершаем создание Maintenance Plan. В результате выполнения этого плана в каталоге<strong> .../Backup/Daily </strong>будут созданы подкаталоги по имени каждой БД и в них файлы вида <em><strong>BaseName_backup_2021_02_16_000005_8640903.bak. </strong></em>Имея такой формат имени файла, очень легко ориентироватся по дате создания каждого из них.</p>
<p> Аналогичным образом можно настроить резервное копирование с недельным, месячным и годичным интервалом, задавая соответственные настройки шедулинга.</p>
<p>Теперь перейдем к созданию <strong>планов очистки</strong>.</p>
<p>Первые шаги будут аналогичными, создаем новый Maintenance Plan, называем его например CleanUpDaily и настраиваем шедулинг. Обратите внимание, что ежедневный план очистки должен выполнятся <em>после завершения ежедневного же бэкапа</em> и тд.</p>
<p> </p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/1199236/word-image-16.jpeg/7f8b1570-219b-d914-e307-6714eba340e6?imagePreview=1" /></p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/1199236/word-image-17.jpeg/69aeb608-b4d2-1fb5-8d4b-8ef43612dc4b?imagePreview=1" /></p>
<p>В примере снизу указан путь к каталогу G:\Backups, однако для каждого из созданных планов резервного копирования надо прописать путь до конечного хранения файлов, например, <strong>.../Backup/Daily</strong>(подкаталоги первого уровня с именами баз будут учтены также, если стоит соответствующая галочка, см ниже).</p>
<p>Тут же настраиваем глубину хранения бэкапа, указав максимальный срок давности для файлов(в данном случае - 7 дней для ежедневного бэкапа):</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/1199236/word-image-19.jpeg/05fb7f12-282e-7a64-122a-2f510b9ff16a?imagePreview=1" /></p>
<p> </p>
<p>Остальные шаги также не вызывают сложностей, завершаете создание плана обслуживания и переходите к созданию следующего, аналогично.</p>
<p> </p>
<p>В следующей статье рассмотрим, как можно организовать резервное копирование MS SQL с помощью Bacula, если нет возможности использовать Enterprise-версию.</p>
<p> </p>
<p> </p>
<p> </p>Дмитрий Сазонов2023-10-19T17:22:00ZReactive REST API. Kotlin, Spring WebFlux и R2DBCRomo Fedoroffhttps://www.tune-it.ru/c/blogs/find_entry?entryId=182841022023-10-18T13:24:27Z2023-10-18T12:40:00Z<style type="text/css">article p {
font-size:11pt;
font-family:Verdana, sans-serif;
text-align:justify;
color:#6a6a6a;
}
article img {
width: 100%;
}
.centered {
text-align:center;
}
</style>
<p class="centered"><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/222.jpg/c603c780-591f-05bd-de1b-9633d751a7ea?imagePreview=1" /></p>
<h2>1. Подготовка и настройка проекта</h2>
<p>Начнем с создания нового проекта в IntelliJ Idea.<br />
В окне New project на необходимо выбрать Spring Initializr.<br />
Настраиваем этот генератор как показано на картинке:</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/1+%282%29.png/db5307fd-b310-2992-053c-61b857735764?imagePreview=1" /></p>
<p>Задаем имя, выбираем язык - Kotlin, выбираем тип проекта - Gradle-Kotlin, выбираем версию JDK и Java - я выбрал 17-ую, выбираем тип упаковывания - jar, и жмем на кнопку Next.</p>
<p>В следующем окне выбираем одну из свежих версий SpringBoot, а также нужные нам зависимости:<br />
1) Spring Reactive Web - необходима для создания реактивного веб-приложения с помощью Spring WebFlux и Netty.<br />
2) Spring Data R2DBC - поможет подключиться к реактивной реляционной базе данных. <br />
3) PostgreSQL Driver - содержит R2DBC-драйвер PostgreSQL.<br />
4) Validation - в нашем случае нужна, чтобы выполнять различные проверки данных на соответствие заданным ограничениям и условиям, а также чтобы задавать эти ограничения и условия.</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/2+%282%29.png/231d325c-f526-387c-79ed-7330a554abdf?imagePreview=1" /></p>
<p>После этого нажимаем кнопку Create.</p>
<p>Ура, наш проект создан.</p>
<p>Первым делом, зайдите в настройки IntelliJ Idea в раздел Build, Execution, Deployment --> Build Tools --> Gradle и убедитесь что версия Gradle JVM совпадает с той версией JDK, что вы выбрали для проекта.</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/3+%282%29.png/98952953-f8e5-4315-0d8c-7c293ad384d3?imagePreview=1" /></p>
<p>В моем случае они не совпадали и проект выдавал ошибку Gradle, пока я не установил нужную версию.</p>
<p>Для хранения и работы с данными наше приложение будет использовать базу данных PostgreSQL.</p>
<p>Для удобства будем использовать Docker Compose. <br />
Установка, настройка Docker, а также правила составления compose файлов не рассматриваются в данной статье и оставляются вам для самостоятельного изучения.</p>
<p>Создадим в корне проекта файл с названием docker-compose.yml со следующим содержимым:</p>
<pre class="brush:jscript;">
version: '3.8'
services:
postgres:
container_name: "reactive-postgres"
image: postgres
ports:
- "127.0.0.1:5432:5432"
environment:
POSTGRES_USER: demo
POSTGRES_PASSWORD: demo
POSTGRES_DB: coursesshopreactive</pre>
<p>Вместо 127.0.0.1 вы можете написать localhost. Просто в моем случае localhost = 127.0.0.1 и я решил указать адрес как есть, не используя синоним. <br />
Далее запустим терминал из директории где находится этот файл и выполним следующую команду:</p>
<pre class="brush:bash;">
docker compose up -d
</pre>
<p>Для управления контейнерами docker, я обычно использую программу Docker Desktop. Зайдя в неё, я вижу, что предыдущая команда скачала образ PostgreSQL, создала контейнер с именем reactive-postgres, в нем запустила скачанный образ; создала в образе базу данных с именем coursesshopreactive и пользователя demo с паролем demo, и наконец пробросила порт 5432, по которому мы можем взаимодействовать с базой.</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/4+%281%29.png/7c31b047-8127-1c47-9990-dc950d35891d?imagePreview=1" /></p>
<p>Теперь нам необходимо создать в базе данных таблицу, в которой будет храниться информация об онлайн курсах нашего интернет магазина. <br />
Для этого воспользуемся функционалом IntelliJ Idea для работы с базами данных. </p>
<p>Для начала добавим в среду разработки подключение к нашей базе:</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/5+%281%29.png/206b71d2-93c9-0b89-24d4-d11d135db650?imagePreview=1" /></p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/6+%281%29.png/4b02b466-1b94-b6d3-c482-8491da57f6cb?imagePreview=1" /></p>
<p>Если у вас не установлен драйвер, среда разработки предложит его скачать.</p>
<p>Далее создадим таблицу со структурой как на картинке:</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/8.png/3fb1b9b0-3f55-f699-83ac-71abf5528967?imagePreview=1" /></p>
<p>Также нам необходимо создать конфигурационный файл нашего приложения, где будут храниться настройки подключения к базе данных, настройки веб-сервера и др.<br />
Этот файл нужно назвать application.yml и разместить его в каталоге src->main->resources.<br />
Картинку с итоговой структурой проекта вы увидите дальше в этой статье.</p>
<p>Содержимое файла следующее:</p>
<pre class="brush:jscript;">
spring:
r2dbc:
url: "r2dbc:postgresql://localhost:5432/coursesshopreactive"
username: demo
password: demo
server:
port: 8080
error:
include-message: always</pre>
<p>В файлах yml важны отступы, которые определяют структуру иерархии параметров. Просто помните об этом.</p>
<p>Отлично! На этом первоначальная подготовка и настройка проекта закончена.<br />
Теперь переходим к созданию функционала приложения. </p>
<h2>2. Программирование функционала приложения</h2>
<p>Создадим приложение по классической схеме разработки приложений АПИ.<br />
У нас будет Контроллер, который будет обслуживать различные запросы пользователей нашего АПИ.<br />
Также у нас будет Репозиторий для взаимодействия с базой данных и Сервис, в котором будет реализована бизнес-логика приложения.<br />
Ну и само по себе разумеющееся, у нас будет Модель, описывающая все необходимые нам сущности нашей предметной области.</p>
<p>Начнем с того, что определим какой функционал мы хотим предоставить пользователям нашего АПИ.<br />
Давайте сделаем так, чтобы пользователи могли:</p>
<p>а) получить информацию по всем имеющимся в нашем магазине онлайн курсам ("получить все курсы", GET-запрос)<br />
б) получить информацию о курсе по его id (GET-запрос)<br />
в) добавить информацию о новом курсе в магазин ("добавить новый курс", POST-запрос) <br />
г) обновить информацию для существующего курса по его id ("обновить курс", PUT-запрос)<br />
д) удалить курс из магазина по его id (DELETE-запрос)</p>
<h3>2.1 Программирование Модели</h3>
<p>Для начала создадим Package с именем model по адресу src->main->kotlin->com.tuneit.coursesshopreactive-><br />
В нем мы будем располагать основные и вспомогательные классы, описывающие Модель нашего приложения.</p>
<p>Создадим класс OnlineCourse.kt:</p>
<pre class="brush:java;">
package com.tuneit.coursesshopreactive.model
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Table
import java.time.LocalDate
@Table(name = "online_courses")
data class OnlineCourse (
@Id
val id : Long?=null,
val name : String,
val price : Float,
val author : String,
val direction : String,
val startDate : LocalDate,
val endDate: LocalDate?=null,
)
</pre>
<p> </p>
<p>Как вы уже смогли понять, это основной класс, который описывает сущность "Онлайн курс". И говоря простым языком, благодаря аннотациям (@Table, @Id), он связан с таблицей online_courses в БД и ее данными (назовем это отображением данных).</p>
<p>Также в этом же package создадим класс CourseRequest.kt:</p>
<pre class="brush:java;">
package com.tuneit.coursesshopreactive.model;
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.PositiveOrZero
import java.time.LocalDate
data class CourseRequest(
@field:NotBlank(message = "name should be specified") val name : String,
@field:PositiveOrZero(message = "price must be >= 0") val price : Float,
@field:NotBlank(message = "author should be specified") val author : String,
@field:NotBlank(message = "direction should be specified") val direction : String,
val startDate : LocalDate,
val endDate: LocalDate?=null
)</pre>
<p>Он необходим для запросов на добавление и обновление курсов (POST, PUT). По сути он отображает тело запроса.<br />
Обратите внимание что мы задали для некоторых полей класса ограничения (с помощью аннотаций @field:NotBlank, @field:PositiveOrZero), которые будут затем валидироваться при обработке запросов. <br />
В случае ошибок валидации, пользователь будет видеть соответствующие сообщения, которые мы указали для аннотаций в свойстве message.</p>
<p>И последний файл, который мы добавим в package модели, имеет название Errors.kt:</p>
<pre class="brush:java;">
package com.tuneit.coursesshopreactive.model
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ResponseStatus
@ResponseStatus(HttpStatus.NOT_FOUND)
data class NotFoundException(val msg: String) : RuntimeException(msg)</pre>
<p>В этом файле мы будем описывать ошибки, которые мы хотим возвращать пользователям.<br />
В данном примере ограничимся одной единственной ошибкой NotFoundException.<br />
Пользователи будут видеть ее, когда будут пытаться выполнять запросы в отношении курса, которого не существует в магазине.<br />
Обратите внимание на аннотацию @ResponseStatus(HttpStatus.NOT_FOUND). Если возникнет эта ошибка, пользователь получит ответ именно с этим статусом.</p>
<h3>2.2 Программирование Репозитория и Сервиса</h3>
<p>Для размещения интерфейса Репозитория создадим Package с именем repository по адресу src->main->kotlin->com.tuneit.coursesshopreactive-></p>
<p>Назовем интерфейс CoursesRepository.kt:</p>
<pre class="brush:java;">
package com.tuneit.coursesshopreactive.repository
import com.tuneit.coursesshopreactive.model.OnlineCourse
import org.springframework.data.repository.reactive.ReactiveCrudRepository
import org.springframework.stereotype.Repository
@Repository
interface CoursesRepository : ReactiveCrudRepository<OnlineCourse, Long> {
}</pre>
<p>Обратите внимание на аннотацию @Repository. Так мы даем понять фреймворку, что этот интерфейс есть не что иное, как Репозиторий. <br />
Немного теории про Репозиторий.</p>
<p>Репозиторий - это абстракция над уровнем доступа к данным в приложении. Он предоставляет возможность взаимодействовать с базой данных или другими системами хранения данных, не прибегая к написанию шаблонного кода для обработки операций CRUD (Create, Read, Update, Delete).</p>
<p>ReactiveCrudRepository - это интерфейс из Spring Data для реактивного программирования в приложениях Spring. Этот интерфейс предоставляет набор методов, позволяющих взаимодействовать с базой данных реактивным способом. Эти методы реализуют все основные операции с сущностями: подсчет (count) сущностей, проверка существования сущности (existsById), поиск (findAll, findAllById, findById), сохранение (save, saveAll), удаления сущности (delete, deleteAll, deleteAllById, deleteById).</p>
<p>Теперь переходим к Сервису.<br />
Для размещения его класса, создадим Package с именем service по адресу.. ну вы уже догадались :)</p>
<p>И сам файл CoursesService.kt:</p>
<pre class="brush:java;">
package com.tuneit.coursesshopreactive.service
import com.tuneit.coursesshopreactive.model.CourseRequest
import com.tuneit.coursesshopreactive.model.NotFoundException
import com.tuneit.coursesshopreactive.model.OnlineCourse
import com.tuneit.coursesshopreactive.repository.CoursesRepository
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@Service
class CoursesService(val repo: CoursesRepository) {
fun getAllCourses() : Flux<OnlineCourse> {
return repo.findAll()
}
fun getCourseById(id:Long): Mono<OnlineCourse> {
return repo.findById(id)
.switchIfEmpty(Mono.error(NotFoundException("The course with id «$id» isn't found")))
}
fun saveCourse(request: CourseRequest) : Mono<OnlineCourse> {
return repo.save(
OnlineCourse(
name = request.name.trim(),
price = request.price,
author = request.author.trim(),
direction = request.direction.trim(),
startDate = request.startDate,
endDate = request.endDate)
)
}
fun updateCourse (id: Long, request: CourseRequest) : Mono<OnlineCourse> {
return getCourseById(id)
.flatMap {
repo.save(
OnlineCourse(
id = id,
name = request.name.trim(),
price = request.price,
author = request.author.trim(),
direction = request.direction.trim(),
startDate = request.startDate,
endDate = request.endDate)
)
}
}
fun deleteCourse (id: Long): Mono<Void> {
return getCourseById(id).flatMap { repo.deleteById(id) }
}
}</pre>
<p>Подобно тому, как мы обозначали Репозиторий, для определения Сервиса в Spring, тоже используем аннотацию, но другую - @Service. <br />
Сервис необходим для выполнения бизнес-логики нашего приложения. В нее входит: работа с данными из БД, валидация данных, обработка, трансформация данных, все возможные вычисления, логирование, генерация реакций на ошибки и прочее и прочее.</p>
<p>Подробно о методах нашего Сервиса рассказывать не имеет смысла, так как они интуитивно понятны и очевидны, исходя из их названий и реализации.</p>
<h3>2.3 Программирование Контроллера</h3>
<p>Для размещения класса Контроллера с именем CoursesController.kt, создадим Package с именем controller по небезызвестному нам пути :)</p>
<p>Код контроллера:</p>
<pre class="brush:java;">
package com.tuneit.coursesshopreactive.controller
import com.tuneit.coursesshopreactive.model.CourseRequest
import com.tuneit.coursesshopreactive.model.OnlineCourse
import com.tuneit.coursesshopreactive.service.CoursesService
import jakarta.validation.Valid
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@RestController
@RequestMapping("/courses")
class CoursesController(val service: CoursesService) {
@GetMapping
fun getAllCourses() : Flux<OnlineCourse> {
return service.getAllCourses()
}
@GetMapping("/{id}")
fun getCourseById(@PathVariable id:Long) : Mono<OnlineCourse> {
return service.getCourseById(id)
}
@PostMapping
fun saveCourse(@Valid @RequestBody request: CourseRequest) : Mono<OnlineCourse> {
return service.saveCourse(request)
}
@PutMapping("/{id}")
fun updateCourse(@PathVariable id:Long, @Valid @RequestBody request: CourseRequest) : Mono<OnlineCourse> {
return service.updateCourse(id, request)
}
@DeleteMapping("/{id}")
fun deleteCourse(@PathVariable id:Long) : Mono<String> {
return service.deleteCourse(id)
.then(Mono.just("The course with id «$id» has been successfully deleted"))
}
}</pre>
<p>Класс помечен аннотацией @RestController и это означает, что каждый его метод автоматически сериализует возвращаемые объекты в HttpResponse. <br />
Как вы уже наверняка заметили, контроллер не совершает какой-либо серьезной обработки данных. Он просто передает входящий запрос в сервис, а сервис уже выполняют всю необходимую бизнес-логику.<br />
Такой подход считается хорошим тоном проектирования приложений АПИ.</p>
<p>Обратите внимание на аннотацию @Valid в методах сохранения и обновления курса. Мы помним, что в классе CourseRequest мы задали ограничения для некоторых полей (например, цена курса должна быть равной или большей нуля). Если эта аннотация присутствует, то перед тем как выполнить код в теле метода, фреймворк выполнит все необходимые валидации, и в случае, когда проверки не пройдены, клиенту будет возвращен ответ, содержащий причины, по которым не прошла валидация.<br />
По умолчанию фреймворк возвращает ошибки валидации со слишком длинным и подробным описанием, включая названия классов, методов и полей.<br />
К примеру, вместо сообщения <em>"price must be >= 0"</em> пользователь увидит</p>
<p><em>"Validation failed for argument at index 1 in method: public reactor.core.publisher.Mono<com.tuneit.coursesshopreactive.model.OnlineCourse> com.tuneit.coursesshopreactive.controller.CoursesController.updateCourse(long,com.tuneit.coursesshopreactive.model.CourseRequest), with 1 error(s): [Field error in object 'courseRequest' on field 'price': rejected value [-2.0]; codes [PositiveOrZero.courseRequest.price,PositiveOrZero.price,PositiveOrZero.float,PositiveOrZero]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [courseRequest.price,price]; arguments []; default message [price]]; default message [price must be >= 0]] " </em> </p>
<p>Для того, чтобы исправить такое поведение фреймворка, создадим свой обработчик ошибок, где зададим нужный нам формат сообщений об ошибках.<br />
Добавим в тот же Package, где и контроллер, класс RestControllerExceptionHandler.kt со следующим содержимым: </p>
<pre class="brush:java;">
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Comparator
import java.util.stream.Collectors
@RestControllerAdvice
class RestControllerExceptionHandler {
@ExceptionHandler(WebExchangeBindException::class)
fun handleWebExchangeBindException(e: WebExchangeBindException, we: ServerWebExchange): ResponseEntity<ExceptionDetails> {
val request = we.request
val errors = e.bindingResult
.allErrors
.stream()
.map { it.defaultMessage?:"" }
.sorted(Comparator.naturalOrder())
.collect(Collectors.toSet())
val details = ExceptionDetails(
path = request.path.value(),
status = e.statusCode.value(),
error = e.reason?:HttpStatus.BAD_REQUEST.reasonPhrase,
message = errors.toString().replace("\\[*]*".toRegex(), ""),
requestId = request.id
)
return ResponseEntity.status(e.statusCode).body(details)
}
data class ExceptionDetails (
val timestamp: String = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS+00:00")
.withZone(ZoneId.from(ZoneOffset.UTC)).format(Instant.now()),
val path: String="",
val status: Int,
val error: String="",
val message: String="",
val requestId: String=""
)
}</pre>
<p>Отлично. Работы по программированию нашего АПИ завершены.<br />
Можно переходить к его тестированию.</p>
<p>Однако перед этим, приведу итоговую структуру проекта, чтобы вы могли сравнить и убедиться, что все нужные классы у вас на месте:</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/9.png/0e3d19dd-1b7d-f427-be8f-eda6ee01a03f?imagePreview=1" /></p>
<p>Также приведу содержимое файла build.gradle.kts:</p>
<pre class="brush:java;">
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "3.1.4"
id("io.spring.dependency-management") version "1.1.3"
kotlin("jvm") version "1.8.22"
kotlin("plugin.spring") version "1.8.22"
}
group = "com.tuneit"
version = "0.0.1-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("org.springframework.boot:spring-boot-starter-validation:3.1.4")
runtimeOnly("org.postgresql:postgresql")
runtimeOnly("org.postgresql:r2dbc-postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.projectreactor:reactor-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "17"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}</pre>
<p>содержимое файла settings.gradle.kts:</p>
<pre class="brush:java;">
rootProject.name = "courses-shop-reactive"</pre>
<p>и содержимое файла CoursesShopReactiveApplication.kt:</p>
<pre class="brush:java;">
package com.tuneit.coursesshopreactive
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class CoursesShopReactiveApplication
fun main(args: Array<String>) {
runApplication<CoursesShopReactiveApplication>(*args)
}</pre>
<h2>3. Тестирование приложения</h2>
<p>Пришло время протестировать наше АПИ.</p>
<p>Чтобы запустить приложение, вы можете воспользоваться встроенными в IntelliJ Idea командами Run или Debug, либо выполнить следующую команду через терминал:</p>
<pre class="brush:bash;">
./gradlew bootRun</pre>
<p>Для выполнения запросов к АПИ, я буду использовать программу Postman. Вы можете воспользоваться той, которая вам нравится больше.</p>
<h3>3.1 Добавление курса</h3>
<p>Давайте к примеру придумаем и добавим в магазин три курса.</p>
<p>Обратите внимание. В Postman для этих и других запросов значение переменной {{defaultUrl}} в строке запроса я задал равным localhost:8080.</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/10.png/ad4b4d7f-3869-9f3a-01a1-87c68c20da04?imagePreview=1" /></p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/11.png/44c7bbd3-b2de-b26a-fc7b-0fedf8eaf50e?imagePreview=1" /></p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/12.png/d74a004b-5bf8-1cba-f43c-27aa44791e01?imagePreview=1" /></p>
<p>Давайте намеренно попытаемся добавить курс с неверными данными, чтобы увидеть как сработает валидация:</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/13.png/d178430b-36b7-7be2-9884-96d75a76a075?imagePreview=1" /></p>
<h3>3.2 Получение всех курсов</h3>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/14.png/a7439ca7-3fe6-10c8-5e5e-3339540e8e62?imagePreview=1" /></p>
<h3>3.3 Получение курса по id</h3>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/15.png/419e29d6-89f4-4320-1532-19da2765d9b9?imagePreview=1" /></p>
<p>Давайте посмотрим, что будет если запросить курс, которого нет в магазине:</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/16.png/61b6f040-d3d9-3ad5-ff3c-9201016de957?imagePreview=1" /></p>
<h3>3.4 Обновление курса</h3>
<p>Давайте обновим информацию для курса с id = 7. Зададим ему новую цену и установим дату окончания:</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/17.png/6a04396f-d6bf-2860-a27e-4cba2df9633a?imagePreview=1" /></p>
<h3>3.5 Удаление курса</h3>
<p>Давайте удалим курс с id = 5.</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/18.png/0b3a1c2a-176c-0762-5ec1-22666883c8f2?imagePreview=1" /></p>
<p>Давайте вновь выполним запрос на получение всех курсов, чтобы увидеть итоговое состояние системы:</p>
<p><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/19.png/d9b18610-d6cf-cd03-f8d0-bbb7cf5b12b3?imagePreview=1" /></p>
<p>На этом тестирование закончено. Как видно наше АПИ работает неплохо :) </p>
<h2>Вместо заключения</h2>
<p>Спасибо что провели свое время вместе с нами, читая эту статью.<br />
Надеюсь она оказалась для вас интересной и полезной.<br />
В нашем блоге есть еще одна статья на эту тему: <a href="https://www.tune-it.ru/web/romo/blog/-/blogs/reactive-kotlin-spring-webflux" target="_blank">вот здесь</a>.<br />
Приглашаю вас прочитать ее, если вы еще не читали.</p>
<p>Желаю вам хорошего кодирования и успехов в ваших проектах, будь то учёба или работа!</p>
<p> </p>
<p> </p>Romo Fedoroff2023-10-18T12:40:00ZЧто нового в Java 21?Romo Fedoroffhttps://www.tune-it.ru/c/blogs/find_entry?entryId=180720752023-09-21T14:16:11Z2023-09-21T13:37:00Z<style type="text/css">article p {
font-size:11pt;
font-family:Verdana, sans-serif;
text-align:justify;
color:#6a6a6a;
}
article img {
width: 100%;
}
.centered {
text-align:center;
}
</style>
<p class="centered"><img src="https://www.tune-it.ru/documents/portlet_file_entry/12054155/java-2.jpg/11b2fb61-e9c8-9a2c-aa32-5f96367fe320?imagePreview=1" /></p>
<h2>Введение</h2>
<p>Java 21 - это свежий крупный релиз известного во всем мире и многими любимого языка программирования.</p>
<p>Релиз включает в себя ряд новых возможностей и улучшений:</p>
<p>- Строковые шаблоны (String Templates)</p>
<p>- Упорядоченные коллекции (Sequenced Collections)</p>
<p>- Сборщик мусора Generational Z (ZGC)</p>
<p>- Record Patterns (сопоставление Records с образцом)</p>
<p>- Сравнение с образцом для оператора switch (Pattern Matching for switch)</p>
<p>- Foreign Function & Memory API</p>
<p>- Безымянные образцы (шаблоны) и переменные (Unnamed Patterns and Variables)</p>
<p>- Виртуальные потоки (Virtual Threads)</p>
<p>- Безымянные классы и методы экземпляра Main (Unnamed Classes and Instance Main Methods)</p>
<p>- Значения с ограниченной областью действия (Scoped Values)</p>
<p>- Vector API</p>
<p>- Упразднение порта JDK для Windows 32-bit x86 с полным отказом от него (удалением) в последующих версиях языка</p>
<p>- Подготовка к запрету динамической загрузки агентов (Prepare to Disallow the Dynamic Loading of Agents)</p>
<p>- API механизма инкапсуляции ключей (Key Encapsulation Mechanism API)</p>
<p>- Структурированный параллелизм (Structured Concurrency)</p>
<p>В этой статье мы рассмотрим некоторые основные из них.</p>
<p>И так, начнем.</p>
<h2>1. Виртуальные потоки (Virtual Threads)</h2>
<p>В Java 21 введены виртуальные потоки для упрощения и масштабирования параллелизма. Это шаг вперед, чтобы сделать параллельное программирование более доступным и эффективным.</p>
<p>Виртуальные потоки, также известные как "потоки пользовательского режима" или "волокна", являются частью структурированного параллелизма в JDK 21. Они являются легковесными, и это означает, что их можно создавать в бОльшем количестве, чем традиционные потоки, и с гораздо меньшими накладными расходами. При грамотном подходе к их использованию, даже в программах с высокой пропускной способностью, может стать более практичным выполнение отдельных задач или запросов в собственных виртуальных потоках.</p>
<p>Существует несколько способов создания и использования виртуальных потоков в Java 21:</p>
<p>1) Используя статический builder-метод и builder (построитель). Виртуальный поток можно создать с помощью статического builder-метода, который принимает в качестве параметра runnable и сразу же запускают виртуальный поток. Также поток можно инициализировать с помощью построителя и в этом случае потоку можно задать некоторые свойства, например, имя.</p>
<p>Это показано в следующем примере:</p>
<pre class="brush:java;">
Runnable runnable = () -> {
System.out.println("Hello from tune-it!");
};
// Используем статический builder-метод.
Thread virtualThread = Thread.startVirtualThread(runnable);
// Используем builder
Thread.ofVirtual()
.name("my-virtual-thread")
.start(runnable);</pre>
<p>2) Используя ExecutorService с виртуальными потоками. Начиная с 5 версии языка Java, разработчикам рекомендуется использовать ExecutorServices вместо вызова класса Thread напрямую.</p>
<p>В Java 21 появился новый ExecutorService, использующий виртуальные потоки.</p>
<p>Приведем пример:</p>
<pre class="brush:java;">
Runnable runnable = () -> {
System.out.println("Tune-it greets you!");
};
try (ExecutorService executorService =
Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100; i++) {
executorService.submit(runnable);
}
}</pre>
<p>Этот код создает ExecutorService внутри оператора try-with-resource, создавая виртуальный поток для каждой утвержденной (submitted) задачи.</p>
<p>3) Используя фабрику виртуальных потоков. Вы можете создать фабрику, которая будет производить для вас виртуальные потоки.</p>
<p>Вот пример:</p>
<pre class="brush:java;">
Runnable runnable = () -> {
System.out.println("Hello, everyone!");
};
ThreadFactory virtualThreadFactory = Thread.ofVirtual()
.name("prefix", 0)
.factory();
Thread factoryThread = virtualThreadFactory.newThread(runnable);
factoryThread.start();</pre>
<p>Этот код создает фабрику виртуальных потоков, и каждый виртуальный поток, инициализированный с помощью этой фабрики, будет иметь имя, содержащее префикс и заканчивающееся числом.</p>
<p>Стоит отметить, что, хотя виртуальные потоки могут привнести значимые плюсы с точки зрения параллелизма и масштабируемости, они не всегда являются правильным выбором для каждой без исключения ситуации. Задачи, требующие значительных вычислений, могут не получить преимуществ от работы в виртуальных потоках и, более того, работать хуже из-за накладных расходов на переключение контекста.</p>
<h2>2. Структурированный параллелизм</h2>
<p>Структурированный параллелизм, который в настоящее время находится в стадии тестирования, упрощает программирование параллельных задач. Он позволяет рассматривать группы связанных задач, выполняемых в разных потоках, как единое целое. Это упрощает обработку ошибок и отмены, повышает надежность кода и облегчает понимание происходящего. Эта концепция уже была опробована в JDK 19 и JDK 20, вышедших в марте и сентябре 2022 года. Она будет протестирована еще раз в качестве предварительной версии в пакете util.concurrent. Единственным существенным отличием на этот раз является то, что метод StructuredTaskScope::(...) теперь возвращает [Subtask] вместо Future. Целью структурированного параллелизма является поощрить такой способ параллельного программирования, который избавляет от распространенных проблем, возникающих при отмене или завершении работы, таких как утечка потоков и задержки при отмене, а также облегчить понимание параллельного кода.</p>
<h2>3. Сборщик мусора Generational Z (ZGC)</h2>
<p>Сборщик мусора Generational Z Garbage Collector (ZGC) предназначен для повышения производительности приложений за счет поддержания отдельных поколений для молодых и старых объектов. Молодые объекты, как правило, умирают молодыми, поэтому разделение поколений позволяет ZGC чаще собирать молодые объекты. Приложения, работающие с поколенческим ZGC, потенциально имеют меньший риск возникновения сбоев при распределении памяти, меньшие требуемые затраты памяти кучи и меньшие затраты процессора на сборку мусора. Эти преимущества потенциально достижимы без существенного снижения пропускной способности по сравнению с GС без поколений.</p>
<h2>4. API механизма инкапсуляции ключей (Key Encapsulation Mechanism (KEM) API)</h2>
<p>В области шифрования KEM API является существенным дополнением. Он позволяет приложениям использовать передовые алгоритмы KEM для защиты симметричных ключей с помощью открытой криптографии, поддерживая шифрование нового поколения.</p>
<h2>5. Отказ от ветки (порта) JDK для Windows 32-bit x86</h2>
<p>Порт для Windows 32-bit x86 помечен как кандидат на удаление в одном из будущих выпусков JDK. Это изменение связано с невозможностью использования виртуальных потоков в 32-битных средах, а также с окончанием срока поддержки 32-битной версии Windows 10 в октябре 2025 года.</p>
<h2>6. Безымянные классы и методы экземпляра Main (Unnamed Classes and Instance Main Method)</h2>
<p>В Java 21 представлены две новые возможности языка: безымянные классы и новый протокол запуска, позволяющий запускать классы Java более просто и без большого количества стандартизированного текста.</p>
<p>1) Безымянный Java-класс - это возможность запускать вашу программу в Java-файле, у которого нет имени класса. Таким образом, вам больше не придется помещать запускаемый метод внутри оператора class.</p>
<p>2) Улучшенный протокол запуска. Теперь вы можете запускать свою программу с помощью простого метода main, который не является статическим. Этот метод не требует каких-либо особых входных данных и является отправной точкой для вашего класса.</p>
<p>Давайте рассмотрим все это на примере знаменитого hello world:</p>
<pre class="brush:java;">
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}</pre>
<p>И так, мы видим, что в исходном классическом варианте получается слишком много стандартизированного текста, слишком много кода.</p>
<p>Однако если применить улучшенный протокол запуска, код будет выглядеть гораздо проще:</p>
<pre class="brush:java;">
class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}</pre>
<p>Однако, это еще не все. Давайте применим безымянные классы, чтобы еще сильнее упростить код:</p>
<pre class="brush:java;">
void main() {
System.out.println("Hello, World!");
}
</pre>
<h2>7. Улучшенное сравнение с образцом. Безымянные образцы (шаблоны) и переменные (Unnamed Patterns and Variables). Сопоставление Records с образцом.</h2>
<p>i) Сравнение с образцом предполагает проверку наличия у объекта определенной структуры и извлечение из него данных в случае совпадения.</p>
<p>В Java 21 оно было улучшено, - стало более лаконичным и читабельным.</p>
<p>Например, использование сравнения в операторе "if" позволяет напрямую использовать сопоставленный объект:</p>
<pre class="brush:java;">
if (obj instanceof Circle c) {
System.out println("Radius: " + c.getRadius());
}</pre>
<p>ii) Еще одной новой возможностью языка Java 21 являются безымянные шаблоны и переменные.</p>
<p>Наряду с "безымянными классами и методами экземпляра main", они направлены на улучшение читаемости и сопровождаемости кода.</p>
<p>Например, если вы объявили в коде переменную, но не собираетесь ее использовать, то теперь ее можно заменить символом подчеркивания (_). Это может применяться в различных сценариях, например, в блоках try-catch, циклах for и при сопоставлении records с образцом (record pattern matching).</p>
<p>В этих двух примерах переменная 'e' и переменная 'c' не используются:</p>
<pre class="brush:java;">
try {
...
} catch(Exception e) {
System.out.println("An error has occurred!");
}
for(Car c: carList) {
++count;
}</pre>
<p>Поэтому для удобства мы можем заменить их безымянными переменными:</p>
<pre class="brush:java;">
try {
...
} catch(Exception _) {
System.out.println("An error has occurred!");
}
for(Car _: carList) {
++count;
} </pre>
<p>iii) Сравнение Records с образцом (Record Patterns).</p>
<p>Record Patterns могут использоваться для упрощения и улучшения читаемости кода, работающего с объектами типа Record.</p>
<p>Например, следующий код использует сравнение с безымянным образцом для выявления Record, у которой задано определенное значение поля x:</p>
<pre class="brush:java;">
record Point(int x, int y) {}
Point point = Point(10, 20);
if (point == Point(10, _)) { // Сравниваем с экземпляром point у которого x = 10
// выполняем действия с point
}
</pre>
<p> </p>
<h2>8. Scoped values (значения с ограниченной областью действия, ограниченные значения)</h2>
<p>Также продолжает тестироваться новая фича (которая уже была опробована в JDK 20), позволяющая обмениваться данными, которые не могут быть изменены внутри потоков и между ними. Речь идет о Scoped values. Это нововведение считается более лучшей практикой, чем использование локальных переменных потоков, особенно при использовании большого количества виртуальных потоков. Локальные переменные потоков имеют ряд проблем, например, их можно слишком часто изменять, они могут храниться слишком долго, и их использование может быть дорогостоящим. Scoped value позволяет безопасно обмениваться данными между частями большой программы без использования аргументов методов.</p>
<h2>9. Сравнение с образцом для оператора switch (Pattern Matching for switch)</h2>
<p>Java 21 расширяет возможности switch-выражений, позволяя использовать более гибкие шаблоны и упрощая условную логику.</p>
<p>К примеру:</p>
<pre class="brush:java;">
static void check(Object obj) {
String formatted = switch (obj) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> obj.toString();
};
}</pre>
<p>Или :</p>
<pre class="brush:java;">
String level = getLevel();
int levelCode = switch (level) {
case "Easy" -> 1;
case "Normal" -> 2;
case "Hard", "Skilled" -> 3;
case "Impossible" -> 4;
default -> break;
};</pre>
<h2>10. Упорядоченные коллекции (Sequenced Collections)</h2>
<p>Упорядоченные коллекции - это новый тип коллекций, который обеспечивает прямой доступ к первому и последнему элементам коллекции. Это может быть полезно для приложений, которым необходимо выполнять итерации по коллекции в определенном порядке. Упорядоченные коллекции реализуются с использованием структуры данных связанного списка (linked list). Это делает их эффективными для итераций по коллекции в прямом или обратном направлении.</p>
<pre class="brush:java;">
interface SequencedCollection<E> extends Collection<E> {
void addFirst(E);
void addLast(E);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
// новый метод
SequencedCollection<E> reversed();
}</pre>
<pre class="brush:java;">
import java.util.ArrayList;
import java.util.List;
import java.util.SequencedCollection;
public class SequencedCollectionExample {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
strings.add("Hello");
strings.add("from Tune-it!");
// Получаем упорядоченную коллекцию с реверсированным содержимым исходного списка строк:
SequencedCollection<String> reversedStrings = strings.reversed();
for (String string : reversedStrings) {
System.out.println(string);
} // напечатает "from Tune-it!" "Hello"
}
}</pre>
<h2>11. Строковые шаблоны (String Templates)</h2>
<p>Строковые шаблоны - это новая возможность в Java 21, которая позволяет интерполировать выражения в строковые литералы. Это может быть полезно для генерации динамического текста, например, сообщений об ошибках или персонализированных приветствий.</p>
<p>Синтаксис строковых шаблонов выглядит следующим образом:</p>
<pre class="brush:java;">
String name="Tune-it";
String text=STR. "The company name is {name}.";</pre>
<p>STR - это обработчик шаблонов, который сообщает компилятору, что это строковый шаблон. Точка (.) является разделителем между обработчиком шаблонов и строкой шаблона. Строка шаблона заключена в двойные кавычки. Выражения, подлежащие интерполяции, заключены в фигурные скобки.</p>
<p>В данном примере выражение {name} будет интерполировано со значением переменной name и в итоге получится строка "The company name is Tune-it".</p>
<p>Платформа Java предоставляет два обработчика шаблонов:</p>
<p>STR: Это обработчик шаблонов по умолчанию. Он выполняет интерполяцию строк.</p>
<p>FMT: Этот обработчик шаблонов можно использовать для форматирования текста, например, для добавления запятых к числам или преобразования строк в верхний регистр.</p>
<h2>Заключительные мысли</h2>
<p>В Java 21 появилось множество новых интересных функций и усовершенствований. Это говорит о том, что развитие Java продолжается и продолжается в правильном направлении.</p>
<p>Независимо от того, являетесь ли вы опытным разработчиком или начинающим, все эти улучшения призваны сделать ваш опыт программирования на Java более приятным и эффективным.</p>
<p>---------------------------------------------------------------------</p>
<p>При подготовке статьи использовались следующие материалы:</p>
<p>"Java Latest Version Update — Java 21" автора Abdul Kadhar</p>
<p>"Java 21: Unleashing Exciting Updates for All Developers!" автора Harshit Raj</p>
<p>"The New Features of Java 21" автора Kesk -*-</p>
<p> </p>Romo Fedoroff2023-09-21T13:37:00ZКонтроль репликации объектов Active Directory с помощью PowerShellДмитрий Сазоновhttps://www.tune-it.ru/c/blogs/find_entry?entryId=43960322023-09-20T05:37:21Z2023-09-20T04:55:00Z<p>Для контроля хода репликации объектов AD можно использовать средства PowerShell из модуля <strong>Active Directory. </strong></p>
<p>Для начала, импортируем модуль:</p>
<pre class="brush:as3;">
Import-Module ActiveDirectory</pre>
<p>Для выведения полного списка командлетов, связанных с работой по репликации AD можно выполнить:</p>
<pre class="brush:as3;">
get-command -module activedirectory -name *ADReplicat*</pre>
<p> </p>
<p>В случае, если нужно вывести список ошибок репликации на контроллере(или контроллерах) домена, можно воспользоваться командлетом</p>
<pre class="brush:as3;">
Get-ADReplicationFailure -Target DC1,DC2</pre>
<p>, где <strong>DC1,DC2</strong> - имена домен-контроллеров.</p>
<p> </p>
<p>Также можно запросить статус репликации для всех контроллеров домена в составе сайта:</p>
<pre class="brush:as3;">
Get-ADReplicationFailure -scope site -target {SITE} | FT Server, LastError, Partner-Auto</pre>
<p>, где {SITE} - имя сайта</p>
<p> </p>
<p>Тоже самое, но для отдельного домена:</p>
<p> </p>
<pre class="brush:as3;">
Get-ADReplicationFailure -Target "domen.com" -Scope Domain</pre>
<p> </p>
<p>Предположим, выявили проблемы с репликацией конкретного объекта AD. В этом случае можно попробовать принудительно инициировать его репликацию с помощью командлета <strong>Sync-ADObject.</strong></p>
<pre class="brush:as3;">
Get-ADDomainController -filter * | foreach {Sync-ADObject -Object "cn=Vasya Pupkin ,cn=Users,dc=domen,dc=com" -source DC1 -Destination $_.hostname}</pre>
<p> </p>
<p>В этом случае произойдет репликация на все контроллеры домена объекта с username <em>Vasya Pupkin.</em></p>
<p>Еще несколько полезных командлетов.</p>
<p><strong>Get-ADReplicationPartnerMetadata</strong> позволяет получить информацию о метаданных репликации между DC и его партнерами, в частности время последней попытки выполнить репликацию и время последней успешной репликации( для этого нужно выбрать данные, которые мы хотим получить (Select-Object Server, LastReplicationAttempt, LastReplicationSuccess, Partner)</p>
<p>С помощью командлета <strong>Get-ADReplicationQueueOperation </strong>можно получить список операций ожидающих репликации на сервере.</p>
<p>Командлет<strong> Get-ADReplicationConnection </strong>позволяет вывести информацию о партнерах репликации для текущего контролера домена. Например, если мы хотим узнать эту информацию для конкретного домен-контроллера, можно выполнить:</p>
<pre class="brush:as3;">
Get-ADReplicationConnection -Filter {ReplicateToDirectoryServer -eq "DC1"}</pre>
<p>Командлет <strong>Get-ADReplicationUpToDatenessVectorTable</strong> выдает список USN для партнеров по репликации:</p>
<pre class="brush:as3;">
Get-ADReplicationUpToDatenessVectorTable * | ft Partner,Server,UsnFilter</pre>
<p> </p>
<p>Для диагностирования проблем с репликацией AD также полезны утилиты <strong>dcdiag,repadmin, nslookup, </strong>их использование будет рассмотрено отдельно (как и диагностика возможных проблем, например утеря доверительных отношений).</p>Дмитрий Сазонов2023-09-20T04:55:00ZРабота с транзакциями в Spring FrameworkErik Karapetyanhttps://www.tune-it.ru/c/blogs/find_entry?entryId=178625962023-09-11T06:33:50Z2023-09-09T20:30:00Z<h3>Основы транзакций в Spring</h3>
<p>Spring поддерживает программное и декларативное управление транзакциями. Программное управление транзакциями обычно происходит с помощью <code>TransactionTemplate</code>, тогда как декларативное управление транзакциями основано на аннотациях или XML-конфигурации.</p>
<h3>Декларативное управление транзакциями</h3>
<p>Самый популярный способ управления транзакциями в Spring - это декларативное управление с помощью аннотаций. Для этого нужно:</p>
<ol>
<li><strong>Настроить менеджер транзакций</strong>: Например, для JPA это может быть <code>JpaTransactionManager</code>.</li>
<li><strong>Включить поддержку аннотаций</strong>: Это можно сделать с помощью аннотации <code>@EnableTransactionManagement</code> или XML-конфигурации.</li>
<li><strong>Использовать аннотацию <code>@Transactional</code></strong>: Эту аннотацию можно применять к методам или классам, чтобы указать, что метод или все методы класса должны выполняться в транзакционном контексте.</li>
</ol>
<p>Пример:</p>
<pre class="brush:java;">
@Service
@Transactional
public class MyService {
@Autowired
private MyRepository repository;
public void saveData(Data data) {
repository.save(data);
}
}</pre>
<h3>Программное управление транзакциями</h3>
<p>Хотя декларативное управление транзакциями является наиболее популярным, иногда может потребоваться программное управление. Для этого можно использовать <code>TransactionTemplate</code>.</p>
<p>Пример:</p>
<pre class="brush:java;">
@Service
public class MyService {
@Autowired
private TransactionTemplate transactionTemplate;
@Autowired
private MyRepository repository;
public void saveData(Data data) {
transactionTemplate.execute(status -> {
repository.save(data);
return null;
});
}
}</pre>
<h3>Советы по работе с транзакциями</h3>
<ul>
<li><strong>Минимизируйте время жизни транзакции</strong>: Долгие транзакции могут привести к блокировкам и уменьшению производительности.</li>
<li><strong>Остерегайтесь "ленивой" инициализации в транзакциях</strong>: Если вы используете JPA или Hibernate, убедитесь, что ваши сущности инициализированы до завершения транзакции.</li>
<li><strong>Будьте внимательны к уровням изоляции</strong>: Уровень изоляции может влиять на производительность и поведение вашего приложения.</li>
</ul>
<p> </p>Erik Karapetyan2023-09-09T20:30:00Z