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

Java

Войти в IT после 30 через Java

27.11.2020 00:13:20 | Автор: admin

Всем ку!

Эта статья является текстовой адаптацией одного из самых популярных интервью на youtube-канале "АйТиБорода" - интервью про Java (более полумиллиона просмотров). Если кто-то не знает, на этом канале несколько раз в месяц появляются интервью с айтишниками о технологиях, ЯП и персоналиях.

Приятного прочтения!

Привет, Рома! Расскажи, где ты учился и как вообще попал в IT?

У меня на самом деле два образования: одно незаконченное высшее, второе законченное. Законченное высшее техническое, но абсолютно никак не было связано с компьютером.

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

А в каком году ты в университет поступал?

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

Ну и решил: дай попробую себя. Может быть, какие курсы. Ну, хобби себе какое-нибудь найду. Открыл ноут, и мне в принципе было просто всё равно: парапланирование, вязание крестиком, вышивание без разницы что. Но так уж получилось, что тогда шла довольно активная агитация IT-Академии (одна из школ в Беларуси). Вот везде эта всплывающая контекстная реклама была. И как раз всплыло в тот день что-то типа: Хочешь зарабатывать миллион миллиардов? Приходи к нам, мы тебя научим! Я решил: почему бы не совместить приятное с полезным? Миллион миллиардов всё-таки заманчиво звучало. Вот так я и попал в IT.

Я решил: почему бы не совместить приятное с полезным? Миллион миллиардов всё-таки заманчиво звучало. Вот так я и попал в IT.

Просто пришёл на курсы. Как-то отучился. Причём первый курс в академии действительно именно как-то отучился. Еле-еле окончил. Из нашей группы сертификаты получили человека, по-моему, четыре из 12.

А откуда желание что-то поменять вообще появилось?

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

Короче, это была запланированная смена работы?

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

Нашёл курс и сразу пошёл на Java? Это осмысленный выбор?

Это было похоже на то, как я в принципе решил попасть в IT. Шёл набор на курсы с таким достаточно абстрактным объявлением: Хочешь стать программистом? Зашёл к ним на сайт, и на той неделе стартовало много курсов: JavaScript, Python, PHP.

Тогда я думал, что PHP это круто. Я с ним ещё когда-то там в школе и на первом курсе более-менее работал: какие-то сайтики делали. Подумал: ну вот тут я, наверное, что-то буду понимать. Потом смотрю JavaScript. Думаю, ну про JavaScript я слышал: там HTML, CSS, JavaScript ну вот слова из одной области какие-то, думал. Но эти курсы шли 11-13 дней от того дня, когда прочёл само объявление. А Java стартовал через три дня. Решил: ну попробую, может быть, есть место в группе. Позвонил, и, действительно, место в группе было. Причём одно. Вот так. Поэтому Java (смеётся).

Страха от такой кардинальной смены рабочей атмосферы не было? Многие боятся идти в программирование: думают, что там много математики нужно, что это сложно. Было у тебя такое?

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

Когда в первый раз написал свою первую программу Hello, world!, понял: ну, всё просто, теперь я умею программировать.

Думал тогда, что пойдёшь работать программистом? Или просто по приколу пошёл на эти курсы?

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

Наверное, с занятия пятого, когда я уже первый свой цикл написал, калькулятор был, он алгоритм какой-то простенький считал, просто запустил эту программу, шифтов 10-12. Мне не надо было мышкой нажимать на этот значок play зелёный в IDEA. И оно что-то мне посчитало и выдало на экран. Числа там рандомно генерились, и я заранее не знал, какой ответ будет. И тогда я решил: ну, а почему бы и нет? То есть я могу рандомно генерировать что-то. Если я здесь испортил программу, я могу портить программу и в коммерческих целях. Как-то так и повелось (смеётся).

Если я здесь испортил программу, я могу портить программу и в коммерческих целях. Как-то так и повелось.

Сколько времени прошло со старта курсов до трудоустройства?

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

Что занимало тебя весь этот год? Ты же, наверное, не только Java изучал?

Понятное дело, что это не сухое изучение языка везде нужны какие-то паттерны, какие-то решения. Если просто писать код, то это будет мало чем отличаться от обычной автогенерации того же кода в IDEA. Книжки читал. Есть, например, такая книга отличная Приёмы объектно-ориентированного проектирования. Паттерны проектирования, Банда четырёх (Gang of Four) написала.

Я пытался их (паттерны) понять. Мне сказали, что это отличная книга. Старшие коллеги говорят: Почитай будет полезно. Но эту книжку, я думаю, нужно было читать хотя бы после года, чтобы понимать, о чём там вообще написано. Но мне сказали, что будет полезно. И я её прочитал, но не всю. Наверное, только треть осилил. Понял, что дальше мне будет понятно ровно столько, сколько и сейчас. То есть ничего.[5]

Из года обучения сколько заняли сами курсы? Весь год ходил на курсы и самообразовывался?

Шесть месяцев заняли курсы. Когда закончил IT-Академию, у меня была конкретная цель попасть в EPAM. Мне предлагали какие-то маленькие компании, типа на стажировки, а потом на трудоустройство. Но вот у меня знакомые работали в EPAM. И я много слышал о том, что это хорошая компания для становления: понимать сами процессы, бизнес-процессы. И да, там действительно довольно строго поставлены именно эти сами методологии. То есть цель была попасть именно туда.

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

Но в итоге ты в лабу попал?

Да, ну после их внутренних курсов я попал в лабораторию.

И уже через лабу твоей первой работой стала EPAM?

Да.

Чем ты там занимался и сколько в целом проработал?

В EPAM проработал где-то два с половиной года. Сразу попал в самый большой проект Thomson Reuters. Раньше это был просто такой проект на EPAM. Потом всё переросло в самый большой юнит. А сейчас Thomson Reuters самый крупный заказчик. И так получилось, что я ещё попал в их самый крупный проект. Я даже и не скажу, сколько он уже пишется, сколько он в уже в активной разработке. Но когда я туда пришёл, я понял ещё меньше, чем я понимал на курсах и в лаборатории. Там всё было какое-то своё. Мне сказали, что там будут классные технологии, стек такой большой и фронт, и бэк, и Spring, и Hibernate, и EclipseLink есть.

Короче, всё, что хочешь: облако, микросервисы?

Да. А когда ты только-только начинаешь, написал первое Hello, world?, и думаешь: Таак, чтобы дальше изучать? Ну, наверное, machine learning!

Написал первое Hello, world?, и думаешь: Таак, чтобы дальше изучать? Ну, наверное, machine learning!

Или сразу ракету запущу на Марс?

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

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

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

Это потом мне, естественно, сказали, что нужен будет английский. А изучать его мне особо негде было. Потому что подтягивать сразу и техническую часть, и часть с английским языком проблематично. Высасывает очень много энергии. Я пытался просто книжечки читать и программировать. Если в одном шло хорошо, то в другом уже не так хорошо. Поэтому я просто перевёл весь свой environment на английский язык, насколько это было возможным. Слава богу, у меня тогда уже был смарт-телевизор. Он перешёл на английский. И все программы, и плейлисты. Телефон тоже перевёл на английский. Литература тоже по максимуму смотрелась на английском. Ну и, понятное дело, что из Stack Overflow это наше всё тоже всё шло на английском.

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

Сколько тебе было, когда начал интенсивно обучаться программированию?

Мне было 28-29 наверное, 30 лет.

Не было какого-то хейта в духе: Куда ты попёрся? Как к решению отнеслись родственники и друзья?

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

А вот на работе коллеги В 18:00 часов заканчивалась работа, а в 18:30 начинались курсы нужно было как раз полчаса, чтобы доехать. Вот там было много негатива. Мне говорили, что не получится: Ну 30 лет. У тебя не тот склад мышления, там нужно образование вот эти все стереотипы, что нужна математика, теория алгоритмов.

Можешь показать им фак!

Да! (смеётся). Ну вот не тот склад ума, и вряд ли мне что-то это даст. Говорили, что курсы напрасная трата денег и времени, что лучше бы я продолжал работать на прежнем месте. Я даже в какой-то момент к этим словам прислушался и пошёл по собеседованиям по своему предыдущему профилю. И слава богу, что меня тогда как раз кризис стукнул: в тот момент было очень тяжело найти работу многим контракты не продлевали. В поисках работы прошатался по рынку в целом где-то месяца два. Меня не брали.

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

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

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

Ну да, прилично.

Были из-за этого опасения, что не туда идёшь?

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

Что-то писал и понимал, что я делаю. Ну, начинал понимать, что я делаю. Появлялся интерес. У меня и сейчас нет предпочтений в плане там на $500 больше платят всё, надо идти туда. Абсолютно нет. Если работа интересна, коллектив хороший, то смысл её менять? По зарплатам в IT когда-то достиг психологической планки: если ниже, то будет какой-то дискомфорт, но тем не менее

Сейчас HR-ры на собеседованиях очень часто спрашивают: Что у вас на первом месте: зарплата, коллектив, проект, технологии? И вот изначально да, вопрос зарплаты был сверху, а потом опускался всё ниже и ниже. Мне настолько уже незначительно, слава богу. Но поначалу было дискомфортно, потому что зарплата отличалась не на, а в раз пять, наверное, в шесть. Раньше доход от сезона зависел я в торговле работал. Поэтому если там всё хорошо в сезон, тогда раз в пять.

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

Хочешь сказать, что эйджизм сказывается на рейте?

Не знаю. Я думаю, что, возможно, и так.

То есть каких-то конкретных примеров на своём опыте ты не ощутил?

Я как-то и не пытался особенно вдаваться вот именно в рамках Java.

За два с половиной года в EPAM-e до кого успел дослужиться? Почему сменил работу?

В EPAM-е есть строгая система рейтингов. Вот у меня был d2 это Middle. Потом перешёл на проект и стал вроде как d2 key developer.

У тебя d2 было с ходу после лабы?

Понятное дело, нет. Первая возможность получить d2 появилась где-то через месяцев пять, но я пошёл получать только через месяцев девять. В EPAM очень интересная система: когда появляется возможность перейти на d2, можно было получить промоушен по тайтлу. Свой, скажем так, рейтинг. И к этому приурочивался промоушен финансовый. Наслушавшись своих старших коллег, сначала по максимуму получал промоушен финансовый: где-то технологии выучил, курсы закончил. Потом шёл на d2, к которому также прилагался промоушен.

Лайфхак?

Не знаю, насколько лайфхак, но меня как бы устраивало (смеётся). А ушёл просто потому, что проект Thomson Reuters это большущий такой юнит. Очень тяжело было развернуться. Он давно был запущен, архитектура строго выстроена. И добавление новой функциональности было минорным, мало отличалось от уже существующей. Грубо говоря, полмодуля берёшь, копируешь и просто немножечко рефакторишь, меняя название.

EPAM же большая, почему не перешёл из одного проекта в другой в рамках одной компании?

Проект Thomson Reuters располагался прямо через дорогу от моего дома. Если нужно было к 9:00 на работу, то в 8:55 я выходил из дома: по переходу и в бизнес-центр, где мы и располагались. И это определённый отпечаток накладывает: уже не хотелось куда-то час ездить. Это первая причина.

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

Мне был интересен Spring, Hibernate, но это стандартный стек. Плюс там по фронтенду можно было Angular какой-то себе выбрать. И вот так было примерно во всех проектах на Thomson Reuters. Это я уже потом понял, что так оно везде, но тогда искал чего-то такого прямо нового-нового и поэтому ушёл.

Следующее место работы Softeco. Долго его искал? Сколько собеседований прошёл, прежде чем устроился? По факту была первая полноценная сессия собеседования в IT?

Нет, первая полноценная сессия собеседования была, опять же, для промоушена. К тому моменту в планах была свадьба. Мне уже не хотелось откладывать 80-90% зарплаты. Хотелось, чтобы было как-то посвободнее. Ну потому что и кушать хочется, и свадьбу.

Мне уже не хотелось откладывать 80-90% зарплаты. Хотелось, чтобы было как-то посвободнее. Ну потому что и кушать хочется, и свадьбу.

Это в каком году?

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

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

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

То есть ты юлил?

Нет, я не юлил. Уже была идея сменить проект. Правда, вряд ли я там конкретную компанию рассматривал, но вот не исключал такую вероятность, если будет что-то действительно интересное. Иногда очень приятно просто пообщаться с HR-ом, с техническим специалистом. И вот ты уже понимаешь: а почему бы не сменить компанию? Здесь действительно может быть интересно и хорошо.

Какие бы ключевые точки выделил в своём становлении тем программистом, которым сейчас являешься?

Когда выбирал между фронтендом и бэкендом, тогда ещё заканчивал лабораторию, пошёл на первый свой проект Thomson Reuters русский. И первые три месяца был очень воодушевлён новым проектом, новыми людьми и тем, что в продакшене. Работал усердно, а потом понял: как тут усердно не работай, это всё будет достаточно долго. Потому что и процесс согласования долгий и коды review небыстрое дело. Хотелось найти себе увлечение, чтобы занять свободное время. И я решил посмотреть в сторону фронтенда.

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

Уже лучше, чем первый.

Ну, наверное. Я с первым вообще не имел дела. В лаборатории из нашей группы ушёл человек, который научился на Джаве и пошёл во фронтенд. Вот он тогда мне и сказал: Второй Angular можешь посмотреть. Я посмотрел и даже написал несколько своих приложений. И в целом интересно было. Тот же, грубо говоря, опыт TypeScript.

Но ты же остался в бэкенде?

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

Потом всё как-то устаканивается, и уже более-менее ориентируешься. И когда это устаканилось, решил: Так, у меня незаконченное дело осталось с фронтендом, с Angular. А он, Angular, в тот момент уже был четвёртый. Я вроде как не сильно понял, что произошло и думаю: дай опять спрошу у коллеги своего. Он говорит: Ну да, Angular это круто, но сейчас очень модно React.

И вот у меня диссонанс начал возникать. То есть год назад я начал учить Angular, вроде что-то из мира JavaScript, а потом мне говорят: Можно учить Angular четвёртый, если хочешь апгрейдить, но там уже есть React. Думаю: ну ладно, хорошо, React, может, что-нибудь ещё из этого? Говорят: Да! Вот сейчас как раз Nod.js, и на нём можно писать можно писать фронтенд, бэкенд. Короче, мне просто слов набросали и говорят: Вот сейчас это модно. Год назад я даже слов ещё таких не знал. Подумал: хочу туда, где немножечко поспокойнее. И как-то так отошёл от фронтенда. Именно тогда решил, что Java всё-таки нравится больше. Там всё как-то постабильнее.

~~~~~~~~~~~~~~~~~~~~~~~~

На этом всё. Спасибо за прочтение, друзья! А вот и полная полуторачасовая видео-версия интервью. Приятного просмотра :)

P.S. Кстати, Рома сейчас активно занимается помощью в переквалификации всех нуждающихся из числа пострадавших от репрессий в Беларуси. Респект, мужик!

Подробнее..

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

17.11.2020 12:21:43 | Автор: admin
Идти вперед туда, где не ждут; атаковать там, где не подготовились.
Искусство войны, Сунь-Цзы

На прошлой неделе я набросал простую математическую модель принятия решения и оценки того, пойдете вы на конференцию или нет. Для того, чтобы модель была наглядной, нам понадобятся Саша (в очках) и Женя (улыбается), вот они:



Математическая модель для конференции и участников


Давайте познакомимся с нашими героями поближе:


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

Впрочем, только ситхи всё возводят в абсолют, а в реальности такие Саша и Женя в природе встречаются очень редко. Выраженность этих признаков, как и всё в нашем мире, градиентно распределена среди всех участников (и даже неучастников!) конференций. Представьте себя на месте наших героев: у вас есть свой набор ожиданий от конференции, давайте возьмем их сумму за единицу, которую можно распределить между Сашей и Женей. Эту единицу мы назовем Business or Banquet Ratio (далее просто BOBR).



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


  1. Вот, например, с бокалом и без улыбки у нас будет СЕНЯ (Четверть от Саши, три четверти от Жени: 75% ради нетворкинга, 25% ради докладов). Кого-то притягивает атмосфера большого события и общество единомышленников, такие люди проводят время в кулуарах и на выставке, иногда заходя на пару интересующих докладов и проводя почти все время на выставке и в кулуарах со старыми и новыми знакомыми.
  2. Кто-то наоборот на три четверти ходит ради докладов, знаний и кругозора, однако эти знания можно получить, общаясь с интересными людьми, задавая вопросы докладчикам и коллегам, так что условная четверть ваших ожиданий будет зависеть не от самих докладов, а от того, кто их рассказывает и в каком круге вы их потом обсуждаете. Тут имя не получилось, к сожалению.
  3. Другие ходят на конференцию слушать доклады совместно с друзьями и коллегами, постоянно общаясь и обсуждая то, что удалось услышать на докладе или в кулуарах конференции, разделяя время на знания и общение пополам. Получается САНЯ! Как видите, модель выходит довольно складной

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



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


Давайте прикинем, где бы оказались наши конференции при таком подходе.


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


А теперь немного отвлечемся от математики и постараемся понять, что именно добавляет ценности по шкале Жени (общение и отдых) и обеспечивает разницу между онлайном и офлайном:



Ретроспективное исследование наших проведенных конференций говорит о том, что люди приходили на них ради образовательных и тусовочных (простите за мой французский) целей примерно в равной степени. То есть примерно вот так выглядел Joker 2019:



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


image А значит, BOBR по шкале Жени включает в себя следующие слагаемые:


  1. Встретиться с друзьями, которые приехали издалека или обычно слишком заняты, обсудить с коллегами насущные проблемы.
  2. Сделать селфи для инстаграма с крутыми инженерами, разработчиками любимых инструментов или автором настольной книги.
  3. Отдохнуть, сменить обстановку, перезагрузиться, просто прогуляться в толпе интеллигентных людей.
  4. Пособирать мерч на стендах компаний-спонсоров конференции.
  5. Найти новых знакомых из огромной толпы единомышленников и оппонентов в кулуарных спорах о технологиях и жизни.

image Что можно делать в онлайне:


  1. Потрепаться в чате конференции и чатах докладов.
  2. Пообщаться со спикером в Zoom-комнате доклада.
  3. Поучаствовать в вечеринке в общей Zoom-комнате конференции с теми, кто тоже хочет обсудить всякое.

А это значит, что онлайновый JPoint 2020 выглядел уже так:



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


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


Впрочем, есть еще один нюанс, посмотрите на графики с количеством участников в 2019 офлайне и 2020 онлайне:



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



А мы вот так:

Команда JUG Ru Group по окончании сезона онлайн-конференций выглядела как-то так:


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


Всё. Из-за этого наши онлайн-конференции оказываются в левой части системы координат с минимальной длиной отрезка BOBR:


Мы, как всегда, сделали хорошую программу и собрали спикеров (шкала Саши) на онлайновых JPoint и HolyJS, но это привело к тому, что они еле-еле переползли в зеленый сегмент из-за низкого индекса по шкале Жени.


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


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


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


Итак, задача ставилась так, чтобы Joker 2020 и HolyJS 2020 Moscow попали примерно сюда:


Ниже я расскажу, что как мы эту задачу решаем.


Что же делать?


С самого начала локдауна и пандемии многие организаторы по всему миру ушли в виртуальные пространства: митинги в Red Dead Redemption, конференции в Animal Crossing, митапы в Minecraft Мы тоже думали об этом: организация виртуального кинотеатра кажется достаточно тривиальной задачей до тех пор, пока не пытаешься ее скалировать до нужных масштабов:


  1. Несколько сотен или тысяч человек в онлайне;
  2. Хорошие звук и видео, в идеале 4К;
  3. Стабильность подключения, видео- и голосовой связи не только для спикеров, но и для участников;
  4. Интеграция контента и нетворкинга на единой платформе;
  5. Размещение на карте всех POI и партнеров.

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


За весну и лето мы упоролись и посмотрели несколько десятков онлайн-мероприятий и кучу готовых платформ, на которых они проходили и везде обнаруживались проблемы и ограничения в качестве звука/видеопотока, стабильности работы или разрозненности компонент (программа на сайте, доклады и панельные дискуссии в YouTube / Zoom, тусовка в Spatial Chat). Поэтому к лету мы делали просто понятный портал с хорошим плеером, встроенной программой и навигацией, прямыми ссылками в чаты и дискуссионные зоны.


В итоге мы решили первые три задачи из списка выше и оставили две на второй сезон, который идет уже сейчас. Что нам осталось? Правильно, сделать платформу интерактивной и интересной по шкале Жени. Для этого мы снова посмотрели на имеющиеся решения типа Gather Town и Spatial Chat и поняли, что с ними мы можем сделать полноценную онлайн-движуху, но остается последняя проблема: интеграция контентной и нетворкинговой частей.


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


Как мы решили задачу с нетворкингом


Поэтому мы что? Правильно! Запилили свой сервис со своей реализацией webRTC и игровыми механиками!


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


Вот классический плеер:



И выставка партнеров:



Или она же в игровом виде:



Давайте посмотрим, что мы сделали, чтобы нашим с вами Саше и Жене было интересно проводить время не только на докладах, но и гуляя по виртуальному пространству.


Логинимся


Логинимся? (гифку безбожно пожало, к сожалению)



Общаемся


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



Как видите, если подойти ближе, то внизу появляется видео собеседника (привет, vbrekelov!) и звук.


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


Отдыхаем


Карта как место проведения: единое пространство, где Женя, отдыхая от докладов, может проходить мини-квесты, общаться со спонсорами конференции и собирать лут (реальный, который потом вышлют к вам!).


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



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



Ищем друзей


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



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


Пробуем


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


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


Реклама как вывод


Как видите, в этом сезоне мы постараемся сделать наши конференции не только полезными, но и более веселыми, и это наш большой эксперимент, который состоится 25-28 ноября, на Java-конференции Joker 2020 и HolyJS 2020 Moscow (да-да, мы все еще называем конференции по городам, вот такие мы ретрограды).


Кроме того, будут еще DotNext, SmartData и DevOops (со 2-го по 12-е декабря), а если планируете отправиться на несколько конференций, то смотрите на Full Pass-билет, который даст вам доступ еще и к уже прошедшим конференциям этого сезона.


Присоединяйтесь, смотрите, хвалите, играйте и ругайте! А заодно вспомните и прикиньте, где бы вы расположили себя на графике с Сашей и Женей. ;)


Disclaimer: Давайте только не приходить в комментарии со словами, что офлайн был лучше. JUG Ru Group будут первыми в очереди на проведение старых добрых конференции, когда спикеры из Штатов и Европы смогут до нас доехать, люди не будут бояться жать друг другу руки, а риски отмены не будут кратно превышать вероятности проведения.

Подробнее..

Lamoda x Joker 2020

18.11.2020 16:23:31 | Автор: admin
Привет, Хабр! Меня зовут Влад Кошкин, я java-разработчик в Lamoda. С 25 по 28 ноября наша команда впервые примет участие в онлайн-конференции Joker 2020.

У Lamoda огромный и сложный склад: 40 000 м, миллионы товаров на полках, тысячи людей и все это мы автоматизируем на Java через WMS (Warehouse Management System).

image

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

Расписание активностей


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

imageКак мы подружили Kotlin и склад, а также другие технические потребности WMS Влад Кошкин, java-разработчик.

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


imageКак мы строим модульную архитектуру без микросервисов Женя Рябышев, java-разработчик.

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

imageБез лишних встреч: как мы позволяем разработчикам разрабатывать Костя Карусев, тимлид команды Acinonyx.

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

Мы будем на стенде три дня, приходите пообщаться с нами в перерывах конференции.

Байки со склада: Автоматизация

26 ноября 12:00 12:30

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

Байки со склада: Черная Пятница

27 ноября 18:00 18:30

Для e-commerce сегодня главный день в году Black Friday. В этот вечер будем травить байки о том, как мы обычно справляемся с пиковыми нагрузками.

Байки со склада: Внутренняя продуктовая разработка

28 ноября 12:00 12:30

Обсудим разницу между inhouse разработкой и софтом на продажу.

Квест: Расследуй самые сложные случаи на складе.


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

image

До встречи на Joker 2020!
Подробнее..

Joker 2020 продолжение сезона онлайн-конференций

29.11.2020 22:13:53 | Автор: admin
Только что, c 25 по 28 ноября 2020 года, прошла Java-конференция Joker 2020. Это уже второй сезон конференций, проводимых JUG Ru Group в формате онлайн.

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



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

В замечательной статье из блога компании JUG Ru Group на Хабре Руслан ARG89 Ахметзянов постарался проанализировать ситуацию (попробуйте оценить, Вы в большей степени персонаж Саша или Женя в отношении конференций). Далее там же анонсируются дополнительные механики, добавленные в стриминговую платформу конференций для того, чтобы удовлетворить вкусы как можно большего числа участников. Удалось или нет достигнуть этим поставленных целей, постараемся разобраться далее.

В преддверии конференции также вышло 8 выпусков шоу Вторая чашка кофе с Joker, в которых в эфире ведущие успели взять интервью с Алексеем Фёдоровым, Дмитрием Чуйко, Александром Белокрыловым, Дмитрием Александровым, Олегом Шелаевым, Сергеем Егоровым, Евгением Борисовым и Тагиром Валеевым.

Так что же, собственно, сама-то конференция?

Открытие


В проведение, открытие, закрытие каждой конференции организаторы раз за разом стараются привнести что-то новое. В данном случае открытие началось с импровизаций Алексея Фёдорова и Глеба Смирнова. На правом фото Алексей Фёдоров демонстрирует возможности игрового вида конференции (о нём рассказывается далее в отдельном разделе обзора).



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

  • интервью;
  • большие доклады;
  • мини-доклады партнёров;
  • воркшопы.

Интервью


В интервью Эволюция Java и Kotlin. Что нас ждет?, взятом у Романа Елизарова, можно было узнать о пути развития языка программирования Kotlin. Накануне конференции официально было объявлено о передаче управления и координации работ по проекту Kotlin от Андрея Бреслава к Роману Елизарову. По этой причине особенно интересно было узнать мнение Романа и про его изменившийся круг обязанностей, и о возможных изменениях развития языка и платформы.



Зачем нужно знание многопоточной разработки в enterprise мини-интервью Евгения phillennium Трифонова с Юрием Бабаком, представителем компании-партнёра конференции. Любопытными показались разнообразные примеры из собственной практики, про которые Юрий живо и интересно рассказал в ответ на очень уместные вопросы Евгения.

Адская кухня: Как приготовить новую версию Java и не отравить пользователей LTS релизов? мини-интервью с Александром Белокрыловым из компании BellSoft, хорошо известной, вероятно, большинству по дистрибутиву Liberica JDK. Новостью стала информация о вхождении представителей компании в исполнительный комитет JCP.



Доклады


Доклад Кирилла Тимофеева под названием JVM-профайлер, который смог (стать кроссплатформенным) был про добавление поддержки Windows в async-profiler при его использовании из среды разработки IntelliJ IDEA. Андрей Паньгин (поздравляем его с присвоением звания Java Champion за неделю до конференции!) выступил в качестве приглашённого эксперта доклада. Отличный докладчик (автор Windows-порта), хороший доклад с глубоким пониманием темы, идеальный эксперт (автор оригинального продукта), полезная информация о скором появлении предмета обсуждения в составе IntelliJ IDEA.



Предполагаю, что аболютное большинство видевших доклад и читающих данный обзор использует Spring Boot как-никак это промышленный стандарт Java-разработки сегодня. Толстый (fat) JAR при использовании Spring Boot также абсолютно распространённая практика. Рискну предположить, что Владимир Плизга со своим докладом Spring Boot fat JAR: Тонкие части толстого артефакта представил информацию, которая наиболее практически применима и востребована. Неплохо дополнили доклад три Андрея Беляев, Когунь и Зарубин.



Доклад Thread Safety with Phaser, StampedLock and VarHandle от легендарного Heinz Kabutz (ведущий известнейшей рассылки JavaSpecialists) и его коллеги John Green. Просмотр данного доклада может быть полезно тем, что в нём акцентируется внимание о менее известных concurrency-классах Phaser, StampedLock и VarHandle (в отличие от, например, многим знакомых классов CountDownLatch и CyclicBarrier).



Spring Patterns для взрослых в исполнении (сначала буквально даже в сопровождении гитары) Евгения Борисова. Ничто не помешало в итоге Жене (ни разряжающаяся батарея ноутбука, ни стремительно заканчивающееся время) успешно завершить доклад. В отличие от привычных докладчику стиля и тем технической жести, хардкора и расчленёнки (только библиотек и фреймворков. естественно) речь в этот раз шла как раз о наиболее типовом использовании Spring Framework. Ещё один абсолютно практически применимый доклад в блестящем исполнении с рекомендацией к просмотру.



Доклад Заменят ли роботы программистов? от Тагира Валеева расстроил меня вслед за докладчиком я тоже осознал, что роботы (библиотеки, сервисы, плагины) в значительной части уже заменили программистов. Частично успокаивает то, что ими автоматизируется наиболее неинтересная и рутинная часть работы программиста. Полезной и приятной частью в подобных докладах является информация о каких-то сервисах, которые можно будет попробовать после конференции. В случае доклада Тагира это информация о сервисах Mergify (есть приложение для GitHub) для автоматизации принятия pull request и сервис Diffblue (есть плагин для IntelliJ IDEA) для автоматизации создания unit-тестов (выглядит впечатляюще, надо попробовать). Полезный, интересный и даже неожиданно, не побоюсь этого слова, философский доклад.



