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

Java

Андрей Когунь зачем развивать IT-сообщество

09.06.2021 16:12:52 | Автор: admin

23 июня DINS проводит бесплатную онлайн-конференцию Java Meeting Point. Наша цель объединить инженеров из разных городов на одной площадке, дать возможность обсудить новые технологии, подходы в разработке и все, что с этим связано. Спикеры конференции инженеры крупных IT-компаний.

Мы решили познакомить вас с людьми, которые выступают на конференции в серии интервью. Наш первый герой Андрей Когунь, ведущий Java Meeting Point, руководитель группы Java-разработчиков в КРОК и основатель jug.msk.ru. Андрей рассказал, почему его вдохновляют митапы, как он успевает совмещать работу и конференции и сложно ли управлять московским сообществом из Кипра.


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

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

Мне повезло моя работа совпадает с интересами. Я организую и провожу встречи jug.msk.ru, участвую в конференциях JUG Ru Group и в мероприятиях КРОК. В сезон успеваю поучаствовать в двух-четырех больших конференциях.

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

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

Почему еще ты считаешь важным участвовать в жизни сообщества?

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

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

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

Столько всего: работа, конференции, jug.msk.ru, студенты как ты все успеваешь?

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

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

Сейчас ты не в Москве. Скажи, сложно ли управлять сообществом удаленно? Или после 2020 года стало нормально?

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

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

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

Заключительный вопрос: почему ты решил участвовать в Java Meeting Point?

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

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

Так чаще всего и работает в IT-мире: если предложить человеку что-то интересное, он согласится. У меня есть хорошая история про это. Когда я только начинал, работал в вузе, Oracle, который спонсировал студенческое сообщество, поручил провести Open Software Day. Нужно было найти спикеров, которые расскажут про открытое ПО. На тот момент я знал примерно никого. Я начал гуглить и нашел Дмитрия Завалишина, который делал операционную систему с открытым кодом Фантом. Дмитрия уже в то время называли одним из отцов российского интернета.

Он вел Live Journal, и я ему написал прямо в директ и пригласил в Зеленоград, выступить на нашем митапе. Он сразу откликнулся и приехал к нам. Хотя мы из разных поколений, мы общаемся до сих пор, когда встречаемся на мероприятиях. Я рад, что в IT большинство людей такие.


Регистрация на Java Meeting Point уже открыта на сайте конференции. Присоединяйтесь будет интересно!

Подробнее..

Паша Финкельштейн о Big Data, Apache Spark и DevRel

15.06.2021 10:13:02 | Автор: admin

Паша Финкельштейн разработчик, серийный спикер, автор и ведущий нескольких подкастов. На конференции Java Meeting Point он сделает доклад Spark: let's touch it, на котором познакомит участников с миром больших данных.

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


Расскажи, чем ты занимаешься?

Я Developer Advocate в JetBrains, занимаюсь темой Big Data и дата-инжиниринга. Я пытаюсь рассказывать людям о том, как устроен мир Big Data, что там интересного, какие есть инструменты.

О чем будет твой доклад на Java Meeting Point?

Я расскажу, что такое большие данные и как они отличаются от маленьких. Разберемся, как работает Apache Spark, как устроен его API, и поймем, что ничего принципиально сложного в этом нет.

Кому будет полезно посмотреть доклад?

Полезно будет Java или JVM-разработчикам ровня Middle+ и тем, кому интересно узнать, как работает Apache Spark. Мы научимся писать простенькие пайплайны на этом фреймворке. Станет понятно как, например, взять и написать пайплайн обработки данных на Apache Spark или проанализировать данные в датасете.

Ты много занимаешься DevRel-активностями: выступаешь с докладами, вел подкасты. Как ты начал этим заниматься?

Мой друг Слава Семушин уехал работать в Чехию. Я не особенно умею поддерживать отношения на расстоянии, но Слава очень классный, с ним хотелось продолжать общаться. Однажды он написал: Слушай, давай запишем подкаст. Решили попробовать, и так родился подкаст Паша+Слава.

Раз в пару недель мы созванивались и говорили на околотехнические темы. Потом к нам присоединился еще один Слава Артемьев, с которым я работал в компании Домклик. Очень удобно не пришлось менять название подкаста. Так возникла моя первая публичность.

Потом появилась идея выступить на сцене, и я сделал доклад на Joker. В последствии этот доклад я переделывал 3 раза и выступил с ним 14 раз.

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

Можешь посоветовать, как научиться так же быстро готовить выступления?

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

Что должно быть в хорошем докладе?

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

Альтернативный вариант, это если доклад полностью теоретический. На такой люди придут вывернуть мозги. Так тоже можно, но сложно. Я один раз сделал доклад про внутренности Kotlin, людям не зашло.

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

Какой формат будет у твоего доклада на Java Meeting Point?

Это будет демо, на котором Java или JVM-разработчик на примере не очень больших данных сможет посмотреть, как работают большие. Мы посмотрим 5-6 слайдов и перейдем к программированию.

Подробнее..

Дмитрий Александров Мы не знали, во что ввязываемся

17.06.2021 20:21:19 | Автор: admin

Дмитрий Александров инженер Oracle, Java Champion, участник и организатор многих IT-мероприятий. На Java Meeting Point 23 июня он расскажет про преимущества фреймворка Helidon, над которым работает.

Мы поговорили с Дмитрием и узнали, чем он поделится с участниками Java Meeting Point, каким был его путь в программировании и как он с товарищами организовал конференцию на 450 человек без опыта в крупных ивентах.


Расскажи, как ты начал заниматься программированием?

Я всегда любил кодить и занимался этим практически всю свою сознательную (может, даже и бессознательную) жизнь. Мне было лет 8 или 9, когда я начал. У моего поколения была такая штука Роботландия на 286-ых компьютерах. Можно было делать нехитрые задачки. И еще на этих же компах был установлен бейсик я в классе 6-м уже писал какие-то простые программы.

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

Потом у меня был собственный стартап, который я успешно продал. Там была чистая Java, и это было своего рода счастье, потому что я сам выбирал, с чем работать. Но в этом проекте приходилось кодить по 12 часов в день, и в таком режиме выгораешь жестко. Только я решил отдохнуть, как меня срекрутили в T-Systems, и через неделю после выхода на работу я уже был в командировке в Германии. Перерыв получился всего в 4 дня.

Сейчас я работаю на Oracle. Работаю над замечательным опенсорсным фреймворком, который называется Helidon.

О нем и пойдет речь на Java Meeting Point, верно? Расскажи подробнее, о чем будет доклад.

На конференции я расскажу, что такое Helidon, и почему микросервисы стоит писать на нем. Дело в том, что большинство фреймворков, которые сейчас используются, как правило, обросли легаси, и чтобы их саппортить, нужно много стараться. А у нас все новенькое и модульное, Java 11+, много фишек, которые не все себе могут позволить, потому что у них есть как раз это легаси. А у нас его нет, поэтому у нас все круто.

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

У Helidon есть большое преимущество это опенсорс. У нас лицензия Apache 2.0, а значит, все могут контрибьютить в проект. Это не реклама, а побуждение к тому, чтобы влиться в сообщество.

Ты много делаешь, чтобы развивать IT-сообщество: выступаешь сам, организуешь митапы и конференцию JPrime в Болгарии. Как ты стал спикером и организатором мероприятий?

Я выбрал для себя роль спикера, потому что очень боялся разговаривать на публике. Чтобы побороть этот страх, я просто начал выступать. Сначала собирался с друзьями, ребятами, которые тоже посещали IT-конференции, мы делали доклады друг для друга. У нас в Софии тогда было полумертвое коммьюнити, нас всего пять человек было.

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

Мы даже не знали на тот момент, во что ввязываемся. Мы все кодили по 8-10 часов в день, и организация конференции стала дополнительной полноценной работой. Тем не менее, деньги нам дали в феврале, а в мае мы уже провели однодневную конференцию на 450 человек. После чего мы продолжили развиваться, к нам подтянулись известные спикеры, и до ковида мы уже провели двухдневное мероприятие на 1000 участников. Причем на него не только компании для сотрудников покупали билеты, но и просто отдельные люди. Но мы ничего на этом не зарабатываем: наша цель формировать коммьюнити.

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

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

Здорово! Это похоже на то, что мы делаем в DINS: все наши мероприятия бесплатные и преследуют одну цель дать инженерам возможность для обмена знаниями и опытом. А что еще тебе нравится в этой деятельности?

Мне нравится этим заниматься, потому что я знакомлюсь с интересными людьми. Здесь нет материального интереса, главное, что я встречаю самых крутых программистов в мире. Я могу многому у них научиться, могу напрямую задавать вопросы это очень ценно.

Приходите на Java Meeting Point 23 июня, чтобы услышать доклад Дмитрия. Регистрация открыта на сайте конференции.

Подробнее..

Как я сделал свою сборку Gulp для быстрой, лёгкой и приятной вёрстки

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

Серьёзно и профессионально я начал заниматься вёрсткой в 2019 году, хотя до этого ещё со школы интересовался данной темой как любитель. Поэтому новичком мне себя назвать сложно, но и профессионалом с опытом 5+ лет я тоже не являюсь. Тем не менее, я успел познакомиться со сборщиком Gulp, его плагинами и сделал для себя хорошую, как по мне, сборку для работы. О её возможностях сегодня и расскажу.

ВАЖНО! В этой статье речь пойдёт о самой последней версии сборки. Если вы пользуетесь версиями сборки, вышедшими до публикации этой статьи, информация будет для вас не релевантна, но полезна.

Какие задачи решает эта сборка?

  • вёрстка компонентами (вам не нужно в каждую страницу копировать head, header, footer и другие повторяющиеся элементы, вплоть до кнопок или кастомных чекбоксов);

  • вёрстка с препроцессорами (SASS/SCSS);

  • конвертация шрифтов из ttf в eot, woff, woff2;

  • лёгкое (почти автоматическое) подключение шрифтов;

  • лёгкое (почти автоматическое) создание псевдоэлементов-иконок;

  • обработка изображений "на лету";

  • минификация html/css/js файлов;

  • возможность вёрстки с использованием php;

  • выгрузка файлов на хостинг по FTP;

  • несколько мелких задач с помощью миксинов.

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

Собственно создание сборки

Начнём собирать нашу сборку (простите за тавтологию). Предварительно нам потребуется уже установленная на компьютере LTS-версия Node.js и NPM (входит в пакет Node.js) либо Yarn. Для нашей задачи не имеет значения, какой из этих пакетных менеджеров использовать, однако я буду объяснять на примере NPM, соответственно, для Yarn вам потребуется нагуглить аналоги NPM-команд.

Первое, что нам нужно сделать - это инициализировать проект. Открываем директорию проекта в командной строке (очень надеюсь, вы знаете, как это делается) и вводим команду npm init.

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

Далее будет намного удобнее работать через Visual Studio Code (поскольку у него есть встроенный терминал) или любой другой удобный вам редактор + терминал.

Прежде всего, нам нужно установить сам Gulp. Делается это двумя командами npm i gulp -global - устанавливаем Gulp глобально на систему и npm i gulp --save-dev - устанавливаем Gulp локально в проект. Ключ --save здесь отвечает за сохранение версии плагина при дальнейшей установке (без него вам может установить более новую, несовместимую с другими плагинами версию), а ключ -dev указывает на то, что этот пакет необходим только во время разработки проекта, а не во время его выполнения. Например, если мы устанавливаем в проект пакет Swiper, который содержит скрипты слайдера и будет отображаться на странице, мы будем устанавливать его без ключа -dev, поскольку он нужен для выполнения, а не для разработки.

После того, как Gulp установился, имеет смысл создать в корне проекта управляющий файл gulpfile.js, в котором мы и будем писать задачи для сборщика.

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

const gulp = require('gulp');

Далее, для каждой задачи будем использовать модули в отдельных файлах. Для того, чтобы не подключать каждый модуль отдельно, нужно установить и подключить плагин require-dir. Устанавливается он всё той же командой (как и все последующие плагины, поэтому далее повторяться не буду, просто знайте, что установить - это npm i $PLUGIN-NAME$ --save-dev). После установки подключаем его и прописываем путь к директории, в которую будем складывать модули (у меня это директория tasks):

const gulp = require('gulp');const requireDir = require('require-dir');const tasks = requireDir('./tasks');

Первая задача

Давайте проверим, всё ли мы правильно сделали. Создадим в директории tasks файл модуля с именем hello.js. В созданном файле напишем простейшую функцию, которая будет выводить в консоль строку "Hello Gulp!" (можете придумать что-то менее банальное, если хотите).

module.exports = function hello () {console.log("Hello Gulp!");}

Теперь вернёмся в gulpfile.js и зададим там задачу hello:

const gulp = require('gulp');const requireDir = require('require-dir');const tasks = requireDir('./tasks');exports.hello = tasks.hello;

Теперь командой gulp hello в терминале запустим нашу задачу. Если всё сделано правильно - в терминал должно вывестись приблизительно такое сообщение:

[13:17:15] Using gulpfile D:\Web projects\Easy-webdev-startpack-new\gulpfile.js[13:17:15] Starting 'hello'...Hello Gulp![13:17:15] The following tasks did not complete: hello[13:17:15] Did you forget to signal async completion?

Так же, можно получить список всех заданных задач командой gulp --tasks.

Файловая структура

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

В директории src/ нам понадобятся следующие поддиректории:
  • components/ - директория для компонентов

  • components/bem-blocks/ - директория для БЭМ-блоков

  • components/page-blocks/ - директория для типовых блоков страницы, таких как хедер, футер и т.п.

  • fonts/ - директория для шрифтов

  • img/ - директория для изображений

  • js/ - директория для файлов JavaScript

  • scss/ - директория для файлов стилей

  • scss/base/ - директория для базовых стилей, которые мы изменять не будем

  • svg/ - директория для файлов SVG

  • svg/css/ - директория для SVG-файлов, которые будут интегрироваться в CSS

Получиться в итоге должно приблизительно следующее:

 project/  build/ 
Подробнее..

Как написать пассивный доход Пишем качественного трейд бота на JS (часть 1)

12.06.2021 20:19:26 | Автор: admin

Начнем писать трейдинг бота, который будет работать на криптобирже Binance. Бот должен уметь:

  1. торговать самостоятельно, принося какой-то доход

  2. должен быть удобен для создания и обкатывания различных стратегий торговли

  3. тестировать стратегию на исторических данных

Пожалуй, начнем с архитектуры

У нас есть биржа Binance, у которой есть шикарное api. Поэтому архитектура могла бы выглядеть так:

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

Базу выбрал PostgreSQL. Тут нет никакого тайного умысла. Вы можете использовать любую.

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

Сервис для логов

Простой класс, который принимает на вход префикс для логирования и имеет два метода log и error. Эти методы печатают лог с текущим временем и перфиксом:

class LoggerService {  constructor(prefix) {    this.logPrefix = prefix  }  log(...props) {    console.log(new Date().toISOString().substr(0, 19), this.logPrefix, ...props)  }  error(...props) {    console.error(new Date().toISOString().substr(0, 19), this.logPrefix, ...props)  }}

Теперь подключим биржу

yarn add node-binance-api

Добавим класс BaseApiService. Сделаем в нем инициализацию Binance SDK, а также применим сервис LoggerService. Учитывая мой опыт с Binance могу сразу сказать, что в зависимости от торговой пары мы должны слать цену и обьем с разным количеством знаков после запятой. Все эти настройки для каждой пары можно взять, сделав запрос futuresExchangeInfo(). И написать методы для получения количества знаков после запятой для цены getAssetPricePrecision и объема getAssetQuantityPrecision.

class BaseApiService {  constructor({ client, secret }) {    const { log, error } = new Logger('BaseApiService')    this.log = log    this.error = error    this.api = new NodeBinanceApi().options({      APIKEY: client,      APISECRET: secret,      hedgeMode: true,    })    this.exchangeInfo = {}  }  async init() {    try {      this.exchangeInfo = await this.api.futuresExchangeInfo()    } catch (e) {      this.error('init error', e)    }  }  getAssetQuantityPrecision(symbol) {    const { symbols = [] } = this.exchangeInfo    const s = symbols.find(s => s.symbol === symbol) || { quantityPrecision: 3 }    return s.quantityPrecision  }  getAssetPricePrecision(symbol) {    const { symbols = [] } = this.exchangeInfo    const s = symbols.find(s => s.symbol === symbol) || { pricePrecision: 2 }    return s.pricePrecision  }}

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

async futuresOrder(side, symbol, qty, price, params={}) {  try {    qty = Number(qty).toFixed(this.getAssetQuantityPrecision(symbol))    price = Number(price).toFixed(this.getAssetPricePrecision(symbol))    if (!params.type) {      params.type = ORDER.TYPE.MARKET    }    const res = await this.api.futuresOrder(side, symbol, qty, price || false, params)    this.log('futuresOrder', res)    return res  } catch (e) {    console.log('futuresOrder error', e)  }}

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

class TradeService {  constructor({client, secret}) {    const { log, error } = new LoggerService('TradeService')    this.log = log    this.error = error    this.api = new NodeBinanceApi().options({      APIKEY: client,      APISECRET: secret,      hedgeMode: true,    })    this.events = new EventEmitter()  }  marginCallCallback = (data) => this.log('marginCallCallback', data)  accountUpdateCallback = (data) => this.log('accountUpdateCallback', data)  orderUpdateCallback = (data) => this.emit(data)  subscribedCallback = (data) => this.log('subscribedCallback', data)  accountConfigUpdateCallback = (data) => this.log('accountConfigUpdateCallback', data)  startListening() {    this.api.websockets.userFutureData(      this.marginCallCallback,      this.accountUpdateCallback,      this.orderUpdateCallback,      this.subscribedCallback,      this.accountConfigUpdateCallback,    )  }  subscribe(cb) {    this.events.on('trade', cb)  }  emit = (data) => {    this.events.emit('trade', data)  }}

При помощи метода из SDK this.api.websockets.userFutureData подписываемся на события из биржы. Самой главный колбек для нас this.orderUpdateCallback . Он вызывается каждый раз когда меняется статус у ордера. Ловим это событие и прокидываем через EventEmitter тому, кто на это событие подписался, используя метод subscribe.

Перейдем к базе данных

Для чего она нужна? В базе будем хранить все ордера, а также всю историю торговли бота. Пользователей с их ключами к бирже и балансами. В последствии сможем считать сколько бот принес прибыли/убытка. Тут останавливаться долго не буду. Подключаю sequlize.

yarn add sequelize-cli -Dyarn add sequelizenpx sequelize-cli init

Добавим docker-compose.yml файл для локальной базы:

version: '3.1'services:  db:    image: 'postgres:12'    restart: unless-stopped    volumes:      - ./volumes/postgresql/data:/var/lib/postgresql/data    environment:      POSTGRES_USER: root      POSTGRES_PASSWORD: example      POSTGRES_DB: bot    ports:      - 5432:5432    networks:      - postgresnetworks:  postgres:    driver: bridge

А также добавляю миграции и модели. User, Order

Продолжение следует.

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

Подробнее..

Распознавание команд

03.06.2021 20:19:56 | Автор: admin

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

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

/** Правило проверяет лексему на соответствие */typealias Rule = (String) -> Boolean/** Нормализованное семантическое представление */open class Semnorm(vararg val rules: Rule)/** Правило задает стемы для семантических представлений */fun stem(vararg stems: String): Rule = { stems.any(it::startsWith) }/** Правило задает точные соответствия для семантических представлений */fun word(vararg words: String): Rule = { words.any(it::equals) }/** Проверяем слово на соответствие семантике */fun String.matches(norm: Semnorm) = norm.rules.any { it(this) }

Теперь у нас появилась возможность задавать предопределенные нормализованные семантические представления в виде объектов:

object Day : Semnorm(stem("day", "суток", "сутк", "дня", "ден", "дне"))

Фреймворк ставит их в соответствие лексемам входящих фраз, и предложение начинает выглядеть, например так:

assertThat(  "забань васю на 5 минут".tokenize(),   equalTo(   listOf(     Token("забань", Ban),      Token("васю", null),     Token("на", null),      Token("5", Number),     Token("минут", Minute)   )  ))

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

object Help : ExecutableSemnorm(stem(  "помощ", "справк", "правил", "help",   "rule", "faq", "start", "старт",)) {  override fun execute(bot: Botm: Message) {    val faq = message.from.relatedFaq()    bot.sendMessage(m.chat.id, faq)  }}

Что насчет более сложного поведения, зависящего от различных слов в предложении? Оно тоже поддерживается, вот как, например, исполняется, уже известное из тестов предложение забанить Васю:

object Ban : DurableSemonrm(stem(  "ban", "block", "mute", "бан", "блок",  "забан", "завали", "замьют",)) {  override fun execute(    bot: Bot, attackerMessage: Message, duration: Duration) {    val victimMessage = attackerMessage.replyToMessage    val victimId = victimMessage.from.id    val untilSecond = now().epochSecond + duration.inWholeSeconds    bot.restrictChatMember(      attackerMessage.chat.id, victimId, untilSecond)  }}

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

object Week : Semnorm(stem("week", "недел")) {  override fun toDuration(number: Long) =     days(number) * 7}

Или для любых команд, зависящих от времени:

class DurableSemnorm(vararg rules: Rule) : ExecutableSemnorm(*rules) {  final override fun execute(    token: Iterator<Token>, bot: Bot, m: Message) =       execute(bot, message, token.parseDuration())  abstract fun execute(bot: Bot, m: Message, duration: Duration)}

Благодаря такой архитектуре, нам больше не приходится думать о запутанной логике работы интерпретатора. Достаточно просто определить желаемые атрибуты для семантических представлений и наслаждаться результатом. Пример бота, использующего эту концепцию, можно посмотреть на Github: https://github.com/demidko/timecobot

Подробнее..

Разгоняем REACTOR

12.06.2021 18:20:44 | Автор: admin

Кому будет интересно?

Реактор сегодня - это стильно, модно, молодежно. Почему многие из нас практикуют реактивное программирование? Мало кто может ответить однозначно на этот вопрос. Хорошо - если Вы понимаете свой выигрыш, плохо - если реактор навязан организацией как данность. Большинство аргументов "ЗА" - это использование микросервисной архитектуры, которая в свою очередь обязывает микросервисы часто и много коммуницировать между собой. Для коммуникации в большинстве случаев выбирают HTTP взаимодействие. Для HTTP нужен легковесный веб-сервер, а что первое приходит на ум? Tomcat. Тут появляются проблемы с лимитом на максимальное количество сессий, при превышении которого веб-сервер начинает реджектить запросы (хотя лимита этого не так уж и легко достичь). Здесь на подмогу приходит реактор, который подобными лимитами не ограничен, и, например, Netty в качестве веб-сервера, который работает с реактивностью из коробки. Раз есть реактивный веб-сервер, нужен реактивный веб-клиент (Spring WebClient или Reactive Feign), а раз клиент реактивный, то вся эта жуть просачивается в бизнес логику, Mono и Flux становятся Вашими лучшими друзьями (хотя по началу есть только ненависть :))

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

Блокирующий и неблокирующий код

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

Лидер здесь - HTTP взаимодействие, вариантов масса, выбирай любой. Я предпочитаю Reactive Feign от Playtika, в комбинации со Spring Boot + WebFlux + Eureka мы получаем очень годную сборку для микросервисной архитектуры.

Давайте по-простому: НЕблокирующий код, это обычно всё, в названии чего есть reactive, а блокирующий - все оставшееся :) Hibernate + PostgreSQL - блокирующий, отправить почту через JavaMail - блокирующий, скинуть сообщение в очередь IBMMQ - блокирующий. Но есть, например, реактивный драйвер для MongoDB - неблокирующий. Отличительной особенностью блокирующего кода, является то, что глубоко внутри произойдет вызов метода, который заставит Ваш поток ждать (Thread.sleep() / Socket.read() и многие подобные), что для реактора - как нож в спину. Что же делать? Большинство бизнес логики завязано на базу данных, без нее никуда. На самом деле достаточно знать и уметь делать 2 вещи:

  • Необходимо понимать где блокирующий код. В этом может помочь проект BlockHound или его аналоги (тут тема для отдельной статьи)

  • Исполнение блокирующего кода необходимо переключать на пулы, готовые его выполнять, например: Schedulers.boundedElastic(). Делается это при помощи операторов publishOn & subscribeOn

Разгоняемся сами

Перед тем, как продолжить, необходимо немного размяться!

Уровень 1

    @Test    fun testLevel1() {        val result = Mono.just("")            .map { "123" }            .block()        assertEquals("123", result)    }

Начнем с простого, такой код обычно пишут начинающие reactor программисты. Как начать цепочку? Mono.just и ты на коне :) Оператор map трансформирует пустую строку в "123" и оператор block делает subscribe.

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

Уровень 2

    fun nonBlockingMethod1sec(data: String)     = data.toMono().delayElement(Duration.ofMillis(1000))    @Test    fun testLevel2() {        val result = nonBlockingMethod1sec("Hello world")            .flatMap { nonBlockingMethod1sec(it) }            .block()        assertEquals("Hello world", result)    }

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

Уровень 3

    fun collectTasks() = (0..99)    @Test    fun testLevel3() {        val result = nonBlockingMethod1sec("Hello world")            .flatMap { businessContext ->                collectTasks()                    .toFlux()                    .map {                        businessContext + it                    }                    .collectList()            }            .block()!!        assertEquals(collectTasks().toList().size, result.size)    }

Начинаем добавлять самое интересное - Flux! У нас появляется метод collectTasks, который собирает массив из сотни чисел, и далее мы делаем из него Flux - это будет наш список задач. К каждой задаче мы применяем трансформацию через оператор map. Оператор collectList собирает все результаты в итоговый список для дальнейшего использования.

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

Уровень 4

    fun collectTasks() = (0..100)        @Test    fun testLevel4() {        val result = nonBlockingMethod1sec("Hello world")            .flatMap { businessContext ->                collectTasks().toFlux()                    .flatMap {                        Mono.deferContextual { reactiveContext ->                            val hash = businessContext + it + reactiveContext["requestId"]                            hash.toMono()                        }                    }.collectList()            }            .contextWrite { it.put("requestId", UUID.randomUUID().toString()) }            .block()!!        assertEquals(collectTasks().toList().size, result.size)    }

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

Уровень 5

    fun collectTasks() = (0..1000)        fun doSomethingNonBlocking(data: String)        = data.toMono().delayElement(Duration.ofMillis(1000))        fun doSomethingBlocking(data: String): String {        Thread.sleep(1000); return data    }    val pool = Schedulers.newBoundedElastic(10, Int.MAX_VALUE, "test-pool")    private val logger = getLogger()    @Test    fun testLevel5() {        val counter = AtomicInteger(0)        val result = nonBlockingMethod1sec("Hello world")            .flatMap { _ ->                collectTasks().toFlux()                    .parallel()                    .runOn(pool)                    .flatMap {                        Mono.deferContextual { _ ->                            doSomethingNonBlocking(it.toString())                                .doOnRequest { logger.info("Added task in pool ${counter.incrementAndGet()}") }                                .doOnNext { logger.info("Non blocking code finished ${counter.get()}") }                                .map { doSomethingBlocking(it) }                                .doOnNext { logger.info("Removed task from pool ${counter.decrementAndGet()}") }                        }                    }.sequential()                    .collectList()            }            .block()!!        assertEquals(collectTasks().toList().size, result.size)    }

Вот мы и добрались до итогового варианта! Часть с реактивным контекстом была опущена для более наглядной демонстрации того, зачем мы здесь собрались. У нас появились два новых метода: doSomethingNonBlocking (3) & doSomethingBlocking (6) - один с неблокирующим ожиданием в секунду, второй с блокирующим. Мы создали пул потоков для обработки задач (10), добавили счетчик активных задач в реакторе (15). У нас появился оператор parallel (19) и обратный ему sequential (29). Задачи мы назначили на свежесозданный пул (20). Для понимания, что же происходит внутри, добавили логирование внутри операторов doOnRequest (вызывается перед исполнением метода), doOnNext (вызывается после исполнения метода). Основная задумка - на примере, определить сколько задач одновременно выполняется в реакторе и за какое время цепочка завершит свою работу.

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

И вот здесь начинается самое интересное. Попробуйте ответить на несколько вопросов. Как Вы считаете, сколько времени будет выполнятся данная цепочка? В ней 100 задач, в каждой задаче неблокирующее ожидание в 1 секунду, блокирующее ожидание в 1 секунду, и у нас в наличии пул из 10 потоков? (Вполне годная задачка на собеседование senior reactor developer :))

Правильный ответ

Около 12 секунд. Рассуждаем от блокирующего :) Блокирующее ожидание никуда не деть, и тут имеем 100 блокирующих секунд на 10 потоков, итого 10 секунд. Неблокирующее ожидание заметно нам лишь в первый раз, далее оно незаметно запускается в передышках между блокирующим. Не забываем про одну секунду сбора "бизнес контекста" перед запуском задач.

А теперь уберем строку (26) .map { doSomethingBlocking(it) } . Освободим наш реактор от блокирующего кода, интересно, сколько теперь времени займет выполнение цепочки?

Правильный ответ

2 секунды! 1 на сбор "бизнес контекста" и 1 на выполнение всех задач. Реактор запустит 100 задач одновременно. Но ведь у нас пул из 10 потоков? Как так? Первый разрыв шаблона.

Мы идем до конца и увеличиваем количество задач в методе collectTasks() до ... 1000? а может быть сразу до 15000? Как долго реактор будет выполнять столько задач?

Правильный ответ

2 секунды! 1 на сбор "бизнес контекста" и 1 на выполнение всех задач. Реактор запустит ВСЕ задачи одновременно. Второй разрыв шаблона. Где предел?

А это вообще легально?

Как же так и как это контролировать? Почему это опасно? Что если внутри параллельной обработки Вы решите вызвать другой микросервис? Если у вас 30000 задач, и по завершению каждой, Вам нужно отправлять запрос соседнему микросервису, Вы с удивлением можете обнаружить, что реактор непременно постарается выполнить все вызовы одновременно (Вы ведь используете реактивный web-client или реактивный feign, верно?) Открытие такого большого количества сокетов повлечет за собой превышение лимита открытых файловых дескрипторов в системе, что как минимум создаст проблемы с невозможностью создания новых сокетов в системе и помешает другим сервисам, а как максимум повалит Вам на сервере SSH и Вы потеряете доступ к серверу. Сомневаюсь, что в этот момент, программист будет кричать "зато смотри как быстро работает".

Разрыв шаблона. Thread Pool & Reactor

Основная проблема начинающего реактор программиста - это образ мышления, если есть медленный процесс - добавь X потоков, будет быстрее в X раз, а если слишком быстро - сократи количество потоков. Как всё просто было раньше? :) С реактором это не работает.

Классический thread pool - двери. Больше дверей - больше пропускная способность, все работает быстрее.

Теперь встречайте reactor! Вы видите двери? Нет никаких дверей

Реактор это большой мешок с подарками, или воздушная труба, задачи в которую валятся и летают там пока не выполнятся. А кто эти люди в желтом? Это наши epoll реактивные потоки, которые ни в коем случае нельзя нагружать блокирующими задачами. Можно провести аналогию с прорабами или инженерами. Они здесь, чтобы управлять процессом, а не чтобы выполнять тяжелую работу. Займите одного инженера тяжелой задачей, и когда к нему придет следующий рабочий с вопросом "что делать дальше?", он не сможет ответить, потому что был занят. Вот так и появляются таймауты в реактивном коде. Казалось бы микросервис стоит без нагрузки, выполняет какие-то задачки, а один из 500 запросов к нему падает с тайм-аутом, и непонятно почему. Велика вероятность что инженер был занят блокирующей задачей! Заботьтесь о своих инженерах и поручайте тяжелую работу специально обученным рабочим, например, Schedulers.boundedElastic().

Как контролировать эту "трубу", в которую валится всё без контроля? Вот мы и подошли к кульминации

Конфигурируем реактор!

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

Парад настроек открывает parallel с его аргументом parallelism

Меняя parallelism, мы можем регулировать количество запускаемых rails (это местное понятие реактора, которое похоже на корутины, но по сути является количеством одновременно выполняемых неблокирующих задач). Prefetch мы рассмотрим более подробно в следующем разделе.

Но одного parallelism недостаточно, реактор все еще будет нагребать задач как не в себя.

Мало кто обращал внимание что у оператора flatMap (только того что запускается на Flux) есть перегрузки с интересными аргументами, а именно maxConcurrency

maxConcurrency очень важен, по дефолту значение стоит Integer.MAX_VALUE (определяет сколько неблокирующих задач может выполняться одновременно на одной рельсе. Понимаете теперь откуда аппетит у реактора?

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

Количество запусков цепочки напрямую влияет на количество одновременно выполняемых задач.

Подведем небольшой итог:

  • parallel (parallelism)

  • flatMap (maxConcurrency)

  • Количество запусков цепочки

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

По дефолту это Кол-во ядер * Integer.MAX_VALUE * Количество запусков цепочки

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

        val result = nonBlockingMethod1sec("Hello world")            .flatMap { _ ->                collectTasks().toFlux()                    .parallel(1)                    .runOn(pool, 1)                    .flatMap({                        Mono.deferContextual { _ ->                            doSomethingNonBlocking(it.toString())                        }                    }, false, 1, 1)                    .sequential()                    .collectList()            }            .block()!!

Стоп, или не всё?

Thread Pool

Зачем же нужен пул потоков в реакторе? Думайте о нем как о двигателе для Вашего автомобиля. Чем пул мощнее - тем блокирующие задачи будут разбираться быстрее, а если потоков мало, то и блокирующие задачи задержатся у вас надолго! А куда же мы без блокирующих вызовов? На количество одновременно выполняемых задач в реакторе он не влияет, вот это поворот :)

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

Распределение задач по рельсам

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

Зеленые прямоугольники это наши задачи, которые распределяются в реакторе по алгоритму round-robin, что в случае с синтетическими данными дает красивую картинку.

Хорошо загруженный реактор (задачи равномерно распределены). 54 блокирующих задачи (каждая по 1сек), round-robin распределение по 6 рельсамХорошо загруженный реактор (задачи равномерно распределены). 54 блокирующих задачи (каждая по 1сек), round-robin распределение по 6 рельсам

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

Плохо загруженный пул (задачи распределены не равномерно)54 блокирующих задачи (каждая по 1сек кроме 2ух), round-robin распределение по 6 рельсамПлохо загруженный пул (задачи распределены не равномерно)54 блокирующих задачи (каждая по 1сек кроме 2ух), round-robin распределение по 6 рельсам

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

Бороться с этим можно несколькими способами

  • concatMap вместо flatMap (посмотрите в профилировщик на ваш пул, передумаете)

  • правильно планировать задачи, чтобы исключить аномалии (почти невозможно)

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

  • prefetch (наш выбор!)

Параметр prefetch у flatMap & runOn позволяет определить, сколько задач будет взято на одну рельсу на старте, а затем при достижении некоторого порога выполнения задач, реквесты будут повторяться с этим количеством. Значение по умолчанию - 256. Сменив значение на 1, можно заставить реактор использовать механизм "work stealing", при котором, рельсы и потоки, которые освободились, будут забирать задачи себе на выполнение и картина получится гораздо более приятная.

Хорошо загруженный пул (задачи равномерно распределены)54 блокирующих задачи (каждая по 1сек кроме 2ух), round-robin распределение по 6 рельсамPrefetch !Хорошо загруженный пул (задачи равномерно распределены)54 блокирующих задачи (каждая по 1сек кроме 2ух), round-robin распределение по 6 рельсамPrefetch !

На этом у меня всё. Будет интересно прочесть Ваши замечания и комментарии, на 100% истину не претендую, но все результаты подкреплены практическими примерами, на Spring Boot + Project Reactor 3.4. Всем спасибо!

Подробнее..
Категории: Kotlin , Java , Concurrency , Parallel , Reactor , Parallelism , Mono , Threads , Flux , Pool , Prefetch

JetBrains Academy платформенные обновления, любимые проекты пользователей и годовая подписка

17.06.2021 14:15:17 | Автор: admin

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

Cо времен нашего последнего поста в JetBrains Academy изменилось довольно много: мы выпустили важные обновления платформы, опубликовали множество новых проектов и тем, а также ввели годовую подписку! Кроме того, мы проанализировали наши проекты и составили рейтинг лучших из них по мнению наших пользователей. Ниже мы расскажем обо всем этом подробнее.

Если вы еще не зарегистрировались на JetBrains Academy, но уже изучаете или планируете изучать программирование, начните бесплатную пробную неделю прямо сейчас! Выполнив первую стадию проекта в течение недели, вы дополнительно получите месяц бесплатного доступа к JetBrains Academy. А закончив свой первый проект в течение этого времени, вы продлите бесплатный доступ еще на один месяц. Таким образом, помимо пробной недели, у вас есть возможность получить до двух месяцев бесплатного обучения, в течение которых вы сможете создавать реальные приложения, способные усилить ваш GitHub-профиль и резюме.

Платформенные обновления

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

  • Появилось два подготовительных трека для пользователей, только начинающих свое знакомство с программированием: Python for Beginners и Preparing for the AP Computer Science. Первый поможет сделать первые шаги в изучении Python, второй в изучении Java. После их прохождения вам будет легче перейти на треки Python Developer и Java Developer. Обратите внимание, что трек Preparing for the AP Computer Science находится на ранней стадии разработки и доступен бесплатно.

  • Мы создали два новых трека для более подробного изучения отдельных концепций Java и Python. Java Desktop Application Developer подойдет для совершенствования навыков написания десктопных приложений на Java. Natural Language Processing научит работе с текстовыми данными с помощью Python. Обратите внимание, что треки находятся на ранней стадии разработки и доступны бесплатно.

  • Мы улучшили технологию интервального повторения для эффективного усвоения пройденного материала. Она заключается в повторении учебного материала по определенным постоянно возрастающим интервалам.

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

    Аналитика обучения пользователя JetBrains Academy Аналитика обучения пользователя JetBrains Academy
  • Появились персональные бейджи виртуальные награды за успешное обучение и помощь пользователям. Среди них: Committed Learner, Brilliant Mind, Helping Hand, Sweetheart и многие другие.

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

    Карта знаний. Раздел Computer science > Programming languages > Java > Code organization > Code styleКарта знаний. Раздел Computer science > Programming languages > Java > Code organization > Code style
  • Для ознакомления с JetBrains Academy теперь предлагается экскурсия, разъясняющая новым пользователями термины и концепции, которые важно знать при обучении на платформе.

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

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

    Возможные действия при работе с комментариями пользователей JetBrains AcademyВозможные действия при работе с комментариями пользователей JetBrains Academy

Любимые проекты пользователей

Со времен нашего прошлого поста мы добавили более 50 новых проектов, 300 тем и получили более 280 тысяч комментариев пользователей! Мы проанализировали любимые проекты наших пользователей и составили из них рейтинг тех, которые оказались наиболее полезными, увлекательными и простыми в понимании среди более чем 110 других проектов.