Мини-доклады партнёров


На мой взгляд, мини-доклады партнёров очень удачная форма докладов, относительно коротких и информативных одновременно. Подводные камни загрузчиков классов в Java и как они могут повлиять на скорость работы с XML от Ильи Ермолина (слева) и Как сказать нет архитектору? Советы по выбору размера микросервиса в исполнении Андрея Даминцева (справа) являются примерами таких докладов.



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

После мини-доклада Самое время попробовать машинное обучение на Java у Артёма Селезнева (фото справа) взял интервью Евгений Трифонов. В какой-то степени были развенчаны мифы (или хотя бы изменено мнение) о слабой применимости Java для машинного обучения.



Воркшопы


Ещё одна замечательная форма донесения технической информации и ещё одная известная личность воркшоп (мастер-класс) Хватит писать тесты, пора писать спецификации! от Алексея Нестерова. Алексей в настоящий момент один из соведущих популярнейшего подкаста Радио-Т (т.н. Алексей второй и Алексей добрый, в отличие от Алексея Абашева).

Воркшоп на конференциях JUG Ru Group обычно разбит на две части и суммарно занимает один конференционный день, к чему надо быть готовым. Для демонстрации написания тестов использовался проект в репозитории (если используете Windows, то дополнительно придётся изменить две строчки в файле frontend/package.json). Высококвалифицированный приятный инструктор-докладчик, возможность спокойно покопаться в проекте на своём удобном привычном рабочем месте, рекомендую.



Сайт


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

Слева представлен общий план возможных локаций, справа локация Холл.



Далее локация Улица слева, Партнёрская выставка справа. При приближении к персонажам других людей внизу появляется видео рядом расположенных субъектов с возможностью диалога. Наверное, следующим шагом могут стать VR-шлемы и трёхмерное пространство выставки с видом от первого лица.



Просмотр информации о конференциях и игра


В обзорах предыдущих конференций уже рассказывалось про приложение, доступное на сайте jugspeakers.info (репозиторий с кодом находится на GitHub). Приложение состоит из двух частей:

  1. Просмотр информации о конференциях JUG Ru Group и JUG-митапах (с поиском данных о конференциях, спикерах, докладах, просмотром видео докладов и презентаций);
  2. Игра Угадай спикера.

Первая часть приложения была дополнена возможностью просмотра статистики по компании, к которым относятся спикеры на момент последнего доклада или в настоящий момент. По количеству сделанных докладов (так сказать, в командном зачёте) уверенно побеждает компания JetBrains. То есть в настоящий момент в данной компании работают спикеры, сделавшие в сумме наибольшее количество докладов в конференциях JUG Ru Group (с учётом и без учёта Java-митапов).



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



Java-код программы на 100% процентов покрыт тестами, для сбора информации о покрытии кода используется библиотека JaCoCo, для контроля покрытия тестами и качества кода сервисы Codecov и SonarCloud.

На конференции Heisenbug две недели назад Евгений Мандриков, ведущий разработчик проектов JaCoCo и SonarQube, проводил воркшоп Покрытие кода в JVM. Посмотреть видео воркшопа могут обладатели билета на конференцию Heisenbug или единого билета.

Закрытие


Открывавшие три дня назад Алексей Фёдоров и Глеб Смирнов и закрыли конференцию. Традиционный выход на сцену спикеров, участников программного комитета и организаторов вынужденно пока заменён демонстрацией слайдов с фото.



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

Всем до следующих Java-конференций!

Осенне-зимний сезон онлайн-конференций JUG Ru Group продолжится конференциями DotNext, DevOops (2-5 декабря 2020 года) и SmartData (9-12 декабря 2020 года). Можно посетить любую из конференций отдельно или купить единый билет на все восемь конференций сезона (пять уже прошедших и три оставшихся), видео докладов при этом доступны сразу же после завершения конференций.
Подробнее..

Из песочницы Обработка исключений в контроллерах Spring

15.11.2020 16:06:22 | Автор: admin

image


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


Изначально до Spring 3.2 основными способами обработки исключений в приложении были HandlerExceptionResolver и аннотация @ExceptionHandler. Их мы ещё подробно разберём ниже, но они имеют определённые недостатки. Начиная с версии 3.2 появилась аннотация @ControllerAdvice, в которой устранены ограничения из предыдущих решений. А в Spring 5 добавился новый класс ResponseStatusException, который очень удобен для обработки базовых ошибок для REST API.


А теперь обо всём по порядку, поехали!


Обработка исключений на уровне контроллера @ExceptionHandler


С помощью аннотации @ExceptionHandler можно обрабатывать исключения на уровне отдельного контроллера. Для этого достаточно объявить метод, в котором будет содержаться вся логика обработки нужного исключения, и проаннотировать его.


В качестве примера разберём простой контроллер:


@RestControllerpublic class Example1Controller {    @GetMapping(value = "/testExceptionHandler", produces = APPLICATION_JSON_VALUE)    public Response testExceptionHandler(@RequestParam(required = false, defaultValue = "false") boolean exception)            throws BusinessException {        if (exception) {            throw new BusinessException("BusinessException in testExceptionHandler");        }        return new Response("OK");    }    @ExceptionHandler(BusinessException.class)    public Response handleException(BusinessException e) {        return new Response(e.getMessage());    }}

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


А вот следующий метод handleException предназначен уже для обработки ошибок. У него есть аннотация @ExceptionHandler(BusinessException.class), которая говорит нам о том что для последующей обработки будут перехвачены все исключения типа BusinessException. В аннотации @ExceptionHandler можно прописать сразу несколько типов исключений, например так: @ExceptionHandler({BusinessException.class, ServiceException.class}).


Сама обработка исключения в данном случае примитивная и сделана просто для демонстрации работы метода по сути вернётся код 200 и JSON с описанием ошибки. На практике часто требуется более сложная логика обработки и если нужно вернуть другой код статуса, то можно воспользоваться дополнительно аннотацией @ResponseStatus, например @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR).


Пример работы с ошибкой:



Пример штатной работы:



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


Обработка исключений с помощью HandlerExceptionResolver


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


ExceptionHandlerExceptionResolver этот резолвер является частью механизма обработки исключений с помощью аннотации @ExceptionHandler, о которой я уже упоминал ранее.


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


Exception HTTP Status Code
BindException 400 (Bad Request)
ConversionNotSupportedException 500 (Internal Server Error)
HttpMediaTypeNotAcceptableException 406 (Not Acceptable)
HttpMediaTypeNotSupportedException 415 (Unsupported Media Type)
HttpMessageNotReadableException 400 (Bad Request)
HttpMessageNotWritableException 500 (Internal Server Error)
HttpRequestMethodNotSupportedException 405 (Method Not Allowed)
MethodArgumentNotValidException 400 (Bad Request)
MissingServletRequestParameterException 400 (Bad Request)
MissingServletRequestPartException 400 (Bad Request)
NoSuchRequestHandlingMethodException 404 (Not Found)
TypeMismatchException 400 (Bad Request)

Основной недостаток заключается в том что возвращается только код статуса, а на практике для REST API одного кода часто не достаточно. Желательно вернуть клиенту еще и тело ответа с описанием того что произошло. Эту проблему можно решить с помощью ModelAndView, но не нужно, так как есть так как есть способ лучше.


ResponseStatusExceptionResolver позволяет настроить код ответа для любого исключения с помощью аннотации @ResponseStatus.


В качестве примера я создал новый класс исключения ServiceException:


@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)public class ServiceException extends Exception {    public ServiceException(String message) {        super(message);    }}

В ServiceException я добавил аннотацию @ResponseStatus и в value указал что данное исключение будет соответствовать статусу INTERNAL_SERVER_ERROR, то есть будет возвращаться статус-код 500.


Для тестирования данного нового исключения я создал простой контроллер:


@RestControllerpublic class Example2Controller {    @GetMapping(value = "/testResponseStatusExceptionResolver", produces = APPLICATION_JSON_VALUE)    public Response testResponseStatusExceptionResolver(@RequestParam(required = false, defaultValue = "false") boolean exception)            throws ServiceException {        if (exception) {            throw new ServiceException("ServiceException in testResponseStatusExceptionResolver");        }        return new Response("OK");    }}

Если отправить GET-запрос и передать параметр exception=true, то приложение в ответ вернёт 500-ю ошибку:



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


Кастомный HandlerExceptionResolver позволит решить проблему из предыдущих примеров, наконец-то можно вернуть клиенту красивый JSON или XML с необходимой информацией. Но не спешите радоваться, давайте для начала посмотрим на реализацию.


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


@Componentpublic class CustomExceptionResolver extends AbstractHandlerExceptionResolver {    @Override    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {        ModelAndView modelAndView = new ModelAndView(new MappingJackson2JsonView());        if (ex instanceof CustomException) {            modelAndView.setStatus(HttpStatus.BAD_REQUEST);            modelAndView.addObject("message", "CustomException was handled");            return modelAndView;        }        modelAndView.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);        modelAndView.addObject("message", "Another exception was handled");        return modelAndView;    }}

Ну так себе, прямо скажем. Код конечно работает, но приходится выполнять всю работу руками: сами проверяем тип исключения, и сами формируем объект древнего класса ModelAndView. На выходе конечно получим красивый JSON, но в коде красоты явно не хватает.


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


@RestControllerpublic class Example3Controller {    @GetMapping(value = "/testCustomExceptionResolver", produces = APPLICATION_JSON_VALUE)    public Response testCustomExceptionResolver(@RequestParam(required = false, defaultValue = "false") boolean exception)            throws CustomException {        if (exception) {            throw new CustomException("CustomException in testCustomExceptionResolver");        }        return new Response("OK");    }}

А вот и пример вызова:



Видим что исключение прекрасно обработалось и в ответ получили код 200 и JSON с сообщением об ошибке.


Обработка исключений с помощью @ControllerAdvice


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


Разберём простой пример эдвайса для нашего приложения:


@ControllerAdvicepublic class DefaultAdvice {    @ExceptionHandler(BusinessException.class)    public ResponseEntity<Response> handleException(BusinessException e) {        Response response = new Response(e.getMessage());        return new ResponseEntity<>(response, HttpStatus.OK);    }}

Как вы уже догадались, любой класс с аннотацией @ControllerAdvice является глобальным обработчиком исключений, который очень гибко настраивается.
В нашем случае мы создали класс DefaultAdvice с одним единственным методом handleException. Метод handleException имеет аннотацию @ExceptionHandler, в которой, как вы уже знаете, можно определить список обрабатываемых исключений. В нашем случае будем перехватывать все исключения BusinessException.


Можно одним методом обрабатывать и несколько исключений сразу: @ExceptionHandler({BusinessException.class, ServiceException.class}). Так же можно в рамках эдвайса сделать сразу несколько методов с аннотациями @ExceptionHandler для обработки разных исключений.
Обратите внимание, что метод handleException возвращает ResponseEntity с нашим собственным типом Response:


public class Response {    private String message;    public Response() {    }    public Response(String message) {        this.message = message;    }    public String getMessage() {        return message;    }    public void setMessage(String message) {        this.message = message;    }}

Таким образом у нас есть возможность вернуть клиенту как код статуса, так и JSON заданной структуры. В нашем простом примере я записываю в поле message описание ошибки и возвращаю HttpStatus.OK, что соответствует коду 200.


Для проверки работы эдвайса я сделал простой контроллер:


@RestControllerpublic class Example4Controller {    @GetMapping(value = "/testDefaultControllerAdvice", produces = APPLICATION_JSON_VALUE)    public Response testDefaultControllerAdvice(@RequestParam(required = false, defaultValue = "false") boolean exception)            throws BusinessException {        if (exception) {            throw new BusinessException("BusinessException in testDefaultControllerAdvice");        }        return new Response("OK");    }}

В результате, как и ожидалось, получаем красивый JSON и код 200:



А что если мы хотим обрабатывать исключения только от определенных контроллеров?
Такая возможность тоже есть! Смотрим следующий пример:


@ControllerAdvice(annotations = CustomExceptionHandler.class)public class CustomAdvice {    @ExceptionHandler(BusinessException.class)    public ResponseEntity<Response> handleException(BusinessException e) {        String message = String.format("%s %s", LocalDateTime.now(), e.getMessage());        Response response = new Response(message);        return new ResponseEntity<>(response, HttpStatus.OK);    }}

Обратите внимание на аннотацию @ControllerAdvice(annotations = CustomExceptionHandler.class). Такая запись означает что CustomAdvice будет обрабатывать исключения только от тех контроллеров, которые дополнительно имеют аннотацию @CustomExceptionHandler.


Аннотацию @CustomExceptionHandler я специально сделал для данного примера:


@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface CustomExceptionHandler {}

А вот и исходный код контроллера:


@RestController@CustomExceptionHandlerpublic class Example5Controller {    @GetMapping(value = "/testCustomControllerAdvice", produces = APPLICATION_JSON_VALUE)    public Response testCustomControllerAdvice(@RequestParam(required = false, defaultValue = "false") boolean exception)            throws BusinessException {        if (exception) {            throw new BusinessException("BusinessException in testCustomControllerAdvice");        }        return new Response("OK");    }}

В контроллере Example5Controller присутствует аннотация @CustomExceptionHandler, а так же на то что выбрасывается то же исключение что и в Example4Controller из предыдущего примера. Однако в данном случае исключение BusinessException обработает именно CustomAdvice, а не DefaultAdvice, в чём мы легко можем убедиться.


Для наглядности я немного изменил сообщение об ошибке в CustomAdvice начал добавлять к нему дату:



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


Исключение ResponseStatusException.


Сейчас речь пойдёт о формировании ответа путём выброса исключения ResponseStatusException:


@RestControllerpublic class Example6Controller {    @GetMapping(value = "/testResponseStatusException", produces = APPLICATION_JSON_VALUE)    public Response testResponseStatusException(@RequestParam(required = false, defaultValue = "false") boolean exception) {        if (exception) {            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "ResponseStatusException in testResponseStatusException");        }        return new Response("OK");    }}

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


Пример вызова:



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


Ссылка на исходники из статьи

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

Перевод Динамическое создание Spring Bean в рантайме

16.11.2020 18:23:29 | Автор: admin

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


Эта статья о динамическом создании бинов за пять лет стала самой популярной в моем блоге (более 9300 просмотров). Пришло время ее обновить. Также я добавил пример на Github.

динамика Spring Bean на Githubдинамика Spring Bean на Github

Однажды на тренинге меня спросили: "Можно ли создать Spring Bean динамически, чтобы можно было выбрать реализацию во время выполнения". Так как во время компиляции еще не известно, какой бин должен быть создан. Приложение должно решить это на основе properties-файла.

1. Создадим аннотацию для того, чтобы отметить метод, который должен создавать объект динамически:

package your.package;@Retention(RetentionPolicy.RUNTIME)public @interface InjectDynamicObject {}

2. Далее используем ее в методе, который должен создать объект:

@Servicepublic class CustomerServiceImpl {    private Customer dynamicCustomerWithAspect;        @InjectDynamicObject    public Customer getDynamicCustomerWithAspect() {        return this.dynamicCustomerWithAspect;    }}

3. Напишем аспект с Pointcut и Advise, который изменяет объект, возвращаемый методом на шаге 2:

@Component@Aspectpublic class DynamicObjectAspect {    // This comes from the property file    @Value("${dynamic.object.name}")    private String object;    @Autowired    private ApplicationContext applicationContext;        @Pointcut("execution(@com.lofi.springbean.dynamic.        InjectDynamicObject * *(..))")    public void beanAnnotatedWithInjectDynamicObject() {    }    @Around("beanAnnotatedWithInjectDynamicObject()")    public Object adviceBeanAnnotatedWithInjectDynamicObject(        ProceedingJoinPoint pjp) throws Throwable {           pjp.proceed();                // Create the bean or object depends on the property file          Object createdObject = applicationContext.getBean(object);        return createdObject;    }}

4. Пишем класс, который должен возвращаться из @InjectDynamicObject. Имя класса настраивается в properties-файле. В данном примере я написал две реализации Customer: CustomerOneImpl и CustomerTwoImpl:

@Component("customerOne")public class CustomerOneImpl implements Customer {    @Override    public String getName() {        return "Customer One";    }}application.propertiesdynamic.object.name=customerOne

5. Пишем тест:

@RunWith(SpringRunner.class)@SpringBootTestpublic class CustomerServiceImplTest {    @Autowired    private CustomerServiceImpl customerService;    @Test    public void testGetDynamicCustomerWithAspect() {        // Dynamic object creation        logger.info("Dynamic Customer with Aspect: " +            customerService.getDynamicCustomerWithAspect()            .getName());}

Но есть еще, более простой, способ сделать это. Без аспектов и AspectJ, только чистый Spring. Можно просто сохранить все реализации в Map и получить из нее необходимую реализацию. Так мы сделали в приложении eXTra Client. В качестве примера можно посмотреть на реализацию PluginsLocatorManager. Spring автомагически инжектит Map с именем бина (String) и самим бином.

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

Подробнее см. в документации Spring.

@Servicepublic class CustomerServiceImpl {        // We inject the customer implementations into a Map    @Autowired    private Map<String, Customer> dynamicCustomerWithMap;        // This comes from the property file as a key for the Map    @Value("${dynamic.object.name}")    private String object;    public Customer getDynamicCustomerWithMap() {        return this.dynamicCustomerWithMap.get(object);    }}

Подробнее о курсе "Разработчик на Spring Framework" можно узнать здесь.

Подробнее..

Перевод Финальные классы в PHP, Java и других языках

23.11.2020 12:04:01 | Автор: admin
Использовать финальные классы или не использовать финальные классы? Вот в чём вопрос. А еще в том, когда и как это делать правильно.



Почему стоит использовать финальные классы


Максимальное уменьшение области видимости


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

Поощрение подхода композиция вместо наследования


Принцип открытости/закрытости гласит: класс должен быть открыт для расширения, но закрыт для изменений.

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

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

Почему этот класс не финальный?


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

Заблуждения


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

Выгоды от использования финальных классов


  • Понятные контракты. Использование интерфейсов заставит вас мыслить в терминах связей между объектами.
  • Изолированные блоки кода без побочных эффектов. Внедрение интерфейсов в качестве зависимостей устранит все неприятные побочные эффекты кода, над которым вы работаете.
  • Тестируемость. Подмена на фиктивные зависимости чрезвычайно проста, если они являются интерфейсами.
  • Низкая и управляемая сложность. Поскольку всё изолировано, вам не нужно беспокоиться о волнообразных изменениях. Это значительно снижает сложность вашего кода.
  • Низкая когнитивная нагрузка. С уменьшением сложности ваш мозг сможет сосредоточиться на самом важном.
  • Гибкость кода. Удаляя любую ненужную связь, вы сможете рефакторить намного легче, чем раньше.
  • Уверенность в себе. Возможность тестировать свой код изолированно не оставляет сомнений в правильности его изменения.

Композиция вместо наследования


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

Что вам следует начать делать [вместо того что вы делаете сейчас]


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

Итого: Интерфейсы Финальные классы Композиция
Подробнее..

Ещё больше строковых оптимизаций

30.11.2020 18:20:43 | Автор: admin

В продолжение своей предыдущей статьи о строках (напоминаю, это была текстовая версия доклада на конференции JPoint-2020) решил дописать ещё одну заметку со строковыми оптимизациями, обнаруженными уже после вёрстки презентации (первые две есть на видео в самом конце, показывал их прямо из "Идеи").

Снова StringBuilder.append(char)

На сцене снова "Спринг", а именно o.s.u.StringUtils.deleteAny(String, String):

// org.springframework.util.StringUtilspublic static String deleteAny(String inString, String charsToDelete) {  if (!hasLength(inString) || !hasLength(charsToDelete)) {    return inString;  }  StringBuilder sb = new StringBuilder(inString.length());  for (int i = 0; i < inString.length(); i++) {    char c = inString.charAt(i);    if (charsToDelete.indexOf(c) == -1) {      sb.append(c);    }  }  return sb.toString();}

В разделе "Склейка: если всё-таки нужно" рассматривая StringBuilder.append(char) я отметил невозможность оптимизации компилятором проверок внутри этого метода даже в счётном цикле с заранее известным количеством проходов. Выходом станет использование массива.

Этот же подход хорошо ложится на случаи, в которых длина преобразованной строки может быть меньше или равна исходной. Действительно, какой бы аргумент не передавался в deleteAny, длина возвращаемой строки никогда не превысит длину исходной. Следовательно, можно незначительно усложнив код избавиться от SB:

public static String deleteAny(String inString, String charsToDelete) {  if (!hasLength(inString) || !hasLength(charsToDelete)) {    return inString;  }    int lastCharIndex = 0;  char[] result = new char[inString.length()];  for (int i = 0; i < inString.length(); i++) {    char c = inString.charAt(i);    if (charsToDelete.indexOf(c) == -1) {      result[lastCharIndex++] = c;    }  }  return new String(result, 0, lastCharIndex);}

Переменная lastCharIndex устраняет возможные "пустоты" в массиве при пропуске одного и более знаков.

Используя бенчмарк легко обнаруживаем существенный прирост производительности даже для небольших объёмов данных:

Benchmark                       Mode     Score     Error   Unitsoriginal                        avgt    90.203    4.317   ns/oppatched                         avgt    25.391    1.118   ns/oporiginal:gc.alloc.rate.norm    avgt   104.000    0.001    B/oppatched:gc.alloc.rate.norm     avgt   104.000    0.001    B/op

Но и это ещё не всё.

Уже после моего коммита наблюдательный пользователь Johnny Lim сообразил, что если после завершения перебора значение переменной lastCharIndex равно длине исходной строки, то ни одной замены не сделано, а значит новую строку создавать не нужно. Итого:

public static String deleteAny(String inString, String charsToDelete) {  if (!hasLength(inString) || !hasLength(charsToDelete)) {    return inString;  }    int lastCharIndex = 0;  char[] result = new char[inString.length()];  for (int i = 0; i < inString.length(); i++) {    char c = inString.charAt(i);    if (charsToDelete.indexOf(c) == -1) {      result[lastCharIndex++] = c;    }  }  if (lastCharIndex == inString.length()) {   // С - сообразительность    return inString;  }  return new String(result, 0, lastCharIndex);}

Похожий подход использован тут.

Коварный StringBuilder.append(Object)

Следующий пример намного менее очевиден:

// org.springframework.http.ContentDispositionprivate static String escapeQuotationsInFilename(String filename) {  if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) {    return filename;  }  boolean escaped = false;  StringBuilder sb = new StringBuilder();  for (char c : filename.toCharArray()) {    sb.append((c == '"' && !escaped) ? "\\\"" : c);    // <----    escaped = (!escaped && c == '\\');  }  // Remove backslash at the end..  if (escaped) {    sb.deleteCharAt(sb.length() - 1);  }  return sb.toString();}

Больное место находится в строке 10, а заключение звучит так: поскольку тернарный оператор возвращает либо String, либо char, то аргументом метода StringBuilder.append() фактически является Object. И всё бы ничего, да преобразование знака в объект происходит с помощью String.valueOf() и мы на ровном месте получаем множество мелкого мусора по схеме:

Character.valueOf(c)   -> StringBuilder.append(Object)     -> String.valueOf()      -> Character.toString()

Отдельно доставляет реализация Character.toString:

Осторожно, не ушибите лицо ладонью
public final class Character {  private final char value;  public String toString() {    char buf[] = {value};    return String.valueOf(buf);  }}

Зачем было оборачивать знак в массив? Тайна сия велика... Как бы то ни было, это уже исправлено.

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

for (int i = 0; i < filename.length() ; i++) {  char c = filename.charAt(i);  if (!escaped && c == '"') {    sb.append("\\\"");    // append(String)  }  else {    sb.append(c);         // append(char)  }  escaped = (!escaped && c == '\\');}

И вновь очень простое изменение приносит впечатляющий прирост (бенчмарк по ссылке):

JDK 8Benchmark                             latin   len   Score          UnitsappendCovariant                        true    10   180.2  10.3   ns/opappendExact                            true    10    68.5   1.4   ns/opappendCovariant                       false    10   177.7   4.4   ns/opappendExact                           false    10    67.7   1.3   ns/opappendCovariant:gc.alloc.rate.norm    true    10   688.0   0.0    B/opappendExact:gc.alloc.rate.norm        true    10   112.0   0.0    B/opappendCovariant:gc.alloc.rate.norm   false    10   816.0   0.0    B/opappendExact:gc.alloc.rate.norm       false    10   112.0   0.0    B/opJDK 14Benchmark                             latin   len    Score         UnitsappendCovariant                        true    10    228.8  18.6  ns/opappendExact                            true    10     57.9   2.6  ns/opappendCovariant                       false    10    292.8  12.4  ns/opappendExact                           false    10     90.2   2.2  ns/opappendCovariant:gc.alloc.rate.norm    true    10    688.0   0.0   B/opappendExact:gc.alloc.rate.norm        true    10    112.0   0.0   B/opappendCovariant:gc.alloc.rate.norm   false    10   1096.0   0.0   B/opappendExact:gc.alloc.rate.norm       false    10    200.0   0.0   B/op

Обратите внимание, что исполнение не смогло выбросить мусорные объекты, хотя их область видимости крайне ограничена. Закономерно возникает вопрос: могёт ли Грааль?

Ответ

Не знаю, не проверял :)
Оставляю этот вопрос энтузиастам в качестве домашнего задания :)

Коварный String.substring

Давно известно, что метод String.substring всегда возвращает новую строку, и тем не менее в задачах на "выкусывание" он всё ещё пользуется незаслуженной популярностью:

// org.springframework.web.util.UrlPathHelper/** * Sanitize the given path replacing "//" with "/" */private String getSanitizedPath(String path) {  String sanitized = path;  while (true) {    int idx = sanitized.indexOf("//");    if (idx < 0) {      break;    }    else {      sanitized = sanitized.substring(0, idx) + sanitized.substring(idx + 1);    }  }  return sanitized;}

Здесь даже если исполнение каким-то чудом сможет распознать шаблон склеивания строк и подставить StringBuilder этот метод всё равно останется чертовски неэффективным.

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

private static String getSanitizedPath(String path) {  int index = path.indexOf("//");  if (index >= 0) {    StringBuilder sanitized = new StringBuilder(path);    while (index != -1) {      sanitized.deleteCharAt(index);      index = sanitized.indexOf("//", index); //не начинай сначала ;)    }    return sanitized.toString();  }  return path;}

В некоторых случаях использование StringBuilder.deleteCharAt(int) позволяет существенно облегчить понимание кода:

// org.springframework.web.util.UrlPathHelperprivate String removeSemicolonContentInternal(String requestUri) {  int semicolonIndex = requestUri.indexOf(';');  while (semicolonIndex != -1) {    int slashIndex = requestUri.indexOf('/', semicolonIndex);    String start = requestUri.substring(0, semicolonIndex);    requestUri = (slashIndex != -1) ? start + requestUri.substring(slashIndex) : start;    semicolonIndex = requestUri.indexOf(';', semicolonIndex);  }  return requestUri;}

Логика здесь довольно запутанная, но на высоком уровне метод удаляет содержимое, выделенное ; внутри ссылки, превращая строку вроде /foo;f=F;o=O;o=O/bar;b=B;a=A;r=R в /foo/bar;b=B;a=A;r=R.

Можно избавиться от взятия подстроки и склейки переписав метод вот так:

private static String removeSemicolonContentInternal(String requestUri) {  int semicolonIndex = requestUri.indexOf(';');  if (semicolonIndex == -1) {    return requestUri;  }  StringBuilder sb = new StringBuilder(requestUri);  while (semicolonIndex != -1) {    int slashIdx = sb.indexOf("/", semicolonIndex + 1);    if (slashIdx == -1) {      return sb.substring(0, semicolonIndex);    }    sb.delete(semicolonIndex, slashIdx);    semicolonIndex = sb.indexOf(";", semicolonIndex);  }  return sb.toString();}

На первый взгляд, кода стало больше: 12 строк превратились в 16. С другой стороны, он стал выразительнее и проще для понимания, что идёт приятной добавкой к улучшенной производительности.

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

Мопед не мой

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

// org.springframework.util.StringUtilspublic static String trimLeadingWhitespace(String str) {  if (!hasLength(str)) {    return str;  }  StringBuilder sb = new StringBuilder(str);  while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) {    sb.deleteCharAt(0);  }  return sb.toString();}

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

public static String trimLeadingWhitespace(String str) {  if (!hasLength(str)) {    return str;  }  int idx = 0;  while (idx < str.length() && Character.isWhitespace(str.charAt(idx))) {    idx++;  }  return str.substring(idx);}

Этот код значительно быстрее первоначальной версии, а также потребляет меньше памяти, ведь в худшем случае создаётся только один объект (в лучшем при idx = 0 метод str.substring()вернёт строку, на которой он был вызван).

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

Мелочь

Если в коде есть что-то вроде

char[] chars;//...return new String(chars, 0, chars.length);

то это всегда можно переписать в виде

char[] chars;//...return new String(chars);

Производительность это сильно не улучшит, однако, перефразируя рекламу моющего средства из 90-х, "если не видно разницы, то зачем писать больше?" :)

Заключение

На этом всё, надеюсь примеры были вам интересны и полезны. Описывайте свои улучшения в комментариях, самые интересные добавлю в статью. До новых встреч!

Подробнее..

Микросервисная авторизация для чайников для чайников

01.12.2020 12:23:05 | Автор: admin
В данной статье рассматривается пример реализации распределенной микросервисной авторизации доступа для множества пользователей к множеству ресурсов или операций. Уровень подготовки читателя может быть любой, кто знаком с программированием и проектированием. Так же рассматриваются примеры использования на практике и одна из задач реализована в виде небольшой микросервисной системы.

1 Немного теории


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

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

1.1 Время восстановления/энтропия входных параметров


Авторизация принимает решение о разграничении доступа на основе множества входных параметров. Если таковых будет слишком много, либо мерность множеств окажется слишком большой, то авторизация будет терять слишком много ресурсов на выполнение ненужных операций. Это как сортировать данные пузырьком и qsort. Не следует выполнять ненужные операции.
Если у вашей системы есть на входе A,B,C,D,E,F факты о пользователе, то если вы будете объединять все в одно, то получите A * B * C * D * E * F комбинаций, которые невозможно эффективно кешировать. По возможности следует найти такую комбинацию входных, что бы у вас было A * B * C и D * E * F комбинаций, а еще лучше A * B, C * D, E * F, которые вы уже легко сможете вычислять и кешировать.
Эта характеристика очень важна так же для построения системы правил, которые будут предоставлять пользователям доступы.

1.2 Детализация авторизации


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

2 Обобщенная модель данных


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

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

2.1 Пользователь


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

2.2 Сервис


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

2.3 Ресурс


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

2.4 Разрешение


Право пользователя выполнять операцию над сервисом и/или ресурсом.

2.5 Роль


Множество разрешений, по сути под словом роль всегда подразумевается множество разрешений.
Следует отметить, что в системе может не быть ролей и напрямую задаваться набор разрешений для каждого пользователя в отдельности. Так же в системе могут отсутствовать разрешения (речь про хранение их в виде записей БД), но быть роли, при этом такие роли будут называться статичными, а если в системе существуют разрешения, тогда роли называются динамическими, так как можно в любой момент поменять, создать или удалить любую роль так, что система все равно продолжит функционировать.
Ролевые модели позволяют как выполнять вертикальное, так и горизонтальное разграничение доступа (описание ниже). Из типа разграничения доступа следует, что сами роли делятся на типы:
  • Глобальные роли применимые ко всем сервисам для вертикального разграничения;
  • Локальные применяются только к ресурсу горизонтальное разграничение.

Но отсюда следует вопрос, если у пользователя есть глобальная роль и локальная, то как определить его эффективные разрешения? Поэтому авторизация должна быть в виде одной из форм:
  • Конъюнктивная
  • Дизъюнктивная

Подробное описание использования типа ролей и формы авторизации ниже.

Роль имеет следующие связи:
  • Пользователь у каждого пользователя может быть множество ролей;
  • Разрешение каждая роль объединяет в себе несколько разрешений всегда вне зависимости от типа ролей.

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

2.6 Группа


Используя группы можно объединить множество ресурсов, в том числе расположенных в разных сервисах в одно целое, и управлять доступом к ним используя одну точку. Это позволяет гораздо проще и быстрее реализовывать предоставление доступа пользователя к большим массивам информации, не требуя избыточных данных в виде записи на каждый доступ к каждому ресурсу по отдельности вручную или автоматически. По сути вы решаете задачу предоставления доступа к N ресурсам создавая 1 запись.
Группы используются исключительно для горизонтального разграничения доступа (описание ниже). Даже если вы будете использовать группы для доступа к сервисам, то это все равно горизонтальное разграничение доступа, потому что сервис это совокупность ресурсов.
Группа имеет следующие связи:
  • Пользователи множество пользователей участвующих в группе, сама связь может быть при этом:
    • Безусловной связь определяется только пользователем и группой, такая связь всегда уникальна;
    • Условной связь определяет роль/разрешение пользователю в группе, например вы можете добавить пользователя с разрешением модерировать ресурсы группы, таких связей может быть сколько угодно.
  • Ресурсы список ресурсов этой группы.


2.7 Вертикальное и горизонтальное разграничение доступа.


Самым простым способом представить себе работу авторизации это нарисовать таблицы на каждого пользователя и каждый сервис, где колонки будут задавать разрешения, а строки ресурсы, на пересечении ставятся галочки там, где доступ разрешен (привет excel-warrior'ам).
Для обеспечения доступа вы можете либо связать пользователя с ролью, что даст вам заполнение соответствующих разрешению ролей колонок в таблицах такое разграничение называется вертикальным. Либо вы можете связать пользователя, роль и ресурс, что ограничит доступ до отдельного ресурса такое предоставление доступ является горизонтальным. Вторым вариантом горизонтального доступа являются группы, когда вы заполняете множество строк соответствующими разрешениями, когда включаете пользователя в эту группу.

2.8 Глобальность/локальность авторизации доступа в микросервисах


Несмотря на то, что все роли могут хранится в единой БД, то вот связи между ними и пользователями хранить все вместе на всю вашу огромную систему это типичный антипаттерн, делать так является грубейшей ошибкой, это все равно что POSIX права на файлы хранить в облачном сервисе, вместе с таблицей inode. Так же вы теряете огромную часть важного функционала, который вам могла бы предоставлять ваша БД сразу, например пагинация ресурсов с разграничением доступа к строкам.
Связи пользователя с глобальными ролями хранить следует в самом сервисе ролей. Связь же пользователя с локальной ролью необходимо хранить там же, где лежат ресурсы соответствующей роли. Например если у вас есть глобальная роль Менеджер проектов, то хранить ее будем в сервисе ролей, а локальная роль Менеджер проекта (окончание у них разное) уже в сервисе проектов, при этом связь будет не (Пользователь, Роль), а (Пользователь, Роль, Проект). Либо в случае групп (Группа, Проект). Под это дело можно будет написать специальный авторизационный клиент, и когда в другом микросервисе, например документов, вам нужно будет авторизовать доступ к проекту, то вы всего лишь повесите 2 аннотации @RequireRole(User) @ProjectAccess. Все остальное уже сделано либо самим спрингом, либо авторизационными клиентами.
При таком подходе вы можете фильтровать видимость данных уже на уровне вашей БД, в сам запрос вам нужно будет отдать массив групп и ролей пользователя, что бы дополнительно отфильтровать данные. Вам ничто не мешает теперь показывать страницу только тех ресурсов, которые пользователь может видеть! Даже если этих ресурсов миллионы.

2.9 Конъюнктивная/дизъюнктивная форма авторизации


Из-за того, что у теперь у нас существуют два варианта наборов разрешений пользователя, то можно выделить два варианта, как объединить все вместе:
  • Конъюнкция требуем, что бы пользователь имел как глобальное разрешение, так и локальное, в этом случае можно закрыть доступ сотруднику сразу после его увольнения, не тратя время и ресурсы на чистку его записей в БД;
  • Дизъюнкция если у пользователя есть глобальное разрешение, то предоставляем доступ ко всем ресурсам, если есть доступ только к конкретному ресурсу, то только к нему.

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

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

Рекомендуется всегда использовать только конъюнктивную форму авторизации в виду ее большей гибкости и возможности снизить вероятность появления инцидентов по утечке данных. Так же именно конъюнктивная форма авторизации имеет большую устойчивость к ДДОС атакам в виду использования меньшего объема ресурсов. Ее проще реализовать и использовать, отлаживать. Разумеется можно и дизъюнктивную форму натравить на анализ аннотаций метода и поиск соответствующих сервисов, но запросы вы отправлять будете скорее всего синхронно, один за другим, если же будете делать асинхронные вызовы, то много ресурсов будет уходить в пустоту, оно вам надо?

2.10 О кешировании данных


Основной проблемой при работе авторизации является точка в которой необходимо кешировать результаты, что бы работало быстрее все, да еще желательно так, что бы нагрузка на ЦПУ не была высокой. Вы можете поместить все ваши данные в память и спокойно их читать, но это дорогое решение и не все готовы будут себе его позволить, особенно если требуется всего лишь пара ТБ ОЗУ. Второй крайностью является попытка кешировать входные параметры и результат (или запрос и ответ), в результате опять же потребуется пара ТБ ОЗУ, но уже для хранения всех ваших вариантов.
Правильным решением было бы найти такие места, которые бы позволяли с минимальными затратами по памяти давать максимум быстродействия.
Предлагаю решить такую задачу на примере ролей пользователя и некоего микросервиса, которому нужно проверять наличие роли у пользователя. Разумеется в данном случае можно сделать карту (Пользователь, Роль) -> Boolean. Проблема в том, что все равно придется на каждую пару делать запрос на удаленный сервис. Даже если вам данные будут предоставлять за 0,1 мс, то ваш код будет работать все равно медленно. Очевидным решением будет кешировать сразу роли пользователя! В итоге у нас будет кеш Пользователь -> Роль[]. При таком подходе на какое-то время микросервис сможет обрабатывать запросы от пользователя без необходимости нагружать другие микросервисы. Разумеется читатель спросит, а что если ролей у пользователя десятки тысяч? Ваш микросервис всегда работает с ограниченным количеством ролей, которые он проверяет, соответственно вы всегда можете либо захардкодить список, либо найти все аннотации, собрать все используемые роли и фильтровать только их.
Полагаю, что ход мыслей, которому стоит следовать, стал понятен.

2.11 Выводы по обобщенной модели


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

3 Как использовать обобщенную модель данных


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

3.1 Закрытый онлайн аукцион


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

3.2 Логистика


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

Для реализации такого функционала нам потребуется реестр регионов и магазинов в качестве отдельного микросервиса, назовем его С1. Заявки и историю будем хранить на С2. Авторизация А.
Далее при обращении продавца для получения списка его магазинов (а у него может быть их несколько), С1 вернет только те, в которых у него есть меппинг (Пользователь, Магазин), так как ни в какие регионы он не добавлен и для продавца регионы всегда пустое множество. Разумеется при условии, что у пользователя есть разрешение просматривать список магазинов посредством микросервиса А.
При получении заявок магазина, проверка в А разрешений смотреть заявки, а затем С2 отравляет запрос в С1 и получает разрешение или запрет на доступ к магазину.

Работа супервизора будет выглядеть немного иначе, вместо регистрации супервизора на каждый магазин региона, мы сделаем меппинги (Пользователь, Регион) и (Регион, Магазин) в этом случае для супервизора у нас всегда будет список актуальных магазинов с которыми он работает.
В таком случае, когда супервизор решит перевести заявку на поставку в исполнение, то после проверки, что у него есть разрешение в А, С2 отправит проверку, что у супервизора есть доступ к магазину.

3.3 Секретные части документа


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

Когда пользователь обращается в сервис для получения блоков документа, микросервис в авторизации проверяет роль пользователя на чтение документов, далее в БД каждый блок имеет флаг секрет. У документа есть меппинг (Пользователь, Документ) список пользователей, которые подписали соглашение. В этом случае при получении из БД блоков документа можно сразу отфильтровать невидимые для пользователя блоки одним запросом.

3.4 Команда и ее проекты


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

Такую проблему проще всего решить при помощи групп.
Группа будет объединять все необходимые проекты в одно целое. При добавлении участника в команду, добавляем меппинг (Пользователь, Группа, Роль).

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

4 Проектируем микросервисы для работы чайников



4.1 Словесное описание решаемой задачи


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

4.2 Архитектура решения


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

Графически это будет выглядеть так:


В качестве БД будем использовать PostgreSQL.
Предусмотрим следующие обязательные для решения задачи роли:
  1. Сотрудник для возможности пить чай;
  2. Удаленный сотрудник для доступа к микросервису кракена (так как сотрудники в офисе не должны иметь возможности пить чай через точки распрастранения чая);
  3. Кракен учетная запись микросервиса, что бы была возможность обращаться к API чайного сервиса;
  4. Авторизационная учетная запись для предоставления доступа микросервисов к ролевой модели.


5 Реализация микросервисов



Для реализации воспользуемся Spring фреймворком, он довольно медленный, но зато на нем легко и быстро можно реализовывать приложения. Так как за перформансом мы не гонимся, то попробуем на нем достичь скромных 1к рпс авторизованных пустых запросов (хотя люди на спринге умудрялись 100к пустых запросов проворачивать [1]).

Весь проект поделен на несколько репозиториев:


Как работать с проектами
У всех проектов есть общая зависимость ее нужно скачать и выполнить mvn clean install.
Далее нужно собрать докер образы каждого микросервиса, что выполяется командой sh buildDocker.sh автоматически выполнит mvn clean install и сборку образа докера.

В каждом проекте есть кофигурация docker-compose с минимальной для каждого сервиса конфигурацией, лучшим вариантом запуска будет sh start.sh. Остановить сервисы можно при помощи docker-compose down, если этого не сделать, то выполнить скрипт из папок других сервисов вы не сможете.

В скрипте инициализации БД ролевой модели выставлено немного пользователей 10к всего лишь, рекомендую для тестов увеличить до 500к хотя бы. Инициализация займет примерно 100 секунд.


Как устроены test suites
Создаете конфигурацию запуска с мейн классом org.lastrix.perf.tester.PerfTester, в аргумент задаете имя класса test suite, через vm аргументы задаете параметры -Dperf.tester.warmup.round.time=PT1S -Dperf.tester.test.round.time=PT60S -Dperf.tester.test.threads.max=32 -Dperf.tester.test.threads.min=4 -Dperf.tester.test.round.count=1

Для начала разберемся с базой данных ролевой модели:


Пользователи задаются другим сервисом, который в нашем случае даже не реализован, так же как аутентификация. Сделано для упрощения примера.
Обратите внимание на поле is_deleted, оно необходимо, так же как и индекс на (user_id,role_id,is_deleted), иначе работать ничего не будет удаление записей из больших таблиц приведет к снижению производительности (а у нас расчет на 5 млн строк). Если у вас окажется слишком много записей с флагом is_deleted=true (например 1 млн строк), то гораздо выгоднее будет в техническое окно запустить скрипт на удаление таких записей разом, чем удалять по запросу.

Для работы с таблицами нам потребуется сделать REST API, причем одно для пользователей людей, второе для пользователей сервисов.

Само апи довольно легко в понимании, но оно работает медленно (в районе 4к-7к рпс из-за БД), причем как получение ролей пользователя, так и проверка наличия ролей. Для ускорения работы необходимо сделать кеширование, поэтому добавляем в конфигурацию mafp.role.cache.enable. Кеш можно сделать в виде карты, но лучше использовать уже готовое решение com.google.common.cache.Cache и сэкономить себе кучу нервов, потому что через HashMap или WeakHashMap сделать быстро не удастся.
И в данный момент у нас появляется вопрос, а что именно нужно кешировать? В данном случае лучшим вариантом будет сами роли, вернее их имена на каждого пользователя, так мы сэкономим запросы к БД.
В абстрактном классе заведем кеш под эти данные, но есть одно но. Если хранить Set, то нам придется столкнуться с тем, что поиск в случае большого количества ролей у пользователя будет работать медленно (в JVM строки никогда не работали быстро и не будут). Так же проблемой будет то, что строки, которые мы получим из хибернейта не будут интернированными или каким-то образом преобразованными до синглтонов в рамках нашего кеша, т.е. a == a всегда для нас будет ложно, и соответственно будет сравнение строк, а нужно указателей или простых значений, например целых чисел. Это помимо того, что у нас память будет забиваться одними и теми же строками.
Поэтому необходимо добавить карту, в которой будем хранить меппинг (Строка, Целое). И в кеше хранить уже не набор имен ролей пользователя, а набор идентификаторов ролей пользователя, причем эти числа могут не совпадать с идентификаторами в вашей БД. Поиск Integer в HashSet выполняется намного быстрее, чем строки, при этом у вас де факто все строки будут интернированными, что в нашем случае является обязательным шагом иначе потребление памяти будет расти неконтролируемым образом.

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

Если инициализировать БД 500к пользователями, 10к ролей и каждому пользователю выдать до 10 ролей, то можно провести нагрузочное тестирование. Результат на выданных 4 ядрах от моего i7-9700k получится скоромный среднее 12992, минимум 12832, максимум 13088 rps. Результат получен на 16 потоках клиента.

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

Идеальным тестом для авторизации является отношение количества пустых запросов за единицу времени к количеству авторизованных пустых запросов за единицу времени для N (желаемое число под в вашей системе, требующих авторизацию, хотя вы можете вычислять это значение от 1 до N и показывать в виде графика). При этом записывать будем в виде Ка1 коэффициент авторизации 1, если количество под равно 1. Чем ближе к 1 это значение, тем лучше работает ваша авторизация как слой. Именно поэтому полученные выше 13к рпс не имеют никакого значения, даже если вы сделаете авторизацию, которая выдает 10e100 rps, она будет бесполезна, если коэффициент авторизации окажется скажем 1000, потому что плохим результатом является уже 100 т.е. ваша авторизация в 100 раз замедляет ничего не делающее приложение.
В данной статье будет измеряться только Ка1, потому что железа нет.

Приступим к чайному сервису. Для тестирования авторизации у нас создан в нем ендпоинт POST /api/v1/tea/dummy.
Если отключить авторизацию (/noauth в конец добавить), то тестирование покажет нам на выданные 2 ядра чайному сервису и 2 ролевому среднее 18208, минимум 18048, максимум 18560 на 32 потоках клиента это количество запросов, которое может обработать наша система с заданными настройками, исключая какую либо бизнес логику, быстрее работать у нас уже ничего не будет. Включив же авторизацию, которая заключается в проверке у пользователя роли User, получим следующие показания на 6 потоках клиента среднее 1062, минимальное 1062, максимальное 1068.
При этом тестировалось на 100к случайно выбираемых пользователях! Ка1 = 17.1.

И напоследок попьем чай (делаем инсерты в БД) в 6 потоков клиента на POST /api/v1/tea получим среднее 1086, минимум 1086, максимум 1086 (а должно быть ~4к). О чем это говорит нам? О том что авторизация является для нас боттлнеком, который замедляет работу системы (из-за сильной нагрузки на ЦПУ, что говорит о том, что алгоритм кеширования у нас не очень). Еще о том, что погрешности измерений огромные.
Коэффициент авторизации позволит вам не только оценить работу алгоритмов авторизации, но и очертить границы в которых может работать весь ваш комплекс микросервисов, а так же сообщить вам имеет ли смысл оптимизировать код конкретного микросервиса или нет, потому что может так оказаться, что выигрыш будет 0.
Если же всего лишь включить роль в JWT токен, то с 12 потоками клиента можно получить от dummy среднее 6396, минимум 6372, максимум 6444. Т.е. Ка1 оказался равен 2.8!
А что будет, если отключить кеширование на клиенте, не использовать роли JWT и оставить его только на ролевой модели? Производительность вырастет, потому что сетевые задержки около нулевые и нагрузка на ЦПУ для dummy станет минимальной среднее 3817, минимум 3806, максимум 3828, что говорит нам о Ка1 = 4.7. Однако следует учитывать, что такое решение не будет масштабируемым, потому что каждый клиент будет обращаться постоянно к сервису ролевой модели и увеличивать на него нагрузку, уже при сотне под начнутся серьезные проблемы, делающие невозможной работу авторизации, разве что у вас на каждый сервис, которому нужна авторизация будет пода авторизации, которая эту функцию будет выполнять, есть в интернете такие сетевые инженеры, которые любую проблему готовы залить водой решить добавив под.

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

Заключение


В статье предложена универсальная модель для авторизации доступа множеству пользователей к множеству ресурсов с минимальными потерями по производительности, позволяющая делать эффективное кеширование и хранение промежуточных данных на микросервисах, для сборки результата авторизации just-in-time. Предложена метрика (в виде коэффициента), для оценки работы авторизации микросервисов с учетом требований работы всего комплекса и показано, как он может меняться в зависимости от подхода, а именно наличия/отсутствия кеша, хранения ролей в JWT токене и т.д. Не показано, что этот коэффициент всегда растет с увеличением количества под в системе.
Предложенная модель данных позволяет разделить данные между частями микросервисной архитектуры, тем самым снизив требования к железу, дает возможность работать только с теми данными, которые нужны в конкретный момент времени конкретному сервису, не пытаться искать в огромном массиве все подряд или пытаться искать данные в полностью обобщенном хранилище.
Показано, что при малом количестве микросервисов нет смысла проводить кеширование на клиенте авторизации, так как это приведет к большей нагрузке на ЦПУ. Предоставлены данные, которые позволяют сделать вывод, что необходимо балансировать ресурсы в зависимости от размеров системы, если она слишком мала, то есть смысл нагружать больше сеть, но при увеличении и разрастании выгоднее добавить пару ядер в микросервисы, для обеспечения работы кешей и разгрузки сети, микросервисов авторизационных данных. Вопрос больше в том, что для вас дешевле, найти дополнительные ядра или сеть улучшать с гигабитной до десятигигабитной, особенно если можно сделать авторизацию, когда достаточно для ее работы 100 мбит сети.

Литература


  1. High-Concurrency HTTP Clients on the JVM
Подробнее..

Сбор данных и отправка в Apache Kafka

15.11.2020 20:17:49 | Автор: admin

Введение


Для анализа потоковых данных необходимы источники этих данных. Так же важна сама информация, которая предоставляется источниками. А источники с текстовой информацией, к примеру, еще и редки.
Из интересных источников можно выделить следующие: twitter, vk. Но эти источники подходят не под все задачи.
Есть источники с нужными данными, но эти источники не потоковые. Здесь можно привести следующее ссылки: public-apis.
При решении задач, связанных с потоковыми данными, можно воспользоваться старым способом.
Скачать данные и отправить в поток.
Для примера можно воспользоваться следующим источником: imdb.
Следует отметить, что imdb предоставляет данные самостоятельно. См. IMDb Datasets. Но можно принять, что данные собранные напрямую содержат более актуальную информацию.


Язык: Java 1.8.
Библиотеки: kafka 2.6.0, jsoup 1.13.1.


Сбор данных


Сбор данных представляет из себя сервис, который по входным данным загружает html-страницы, ищет нужную информацию и преобразует в набор объектов.
Итак источник данных: imdb. Информация будет собираться о фильмах и будет использован следующий запрос: https://www.imdb.com/search/title/?release_date=%s,%s&countries=%s
Где 1, 2 параметр это даты. 3 параметр страны.
Для лучшего понимания источника данных можно обратится к следующему ресурсу: imdb-extensive-dataset.


Интерфейс для сервиса:


public interface MovieDirectScrapingService {    Collection<Movie> scrap();}

Класс Movie это класс, которые содержит информацию об одном фильме (или о шоу и т.п.).


class Movie {    public final String titleId;    public final String titleUrl;    public final String title;    public final String description;    public final Double rating;    public final String genres;    public final String runtime;    public final String baseUrl;    public final String baseNameUrl;    public final String baseTitleUrl;    public final String participantIds;    public final String participantNames;    public final String directorIds;    public final String directorNames;

Анализ данных на одной странице.
Информация собирается следующим образом. Данные закачиваются с помощью jsoup. Далее ищутся нужные html-элементы и трансформируются в экземпляры для фильмов.


String scrap(String url, List<Movie> items) {    Document doc = null;    try {        doc = Jsoup.connect(url).header("Accept-Language", language).get();    } catch (IOException e) {        e.printStackTrace();    }    if (doc != null) {        collectItems(doc, items);        return nextUrl(doc);    }    return "";}

Поиск ссылки на следующею страницу.


String nextUrl(Document doc) {    Elements nextPageElements = doc.select(".next-page");    if (nextPageElements.size() > 0) {        Element hrefElement = nextPageElements.get(0);        return baseUrl + hrefElement.attributes().get("href");    }    return "";}

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


@Overridepublic Collection<Movie> scrap() {    String url = String.format(            baseUrl + "/search/title/?release_date=%s,%s&countries=%s",            startDate, endDate, countries    );    List<Movie> items = new ArrayList<>();    String nextUrl = url;    while (true) {        nextUrl = scrap(nextUrl, items);        if ("".equals(nextUrl)) {            break;        }        try {            Thread.sleep(50);        } catch (InterruptedException e) {        }    }    return items;}

Подробности по остальным методам можно найти в ссылках на ресурсы.


Отправка данных в топик


Формируется следующий сервис: MovieProducer. Здесь будет один единственный публичный метод: run.


Создается продюсер для кафки. Загружаются данные из источника. Трансформируются и отправляются в топик.


public void run() {    try (SimpleStringStringProducer producer = new SimpleStringStringProducer(            bootstrapServers, clientId, topic)) {        Collection<Data.Movie> movies = movieDirectScrapingService.scrap();        List<SimpleStringStringProducer.KeyValueStringString> kvList = new ArrayList<>();        for (Data.Movie move : movies) {            Map<String, String> map = new HashMap<>();            map.put("title_id", move.titleId);            map.put("title_url", move.titleUrl);                        String value = JSONObject.toJSONString(map);            String key = UUID.randomUUID().toString();            kvList.add(new SimpleStringStringProducer.KeyValueStringString(key, value));        }        producer.produce(kvList);    }}

Теперь все вместе


Формируются нужные параметры для поиска. Загружаются данные и отправляются в топик.
Для этого понадобится еще один класс: MovieDirectScrapingExecutor. С одним публичным методом: run.


В цикле создаются данные для поиска из текущей даты. Происходит загрузка и отправка данных в топик.


public void run() {    int countriesCounter = 0;    List<String> countriesSource = Arrays.asList("us");    while (true) {        try {            LocalDate localDate = LocalDate.now();            int year = localDate.getYear();            int month = localDate.getMonthValue();            int day = localDate.getDayOfMonth();            String monthString = month < 9 ? "0" + month : Integer.toString(month);            String dayString = day < 9 ? "0" + day : Integer.toString(day);            String startDate = year + "-" + monthString + "-" + dayString;            String endDate = startDate;            String language = "en";            String countries = countriesSource.get(countriesCounter);            execute(language, startDate, endDate, countries);            Thread.sleep(1000);            countriesCounter += 1;            if (countriesCounter >= countriesSource.size()) {                countriesCounter = 0;            }        } catch (InterruptedException e) {        }    }}

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


Пример отправляемых данных для одного фильма.


{  "base_name_url": "https:\/\/www.imdb.com\/name",  "participant_ids": "nm7947173~nm2373827~nm0005288~nm0942193~",  "title_id": "tt13121702",  "rating": "0.0",  "base_url": "https:\/\/www.imdb.com",  "description": "It's Christmas time and Jackie (Carly Hughes), an up-and-coming journalist, finds that her life is at a crossroads until she finds an unexpected opportunity - to run a small-town newspaper ... See full summary ",  "runtime": "",  "title": "The Christmas Edition",  "director_ids": "nm0838289~",  "title_url": "\/title\/tt13121702\/?ref_=adv_li_tt",  "director_names": "Peter Sullivan~",  "genres": "Drama, Romance",  "base_title_url": "https:\/\/www.imdb.com\/title",  "participant_names": "Carly Hughes~Rob Mayes~Marie Osmond~Aloma Wright~"}

Подробности можно найти в ссылках на ресурсы.


Тесты


Для тестирования основной логики, которая связана с отправкой данных, можно воспользоваться юнит-тестами. В тестах предварительно создается kafka-сервер.
См. Apache Kafka и тестирование с Kafka Server.


Сам тест: MovieProducerTest.


public class MovieProducerTest {    @Test    void simple() throws InterruptedException {        String brokerHost = "127.0.0.1";        int brokerPort = 29092;        String zooKeeperHost = "127.0.0.1";        int zooKeeperPort = 22183;        String bootstrapServers = brokerHost + ":" + brokerPort;        String topic = "q-data";        String clientId = "simple";        try (KafkaServerService kafkaServerService = new KafkaServerService(                brokerHost, brokerPort, zooKeeperHost, zooKeeperPort        )        ) {            kafkaServerService.start();            kafkaServerService.createTopic(topic);            MovieDirectScrapingService movieDirectScrapingServiceImpl = () -> Collections.singleton(                    new Data.Movie()            );            MovieProducer movieProducer =                    new MovieProducer(bootstrapServers, clientId, topic, movieDirectScrapingServiceImpl);            movieProducer.run();            kafkaServerService.poll(topic, "simple", 1, 5, (records) -> {                assertTrue(records.count() > 0);                ConsumerRecord<String, String> record = records.iterator().next();                JSONParser jsonParser = new JSONParser();                JSONObject jsonObject = null;                try {                    jsonObject = (JSONObject) jsonParser.parse(record.value());                } catch (ParseException e) {                    e.printStackTrace();                }                assertNotNull(jsonObject);                    });            Thread.sleep(5000);        }    }}

Заключение


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


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


Исходный код.

Подробнее..

4 книги по цифровой трансформации для тимлидов, шпаргалка по Quarkus amp Observability

19.11.2020 12:06:35 | Автор: admin


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

Начни новое:



Качай:


  • Debezium на OpenShift
    Debezium это распределенная опенсорсная платформа для отслеживания изменений в данных. Благодаря ее надежности и скорости ваши приложения смогут реагировать быстрее и никогда не пропустят события, даже если что-то пойдет на так. Наша шпаргалка поможет с развертыванием, созданием, запуском и обновление DebeziumConnector на OpenShift.
    Загрузить шпаргалку
  • Шпаргалка Quarkus & Observability (придется зарегистрироваться в девелоперской программе и стать частью community, мухахаха)



Почитать на досуге:


  • Объясняем простым языком, что такое гибридное облачное хранилище
    Что это вообще и какие задачи оно решает в условиях постоянного роста объемы данных и эволюции приложений.
    Вкратце: гибридные облачные хранилища сейчас в тренде, и не зря. Майк Пих (Mike Piech), вице-президент и генеральный менеджер Red Hat по облачным хранилищам и дата-сервисам, а также другие эксперты рассказывают о преимуществах, сценариях использования и ограничениях этой технологии.
  • 4 книги по цифровой трансформации, которые должен прочесть каждый руководитель
    Технологии это далеко не всё, на чем фокусируются руководители, успешно осуществляющие цифровую трансформацию. Представленные книги расширят ваше понимание путей развития корпоративные заказчиков, глобальных рынков и других важных тем.
    Вкратце: эти 4 книги помогут освежить понимание перспектив цифровой трансформации.


  • 7 способов применения микрокомпьютеров Raspberry Pi на предприятии
    От тимбилдинга до сверхдешевых средств безопасности и экспериментов с Kubernetes рассказываем, как задействовать Raspberry Pi на предприятиях.
    Вкратце: крохотный Raspberry Pi способен придать большой импульс развитию корпоративной ИТ-системы.

Смотри в записи:


  • jconf.dev (30 сентября)
    Бесплатная виртуальная Java-конференция прямо у вас на экране: четыре техно-трека с нашими комьюнити-экспертами по Java и облаку, 28 углубленных сессий и два потрясающих основных доклада.
  • AnsibleFest (13-14 октября)
    Два дня интереснейших докладов, демонстраций и практических занятий. Отличная возможность узнать, как разработчики, администраторы и ЛПР в сфере ИТ отвечают на вызовы перемен с помощью гибких технологий автоматизации с открытым кодом, которые позволяют перейти от того, что есть, к тому, что нужно.
  • J4K Conference (13-14 октября)
    Новая виртуальная конференция по Kubernetes, Java и облаку: 17 сессий с сотрудниками Red Hat, включая доклад Марка Литтла (Mark Little), главного человека в Red Hat по связующему ПО.
  • График предстоящих мероприятия DevNation
    Ознакомьтесь с планом мероприятия DevNation на портале Red Hat Developer, включая все вебинары Tech Talks и мастер-курсы, чтобы заранее спланировать свое расписание и зарегистрироваться на заинтересовавшие вас мероприятия.

По-русски:


Подробнее..

NX QA Meetup 14 (Не)адекватное code review автотестов и тестирование модуля расчета прав

16.11.2020 18:23:29 | Автор: admin

19 ноября приглашаем на NX QA Meetup #14. Дмитрий Тучс из PropellerAds расскажет о хороших и плохих примерах code review в классических selenium end-to-end тестах. С Олегом Журавлевым из Nexign поговорим о моделях прав пользователей и способах тестирования при обновлении модуля расчета прав.

Есть мнение, что многие начинающие (и не только) QA automation engineers не совсем правильно понимают значение code review автотестов, обращают внимание на незначительные детали и иногда пропускают действительно важные.

Code review это то, что полностью роднит QA-инженера с разработчиком, но не всегда большого бэкграунда в тестировании достаточно, чтобы ревью кода приносило только пользу. Дмитрий Тучас предлагает обсудить примеры хороших и плохих ревью в классических selenium end-to-end тестах. Примеры не привязаны к конкретным технологиям: что-то будет из Java, что-то из JS.

Вмес с Олегом Журавлевым поговорим о том, какие модели прав пользователей бывают, что и как нужно протестировать при обновлении такого важного компонента любой системы, как модуль расчета прав с примерами кода на Python и Java. Рассмотрим авторизацию и аутентификацию пользователей в системах, обсудим единую точку аутентификации (SSO) в разных системах.

Когда: 19 ноября

Во сколько: 19:30 (МСК)

Для участия достаточно зарегистрироваться здесь.

Ссылку на Zoom пришлем в день мероприятия на e-mail, указанный при регистрации.

До встречи!

Подробнее..

Исследование возможных заимствований и нарушений условий лицензирования в Java-коде на GitHub

13.11.2020 20:09:06 | Автор: admin
Меня зовут Ярослав Голубев, я работаю в JetBrains Research, в лаборатории методов машинного обучения в программной инженерии. Некоторые мои коллеги уже писали здесь о своих проектах (например, о подсказках для онлайн-курсов). Общая цель нашей группы сделать работу программистов проще, удобнее и эффективнее, используя данные о том, что и как люди программируют. Однако процесс создания программных продуктов состоит не только из написания кода есть еще документация, комментарии, рабочие обсуждения и многое другое и со всем этим людям тоже можно и нужно помогать.

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

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

Введение в лицензирование кода


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

Мы с вами будем говорить только о лицензировании открытого (open-source) программного обеспечения. Во-первых, это связано с тем, что именно в такой парадигме мы можем легко найти много доступных данных, а во-вторых, сам термин открытое ПО способен ввести в заблуждение. Когда вы скачиваете и устанавливаете обычную проприетарную программу с сайта компании, вас просят согласиться с условиями лицензии. Разумеется, вы их обычно не читаете, но в целом понимаете, что это чья-то интеллектуальная собственность. В то же время, когда разработчики заходят в проект на GitHub и видят все исходные файлы, отношение к ним совсем другое: да, какая-то лицензия там есть, но она же открытая, и программное обеспечение это открытое, значит, можно просто брать и делать что хочешь, так? К сожалению, не все так просто.

Как же устроено лицензирование? Начнем с самого общего деления прав:



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

Так в чем же различие между разрешительными и копилефтными лицензиями? Как и все в нашей теме, этот вопрос достаточно специфический, и здесь есть исключения, но если упростить, то разрешительные лицензии не накладывают ограничений на лицензию измененного продукта. То есть можно взять такой продукт, изменить его и выложить в проект под другой лицензией даже проприетарной. Главным отличием от общественного достояния тут чаще всего является обязательство сохранять авторство и упоминание оригинального автора. Наиболее известными разрешительными лицензиями являются лицензии MIT, BSD и Apache. Многие исследования указывают MIT как наиболее распространенную лицензию открытого программного обеспечения вообще, а также отмечают значительный рост популярности лицензии Apache-2.0 с момента ее создания в 2004 году (например, исследование для Java).

Копилефтные лицензии чаще всего накладывают ограничения на распространение и модификацию побочных продуктов вы получаете продукт, имея определенные права, и обязаны запустить его дальше, предоставляя всем пользователям такие же права. Обычно это означает обязательство распространять программное обеспечение в рамках той же лицензии и предоставлять доступ к исходному коду. На основе такой философии Ричард Столлман создал первую и самую популярную копилефтную лицензию GNU General Public License (GPL). Именно она обеспечивает максимальную защиту свободы для будущих пользователей и разработчиков. Рекомендую почитать историю движения Ричарда Столлмана за свободное программное обеспечение, это очень интересно.

С копилефтными лицензиями есть одна сложность их традиционно делят на сильный и слабый копилефт. Сильный копилефт представляет собой ровно то, что описано выше, в то время как слабый копилефт предоставляет различные послабления и исключения для разработчиков. Наиболее известный пример такой лицензии GNU Lesser General Public License (LGPL): так же как и ее старшая версия, она разрешает изменять и распространять код только при условии сохранения данной лицензии, однако при динамическом линковании (использовании ее как библиотеки в приложении) это требование можно не выполнять. Иными словами, если вы хотите позаимствовать отсюда исходный код или что-то поменять соблюдайте копилефт, но если хотите просто использовать как динамически подключаемую библиотеку можете делать это где угодно.

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



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

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

Постановка задачи и методология


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

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

Чтобы провести такой анализ, нам необходимо:

  1. Собрать датасет из большого количества открытых проектов.
  2. Найти среди них клоны фрагментов кода.
  3. Определить те клоны, которые действительно могут являться заимствованиями.
  4. Для каждого фрагмента кода определить два параметра его лицензию и время его последней модификации, которое необходимо, чтобы узнать, какой фрагмент в паре клонов старше, а какой младше, и следовательно кто мог потенциально скопировать у кого.
  5. Определить, какие возможные переходы между лицензиями являются разрешенными, а какие нет.
  6. Проанализировать все полученные данные, чтобы ответить на вышепоставленные вопросы.

Теперь разберем каждый шаг подробнее.

Сбор данных


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

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

За основу мы взяли существующий Public Git Archive, в начале 2018 года собравший воедино все проекты на GitHub, у которых было более 50 звездочек. Мы отобрали все проекты, в которых есть хотя бы одна строчка на Java и скачали их с полной историей изменений. После фильтрации проектов, которые переехали или более недоступны, получилось 23 378 проектов, занимающих примерно 1,25 ТБ места на жестком диске.

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

Поиск клонов


Для поиска клонов, то есть похожих кусков кода, тоже необходимо принять некоторые решения. Во-первых, нужно определиться, насколько и в каком качестве нам интересен похожий код. Традиционно выделяют 4 типа клонов (от самых точных до наименее точных):

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

Нас интересует именно копирование и модификация, поэтому мы рассматриваем только клоны первых трех типов.

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

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

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

Такой поиск занял целых 66 суток непрерывных вычислений, было определено 38,6 миллиона методов, из которых только 11,7 миллиона проходили минимальный порог по размеру, а из них 7,6 миллиона приняли участие в клонировании. Всего обнаружилось 1,2 миллиарда пар клонов.

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


Для дальнейшего анализа мы отобрали только межпроектные пары клонов, то есть пары похожих фрагментов кода, которые встречаются в разных проектах. С точки зрения лицензирования нас мало интересуют фрагменты кода в рамках одного проекта: повторять свой же код считается плохой практикой, но не запрещено. Всего межпроектных пар оказалось примерно 561 миллион, то есть приблизительно половина всех пар. Данные пары включали в себя 3,8 миллиона методов, для которых и нужно было определить время последней модификации. Для этого к каждому файлу (которых оказалось 898 тысяч, потому что в файлах может быть более одного метода) была применена команда git blame, которая выдает время последней модификации для каждой строки в файле.

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



Предположим, мы нашли клон в виде пары фрагментов, каждый по 25 строчек. Более насыщенный цвет тут означает более позднее время модификации. Допустим, фрагмент слева был написан за раз в 2017 году, а во фрагменте справа 22 строчки были написаны в 2015, а три модифицированы в 2019. Выходит, фрагмент справа был модифицирован позднее, однако если бы мы хотели определить, кто у кого мог скопировать, логичнее было бы предположить обратное: левый фрагмент заимствовал правый, а правый позднее незначительно поменялся. Исходя из этого, мы определяли время последнего изменения фрагмента кода как наиболее часто встречающееся время последнего изменения его отдельных строк. Если вдруг таких времен было несколько, выбиралось более позднее.

Интересно, что наиболее старый фрагмент кода в нашем датасете был написан аж в апреле 1997 года, на самой заре создания Java, и у него нашелся клон, сделанный в 2019!

Определение лицензий


Вторым и наиболее важным этапом является определение лицензии для каждого фрагмента. Для этого мы использовали следующую схему. Для начала с помощью инструмента Ninka определялась лицензия, указанная непосредственно в заголовке файла. Если таковая есть, то она и считается лицензией каждого метода в нем (Ninka способна распознавать и несколько лицензий одновременно). Если же в файле ничего не указано, либо указано недостаточно информации (например, только копирайт), то использовалась лицензия всего проекта, к которому относится файл. Данные о ней содержались в оригинальном Public Git Archive, на основании которого мы собирали датасет, и определялись с помощью другого инструмента Go License Detector. Если же лицензии нет ни в файле, ни в проекте, то такие методы отмечались как GitHub, так как в таком случае они подчиняются условиям использования GitHub (именно оттуда были скачаны все наши данные).

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

Главная особенность данного графика состоит в сильнейшей неравномерности распределения лицензий. На графике можно заметить три области: две лицензии с более чем 100 тысячами файлов, еще десять с 10100 тысячами и длинный хвост из лицензий с менее чем 10 тысячами файлов.

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



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

Следом за ней находится пресловутое отсутствие лицензии, и нам все же придется разобрать его подробнее, раз уж данная ситуация настолько часто встречается даже среди средних и крупных репозиториев (более 50 звезд). Данное обстоятельство очень важно, поскольку просто загрузка кода на GitHub не делает его открытым и если что-то практическое и нужно запомнить из данной статьи, то это оно. Загружая код на GitHub, вы соглашаетесь с условиями использования, которые гласят, что ваш код можно будет просматривать и форкать. Однако за исключением этого, все права на код остаются у автора, поэтому распространение, модификация и даже использование требуют явного разрешения. Получается, мало того, что не весь открытый код является полностью свободным, даже не весь код на GitHub является в полном смысле открытым! И так как такого кода много (14% файлов, а среди менее популярных проектов, не вошедших в датасет, скорее всего, и того больше), это может являться причиной значительного количества нарушений.

В пятерке мы также видим уже упомянутые разрешительные лицензии MIT и BSD, а также копилефтную GPL-3.0-or-later. Лицензии из семейства GPL разнятся не только значительным количеством версий (полбеды), но еще и припиской or later, которая позволяет пользователю использовать условия данной лицензии или ее более поздних версий. Это наводит еще на один вопрос: среди этих 94 лицензий явно встречаются подобные семейства какие из них самые большие?

На третьем месте как раз GPL-лицензии их в списке 8 видов. Именно это семейство самое значимое, потому что вместе они покрывают 12,6% файлов, уступая только Apache-2.0 и отсутствию лицензии. На втором месте, неожиданно, BSD. Кроме традиционной версии с 3 параграфами и даже версий с 2 и 4 пунктами, существуют очень специфичные лицензии всего 11 штук. К таким, например, относится BSD 3-Clause No Nuclear License, которая представляет собой обычную BSD с 3 пунктами, к которой снизу приписано, что данное ПО не должно применяться для создания или эксплуатации ничего ядерного:

You acknowledge that this software is not designed, licensed or intended for use in the design, construction, operation or maintenance of any nuclear facility.

Самым разнообразным является семейство лицензий Creative Commons, о которых можно почитать тут. Их встретилось целых 13 и их тоже стоит хотя бы пробежать глазами по одной важной причине: весь код на StackOverflow лицензирован под СС-BY-SA.

Среди более редких лицензий есть некоторые примечательные, например, Do What The F*ck You Want To Public License (WTFPL), которая покрывает 529 файлов и позволяет делать с кодом именно то, что указано в названии. Есть еще, например, Beerware License, которая также разрешает делать что угодно и призывает купить автору пива при встрече. В нашем датасете мы также встретили вариацию этой лицензии, которую больше нигде не нашли Sushiware License. Она, соответственно, призывает купить автору суши.

Еще любопытна ситуация, когда в одном файле (именно в файле) встречается сразу несколько лицензий. В нашем датасете таких файлов всего 0,9%. 7,4 тысячи файлов покрываются сразу двумя лицензиями, и всего обнаружилось 74 разные пары таких лицензий. 419 файлов покрывается аж тремя лицензиями, и таких троек насчитывается 8. И, наконец, один файл в нашем датасете упоминает четыре разные лицензии в заголовке.

Возможные заимствования


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

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

Любопытно, что из оставшихся пар целых 11,7% составляют идентичные клоны с порогом схожести 100% возможно, интуитивно кажется, что абсолютно одинакового кода на GitHub должно быть меньше.

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

  1. Сравниваем время последней модификации двух методов в паре.
  2. Если они совпадают с точностью до дня, игнорируем такую пару: нет смысла искать нарушения с такой точностью.
  3. Если же они не совпадают, берем пару их лицензий от старшего к младшему и записываем. Например, если у блока из 2015 года лицензия MIT, а у блока из 2018 Apache-2.0, то записываем такую пару как потенциальное заимствование MIT Apache-2.0.

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



Здесь зависимость еще более экстремальная: возможное заимствование кода внутри Apache-2.0 составляет более половины всех пар клонов, а первые 10 пар лицензий покрывают уже более 80% клонов. Важно также отметить, что вторая и третья самая частые пары имеют дело с нелицензированными файлами также явное следствие их частоты. Для пяти наиболее популярных лицензий можно изобразить переходы в виде тепловой карты:



Возможные нарушения лицензирования


Следующий шаг в нашем исследовании определить пары клонов, являющиеся потенциальными нарушениями, то есть заимствованиями, которые нарушают условия оригинальной и принимающей лицензий. Для этого необходимо разметить вышеупомянутые пары лицензий как разрешенные либо запрещенные переходы. Так, например, наиболее популярный переход (Apache-2.0 Apache-2.0), разумеется, разрешен, а вот второй (GitHub Apache-2.0) запрещен. Но их очень и очень много, таких пар тысячи.

Чтобы с этим справиться, вспомним, что визуализированные первые 10 пар лицензий покрывают 80% всех пар клонов. Благодаря такой неравномерности, оказалось достаточно вручную разметить всего 176 пар лицензий, чтобы покрыть 99% пар клонов, что показалось нам вполне приемлемой точностью. Среди этих пар, мы считали запрещенными пары четырех типов:

  1. Копирование из файлов без лицензии (GitHub). Как уже было сказано, такое копирование требует прямого разрешения от автора кода, и мы предполагаем, что в подавляющем большинстве случаев его нет.
  2. Копирование в файлы без лицензии также запрещено, потому что это есть по сути стирание, убирание лицензий. Разрешительные лицензии вроде Apache-2.0 или BSD разрешают переиспользовать код в других лицензиях (в том числе проприетарных), однако даже они требуют, чтобы сохранялось упоминание оригинальной лицензии в файле.
  3. Копирование из копилефтных лицензий в более слабые.
  4. Специфические несовместимости между версиями лицензий (например, Apache-2.0 GPL-2.0).

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

В итоге после разметки оказалось, что 72,8% заимствований представляют собой разрешенные заимствования, а 27,2% запрещенные. На следующих графиках представлены наиболее нарушаемые и наиболее нарушающие лицензии.



Слева представлены наиболее нарушаемые лицензии, то есть источники наибольшего количества возможных нарушений. Среди них первое место занимают файлы без лицензий, что является важным практическим замечанием нужно особенно пристально следить за файлами без лицензий. Можно удивиться, что в этом списке делает разрешительная лицензия Apache-2.0. Однако, как видно из вышепредставленной тепловой карты, ~25 миллионов запрещенных заимствований из нее это заимствования в файл без лицензии, так что это следствие ее популярности.

Справа представлены лицензии, в которые копируют с нарушениями, и здесь больше всего представлены все те же Apache-2.0 и GitHub.

Происхождение отдельных методов


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

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

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



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

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

Остальные конфигурации не являются нарушениями:

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

Итак, как же распределены методы в нашем датасете?



Можно заметить, что примерно треть методов не имеет клонов вообще, и еще треть имеет клоны только в связанных проектах. С другой стороны, 5,4% методов представляют слабое нарушение, а 4% сильное нарушение. Хотя такие цифры и могут показаться не очень большими, это все еще сотни тысяч методов в более-менее крупных проектах.

TL;DR


Учитывая, что в этой статье много эмпирических цифр и графиков, повторим наши основные находки:

  • Методы, у которых есть клоны, исчисляются миллионами, а пар между ними больше миллиарда.
  • Всего в нашем датасете, состоящем из Java-проектов с более чем 50 звездами, найдено 94 вида лицензий, которые распределены очень неравномерно: наиболее часто встречаются Apache-2.0 и файлы без лицензии. Возможные переходы также встречаются чаще всего между Apache-2.0 и файлами без лицензии.
  • Что касается запрещенных возможных переходов, то таких 27,2%, и наиболее часто нарушаются права авторов файлов без лицензии.
  • Из самих методов всего 35,4% не имеют клонов вообще, у 5,4% часть старших клонов запрещают возможное заимствование, а у 4% все старшие клоны таковы.

А к чему все это?


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

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

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

  1. Не стоит бояться юридического веса темы лицензирования и переживать из-за большого количества лицензий и их параметров. Для начала вполне достаточно понимать суть лицензий Apache-2.0, MIT, BSD-3-Clause, GPL и LGPL.
  2. Даже для этих лицензий достаточно понимания одного главного параметра: является ли лицензия разрешительной или копилефтной. Так что если вы вдруг встретите какую-то незнакомую редкую лицензию, не обязательно читать все пять мониторов ее текста, для начала можно просто отыскать в интернете именно это ее свойство.
  3. Наиболее пристального внимания требуют файлы на GitHub, для которых лицензия не задана. Такие файлы по умолчанию не являются открытыми и их заимствование требует разрешения автора. Вместе с тем отсутствие лицензии очень редко является намеренным выбором скорее, люди просто забывают об этом. В нашей лаборатории мы ввели следующую практику: когда кому-то надо позаимствовать код, не защищенный лицензией, мы просто пишем автору или создаем ишью, объясняя нашу заинтересованность, и просим добавить в проект лицензию. В подавляющем большинстве случаев разработчик добавляет лицензию, и сообщество открытого программного обеспечения становится чуточку лучше.

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

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

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

Перевод Обеспечение границ компонент чистой архитектуры с помощью Spring Boot и ArchUnit

15.11.2020 20:17:49 | Автор: admin

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

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

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

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

Это тем более важно, если мы работаем над монолитной кодовой базой, охватывающей множество различных областей бизнеса или ограниченных контекстов, если использовать жаргон Domain-Driven Design.

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

Пример кода

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

Видимость Package-Private

Что помогает с соблюдением границ компонентов?Уменьшение видимости.

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

Итак, просто поместите все классы компонента в один и тот же пакет и сделайте общедоступными только те классы, которые нам нужны вне компонента.Задача решена?

Нет, на мой взгляд.

Это не работает, если нам нужны подпакеты в нашем компоненте.

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

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

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

Модульный подход к ограниченным контекстам

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

Пример использования

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

Компонент биллинга предоставляет внешний вид калькулятора счетов.Калькулятор счетов генерирует счет для определенного клиента и определенного периода времени.

Чтобы использовать язык Domain-Driven Design (DDD): компонент биллинга реализует ограниченный контекст, который предоставляет варианты использования биллинга.Мы хотим, чтобы этот контекст был как можно более независимым от других ограниченных контекстов.В остальной части статьи мы будем использовать термины компонент и ограниченный контекст как синонимы.

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

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

Классы API и внутренние классы

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

billing api internal     batchjob    |    internal     database         api         internal

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

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

  • Мы можем легко вкладывать компоненты друг в друга.

  • Легко догадаться, что классы внутриinternalпакета нельзя использовать извне.

  • Легко догадаться, что классы внутриinternalпакета можно использовать из его подпакетов.

  • Пакеты apiиinternalдают нам инструмент для обеспечения соблюдения правил зависимостей с ArchUnit (подробнее об этомпозже).

  • Мы можем использовать столько классов или подпакетов впакетеapiилиinternal, сколько захотим, при этом границы наших компонентов по-прежнему четко определены.

Если возможно, классы внутриinternalпакета должны быть package-private.Но даже если они являются public (и они должны быть public, если мы используем подпакеты), структура пакета определяет четкие и легко отслеживаемые границы.

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

Теперь давайте посмотрим на эти пакеты.

Инверсия зависимостей для предоставления доступа к Package-Private функциям

Начнем сdatabaseподкомпонента:

database api|    + LineItem|    + ReadLineItems|    + WriteLineItems internal     o BillingDatabase

+означает, что класс является public,oозначает, что он является package-private.

databaseКомпонент выставляет API с двумя интерфейсамиReadLineItemsиWriteLineItems, которые позволяют читать и записвысать строку заказа клиента из и в базу данных, соответственно.ТипLineItemдомена также является частью API.

Внутриdatabaseподкомпонент имеет класс,BillingDatabaseреализующий два интерфейса:

@Componentclass BillingDatabase implements WriteLineItems, ReadLineItems {  ...}

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

Обратите внимание, что это применение принципа инверсии зависимостей.

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

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

Давайте также заглянем вbatchjobподкомпонент:

Подкомпонент batchjob не предоставляет API доступа к другим компонентам.У него просто есть классLoadInvoiceDataBatchJob(и, возможно, несколько вспомогательных классов), который ежедневно загружают данные из внешнего источника, преобразуют их и передают их в базу данных биллингового компонента черезWriteLineItemsинтерфейс:

@Component@RequiredArgsConstructorclass LoadInvoiceDataBatchJob {  private final WriteLineItems writeLineItems;  @Scheduled(fixedRate = 5000)  void loadDataFromBillingSystem() {    ...    writeLineItems.saveLineItems(items);  }}

Обратите внимание, что мы используем@ScheduledаннотациюSpring,чтобы регулярно проверять наличие новых элементов в биллинговой системе.

Наконец, содержимое компонента верхнего уровняbilling:

billing api|    + Invoice|    + InvoiceCalculator internal     batchjob     database     o BillingService

Компонент billingпредоставляет доступ к интерфейсу InvoiceCalculatorи доменному типуInvoice.Опять же, интерфейс InvoiceCalculatorреализован внутренним классом, который вызываетсяBillingServiceв примере.BillingServiceобращается к базе данных черезReadLineItemsAPI базы данных для создания счета-фактуры клиента из нескольких позиций:

@Component@RequiredArgsConstructorclass BillingService implements InvoiceCalculator {  private final ReadLineItems readLineItems;  @Override  public Invoice calculateInvoice(        Long userId,         LocalDate fromDate,         LocalDate toDate) {        List<LineItem> items = readLineItems.getLineItemsForUser(      userId,       fromDate,       toDate);    ...   }}

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

Соединяем все вместе с помощью Spring Boot

Чтобы связать все вместе с приложением, мы используем функцию Spring Java Config и добавляемConfigurationкласс вinternalпакеткаждого модуля:

billing internal     batchjob    |    internal    |        o BillingBatchJobConfiguration     database    |    internal    |        o BillingDatabaseConfiguration     o BillingConfiguration

Эти конфигурации говорят Spring внести набор компонентов Spring в контекст приложения.

Полкомпонент конфигурации databaseвыглядит следующим образом:

@Configuration@EnableJpaRepositories@ComponentScanclass BillingDatabaseConfiguration {}

С помощью аннотации @Configurationмы сообщаем Spring, что это класс конфигурации, который вносит компоненты Spring в контекст приложения.

Аннотация @ComponentScanговорит Spring, что нужно включить все классы ,которые находятся в том же пакете, что икласс конфигурации (или подпакет) и аннотированные с@Componentкак бины в контекст приложения.Это загрузит нашBillingDatabaseкласс, приведенный выше.

Вместо @ComponentScanмы могли бы также использовать@Beanаннотированные фабричные методы внутри@Configurationкласса.

Под капотом для подключения к базе данныхdatabaseмодуль использует репозитории Spring Data JPA.Мы включаем их с помощью аннотации @EnableJpaRepositories.

Конфигурация batchjobвыглядит также:

@Configuration@EnableScheduling@ComponentScanclass BillingBatchJobConfiguration {}

Только аннотация @EnableSchedulingдругая.Нам это нужно, чтобы включить аннотацию @Scheduledв нашем bean-компонентеLoadInvoiceDataBatchJob.

Наконец, конфигурация компонента верхнего уровняbillingвыглядит довольно скучно:

@Configuration@ComponentScanclass BillingConfiguration {}

С помощью аннотации @ComponentScanэта конфигурация гарантирует, что подкомпоненты@Configurationбудут обнаружены Spring и загружены в контекст приложения вместе с их bean-компонентами.

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

Это означает, что мы можем настроить таргетинг на каждый компонент и подкомпонент отдельно, обращаясь к его@Configurationклассу.Например, мы можем:

  • Загрузить только один (под) компонент в контекст приложения в рамкахинтеграционного теста SpringBootTest.

  • Включить или отключить определенные (под) компоненты, добавиваннотацию @Conditional...к конфигурации этого подкомпонента.

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

Однако у нас все еще есть проблема: классы вbilling.internal.database.apiпакете являются public, то есть к ним можно получить доступ извнеbillingкомпонента, что нам не нужно.

Давайте решим эту проблему, добавив в игру ArchUnit.

Обеспечение границ с помощью ArchUnit

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

В нашем случае мы хотим определить правило, согласно которому все классы вinternalпакете не используются извне этого пакета.Это правило гарантирует, что классы внутриbilling.internal.*.apiпакетов недоступны извнеbilling.internalпакета.

Маркировка внутренних пакетов

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

Мы могли бы сделать это по имени (то есть рассматривать все пакеты с именем internal как внутренние пакеты), но мы также можем отметить пакеты другим именем, для чего создадим аннотацию@InternalPackage:

@Target(ElementType.PACKAGE)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface InternalPackage {}

Затем во все наши внутренние пакеты мы добавляемpackage-info.javaфайл с этой аннотацией:

@InternalPackagepackage io.reflectoring.boundaries.billing.internal.database.internal;import io.reflectoring.boundaries.InternalPackage;

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

Проверка отсутствия доступа к внутренним пакетам извне

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

class InternalPackageTests {  private static final String BASE_PACKAGE = "io.reflectoring";  private final JavaClasses analyzedClasses =       new ClassFileImporter().importPackages(BASE_PACKAGE);  @Test  void internalPackagesAreNotAccessedFromOutside() throws IOException {    List<String> internalPackages = internalPackages(BASE_PACKAGE);    for (String internalPackage : internalPackages) {      assertPackageIsNotAccessedFromOutside(internalPackage);    }  }  private List<String> internalPackages(String basePackage) {    Reflections reflections = new Reflections(basePackage);    return reflections.getTypesAnnotatedWith(InternalPackage.class).stream()        .map(c -> c.getPackage().getName())        .collect(Collectors.toList());  }  void assertPackageIsNotAccessedFromOutside(String internalPackage) {    noClasses()        .that()        .resideOutsideOfPackage(packageMatcher(internalPackage))        .should()        .dependOnClassesThat()        .resideInAPackage(packageMatcher(internalPackage))        .check(analyzedClasses);  }  private String packageMatcher(String fullyQualifiedPackage) {    return fullyQualifiedPackage + "..";  }}

ВinternalPackages(), мы используем reflection библиотеку для сбора всех пакетов, аннотированных нашей@InternalPackageаннотацией.

Затем для каждого из этих пакетов мы вызываемassertPackageIsNotAccessedFromOutside().Этот метод использует API-интерфейс ArchUnit, подобный DSL, чтобы гарантировать, что классы, которые находятся вне пакета, не должны зависеть от классов, которые находятся внутри пакета.

Этот тест теперь завершится ошибкой, если кто-то добавит нежелательную зависимость к publicклассу во внутреннем пакете.

Но у нас все еще есть одна проблема: что, если мы переименуем базовый пакет (io.reflectoringв данном случае) в процессе рефакторинга?

Тогда тест все равно пройдет, потому что он не найдет никаких пакетов в (теперь несуществующем)io.reflectoringпакете.Если у него нет пакетов для проверки, он не может потерпеть неудачу.

Итак, нам нужен способ сделать этот тест безопасным при рефакторинге.

Обеспечение безопасного рефакторинга правил архитектуры

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

class InternalPackageTests {  private static final String BASE_PACKAGE = "io.reflectoring";  @Test  void internalPackagesAreNotAccessedFromOutside() throws IOException {    // make it refactoring-safe in case we're renaming the base package    assertPackageExists(BASE_PACKAGE);    List<String> internalPackages = internalPackages(BASE_PACKAGE);    for (String internalPackage : internalPackages) {      // make it refactoring-safe in case we're renaming the internal package      assertPackageIsNotAccessedFromOutside(internalPackage);    }  }  void assertPackageExists(String packageName) {    assertThat(analyzedClasses.containPackage(packageName))        .as("package %s exists", packageName)        .isTrue();  }  private List<String> internalPackages(String basePackage) {    ...  }  void assertPackageIsNotAccessedFromOutside(String internalPackage) {    ...  }}

Новый методassertPackageExists()использует ArchUnit, чтобы убедиться, что рассматриваемый пакет содержится в классах, которые мы анализируем.

Мы делаем эту проверку только для базового пакета.Мы не выполняем эту проверку для внутренних пакетов, потому что знаем, что они существуют.В конце концов, мы идентифицировали эти пакеты по@InternalPackageаннотации внутриinternalPackages()метода.

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

Вывод

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

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

Дайте мне знать свои мысли в комментариях!

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

Если вас интересуют другие способы работы с границами компонентов с помощью Spring Boot, вам может бытьинтересен проект moduliths.

Подробнее..
Категории: Java , Testing , Best practices , Spring boot 2

Из песочницы Мой путь к получению Oracle Certified Associate и Oracle Certified Professional

16.11.2020 20:18:08 | Автор: admin
Всем привет, меня зовут Руслан. Я работаю в крупном банке на должности team lead'a.

Хочу поделиться с вами моим опытом получения заветных званий Oracle Certified Associate, Java SE 8 Programmer (далее OCA) и Oracle Certified Professional, Java SE 8 Programmer (далее OCP).

image

Обновленный бейдж Oracle Certified Associate
image

Обновленный бейдж Oracle Certified Professional
image

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

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

Итак, мой бэкграунд на момент начала подготовки к экзамену:

  1. Прочтенная книга Философия Java авторства Брюса Эккель
  2. Около 1,5 лет работы с Java
  3. Базовые знание ООП и многопоточного программирования

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

image

Лектор, по счастливому стечению обстоятельств, является автором книги Nailing 1Z0-808: Practical Guide to Oracle Java SE8 Programmer I Certification по подготовке ОСА. Подготовка к первому экзамену (ОСА) заняла около месяца, практически все свободное время я проводил с книгой в руках либо за тренажером enthuware.

Процесс сдачи довольно прост:

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

Экзамен проходит в отдельной комнате, с кучей камер направленных на вас. Идею списать отбросьте сразу, на мой взгляд, это просто нереально. Время ограниченно, но для ОСА я считаю его достаточным при достаточном уровне подготовки. Результат вы узнаете довольно быстро, я, например, получил письмо на e-mail спустя 30-40 минут. И ура! Первый экзамен сдан с приличным результатом в 94%

Результат ОСА
image

Промежуточные итоги после сдачи ОСА, с точки зрения работающего разработчика. На первый взгляд некоторые темы затронутые в процессе сертификации кажутся игрушечными, но недооценивать их нельзя. Я считаю, что это те самые тонкости, которые отделяют одних разработчиков от других. Да, все из нас наверняка умеют создавать классы\интерфейсы, пользоваться наследованием и писать if statement. Но лишь малый процент людей заглядывает под капот инструмента, с которым работает. Все, чему я научился в рамках подготовки и сдачи ОСА, тут же начал распространять среди членов моей команды. Нет, я не кичился этим сертификатом, задирая свой нос, я никому ничего не сказал, а просто начал применять полученные знания и делиться ими. Без преувеличения, я бы сравнил это с курсами повышения квалификации.

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

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

Книги, которые я использовал:

  1. Java I/O, NIO and NIO.2
  2. OCP: Oracle Certified Professional Java SE 8 Programmer II Study Guide: Exam 1Z0-809
  3. OCP Java SE 7 Programmer II Certification Guide: Prepare for the 1ZO-804 exam

Ну и куда же без тренажeра enthuware.

В этот раз подготовка заняла 2,5 месяца. Я также почти все свободное время проводил либо за книгой, либо за тренажером. В ОСР темы более сложные и рассматриваются куда намного глубже, чем в ОСА. Повторяем процедуру регистрации, платим 150$ и едем сдавать. Я ради интереса выбрал другой центр для сдачи. На удивление помещение было почти таким же с той же кучей камер. В этот раз времени не хватало катастрофически, фрагменты, которые надо было прочесть, стали больше + усложнилась сама логика, которой надо было следовать. Несмотря на мою активную подготовку, я еле-еле успел ответить на все вопросы и сделать небольшое ревью. Как и в прошлый раз, ответ пришел в течение 30-40 минут. В этот раз результат был ниже, но я все равно считаю его достойным 85%.

Результат ОСР
image

Итак, что получилось в сухом остатке, времени на подготовку и сдачу я затратил около 4 месяцев, 300$ за сами экзамены + покупка книг и две лицензии на тренажер enthuware. Большие ли это затраты времени и денег в сравнении с полученными знаниями? Мой ответ нет. В процессе этого обучения мне удалось понять принцип работы Stream API, какой-то процент работы с многопоточностью и многое-многое другое. Можно ли было это все выучить, не сдавая никаких экзаменов, не покупая книг и прочего? Тут я отвечу да, но все не так однозначно. На личном примере могу сказать, что обучение вне временных рамок не так эффективно.

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

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

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

Spring Security пример REST-сервиса с авторизацией по протоколу OAuth2 через BitBucket и JWT

17.11.2020 00:09:32 | Автор: admin
В предыдущей статье мы разработали простое защищенное веб приложение, в котором для аутентификации пользователей использовался протокол OAuth2 с Bitbucket в качестве сервера авторизации. Кому-то такая связка может показаться странной, но представьте, что мы разрабатываем CI (Continuous Integration) сервер и хотели бы иметь доступ к ресурсам пользователя в системе контроля версий. Например, по такому же принципу работает довольно известная CI платформа drone.io.

В предыдущем примере для авторизации запросов к серверу использовалась HTTP-сессия (и куки). Однако для реализации REST-сервиса данный способ авторизации не подходит, поскольку одним из требований REST архитектуры является отсутсвие состояния. В данной статье мы реализуем REST-сервис, авторизация запросов к которому будет осуществляться с помощью токена доступа (access token).

Немного теории


Аутентификация это процесс проверки учётных данных пользователя (логин/пароль). Проверка подлинности пользователя осуществляется путём сравнения введённого им логина/пароля с сохраненными данными.
Авторизация это проверка прав пользователя на доступ к определенным ресурсам. Авторизация выполняется непосредственно при обращении пользователя к ресурсу.

Рассмотрим порядок работы двух вышеупомянутых способов авторизации запросов.
Авторизация запросов с помощью HTTP-сессии:
  • Пользователь проходит аутентификацию любым из способов.
  • На сервере создается HTTP-сессия и куки JSESSIONID, хранящий идентификатор сессии.
  • Куки JSESSIONID передается на клиент и сохраняется в браузере.
  • С каждым последующим запросом на сервер отправляется куки JSESSIONID.
  • Сервер находит соответствующую HTTP-сессию с информацией о текущем пользователе и определяет имеет ли пользователь права на выполнение данного вызова.
  • Для выполнения выхода из приложения необходимо удалить с сервера HTTP-сессию.


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


Распространенным форматом токена доступа в настоящее время является JSON Web Token (JWT). Токен в формате JWT содержит три блока, разделенных точками: заголовок (header), набор полей (payload) и сигнатуру (подпись). Первые два блока представлены в JSON-формате и закодированы в формат base64. Набор полей может состоять как из зарезервированных имен (iss, iat, exp), так и произвольных пар имя/значение. Подпись может генерироваться как при помощи симметричных, так и асимметричных алгоритмов шифрования.

Реализация


Мы реализуем REST-сервис, предоставляющий следующее API:
  • GET /auth/login запустить процесс аутентификации пользователя.
  • POST /auth/token запросить новую пару access/refresh токенов.
  • GET /api/repositories получить список Bitbucket репозиториев текущего пользователя.


Высокоуровневая архитектура приложения.

Заметим, что поскольку приложение состоит из трех взаимодействующих компонентов, помимо того, что мы выполняем авторизацию запросов клиента к серверу, Bitbucket авторизует запросы сервера к нему. Мы не будем настраивать авторизацию методов по ролям, чтобы не делать пример сложнее. У нас есть только один API метод GET /api/repositories, вызывать который могут только аутентифицированные пользователи. Сервер может выполнять на Bitbucket любые операции, разрешенные при регистрации OAuth клиента.

Процесс регистрации OAuth клиента описан в предыдущей статье.

Для реализации мы будем использовать Spring Boot версии 2.2.2.RELEASE и Spring Security версии 5.2.1.RELEASE.

Переопределим AuthenticationEntryPoint.


В стандартном веб-приложении, когда осуществляется обращение к защищенному ресурсу и в секьюрити контексте отсутствует объект Authentication, Spring Security перенаправляет пользователя на страницу аутентификации. Однако для REST-сервиса более подходящим поведением в этом случае было бы возвращать HTTP статус 401 (UNAUTHORIZED).

RestAuthenticationEntryPoint
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {    @Override    public void commence(            HttpServletRequest request,            HttpServletResponse response,            AuthenticationException authException) throws IOException {        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());    }}


Создадим login endpoint.


Для аутентификации пользователя мы по-прежнему используем OAuth2 с типом авторизации Authorization Code. Однако на предыдущем шаге мы заменили стандартный AuthenticationEntryPoint своей реализацией, поэтому нам нужен явный способ запустить процесс аутентификации. При отправке GET запроса по адресу /auth/login мы перенаправим пользователя на страницу аутентификации Bitbucket. Параметром этого метода будет URL обратного вызова, по которому мы возвратим токен доступа после успешной аутентификации.

Login endpoint
@Path("/auth")public class AuthEndpoint extends EndpointBase {...    @GET    @Path("/login")    public Response authorize(@QueryParam(REDIRECT_URI) String redirectUri) {        String authUri = "/oauth2/authorization/bitbucket";        UriComponentsBuilder builder = fromPath(authUri).queryParam(REDIRECT_URI, redirectUri);        return handle(() -> temporaryRedirect(builder.build().toUri()).build());    }}


Переопределим AuthenticationSuccessHandler.


AuthenticationSuccessHandler вызывается после успешной аутентификации. Сгенерируем тут токен доступа, refresh токен и выполним редирект по адресу обратного вызова, который был передан в начале процесса аутентификации. Токен доступа вернем параметром GET запроса, а refresh токен в httpOnly куке. Что такое refresh токен разберем позже.

ExampleAuthenticationSuccessHandler
public class ExampleAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {    private final TokenService tokenService;    private final AuthProperties authProperties;    private final HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository;    public ExampleAuthenticationSuccessHandler(            TokenService tokenService,            AuthProperties authProperties,            HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository) {        this.tokenService = requireNonNull(tokenService);        this.authProperties = requireNonNull(authProperties);        this.authorizationRequestRepository = requireNonNull(authorizationRequestRepository);    }    @Override    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {        log.info("Logged in user {}", authentication.getPrincipal());        super.onAuthenticationSuccess(request, response, authentication);    }    @Override    protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {        Optional<String> redirectUri = getCookie(request, REDIRECT_URI).map(Cookie::getValue);        if (redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) {            throw new BadRequestException("Received unauthorized redirect URI.");        }        return UriComponentsBuilder.fromUriString(redirectUri.orElse(getDefaultTargetUrl()))                .queryParam("token", tokenService.newAccessToken(toUserContext(authentication)))                .build().toUriString();    }    @Override    protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {        redirectToTargetUrl(request, response, authentication);    }    private boolean isAuthorizedRedirectUri(String uri) {        URI clientRedirectUri = URI.create(uri);        return authProperties.getAuthorizedRedirectUris()                .stream()                .anyMatch(authorizedRedirectUri -> {                    // Only validate host and port. Let the clients use different paths if they want to.                    URI authorizedURI = URI.create(authorizedRedirectUri);                    return authorizedURI.getHost().equalsIgnoreCase(clientRedirectUri.getHost())                            && authorizedURI.getPort() == clientRedirectUri.getPort();                });    }    private TokenService.UserContext toUserContext(Authentication authentication) {        ExampleOAuth2User principal = (ExampleOAuth2User) authentication.getPrincipal();        return TokenService.UserContext.builder()                .login(principal.getName())                .name(principal.getFullName())                .build();    }    private void addRefreshTokenCookie(HttpServletResponse response, Authentication authentication) {        RefreshToken token = tokenService.newRefreshToken(toUserContext(authentication));        addCookie(response, REFRESH_TOKEN, token.getId(), (int) token.getValiditySeconds());    }    private void redirectToTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {        String targetUrl = determineTargetUrl(request, response, authentication);        if (response.isCommitted()) {            logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);            return;        }        addRefreshTokenCookie(response, authentication);        authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);        getRedirectStrategy().sendRedirect(request, response, targetUrl);    }}


Переопределим AuthenticationFailureHandler.


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

ExampleAuthenticationFailureHandler
public class ExampleAuthenticationFailureHandler implements AuthenticationFailureHandler {    private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();    private final HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository;    public ExampleAuthenticationFailureHandler(            HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository) {        this.authorizationRequestRepository = requireNonNull(authorizationRequestRepository);    }    @Override    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {        String targetUrl = getFailureUrl(request, exception);        authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);        redirectStrategy.sendRedirect(request, response, targetUrl);    }    private String getFailureUrl(HttpServletRequest request, AuthenticationException exception) {        String targetUrl = getCookie(request, Cookies.REDIRECT_URI)                .map(Cookie::getValue)                .orElse(("/"));        return UriComponentsBuilder.fromUriString(targetUrl)                .queryParam("error", exception.getLocalizedMessage())                .build().toUriString();    }}


Создадим TokenAuthenticationFilter.


Задача этого фильтра извлечь токен доступа из заголовка Authorization в случае его наличия, провалидировать его и инициализировать секьюрити контекст.

TokenAuthenticationFilter
public class TokenAuthenticationFilter extends OncePerRequestFilter {    private final UserService userService;    private final TokenService tokenService;    public TokenAuthenticationFilter(            UserService userService, TokenService tokenService) {        this.userService = requireNonNull(userService);        this.tokenService = requireNonNull(tokenService);    }    @Override    protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain chain) throws ServletException, IOException {        try {            Optional<String> jwtOpt = getJwtFromRequest(request);            if (jwtOpt.isPresent()) {                String jwt = jwtOpt.get();                if (isNotEmpty(jwt) && tokenService.isValidAccessToken(jwt)) {                    String login = tokenService.getUsername(jwt);                    Optional<User> userOpt = userService.findByLogin(login);                    if (userOpt.isPresent()) {                        User user = userOpt.get();                        ExampleOAuth2User oAuth2User = new ExampleOAuth2User(user);                        OAuth2AuthenticationToken authentication = new OAuth2AuthenticationToken(oAuth2User, oAuth2User.getAuthorities(), oAuth2User.getProvider());                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));                        SecurityContextHolder.getContext().setAuthentication(authentication);                    }                }            }        } catch (Exception e) {            logger.error("Could not set user authentication in security context", e);        }        chain.doFilter(request, response);    }    private Optional<String> getJwtFromRequest(HttpServletRequest request) {        String token = request.getHeader(AUTHORIZATION);        if (isNotEmpty(token) && token.startsWith("Bearer ")) {            token = token.substring(7);        }        return Optional.ofNullable(token);    }}


Создадим refresh token endpoint.


В целях безопасности время жизни токена доступа обычно делают небольшим. Тогда в случае его кражи злоумышленник не сможет пользоваться им бесконечно долго. Чтобы не заставлять пользователя выполнять вход в приложение снова и снова используется refresh токен. Он выдается сервером после успешной аутентификации вместе с токеном доступа и имеет большее время жизни. Используя его можно запросить новую пару токенов. Refresh токен рекомендуют хранить в httpOnly куке.

Refresh token endpoint
@Path("/auth")public class AuthEndpoint extends EndpointBase {...    @POST    @Path("/token")    @Produces(APPLICATION_JSON)    public Response refreshToken(@CookieParam(REFRESH_TOKEN) String refreshToken) {        return handle(() -> {            if (refreshToken == null) {                throw new InvalidTokenException("Refresh token was not provided.");            }            RefreshToken oldRefreshToken = tokenService.findRefreshToken(refreshToken);            if (oldRefreshToken == null || !tokenService.isValidRefreshToken(oldRefreshToken)) {                throw new InvalidTokenException("Refresh token is not valid or expired.");            }            Map<String, String> result = new HashMap<>();            result.put("token", tokenService.newAccessToken(of(oldRefreshToken.getUser())));            RefreshToken newRefreshToken = newRefreshTokenFor(oldRefreshToken.getUser());            return Response.ok(result).cookie(createRefreshTokenCookie(newRefreshToken)).build();        });    }}


Переопределим AuthorizationRequestRepository.


Spring Security использует объект AuthorizationRequestRepository для хранения объектов OAuth2AuthorizationRequest на время процесса аутентификации. Реализацией по умолчанию является класс HttpSessionOAuth2AuthorizationRequestRepository, который использует HTTP-сессию в качестве хранилища. Т.к. наш сервис не должен хранить состояние, эта реализация нам не подходит. Реализуем свой класс, который будет использовать HTTP cookies.

HttpCookieOAuth2AuthorizationRequestRepository
public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {    private static final int COOKIE_EXPIRE_SECONDS = 180;    private static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "OAUTH2-AUTH-REQUEST";    @Override    public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {        return getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME)                .map(cookie -> deserialize(cookie, OAuth2AuthorizationRequest.class))                .orElse(null);    }    @Override    public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {        if (authorizationRequest == null) {            removeAuthorizationRequestCookies(request, response);            return;        }        addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, serialize(authorizationRequest), COOKIE_EXPIRE_SECONDS);        String redirectUriAfterLogin = request.getParameter(QueryParams.REDIRECT_URI);        if (isNotBlank(redirectUriAfterLogin)) {            addCookie(response, REDIRECT_URI, redirectUriAfterLogin, COOKIE_EXPIRE_SECONDS);        }    }    @Override    public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {        return loadAuthorizationRequest(request);    }    public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {        deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);        deleteCookie(request, response, REDIRECT_URI);    }    private static String serialize(Object object) {        return Base64.getUrlEncoder().encodeToString(SerializationUtils.serialize(object));    }    @SuppressWarnings("SameParameterValue")    private static <T> T deserialize(Cookie cookie, Class<T> clazz) {        return clazz.cast(SerializationUtils.deserialize(Base64.getUrlDecoder().decode(cookie.getValue())));    }}


Настроим Spring Security.


Соберем все проделанное выше вместе и настроим Spring Security.

WebSecurityConfig
@Configuration@EnableWebSecuritypublic static class WebSecurityConfig extends WebSecurityConfigurerAdapter {    private final ExampleOAuth2UserService userService;    private final TokenAuthenticationFilter tokenAuthenticationFilter;    private final AuthenticationFailureHandler authenticationFailureHandler;    private final AuthenticationSuccessHandler authenticationSuccessHandler;    private final HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository;    @Autowired    public WebSecurityConfig(            ExampleOAuth2UserService userService,            TokenAuthenticationFilter tokenAuthenticationFilter,            AuthenticationFailureHandler authenticationFailureHandler,            AuthenticationSuccessHandler authenticationSuccessHandler,            HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository) {        this.userService = userService;        this.tokenAuthenticationFilter = tokenAuthenticationFilter;        this.authenticationFailureHandler = authenticationFailureHandler;        this.authenticationSuccessHandler = authenticationSuccessHandler;        this.authorizationRequestRepository = authorizationRequestRepository;    }    @Override    protected void configure(HttpSecurity http) throws Exception {        http                .cors().and()                .csrf().disable()                .formLogin().disable()                .httpBasic().disable()                .sessionManagement(sm -> sm.sessionCreationPolicy(STATELESS))                .exceptionHandling(eh -> eh                        .authenticationEntryPoint(new RestAuthenticationEntryPoint())                )                .authorizeRequests(authorizeRequests -> authorizeRequests                        .antMatchers("/auth/**").permitAll()                        .anyRequest().authenticated()                )                .oauth2Login(oauth2Login -> oauth2Login                        .failureHandler(authenticationFailureHandler)                        .successHandler(authenticationSuccessHandler)                        .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint.userService(userService))                        .authorizationEndpoint(authEndpoint -> authEndpoint.authorizationRequestRepository(authorizationRequestRepository))                );        http.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);    }}


Создадим repositories endpoint.


То ради чего и нужна была аутентификация через OAuth2 и Bitbucket возможность использовать Bitbucket API для доступа к своим ресурсам. Используем Bitbucket repositories API для получения списка репозиториев текущего пользователя.

Repositories endpoint
@Path("/api")public class ApiEndpoint extends EndpointBase {    @Autowired    private BitbucketService bitbucketService;    @GET    @Path("/repositories")    @Produces(APPLICATION_JSON)    public List<Repository> getRepositories() {        return handle(bitbucketService::getRepositories);    }}public class BitbucketServiceImpl implements BitbucketService {    private static final String BASE_URL = "https://api.bitbucket.org";    private final Supplier<RestTemplate> restTemplate;    public BitbucketServiceImpl(Supplier<RestTemplate> restTemplate) {        this.restTemplate = restTemplate;    }    @Override    public List<Repository> getRepositories() {        UriComponentsBuilder uriBuilder = fromHttpUrl(format("%s/2.0/repositories", BASE_URL));        uriBuilder.queryParam("role", "member");        ResponseEntity<BitbucketRepositoriesResponse> response = restTemplate.get().exchange(                uriBuilder.toUriString(),                HttpMethod.GET,                new HttpEntity<>(new HttpHeadersBuilder()                        .acceptJson()                        .build()),                BitbucketRepositoriesResponse.class);        BitbucketRepositoriesResponse body = response.getBody();        return body == null ? emptyList() : extractRepositories(body);    }    private List<Repository> extractRepositories(BitbucketRepositoriesResponse response) {        return response.getValues() == null                ? emptyList()                : response.getValues().stream().map(BitbucketServiceImpl.this::convertRepository).collect(toList());    }    private Repository convertRepository(BitbucketRepository bbRepo) {        Repository repo = new Repository();        repo.setId(bbRepo.getUuid());        repo.setFullName(bbRepo.getFullName());        return repo;    }}


Тестирование


Для тестирования нам понадобится небольшой HTTP-сервер, на который будет отправлен токен доступа. Сначала попробуем вызвать repositories endpoint без токена доступа и убедимся, что в этом случае получим ошибку 401. Затем пройдем аутентификацию. Для этого запустим сервер и перейдем в браузере по адресу http://localhost:8080/auth/login. После того как мы введем логин/пароль, клиент получит токен и вызовет repositories endpoint еще раз. Затем будет запрошен новый токен и снова вызван repositories endpoint с новым токеном.

OAuth2JwtExampleClient
public class OAuth2JwtExampleClient {    /**     * Start client, then navigate to http://localhost:8080/auth/login.     */    public static void main(String[] args) throws Exception {        AuthCallbackHandler authEndpoint = new AuthCallbackHandler(8081);        authEndpoint.start(SOCKET_READ_TIMEOUT, true);        HttpResponse response = getRepositories(null);        assert (response.getStatusLine().getStatusCode() == SC_UNAUTHORIZED);        Tokens tokens = authEndpoint.getTokens();        System.out.println("Received tokens: " + tokens);        response = getRepositories(tokens.getAccessToken());        assert (response.getStatusLine().getStatusCode() == SC_OK);        System.out.println("Repositories: " + IOUtils.toString(response.getEntity().getContent(), UTF_8));        // emulate token usage - wait for some time until iat and exp attributes get updated        // otherwise we will receive the same token        Thread.sleep(5000);        tokens = refreshToken(tokens.getRefreshToken());        System.out.println("Refreshed tokens: " + tokens);        // use refreshed token        response = getRepositories(tokens.getAccessToken());        assert (response.getStatusLine().getStatusCode() == SC_OK);    }    private static Tokens refreshToken(String refreshToken) throws IOException {        BasicClientCookie cookie = new BasicClientCookie(REFRESH_TOKEN, refreshToken);        cookie.setPath("/");        cookie.setDomain("localhost");        BasicCookieStore cookieStore = new BasicCookieStore();        cookieStore.addCookie(cookie);        HttpPost request = new HttpPost("http://localhost:8080/auth/token");        request.setHeader(ACCEPT, APPLICATION_JSON.getMimeType());        HttpClient httpClient = HttpClientBuilder.create().setDefaultCookieStore(cookieStore).build();        HttpResponse execute = httpClient.execute(request);        Gson gson = new Gson();        Type type = new TypeToken<Map<String, String>>() {        }.getType();        Map<String, String> response = gson.fromJson(IOUtils.toString(execute.getEntity().getContent(), UTF_8), type);        Cookie refreshTokenCookie = cookieStore.getCookies().stream()                .filter(c -> REFRESH_TOKEN.equals(c.getName()))                .findAny()                .orElseThrow(() -> new IOException("Refresh token cookie not found."));        return Tokens.of(response.get("token"), refreshTokenCookie.getValue());    }    private static HttpResponse getRepositories(String accessToken) throws IOException {        HttpClient httpClient = HttpClientBuilder.create().build();        HttpGet request = new HttpGet("http://localhost:8080/api/repositories");        request.setHeader(ACCEPT, APPLICATION_JSON.getMimeType());        if (accessToken != null) {            request.setHeader(AUTHORIZATION, "Bearer " + accessToken);        }        return httpClient.execute(request);    }}


Консольный вывод клиента.
Received tokens: Tokens(accessToken=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJldm9sdmVjaS10ZXN0a2l0IiwidXNlcm5hbWUiOiJFdm9sdmVDSSBUZXN0a2l0IiwiaWF0IjoxNjA1NDY2MDMxLCJleHAiOjE2MDU0NjY2MzF9.UuRYMdIxzc8ZFEI2z8fAgLz-LG_gDxaim25pMh9jNrDFK6YkEaDqDO8Huoav5JUB0bJyf1lTB0nNPaLLpOj4hw, refreshToken=BBF6dboG8tB4XozHqmZE5anXMHeNUncTVD8CLv2hkaU2KsfyqitlJpgkV4HrQqPk)Repositories: [{"id":"{c7bb4165-92f1-4621-9039-bb1b6a74488e}","fullName":"test-namespace/test-repository1"},{"id":"{aa149604-c136-41e1-b7bd-3088fb73f1b2}","fullName":"test-namespace/test-repository2"}]Refreshed tokens: Tokens(accessToken=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJldm9sdmVjaS10ZXN0a2l0IiwidXNlcm5hbWUiOiJFdm9sdmVDSSBUZXN0a2l0IiwiaWF0IjoxNjA1NDY2MDM2LCJleHAiOjE2MDU0NjY2MzZ9.oR2A_9k4fB7qpzxvV5QKY1eU_8aZMYEom-ngc4Kuc5omeGPWyclfqmiyQTpJW_cHOcXbY9S065AE_GKXFMbh_Q, refreshToken=mdc5sgmtiwLD1uryubd2WZNjNzSmc5UGo6JyyzsiYsBgOpeaY3yw3T3l8IKauKYQ)

Исходный код


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

Ссылки



P.S.
Созданный нами REST-сервис работает по протоколу HTTP, чтобы не усложнять пример. Но поскольку токены у нас никак не шифруются, рекомендуется перейти на защищенный канал (HTTPS).
Подробнее..

Ласточка в мире микросервисов

20.11.2020 16:08:20 | Автор: admin

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

Уверен, каждый из нас восхищается полетом и грацией этой птички, а то, как она живет в стаях, чем-то напоминает мир микросервисов.

Helidon для нас, программистов, это набор библиотек прежде всего для разработки микросервисов, и является представителем семейства, назовем его, MicroProfile based средств разработки. Является полностью Open Source проектом, лежит на GitHub, и распространяется под лицензией Apache 2.0.


Уверен, MicroProfile уже известен многим разработчикам, как набор спецификаций для создания микросервисов. Начав свое развитие в 2016м году всего с трех спецификаций - CDI, JAX-RS и JSON-P, да и то, взятых с Java EE, к концу 2020 году в версии 3.3 это уже 12 спецификаций целиком ориентированные на создание микросервисов:

MicroProfile 3.3. В желтом с изменениями, в синем без.MicroProfile 3.3. В желтом с изменениями, в синем без.

Major release версии MicroProfile происходит раз в год. Minor release-ы происходят раз в квартал. Как видно из иллюстрации, каждая из спецификаций может развиваться со своей скоростью. Некоторые спецификации могут не меняться от релиза к релизу.

Существуют еще несколько спецификаций, не входящих в официальную версию MicroProfile, такие как, спецификации на поддержку GraphQL, Long Running Actions, Concurrency, Reactive Messaging, Event Data и Reactive DB Access.

Сам себе MicroProfile существует под шапкой Eclipse Foundation и это не готовая реализация, а набор общепринятых интерфейсов и стандартов. Реализации этих интерфейсов создаются несколькими вендорами такими как Red Hat, IBM, Tomitribe, Payara и др. Первоначально имплементации стандартов были включены в крупные серверные продукты такие как OpenLiberty, TomEE и т.д. Но в последнее время с появлением концепции DevOps и общей идеи, что один сервер обслуживает одно приложение, и, более того, что сам сервер упаковывается в дистрибуцию продуктов, необходимость в полноценных серверных продуктах для реализации микросервисных архитектур постепенно исчезает. Сами платформы стремятся к своей миниатюризации. Для многих, идейным вдохновителем стал Spring Boot. Небольшого количества кода и несколький аннотаций вполне достаточно, чтобы создавать fat-jar дистрибуций, которые потом так удобно упаковывать в docker контейнеры и деплоить в облака. MicroProfile дал солидную основу для развития этой идеи и ввел понятия стандартизация и портируемость для нее. Это значит, что код написанный в рамках этой спецификации будет работать на любой платформе, которае ее поддерживает. И конечный пользователь волен выбирать платформу не только в рамках технологий, но и по параметрам лицензии, коммерческой или некоммерческой поддержки, сертификации и законодательству в разных странах и т.д. И в случае изменений, портировать свой код с наименьшими потерями.

Heldon на данном этап своего развития не стремится быть Full-Stack средством разработки как Spring Boot или Dropwizard, он ориентирован именно на микросервисы. Типичный микросервис на Helidon (под JVM) будет весить не более 20 Mb, что в несколько раз меньше чем, например, у Spring Boot. Ну а скомпилированный ahead-of-time в Native Image и того менее 10 Mb.

Что самое приятное, API полностью в декларативном стиле и абсолютно портируемое в рамках стандартов MicroProfile.

Как говорится - Talk is cheap, show me the code:

@Path("hello")public class HelloWorld {    @GET     public String hello() {        return "Hello World";    }}

Буквально пара стандартных аннотаций - и можно уходить в продакшан!

Helidon 2.1.0

Так что же умеет Ласточка версии 2.1.0?

Спецификации, поддерживаемые Helidon MP 2.1.1Спецификации, поддерживаемые Helidon MP 2.1.1

А поддерживает Helidon все, что специфицированно в MicroProfile 3.3 плюс CORS и gRPC (Server + Client)

Как же начать кодить? Есть два варианта - скачать CLI, запустить ее и ответить на пару вопросов (об этом далее), или просто в пустом Maven проекте добавить одну зависимость:

<dependency><groupId>io.helidon.microprofile.bundles</groupId><artifactId>helidon-microprofile</artifactId></dependency>

Вышеупомянутая зависимость добавляет все функции, доступные в MicroProfile. Если вы хотите начать с меньшего основного набора функций, вы можете вместо этого использовать основной пакет. Этот пакет включает базовую функцию в MicroProfile (такую как JAX-RS, CDI, JSON-P / B и Config) и не включает некоторые дополнительные функции, такие как Metrics и Tracing. При желании вы можете добавить эти зависимости по отдельности.

Зависимость только для базового набора фичей MicroProfile выглядит так:

<dependency><groupId>io.helidon.microprofile.bundles</groupId><artifactId>helidon-microprofile-core</artifactId></dependency>

Давайте же сделаем небольшое Hello World JAX-RS приложение!

Для этого нам нужен JAX-RS Resource Class:

@Path("/")@RequestScopedpublic class HelloWorldResource {@GET@Produces(MediaType.TEXTPLAIN)public String message() {return "Hello World";}}

Далее нам нужно создать JAX-RS Application:

@ApplicationScoped@ApplicationPath("/")public class HelloWorldApplication extends Application {@Overridepublic Set<Class<?>> getClasses() {return Set.of(HelloWorldResource.class);}}

Учитывая, что у нас CDI приложение, нам на данном этапе развития, все-таки нужно добавить пустой beans.xml в src/main/resources/META-INF

<?xml version="1.0" encoding="UTF-8"?><beans/>

Далее просто в main запускаем наше приложение!

public static void main(String[] args) {io.helidon.microprofile.server.Main.main(args);}

Открываете вашу любимую консоль, в которой безусловно есть инструмент curl пишем:

curl -X GET http://localhost:7001/

и получаем!

{"message":"Hello World"}

ТАДААААМ!! Все очень быстро и качественно!

Если нам не хочется такого низкоуровневого хардкора, уже есть готовый Maven Archetype, который мы просто вызываем:

mvn -U archetype:generate -DinteractiveMode=false \-DarchetypeGroupId=io.helidon.archetypes \-DarchetypeArtifactId=helidon-quickstart-mp \-DarchetypeVersion=2.1.0 \-DgroupId=io.helidon.examples \-DartifactId=helidon-quickstart-mp \-Dpackage=io.helidon.examples.quickstart.mp

Входим в созданную папку (или если вы изменили название - одноименную папку):

cd helidon-quickstart-mp

В ней у нас полность готовый maven проект, который мы можем сразу сбилдить!

mvn package

Проект создает jar-файл приложение для примера и сохраняет все runtime зависимости в каталоге target/libs. Это означает, что вы можете легко запустить приложение, запустив jar-ник:

java -jar target/helidon-quickstart-mp.jar

Пример - очень простой сервис Hello World. Он поддерживает запросы GET для создания приветственного сообщения и запрос PUT для изменения самого приветствия. Ответ кодируется с помощью JSON. Например:

curl -X GET http://localhost:8080/greet{"message":"Hello World!"}curl -X GET http://localhost:8080/greet/Joe{"message":"Hello Joe!"}curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Hola"}' http://localhost:8080/greet/greetingcurl -X GET http://localhost:8080/greet/Jose{"message":"Hola Jose!"}

Более того, прямо из коробочки нам доступны такие плюшки MicroProfile, как Здоровье (Health Check) и Метрики (Metrics):

Про Здоровье можно узнать так:

curl -s -X GET http://localhost:8080/health

про Метрики в формате Prometheus:

curl -s -X GET http://localhost:8080/metrics

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

curl -H 'Accept: application/json' -X GET http://localhost:8080/metrics

В рамках этого артефакта генерируется и Docker файл, который позволяет нам легко сбилдить docker image:

docker build -t helidon-quickstart-mp .

и сразу же его запустить:

docker run --rm -p 8080:8080 helidon-quickstart-mp:latest

Соответственно, все готово и к деплою в Kubernetes:

kubectl create -f app.yaml

kubectl get pods # Wait for quickstart pod to be RUNNING

Проверим как он там запустился:

kubectl get service helidon-quickstart-mp

В зависимости на каком порту все задеплоилось, можем проверить работает ли наш сервис:

curl -X GET http://localhost:31431/greet

Уверены, все работает как надо!

Не забывайте за собой почистить мусор, как закончите:

kubectl delete -f app.yaml

Все очень здорово, работает очень быстро, и прямо из коробочки!!!

Кстати, Helidon очень современный!

Это значит, что он с самого начала живет в среде Java минимум версии 11. Это значит, что начиная JDK 9 нам доступна команд jlink, которая поддерживает сборку набора модулей и их зависимостей в custom runtime image. Плагин helidon-maven-plugin поддерживает простое создание такого runtime image для вашего приложения Helidon, что приводит к уменьшению размера и повышению производительности, выкинув все ненужное.

Создать собственный образ среды выполнения можно двумя способами:

  • Локально, на рабочей машине

  • С использованием Docker

Локально это делается безобразно просто, нужно просто использовать профиль jlink-image

Данный профиль использует helidon-maven-plugin, который и создает наш кастомный образ. Кстати, при регенерации, плагин печатает много полезной информации, по тому как уменьшился размер образа.

Каталог target/helidon-quickstart-mp это автономный пользовательский образ нашего приложения. Он содержит ваше приложение, его зависимости и модули JDK, от которых оно зависит. Вы можете запустить свое приложение, используя команду:

./target/helidon-quickstart-mp/bin/start

Также в кастомный образ включен архив Class Data Sharing (CDS), который улучшает производительность запуска вашего приложения и оптимизирует in-memory footprint.

Архив CDS немного увеличивает размер вашего образа для оптимизации производительности. Но он может вырасти до значительного размера (десятки МБ). Размер архива CDS сообщается в конце вывода сборки.

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

mvn package -Pjlink-image -Djlink.image.addClassDataSharingArchive=false

Но можно билдить и сразу в docker образ, причем тоже простой командой:

docker build -t helidon-quickstart-mp-jlink -f Dockerfile.jlink

Эта команда сделает полную сборку внутри Docker контейнера. При первом запуске это займет некоторое время, потому что будут загружены все зависимости Maven и скэшированы на уровне Docker. Последующие сборки будут намного быстрее, если вы не измените файл pom.xml. Если pom изменен, зависимости будут загружены повторно.

И вуаля, можем запустить наше приложение прямо в Docker:

docker run --rm -p 8080:8080 helidon-quickstart-mp-jlink:latest

Можете его протестировать curl-ами quickstarter-a выше.

Custom runtime images идеально подходят для использования, когда вам нужна вся производительность JDK JVM в достаточно компактной форме.

Ну а что если нам нужен бескомпромиссно малый размер образа и минимальное время старта приложения?

Для этого у нас есть GraalVM Native Images!

Native images представляют собой ahead-of-time скомпилированный код Java, в результате которого создается автономный исполняемый файл. В результате приложение стартует почти мгновенно и имеет меньшие накладные расходы памяти во время выполнения по сравнению с выполнением на JVM.

Чтобы оценить все прелести этой технологии, необходимо скачать и установить GraalVM и указать, где он установлен:

# Ваш путь к установке может отличаться

export GRAALVMHOME=/usr/local/graalvm-ce-20.0.0/Contents/Home/

Далее нужно доустановить команду native-image:

$GRAALVMHOME/bin/gu install native-image

Мы создадим native image из нашего quickstarter-mp приложения.

Вы можете создать native файл все также двумя способами:

  • На локальной GraalVM

  • И, конечно, же на Docker-е!

На локальной машине все также до безобразия просто! У нас есть готовый профиль native-image , который использует все тот же helidon-maven-plugin. Просту билдим с его использованием:

mvn package -Pnative-image

Билд будет длиться дольше обычного, ибо выполняется огромное количество ahead-of-time компиляции. В конце мы получим нативный исполняемый файл! Да да, прям исполняемый файл, JVM больше не нужно! Запустить можно выполнив:

./target/helidon-quickstart-mp

и да, эта штука получилась просто неприлично быстрая и маленькая!

Ну и как в случае с jlink, все манипуляции по билду в native image, можно проводить непосредственно в docker! Просто выполняем:

docker build -t helidon-quickstart-mp-native -f Dockerfile.native

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

Чтобы запустить приложение выполняем:

docker run --rm -p 8080:8080 helidon-quickstart-mp-native:latest

и как в прошлый раз наше приложение запускается неприлично быстро и занимает неприлично мало места!

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

На локальной машине native-image представляет собой выполнимый файл для платформы, на которой эта самая машина работает. И поддерживается пока только Linux и Mac. Windows пока ждем. А в Docker только Linux, соответственно. То есть на Mac можно сбилдить Linux binary.

Тем не менее, native images имеют некоторые ограничения, и для долго работающих приложений, где запуск и занимаемый объем памяти не столь критичны, виртуальная машина Java SE HotSpot может быть более подходящей.

Полный список модулей, которые поддерживают native image приведен тут:

https://helidon.io/docs/v2/#/mp/aot/01introduction

Следует отметить, что поддержка native-image реализована на полной версии CDI без каких либо ограничений!

Как вы видите, Helidon по максимуму использует современные достижения в Java-строении, и предлагает все варианты билдов для наиболее оптимальных результатов!

Но это еще не все!..

Tooling

В Helidon 2 появилась консольная утилита, которая помогает нам создавать, а также ускорят наш процесс разработки.

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

CLI распространяется как отдельный исполняемый файл (скомпилированный с использованием GraalVM) для простоты установки. В настоящее время он доступен для загрузки для Linux и Mac. Просто загрузите binary, установите его в месте, доступном из вашего PATH, и все готово.

Далее наберите:

helidon init

и ответьте на несколько вопросов и все готово!

Откройте созданный проект в любимом IDE и начинайке кодить!

На данный момент с помощью инструмента можно создать примерный проект с полной поддержкой MicroProfile. Можно также создать проект, основанный целиком на низкоуровневых реактивных API для максимальной производительности.

И на данный момент, третьим вариантом приложения является пример с использованием DB Client.

Функционал CLI Helidon будет расширен совсем скоро. Но того, что есть сейчас уже достаточно для 80% случаев в ежедневной разработке.

Стоит также отметить, что у Helidon есть поддержка и на стороне Intellij IDEA. Достаточно лишь нажать два раза shift, набрать Endpoints, и IDEA нам их покажет:

Поддержка в Intellij IdeaПоддержка в Intellij Idea

Welcome to the Danger Zone!

Как мы уже смогли убедиться, Helidon прекрасное средство разработки, значительно упрощающее создание Микросервисов. С помощью нескольких аннотаций и минимального количества кода можно практически без усилий написать полноценное продукционное приложение. Но этим сейчас никого не удивишь. Очень многие это могут. Правда, стоит отметить, что, Helidon будучи MicroProfile based, обеспечивает полную портируемость программ идущих на нем в рамках спецификаций. Стандартизация - основной конек и акцент Helidon.

Но вся эта магия дается не бесплатно, за нее приходится платить производительностью. Архитектура Helidon строится на CDI Extensions, которые вызывают низкоуровневое API. Все обслуживание данной CDI инфраструктуры стоит времени.

Так что это за низкоуровневое API? Данное API носит имя Helidon SE.

По сути Helidon SE представляет собой очень компактный реактивный toolkit построенный поверх Netty, и использует последние нововведения Java SE, такие как reactive streams, асинхронное и функциональное программирование, а также fluent-style APIs.

Архитектура HelidonАрхитектура Helidon

По сути, в некотором смысле мягкий и пушистый Helidon является оберткой над суровым и хардкорным Helidon SE.

Helidon SE представляет из себя неблокирующий микрофреймворк, с микроскопическим memory footprint, где особое внимание уделяется асинхронности и производительности. В нем нет никакой магии - никакой dependency injection, практически нет аннотаций, только новейшие фичи Java (начиная с версии 11). Helidon SE предлагает полный набор API для создания реактивных приложений в функциональном стиле.

На данный момент Helidon SE поддерживает следующие спецификации:

Модули, поддерживаемые Helidon SE Модули, поддерживаемые Helidon SE

Зеленым цветом обозначены экспериментальные компоненты. В рамках Helidon экспериментальные означает, что разработчики оставляют за собой право менять API в минорном релизе.

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

Routing routing = Routing.builder()    .get("/hello", (req, res) -> res.send("Hello World")).build();WebServer.create(routing)   .start();

Особое внимание стоит уделить Helidon DB Client. В нем удалось реализовать полность неблокирующий доступ к базам данным, при этом независимо от того, является ли подлежащий JDBC драйвер БД блокирующим или нет. Из коробочки поддерживаются такие реляционные СУБД, как Postgres, MySQL, MariaDB (вообще все для чего есть JDBC драйвер), a также нереляционные как MongoDB.

Вот так, например выглядит вызов Select в транзакции:

dbClient.inTransaction(tx -> tx.createQuery("SELECT name FROM Pokemons WHERE id = :id") .addParam("id", 1).execute());

А так Update вызов для MongoDb:

dbClient.execute(exec -> exec.createUpdate("{\"collection\": \"pokemons\","+ "\"value\":{$set:{\"name\":$name}},"+ "\"query\":{id:$id}}").addParam("id", 1).addParam("name", "Pikachu").execute());

На данный момент DB Client

При этом, конфигурация происходит централизованно через Helidon Config. Например:

db:source: "jdbc"connection:url: "jdbc:mysql://127.0.0.1:3306/pokemon?useSSL=false"username: "user"password: "password"statements:ping: "DO 0"select-all-pokemons: "SELECT id, name FROM Pokemons"

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

Больше информации и примеров в официальной документации https://helidon.io/docs/v2/#/se/dbclient/01introduction

Helidon SE также предлагает свой реактивный неблокирующий Web Client. Он поддерживает все Helidon SE observability фичи, и расширяем по своему дизайну. Пользоваться им исключительно легко:

WebClient client = WebClient.builder().baseUri("http://localhost").build();Single<String> response = client.get().path("/endpoint").request(String.class);

Его можно конфигурировать через Helidon Config.

Больше инфы в прекрасной и подробной документации https://helidon.io/docs/v2/#/se/webclient/01introduction

Стоит отметить и Reactive Streams / Messaging поддержку в Helidon.

Helidon имеет собственный набор реактивных операторов, независимых вне экосистемы Helidon. Эти операторы могут использоваться с реактивными потоками на основе java.util.concurrent.Flow. Цепочку операторов потоковой обработки можно легко построить с помощью io.helidon.common.reactive.Multi или io.helidon.common.reactive.Single для потоков с одним значением.

Reactive Streams API применяется очень широко и полность совместимо с существующими API и имплементациями (RxJava, Reactor, и т.д.).

Уже есть интеграция с Kafka и Oracle Streaming Service.

Поддержка JMS и Oracle Advanced Queueing уже на подходе.

Пример Multi:

AtomicInteger sum = new AtomicInteger();Multi.just("1", "2", "3", "4", "5").limit(3).map(Integer::parseInt).forEach(sum::addAndGet);System.out.println("Sum: " + sum.get());> Sum: 6

Пример Single:

Single.just("1").map(Integer::parseInt).map(i -> i + 5).toStage().whenComplete((i, t) -> System.out.println("Result: " + i));> Result: 6

Особую гордость разработчиков Helidon составляют gRPC Server & Client. Реализации обоих модулей по сути написали с нуля, без каких либо зависимостей от библиотек Google, и прекрасно работают в модульной среде Java9+.

Минималистичное gRPC приложение выглядит так:

public static void main(String[] args) throws Exception {// Implement the simplest possible gRPC service.       GrpcServer grpcServer = GrpcServer                      .create(GrpcRouting.builder()                      .register(new HelloService())                      .build())                      .start()                      .toCompletableFuture()                      .get(10, TimeUnit.SECONDS);      System.out.println("gRPC Server started at: http://localhost:"          + grpcServer.port());}static class HelloService implements GrpcService {@Overridepublic void update(ServiceDescriptor.Rules rules) {         rules.unary("SayHello", ((request, responseObserver) -> complete(responseObserver, "Hello " + request)));}}

Но, Helidon может быть не только сервером gRPC услуг, но и сам быть их потребителем.

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

Пример клиента выглядит следующим образом:

public static void main(String[] args) throws Exception {    // Дескриптор gRPC, который использует унарный метод. По умолчанию, Java    // сериализация, сериализация и десериализация.    ClientServiceDescriptor descriptor = ClientServiceDescriptor      .builder(HelloService.class)      .unary("SayHello")      .build();    // Создаем канал на порту 1408    Channel channel = ManagedChannelBuilder.forAddress("localhost", 1408)                                                           .usePlaintext()                                               .build();    // Ну и инициализируем непосредственно клиент GrpcServiceClient client = GrpcServiceClient.create(channel, descriptor);    // Теперь можно вызывать метод SayHello, который возвращает CompletableFutureCompletionStage<String> future = client.unary("SayHello", "Helidon gRPC!!");// С чистой совестью печатаем результат! System.out.println(future.get());}

gRPC и сервер и клиент уже стабилизированы и готовы использованию в production. Больше ингормации: https://helidon.io/docs/v2/#/se/grpc/01_introduction

Стоит отметить, совсем недавно, при интеграции Helidon и Neo4J, как раз было использовано реактивное API Neo4J, обернутое со стороны Helidon SE в Multi, и далее асинхронно отправлено в Response. Получилась отличная кооперация между ребятами из Neo4J и Helidon! Примеры будут скоро! Сейчас идет процесс полной интеграции Neo4j и Helidon.

Учитывая, что драйвер Neo4J полность совместим с GraalVM, весь проект полностью компилируется в Native. и он очень быстро работает!

Кстати, раз уж речь пошла о производительности, стоит отметить, что Helidon чаще лучше других справляется со многими операциями. Порой в два раз лучше!

Операций в секунду. Больше лучше. Код данного бенчмарка доступен здесь: https://github.com/danielkec/helidon-jmhОпераций в секунду. Больше лучше. Код данного бенчмарка доступен здесь: https://github.com/danielkec/helidon-jmh

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

Судя по отзывам, пользователи отмечают очень высокую интуитивность архитектуры Helidon SE. Просто нажмите точку в конце выражения в примерах стартеров, и из intellisense-a вашего IDE становится понятно, что еще можно сделать и какую функциональность приктутить.

Вместо того, чтобы закрывать низкоуровневые API, команда Helidon открыла их для всех. Понятно, что для 90% случаев магии будет вполне достаточно, но часто те 10% остальных хардкорных случаев могут быть абсолютно критичными с точки зрения производительности и скорости реакции. Необходимо написать чуть больше кода, но работать он будет кратно быстрее!

Отлично.. но как сие тестировать?

На самом деле очень просто! Helidon SE это чистая Java без магии. Можно тестить просто Junit :)

Но для Helidon MP, учитывая, что там много магии от CDI, существует специальные помощники.

Для начала нужно добавить одну зависимость:

<dependency>      <groupId>io.helidon.microprofile.tests</groupId>         <artifactId>helidon-microprofile-tests-junit5</artifactId>         <scope>test</scope> </dependency>

Тест можно обозначить аннотацией @HelidonTest. Эта аннотация запустит контейнер CDI перед вызовом любого тестового метода и остановит его после вызова последнего метода. Эта аннотация также позволяет выполнять Injection в сам тестовый класс. Поэтому само тестирование получается очень простым:

@HelidonTest@DisableDiscovery@AddBean(MyBean.class)@AddExtension(ConfigCdiExtension.class)@AddConfig(key = "app.greeting", value = "TestHello")class TestNoDiscovery {    @Inject    private MyBean myBean;    @Test    void testGreeting() {        assertThat(myBean, notNullValue());        assertThat(myBean.greeting(), is("TestHello"));    }}

Как видно из примера можно применят и другие аннотации, позволяющие отключать Beans Discovery, добавить конфигурацию и т.д.

Кстати, когда генерируются примеры квикстартеров, они все идут с тестами. Можно из изучить там. Все очень интуитивно!

Что же в конце концов получает пользователь?

Все вышеперечисленное просто замечательно, но, уверен, многим из нас хочется видеть цифры! Действительно ли Ласточка такая маленькая?

Сколько может весить наше приложение? На самом деле вариантов может быть несколько, в зависимости от того как мы его билдим. Helidon предлагает несколько build профилей.

  1. Executable JAR: Hollow jar. Все внешние зависимости сохраняются отдельно. Это особенно полезно в условиях использования Docker-а и его layering-а;

  2. Jlink image: как уже было сказано, Helidon живет на Java версии не ниже 11, поэтому спокойно пользуется всеми плюшками, в том числе Jlink! Тем самым создается оптимизированное по размеру JRE + приложение. В результате имеем лучший старт, меньший размер.. и никаких ограничений по коду!

  3. Ну и конечно GraalVM native-image: с определенным количеством известных ограничений по коду и runtime операциями мы получаем самое быстрое время старта, минимальный memory footprint и занимаемый размер!

Если в цифрах то это выглядит так:

И место на диске:

Ну и конечно время старта!!!:

Более чем достойные результаты! Особенно если посмотреть на Helidon SE приложение скомпилированное в native-image!

А, как известно, время (и место) это деньги! Особенно в облаках!

Вместо вывода.

Наша ласточка продолжает очень быстро развиваться, но остается все такой же быстрой и изящной. Она маленькая, скоростная и очень маневренная! Helidon сочетает в себе такие качества как стандартизация и портируемость, благодаря поддержке MicroProfile, а также высочайшая производительность, благодаря написанным с нуля реактивным низкоуровневым API и их реализаций на новейших версиях Java, начиная с версии 11, с полноценным использованием всех нововведений.

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

Для начала работы нужно просто сходить на сайт http://helidon.io и ознакомится с очень подробной документацией с кучей примеров. Для любителей видео есть отличный канал на ютубчике, а для любителей почитать - официальный блог :)

Ну а еще лучше скачать CLI :)

На данный момент ведется разработка имплементаций как основных, так и не основных спецификаций MicroProfile, таких как Long Running Actions и GraphQL. Все это с обязательной поддержкой GraalVM native-image!

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

Helidon это полноценное, готовое к production, решение, созданное на основе новейших версий Java, без каких либо компромиссов. К тому же полностью стандартизованное!

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

Книга Система модулей Java

23.11.2020 16:15:20 | Автор: admin
image Привет, Хаброжители! Создать надежное и безопасное приложение гораздо проще, если упаковать код в аккуратные блоки. Система модулей в Java представляет собой языковой стандарт для создания таких блоков. Теперь вы можете контролировать взаимодействия различных JAR и легко обнаруживать недостающие зависимости. Фундаментальные изменения архитектуры затронули ядро Java, начиная с версии 9. Все API ядра распространяются в виде модулей, а для библиотек, фреймворков и приложений аналогичный подход можно считать хорошей практикой и рекомендацией.

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

Стратегии миграции и модуляризации


  • Подготовка к переходу на Java 9 и выше.
  • Непрерывная интеграция изменений.
  • Постепенная модуляризация проектов.
  • Генерация декларации модуля с помощью JDeps.
  • Взлом сторонних JAR-файлов с помощью инструмента jar.
  • Публикация модульных JAR для Java 8 и старше.

В главах 6, 7 и 8 обсуждаются технические подробности перехода на Java 9+ и превращения существующей кодовой базы в модульную. В этой главе мы более подробно рассмотрим, как наилучшим образом объединить эти подробности в успешные усилия по миграции и модуляризации. Сначала обсудим, как провести постепенную миграцию, которая хорошо согласуется с процессом разработки, особенно с инструментами сборки и непрерывной интеграцией. Далее рассмотрим, как использовать безымянный модуль и автоматические модули в качестве строительных блоков для конкретных стратегий модуляризации. И наконец, разберем варианты превращения JAR в модульные их самих или их зависимостей. К концу этой главы вы не только поймете механизмы, стоящие за миграционными проблемами, но и узнаете, как наилучшим образом использовать их в своих целях.

9.1. Стратегии миграции


Благодаря знаниям, полученным в главах 6 и 7, вы стали готовыми к любому бою, в который Java 9+ может втянуть вас. Теперь пришло время расширить кругозор и разработать более масштабную стратегию. Как лучше организовать фрагменты, чтобы сделать миграцию максимально тщательной и предсказуемой? В этом разделе приведены рекомендации по подготовке к миграции, оценке усилий по ее проведению, настройке непрерывной сборки на Java 9+ и преодолению недостатков параметров командной строки.

ПРИМЕЧАНИЕ
Многие темы в данном разделе связаны с инструментами сборки, но они достаточно универсальны, поэтому вам не требуется знание какого-либо конкретного инструмента. В то же время я хотел поделиться своим опытом с Maven (единственным инструментом сборки, который я использовал на Java 9+), поэтому иногда упоминал функцию Maven, применяемую мной для выполнения определенных требований. Я не буду вдаваться в подробности, поэтому вам самим придется выяснить, как работают все эти функции.

9.1.1. Подготовительные обновления


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

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

Обзор охвата с AdoptOpenJDK
AdoptOpenJDK, сообщество членов групп пользователей, разработчиков и поставщиков Java, которые являются сторонниками OpenJDK, предоставляет список различных проектов с открытым исходным кодом и их совместимостью с последней и следующей версиями Java: mng.bz/90HA.

9.1.2. Оценка усилий


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

Поиск проблем

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

  • Настройте сборку для компиляции и тестирования на Java 9+ (Maven: набор инструментов) в идеале таким образом, чтобы можно было собирать все ошибки вместо того, чтобы останавливаться на первой (Maven: --fail-never).
  • Запустите всю сборку на Java 9+ (Maven: ~/.mavenrc), снова собирая все ошибки.
  • Если вы разрабатываете приложение, то создайте его как обычно (имеется в виду еще не в Java 9+), а затем запустите в Java 9+. Используйте --illegal-access=debug или deny для получения дополнительной информации о несанкционированном доступе.

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

Рекомендуется применить несколько быстрых исправлений, таких как добавление экспорта или модулей JEE. Это позволит заметить более сложные проблемы, которые могут скрываться за доброкачественными. На данном этапе ни одно исправление не является слишком быстрым или слишком грязным все, что заставляет сборку выдать новую ошибку, считается победой. Если выводится слишком много ошибок компиляции, то можно скомпилировать проект на Java 8 и просто запустить тесты на Java 9+ (Maven: mvn surefire:test).

Затем запустите JDeps для проекта и зависимостей. Проанализируйте зависимости от внутренних API JDK (см. подраздел 7.1.2) и запишите все модули JEE (раздел 6.1). Кроме того, обратите внимание на разделение пакетов между платформенными модулями и JAR-файлами приложения (см. подраздел 7.2.5).

Наконец, найдите в кодовой базе вызовы AccessibleObject::setAccessible (см. подраздел 7.1.4), приведения к TOURLClassLoader (раздел 6.2), анализ системных свойств java.version (см. подраздел 6.5.1) или напрямую вписанные в код URL ресурсов (см. раздел 6.3). Поместите все, что нашли, в один большой список теперь пришло время проанализировать его.

Насколько все плохо?

Найденные проблемы должны быть разделены на две категории: Я видел подобное в этой книге и Что, & *1 #?, происходит?. Далее разделите проблемы из первой категории на Имеет по крайней мере временное решение и Это сложная проблема. Особо сложные проблемы удаленные API и разделения пакетов между платформенными модулями и JAR, которые не реализуют утвержденный стандарт или самостоятельную технологию.

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

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

К концу анализа у вас должен получиться список проблем в этих трех категориях:

  • известная проблема с легким решением;
  • известная сложная проблема;
  • неизвестная проблема, требующая изучения.

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

Об измерении оценки

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

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

9.1.3. Непрерывная интеграция в Java 9+


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

  • Какую ветку нужно собрать?
  • Нужна ли отдельная версия?
  • Как разделить сборку, если она не может полностью работать на Java 9+ с первого дня?
  • Как поддерживать параллельные сборки Java 8 и Java 9+?

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

Какую ветку собирать

У вас может возникнуть соблазн настроить собственную ветку для миграции и позволить НИ-серверу создать данную ветку на Java 9+, а остальные на Java 8, как и раньше. Но миграция может занять некоторое время, что приведет к долгоживущей ветке, и я обычно стараюсь не делать это по разным причинам:

  • вы сами по себе и ваши изменения не находитесь под постоянным контролем команды, которая работает на их основе;
  • обе ветки могут претерпеть множество изменений, что увеличивает вероятность конфликтов при обновлении или объединении ветки Java 9+;
  • если для того, чтобы продлить изменения в основной ветке разработки к ветке Java 9+, потребуется некоторое время, то остальная часть команды тогда же может свободно добавлять в нее код, который создаст новые проблемы в Java 9+ при отсутствии немедленной обратной связи.

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

Какую версию собирать

Должна ли сборка Java 9+ создавать отдельную версию артефактов что-то вроде -JAVA-LATEST-SNAPSHOT? Вслед за решением создать отдельную ветку Java 9+, вероятно, придется создавать отдельную версию. В противном случае можно легко смешать артефакты снимков файловой системы из разных веток, что нарушает сборку тем чаще, чем больше веток отклоняется. Если вы решили проводить сборку из основной ветки разработки, то создание отдельной версии может быть непростым, но я никогда не пытался это делать, поскольку не нашел на то веских причин.

Независимо от способа работы с версиями, при попытке заставить что-то работать на Java 9+ вы, вероятно, иногда будете собирать один и тот же подпроект с одной и той же версией на Java 8. Единственное, что я делаю снова и снова, даже если решаю не делать, устанавливаю артефакты, собранные с помощью Java 9+, в локальный репозиторий. Знаете, тот самый рефлекторный mvn clean install?

Это не очень хорошая идея: тогда нельзя будет использовать эти артефакты в сборке Java 8, поскольку она не поддерживает байт-код Java 9+.

При локальной сборке на Java 9+ старайтесь не устанавливать артефакты! Я для этого использую mvn clean.

Что собирать на Java 9+

Конечная цель запустить инструмент сборки на Java 9+ и собрать все проекты на всех этапах/задачах. В зависимости от того, сколько элементов в данном списке вы создали ранее, возможно, потребуется всего лишь изменить парочку вещей, чтобы этого достичь. В таком случае выполните данные действия не нужно усложнять процесс. С другой стороны, если список более сложный, то разделить сборку Java 9 можно несколькими способами:

  • запустить сборку на Java 8 и только компилировать и тестировать на Java 9+. Я поговорю об этом чуть позже;
  • провести миграцию для каждой цели/задачи. Это значит, что сначала нужно попытаться скомпилировать весь проект с помощью Java 9+, прежде чем запускать тесты;
  • провести миграцию по подпроекту, то есть сначала попытаться скомпилировать, протестировать и упаковать один подпроект, прежде чем переходить к следующему.

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

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

1. Собрать все на Java 8.
2. Собрать все на Java 9+, кроме проблемных подпроектов (подпроекты, зависящие от них, были собраны на основе артефактов Java 8).

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону Java

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Telegram-бот на Java для самых маленьких от старта до бесплатного размещения на heroku

25.11.2020 10:14:05 | Автор: admin


Для кого написано


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

Предыстория


Когда моя дочь начала изучать арифметику, я между делом накидал алгоритм генерации простых примеров на сложение и вычитание вида 5 + 7 =, чтобы не придумывать и не гуглить для неё задания.

И тут на глаза попалась новость, что Telegram выпустил новую версию Bot API 5.0. Ботов я раньше не писал, и потому решил попробовать поднять бота как интерфейс для своей поделки. Все примеры, которые мне удалось найти, показались либо совсем простыми (нужные мне функции не были представлены), либо очень сложными для новичка. Также мне не хватало объяснений, почему выбран тот или иной путь. В общем, написано было сразу для умных, а не для меня. Потому я решил описать свой опыт создания простого бота надеюсь, кому-нибудь это поможет быстрее въехать в тему.

Что в статье есть, чего нет


В статье есть про:

  • создание бекенда не-инлайн бота на Java 11 с использованием Telegram Bot Api 5.0;
  • обработка команд вида /dosomething;
  • обработка текстовых сообщений, не являющихся командами (т.е. не начинающихся с "/");
  • отправку пользователю текстовых сообщений и файлов;
  • деплой и запуск бота на heroku.

В статье нет про:

  • использование функций ботов, не перечисленных выше (например, создание клавиатур я их сначала добавил, но в итоге они мне просто не пригодились);
  • создание списка заданий;
  • работу с Word-документом;
  • обеспечивающие функции логирование, тесты и т.п.;
  • общение с BotFather (создание бота, получение его токена и формирование списка команд подробно и понятно описано во многих источниках, вот первый попавшийся мануал).

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

Бизнес-функции бота


Очень кратко, чтобы проще было воспринимать код. Бот позволяет:

  • выдавать пользователю справочную текстовую информацию в ответ на команды /start, /help и /settings;
  • обрабатывать и запоминать пользовательские настройки, направленные текстовым сообщением заданного формата. Настроек три минимальное + максимальное число, используемые в заданиях, и количество страниц выгружаемого файла;
  • оповещать пользователя о несоблюдении им формата сообщения;
  • формировать Word-файл с заданиями на сложение, вычитание или вперемешку в ответ на команды /plus, /minus и /plusminus с использованием дефолтных или установленных пользователем настроек.

Можно потыкать MentalCalculationBot (должен работать). Выглядит так:



Общий порядок действий


  • разобраться с зависимостями;
  • создать класс бота и реализовать обработку текстовых сообщений пользователя, не являющихся командами;
  • создать классы команд;
  • прописать запуск приложения;
  • задеплоить на heroku.

Ниже подробно расписан каждый пункт.

Зависимости


Для управления зависимостями использовался Apache Maven. Нужные зависимости собственно Telegram Bots и Lombok, использовавшийся для упрощения кода (заменяет стандартные java-методы аннотациями).

Вот что вышло в
pom.xml
    <groupId>***</groupId>    <artifactId>***</artifactId>    <version>1.0-SNAPSHOT</version>    <name>***</name>    <description>***</description>    <packaging>jar</packaging>    <properties>        <java.version>11</java.version>        <maven.compiler.source>${java.version}</maven.compiler.source>        <maven.compiler.target>${java.version}</maven.compiler.target>        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>        <org.projectlombok.version>1.18.16</org.projectlombok.version>        <apache.poi.version>4.1.2</apache.poi.version>        <telegram.version>5.0.1</telegram.version>    </properties>    <dependencies>        <!-- Telegram API -->        <dependency>            <groupId>org.telegram</groupId>            <artifactId>telegrambots</artifactId>            <version>${telegram.version}</version>        </dependency>        <dependency>            <groupId>org.telegram</groupId>            <artifactId>telegrambotsextensions</artifactId>            <version>${telegram.version}</version>        </dependency>        <!-- Lombok -->        <dependency>            <groupId>org.projectlombok</groupId>            <artifactId>lombok</artifactId>            <version>${org.projectlombok.version}</version>            <scope>compile</scope>        </dependency>    </dependencies>    <build>        <finalName>${project.artifactId}</finalName>        <plugins>            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-compiler-plugin</artifactId>                <version>3.8.1</version>                <configuration>                    <release>${java.version}</release>                    <annotationProcessorPaths>                        <path>                            <groupId>org.projectlombok</groupId>                            <artifactId>lombok</artifactId>                            <version>${org.projectlombok.version}</version>                        </path>                    </annotationProcessorPaths>                </configuration>            </plugin>            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-dependency-plugin</artifactId>                <version>3.1.2</version>                <executions>                    <execution>                        <id>copy-dependencies</id>                        <phase>package</phase>                        <goals>                            <goal>copy-dependencies</goal>                        </goals>                    </execution>                </executions>            </plugin>            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-surefire-plugin</artifactId>                <version>3.0.0-M5</version>            </plugin>        </plugins>    </build>


Класс бота и обработка текстовых сообщений


Мой класс Bot унаследован от TelegramLongPollingCommandBot, который, в свою очередь, наследуется от более распространённого в примерах TelegramLongPollingBot.

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

В классе Bot таким образом остаётся только логика обработки текстовых сообщений, не являющихся командами. В моём случае это пользовательские настройки или мусорные сообщения, не соответствующие формату. Для лаконичности логику их обработки тоже стоит вынести в отдельный вспомогательный класс, вызывая его метод из переопределенного метода processNonCommandUpdate(Update update) класса Bot.

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

Получился вот такой
Bot.java
import lombok.Getter;import org.telegram.telegrambots.extensions.bots.commandbot.TelegramLongPollingCommandBot;import org.telegram.telegrambots.meta.api.methods.send.SendMessage;import org.telegram.telegrambots.meta.api.objects.Message;import org.telegram.telegrambots.meta.api.objects.Update;import org.telegram.telegrambots.meta.api.objects.User;import org.telegram.telegrambots.meta.exceptions.TelegramApiException;import ru.taksebe.telegram.mentalCalculation.telegram.commands.operations.MinusCommand;import ru.taksebe.telegram.mentalCalculation.telegram.commands.operations.PlusCommand;import ru.taksebe.telegram.mentalCalculation.telegram.commands.operations.PlusMinusCommand;import ru.taksebe.telegram.mentalCalculation.telegram.commands.service.HelpCommand;import ru.taksebe.telegram.mentalCalculation.telegram.commands.service.SettingsCommand;import ru.taksebe.telegram.mentalCalculation.telegram.commands.service.StartCommand;import ru.taksebe.telegram.mentalCalculation.telegram.nonCommand.NonCommand;import ru.taksebe.telegram.mentalCalculation.telegram.nonCommand.Settings;import java.util.HashMap;import java.util.Map;public final class Bot extends TelegramLongPollingCommandBot {    private final String BOT_NAME;    private final String BOT_TOKEN;    //Класс для обработки сообщений, не являющихся командой    private final NonCommand nonCommand;    /**     * Настройки файла для разных пользователей. Ключ - уникальный id чата     */    @Getter    private static Map<Long, Settings> userSettings;    public Bot(String botName, String botToken) {        super();        this.BOT_NAME = botName;        this.BOT_TOKEN = botToken;        //создаём вспомогательный класс для работы с сообщениями, не являющимися командами        this.nonCommand = new NonCommand();        //регистрируем команды        register(new StartCommand("start", "Старт"));        register(new PlusCommand("plus", "Сложение"));        register(new MinusCommand("minus", "Вычитание"));        register(new PlusMinusCommand("plusminus", "Сложение и вычитание"));        register(new HelpCommand("help","Помощь"));        register(new SettingsCommand("settings", "Мои настройки"));        userSettings = new HashMap<>();    }    @Override    public String getBotToken() {        return BOT_TOKEN;    }    @Override    public String getBotUsername() {        return BOT_NAME;    }    /**     * Ответ на запрос, не являющийся командой     */    @Override    public void processNonCommandUpdate(Update update) {        Message msg = update.getMessage();        Long chatId = msg.getChatId();        String userName = getUserName(msg);        String answer = nonCommand.nonCommandExecute(chatId, userName, msg.getText());        setAnswer(chatId, userName, answer);    }    /**     * Формирование имени пользователя     * @param msg сообщение     */    private String getUserName(Message msg) {        User user = msg.getFrom();        String userName = user.getUserName();        return (userName != null) ? userName : String.format("%s %s", user.getLastName(), user.getFirstName());    }    /**     * Отправка ответа     * @param chatId id чата     * @param userName имя пользователя     * @param text текст ответа     */    private void setAnswer(Long chatId, String userName, String text) {        SendMessage answer = new SendMessage();        answer.setText(text);        answer.setChatId(chatId.toString());        try {            execute(answer);        } catch (TelegramApiException e) {            //логируем сбой Telegram Bot API, используя userName        }    }}


Класс обработки текстовых сообщений
NonCommand.java
import ru.taksebe.telegram.mentalCalculation.exceptions.IllegalSettingsException;import ru.taksebe.telegram.mentalCalculation.telegram.Bot;/** * Обработка сообщения, не являющегося командой (т.е. обычного текста не начинающегося с "/") */public class NonCommand {    public String nonCommandExecute(Long chatId, String userName, String text) {        Settings settings;        String answer;        try {            //создаём настройки из сообщения пользователя            settings = createSettings(text);            //добавляем настройки в мапу, чтобы потом их использовать для этого пользователя при генерации файла            saveUserSettings(chatId, settings);            answer = "Настройки обновлены. Вы всегда можете их посмотреть с помощью /settings";            //логируем событие, используя userName        } catch (IllegalSettingsException e) {            answer = e.getMessage() +                    "\n\n Настройки не были изменены. Вы всегда можете их посмотреть с помощью /settings";            //логируем событие, используя userName        } catch (Exception e) {            answer = "Простите, я не понимаю Вас. Возможно, Вам поможет /help";            //логируем событие, используя userName        }        return answer;    }    /**     * Создание настроек из полученного пользователем сообщения     * @param text текст сообщения     * @throws IllegalArgumentException пробрасывается, если сообщение пользователя не соответствует формату     */    private Settings createSettings(String text) throws IllegalArgumentException {        //отсекаем файлы, стикеры, гифки и прочий мусор        if (text == null) {            throw new IllegalArgumentException("Сообщение не является текстом");        }        //создаём из сообщения пользователя 3 числа-настройки (min, max, listCount) либо пробрасываем исключение о несоответствии сообщения требуемому формату        return new Settings(min, max, listCount);    }    /**     * Добавление настроек пользователя в мапу, чтобы потом их использовать для этого пользователя при генерации файла     * Если настройки совпадают с дефолтными, они не сохраняются, чтобы впустую не раздувать мапу     * @param chatId id чата     * @param settings настройки     */    private void saveUserSettings(Long chatId, Settings settings) {        if (!settings.equals(Settings.getDefaultSettings())) {            Bot.getUserSettings().put(chatId, settings);        }    }}


Классы команд


Все классы команд наследуются от BotCommand.

Команды в моём боте делятся на 2 группы:

  • Сервисные возвращают справочную информацию;
  • Основные формируют файл с заданиями.

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

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

Абстрактный суперкласс Сервисных команд
ServiceCommand.java
import org.telegram.telegrambots.extensions.bots.commandbot.commands.BotCommand;import org.telegram.telegrambots.meta.api.methods.send.SendMessage;import org.telegram.telegrambots.meta.bots.AbsSender;import org.telegram.telegrambots.meta.exceptions.TelegramApiException;/** * Суперкласс для сервисных команд */abstract class ServiceCommand extends BotCommand {    ServiceCommand(String identifier, String description) {        super(identifier, description);    }    /**     * Отправка ответа пользователю     */    void sendAnswer(AbsSender absSender, Long chatId, String commandName, String userName, String text) {        SendMessage message = new SendMessage();        //включаем поддержку режима разметки, чтобы управлять отображением текста и добавлять эмодзи        message.enableMarkdown(true);        message.setChatId(chatId.toString());        message.setText(text);        try {            absSender.execute(message);        } catch (TelegramApiException e) {            //логируем сбой Telegram Bot API, используя commandName и userName        }    }}


Класс Сервисной команды на примере
StartCommand.java
import org.telegram.telegrambots.meta.api.objects.Chat;import org.telegram.telegrambots.meta.api.objects.User;import org.telegram.telegrambots.meta.bots.AbsSender;/** * Команда "Старт" */public class StartCommand extends ServiceCommand {    public StartCommand(String identifier, String description) {        super(identifier, description);    }    @Override    public void execute(AbsSender absSender, User user, Chat chat, String[] strings) {        //формируем имя пользователя - поскольку userName может быть не заполнено, для этого случая используем имя и фамилию пользователя        String userName = (user.getUserName() != null) ? user.getUserName() :                String.format("%s %s", user.getLastName(), user.getFirstName());        //обращаемся к методу суперкласса для отправки пользователю ответа        sendAnswer(absSender, chat.getId(), this.getCommandIdentifier(), userName,                "Давайте начнём! Если Вам нужна помощь, нажмите /help");    }}


В суперклассе Основных команд, помимо аналогичного метода отправки ответов, содержится формирование Word-документа.
OperationCommand.java
import org.telegram.telegrambots.extensions.bots.commandbot.commands.BotCommand;import org.telegram.telegrambots.meta.api.methods.send.SendDocument;import org.telegram.telegrambots.meta.api.methods.send.SendMessage;import org.telegram.telegrambots.meta.api.objects.InputFile;import org.telegram.telegrambots.meta.bots.AbsSender;import org.telegram.telegrambots.meta.exceptions.TelegramApiException;import ru.taksebe.telegram.mentalCalculation.calculation.Calculator;import ru.taksebe.telegram.mentalCalculation.calculation.PlusMinusService;import ru.taksebe.telegram.mentalCalculation.enums.OperationEnum;import ru.taksebe.telegram.mentalCalculation.fileProcessor.WordFileProcessorImpl;import ru.taksebe.telegram.mentalCalculation.telegram.Settings;import java.io.FileInputStream;import java.io.IOException;import java.util.List;/** * Суперкласс для команд создания заданий с различными операциями */abstract class OperationCommand extends BotCommand {    private PlusMinusService service;    OperationCommand(String identifier, String description) {        super(identifier, description);        this.service = new PlusMinusService(new WordFileProcessorImpl(), new Calculator());    }    /**     * Отправка ответа пользователю     */    void sendAnswer(AbsSender absSender, Long chatId, List<OperationEnum> operations, String description, String commandName, String userName) {        try {            absSender.execute(createDocument(chatId, operations, description));        } catch (IOException | IllegalArgumentException e) {            sendError(absSender, chatId, commandName, userName);            e.printStackTrace();        } catch (TelegramApiException e) {            //логируем сбой Telegram Bot API, используя commandName и userName        }    }    /**     * Создание документа для отправки пользователю     * @param chatId id чата     * @param operations список типов операций (сложение и/или вычитание)     * @param fileName имя, которое нужно присвоить файлу     */    private SendDocument createDocument(Long chatId, List<OperationEnum> operations, String fileName) throws IOException {        FileInputStream stream = service.getPlusMinusFile(operations, Bot.getUserSettings(chatId));        SendDocument document = new SendDocument();        document.setChatId(chatId.toString());        document.setDocument(new InputFile(stream, String.format("%s.docx", fileName)));        return document;    }    /**     * Отправка пользователю сообщения об ошибке     */    private void sendError(AbsSender absSender, Long chatId, String commandName, String userName) {        try {            absSender.execute(new SendMessage(chatId.toString(), "Похоже, я сломался. Попробуйте позже"));        } catch (TelegramApiException e) {            //логируем сбой Telegram Bot API, используя commandName и userName        }    }}


Класс Основной команды на примере
PlusMinusCommand.java
import org.telegram.telegrambots.meta.api.objects.Chat;import org.telegram.telegrambots.meta.api.objects.User;import org.telegram.telegrambots.meta.bots.AbsSender;import ru.taksebe.telegram.mentalCalculation.enums.OperationEnum;/** * Команда получение файла с заданиями на сложение и вычитание */public class PlusMinusCommand extends OperationCommand {    public PlusMinusCommand(String identifier, String description) {        super(identifier, description);    }    @Override    public void execute(AbsSender absSender, User user, Chat chat, String[] strings) {        //формируем имя пользователя - поскольку userName может быть не заполнено, для этого случая используем имя и фамилию пользователя        String userName = (user.getUserName() != null) ? user.getUserName() :                String.format("%s %s", user.getLastName(), user.getFirstName());        //обращаемся к методу суперкласса для формирования файла на сложение и вычитание (за это отвечает метод getPlusMinus() перечисления OperationEnum) и отправки его пользователю        sendAnswer(absSender, chat.getId(), OperationEnum.getPlusMinus(), this.getDescription(), this.getCommandIdentifier(), userName);    }}


Приложение


В методе main инициализируется TelegramBotsApi, в котором и регистрируется Bot.

TelegramBotsApi в качестве параметра принимает Class<? extends BotSession>. Если нет никаких заморочек с прокси, можно использовать DefaultBotSession.class.

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

Получаем вот такой
MentalCalculationApplication.java
import org.telegram.telegrambots.meta.TelegramBotsApi;import org.telegram.telegrambots.meta.exceptions.TelegramApiException;import org.telegram.telegrambots.updatesreceivers.DefaultBotSession;import ru.taksebe.telegram.mentalCalculation.telegram.Bot;import java.util.Map;public class MentalCalculationApplication {    private static final Map<String, String> getenv = System.getenv();    public static void main(String[] args) {        try {            TelegramBotsApi botsApi = new TelegramBotsApi(DefaultBotSession.class);            botsApi.registerBot(new Bot(getenv.get("BOT_NAME"), getenv.get("BOT_TOKEN")));        } catch (TelegramApiException e) {            e.printStackTrace();        }    }}


Деплой на heroku


Для начала нужно создать в корне проекта файл Procfile и написать в него одну строку:
worker: java -Xmx300m -Xss512k -XX:CICompilerCount=2 -Dfile.encoding=UTF-8 -cp ./target/classes:./target/dependency/* <путь до приложения, в моём случае ru.taksebe.telegram.mentalCalculation.MentalCalculationApplication>
, где worker это тип процесса.

Если в проекте используется версия Java, отличная от 8, также необходимо создать в корне проекта файл system.properties и прописать в нём одну строку:
java.runtime.version=<версия Java>

Далее порядок такой:

  1. Регистрируемся на heroku и идём в консоль;
  2. mvn clean install;
  3. heroku login после выполнения потребуется нажать любую клавишу и залогиниться в открывшемся окне браузера;
  4. heroku create <имя приложения> создаём приложение на heroku;
  5. git push heroku master пушим в репозиторий heroku;
  6. heroku config:set BOT_NAME=<имя бота> добавляем имя бота в переменные окружения;
  7. heroku config:set BOT_TOKEN=<токен бота> добавляем токен бота в переменные окружения;
  8. heroku config:get BOT_NAME (аналогично BOT_TOKEN) убеждаемся, что переменные окружения установлены верно;
  9. heroku ps:scale worker=1 устанавливаем количество контейнеров (dynos) для типа процесса worker (ранее мы выбрали этот тип в Procfile), при этом происходит рестарт приложения;
  10. В интерфейсе управления приложением в личном кабинете на heroku переходим к логам (прячутся под кнопкой More в правом верхнем углу) и убеждаемся, что приложение запущено;
  11. Тестируем бота через Telegram.

Если вы храните код на GitHub, то в интерфейсе управления приложением в личном кабинете на heroku на вкладке Deploy вы можете в дальнейшем переключить деплой на GitHub-репозиторий (по запросу или автоматически), чтобы не пушить параллельно в два репозитория.

Вместо заключения


Как выяснилось, не только лишь все видели чудесный советский мультик про козлёнка, который учился считать до 10.
Подробнее..

Раздел Refactor в IDEA

28.11.2020 16:23:21 | Автор: admin

Эту статью можно рассматривать как краткий обзор c gif-ками по рефакторингам Java-файлов в IDEA для начинающих.

Осторожно, много тяжелых gif-картинок.

"Any fool can write code that a computer can understand. Good programmers write code that humans can understand. M. Fowler (1999)

Содержание

Введение

Раздел Refaсtor

- Refactor This

- Rename

- Rename File

- Change Signature

- Edit Property Value (без примера)

- Type Migration

- Make Static

- Convert To Instance Method

- Move Classes

- Copy Classes

- Safe Delete

- Extract/Introduce

- - Variable

- - Constant

- - Field

- - Parameter

- - Functional Parameter

- - Functional Variable

- - Parameter Object

- - Method

- - Type Parameter (без примера)

- - Interface

- - Superclass

- - Subquery as CTE (без примера)

- - RSpec 'let' (без примера)

- Inline

- Find and Replace Code Duplicate

- Pull Member Up

- Pull Member Down

- Push ITds In

- Use Interface Where Possible

- Replace Inheritance with Delegation

- Remove Middleman

- Wrap Method Return Value

- Encapsulate Field

- Replace Temp with Query

- Replace Constructor with Factory Method

- Replace Constructor with Builder

- Generify

- Migrate

- Lombok и Delombok (без примера)

- Internationalize Список источников

Введение

Цель данной статьи - показать доступные способы рефакторинга для Java-файлов (многие способы будут работать и для других языков). Как использовать эти приемы в реальной жизни показано в замечательном видео Тагира Валеева (ссылка в списке источников).

Думаю, каждый, кто работает в IDEA, знает, что в ней куча способов для рефакторинга кода. И почти уверен, что каждый второй смотрит анонсы новой версии, где красиво показаны новые способы рефакторинга и заглядывал в раздел Refaсtor:

 Рис. 1. Раздел Refactoring IDEA Рис. 1. Раздел Refactoring IDEA

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

В статье представлены фрагменты кода, порядок действий и анимации почти для каждого пункта. Также постарался добавить, где возможно, ссылку на замечательную книгу Refactoring: Improving the Design of Existing Code (Martin Fowler). Чтобы не сильно раздувать трафик пришлось довольно сильно обрезать много gif-картинок, поэтому обязательно смотрите использованный код под катом. Горячие клавиши приведены для Windows/LInux по умолчанию.

Раздел Refaсtor

Пойдем сверху вниз по порядку.

Пункт Refactor This (Ctrl+Alt+Shift+T)

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

Рис. 2. Refactor This для имени функцииРис. 2. Refactor This для имени функцииРис. 3. Refactor This для аргументов функцииРис. 3. Refactor This для аргументов функции

Пункт Rename (Shift+F6)

Позволяет переименовать практически любой идентификатор в коде, будь то переменная или названия класса. Изменения распространяются по всему проекту, в некоторых случаях, включая и комментарии. (У Фаулера переименованию посвящено 2 главы - Rename Field и Rename Variable)

Рис. 4. Переименование методаРис. 4. Переименование методаИспользованный код

До

public class Main {   public static void main(String[] args) {       System.out.print("Hello");       invo<caret/>ke(", World");   }   private static void invoke(String text) {       //text       System.out.println(text);   }}

После

public class Main {   public static void main(String[] args) {       System.out.print("Hello");       newFunctionName(", World");   }   private static void newFunctionName(String text) {       //text       System.out.println(text);   }}
  • Переименование переменной

 Рис. 5. Переименование переменной Рис. 5. Переименование переменнойИспользованный код

До

public class Main {   public static void main(String[] args) {       System.out.print("Hello");       invoke(", World");   }   private static void invoke(String te<caret>xt) {       //text       System.out.println(text);   }}

После

public class Main {   public static void main(String[] args) {       System.out.print("Hello");       invoke(", World");   }   private static void invoke(String newText) {       //newText       System.out.println(newText);   }}
  • Переименование вложенного класса

 Рис. 6. Переименование вложенного класса Рис. 6. Переименование вложенного классаИспользованный код

До

public class Main {   public static void main(String[] args) {       System.out.print("Hello");       invoke(", World");   }   private static void invoke(String text) {       //text       System.out.println(text);       throw new MyExc<caret>eption();   }   public static class MyException extends RuntimeException {   }}

После

public class Main {   public static void main(String[] args) {       System.out.print("Hello");       invoke(", World");   }   private static void invoke(String text) {       //text       System.out.println(text);       throw new NewMyException ();   }   public static class NewMyException extends RuntimeException {   }}
  • Переименование класса

 Рис. 7. Переименование класса Рис. 7. Переименование классаИспользованный код

До

public class Main {   public static void main(String[] args) {       MyS<caret>ervice service = new MyService();       service.service();   }}

После

public class Main {   public static void main(String[] args) {       NewMyService myService = new NewMyService ();       myService.service();   }}
  • Переименование пакета

 Рис. 8. Переименование пакета Рис. 8. Переименование пакетаИспользованный код
package gen<caret>eral;public class Main {   public static void main(String[] args) {       NewMyService service = new NewMyService();       service.service();   }}

После

package org.test.java.src;public class Main {   public static void main(String[] args) {       NewMyService service = new NewMyService();       service.service();   }}

Пункт Rename File

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

 Рис. 9. Пример использования Rename File Рис. 9. Пример использования Rename FileИспользованный код

До

public class Main {   public static void main(String[] args) throws IOException {       Path path = Paths.get("src/general/TestFile.txt");       String read = Files.readAllLines(path).get(0);       System.out.println(read);   }}

После

public class Main {   public static void main(String[] args) throws IOException {       Path path = Paths.get("src/general/TestFile2.txt");       String read = Files.readAllLines(path).get(0);       System.out.println(read);   }}

Пункт Change Signature (Ctrl+F6)

У Фаулера этому посвящена глава Change Function Declaration. В новой версии IDEA Change Signature был немного доработан. Я знаю два пути:

  • первый путь - прямой - изменить сигнатуру метода и вызвать обновление,

  • второй - через диалоговое окно.

Пример для первого способа (через изменения сигнатуры метода)

Изменяем сигнатуру метода. В этот момент слева появляется "R" и в контекстном меню появляется пункт "Update usages to reflect signature change", который позволяет обновить все использования метода.

Рис. 10. Пример использования Update usages to reflect signature changeРис. 10. Пример использования Update usages to reflect signature changeИспользованный код

До

public class ChangeSignature {   public static void main(String[] args) {       invokeMethod("Hello");       invokeMethod("World");   }   private static void invokeMethod(String text) {       System.out.println(text);   }}

После

public class ChangeSignature {   public static void main(String[] args) {       invokeMethod("Hello", null);       invokeMethod("World", null);   }   private static void invokeMethod(String text, String newType) {       System.out.println(text);   }}

Пример для второго способа (через диалоговое окно)

В диалоговом окне можно изменить состав переменных, exception, и даже сгенерировать переопределенный метод.

Рис. 11. Пример использования Change SignatureРис. 11. Пример использования Change SignatureИспользованный код

До

public class ChangeSignature {   public static void main(String[] args) {       invokeMethod("Hello");       invokeMethod("World");   }   private static void invokeMethod(String<caret> text) {       System.out.println(text);   }}

После

public class ChangeSignature {   public static void main(String[] args) {       invokeMethod("Hello");       invokeMethod("World");   }   private static void invokeMethod(String text) {       invokeMethod(text, null);   }   private static void invokeMethod(String text, String newName) {       System.out.println(text);   }}

Пункт Edit Property Value (Alt + F6)

На данный момент (Idea 2020.2) экспериментальная функция и по умолчанию не включена. Включить можно параметром property.value.inplace.editing=true Поэтому примеры не привожу.

Пункт Type Migration (Ctrl + Shift + F6)

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

Рис. 12. Пример использования Type MigrationРис. 12. Пример использования Type MigrationИспользованный код

До

public class ChangeSignature {   public static void main(String[] args) {       Inte<caret>ger hello = 1;       print(hello);   }   private static void print(Integer text) {       System.out.println(text);   }}

После

public class ChangeSignature {   public static void main(String[] args) {       Number hello = 1;       print(hello);   }   private static void print(Number text) {       System.out.println(text);   }}

Пункт Make Static (Ctrl + Shift + F6)

Позволяет сконвертировать метод или внутренний класс в статический. (Противоположность Convert To Instance Method)

Рис. 13. Пример использования Make StaticРис. 13. Пример использования Make StaticИспользованный код

До

public class MakeStatic {   public static void main(String[] args) {       MakeStatic makeStatic = new MakeStatic();       makeStatic.sayHello();   }   public void say<caret>Hello() {       System.out.println("Hello, World");   }}

После

public class MakeStatic {   public static void main(String[] args) {       MakeStatic makeStatic = new MakeStatic();       MakeStatic.sayHello();   }   public static void sayHello() {       System.out.println("Hello, World");   }}

Пункт Convert To Instance Method

Позволяет сконвертировать статический метод в нестатический (противоположность Make Static). При этом можно указать к какому классу будет относится новый метод.

Рис. 14. Пример использования Convert To Instance MethodРис. 14. Пример использования Convert To Instance MethodИспользованный код

До

public class MakeStatic {   public static void main(String[] args) {       sayHello();   }   public static void sa<caret>yHello() {       System.out.println("Hello, World");   }}

После

public class MakeStatic {   public static void main(String[] args) {       new MakeStatic().sayHello();   }   public void sayHello() {       System.out.println("Hello, World");   }}

Пункт Move Classes (F6)

В принципе делает, что и написано, перемещает классы.

Рис. 15. Пример использования Move ClassesРис. 15. Пример использования Move ClassesИспользованный код

До

package org.example.test.service;public class TestService {<caret>}

После

package org.example.test;public class TestService {}

Пункт Copy Classes (F5)

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

 Рис. 16. Пример использования Copy Classes Рис. 16. Пример использования Copy Classes

Пункт Safe Delete (Alt+Delete)

По функциональности почти повторяет то, что можно получить через контекстное меню (Alt + Enter), но позволяет удалять чуть больше. Поэтому, я заметил, у многих знакомых любимый способ рефакторинга - F2(следующая ошибка) и Alt + Enter или Alt + Delete. Можно удалять классы, переменные, методы. Перед удалением IDEA выполнит поиск использования удаляемых элементов, и если IDEA найдет, что они где-то используется покажет диалоговое окно Usages Detected. Про удаление неиспользуемого кода у Фаулера есть целая глава - Remove Dead Code

 Рис. 17. Пример использования Safe Delete Рис. 17. Пример использования Safe DeleteИспользованный код

До

package org.example.test;public class MainClass {   public static void main(String[] args) {       start();   }   private static void start() {       String unUsedVariable;       System.out.println("Hello, World!");   }   private static void unUsedMethod() {   }}

После

<empty>

Пункт Extract/Introduce

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

 Рис. 18. Список доступных способов рефакторинга Extract/Introduce Рис. 18. Список доступных способов рефакторинга Extract/Introduce

Пункт Variable (Ctrl+Alt+V)

Создает новую переменную из выделенного фрагмента. (Этому способу у Фаулера посвящена глава Extract Variable).

 Рис. 19. Пример использования Extract/Introduce->Variable Рис. 19. Пример использования Extract/Introduce->VariableИспользованный код

До

public class ExtractVariable {   public static void main(String[] args) {       sayHello();   }   private static void sayHello() {       System.out.println("He<caret>llo, World!");   }}

После

public class ExtractVariable {   public static void main(String[] args) {       sayHello();   }   private static void sayHello() {       String text = "Hello, World!";       System.out.println(text);   }}

Пункт Constant (Ctrl+Alt+C)

Создает новую константу из выделенного фрагмента.

 Рис. 20. Пример использования Extract/Introduce->Constant Рис. 20. Пример использования Extract/Introduce->ConstantИспользованный код

До

public class ExtractVariable {   public static void main(String[] args) {       sayHello();   }   private static void sayHello() {       System.out.println("He<caret>llo, World!");   }}

После

public class ExtractVariable {   public static final String HELLO_WORLD = "Hello, World!";   public static void main(String[] args) {       sayHello();   }   private static void sayHello() {       System.out.println(HELLO_WORLD);   }}

Пункт Field (Ctrl+Alt+F)

Создает новое поле класса из выделенного фрагмента.

 Рис. 21. Пример использования Extract/Introduce->Field Рис. 21. Пример использования Extract/Introduce->FieldИспользованный код

До

public class ExtractVariable {   public static void main(String[] args) {       sayHello();   }   private static void sayHello() {       System.out.println("He<caret>llo, World!");   }}

После

public class ExtractVariable {   private static String x;   public static void main(String[] args) {       sayHello();   }   private static void sayHello() {       x = "Hello, World!";       System.out.println(x);   }}

Пункт Parameter (Ctrl+Alt+P)

Создает новый параметр (функции) из выделенного фрагмента.

Рис. 22. Пример использования Extract/Introduce->ParameterРис. 22. Пример использования Extract/Introduce->ParameterИспользованный код

До

public class ExtractVariable {   public static void main(String[] args) {       sayHello();   }   private static void sayHello() {       System.out.println("He<caret>llo, World!");   }}

После

public class ExtractVariable {   public static void main(String[] args) {       sayHello("Hello, World!");   }   private static void sayHello(String x) {       System.out.println(x);   }}

Пункт Functional Parameter

Очень похож на пункт Parameter, но теперь в функцию мы передаем или java.util.function.Supplier, или javafx.util.Builder. Обратите внимание, данный рефакторинг может привести к нежелательным эффектам.

Рис. 23. Пример использования Extract/Introduce->Functional ParameterРис. 23. Пример использования Extract/Introduce->Functional ParameterИспользованный код

До

public class ExtractParameter {   public static void main(String[] args) {       System.out.println(generateText());   }   private static String generateText() {       return "Hello, Wor<caret>ld!".toUpperCase();   }}

После

public class ExtractParameter {   public static void main(String[] args) {       System.out.println(generateText(() -> "Hello, World!"));   }   private static String generateText(final Supplier<string> getText) {       return getText.get().toUpperCase();   }}

Пункт Functional Variable

Очень похож на пункт Variable, но теперь мы получаем или java.util.function.Supplier или javafx.util.Builder.

 Рис. 24. Пример использования Extract/Introduce->Functional Variable Рис. 24. Пример использования Extract/Introduce->Functional Variable Использованный код

До

public class ExtractParameter {   public static void main(String[] args) {       System.out.println(generateText());   }   private static String generateText() {       return "Hello, W<caret>orld!".toUpperCase();   }}

После

public class ExtractParameter {   public static void main(String[] args) {       System.out.println(generateText());   }   private static String generateText() {       Supplier<string> getText = () -> "Hello, World!";       return getText.get().toUpperCase();   }}
ParameterObject

Пункт Parameter Object

Удобный способ, когда в функцию передается много аргументов и вам надо обернуть их в класс. (У Фаулера это глава Introduce Parameter Object).

Рис. 25. Пример использования Extract/Introduce->Parameter Object Рис. 25. Пример использования Extract/Introduce->Parameter Object Использованный код

До

public class ExtractParameter {   public static void main(String[] args) {       System.out.println(generateText("Hello", "World!"));   }   private static String generateText(Str<caret>ing hello, String world) {       return hello.toUpperCase() + world.toUpperCase();   }}

После

public class ExtractParameter {   public static void main(String[] args) {       System.out.println(generateText(new HelloWorld("Hello", "World!")));   }   private static String generateText(HelloWorld helloWorld) {       return helloWorld.getHello().toUpperCase() + helloWorld.getWorld().toUpperCase();   }   private static class HelloWorld {       private final String hello;       private final String world;       private HelloWorld(String hello, String world) {           this.hello = hello;           this.world = world;       }       public String getHello() {           return hello;       }       public String getWorld() {           return world;       }   }}

Пункт Method (Ctrl+Alt+M)

Извлекаем метод из выделенного фрагмента. (У Фаулера есть глава про похожий способ рефакторинга - Extract Function).

 Рис. 26. Пример использования Extract/Introduce->Method Рис. 26. Пример использования Extract/Introduce->Method Использованный код

До

public class ExtractMethod {   public static void main(String[] args) {       String text = "Hello, World!";       System.out.prin<caret>tln(text);   }}

После

public class ExtractMethod {   public static void main(String[] args) {       String text = "Hello, World!";       print(text);   }   private static void print(String text) {       System.out.println(text);   }}

Пункт Type Parameter

Рефакторинг из мира Kotlin, и для Java не применим (буду рад добавить, если кто-то сделает пример).

Пункт Replace Method With Method Object

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

Рис. 27. Пример использования Extract/Introduce->Replace Method With Method Object Рис. 27. Пример использования Extract/Introduce->Replace Method With Method Object Использованный код

До

public class ExtractMethod {   public static void main(String[] args) {       String text = "Hello, World!";       print(text);   }   private static void print(String text) {       System.out.p<caret>rintln(text);   }}

После

public class ExtractMethod {   public static void main(String[] args) {       String text = "Hello, World!";       print(text);   }   private static void print(String text) {       new Printer(text).invoke();   }   private static class Printer {       private String text;       public Printer(String text) {           this.text = text;       }       public void invoke() {           System.out.println(text);       }   }}

Пункт Delegate

Позволяет извлечь методы и поля в отдельный класс.

 Рис. 28. Пример использования Extract/Introduce->Delegate Рис. 28. Пример использования Extract/Introduce->Delegate Использованный код

До

public class Delegate {   public static void main(String[] args) {       new Delegate().print();   }   private void print() {       System.ou<caret>t.println("Hello, World!");   }}

После

public class Delegate {   private final Printer printer = new Printer();   public static void main(String[] args) {       new Delegate().print();   }   private void print() {       printer.print();   }   public static class Printer {       public Printer() {       }       private void print() {           System.out.println("Hello, World!");       }   }}

Пункт Interface

Для заданного класса и его методов создает интерфейс. (Особенно удобно, когда при работе со Spring, когда кто-то забыл для компонента создать соответствующий интерфейс)

 Рис. 29. Пример использования Extract/Introduce->Interface Рис. 29. Пример использования Extract/Introduce->Interface Использованный код

До

public class ExtractImpl {   public static void main(String[] args) {       new ExtractImpl().print();   }   public void print() {       System.out.println("Hello, World!");   }}

После

public class ExtractImpl implements ExtractInterface {   public static void main(String[] args) {       new ExtractImpl().print();   }   @Override   public void print() {       System.out.println("Hello, World!");   }}public interface ExtractInterface {   void print();}

Пункт Superclass

Аналогично пункту Interface, только теперь создается класс-родитель (Superclass). Фаулер описывает этот способ рефакторинга в главе Extract Superclass.

Рис. 30. Пример использования Extract/Introduce->Superclass Рис. 30. Пример использования Extract/Introduce->Superclass Использованный код

До

public class ExtractImpl {   public static void main(String[] args) {       new ExtractImpl().print();   }   public void print() {       System.out.println("Hello, World!");   }}

После

public class ExtractImpl extends ExtractAbstr {   public static void main(String[] args) {       new ExtractImpl().print();   }}public class ExtractAbstr {   public void print() {       System.out.println("Hello, World!");   }}

Пункт Subquery as CTE

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

Пункт RSpec 'let'

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

Пункт Inline

Возможно один из самых крутых методов рефакторинга, Инлайнить можно почти все. Фаулер описывает этот способ рефакторинга в главах Inline Class, Inline Function, Inline Variable.

 Рис. 31. Пример использования пункта Inline Рис. 31. Пример использования пункта InlineИспользованный код

До

public class Inline {   public static void main(String[] args) {       print();   }   private static void print() {       new Printer().print();   }   private static class Printer {       public void print() {           String text = "Hello, World!";           System.out.println(t<caret>ext);       }   }}

После

public class Inline {   public static void main(String[] args) {           System.out.println("Hello, World!");   }}

Пункт Find and Replace code duplicate

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

 Рис. 32. Пример использования пункта Find and Replace code duplicate Рис. 32. Пример использования пункта Find and Replace code duplicateИспользованный код

До

public class Replace {   public static void main(String[] args) {       System.out.println("Hello, World!");   }   public void print() {       System.out.println("Hello, World!");   }   public void print2() {       System.out.prin<caret>tln("Hello, World!");   }}

После

public class Replace {   public static void main(String[] args) {       print2();   }   public void print() {       print2();   }   public static void print2() {       System.out.println("Hello, World!");   }}

Пункт Invert Boolean

Позволяет инвертировать булевые переменные.

Рис. 33. Пример использования пункта Invert BooleanРис. 33. Пример использования пункта Invert BooleanИспользованный код

До

public class Invert {   public static void main(String[] args) {       boolean co<caret>ndition = true;       if (condition) {           System.out.println("Hello, World!");       }   }}

После

public class Invert {   public static void main(String[] args) {       boolean condition = false;       if (!condition) {           System.out.println("Hello, World!");       }   }}

Пункт Pull Member Up

Позволяет перемещать методы или поля по иерархии вверх. Зачем это нужно написано у Фаулера в главах Pull Up Field и Pull Up Method. Выполняет обратную задачу пункта Pull Member Down.

Рис. 34. Пример использования пункта Pull Member UpРис. 34. Пример использования пункта Pull Member UpИспользованный код

До

public class PullMethod {   public static void main(String[] args) {       new InnerClass().print();   }   private static class InnerClass extends AbstClass {       public void print() {           System.out.pri<caret>ntln("Hello, World");       }   }   private static abstract class AbstClass {   }}

После

public class PullMethod {    public static void main(String[] args) {        new InnerClass().print();    }    private static class InnerClass extends AbstClass {    }    private static abstract class AbstClass {        public void print() {            System.out.println("Hello, World");        }    }}

Пункт Pull Member Down

Выполняет обратную задачу пункта Pull Member Up. Позволяет перемещать методы или поля по иерархии вниз. (У Фаулера - глава Push Down Method)

Рис. 35. Пример использования пункта Pull Member DownРис. 35. Пример использования пункта Pull Member DownИспользованный код

До

public class PullMethod {   public static void main(String[] args) {       new InnerClass().print();   }   private static class InnerClass extends AbstClass {   }   private static abstract class AbstClass {       public void print() {           System.out.prin<caret>tln("Hello, World");       }   }}

После

public class PullMethod {   public static void main(String[] args) {       new InnerClass().print();   }   private static class InnerClass extends AbstClass {       @Override       public void print() {           System.out.println("Hello, World");       }   }   private static abstract class AbstClass {       public abstract void print();   }}

Пункт Push ITds In

Используется при работе с AsperctJ.

 Рис. 36. Пример использования пункта Push ITds In Рис. 36. Пример использования пункта Push ITds InИспользованный код

До

aspect myAspect {   boolean Account.closed = <caret>false;   void Account.close() {       closed = true;   }}class Account {}

После

aspect myAspect {   boolean Account.closed = false;}class Account {   void close() {       closed = true;   }}

Пункт Use Interface Where Possible

IDEA старается заменить, где это возможно, указания классов на указание интерфейсов.

Рис. 37. Пример использования пункта Use Interface Where PossibleРис. 37. Пример использования пункта Use Interface Where PossibleИспользованный код

До

public class ExtractInterface {   public static void main(String[] args) {       InnerClass innerClass = new InnerClass();       print(innerClass);   }   private static void print(InnerClass innerClass) {       innerClass.print();   }   private static class InnerClass implements InnerInterface{       @Override       public void print() {           System.out.println("Hello, World!");       }   }   private static interface InnerInterface{       void print();   }}

После

public class ExtractInterface {   public static void main(String[] args) {       InnerInterface innerClass = new InnerClass();       print(innerClass);   }   private static void print(InnerInterface innerClass) {       innerClass.print();   }   private static class InnerClass implements InnerInterface{       @Override       public void print() {           System.out.println("Hello, World!");       }   }   private static interface InnerInterface{       void print();   }}

Пункт Replace Inheritance with Delegation

Заменяет наследование делегированием. У Фаулера про это главы Replace Subclass with Delegate и Replace Superclass with Delegate.

Рис. 38. Пример использования пункта Replace Inheritance with DelegationРис. 38. Пример использования пункта Replace Inheritance with DelegationИспользованный код

До

public class InheritanceDelegation {   public static void main(String[] args) {       InnerClass innerClass = new InnerClass();       print(innerClass);   }   private static void print(InnerClass innerClass) {       innerClass.print();   }   private static class In<caret>nerClass extends AbstractClass {   }   private static class AbstractClass {       public void print() {           System.out.println("Hello, World!");       }   }}

После

public class InheritanceDelegation {    public static void main(String[] args) {        InnerClass innerClass = new InnerClass();        print(innerClass);    }    private static void print(InnerClass innerClass) {        innerClass.print();    }    private static class InnerClass {        private final AbstractClass abstractClass = new AbstractClass();        public void print() {            abstractClass.print();        }    }    private static class AbstractClass {        public void print() {            System.out.println("Hello, World!");        }    }}

Пункт Remove Middleman

Заменяет все делегированные вызовы на прямые. (У Фаулера - глава Remove Middle Man).

 Рис. 39. Пример использования пункта Remove Middleman Рис. 39. Пример использования пункта Remove MiddlemanИспользованный код

До

public class Middleman {   public static void main(String[] args) {       InnerClass innerClass = new InnerClass();       innerClass.print();   }   private static class InnerClass {       private final NextClass next<caret>Class = new NextClass();       public void print() {           nextClass.print();       }   }   private static class NextClass {       public void print() {           System.out.println("Hello, World!");       }   }}

После

public class Middleman {   public static void main(String[] args) {       InnerClass innerClass = new InnerClass();       innerClass.getNextClass().print();   }   private static class InnerClass {       private final NextClass nextClass = new NextClass();       public NextClass getNextClass() {           return nextClass;       }   }   private static class NextClass {       public void print() {           System.out.println("Hello, World!");       }   }}

Пункт Wrap Method Return Value

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

 Рис. 40. Пример использования пункта Wrap Method Return Value Рис. 40. Пример использования пункта Wrap Method Return ValueИспользованный код

До

public class WrapMethodReturnValue {   public static void main(String[] args) {       System.out.println(new MessageFolder().get());   }   private static class MessageFolder {       public String get() {           ret<caret>urn "Hello, World!";       }   }}

После

public class WrapMethodReturnValue {   public static void main(String[] args) {       System.out.println(new MessageFolder().get().getValue());   }   private static class MessageFolder {       public Message get() {           return new Message("Hello, World!");       }       public class Message {           private final String value;           public Message(String value) {               this.value = value;           }           public String getValue() {               return value;           }       }   }}

Пункт Encapsulate Field

Скрывает поле за getter, setter.

 Рис. 41. Пример использования пункта Encapsulate Field Рис. 41. Пример использования пункта Encapsulate FieldИспользованный код

До

public class EncapsulateField {   public static void main(String[] args) {       System.out.println(new InnerClass().message);   }   private static class InnerClass {       public String m<caret>essage = "Hello, World!";   }}

После

public class EncapsulateField {   public static void main(String[] args) {       System.out.println(new InnerClass().getMessage());   }   private static class InnerClass {       private String message = "Hello, World!";       public String getMessage() {           return message;       }       public void setMessage(String message) {           this.message = message;       }   }}

Пункт Replace Temp with Query

Пусть у вас есть

int size = getActualSize()

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

 Рис. 42. Пример использования пункта Replace Temp with Query Рис. 42. Пример использования пункта Replace Temp with QueryИспользованный код

До

public class ReplaceTemp {   public static void main(String[] args) {       String hello = "Hello";       String mes<caret>sage = hello + ", World!";       System.out.println(message);   }}

После

public class ReplaceTemp {   public static void main(String[] args) {       String hello = "Hello";       System.out.println(message(hello));   }   private static String message(String hello) {       return hello + ", World!";   }}

Пункт Replace Constructor with Factory Method

Генерирует фабричный метод для указанного конструктора. Идеально, если у вас нет Lombok. (У Фаулера этому посвящена глава Replace Constructor with Factory Function).

Рис. 43. Пример использования пункта Replace constructor with factory methodРис. 43. Пример использования пункта Replace constructor with factory methodИспользованный код

До

public class ReplaceConstructor {   public static void main(String[] args) {       new InnerClass("Hello", "World").print();   }   private static class InnerClass {       private String message;       public Inner<caret>Class(String hello, String world) {           message = hello + ", " + world;       }       public void print() {           System.out.println(message);       }   }}

После

public class ReplaceConstructor {   public static void main(String[] args) {       InnerClass.createInnerClass("Hello", "World").print();   }   private static class InnerClass {       private String message;       private InnerClass(String hello, String world) {           message = hello + ", " + world;       }       public static InnerClass createInnerClass(String hello, String world) {           return new InnerClass(hello, world);       }       public void print() {           System.out.println(message);       }   }}

Пункт Replace Constructor with Builder

Генерирует builder для указанного конструктора. Идеально, если у вас нет Lombok.

Рис. 44. Пример использования пункта Replace Constructor with BuilderРис. 44. Пример использования пункта Replace Constructor with BuilderИспользованный код

До

public class ReplaceConstructor {   public static void main(String[] args) {       new InnerClass("Hello", "World").print();   }   private static class InnerClass {       private String message;       public InnerC<caret>lass(String hello, String world) {           message = hello + ", " + world;       }       public void print() {           System.out.println(message);       }   }}

После

public class ReplaceConstructor {   public static void main(String[] args) {       new InnerClassBuilder().setHello("Hello").setWorld("World").createInnerClass().print();   }   static class InnerClass {       private String message;       public InnerClass(String hello, String world) {           message = hello + ", " + world;       }       public void print() {           System.out.println(message);       }   }}public class InnerClassBuilder {   private String hello;   private String world;   public InnerClassBuilder setHello(String hello) {       this.hello = hello;       return this;   }   public InnerClassBuilder setWorld(String world) {       this.world = world;       return this;   }   public ReplaceConstructor.InnerClass createInnerClass() {       return new ReplaceConstructor.InnerClass(hello, world);   }}

Пункт Generify

Пытается код с raw-типами превратить в код с Generic-типами. Актуален при миграции с java версий ранее 1.5 на современные версии.

Рис. 45. Пример использования пункта GenerifyРис. 45. Пример использования пункта GenerifyИспользованный код

До

public class Generify {   public static void main(String[] args) {       List list = getList();       Object message = list.get(0);       System.out.println(message);   }   private static List getList() {       ArrayList arrayList = new ArrayList();       arrayList.add("Hello, World!");       return arrayList;   }}

После

public class Generify {   public static void main(String[] args) {       List<string> list = getList();       String message = list.get(0);       System.out.println(message);   }   private static List<string> getList() {       ArrayList<string> arrayList = new ArrayList&lt;>();       arrayList.add("Hello, World!");       return arrayList;   }}

Пункт Migrate

Предоставляет готовые миграции для следующего списка:

Рис. 46. Список доступных миграций пункта MigrateРис. 46. Список доступных миграций пункта Migrate

А также предоставляет возможность делать свои. Вот, например, правила миграции для JUnit(4.x -> 5.0):

Рис. 47. Правила миграции для JUnit(4.x -> 5.0)Рис. 47. Правила миграции для JUnit(4.x -> 5.0)

Вот здесь есть подробное видео про миграцию для JUnit(4.x -> 5.0).

Пункт Lombok и Delombok

Предоставляются плагином Lombok. Недавно было объявлено, что теперь он будет входить в стандартную поставку IDEA. Используется при работе с библиотекой кодогенерации Lombok.

Пункт Internationalize

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

Список источников

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

Категории

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

© 2006-2020, personeltest.ru