Java Developer

  1. Проект Simple Banking System. Работая над упрощенной версией банковской системы, вы изучите основы SQL и познакомитесь с алгоритмом Луна, помогающим избежать ошибок при вводе номера банковской карты.

    Что отмечают пользователи: практика ООП и структурирования кода, знакомство с базами данных через JDBC.

  2. Проект Tic-Tac-Toe with AI. Напишите собственную игру в крестики-нолики и сыграйте в нее с компьютером! Работая над этим проектом, вы узнаете о планировании и разработке сложных программ с нуля, научитесь использовать классы, объекты и методы в Java, обрабатывать ошибки и осуществлять ввод данных.

    Что отмечают пользователи: интересное применение рекурсии, углубленное ООП, знакомство с алгоритмом Минимакс.

  3. Проект Maze Runner. Предлагаем вам написать программу по созданию лабиринтов и нахождению выходов из них! Вы научитесь работать с классом Random и многомерными массивами, а также сохранять результат работы программы в файл.

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

Python Developer

  1. Проект Simple Banking System. Работая над упрощенной версией банковской системы, вы изучите основы SQL и познакомитесь с алгоритмом Луна, помогающим избежать ошибок при вводе номера банковской карты.

    Что отмечают пользователи: практика ООП и структурирования кода, работа с базами данных через SQLite.

  2. Проект Rock-Paper-Scissors.Предлагаем вам создать усовершенствованную версию игры камень-ножницы-бумага камень-ножницы-бумага-ящерица-спок а затем сыграть в нее против компьютера! Это хорошая тренировка навыков написания алгоритмов, работы с массивами, модулем random и форматированием строк.

    Что отмечают пользователи: работа с файлами и коллекциями.

  3. Проект To-Do List.Создайте свой список дел для управления повседневными задачами! Создавая эту программу, вы потренируетесь работать с циклами и условными операторами, а также изучите основы SQLAlchemy.

    Что отмечают пользователи: знакомство с базами данных через SQLAlchemy.

Kotlin Developer

  1. Проект Parking Lot. В рамках этого проекта предлагаем вам создать собственную программу по управлению парковкой, которая будет сообщать о количестве свободных и занятых мест. Вы узнаете и закрепите основы программирования на Kotlin: ввод и вывод консоли, циклы и условные операторы. Также вы научитесь использовать списки и применять к ним различные операции.

    Что отмечают пользователи: практика ООП, особенно классов данных.

  2. Проект Tic-Tac-Toe. Напишите собственную игру в крестики-нолики и сыграйте в нее с компьютером! Работая над этим проектом, вы узнаете о планировании и разработке сложных программ с нуля, научитесь использовать классы, объекты и методы в Java, обрабатывать ошибки и осуществлять ввод данных.

    Что отмечают пользователи: отличная проработка основ Kotlin.

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

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

Годовая подписка

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

В декабре мы объявили о введении годовой подписки на JetBrains Academy. Ее стоимость равна пяти месяцам ежемесячной подписки! Теперь, в зависимости от целей и сроков обучения, вы можете воспользоваться наиболее удобной для вас формой подписки. Оформить ее можно на странице подписки вашего профиля на JetBrains Academy или на нашем сайте.

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

Мы всегда рады вашим комментариям здесь, в Twitter, Facebook и на Reddit! Они помогают нам развивать функциональность JetBrains Academy и делать проекты более качественными, полезными и увлекательными.

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

Ваша команда JetBrains Academy

Подробнее..

Как отслеживать использование памяти CPU и загруженность диска в Java

04.06.2021 12:23:32 | Автор: admin

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

Java инструменты наблюдают за конструкциями и процессами байткода Java. Профайлеры Java следят за всеми системными командами и использованием процессора. Это позволяет вам вызвать средства в любой момент.

Стандартный профайлер предоставляет большой объем знаний, ценность которых зависит от назначения отладки. Некоторые инструменты лучше для объединения логов для всех приложений и серверов, таких как Stackify Retrace, настраивая и отслеживая автоматический лог запросов. Инструменты: Stackifys Application Performance Management, Retrace - предлагают Java пользователям лучшее понимание устройства приложения при помощи внедренного логирования и профилирования кода. Вместе с внедренным централизованным и структурированным логированием, можно получить доступ ко всем логам приложения из единой точки для всех приложений и серверов.

Здесь мы перечислили способы демонстрации памяти и отслеживания CPU в Java для оптимизации кода.

5 способов для наблюдения за памятью CPU и загруженностью диска в Java!

  1. Базовые Linux команды для отслеживания использования памяти CPU и загруженности диска

    a) free m

    Команда передает доступную и занятую память вашей виртуальной машины.

    b) top

    Эта команда для просмотра использования CPU и памяти.

    Когда вы введете 1 на клавиатуре, тогда top сработает и отобразит все возможные CPU и использование каждого.

    Всего 4 CPU - CPU0, CPU1, CPU2, CPU3 - и вся их статистика использования.

    c) meminfo и cpuinfo

    Используйте следующие команды для детальной информации относительно памяти и процессоров виртуальной машины.

    $cat /proc/cpuinfo

    $cat /proc/meminfo

    d) Память CPU и использование диска конкретного процесса.

    $ps -p [pid] -o %cpu,%mem,cmd

    Эта команда для отслеживания использования CPU и загруженности памяти методом приложения Java. Также отображает какие команды запустили процесс.

    e) Это статистика потока Java метода.

    $ps uH p [pid]

    Эта команда предоставляет число потоков в процессе и их статистику.

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

    Когда приложение не отвечает, эти команды могут использоваться для выполнения предыдущих запросов, прежде чем переходить к памяти и дырам в потоках. Это важные команды для первоначального исследования кроме статистики памяти и CPU.

    df k > Для проверки места на диске

    lsof -i :<port> > Для проверки, является ли данный порт открытым

    lsof i: tcp > Все установленные и слушающие TCP подключения

    netstat -plunt > Подключение к сети

  2. Отслеживание использования памяти

    Далее рассмотрим класс ManagementFactory для отслеживания использования памяти. Это реализовано путем запроса к MemoryMXBean. Раздел памяти куча необходим для использования MemoryMXBean:

    MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();System.out.println(String.format(Initial memory: %.2f GB,   (double)memoryMXBean.getHeapMemoryUsage().getInit() /1073741824));System.out.println(String.format(Used heap memory: %.2f GB,   (double)memoryMXBean.getHeapMemoryUsage().getUsed() /1073741824));System.out.println(String.format(Max heap memory: %.2f GB,   (double)memoryMXBean.getHeapMemoryUsage().getMax() /1073741824));System.out.println(String.format(Committed memory: %.2f GB,   (double)memoryMXBean.getHeapMemoryUsage().getCommitted() /1073741824));
    

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

    Исходная: исходный концепт Java приложений из ОС в течение запуска

    Используемая: память, используемая Java

    Максимальная: большая часть памяти относится к JVM. OutOfMemoryException возникает, если память заполнена

    Переданная: объем памяти доступный JVM

  3. Нативный агент профилирования (-Agentpath)

    Некоторые профилируемые java инструменты построены на нативных библиотеках для Windows, Linux или masOS. -Agentpath - это виртуальная машина которая используется для настройки библиотеки инструментов.

    Например:

    java agentpath:/usr/profiler/lib/[agent].so -jar application.jar

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

    Инструменты такие как JProfiler и Yourkit используют нативные инструменты профилирования.

  4. Java агент профилирования (-Javaagent)

    Агенты профилирования Java оснащены возможностью использования -javaagent виртуальной машины, как альтернатива. Вы можете создать легковесную программу в Java следуя следующим шагам:

    a) Оснащение программы требует разработки premain() системы

    b) Класс программы должен быть спроектирован как Pre-Main в MANIFEST.MF записи в данных JAR, где программа и ее зависимые классы создаются

    c) JVM начинается с javaagen

    java -javaagent:agent.jar -jar application.jar

    Класс агент premain() процесса выполняется до main() процесса использования, -javaagent представляет -jar.

    Если инструменты системы ожидают агент, для расположения после main() системы приложения, следующий шаг получить agentmain() метод в другом агент классе. Это следует сконструировать как Agent-Main класс в MANIFEST.MF записи.

  5. Агент библиотека (-Agentlib)

    -Agentlib - это виртуальная машина, использующаяся для упорядочения локальной библиотеки через JVM проект. Две главные причины по которым следуют использовать:

    1) Загрузка HPROF

    2) Для контроля JVM в режиме отладки

    1) Использование HPROF:

    HPROF профилируют кучу и CPU, которые передаются рядом с Java. Это делает язык эффективным. agentlib:hprof и Xrunhprof обычно используются как параметры виртуальной машины с HPROF.

    JVM отслеживает данные относящиеся к профилям кучи, обозначенных концептов, трассировка стека в java.hprof.txt.

    java agentlib:hprof=heap=sites Hello.java

    HPROF может быть покрыт во время сборки, так разработчик может выявить серьезные проблемы.

    javac -J-agentlib:hprof=heap=sites Hello.java

    2) Запуск приложения в режиме отладки

    Запуск приложения в режиме отладки подтверждается частью agentlib:jdwp виртуальной машины. Это решение размещает внутрипроцессные архивы отладки JVM

    java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:9001, server=y,suspend=y jar application.jar

    Этот параметр поддерживает отладку в приложении, использующим IDE.

    Простые шаги

    Используйте команды ограниченно/удаленно для метода отладки в Java. Приложение, которое отслеживает записи на указанном порту не переместится дальше пока шаг (ii) не завершится.

    Скриншот записи из приложения: Monitoring for transport dt_socket at address: 9001

    Инициализируйте проект в IDE в режиме отладки и присоедините шифрование к хосту и порту.

    Отлаживайте код на единственном сервере с IDE.

Подробнее..
Категории: Программирование , Java

Перевод Java 15 и IntelliJ IDEA

07.06.2021 16:20:09 | Автор: admin

В Java 15 появились sealed-классы и sealed-интерфейсы, с помощью которых стало возможным ограничивать иерархию классов и интерфейсов на уровне синтаксиса языка. Теперь возможные иерархии определяются декларативно. Этот функционал пока представлен в режиме превью (preview).

Также в Java 15 есть изменения в записях (Records), появившихся в Java 14. А сопоставление с образцом (pattern matching) для instanceof вошло в Java 15 как второе превью без изменений. Текстовые блоки (text block) из Java 13 включены в Java 15 как стандартная языковая конструкция. Изменений в них по сравнению с Java 14 нет.

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

Sealed-классы и интерфейсы

Определяя класс как sealed, вы можете явно указать, каким классам разрешено его расширять. Это, с одной стороны, позволяет использовать класс повторно через наследование, а с другой ограничить допустимых наследников. Но зачем вам ограничивать иерархии наследования?

Необходимость ограниченных иерархий

Представьте, что вы разрабатываете приложение для садовников. Садовнику, в зависимости от вида растения, требуется выполнять различные действия. Давайте смоделируем иерархию растений следующим образом (я намеренно не привожу полный текст классов):

class Plant {}class Herb extends Plant {}class Shrub extends Plant {}class Climber extends Plant{}class Cucumber extends Climber {}

Ниже приведен пример того, как класс Gardener (садовник) может использовать эту иерархию классов:

public class Gardener {   int process(Plant plant) {       if (plant instanceof Cucumber) {           return harvestCucumber();       } else if (plant instanceof Climber) {           return sowClimber();       } else if (plant instanceof Herb) {           return sellHerb();       } else if (plant instanceof Shrub) {           return pruneShrub();       } else {           System.out.println("Unreachable CODE. Unknown Plant type");           return 0;       }   }   private int pruneShrub() { .. }   private int sellHerb() { .. }   private int sowClimber() { .. }   private int harvestCucumber() { .. }}

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

Определение защищенных иерархий с помощью sealed-классов

Объявить sealed-класс можно с помощью модификатора sealed. Для указания классов, которые могут его расширять напрямую, используется ключевое слово permits. Подклассы могут быть final, non-sealed или sealed.

Gif'ка ниже показывает, как изменить объявление обычного класса на sealed-класс и модифицировать его наследников:

Вот измененный код:

sealed public class Plant permits Herb, Shrub, Climber {}final class Herb extends Plant {}non-sealed class Shrub extends Plant {}sealed class Climber extends Plant permits Cucumber{}final class Cucumber extends Climber {}

Позволяя расширять класс только определенному перечню классов, вы можете отделить доступность (accessibility) от расширяемости (extensibility). Можно сделать sealed-класс доступным для других пакетов и модулей и контролировать, кто может его расширять. В прошлом, чтобы предотвратить расширение классов, разработчики создавали package-private классы. Однако это также ограничивало к ним доступ. Для sealed-классов это уже не так.

Перечень permitted-подклассов доступен через рефлексию (reflection) с помощью метода Class.permittedSubclasses(). Можно получить всю sealed-иерархию в рантайме.

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

Конфигурация IntelliJ IDEA

Возможности Java 15 поддерживаются в IntelliJ IDEA с версии 2020.2, выпущенной в июле 2020 года. Для настройки использования Java 15 выберите в свойствах проекта и модулей в параметре "Project SDK" значение "15", а в "Project language level" "15 (Preview) Sealed types, records, patterns, local enums and interfaces".

Также вы можете скачать Java 15 непосредственно из IntelliJ IDEA. Для этого в левой части окна "Project Structure" в разделе "Platform Settings" выберите "SDKs", затем нажмите вверху значок "+" и выберите "Download JDK". Укажите поставщика (Vendor), версию (Version) и каталог для загрузки JDK.

Возвращаемся к обработке подтипов Plant в классе Gardener

При создании sealed-иерархии вы знаете полный список наследников и вам не нужно обрабатывать какие-то общие случаи. Ветка else в методе process() класса Gardener никогда не будет выполнена. Однако нам все-равно нужно оставить else из-за return.

Сопоставление с образцом, добавленное в Java 14 для instanceof, может появиться в будущих версиях Java и в выражениях switch. С помощью улучшенного switch можно работать с полным списком наследников. Это позволит исключить написание любого "обобщенного кода" для обработки ситуаций, когда передается непредусмотренный подтип Plant:

// Этот код не работает в Java 15.// Он будет работать в будущих версиях Java // после реализации type-test-pattern в switch int processInAFutureJavaVersion(Plant plant) {   return switch (plant) {       case Cucumber c -> c.harvestCucumber();       case Climber cl -> cl.sowClimber();       case Herb h -> h.sellHerb();       case Shrub s -> s.pruneShrub();   }}

Пакеты и модули

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

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

Правила для базовых классов и наследников классов

Классы, расширяющие sealed-класс, должны быть объявлены как final, non-sealed или sealed. Модификатор final запрещает дальнейшее расширение, non-sealed позволяет другим классам расширять его, а sealed-подкласс должен следовать тем же правилам, что и родительский базовый класс необходимо явно указать список классов, которые могут его расширять.

Sealed-класс также может быть абстрактным. Его наследники могут быть как абстрактными, так и конкретными классами.

Давайте изменим набор классов, используемый в предыдущем разделе, и определим класс Plant как абстрактный с абстрактным методом grow(). Поскольку производный класс Herb является final-классом, то в нем должна быть реализация метода grow(). Non-sealed класс Shrub объявлен абстрактным и может не реализовывать метод grow(). Sealed-класс Climber реализует абстрактный метод grow():

Вот измененный код:

sealed abstract public class Plant permits Herb, Shrub, Climber {   abstract void grow();}final class Herb extends Plant {   @Override   void grow() {   }}non-sealed abstract class Shrub extends Plant {}sealed class Climber extends Plant permits Cucumber{   @Override   void grow() {   }}final class Cucumber extends Climber {}

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

Sealed-интерфейсы

Sealed-интерфейс позволяет явно указать интерфейсы, которые могут его расширять, и классы (включая записи), которые могут его реализовать. Для интерфейсов применяются правила, аналогичные sealed-классам.

Однако поскольку вы не можете объявить интерфейс с помощью модификатора final (иначе это противоречило бы его назначению, так как интерфейсы должны быть реализованы) интерфейс может быть объявлен только с использованием модификаторов sealed или non-sealed. В разделе permits перечисляются классы, которые непосредственно могут реализовать sealed-интерфейс, и интерфейсы, которые могут его расширять. Реализующий класс может быть final, sealed или non-sealed. Поскольку записи, появившиеся в Java 14, неявно являются final, то они не нуждаются в каких-либо дополнительных модификаторах:

sealed public interface Move permits Athlete, Person, Jump, Kick {}final class Athlete implements Move {}record Person(String name, int age) implements Move {}non-sealed interface Jump extends Move {}sealed interface Kick extends Move permits Karate {}final class Karate implements Kick {}

Давайте перейдем к следующему нововведению Java 15 локальные записи (record).

Записи (records)

Записи (records) предназначены для компактной записи объектов-значений (value object). Первое превью записей появилось в Java 14, а в Java 15 второе превью с некоторыми изменениями.

Если вы не знакомы с записями или хотите узнать об их поддержке в IntelliJ IDEA, то обратитесь к статье Java 14 и IntelliJ IDEA. В IntelliJ IDEA есть множество функций, которые помогут вам создавать и использовать записи.

В этом посте я расскажу об изменениях в Java 15 по сравнению с Java 14.

Java 15 позволяет определять локальные записи внутри метода. В следующем примере метод getTopPerformingStocks() ищет акции (stock), которые имеют наибольшую стоимость на указанную дату, и возвращает их названия.

List<String> getTopPerformingStocks(List<Stock> allStocks, LocalDate date) {   // TopStock - локальная запись (Record)   record TopStock(Stock stock, double stockValue) {}   return allStocks.stream()              .map(s -> new TopStock(s, getStockValue(s, date)))              .sorted((s1, s2) -> Double.compare(s1.stockValue(), s2.stockValue()))              .limit(2)              .map(s -> s.stock.getName())              .collect(Collectors.toList());}

Локальные интерфейсы и перечисления

Java 15 также позволяет объявлять локальные перечисления и интерфейсы. Внутри метода можно инкапсулировать локальные для него данные или бизнес-логику.

public void createLocalInterface() {   interface LocalInterface {       void aMethod();   }   // Код, использующий LocalInterface}public void createLocalEnum() {   enum Color {RED, YELLOW, BLUE}   // Код, использующий enum Color}

Однако в этих случаях нельзя использовать контекстные переменные. Например, для создания значений перечисления FOO и BAR нельзя использовать параметры метода:

void test(int input) {   enum Data {       FOO(input), BAR(input*2); // Ошибка. Нельзя обращаться к input       private final int i;       Data(int i) {           this.i = i;       }   }}

Сопоставление с образцом (pattern matching) для instanceof

Многие Java-разработчики используют оператор instanceof для сравнения типов. Если результат сравнения будет true, то далее следует явное приведение к типу, с которым сравнивали. Этот паттерн применяется довольно часто и выглядит следующим образом: сравнение - ifTrue - приведениеКТипу.

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

Это уже второе превью сопоставления с образцом для instanceof (изменений по сравнению с Java 14 нет).

Подробнее узнать о поддержке этой функциональности в IntelliJ IDEA вы можете в статье Java 14 и IntelliJ IDEA. Там также есть несколько интересных примеров того рефакторинга кода с помощью этой возможности и других инспекций из IntelliJ IDEA, таких как объединение вложенных if и извлечение или инлайнинг переменных.

Текстовые блоки

Многострочные строки и текстовые блоки были добавлены в Java 15 как стандартная языковая конструкция без каких-либо изменений по сравнению с Java 14.

Подробнее о поддержке текстовых блоков в IntelliJ IDEA вы можете узнать в статье Java 14 и IntelliJ IDEA.

Превью

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

Превью версии являются полноценными, но могут измениться, что, по сути, означает готовность функциональности к использованию разработчиками, но ее детали могут поменяться в будущих релизах Java в зависимости от отзывов. В отличие от API, языковые конструкции не могут в будущем быть объявлены устаревшими (deprecated). Если вы хотите высказать свое мнение о каких-либо превью возможностях, то можете сделать это в списке рассылки JDK (требуется бесплатная регистрация).

По указанной выше причине IntelliJ IDEA поддерживает превью возможности Java только для текущего JDK. Реализация превью возможностей может измениться от версии к версии, пока они не будут удалены или добавлены в качестве стандарта. Код, использующий превью возможности из более ранней версии Java SE, может не компилироваться или не запускаться в новой версии. Например, Switch Expressions в Java 12 использовали break для возврата значения из ветки, а позже это было изменено на yield. Поддержка использования break для возврата значения из Switch Expressions уже отсутствует в IntelliJ IDEA.

Резюме

IntelliJ IDEA стремится не только к поддержке новых функций Java, но и к разработке для них новых проверок и инспекций.

IntelliJ IDEA 2020.2 поддерживает все новые языковые конструкции Java 15. Попробуйте уже сегодня sealed-классы и интерфейсы, а также записи (record), сопоставление с образцом (pattern matching) для instanceof и текстовые блоки (text block). Скачать IntelliJ IDEA вы можете по этой ссылке.

Мы всегда рады обратной связи от наших пользователей. Не забудьте оставить отзыв о поддержке этих возможностей в IntelliJ IDEA.


А прямо сейчас в OTUS открыт набор на новый поток курса Java Developer. Professional. Приглашаю всех желающих на demo day курса, в рамках которого можно будет подробно ознакомиться с программой и процессом обучения, а также задать вопросы экспертам OTUS.

Подробнее..

Перевод Как использовать Python для проверки протокола Signal

08.06.2021 18:13:14 | Автор: admin

Galois работает над повышением удобства SAW, инструмента для верификации программ наCиJava, исходный код кторого открыт. Основным способом взаимодействия пользователей сSAW является его спецификация иязык программирования сценариев. Чтобы сделать SAW как можно более доступным, вкачестве языка программирования SAW теперь можно использовать Python! Для демонстрации этой новой возможности в Galoisсоздали пример, выполнив проверку части реализации протокола Signal наязыкеС.В частности, как спецификация SAW определяются условия, при которых сообщение протокола Signal будет успешно аутентифицировано. К старту курса о Fullstack-разработке на Python мы перевели материал об этом примере.


SAW-клиент Python

Управление SAW может осуществляться средствами Python через библиотеку saw-client вPyPI. Реализация спомощью Python непредставляет сложности управление SAW осуществляется через JSON-RPC API, как показано впредыдущей статье. Библиотека saw-client постоянно развивалась, итеперь вней реализован высокоуровневый интерфейс, отвечающий зареализацию функций RPC.

Помимо Python, вSAW также используется альтернативный язык программирования сценариев, называемый SAWScript. Хотя наSAWScript возможно писать теже проверки, что иPython, этотязыкнелишён недостатков. SAWScript специализированный язык, поэтому ондовольно сложен для понимания теми, кто впервые берётся заизучениеSAW. Кроме того, вSAWScript практически отсутствует возможность подключения внешних библиотек. Если вызахотите написать наSAWScript то, чего нет встандартной библиотеке, вам придётся реализовать нужную функцию самостоятельно.

Сдругой стороны, Python широко используемый язык, изначально хорошо знакомый гораздо большему числу людей. УPython также имеется богатый набор библиотек ивспомогательных программ, доступных вкаталоге PyPI. Даже если Python невходит вчисло ваших любимых языков программирования, мывсё равно советуем попробовать saw-client. Если под рукой неокажется ничего другого, код, написанный вsaw-client, может послужить источником вдохновения для реализации аналогичного клиента надругом языке.

Базовая спецификация вsaw-client

Давайте рассмотрим, как saw-client можно использовать для создания спецификаций реального кода наязыкеC. Для примера возьмём libsignal-protocol-c. Эта библиотека представляет собой реализованный наязыке Cпротокол Signal, криптографический протокол, используемый для шифрования мгновенных сообщений, голосовых ивидеозвонков. Этот протокол применяется вприложении Signal Messenger, получившем название попротоколу, нотакже поддерживается вдругих приложениях, таких как WhatsApp, Facebook Messenger иSkype.

Общее описание возможностей SAW сиспользованием библиотеки libsignal-protocol-c приведено вразделе "Планы".

Для начала рассмотрим базовую структуру данных, используемую библиотекой libsignal-protocol-c, аименно signal_buffer:

struct signal_buffer {    size_t len;    uint8_t data[];};

signal_buffer представляет собой байтовый массив (массив данных) сдлинойlen. При отправке сообщения спомощью libsignal-protocol-c основным компонентом сообщения является signal_buffer.

Чтобы быть уверенным, что libsignal-protocol-c работает так, как заявлено, нужно убедиться, что содержимое signal_buffer сообщения соответствует ожидаемому. Библиотека проверяет соответствие двух буферов signal_buffer спомощью функции signal_constant_memcmp:

int signal_constant_memcmp(const void *s1, const void *s2, size_t n){    size_t i;    const unsigned char *c1 = (const unsigned char *) s1;    const unsigned char *c2 = (const unsigned char *) s2;    unsigned char result = 0;    for (i = 0; i < n; i++) {        result |= c1[i] ^ c2[i];    }    return result;}

Интуитивно понятно, что утилита signal_constant_memcmp должна проверить, одинаковоли содержимое двух байтовых массивов signal_buffer. Если они одинаковы, функция вернёт значение0. Если содержимое несовпадает, возвращается значение, указывающее набайты, вкоторых массивы отличаются.

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

from saw_client.llvm import *class ConstantMemcmpEqualSpec(Contract):    def specification(self) -> None:        _1        self.execute_func(_2)        _3

Класс Contract определяет спецификации SAW сиспользованием метода specification. Чтобы создать собственную спецификацию, достаточно создать подкласс Contract ипереопределить метод specification. Каждая спецификация состоит изтрёх частей:

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

  • Аргументы для передачи впроверяемую функцию (_2).

  • Постусловия (_3), определяющие характер проверки после вызова верифицируемой функции.

Учитывая требования кспецификации, проверим, как утилита signal_constant_memcmp работает в пределах спецификации SAW:

class ConstantMemcmpEqualSpec(Contract):    n: int    def __init__(self, n: int):        super().__init__()        self.n = n    def specification(self) -> None:        s1  = self.fresh_var(array_ty(self.n, i8), "s1")        s1p = self.alloc(array_ty(self.n, i8), points_to = s1)        s2  = self.fresh_var(array_ty(self.n, i8), "s2")        s2p = self.alloc(array_ty(self.n, i8), points_to = s2)        self.precondition(cryptol(f"{s1.name()} == {s2.name()}"))        self.execute_func(s1p, s2p, cryptol(f"{self.n} : [64]"))        self.returns(cryptol("0 : [32]"))

Предварительными условиями являются наличие двух байтовых массивов (s1p иs2p), содержимое которых s1 иs2одинаково. Вчастности, одинаковость содержимого гарантирует вызов self.precondition(...). Аргумент self.precondition(...) записывается наCryptol, предметно-ориентированном языке программирования (DSL), используемом вкриптографии. Приведённое выражение наCryptol довольно простое, так как выполняет только проверку равенства, нониже мыувидим более сложные примеры наCryptol.

Аргументами функции являются два байтовых массива суказанием ихдлин (self.n), преобразуемых вначале ввыражение Cryptol, чтобы SAW мог получить оних представление. Порстусловие, снова ввиде выражения наCryptol, заключается втом, что функция возвращает значение 0.

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

mod = llvm_load_module("libsignal-protocol-c.bc") # An LLVM bitcode filearray_len = 42 # Pick whichever length you want to checkllvm_verify(mod, "signal_constant_memcmp", ConstantMemcmpEqualSpec(array_len))

Если проверка пройдёт нормально, можно запустить этот код наPython иувидеть следующий результат:

Verified: lemma_ConstantMemcmpEqualSpec (defined at signal_protocol.py:122)

Ура! Инструмент SAW проверил правильность работы утилиты signal_constant_memcmp. Важно отметить, что нам ненужно было даже упоминать обитовых манипуляциях внутри функции SAW выполнил ихавтоматически. Отметим, однако, что команда ConstantMemcmpEqualSpec определяет происходящее только втом случае, если байтовые массивы равны друг другу. Еслибы мыхотели охарактеризовать происходящее вслучае неравенства байтовых массивов, потребоваласьбы несколько более сложная спецификация.

Также следует отметить, что вприведённом выше коде встречаются повторения, так как мыдважды вызываем функцию self.fresh_var(), азатем self.alloc(). Ксчастью, Python избавляет оттаких проблем:

def ptr_to_fresh(spec: Contract, ty: LLVMType,                 name: str) -> Tuple[FreshVar, SetupVal]:    var = spec.fresh_var(ty, name)    ptr = spec.alloc(ty, points_to = var)    return (var, ptr)class ConstantMemcmpEqualSpec(Contract):    ...    def specification(self) -> None:        (s1, s1p) = ptr_to_fresh(self, array_ty(self.n, i8), "s1")        (s2, s2p) = ptr_to_fresh(self, array_ty(self.n, i8), "s2")        ...

Верификация кода сиспользованием HMAC

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

Одним изосновных этапов шифрования сообщения является присоединение кода аутентификации сообщения (MAC), который можно использовать для проверки того, что после отправки сообщения его содержимое неменялось. Вчастности, libsignal-protocol-c использует HMAC, тип MAC, вычисляемый спомощью криптографической хеш-функции.

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

hmac_init : {n} [n][8] -> HMACContexthmac_init = undefinedhmac_update : {n} [n][8] -> HMACContext -> HMACContexthmac_update = undefinedhmac_final : HMACContext -> [SIGNAL_MESSAGE_MAC_LENGTH][8]hmac_final = undefined

Это будут неинтерпретируемые функции, используемые для создания кода, связанного сHMAC, вбиблиотеке libsignal-protocol-c. Основная идея заключается втом, что, получив навходе криптографический ключ, hmac_init создаст HMACContext. HMACContext будет многократно обновляться через hmac_update, используя данные первого аргумента. Затем hmac_final преобразует HMACContext вsignal_buffer достаточной длины для хранения MAC.

Определение HMACContext зависит оттого, какая криптографическая хэш-функция используется всочетании сHMAC. Параметры библиотеки libsignal-protocol-c настроены для используемых еюхеш-функций, поэтому можно свободно подключать библиотеки OpenSSL, Common Crypto или другую подходящую библиотеку.

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

После определения этих функций можно связать ихссоответствующими функциями Cвсамой библиотеке. Например, вот сокращённая спецификация для функции signal_hmac_sha256_initC:

class SignalHmacSha256InitSpec(Contract):    key_len: int    def specification(self) -> None:        hmac_context_ptr = self.alloc(...)        (key_data, key)  = ptr_to_fresh(self, array_ty(self.key_len, i8),                                        "key_data")            self.execute_func(..., hmac_context_ptr, key,                          cryptol(f"{self.key_len} : [64]"))        init = f"hmac_init`{{ {self.key_len} }} {key_data.name()}"        dummy_hmac_context = self.alloc(..., points_to = cryptol(init))        self.points_to(hmac_context_ptr, dummy_hmac_context)        self.returns(cryptol("0 : [32]"))key_len = 32init_spec = llvm_assume(mod, "signal_hmac_sha256_init",                        SignalHmacSha256InitSpec(key_len))

Нестарайтесь понять каждую строчку кода. Просто знайте, что самой важной его частью является последняя строка, вкоторой вместо llvm_verify используется llvm_assume. Функция llvm_assume позволяет SAW использовать спецификацию, фактически немоделируя её посути SAW трактует еёкак аксиому. Это позволяет привязать поведение signal_hmac_sha256_init кнеинтерпретируемой функции hmac_init впостусловиях спецификации.

Аналогичным образом llvm_assume также можно использовать для создания спецификаций, включающих hmac_update иhmac_final. После этого можно проверить очень важную функцию, связанную сMAC: signal_message_verify_mac. Фактически данная функция принимает сообщение вкачестве аргумента, вычисляет MAC для данных внутри сообщения ипроверяет, совпадаетли онсMAC вконце сообщения. Если значения совпадают, можно суверенностью утверждать, что при отправке получателю сообщение неменялось.

Объяснение всех тонкостей работы signal_message_verify_mac занялобы довольно много времени, поэтому вэтой заметке мыкоснёмся лишь главного вопроса: как должно выглядеть содержимое сообщения? Данные внутри сообщения могут быть произвольными, однако MAC вконце должен иметь вполне определённую форму. Эту форму можно определить спомощью функции Python:

def mk_hmac(serialized_len: int, serialized_data: FreshVar,        receiver_identity_key_data : FreshVar,        sender_identity_key_data: FreshVar,        mac_key_len: int, mac_key_data: FreshVar) -> SetupVal:    sender_identity_buf = f"""        [{DJB_TYPE}] # {sender_identity_key_data.name()}            : [{DJB_KEY_LEN} + 1][8]        """    receiver_identity_buf = f"""        [{DJB_TYPE}] # {receiver_identity_key_data.name()}            : [{DJB_KEY_LEN} + 1][8]        """    hmac = f"""        hmac_final         (hmac_update`{{ {serialized_len} }} {serialized_data.name()}          (hmac_update`{{ {DJB_KEY_LEN}+1 }} ({receiver_identity_buf})           (hmac_update`{{ {DJB_KEY_LEN}+1 }} ({sender_identity_buf})            (hmac_init`{{ {mac_key_len} }} {mac_key_data.name()}))))        """    return cryptol(hmac)

Довольно сложно, неправдали? Ноещё раз нестарайтесь понять каждую строчку кода. Тут важно понять, что сначала вызывается hmac_init, затем выполняются несколько вызовов hmac_update, после чего осуществляется вызов hmac_finalcall. Это весьма близко интуитивным допущениям, сделанным ранее для HMAC, поэтому, если SAW убедится втом, что MAC выглядит как данное выражение Cryptol, можно быть уверенным, что онработает так, как ожидалось.

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

lass SignalMessageVerifyMacSpec(Contract):    serialized_len: int    def specification(self) -> None:        ...        mac_index = 8 + self.serialized_len - SIGNAL_MESSAGE_MAC_LENGTH        ser_len   = f"{self.serialized_len} : [64]"        self.points_to(serialized[0], cryptol(ser_len))        self.points_to(serialized[8], serialized_message_data)        self.points_to(serialized[mac_index], mk_hmac(...))        self.execute_func(...)        self.returns(cryptol("1 : [32]"))

Здесь serialized указывает наsignal_buffer для всего сообщения. Для описания памяти, содержащейся вразличных частях буфера, можно использовать нотацию слайса Python (например, serialized[0]). Первая часть содержит self.serialized_len, общую длину сообщения. Через восемь байтразмещается serialized_message_data данные сообщения. Всамом конце буфера содержится MAC, вычисленный спомощью mk_hmac(...).

Проверяем всё напрактике вызываем llvm_verify согласно этой спецификации. Вэтот раз нужно передать несколько дополнительных аргументов. Нужно явно указать, какие допущения мысделали ранее спомощью llvm_assume посредством аргумента lemmas. Также нужно указать инструменту решения SMT, какие функции должны рассматриваться как неинтерпретируемые. Это делается спомощью аргумента script:

uninterps = ["hmac_init", "hmac_update", "hmac_final"]llvm_verify(mod, "signal_message_verify_mac",  SignalMessageVerifyMacSpec(...),            lemmas=[init_spec, update_spec1, update_spec2, final_spec],            script=ProofScript([z3(uninterps)]))

В результате мы видим долгожданную зелёную галочку:

Планы

Спомощью saw-client мысмогли получить ряд интересных данных окоде вlibsignal-protocol-c. Мысмогли продемонстрировать, что signal_message_verify_mac, функция, проверяющая целостность сообщения, отправленного попротоколу Signal, работает правильно, если последняя часть сообщения содержит верный код аутентификации сообщения (MAC). Кроме того, мыопределили, каким должно быть содержимое MAC относительно абстрактной спецификации криптографических хэш-функций.

Однако спомощью инструмента saw-client можно сделать гораздо больше, чем рассказано вданной заметке. Мыпроверяли ключевое свойство кода, проверяющего целостность сообщений. Нотакая проверка целостности некасалась сообщений, передаваемых попроводам. Мытакже нестали полностью определять поведение HMAC, хотя это можно былобы сделать; смотрите здесь.

Несмотря нато что saw-client может использоваться как самостоятельный инструмент верификации, внекоторых аспектах saw-client недостигает функциональности SAWScript. saw-client внастоящее время неподдерживает ряд функций SAW, например функцию инициализации глобальных переменных вспецификациях. Кроме того, некоторые идиомы SAWScript реализованы вsaw-client нетак "красиво", пример квазикавычки ввыражениях Cryptol. Мысчитаем, что современем нам удастся решить эти проблемы.

Вперспективе мыпопытаемся сделать Python полноправным языком написания кода для SAW, иданная работа первый шаг вэтом направлении. Весь код, представленный вэтой заметке, можно найти здесь. Рекомендуем испытать вработе инструмент saw-client. Любые ваши пожелания икомментарии отправляйте в трекер проблем ивопросов SAW.

А если вам инетересен не только анализ программ с помощью Python, но и активная разработка на этом языке в направлениях как бекенда, так и фронтенда, то вы можете обратить внимание на наш курс "Fullstack-разработчик на Python", где также рассматривается тестирование ПО и многие другие аспекты профессии программиста.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Перевод Optional.stream()

08.06.2021 20:05:53 | Автор: admin

На этой неделе я узнал об одной интересной "новой" возможности Optional, о которой хочу рассказать в этом посте. Она доступна с Java 9, так что новизна ее относительна.

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

public BigDecimal getOrderPrice(Long orderId) {    List<OrderLine> lines = orderRepository.findByOrderId(orderId);    BigDecimal price = BigDecimal.ZERO;           for (OrderLine line : lines) {        price = price.add(line.getPrice());       }    return price;}
  • Предоставьте переменную-аккумулятор для цены

  • Добавьте цену каждой строки к общей цене

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

public BigDecimal getOrderPrice(Long orderId) {    List<OrderLine> lines = orderRepository.findByOrderId(orderId);    return lines.stream()                .map(OrderLine::getPrice)                .reduce(BigDecimal.ZERO, BigDecimal::add);}

Давайте сосредоточимся на переменной orderId : она может содержать null.

Императивный способ обработки nullзаключается в том, чтобы проверить его в начале метода - и в конечном итоге сбросить:

public BigDecimal getOrderPrice(Long orderId) {    if (orderId == null) {        throw new IllegalArgumentException("Order ID cannot be null");    }    List<OrderLine> lines = orderRepository.findByOrderId(orderId);    return lines.stream()                .map(OrderLine::getPrice)                .reduce(BigDecimal.ZERO, BigDecimal::add);}

Функциональный способ заключается в том, чтобы обернуть orderId в Optional. Вот как выглядит код с использованием Optional:

public BigDecimal getOrderPrice(Long orderId) {    return Optional.ofNullable(orderId)                                        .map(orderRepository::findByOrderId)                               .flatMap(lines -> {                                                    BigDecimal sum = lines.stream()                        .map(OrderLine::getPrice)                        .reduce(BigDecimal.ZERO, BigDecimal::add);                return Optional.of(sum);                                       }).orElse(BigDecimal.ZERO);                            }
  1. Оберните orderId в Optional

  2. Найдите соответствующие строки заказа

  3. Используйте flatMap(), чтобы получить Optional<BigDecimal>; map() получит Optional<Optional<BigDecimal>>

  4. Нам нужно обернуть результат в Optional, чтобы он соответствовал сигнатуре метода.

  5. Если Optional не содержит значения, сумма равна 0

Optional делает код менее читабельным! Я считаю, что понятность должна быть всегда важнее стиля кода.

К счастью, Optional предлагает метод stream() (начиная с Java 9). Он позволяет упростить функциональный конвейер:

public BigDecimal getOrderPrice(Long orderId) {    return Optional.ofNullable(orderId)            .stream()            .map(orderRepository::findByOrderId)            .flatMap(Collection::stream)            .map(OrderLine::getPrice)            .reduce(BigDecimal.ZERO, BigDecimal::add);}

Вот краткая информация о типе на каждой строке:

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


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

Подробнее..

Перевод Сравнение Java-записей, Lombok Data и Kotlin data-классов

16.06.2021 20:20:54 | Автор: admin

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

Я уверен, что вы уже видели примеры, как с помощью записей превратить обычный POJO ...

class Range {private final int low;private final int high;public Range(int low, int high) {this.low = low;this.high = high;}public int getLow() {return low;}public int getHigh() {return high;}@Overridepublic boolean equals(Object o) {if (this == o)return true;if (o == null || getClass() != o.getClass())return false;Range range = (Range) o;return low == range.low &&high == range.high;}@Overridepublic int hashCode() {return Objects.hash(low, high);}@Overridepublic String toString() {return "[" + low + "; " + high + "]";}}

в одну строчку кода:

//          это компоненты записи (components)record Range (int low, int hight) { }

Конечно, аннотации @Data и @Value из Lombok обеспечивают аналогичную функциональность с давних пор, хоть и с чуть большим количеством строк:

@Dataclass Range {private final int low;private final int high;}

А если вы знакомы с Kotlin, то знаете, что то же самое можно получить, используя data-класс:

data class Range(val low: Int, val high: Int)

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

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

Семантика записей (records)

В JEP 395 говорится:

Записи (records) это классы, которые действуют как прозрачные носители неизменяемых данных.

Таким образом, создавая запись, вы говорите компилятору, своим коллегам, всему миру, что указанный тип хранит данные. А точнее, иммутабельные (поверхностно) данные с прозрачным доступом. Это основная семантика все остальное вытекает из нее.

Если такая семантика не применима к нужному вам типу, то не используйте записи. А если вы все равно будете их использовать (возможно, соблазнившись отсутствием бойлерплейта или потому что вы думаете, что записи эквивалентны @Data / @Value и data-классам), то только испортите свою архитектуру, и велики шансы, что это обернется против вас. Так что лучше так не делать.

(Извините за резкость, но я должен был это сказать.)

Прозрачность и ограничения

Давайте подробнее поговорим о прозрачности (transparency). По этому поводу у записей есть даже девиз (перефразированный из Project Amber):

API записей моделирует состояние, только состояние и ничего, кроме состояния.

Для реализации этого необходимы ряд ограничений:

  • для всех компонент должны быть аксессоры (методы доступа) с именем, совпадающим с именем компонента, и возвращающие такой же тип, как у компонента (иначе API не будет моделировать состояние)

  • должен быть конструктор с параметрами, которые соответствуют компонентам записи (так называемый канонический конструктор; иначе API не будет моделировать состояние)

  • не должно быть никаких дополнительных полей (иначе API не будет моделировать состояние)

  • не должно быть наследования классов (иначе API не будет моделировать состояние, так как некоторые данные могут находиться в другом месте за пределами записи)

И Lombok и data-классы Kotlin позволяют создавать дополнительные поля, а также приватные "компоненты" (в терминах записей Java, а Kotlin называет их параметрами первичного конструктора). Так почему же Java относится к этому так строго? Чтобы ответить на этот вопрос, нам понадобится вспомнить немного математики.

Математика

Множество (set) это набор некоторых элементов. Например, можно сказать, что C это множество всех цветов {синий, золотой, ...}, а N множество всех натуральных чисел {0, 1, ...}. Конечное множество {-2147483648, ..., 0, ..., 2147483647} это то, что в Java мы называем типом int. А если добавить к этому множеству null, то получим Integer. Аналогично бесконечное множество всех возможных строк (плюс null) мы называем String.

Итак, как вы поняли, тип это множество, значения которого допустимы для данного типа. Это также означает, что теория множеств "раздел математики, в котором изучаются общие свойства множеств" (как говорит Википедия), связана с теорией типов "академическим изучением систем типов" (аналогично), на которую опирается проектирование языков программирования.

class Pair {private final int first;private final int second;}

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

Это здорово, потому что теория множеств может многое сказать о применении функций к произведениям. Одним из аспектов этого является то, как функции, работающие с одним операндом, могут комбинироваться с функциями, работающими с несколькими операндами, и какие свойства функций (инъективные, биективные и т. д.) остаются нетронутыми.

// given: bijective function from int to intIntUnaryOperator increment =i -> i == Integer.MAX_VALUE ? Integer.MIN_VALUE : ++i;// then: combining two `increment`s yields a bijective function//       (this requires no additional proof or consideration)UnaryOperator<Pair> incrementPair =pair -> new Pair(increment.applyAsInt(pair.first()),increment.applyAsInt(pair.second()));

Вы обратили внимание на аксессоры Pair::first и Pair::second? Их не было в классе выше, поэтому их пришлось добавить. Иначе нельзя было бы применить функции к отдельным компонентам / операндам, и использовать Pair в качестве пары целых чисел. Аналогично, но с другой стороны, мне нужен конструктор, принимающий в качестве аргументов оба целых числа, чтобы можно было воспроизвести pair.

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

На самом деле записи лучше кортежей. В JEP 395 говорится:

Записи можно рассматривать как номинативные кортежи.

Где "номинативность" означает, что записи идентифицируются по их именам, а не по их структуре. Таким образом, два типа записей, которые моделируют int int, например, Pair(int first, int second) и Range(int low, int high) будут разными типами. А также обращение к компонентам записи идет не по индексу (range.get1()), а по имени (record.low()).

Следствия

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

Итак, если подытожить:

  • Аксессоры (методы доступа) генерируются компилятором.

  • Мы не можем изменять их имена или возвращаемый тип.

  • Мы должны быть очень осторожны с их переопределением.

  • Компилятор генерирует канонический конструктор.

  • Наследование отсутствует.

Преимущества записей

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

Деструктурирующие паттерны

if (range instanceof Range(int low, int high) && high < low)    return new Range(high, low);

Благодаря полной прозрачности записей мы можем быть уверены, что не пропустим скрытое состояние. Это означает, что разница между range и возвращаемым экземпляром это именно то, что вы видите: low и high меняются местами не более того.

Блок with

Range range = new Range(5, 10);// SYNTAX IS MADE UP!Range newRange = range with { low = 0; }// range: [5; 10]// newRange: [0; 10]

И, как и раньше, мы можем рассчитывать на то, что newRange будет точно таким же, как и range за исключением low: нет скрытого состояния, которое мы не перенесли. И синтаксически здесь все просто:

  • объявить переменные для компонент (например, low, high) и присвоить значения с помощью аксессоров

  • выполнить блок with

  • передать переменные в канонический конструктор

(Обратите внимание, что этот функционал далек от реальности и может быть не реализован или быть значительно изменен.)

Сериализация

Для представления объекта в виде потока байт, JSON / XML-документа или в виде любого другого внешнего представления и обратной конвертации, требуется механизм разбивки объекта на его значения, а затем сборки этих значений снова вместе. И вы сразу же можете увидеть, как это просто и хорошо работает с записями. Они не только раскрывают все свое состояние и предлагают канонический конструктор, но и делают это структурированным образом, что делает использование Reflection API очень простым.

Более подробно том, как записи изменили сериализацию, слушайте в подкасте Inside Java Podcast, episode 14 (также в Spotify). Если вы предпочитаете короткие тексты, то читайте твит.

Бойлерплейт код

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

  • канонический конструктор

  • аксессоры (методы доступа)

  • отсутствие наследования

Я не сказал об этом явно, но было бы неплохо, если (0, 0) = (0, 0), то есть должна быть правильная реализация equals, которая сразу же требует реализации hashCode.

И все это генерируется компилятором (а также еще toString) не столько для того, чтобы избавить нас от написания этого кода, сколько потому, что это естественное следствие алгебраической структуры.

Недостатки записей

Семантика записей ограничивает возможности по работе с классами. Как уже говорилось, вы не можете добавлять скрытое состояние через добавление полей, не можете переименовывать аксессоры и изменять тип возвращаемого значения и, вероятно, не должны менять возвращаемое ими значение. Записи также не позволяют изменять значения компонент, так как соответствующие им поля объявлены final. И отсутствует наследование классов (хотя вы можете реализовать интерфейсы).

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

Преимущества Lombok @Data/@Value

Lombok просто генерирует код. У него нет семантики, поэтому у вас есть полная свобода в изменении класса. Конечно, вы не получите преимуществ более строгих гарантий, хотя в будущем Lombok, возможно, сможет генерировать деструктурные методы.

(При этом я не рекламирую Lombok. Он в значительной степени полагается на внутренние API компилятора, которые могут измениться в любой момент, а это означает, что проекты, использующие его, могут сломаться при любом незначительном обновлении Java. То, что он много делает для скрытия технического долга от своих пользователей, тоже не очень хорошо.)

Преимущества data-классов Kotlin

Вот что говорится в документации о data-классах:

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

Вы можете видеть, что здесь также присутствует семантика хранения данных, но она довольно слабая, и основное внимание уделяется получению функциональности, то есть генерации кода. Действительно, data-классы предлагают больше возможностей по работе с классами, чем записи (мутабельные "компоненты", скрытое состояние, ...), но в отличие от Lombok не все (не могут расширяться, нельзя создавать свой метод copy, ...). С другой стороны, data-классы не дают сильных гарантий как записи, поэтому Kotlin не может построить на их основе аналогичную функциональность. Это разные подходы с разной ценой и выгодой.

Некоторые указывали на @JvmRecord в Kotlin как на большую ошибку: "Видите, data-классы могут быть записями шах и мат ответ" (я перефразировал, но смысл был такой). Если у вас возникли такие же мысли, то я прошу вас остановиться и подумать на секунду. Что именно это дает вам?

Data-класс должен соблюдать все правила записи, а это значит, что он не может делать больше, чем запись. Но Kotlin все еще не понимает концепции прозрачных кортежей и не может сделать с @JvmRecord data-классом больше, чем с обычным data-классом. Таким образом, у вас есть свобода записей и гарантии data-классов данных худшее из обоих миров.

Для чего тогда нужен @JvmRecord? Просто для совместимости. Как говорится в proposal:

В Kotlin нет большого смысла использовать JVM-записи, за исключением двух случаев:

  • перенос существующей Java-записи на Kotlin с сохранением ее ABI;

  • генерация атрибута класса записи с информацией о компоненте записи для класса Kotlin для последующего чтения каким-либо фреймворком, использующим Java reflection для анализа записей.

Рефлексия

Записи не лучше и не хуже рассмотренных альтернатив или других вариантов с аналогичным подходом, таких как case-классы Scala. У них действительно сильная семантика с твердым математическим фундаментом, которая хотя и ограничивает возможности по проектированию классов, но приносит мощные возможности, которые, в противном, случае были бы невозможны или, по крайней мере, не столь надежны.

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


В преддверии старта курса "Java Developer. Professional" приглашаю всех желающих на бесплатный демоурок по теме: Система получения курсов валют ЦБ РФ.

Подробнее..

Перевод 10 топовых плагинов для IntelliJ IDEA, которые ты не должен пропустить

17.06.2021 16:13:58 | Автор: admin

Хотя IntelliJ IDEA является полноценной IDE (Интегрированная среда разработки), вы наверняка захотите ее персонализировать. В JetBrains Marketplace есть множество плагинов с полезными функциями, которые могут удовлетворить ваши личные или деловые потребности.

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

Напоминаем, что все плагины можно установить через Настройки/Параметры | Плагины или на вкладке Плагины на экране приветствия.

Погнали!

Хиты

Плагин Jump to Line

Многие навигационные действия в дебаггере IntelliJ IDEA позволяют установить точку останова в нужном месте, но иногда необходимо достичь строки одним щелчком мыши. Здесь на помощь приходит плагин Jump To Line. Он позволяет добраться до любой строки и установить там точку выполнения, не выполняя предыдущий код.

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

Узнайте больше об этом плагине в нашем блоге.

Плагин Key Promoter X

Не секрет, что кодинг без использования мыши быстрее и эффективнее, но как стать ориентированным на клавиатуру, когда в IntelliJ IDEA так много сочетаний клавиш, которые нужно запомнить? Key Promoter X научит вас пользоваться ими. Как настойчивый и дотошный тренер, он отобразит всплывающую подсказку с соответствующим сочетанием клавиш при нажатии на элемент внутри IDE. Более того, для кнопок, не имеющих шортката, Key Promoter X предложит вам создать его.

Практика доведет все до идеала! Через некоторое время вы заметите, что подсознательно экономите время и используете нужный шорткат.

Плагин Maven Helper

Если вы ищете дополнительные возможности для работы с проектами Maven, этот плагин вам просто необходим. Он позволяет просматривать, анализировать и исключать конфликтующие зависимости. Также можно запускать и отлаживать цели Maven и многое другое. Попробуйте этот 5-звездочный плагин!

Плагин Doc-Aware Search Everywhere

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

Плагин Rainbow brackets

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

Плагин Randomness

Вам нужно добавить в проект рандомные данные, например, слово, число или строку? Если у вас закончились варианты, установите этот плагин и нажмите Alt+R в Windows и Linux или R в macOS, чтобы увидеть выпадающий список возможных типов данных, которые вы можете добавить. Выберите нужный, и тогда произойдет волшебство - плагин Randomness будет добавлять разные значения каждый раз, когда вы применяете это действие.

Плагин EduTools

Этот плагин полезен как для учащихся, так и для преподавателей. Он позволяет изучать и преподавать языки программирования, такие как Kotlin, Java, Python, JavaScript, Rust, Scala, C/C++ и Go, прямо из IDE. Если вы изучаете программирование, мы призываем вас учиться на практике. Установите плагин, чтобы присоединиться к публичному курсу программирования, доступному в системе. Также вы можете записаться на индивидуальный курс вашего учителя или коллеги по работе. Да, вы не ослышались, плагин Edu Tools позволяет создавать упражнения и делиться ими со своими коллегами.

Плагин GitToolBox

IntelliJ IDEA уже поддерживает полноценную интеграцию с Git, но этот плагин предлагает дополнительные возможности, которые можно использовать по своему усмотрению. Люди приобретают его в основном для inline blame - аннотации, которая показывает, кто и когда изменил код в строке. GitToolBox также добавляет отображение статуса, автоматическую выборку, оповещения и многое другое.

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

Плагин WakaTime

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

Плагин Extra Icons

Плагин Extra Icons предназначен для тех, кто хочет приукрасить вид проекта. Он добавляет набор значков, которые не поддерживаются IntelliJ IDEA по умолчанию. Они выглядят потрясающе и упрощают навигацию между файлами, поскольку вы можете визуально определить их тип. Кроме того, значки очень легко настраиваются. Вы можете настроить их в Preferences| Settings / Appearance & Behavior/ Appearance/ Extra Icons.

Бонус

В качестве дополнения установите Nyan Progress Bar, чтобы сделать индексирование более спокойным занятием. Если вам не нравится кот Nyan, попробуйте других персонажей, например, Mario или случайного покемона.

Мы надеемся, что эти плагины помогут вам настроить вашу IDE и буду делать вас немного счастливее каждый день. Оставайтесь продуктивными и получайте удовольствие от работы с IntelliJ IDEA!

Удачной работы!


В преддверии старта курса "Java Developer. Basic" приглашаем всех желающих на бесплатный двухдневный интенсив по теме "Хороший код".


Подробнее..

Как подружить Redis Cluster c Testcontainers?

20.06.2021 12:09:43 | Автор: admin
В 26-м выпуске NP-полного подкаста я рассказывал, что начал переводить один из своих сервисов из Redis Sentinel на Redis Cluster. На этой неделе я захотел потестировать данный код, и, конечно же, выбрал Testcontainers для этого. К сожалению, Redis Cluster в тестовых контейнерах не завелся из коробки, и мне пришлось вставить несколько костылей. О них и пойдет речь далее.



Вводные


Сначала я бы хотел описать все вводные, а потом рассказать про костыли. Мой проект построен на Spring Boot. Для взаимодействия с редисом используется Lettuce клиент. Для тестирования testcontainers-java с JUnit. Версия обоих редисов 6. В общем, всё типичное, нет ничего особенного с точки зрения стека.

Если кто-то еще не знаком с testcontainers, то пара слов о них. Это библиотека для интеграционного тестирования. Она построена на другой библиотеке https://github.com/docker-java/docker-java. Тестконтейнеры, по сути говоря, помогают быстро и просто запускать контейнеры с разными зависимостями в ваших интеграционных тестах. Обычно это базы данных, очереди и другие сложные системы. Некоторые люди используют testcontainers и для запуска своих сервисов, от которых зависит тестируемое приложение (чтобы тестировать микросервисное взаимодействие).

Про Redis Cluster


Redis Cluster это одна из нескольких реализаций распределнного режима Редиса. К сожалению, в Редисе нет единого правильного способа, как масштабировать базу. Есть Sentinel, есть Redis Cluster, а еще ребята активно разрабатывают RedisRaft распредеделенный редис на базе протокола консенсуса Raft (у них там своя реализация, которая, как они сами заявляют, не совсем каноничный Рафт, но конкретно для Redis самое то).

В целом, про Redis Cluster есть две замечательных статьи на официальном сайте https://redis.io/topics/cluster-tutorial и https://redis.io/topics/cluster-spec. Большинство деталей описано там.

Для использования Redis Cluster в testcontainers важно знать несколько вещей из документации. Во-первых, Redis Cluster использует gossip протокол поэтому каждый узел кластера имеет TCP-соединение со всеми другими узлами. Поэтому, между нодами должна быть сетевая связность, даже в тестах.

Вторая важная штука, которую надо знать при тестировании это наличие в Redis Cluster bootstrap узлов для конфигурации. То есть, вы в настройках можете задать лишь подмножество узлов, которые будут использоваться для старта приложения. В последствие, Redis клиент сам получит Топологию кластера через взаимодействие с Редисом. Исходя из этого, получается вторая особенность тестируемое приложение должно иметь сетевую связность с теми Redis URI, которые будут аннонсированы со стороны редис кластера (кстати, эти адреса можно сконфигурировать через cluster-announce-port и cluster-announce-ip).

Про костыли с Redis Cluster и testcontainers


Для тестирования я выбрал довольно популярный docker-образ https://github.com/Grokzen/docker-redis-cluster. Он не подходит для продакшена, но очень прост в использовании в тестах. Особенность этого образа все Редисы (а их 6 штук, по умолчанию 3 мастера и 3 слейва) будут подняты в рамках одного контейнера. Поэтому, мы автоматически получаем сетевую связность между узлами кластера из коробки. Осталось решить вторую из двух проблем, связанную с получением приложением топологии кластера.

Я не хотел собирать свой docker-образ, а выбранный мной image не предоставляет возможности задавать настройки cluster-announce-port и cluster-announce-ip. Поэтому, если ничего не делать дополнительно, при запуске тестов вы увидите примерно такие ошибки:

Unable to connect to [172.17.0.3/<unresolved>:7003]: connection timed out: /172.17.0.3:7003


Ошибка означает, что мы со стороны приложения пытаеся приконнектится к Узлу редис кластера, используя IP докер контейнера и внутренний порт (порт 7003 используется данным узлом, но наружу он отображается на какой-то случайный порт, который мы и должны использовать в нашем приложении; внутренний порт, по понятным причинам, не доступен из вне). Что касается данного IP-адреса он доступен для приложения, если это Linux, и он не доступен для приложения, если это MacOs/Windows (из-за особенностей реализации докера на этих ОС).

Решение проблемы (а-ка костыль) я собрал по частичкам из разных статей. А давайте сделаем NAT RedisURI на стороне приложения. Ведь это нужно именно для тестов, и тут не так страшно вставлять такой ужас. Решение, на самом деле, состоит из пары строк (огромное спасибо Спрингу и Lettuce, где можно сконфигурировать практически всё, только и успевай, как переопределять бины).

public SocketAddress resolve(RedisURI redisURI) {    Integer mappedPort = redisClusterNatPortMapping.get(redisURI.getPort());    if (mappedPort != null) {        SocketAddress socketAddress = redisClusterSocketAddresses.get(mappedPort);        if (socketAddress != null) {            return socketAddress;        }        redisURI.setPort(mappedPort);    }    redisURI.setHost(DockerClientFactory.instance().dockerHostIpAddress());    SocketAddress socketAddress = super.resolve(redisURI);    redisClusterSocketAddresses.putIfAbsent(redisURI.getPort(), socketAddress);    return socketAddress;}


Полный код выложен на гитхаб https://github.com/Hixon10/spring-redis-cluster-testcontainers.

Идея кода супер простая. Будем хранить две Map. В первой маппинг между внутренними портами редиса (7000..7005) и теми, что доступны для приложения (они могут быть чем-то типа 51343, 51344 и тд). Во-второй внешние порты (типа, 51343) и SocketAddress, полученный для них. Теперь, когда мы получаем от Редиса при обновлении топологии что-то типа 172.17.0.3:7003, мы сможем легко найти нужный внешний порт, по которому сможем найти SocketAddress и переиспользовать его. То есть, с портами проблема решена. А что с IP?

С IP-адресом всё просто. Тут нам на помощь приходят Тест контейнеры в которых есть утилитный метод DockerClientFactory.instance().dockerHostIpAddress(). Для MacOs/Windows он будет отдавать localhost, а для linux IP-адрес контейнера.

Выводы


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

Как синхронизировать сценарий без транзакций? Штатными средствами Java

15.06.2021 02:14:19 | Автор: admin

Давайте представим, что вы параноик, и параноик вдвойне, когда дело касается многопоточности. Предположим, что вы делаете backend некого функционала приложения, а приложение переодически дергает на вашем серверы какие-то методы. Все вроде хорошо, но есть одно но. Что если ваш функционал напрямую зависит от каких-либо других данных, того же банального профиля например? Встает вопрос, как гарантировать то, что сценарий отработает именно так, как вы планировали и не будет каких-либо сюрпризов? Транзакции? Да это можно использовать, но что если Вы фантастический параноик и уже представляете как к вам на сервер летит 10 запросов к одному методу от разных клиентов и все строго в одно время. А в этот момент бизнес-логика данного метода завязана на 100500 разных данных. Как всем этим управлять? Можно просто синхронизировать метод и все. Но что если летят еще и те запросы, держать которые нет смысла? Тут уже начинаются костыли. Я пару раз уже задавался подобным вопросом, и были интересно, ведь задача до абсурда простая и повседневная (если вы заботитесь о том, чтобы не было логических багов конечно же :). Сегодня решил подумать, как это можно очень просто и без костылей реализовать. И решение вышло буквально на 100 строк кода.

Немного наглядного примера

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

String result = l.lock(new ArrayList<Locker.Item>() {{    add(new Locker.Item(SimpleType.TRIP, 1));    add(new Locker.Item(SimpleType.USER, 2));}}, () -> {    // Тут выполняем отмену поездки и держим водителя на привязи    // Кстати если кто-то где-то вызовет USER=2 (водитель), то он также будет ждать    // ну или кто-то обратится к поездке TRIP=1    // А если обратится к USER=3, то уже все будет нормально :)    // так как никто не блокировал третьего пользователя :)    return "Тут любой результат :)";    });

Элегантно и просто! :)

Исходники тут - https://github.com/GRIDMI/GRIDMI.Sync

Камнями не бросаться! :)

Подробнее..

Подведены итоги Tech Monsters Night

08.06.2021 20:05:53 | Автор: admin


4 июня 2021 года состоялась битва Java-разработчиков Tech Monsters Night от М.Видео-Эльдорадо.
Почти три сотни разработчиков не спали в ночь с 4 на 5 июня. Участникам хакатона представился шанс обнулить цены на топовую технику.

Уникальных посетителей лендинга 3873 человек (5035 визитов);
Регистраций 540;
Пришли на ночь монстров 286 человек;
Решали задачи 203 человека;
Просмотров стрим-конференции на YouTube 554;
Обнулили и забрали подарки 30 участников;
Охваты свыше 30 млн (включая Хабр и др. медиа гиганты).

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





Время соревнования было определено по Москве, но география участников растянулась от Московской области до Улан-Удэ и Южно-Сахалинска. Из 540 регистраций на сам чемпионат пришло 286 человек, это больше половины, учитывая, что хакатон был нетипичный.



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



В итоге получилось не 3 победителя, как обычно, а целых 30 призеров и 30 подарков. Лучшим из лучших монстров досталась топовая техника: мощный ПК, широкоформатный монитор, игровые клавиатуры, компьютерные мыши и iPhone 12 mini, а также стильный мерч и другие призы.



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

Финал вообще был непредсказуем: буквально за несколько минут до окончания чемпионата лидер сменился. Это нужно видеть! Полный лидерборд с именами призеров находится на лендинге.



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

Еще одной фишкой стрима стали собеседования в прямом эфире. Любой желающий мог показать свои скиллы и online доказать компетенции. Принимали ответы представители HR-бренда работодателя М.Видео-Эльдорадо Фролова Екатерина и Рамиль Акберов. В итоге два смельчака из числа добровольцев прошли online-собеседование. Один из них даже получил One night offer.



Пока на YouTube зрители смотрели конференцию, в телеграм- чате проекта проходил конкурс на лучшее фото. Участники делились атмосферой в которой они решают поставленные задачи. В результате организаторы выбрали самое атмосферное фото, получившее специальный приз.

Для того, чтобы силы окончательно не покинули монстров кода посреди битвы, всем выслали energy break энергетики и пиццу.



Чемпионат только закончился, а в чате уже появились вопросы когда следующая Tech monsters night. Что же, отвечаем coming soon.



Кто монстр, тот знает!
Подробнее..

Перевод Изучение и анализ Spring Boot приложения с помощью Actuator и jq

10.06.2021 10:07:14 | Автор: admin

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

Хотя Actuator в основном используется в производственной среде, он также может помочь нам во время разработки и сопровождения.Мы можем использовать его для изучения и анализа нового приложения Spring Boot.

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

Пример кода

Эта статья сопровождается примером рабочего кодана GitHub.

Зачем использовать Actuator для анализа и изучения приложения?

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

Хотя это важные шаги, они дают нам только статичное представление о приложении.Мы не можем получить полную картину, не понимая, что происходит во время выполнения.Например, что представляют собой все создаваемые Spring Beans?Какие конечные точки API доступны?Каковы все фильтры, через которые проходит запрос?

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

Общий обзор Spring Boot Actuator

Начнем с краткого обзора по Spring Boot Actuator.

На верхнем уровне, когда мы работаем с Actuator, мы делаем следующие шаги:

  1. Добавляем Actuator как зависимость к нашему проекту

  2. Включаем и открываем конечные точки

  3. Защищаем и настраиваем конечные точки

Давайте кратко рассмотрим каждый из этих шагов.

Шаг 1. Добавьте Actuator зависимость

Добавление Actuator в наш проект похоже на добавление любой другой зависимости.Вот фрагмент для Mavenpom.xml:

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

Если бы мы использовали Gradle, мы бы добавили вфайл build.gradle следующийфрагмент:

dependencies {    implementation 'org.springframework.boot:spring-boot-starter-actuator'}

Простое добавление указанной выше зависимости в приложение Spring Boot предоставляет некоторые готовые конечные точки, такие как/actuator/health, которые могут использоваться, например, для поверхностной проверки работоспособности с помощью балансировщика нагрузки.

$ curl http://localhost:8080/actuator/health{"status":"UP"}

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

$ curl http://localhost:8080/actuator{"_links":{"self":{"href":"http://localhost:8080/actuator","templated":false},"health":{"href":"http://localhost:8080/actuator/health","templated":false},"health-path":{"href":"http://localhost:8080/actuator/health/{*path}","templated":true},"info":{"href":"http://localhost:8080/actuator/info","templated":false}}}

Шаг 2. Включите и откройте конечные точки

Конечные точки идентифицируются идентификаторами,такими как health, info, metrics и так далее.Включение и открытие конечной точки делает ее доступной для использования попути /actuator URL-адреса приложения, напримерhttp://your-service.com/actuator/health,http://your-service.com/actuator/metrics и т. д.

Большинство конечных точек, за исключениемshutdown, включены по умолчанию.Мы можем отключить конечную точку, установив для свойства management.endpoint.<id>.enabled значениеfalseвapplication.propertiesфайле.Например, вот как мы отключимmetrics конечную точку:

management.endpoint.metrics.enabled=false

Доступ к отключенной конечной точке возвращает ошибку HTTP 404:

$ curl http://localhost:8080/actuator/metrics{"timestamp":"2021-04-24T12:55:40.688+00:00","status":404,"error":"Not Found","message":"","path":"/actuator/metrics"}

Мы можем предоставить доступ к конечным точкам через HTTP и / или JMX.Хотя обычно используется HTTP, для некоторых приложений может быть предпочтительнее JMX.

Мы можем раскрыть конечные точки, установивmanagement.endpoints.[web|jmx].exposure.include для списка идентификаторов конечных точек, которые мы хотим раскрыть.Вот как мы можем открытьmetrics конечную точку, например:

management.endpoints.web.exposure.include=metrics

Чтобы конечная точка была доступна, она должна быть включена и доступна.

Шаг 3. Защитите и настройте конечные точки

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

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

Краткое введение вjq

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

Предположим, у нас в файле есть следующий JSONsample.json:

{  "students": [    {      "name": "John",      "age": 10,      "grade": 3,      "subjects": ["math", "english"]          },    {      "name": "Jack",      "age": 10,      "grade": 3,      "subjects": ["math", "social science", "painting"]    },    {      "name": "James",      "age": 11,      "grade": 5,      "subjects": ["math", "environmental science", "english"]    },    .... other student objects omitted ...  ]}

Это объект, содержащий массив объектов student с некоторыми деталями для каждого ученика.

Давайте рассмотрим несколько примеров обработки и преобразования этого JSON с помощьюjq.

$ cat sample.json | jq '.students[] | .name'"John""Jack""James"

Рассмотримjq команду, чтобы понять, что происходит:

Выражение

Эффект

.students[]

перебиратьмассив students

|

вывод каждогоstudent для следующего фильтра

.name

выборка атрибутаnameиз объектаstudent

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

$ cat sample.json | jq '.students[] | select(.subjects[] | contains("science"))'{  "name": "Jack",  "age": 10,  "grade": 3,  "subjects": [    "math",    "social science",    "painting"  ]}{  "name": "James",  "age": 11,  "grade": 5,  "subjects": [    "math",    "environmental science",    "english"  ]}

Рассмотримкоманду еще раз:

Выражение

Эффект

.students[]

перебирать массивstudents

|

вывод каждогоstudent для следующего фильтра

select(.subjects[] | contains("science"))

выберите студента, если его массивsubjects содержит элемент со строкой наука

С одним небольшим изменением мы можем снова собрать эти элементы в массив:

$ cat sample.json | jq '[.students[] | select(.subjects[] | contains("science"))]'[  {    "name": "Jack",    "age": 10,    "grade": 3,    "subjects": [      "math",      "social science",      "painting"    ]  },  {    "name": "James",    "age": 11,    "grade": 5,    "subjects": [      "math",      "environmental science",      "english"    ]  }]

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

Мы можем использоватьjq как для фильтрации, так и для изменения формы JSON:

$ cat sample.json | jq '[.students[] | {"studentName": .name, "favoriteSubject": .subjects[0]}]'[  {    "studentName": "John",    "favoriteSubject": "math"  },  {    "studentName": "Jack",    "favoriteSubject": "math"  },  {    "studentName": "James",    "favoriteSubject": "math"  }]

Мы, выполнив итерацию по массивуstudents, создали новый объект,содержащий свойствоstudentName иfavoriteSubject со значениями устанавленными из атрибутаname и первогоsubject из исходногообъекта student .В результате мы собрали все новые объекты в массив.

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

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

Изучение приложения Spring Boot

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

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

се конечные точки, которые мы увидим, включены по умолчанию. Давйте откроем их:

management.endpoints.web.exposure.include=mappings,beans,startup,env,scheduledtasks,caches,metrics

Использование конечной точки отображения

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

Давайте перейдем на конечную точку с помощью команды curl и направим ответ в jq, чтобы красиво его распечатать:

$ curl http://localhost:8080/actuator/mappings | jq

Вот ответ:

{  "contexts": {    "application": {      "mappings": {        "dispatcherServlets": {          "dispatcherServlet": [            {              "handler": "Actuator web endpoint 'metrics'",              "predicate": "{GET [/actuator/metrics], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}",              "details": {                "handlerMethod": {                  "className": "org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler",                  "name": "handle",                  "descriptor": "(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;"                },                "requestMappingConditions": {                  ... properties omitted ...                  ],                  "params": [],                  "patterns": [                    "/actuator/metrics"                  ],                  "produces": [                    ... properties omitted ...                  ]                }              }            },          ... 20+ more handlers omitted ...          ]        },        "servletFilters": [          {            "servletNameMappings": [],            "urlPatternMappings": [              "/*"            ],            "name": "webMvcMetricsFilter",            "className": "org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter"          },          ... other filters omitted ...        ],        "servlets": [          {            "mappings": [              "/"            ],            "name": "dispatcherServlet",            "className": "org.springframework.web.servlet.DispatcherServlet"          }        ]      },      "parentId": null    }  }}

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

jq для дальнейшей фильтрации этой информации. Поскольку мы знаем имена пакетов из нашей службы, jq будет выбирать только те обработчики, которые содержат имя нашего пакета io.reflectoring.springboot.actuator:

Давайте воспользуемся jq для дальнейшей фильтрации этой информации. Поскольку мы знаем имена пакетов из нашей службы, jqselectбудет выбирать только те обработчики, которые содержат имя нашего пакета io.reflectoring.springboot.actuator:

$ curl http://localhost:8080/actuator/mappings | jq '.contexts.application.mappings.dispatcherServlets.dispatcherServlet[] | select(.handler | contains("io.reflectoring.springboot.actuator"))'{  "handler": "io.reflectoring.springboot.actuator.controllers.PaymentController#processPayments(String, PaymentRequest)",  "predicate": "{POST [/{orderId}/payment]}",  "details": {    "handlerMethod": {      "className": "io.reflectoring.springboot.actuator.controllers.PaymentController",      "name": "processPayments",      "descriptor": "(Ljava/lang/String;Lio/reflectoring/springboot/actuator/model/PaymentRequest;)Lio/reflectoring/springboot/actuator/model/PaymentResponse;"    },    "requestMappingConditions": {      "consumes": [],      "headers": [],      "methods": [        "POST"      ],      "params": [],      "patterns": [        "/{orderId}/payment"      ],      "produces": []    }  }}{  "handler": "io.reflectoring.springboot.actuator.controllers.OrderController#getOrders(String)",  "predicate": "{GET [/{customerId}/orders]}",  "details": {    "handlerMethod": {      "className": "io.reflectoring.springboot.actuator.controllers.OrderController",      "name": "getOrders",      "descriptor": "(Ljava/lang/String;)Ljava/util/List;"    },    "requestMappingConditions": {      "consumes": [],      "headers": [],      "methods": [        "GET"      ],      "params": [],      "patterns": [        "/{customerId}/orders"      ],      "produces": []    }  }}{  "handler": "io.reflectoring.springboot.actuator.controllers.OrderController#placeOrder(String, Order)",  "predicate": "{POST [/{customerId}/orders]}",  "details": {    "handlerMethod": {      "className": "io.reflectoring.springboot.actuator.controllers.OrderController",      "name": "placeOrder",      "descriptor": "(Ljava/lang/String;Lio/reflectoring/springboot/actuator/model/Order;)Lio/reflectoring/springboot/actuator/model/OrderCreatedResponse;"    },    "requestMappingConditions": {      "consumes": [],      "headers": [],      "methods": [        "POST"      ],      "params": [],      "patterns": [        "/{customerId}/orders"      ],      "produces": []    }  }}

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

Точно так же мы можем проверить, через какие фильтры проходят наши запросы, прежде чем они достигнут контроллеров:

$ curl http://localhost:8080/actuator/mappings | jq '.contexts.application.mappings.servletFilters'[  {    "servletNameMappings": [],    "urlPatternMappings": [      "/*"    ],    "name": "webMvcMetricsFilter",    "className": "org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter"  },  ... other filters omitted ...]

Использование конечной точкиbeans

Теперь давайте посмотрим на список созданных bean-компонентов:

$ curl http://localhost:8080/actuator/beans | jq{  "contexts": {    "application": {      "beans": {        "endpointCachingOperationInvokerAdvisor": {          "aliases": [],          "scope": "singleton",          "type": "org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor",          "resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.class]",          "dependencies": [            "org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration",            "environment"          ]        },               .... other beans omitted ...    }  }}

Это дает общее представление обо всех компонентах вApplicationContext. Промотр его дает нам некоторое представление о структуре приложения во время выполнения - какие внутренние bean-компоненты Spring, каковы bean-компоненты приложения, каковы их области действия, каковы зависимости каждого bean-компонента и т. д.

Опять же, мы можем использоватьjq для фильтрации ответов и сосредоточиться на тех частях ответа, которые нам интересны:

$ curl http://localhost:8080/actuator/beans | jq '.contexts.application.beans | with_entries(select(.value.type | contains("io.reflectoring.springboot.actuator")))'{  "orderController": {    "aliases": [],    "scope": "singleton",    "type": "io.reflectoring.springboot.actuator.controllers.OrderController",    "resource": "file [/code-examples/spring-boot/spring-boot-actuator/target/classes/io/reflectoring/springboot/actuator/controllers/OrderController.class]",    "dependencies": [      "orderService",      "simpleMeterRegistry"    ]  },  "orderService": {    "aliases": [],    "scope": "singleton",    "type": "io.reflectoring.springboot.actuator.services.OrderService",    "resource": "file [/code-examples/spring-boot/spring-boot-actuator/target/classes/io/reflectoring/springboot/actuator/services/OrderService.class]",    "dependencies": [      "orderRepository"    ]  },  ... other beans omitted ...  "cleanUpAbandonedBaskets": {    "aliases": [],    "scope": "singleton",    "type": "io.reflectoring.springboot.actuator.services.tasks.CleanUpAbandonedBaskets",    "resource": "file [/code-examples/spring-boot/spring-boot-actuator/target/classes/io/reflectoring/springboot/actuator/services/tasks/CleanUpAbandonedBaskets.class]",    "dependencies": []  }}

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

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

Использование конечной точкиstartup

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

SpringApplication app = new SpringApplication(DemoApplication.class);app.setApplicationStartup(new BufferingApplicationStartup(2048));app.run(args);

Здесь мы установили для нашего приложенияApplicationStartup значение a,BufferingApplicationStartup которое является структурой в памяти, которая фиксирует события в сложном процессе запуска Spring.Внутренний буфер будет иметь указанную нами емкость - 2048.

Теперь перейдем конечной точкеstartup.В отличие от других конечных точек startup поддерживаетPOST метод:

$ curl -XPOST 'http://localhost:8080/actuator/startup' | jq{  "springBootVersion": "2.4.4",  "timeline": {    "startTime": "2021-04-24T12:58:06.947320Z",    "events": [      {        "startupStep": {          "name": "spring.boot.application.starting",          "id": 1,          "parentId": 0,          "tags": [            {              "key": "mainApplicationClass",              "value": "io.reflectoring.springboot.actuator.DemoApplication"            }          ]        },        "startTime": "2021-04-24T12:58:06.956665337Z",        "endTime": "2021-04-24T12:58:06.998894390Z",        "duration": "PT0.042229053S"      },      {        "startupStep": {          "name": "spring.boot.application.environment-prepared",          "id": 2,          "parentId": 0,          "tags": []        },        "startTime": "2021-04-24T12:58:07.114646769Z",        "endTime": "2021-04-24T12:58:07.324207009Z",        "duration": "PT0.20956024S"      },         .... other steps omitted ....      {        "startupStep": {          "name": "spring.boot.application.started",          "id": 277,          "parentId": 0,          "tags": []        },        "startTime": "2021-04-24T12:58:11.169267550Z",        "endTime": "2021-04-24T12:58:11.212604248Z",        "duration": "PT0.043336698S"      },      {        "startupStep": {          "name": "spring.boot.application.running",          "id": 278,          "parentId": 0,          "tags": []        },        "startTime": "2021-04-24T12:58:11.213585420Z",        "endTime": "2021-04-24T12:58:11.214002336Z",        "duration": "PT0.000416916S"      }    ]  }}

Ответ представляет собой массив с подробной информацией о событиях: name, startTime, endTimeиduration.

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

Поскольку приведенный выше ответ содержит много деталей, давайте сузим его, отфильтровав поspring.beans.instantiate шагу, а также отсортируем события по продолжительности в порядке убывания:

$ curl -XPOST 'http://localhost:8080/actuator/startup' | jq '.timeline.events | sort_by(.duration) | reverse[] | select(.startupStep.name | contains("instantiate"))'$ 

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

$ curl -XPOST 'http://localhost:8080/actuator/startup' | jq '[.timeline.events | sort_by(.duration) | reverse[] | select(.startupStep.name | contains("instantiate")) | {beanName: .startupStep.tags[0].value, duration: .duration}]' [  {    "beanName": "orderController",    "duration": "PT1.010878035S"  },  {    "beanName": "orderService",    "duration": "PT1.005529559S"  },  {    "beanName": "requestMappingHandlerAdapter",    "duration": "PT0.11549366S"  },  {    "beanName": "tomcatServletWebServerFactory",    "duration": "PT0.108340094S"  },  ... other beans omitted ...]

Таким образом, на созданиеbean-компонентовorderController иуходит больше секундыorderService!Это интересно - теперь у нас есть конкретная область приложения, на которой мы можем сосредоточиться, чтобы понять больше.

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

jq '[.timeline.events \  | sort_by(.duration) \  | reverse[] \  | select(.startupStep.name \  | contains("instantiate")) \  | {beanName: .startupStep.tags[0].value, duration: .duration}]'

Выражение

Эффект

.timeline.events | sort_by(.duration) | reverse

отсортируйтемассив timeline.events по свойству duration и реверсируйте результат, чтобы он был отсортирован в порядке убывания

[]

перебирать результирующий массив

select(.startupStep.name | contains("instantiate"))

выберите элемент объекта только в том случае, еслисвойствоstartupStep элементаname содержит текст instantiate

{beanName: .startupStep.tags[0].value, duration: .duration}

создать новый объект JSON со свойствами beanName и duration

Скобки над всем выражением указывают на то, что мы хотим собрать все созданные объекты JSON в массив.

Использование конечной точкиenv

Конечная точка env дает обобщенное представление всех свойств конфигурации приложения.Сюда входят конфигурации изapplication.properties файла, системные свойства JVM, переменные среды и т. д.

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

$ curl http://localhost:8080/actuator/env | jq{  "activeProfiles": [],  "propertySources": [    {      "name": "server.ports",      "properties": {        "local.server.port": {          "value": 8080        }      }    },    {      "name": "servletContextInitParams",      "properties": {}    },    {      "name": "systemProperties",      "properties": {        "gopherProxySet": {          "value": "false"        },        "java.class.path": {          "value": "/target/test-classes:/target/classes:/Users/reflectoring/.m2/repository/org/springframework/boot/spring-boot-starter-actuator/2.4.4/spring-boot-starter-actuator-2.4.4.jar:/Users/reflectoring/.m2/repository/org/springframework/boot/spring-boot-starter/2.4.4/spring-boot-starter-2.4.4.jar: ... other jars omitted ... "        },       ... other properties omitted ...      }    },    {      "name": "systemEnvironment",      "properties": {        "USER": {          "value": "reflectoring",          "origin": "System Environment Property \"USER\""        },        "HOME": {          "value": "/Users/reflectoring",          "origin": "System Environment Property \"HOME\""        }        ... other environment variables omitted ...      }    },    {      "name": "Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'",      "properties": {        "management.endpoint.logfile.enabled": {          "value": "true",          "origin": "class path resource [application.properties] - 2:37"        },        "management.endpoints.web.exposure.include": {          "value": "metrics,beans,mappings,startup,env, info,loggers",          "origin": "class path resource [application.properties] - 5:43"        }      }    }  ]}

Использование конечной точкиscheduledtasks

Эта конечная точка позволяет нам проверять, выполняет ли приложение какую-либо задачу периодически, используя@Scheduled аннотациюSpring:

$ curl http://localhost:8080/actuator/scheduledtasks | jq{  "cron": [    {      "runnable": {        "target": "io.reflectoring.springboot.actuator.services.tasks.ReportGenerator.generateReports"      },      "expression": "0 0 12 * * *"    }  ],  "fixedDelay": [    {      "runnable": {        "target": "io.reflectoring.springboot.actuator.services.tasks.CleanUpAbandonedBaskets.process"      },      "initialDelay": 0,      "interval": 900000    }  ],  "fixedRate": [],  "custom": []}

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

Использование конечной точкиcaches

Эта конечная точка перечисляет все кэши приложений:

$ curl http://localhost:8080/actuator/caches | jq{  "cacheManagers": {    "cacheManager": {      "caches": {        "states": {          "target": "java.util.concurrent.ConcurrentHashMap"        },        "shippingPrice": {          "target": "java.util.concurrent.ConcurrentHashMap"        }      }    }  }}

Мы можем сказать,что приложение кэширует некоторые данные: states и shippingPrice. Это дает нам еще одну область приложения, которую нужно изучить и узнать больше: как создаются кеши, когда удаляются записи кеша и т. д.

Использование конечной точкиhealth

Конечная точка health показывает информацию оздоровье приложения:

$ curl http://localhost:8080/actuator/health{"status":"UP"}

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

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

Прочтитеэтустатьюна Reflectoring,чтобы узнать больше о реализации проверок работоспособности с помощью Actuator.

Использование конечной точкиmetrics

Эта конечная точка перечисляет все метрики, сгенерированные приложением:

$ curl http://localhost:8080/actuator/metrics | jq{  "names": [    "http.server.requests",    "jvm.buffer.count",    "jvm.buffer.memory.used",    "jvm.buffer.total.capacity",    "jvm.threads.states",    "logback.events",    "orders.placed.counter",    "process.cpu.usage",    ... other metrics omitted ...  ]}

Затем мы можем получить данные отдельных показателей:

$ curl http://localhost:8080/actuator/metrics/jvm.memory.used | jq{  "name": "jvm.memory.used",  "description": "The amount of used memory",  "baseUnit": "bytes",  "measurements": [    {      "statistic": "VALUE",      "value": 148044128    }  ],  "availableTags": [    {      "tag": "area",      "values": [        "heap",        "nonheap"      ]    },    {      "tag": "id",      "values": [        "CodeHeap 'profiled nmethods'",        "G1 Old Gen",                ... other tags omitted ...      ]    }  ]}

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

Заключение

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

Вы можете поиграть с полным приложением, иллюстрирующим эти идеи, используя кодна GitHub.

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

Перевод Кросс-браузерное тестирование в Selenium

11.06.2021 02:20:41 | Автор: admin

В этой статье мы рассмотрим кросс-браузерное тестирование. Это тип тестирования, который проверяет, работает ли приложение так, как ожидается, в нескольких браузерах, операционных системах и устройствах. Мы можем проводить кросс-браузерное тестирование с помощью автоматизации и без нее. Сценарии автоматизированного тестирования могут быть написаны или созданы с помощью таких программ, как TestProject и Selenium.

К концу этой статьи вы узнаете об определении кросс-браузерного тестирования, его преимуществах и работе с ним в Selenium и TestProject.

Примечание: Код из этой статьи находится на GitHub здесь.

Что такое кросс-браузерное тестирование?

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

Возможно, сбой произошел из-за нашего тестового скрипта или приложения. Вы когда-нибудь пытались открыть веб-сайт с помощью Internet Explorer, но он не работал, а затем тот же сайт без проблем открывался в Chrome? Такие проблемы выявляются во время кросс-браузерного тестирования, поскольку данные из AUT отображаются по-разному в каждом браузере.

Преимущества кросс-браузерного тестирования

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

Я сосредоточусь на двух преимуществах кросс-браузерного тестирования:

  1. Время

  2. Тестовое покрытие

Время

Создание и выполнение индивидуального сценария тестирования (Test Script) для уникальных сценариев занимает много времени. Поэтому наши тестовые сценарии создаются с тестовыми данными для использования их комбинаций. Один и тот же сценарий тестирования может выполняться на Chrome и Windows для первой итерации, затем на Firefox и Mac для второй итерации, а затем на других сценариях для последующих итераций.

Это экономит время, поскольку мы создаем только один тестовый сценарий, а не несколько. Ниже приведены 2 фрагмента кода для загрузки и получения заголовка для страницы TestProject. Один пример - это кросс-браузерное тестирование, а другой пример содержит отдельные тестовые сценарии для трех браузеров (Chrome, Firefox и Edge).

package tutorials.testproject;import io.github.bonigarcia.wdm.WebDriverManager;import org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;import org.openqa.selenium.edge.EdgeDriver;import org.openqa.selenium.firefox.FirefoxDriver;import org.testng.annotations.Parameters;import org.testng.annotations.Test;public class CrossBrowserTesting {  WebDriver driver;  @Test  @Parameters ( {"BrowserType"} )  public void testExamplePageOnMultipleBrowsers (String browserType) {    if (browserType.equalsIgnoreCase("Chrome")) {      WebDriverManager.chromedriver().setup();      driver = new ChromeDriver();    }    else if (browserType.equalsIgnoreCase("Edge")) {      WebDriverManager.edgedriver().setup();      driver = new EdgeDriver();    }    else if (browserType.equalsIgnoreCase("Firefox")) {      WebDriverManager.firefoxdriver().setup();      driver = new FirefoxDriver();    }    driver.manage().window().maximize();    driver.get("https://example.testproject.io/web/index.html");    System.out.println(browserType + ": " + driver.getTitle());  }}
package tutorials.testproject;import io.github.bonigarcia.wdm.WebDriverManager;import org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;import org.openqa.selenium.edge.EdgeDriver;import org.openqa.selenium.firefox.FirefoxDriver;import org.testng.annotations.Test;public class IndividualBrowserTesting {  WebDriver driver;  @Test  public void testExamplePageOnMultipleBrowsersOnChrome () {    WebDriverManager.chromedriver().setup();    driver = new ChromeDriver();    driver.manage().window().maximize();    driver.get("https://example.testproject.io/web/index.html");    System.out.println("Chrome: " + driver.getTitle());  }  @Test  public void testExamplePageOnMultipleBrowsersOnFirefox () {    WebDriverManager.firefoxdriver().setup();    driver = new FirefoxDriver();    driver.manage().window().maximize();    driver.get("https://example.testproject.io/web/index.html");    System.out.println("Chrome: " + driver.getTitle());  }  @Test  public void testExamplePageOnMultipleBrowsersOnEdge () {    WebDriverManager.edgedriver().setup();    driver = new EdgeDriver();    driver.manage().window().maximize();    driver.get("https://example.testproject.io/web/index.html");    System.out.println("Chrome: " + driver.getTitle());  }}

Тестовое покрытие

Тестовое покрытие - это техника, которая определяет, что и в каком объеме покрывается в наших тестовых сценариях.

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

  • Что будет включено в наши сценарии тестирования, зависит от требований.

  • То, сколько охвачено в наших сценариях тестирования, зависит от браузеров и их различных версий.

Тестовое покрытие является эффективным мерилом для процесса тестирования. Однако 100% покрытие трудно обеспечить, и, скорее всего, функция ведет себя не так, как это обычно происходит в конкретной версии.

Как осуществить кросс-браузерное тестирование в Selenium?

Мы осуществляем кросс-браузерное тестирование в Selenium, используя его сетку (grid) или тестовые данные. Selenium Grid упрощает процесс, а тестовые данные используются в качестве исходных. С помощью Selenium Grid наши тестовые сценарии выполняются параллельно на нескольких удаленных устройствах. Команды отправляются клиентом удаленным экземплярам браузера.

Тестовые данные могут храниться в файле Excel, CSV, файле свойств, XML или базе данных. Мы также можем объединить TestNG с тестовыми данными для проведения тестирования на основе данных или кросс-браузерного тестирования. Для тестирования на основе данных аннотация DataProvider и атрибут dataProvider или атрибут dataProviderClass позволяют нашему тестовому сценарию получать неограниченное количество значений.

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

<suite name="Cross Browser Testing">    <test name = "Test On Chrome">        <parameter name = "BrowserType" value="Chrome"/>            <classes>                <class name = "tutorials.testproject.CrossBrowserTesting"/>            </classes>    </test>    <test name = "Test On Edge">        <parameter name = "BrowserType" value="Edge"/>        <classes>            <class name = "tutorials.testproject.CrossBrowserTesting"/>        </classes>    </test>    <test name = "Test On Firefox">        <parameter name = "BrowserType" value="Firefox"/>        <classes>            <class name = "tutorials.testproject.CrossBrowserTesting"/>        </classes>    </test></suite>
package tutorials.testproject;import io.github.bonigarcia.wdm.WebDriverManager;import org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;import org.openqa.selenium.edge.EdgeDriver;import org.openqa.selenium.firefox.FirefoxDriver;import org.testng.annotations.Parameters;import org.testng.annotations.Test;public class CrossBrowserTesting {  WebDriver driver;  @Test  @Parameters ( {"BrowserType"} )  public void testExamplePageOnMultipleBrowsers (String browserType) {    if (browserType.equalsIgnoreCase("Chrome")) {      WebDriverManager.chromedriver().setup();      driver = new ChromeDriver();    }    else if (browserType.equalsIgnoreCase("Edge")) {      WebDriverManager.edgedriver().setup();      driver = new EdgeDriver();    }    else if (browserType.equalsIgnoreCase("Firefox")) {      WebDriverManager.firefoxdriver().setup();      driver = new FirefoxDriver();    }    driver.manage().window().maximize();    driver.get("https://example.testproject.io/web/index.html");    System.out.println(browserType + ": " + driver.getTitle());  }}

В XML-файле тег параметра расположен на уровне теста. У нас есть возможность разместить тег на уровне тестового набора, на уровне теста или на обоих уровнях. Обратите внимание, что тег параметра имеет имя и значение с данными между двойными кавычками. Его имя, т.е. "BrowserType", передается тестовому сценарию через аннотацию @Parameters, а значение, т.е. "Chrome", передается в операторы if и else if.

Операторы if и else if устанавливают Chrome, Edge или Firefox. Каждый браузер получал команды от одного и того же тестового сценария после выполнения из XML-файла. Следующие результаты тестирования показывают, как успешно загружается страница TestProject, а консоль печатает уникальное имя браузера и заголовок страницы.

Кросс-браузерное тестирование в Selenium с помощью TestProject

OpenSDK / Закодированный тест

Существует 2 способа проведения кросс-браузерного тестирования с помощью TestProject. Мы можем использовать OpenSDK с открытым исходным кодом или AI-Powered Test Recorder. OpenSDK оборачивается в Selenium и поддерживает Java, C# или Python. Наши тестовые сценарии похожи на кросс-браузерное тестирование в Selenium с минимальными изменениями в коде и зависимостях. Мы должны включить зависимость TestProject для Maven или Gradle, импортировать драйверы браузера и передать токен.

<dependency>     <groupId>io.testproject</groupId>     <artifactId>java-sdk</artifactId>     <version>0.65.0-RELEASE</version> </dependency>
implementation 'io.testproject:java-sdk:0.65.0-RELEASE'
import io.testproject.sdk.drivers.web.ChromeDriver;import io.testproject.sdk.drivers.web.FirefoxDriver;import io.testproject.sdk.drivers.web.EdgeDriver;

AI-Powered Test Recorder

С помощью AI-Powered Test Recorder мы создаем новое веб-задание, затем выбираем несколько браузеров, таких как Chrome, Edge и Firefox. Тест в задании TestProject позволяет нам выбрать дополнительный источник данных CSV, если мы хотим выполнить тестирование на основе данных. Вот несколько скриншотов, показывающих шаги по выполнению кросс-браузерного тестирования и отчета.

Вот пошаговая демонстрация кросс-браузерного тестирования с помощью TestProject AI-Powered Test Recorder.

Выводы

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

Кросс-браузерное тестирование осуществляется с помощью Selenium и TestProject.

TestProject позволяет нам создавать собственные тестовые сценарии с использованием Java, C# или Python после добавления OpenSDK с открытым исходным кодом. OpenSDK является оберткой Selenium, поэтому он содержит команды Selenium плюс дополнительные команды из TestProject. Кроме того, мы можем использовать TestProject's AI-Powered Test Recorder для проведения кросс-браузерного тестирования. Это удобный процесс, который требует от нас только выбора браузера, который мы хотим использовать для кросс-браузерного тестирования.


Перевод статьи подготовлен в рамках курса "Java QA Engineer. Basic". Всех желающих приглашаем на двухдневный онлайн-интенсив Теория тестирования и практика в системах TestIT и Jira. На интенсиве мы узнаем, что такое тестирование и откуда оно появилось, кто такой тестировщик и что он делает. Изучим модели разработки ПО, жизненный цикл тестирования, чек листы и тест-кейсы, а также дефекты. На втором занятии познакомимся с одним из главных трекеров задач и дефектов Jira, а также попрактикуемся в TestIT отечественной разработке для решения задач по тестированию и обеспечению качества ПО.

Подробнее..

Морской бой на Java для новичков. Level 1

17.06.2021 10:17:44 | Автор: admin

Всем привет!

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

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

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

  2. Одновременно в игре могут участвовать только два человека.

  3. В самом начале игроки "представляются" - программа "спрашивает" (предлагает пользователям ввести), какие у них имена

  4. У каждого игрока есть своё поле - квадрат 10х10 клеток

  5. Затем игроки по очереди расставляют свои корабли. Как и в "бумажной" версии - каждый может поставить 4 однопалубных корабля, 3 двухпалубных, 2 трехпалубных и 1 четырёхпалубный.

  6. Корабли можно располагать только по горизонтали или по вертикали.

  7. Игроки не видят расположение кораблей друг друга.

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

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

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

    Второй вариант, если игрок не попал ни в какой корабль, то ход переходит второму игроку.

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

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

Как и в любом Java приложении нам потребуется класс (не умаляя общности назовём его Main), в котором будет объявлен, я думаю уже всем известный, метод main.

public class Main {public static void main(String[] args) {  //your code will be here}}

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

Опираясь на пункты 1-3 утвержденного сценария, реализуем функционал приложения, который будет предлагать игрокам ввести свои имена. Здесь нам придётся использовать класс java.util.Scanner, который умеет считывать введенные значения в консоли.

public class Main {    static Scanner scanner = new Scanner(System.in);    public static void main(String[] args) {        System.out.println("Player 1, please, input your name");        String player1Name = scanner.nextLine();        System.out.println("Hello, " + player1Name + "!");        System.out.println("Player 2, please, input your name");        String player2Name = scanner.nextLine();        System.out.println("Hello, " + player2Name + "!");    }}

Подробнее о коде:

В строке 2 создаем статичное свойство класса Main scanner.

Нестатический метод nextLine() класса Scanner (строки 6 и 11) обращается к консоли и возвращает строку, которую он еще не считывал.

После получения имени пользователей, программа выводит приветствие в консоль - "Hello, {username} !"

В консоли будем видеть следующее, если запустим код сейчас.

Player 1, please, input your nameEgorHello, Egor!Player 2, please, input your nameMaxHello, Max!

Поговорим о том, как мы будем отображать поле боя и заполнять его кораблями. Пожалуй, что наиболее логичным будет использование двумерного массива char[][] buttlefield. В нем мы будем отображать расположение кораблей. Договоримся, что удачное попадание в корабль противника будем отображать символом #. Неудачный выстрел будем помечать символом *. Таким образом изначально, массив будет проинициализирован дефолтовым для примитива char значением ('\u0000'), а в процессе игры будет заполняться символами # и *.

public class Main {static final int FILED_LENGTH = 10;    static Scanner scanner = new Scanner(System.in);public static void main(String[] args) {    System.out.println("Player 1, please, input your name");    String player1Name = scanner.nextLine();    System.out.println("Hello, " + player1Name + "!");    System.out.println("Player 2, please, input your name");    String player2Name = scanner.nextLine();    System.out.println("Hello, " + player2Name + "!");        char[][] playerField1 = new char[FILED_LENGTH][FILED_LENGTH];    char[][] playerField2 = new char[FILED_LENGTH][FILED_LENGTH];    char[][] playerBattleField1 = new char[FILED_LENGTH][FILED_LENGTH];    char[][] playerBattleField2 = new char[FILED_LENGTH][FILED_LENGTH];}}

Подробнее о коде:

В строке 2 мы создаем константу FIELD_LENGTH, которая будет содержать размер поля - согласно требованию 4 - проинициализируем FIELD_LENGTH значением 10.

В строках 14-18 создаем двумерные массивы char. playerFiled1 и playerField2 - массивы, в которые будем записывать расположение кораблей каждого игрока. Размеры массивов равны размерам поля - логично, ведь эти двумерные массивы фактически отображают поля.

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

public class Main {    static final int FILED_LENGTH = 10;    static Scanner scanner = new Scanner(System.in);    public static void main(String[] args) {        System.out.println("Player 1, please, input your name");        String player1Name = scanner.nextLine();        System.out.println("Hello, " + player1Name + "!");        System.out.println("Player 2, please, input your name");        String player2Name = scanner.nextLine();        System.out.println("Hello, " + player2Name + "!");        char[][] playerField1 = new char[FILED_LENGTH][FILED_LENGTH];        char[][] playerField2 = new char[FILED_LENGTH][FILED_LENGTH];        fillPlayerField(playerField1);        fillPlayerField(playerField2);    }}private static void fillPlayerField(char[][] playerField) {// your code will be here}

Метод fillPlayerField должен быть статическим (static), так как вызываться он будет из метода main, который по определению должен быть статическим. fillPlayerField не будет возвращать никакого значения (void). В этом методе будет реализована логика по получению координат корабля от пользователя и запись в массив playerField нового корабля.

Поговорим о том, как пользователь будет задавать координаты кораблей. Будем размещать корабли по убыванию палуб, то есть первым разместим четырёхпалубный корабль, затем все трёхпалубные корабли, все двухпалубные корабли и все однопалубные. Программа будет принимать от пользователя координаты начала корабля и его положение (см. бизнес-требование 6) - по горизонтали или по вертикали.

Приведу вывод программы в консоль, который мы ожидаем увидеть после создания метода fillPlayerField:

Расставляем 4-палубный корабль. Осталось расставить: 1Input x coord: 1Input y coord: 11. Horizontal; 2. Vertical ?1

Наконец-то приступаем. На данный момент имеем:

private static void fillPlayerField(char[][] playerField) {    // your code will be here}

Нам нужно расставить корабли с палубами от 4 до 1. Здесь дело идёт к циклу. Предлагаю без лишнего пафоса использовать for. Заметим, кораблей с одинаковым числом палуб может быть несколько, поэтому нам нужно ещё как-то контролировать, чтобы пользователь мог разместить лишь определенное число кораблей с заданным количеством палуб (см. бизнес требование 5) - эта задача также эффективно решается с помощью цикла - без пафоса также используем for.

private static void fillPlayerField(char[][] playerField) {// i - счётчик количества палуб у корабля    // начинаем расстановку с корабля, которого 4 палубы, а заканчиваем кораблями с одной палубой    for (int i = 4; i >= 1; i--) {      // см. подробнее о коде под этой вставкой    for (int k = i; k <= 5 - i; k++) {          System.out.println("Расставляем " + i + "-палубный корабль. Осталось расставить: " + (q + 1));        // some your code here        }    }}

Подробнее о коде:

На 5 строчке мы создаём цикл for (int k = 0; k <= 5 - i; k++). Объясню, откуда такая магия. Нам нужно как-то понимать, сколько кораблей каждого типа (с определенным количеством палуб) пользователь может поставить.

Мы можем создать еще один двумерный массив, в котором мы захардкодим что-то в духе:

int[][] shipTypeAmount = {{1, 4}, {2, 3}, {3, 2}, {4, 1}};

Мы бы в первом цикле (см. строку 4) перебирали значение количества палуб, потом искали бы в двумерном массиве, какое количество кораблей соответствует данному типу. На мой взгляд, такое решение качественное, однозначно, если бы мы писали коммерческую программу, то сделали бы именно так, но сейчас такая красота выглядит сложной - оставим это как задачу со звёздочкой, для тех, кто захочет "причесать" свой код.

Я же предлагаю отметить особенность, что сумма количества кораблей и количества палуб - величина постоянная. Действительно, 1 + 2 = 5; 2 + 3 = 5; 3 + 2 = 5; 4 + 1 = 5 . Поэтому, зная количество палуб у корабля (зная тип корабля), мы можем посчитать, сколько кораблей такого типа может быть установлено одним играком. Например, 5 - 1 = 4 - таким образом, каждый игрок может поставить 4 однопалубных корабля. В цикле for на строке 6 реализована проверка условия цикла "лайтовым" способом - на основе этого интересного свойства.

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

private static void fillPlayerField(char[][] playerField) {        for (int i = 4; i >= 1; i--) {            // растановка самих кораблей            for (int k = i; k <= 5 - i; k++) {                System.out.println("Расставляем " + i + "-палубный корабль. Осталось расставить: " + (q + 1));                System.out.println("Input x coord: ");                x = scanner.nextInt();                System.out.println("Input y coord: ");                y = scanner.nextInt();                System.out.println("1 - horizontal; 2 - vertical ?");                position = scanner.nextInt();                // если корабль располагаем горизонтально              if (position == 1) {                    // заполняем '1' столько клеток по горизонтали, сколько палуб у корабля                    for (int q = 0; q < i; q++) {                        playerField[y][x + q] = '1';                    }                }                            // если корабль располагаем вертикально                if (position == 2) {                  // заполняем столько клеток по вертикали, сколько палуб у корабля                    for (int m = 0; m < i; m++) {                        playerField[y + m][x] = '1';                    }                }              // печатаем в консоли поле игрока, на котором будет видно, где игрок уже поставил корабли              // о реализации метода - см. ниже                printField(playerField);            }        }    }

Подробнее о коде:

Корабль помечаем символом '1' столько раз, сколько палуб он имеет - если корабль четырёхпалубный, то он займёт 4 клетки - помечаем 4 клетки значением '1'.

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

static void printField(char[][] field) {        for (char[] cells : monitor) {            for (char cell : t) {              // если значение дефолтовое (в случае char - 0), значит в данной клетке              // корабль не установлен - печатаем пустую клетку                if (cell == 0) {                    System.out.print(" |");                } else {   // если клетка непустая (значение отличается от дефолтового),                  //тогда отрисовываем сожержимое клетки (элемента массива)                    System.out.print(cell + "|");                }            }            System.out.println("");            System.out.println("--------------------");        }    }

На экране метод будет так отображать расстановку кораблей:

 | | | | | | | | | |-------------------- |1|1|1|1| | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |--------------------

Вот игроки уже заполнили свои корабли на карте, и теперь мы можем приступить к реализации пунктов 8-10 бизнес-требований заказчика.

Логику по получению от пользователя координат выстрела, обработки выстрела и передачи хода опишем в методе playGame. Дабы придерживаться (или пока только стараться) принципа single responsobility - не забываем делить логику на методы (1 функциональность - 1 метод, но тоже держим себя в руках, код, в котором 100500 однострочных методов тоже не комильфо) - примерно из этих соображений получились еще методы handleShot и isPlayerAlive. Реализация обоих приведена ниже

/*** Метод реализует логику игры: выстрел и передача хода.*/private static void playGame(String player1Name, String player2Name, char[][] playerField1, char[][] playerField2) {        // "карты" выстрелов - создаём двумерные массивы, которые содержат все выстрелы  // удачные (#) и неудачные (*)  char[][] playerBattleField1 = new char[FILED_LENGTH][FILED_LENGTH];        char[][] playerBattleField2 = new char[FILED_LENGTH][FILED_LENGTH];  // вспомогательные переменные, которым будут присваиваться значения текущего игрока -   // игрока, чья очередm делать выстрел. Сначала играет первый игрок, прошу прошения  // за тавтологию        String currentPlayerName = player1Name;        char[][] currentPlayerField = playerField2;        char[][] currentPlayerBattleField = playerBattleField1;  // внутри цикла происходит смена очередности игроков, выстрел, его обработка.  // код внутри цикла выполняется до тех пор, пока "живы" оба игрока - пока у двух игроков  // "частично" цел (ранен) ещё хотя бы один корабль        while (isPlayerAlive(playerField1) && isPlayerAlive(playerField2)) {          // принимаем от пользователя координаты выстрела            System.out.println(currentPlayerName + ", please, input x coord of shot");            int xShot = scanner.nextInt();            System.out.println(currentPlayerName + ", please, input y coord of shot");            int yShot = scanner.nextInt();          // обрабатываем выстрел и получаем возвращаемое значение метода handleShot            int shotResult = handleShot(currentPlayerBattleField, currentPlayerField, xShot, yShot);            // если выстрел неудачный, и не один корабль не повреждён, то очередь переходит к следующему игроку          if (shotResult == 0) {                currentPlayerName = player2Name;              currentPlayerField = playerField1;              currentPlayerBattleField = playerBattleField2;            }        }    }/**    * Метод обрабатывает выстрел. Если выстрел удачный, то есть снаряд достиг цели -    * в клетку записывается значение '#' (отображается к в массиве игрока, так и в массиве соперника),    * а также на экран выводится сообщение 'Good shot!'. В этом случае метод возвращает значение 1.    * В случае неудачного выстрела - в массив battleField записывается значение '0' в элемент [y][x], и    * и возвращается значение 0.    * Возвращаемые значения нам нужны для того, чтобы в методе, внутри которого вызывается метод handleShot,    * мы могли понимать, успешно или неуспешно прошёл выстрел. На основе этого мы принимаем решение, * переходит ход к другому игроку или нет.    */    private static int handleShot(char[][] battleField, char[][] field, int x, int y) {        if ('1'.equals(field[y][x])) {            field[y][x] = '#';            battleField[y][x] = '#';            System.out.println("Good shot!");            return 1;        }        battleField[y][x] = '*';        System.out.println("Bad shot!");        return 0;    }/***Метод определяет, не проиграл ли еще игрок. Если у игрока остался хотя бы    * один "раненный" корабль, тогда пользователь продолжает игру.    * То есть, если на карте у игрока остался хотя бы один символ '1', которым мы отмечали    * корабли, то игра продолжается - возвращается значение true. Иначе false.*/    private static boolean isPlayerAlive(char[][] field) {        for (char[] cells : field) {            for (char cell : cells) {                if ('1' == cell) {                    return true;                }            }        }        return false;    }

Думаю, что к комментариям в коде мне добавить нечего. Единственное, обращу внимание на тонкий момент. Мы привыкли в математике к записи (x, y) - где первой идёт координат абсцисс, а второй - координата ординат. Казалось бы, чтобы обратиться к элементу двумерного массива (иногда срываюсь и называю в тексте элемент клеткой, но суть не меняется) нужно написать arr[x][y], но это будет неверно, и чтобы это доказать воспользуемся неопрвергаемым методом пристального взгляда. Для примера рассмотрим следующий двумерный массив:

int[][] arr = {{1, 2}, {7, 4}, {8, 3, 5, 9}, {1}}System.out.println(arr[0][1]); // ?System.out.println(arr[1][0]); // ?

Теперь вопрос из квиза "Программирование и мир" - что выведется на консоль в строках 3 и 4?
Вспоминаем, что двумерный массив - это не совсем таблица (нам так проще его воспринимать и детектировать его в задачах) - это "массив массивов" - вложенные массивы. Если в одномерных целочисленных массивах элементом является целое число, то в случае двумерного массива - элементом является массив (а в случае трёхмерного массива - элементом является двумерный массив). Таким образом, первый индекс указывает, какой по счёту массив мы выбираем. Второй индекс указывает, какой элемент по счёту мы выбираем в выбранном ранее массиве. Запись arr[1][2] указывает, что мы обращаемся к элементу с индексом 2 (то есть 3 по порядку) в массиве с индексом 1 (или второму по порядку). Соответсвенно, в строке 3 в консоль выведется значение 2, а в строке 4 - 7.

Постепенно подбираемся к концу. Что нам осталось реализовать?

  1. Вывод имени победителя

  2. Проверка клетки, которую пользователь указал как начало корабля

Первое кажется проще, стартанём с него. Потопали в метод playGame - как вы помните, там есть цикл while, в условии которого есть проверка - живы ли еще оба игрока. Напомню, что если игрок "мёртв", то есть у него не осталось ни одного корабля, то игра прекращается, а выживший игрок считается победителем. Собственно, единственное, что добавилось - строчка 36 - вызов метода System.out.println()

/*** Метод реализует логику игры: выстрел и передача хода.*/private static void playGame(String player1Name, String player2Name, char[][] playerField1, char[][] playerField2) {// "карты" выстрелов - создаём двумерные массивы, которые содержат все выстрелы    // удачные (#) и неудачные (*)    char[][] playerBattleField1 = new char[FILED_LENGTH][FILED_LENGTH];    char[][] playerBattleField2 = new char[FILED_LENGTH][FILED_LENGTH];    // вспомогательные переменные, которым будут присваиваться значения текущего игрока -     // игрока, чья очередm делать выстрел. Сначала играет первый игрок, прошу прошения    // за тавтологию    String currentPlayerName = player1Name;    char[][] currentPlayerField = playerField2;    char[][] currentPlayerBattleField = playerBattleField1;    // внутри цикла происходит смена очередности игроков, выстрел, его обработка.    // код внутри цикла выполняется до тех пор, пока "живы" оба игрока - пока у двух игроков    // "частично" цел (ранен) ещё хотя бы один корабль    while (isPlayerAlive(playerField1) &amp;&amp; isPlayerAlive(playerField2)) {      // перед каждым выстрелом выводим в консоль отображение всех выстрелов игрока      printField(currentPlayerBattleField);        // принимаем от пользователя координаты выстрела        System.out.println(currentPlayerName + ", please, input x coord of shot");        int xShot = scanner.nextInt();        System.out.println(currentPlayerName + ", please, input y coord of shot");        int yShot = scanner.nextInt();        // обрабатываем выстрел и получаем возвращаемое значение метода handleShot        int shotResult = handleShot(currentPlayerBattleField, currentPlayerField, xShot, yShot);        // если выстрел неудачный, и не один корабль не повреждён, то очередь переходит к следующему игроку          if (shotResult == 0) {            currentPlayerName = player2Name;              currentPlayerField = playerField1;              currentPlayerBattleField = playerBattleField2;        }    }  System.out.println(currentPlayerName + " is winner!");}

Переходим ко второму пункту наших "остатков" - реализуем проверку клетки, которую указал пользователь - начало корабля.

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

Окрестность с радиусом одна клетка. Красным кругом помечена стартовая клетка - начало корабля.Окрестность с радиусом одна клетка. Красным кругом помечена стартовая клетка - начало корабля.

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

Какие нюансы есть в этом алгоритме? Снова посмотрим на рисунок.

Неэффективность нашего простого алгоритма. Некоторые клетки проверяем несколько раз.Неэффективность нашего простого алгоритма. Некоторые клетки проверяем несколько раз.
private static int validateCoordForShip(char[][] field, int x, int y, int position, int shipType) {        // если пользователь хочет расположить корабль горизонтально  if (position == 1) {            for (int i = 0; i < shipType - 1; i++) {if ('1' == field[y][x + i]                                || '1' == field[y - 1][x + i]                                || '1' == field[y + 1][x + i]                                || '1' == field[y][x + i + 1]                                || '1' == field[y][x + i - 1]|| (x + i) > 9) {                    return -1;                }            }        } else if (position == 2) {          // если пользователь хочет расположить корабль вертикально            for (int i = 0; i < shipType - 1; i++) {                if ('1' == field[y][x + i]                        || '1' == field[y - 1][x + i]                        || '1' == field[y + 1][x + i]                        || '1' == field[y][x + i + 1]                        || '1' == field[y][x + i - 1]|| (y + i) > 9) {                    return -1;                }            }        }        return 0;    }

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

private static void fillPlayerField(char[][] playerField) {        for (int i = 4; i >= 1; i--) {            // растановка кораблей            for (int k = i; k <= 5 - i; k++) {                System.out.println("Расставляем " + i + "-палубный корабль. Осталось расставить: " + (q + 1));              // иницализируем переменную начальным значением              int validationResult = 1;            while (validationResult != 0) {              System.out.println("Input x coord: ");            x = scanner.nextInt();            System.out.println("Input y coord: ");            y = scanner.nextInt();            System.out.println("1 - horizontal; 2 - vertical ?");            position = scanner.nextInt();                  // если координата не прошла валидацию (проверку), то метод возвращает отрицательное// значение, конечно, оно не равно нулю, поэтому пользователю придётся ввести координаты                  // ещё раз                  validationResult = validateCoordForShip(playerField, x, y, position, i);                }            // если корабль располагаем горизонтально              if (position == 1) {                // заполняем '1' столько клеток по горизонтали, сколько палуб у корабля                for (int q = 0; q < i; q++) {                    playerField[y][x + q] = '1';                }            }                        // если корабль располагаем вертикально            if (position == 2) {                  // заполняем столько клеток по вертикали, сколько палуб у корабля                for (int m = 0; m < i; m++) {                    playerField[y + m][x] = '1';                }            }              // печатаем в консоли поле игрока, на котором будет видно, где игрок уже поставил корабли              // о реализации метода - см. ниже            printField(playerField);        }    }}

Вот мы и написали игру "Морской бой" - level 1. Начали с простого - простых конструкций, идей и методов. Один класс, нет коллекций - только массивы. Оказывается на циклах и массивах можно написать игру.

Мы удовлетворили все требования бизнеса. Доигрывая до конца, получили отличную оценку от заказчика, он полностью доволен приложением. Ждём, когда он опробует игру и вернётся снова за апгрейдом. А тут и будет level - 2.

Всем спасибо, всегда рад обратной связи!

Подробнее..

Категории

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

© 2006-2021, personeltest.ru