На самом деле это не ридми, а стенограмма лекции Е.Борисова
Пишем свой SpringData-стартер! Для работы сo Spark.
Попутно затронуты темы:
- Что такое Spark и как с ним работать в Java
- работа по конвенциям Spring Data
- механика работы SpringBoot на ранних этапах посторения контекста (более подробно эта тема разобрана здесь )
- как безопасно запустить Спринг внутри Спринга? о_О
Тут и там встречаются хорошие советы общего характера:
- по технике написания сложного кода.
- как удержать основную идею, не потонув в детялях.
- как защитить код от необдуманного вмешательства (реализация O/C Principle)
- ...
- прочие мудрости от самого популярного русскоязычного Java-разработчика :)
1:30 После как разберемся, как в принципе это работает, первая конкретная задача - это зарегистрировать бины и наших репозитории, из классов, которых в принципе не существует природе. Как работает Спринг-Дата? Мы пишем какой-то там интерфейс, там методы findByName() и т.д., класс не пишем. И потом во все наши сервисы инжектить, все отлично инжектится. При этом если посмотреть реализацию того что занжектилось, то там все-таки какой-то класс есть, соответственно нам надо разобраться, как можно из несуществующих классов насоздавать сингальтонов и засунуть их в контекст.
Есть разные техники, как этому как как этому можно прийти, и мы обсудим, что нам более удобно - это можно делать через регистрары (см предыдущий доклад), это можно делать через ApplicationContext6Initializer'ы (см доклад про СпрингБут с кириллом). Можно это попытаться через Листенер сделать. Нам надо понять куда мы воткнём вот этот код, который сделает эту магию: нагенерит классов, насоздает бинов, и зарегистрирует их в контексте.
Дальше у нас будет очень интересная дилемма потому что как вы понимаете когда пишется какой-то фреймворк на Спринге, который должен что-то делать с этим с нашим контекстом, туда чё-то регистрировать то мы не сможем пользоваться спрингом на этапе разработки нашего кода, то есть когда вы берете спринг и начинате писать бизнес-логику, вы можете там autoiwired делать, constructor injection, transaction и всю прочую магию. А когда вы пишете какой-то код, который будет что-то совать в контекст, на этапе когда контекст еще не построен, то как бы не получится использовать Спринг, а поскольку писать мы будем сложную штуку, то будет интересно написать так чтобы в конечном итоге код можно было поддерживать-читать, чтобы все было понятно.
3:26 Заодно рассмотрим, как в Hibernate реализованная загрузка ленивых коллекций. Это немножко выходит за рамки про СпрингДату, но это тем не менее очень связано со всем этим миром (см доклад Н. Алименкова "босиком по граблям Hibernate", там он рассказывает, какие неправильные ответы ему дают на собеседованиях, а как на самом деле это реализовано, так и не сказал, мы здесь откроем это секрет) Наш аналог СпрингДаты будет возвращать объекты, у которых могут быть вложены ленивые коллекции, который был подгружаться тогда как кним будут обращаться.
4:45 Слайд 1, ПЛАН ДОКЛАДА
- Как написать (регистрация бинов из несуществующих классов)
- Где написать (Regiatrar? AppCtxInitializer? ContextRefresh?)
- Как написать красивый код (без Спринга)
- Раскрыть секрет Алименкова (ленивые коллекции)
- Как это уже сделано в настоящей СпрингДате
последний пункт на слайде - посмотреть как это в принципе все сделано в реальной СпрингДате. При подготовке доклада возникла дилема, сразу залезть посмотреть в кишки СпроингаДаты, или написать что-то аналогичное не подглядывая, а потом сравнить. И решил вот пойти по этому пути.
Когда я закончил и расковырял СпрингДату (спасибо Г Зайцеву из Epamm), то при сравнении с тем что я написал, нашлись вещи которые принципиально отличаются, но самые важные вещи оказались достаточно похожи, поэтому если мы напишем полностью с нуля, нам будет намного более понятно как это все работает, чем если мы будем смотреть дикое количество очень сложных классов которые не везде написаны по дизайн-паттернам. Видимо когда пишут такие сложные фреймворки, редко получается написать идеально. Встречаются методы которые делают сразу 3 вещи одновременно, по двести-триста строчек кода внутри метода. Очень сложно это понимать, намного проще если мы напишем это все с нуля.
Итак, начнинаем думать, что мы будем писать. В конвенции спрингДата мы должны создавать интерфейс, у которого будут методы, которые вот там типа fineBy...(), и если задуматься, а что можно сообщить при помощи названия этого метода то тут есть как бы по большому счету на данный момент два вида операции: у нас есть разные виды фильтров потому что после findBy..() при помощи and мы можем клеить разные фильтры. типатакого findByNameContainsSortByAge(String partOfName)
у фильтра есть название у фильтра есть field по которым фильтруется это может быть контент может быть и просто может быть там целый список слов и можно добавлять сортировки на какие-то полям и приятно при помощи and клеить какие филды должны участвовать в сортировке.
поскольку мы будем писать аналог СспрингДаты для spark там на самом деле возможно мы захотим чуть чуть больше возможности всяких разных делать поэтому мы задумались об этом чуть ближе к делу
7:35 чтобы все это заработало, в первую очередь нам надо написать какой-то код, который на каждый такой интерфейс будет генерить динамический класс на лету. Вообще чтобы сгенерить класс на лету у нас есть разные библиотеки, которые умеют это делать. мы будем пользоваться стандартной Java-библиотекой которая называется Дайнамик прокси, она идет джаве в пакете java.lang.reflect - не надо будет импортировать, потому что такая достаточно стандартная штука, очень многие инфраструктуры и пользуются потому что очень многие фромборке генерят на лету классы. Есть еще одна порулярная библиотека, которая тоже умеет генерить классы, она называется CGLib, и она действует немножко по другому: она создает наследника от какого-то указанного класса и например Мокито пользуются для ваших моков, Hibernate пользуется когда он там тоже делать всякие разные прокси для ваших entity... Нам эта библиотека не понадобится, потому что мы хотим генерить классы из интерфейсов. Библиотека Дайнамик прокси умеет создавать класс который реализует указанный интерфейс или несколько интерфейсов и понятно что в сгенерированном классе будут все методы, которые указаны в интерфейсе. Вопрос - что эти методы будут делать? Ну понятно что можно написать какой-то код который будет создавать байт-код и прописывать какие-то методы, но как он догадается что эти методы должны делать?
В дизайн паттерне Прокси это все выглядит следующим образом: вначале вы пишете интерфейс, потом вы создаете прокси класс, в котором будут те же самые методы. Реализации этих методов идет в InvocationHandler, который вы при создании прокси класса передаете, у него один-единственный метод invoke(), поэтому можно его даже лямбда передать, и он принимает сигнатуру метода который вызвали его прокси-класс, аргументы которые ему передали и зачем-то передается сам объект прокси.
Визуально я долго думал какую аллегорию взять мне кажется что очень хорошо олицетворяет дизайн паттерн Прокси вот этот Крэнг из Черепашек Ниндзя: допустим, эта штука будет не прозрачна и люди которые будут общаться с этим существом, они будут думать что это он, соответственно ему вызывают какие-то методы, например беги(30). Он не знает что делать, но у него есть InvocationHandler, который сидит внутри, потому что мы передадим tuij при создании этогопроски, и он будет там прикреплен и соответственно каждый раз на вызов любого метода он будет бежать к своему InvocationHandler'у - "тут меня вызвали метод бежать и параметр 30 километров в час, что надо делать?" И в результате логику по факту будет реализовывать InvocationHandler, если метод беги что-то возвращает то соответственно опять же наш хэндлер должен будет что-то вернуть обратно прокси, прокси возвращает тому кто обратился к нему. То есть, для всех кажется, что прокси такой умный классный объект, который офигенно работает, а по факту это очень тупой объект, который ничего не умеет делать, но никто про это не знает потому что за него все решает InvocationHandler, абсолютно любые методы острыми бегает его и говорит и соответственно в нашем 11:01 случае когда будет вызываться метод файл был штамм эклз или там контент и так 11:06 далее он будет выборки шимко для передавать информацию о том чтобы вызван именно этот метод параметры которые туда 11:14 пришли а дальше и напишем федор будет решать как на это реагировать и что надо вернуть
11:19 //важное!
Таким образом очень важно понимать что мы на каждый Repository-интерфейс. который мы обнаружим в нашем класспасе (а мы вообще-то делаем стартер, который все будут подключать как SpringDataJPA, и мы потом придумаем, как сделать так чтобы пользователь нашего стартера имел возможность сообщить какие пакеты надо сканировать, где искать наши репозитории, и на каждый обнаруженный репозиторий будет генерится вот это вот "Крэнг", в него будет сетиться очень умный InvocationHandler, который знает как анализировать название метода и как понять, какую логику надо запустить.
//Слайд: PersonRepository UserRepository TurtleRepository
12:05 Допустим у нас есть интерфейс, который называется PersonRepository, у нас естественно появится какой-то свой интерфейс аналогичном JPARepository у нас будет SparkRepository, при помощи дженерика мы будем сообщать, какой модели этот репозиторий. И дальше мы пропишем какие-то методы.
//Слайд "SparkInmvocationHandler долджен уметь:"
interface PersonRepo extends SparkRepository<Person>{
List<Person> findByNameContainsSortByAge(String partOfName);
List<Person> findByAgeGreaterThan(String age);
long.count();
long findByAgeGreaterThanCount(String age);
long findByAgeGreaterThanSave(String age);
...
}
Здесь на примере сделано специально немножечко больше методов, чем это делает обычно спрингДата - кроме стандартных методов которые они поддерживают, где только фильтры и сортировка, у нас будут специфичнфе мектоды для раброты со Спарком.
12:34
стоит наверное даже сейчас буквально минуточку потратить на то чтобы объяснить про spark тем кто не знаком, а для этого чуть чуть объясним концепцию стримАпи. появилась она с 8 джавы, но она появилась далеко не только там если java не добавила в себя очень много всего что связано с функциональным программированием, то наверно очень большую часть рынка потеряла, потому что для современных задач подход функционального программирования очень часто хорошо ложится на наши бизнес-задачи, особенно когда мы работаем с той же самой big data и когда уж много данных и мы не очень понимаем как нам по этим данным итерироваться, потому что они физически не влезают в одну машину, и поэтому мы хотим работать в функциональном стиле: не мы вытягиваем данные, потом по ним итерируемся и что-то с ними делаем, а мы стоим цепочку из трансформаций, передаем всякие разные функции, и потом говорим - ну давайте, кто-нибудь там запустите эту цепочку, и там вот либо Стрим АПИ, либо Spark, либо Скалавский API умеет в функциональном стиле запустить все наши объектики, все наши трансформации.
И я бы на самом деле вот такой подход сравнил бы с выпечкой пирога. У вас есть бесконечное количество трансформаций, которые вы можете сделать на ваши данные. В аллегории с пирогом данные - это какие ингредиенты, из которых мы лепим пирог. И мы там что-то отфильтровываем, потом что-то там мэпим, как-то там тесто наше сбиваем... и таких операций может быть очень много и все эти НЕ терминальные трансформационные операции всегда нам возвращают обратно нашу вот эту заготовку. Мы что-то отфильтровали, и теперь заготовка после того, как что-то отфильтровалось. Потом еще что-то отфильтровали, что-то с мэпировали, отсортировали. И в конечном итоге мы поставим пирог либо в духовку, либо мы поставим на солнышко, либо там в морозилку, либо кастрюлю чтобы сварился. То есть какая-то терминальная операция, после которой пирог будет готов и можно подавать на стол.
15:00 Мы принципе тоже можем рассматривать эти методы как какую-то цепочку действий, которые производятся на данными, а потом в конце получаем результат: у нас есть вот всякие разные findByNN..(), и у нас есть count(). В стримAPI, если я хочу в конечном итоге сохранить свои данные, то у меня нет никакой проблемы вызвать метод findByName(), он мне вернет коллекцию, а потом я эту коллекцию куда хочу туда и сохраню, и нет никакой проблемы. А когда мы говорим про Spark, то к терминальным операциям, которые поддерживаются Спринг-датой, хотелось бы еще добавить метод save().
Со Спарком сделать collect достаточно проблематичная штука, потому что если у меня данных мало collect сработает. Но если данных много, то у меня будет out of memory эксепшен. Хоть spark он абсолютно аналогично стримAPI - там тоже есть трансформации, там даже методы все похоже называются. Но разница заключается в том что sаrk использует кластер, его можно настроить - сколько машин, сколько памяти у машин... Вы пишете по-прежнему ваши цепочки, но в отличие от stream API где максимум что можете сделать - это сказать что наш стрим это ParallelStream, и тогда будет использоваться хотя бы многопоточность для вычисления всех этих операций. То Спарк может еще использовать очень много машин, соответственно данные размазаны по кластеру, да и в принципе данные очень часто выкачиваются из хранилища, которое тоже задействует тот же самый кластер, и наши там всякие файлы в htfs streamBucket'е. Они тоже сидят на большом количестве машин поэтому как бы их намного проще обрабатывать используя те же самые машины. И если в конце мне надо сохранить данные, то какой смысл собрать их в одну машину на драйвере, а потом как-то сохранять, может быть я захочу просто отсортировать то что мне надо, сгруппировать как надо, отфильтровать то что не надо, и сказать - а теперь сохранить.
Поэтому у меня чуть-чуть будет отличаться конвенция кроме всех этих findBy....() у меня возможно в конце поставить какое то слово которое называется терпим как думаете по терминальная операция так называемый финал айзен да то есть если я заканчиваю метод словом count(), это не значит что старк должен на драйвер притащить все результаты со всех машин, а потом я буду вызывать у получившегося колекшена size(), это будет сумасшедший трафик который может закончиться, зачем? Я могу сразу в методе сказать что я хочу сделать каунт того что получилось, и никто не будет эти данные мне реально вытаскивать на драйвер, или я могу добавить слово size случай если я не добавил ни какого слова будет стандартный collect ....
17:35 Вот что должно быть у нашего умного InvocationHandler'a, который будет сетится в этот прокси объект, для того чтобы он мог реализовать эту цепочку.
//Слайд "Что имеет SparkInvocationHndler?"
- засечен класс Модели
- ссылка на данные для данной модели
- DataExtractor
- Трансформации (у каждого метода свой список)
- Терминальная операция (у каждого метода своя)
Во первых в нем должен быть засечен класс модели: если мы про PersonRepository, он же назначен отвечать за Person, класс модели но хотя бы потому что модель будет хранить в себе какую-то метаинформацию о том, где находятся физически эти данные также как Hibernate: там вы пишите класс, помечаете его @Entity, а потом можете при помощи @Table() рассказать, где находится таблица. То же самое будет у меня у меня будет класс модели, и нее наверняка появится со временем какая-то аннотация @Source, которая будет ссылаться на то место, где лежит файл с этими данными. И понятно, что мой SparkInmvocationHandler, который в конечном итоге будет запускать всю цепочку трансформаций, должен будет начать с того что он вытащит эти данные, и поэтому ему естественно нужна 1) модель для того чтобы с неё считать информацию, где данные лежат,2) ему нужна модель для того чтобы знать какие есть field'ы у данной модели, потому что это тоже будет использоваться в анализах всех этих методов, 3) у нас будет DataExtractor, потому что мы за siongle responsibility, нам и так будет сложно написать красивый код, т к очень много всего сложного, поэтому мы будем стараться, чтобы наши наши объекты составлялись из вспомогательных объектов, а не делали все сами, поэтому соответственно у каждого InvocationHandler'a будет DataExtractor, с которым он будет начинать свою цепочку. 4) потом у него будет трансформации, которых может быть бесконечное количество. 5) И у нас будет терминальная операция у каждого InvocationHandler'a она всегда будет одна на каждый метод.
Cколько пишу что например класс модели 1 штука навесим да конечно потому что ну если два kinder помогает песчаной почве торе тон все методы связаны с пирсом если он и соответственно наша модель она тоже у 19:38 нас одна на весь репозитории и data extractor нас тоже один на весь репозиторий потому что неважно что будет делать 19:44 метод но если этот метод выкачивает данные то есть с там будет выкачивать данные тем же самым data extractor 19:50 потому что data extractor уже настроен выкачивать их из того места куда указала модель трансформации у нас будет много на 19:58 каждый метод методы может состоять из большого количества соваться мольбой ним и пост 20:04 бла-бла-бла and its бла-бла-бла and ордера что-то там
поэтому трансформации будет много на каждый метод, и терминальная операция будет одна, но на каждый метод. то есть у нас уже сам видится какие-то мапы и какие-то листы...
теперь еще одна вещь должна быть SparkInvocationHandler - у него должен быть sparkSession ну или там какой-то SparkContext. Контекст - это аналог энтити-менеджера. Понятно, что если SparkInmvocationHandler будет пользоваться дата экстрактором, то для того чтобы дата экстрактор мог выкачать данные, ему нужно пользоваться SparkAPI. Сейчас не будем щас глубоко и детально уходить в SparkAPI, но основной объект который у нас в Спарке есть, он либо называется SparkSession, и с его помощью можно выкачивать данные откуда-то и потом навешивать на то что получится трансформации, либо SparkContext, если мы пользуемся более старым API. В джава это называется JavaSparkContext (они постоянно это меняют, потому что у них изначально была такая скаловская политика, что вот мы взяли какой-то класс и стали типа теперь он не интересен - либо деприкейтед, либо выпилили.... теперь надо вот этим пользоваться, он лучше). Поэтому я на всякий случай чтобы у нашего InvocationHandler'a был доступ к любому виду Spark'овских объектов тоже, я не знаю что они поменяют завтра, поэтому пусть он лучше держит референс на Контекст, в которыми естественно зарегистрированы все бины связанные со Спарком, а он уже даже будет выковыривать что ему нужно. В нашей реализации мы будем всегда SparkSession'ом пользоваться, поэтому в принципе можно было бы его туда инжектить но так будет удобнее.
21:38
Давайте мы сразу напишем а потом будем думать что мы делаем дальше. Нам надо написать.... IDE: пустое спрингбутовое приложение структура каталогов:
src/main
com/epam/repetition/
sparkdatastarterrepetition/
SparkDataStarterRepetitionApplication
starter
data
criminals.csv
orders.csv
Ну и нам нужен какой-то пример с данными, давайте будем о черном списке - помните такой сериал "черный список"?
//содержимое data/criminals.csv:
id,name,number
1,Ekaterina Rostova,12
2,Edmund Dandes,13
3,Linkoln Barrows, 43
4,John Abruci,18
Вот так меня будет выглядеть данные. то есть это такой csv файл, в котором у нас есть три колонки: есть айдишник есть имя человека черного списка и есть его номер черном списке это не айдшник, это уникальные номера которые NAME им всем раздает.
//содержимое data/orders.csv:
name,desc,price,criminalId
Jack,kidnap,100,1
John,kill,200,2
Bill,murder,150,2
Sauron,prison,1500,4
Чуть позже появится вложенная коллекция которая будет хранить заказы на всякий разный там киднэпинги, убийства, тюрьмы и так далее, которые будут по CriminalId мэпироваться, но это будет потом, когда мы дойдем до ленивых коллекций.
давайте писать это дело модели.
Раньше я когда учил как писать стартер, я всегда принципиально дело открывал 2 intelliJ, в одном писал стартер, в другом писал проект, который им пользуется. Но поскольку все таки сейчас стартер написать это не главное (хотя мы это делать будем), то это будет все в одном проекте, просто вот у меня есть отдельный пакет, который будет называться starter, и чуть позже там будут появляться все вещи которые относятся к стартеру, а тут у нас типа бизнес-логика. Эти пакеты как бы параллельны, и один другой не видит. Поэтому модель мы будем делать тут значит делаем класс модели на забавному криминал //на трансляции всё заглючило 27:20 сделаем новый проект Spring Initializr (на java 11) Зависимости никакие не проставляем,т к из старого проекта просто скоопируем Пом
29:22 todo рашифровать ...вот пока он это все подтягивается и синхронизируется я вам покажу зависимости очень мало у меня зависимости на самом деле у меня есть стартовал arrow потому что мы будем использовать hp для ленивых коллекций мы здесь имеем скалу потому что нам некоторые вещи скале понадобится. Ну, по любому скалу за собой тянет spark, поэтому мне нужно было прописать ту версию который я хочу поэтому все равно эту библиотеку я указал. SparkSQL мы тоже будем пользоваться. Стартер-веб принципе не нужен. но пусть будет... Ломбок - куда без него. И есть библиотека reflections, в которой есть полезные всякие штуки, поскольку мы будем писать внутри, до того как спринт построим то соответственно мне придется самому просканировать, вот эта штукой удобно это делать. но и в принципе как у все остальное не интересно стандарт
теперь возьмем наши модел //У автора по-прежнему глючит Идея //32:30 отпустило
Пишем дата-класс Criminal, аннотированный @Data, @Builder и полный и пустой конструкторы. там будут поля наших преступников - id, name, number (это не id)
Заведем для него репозиторий - интерфейс CriminalRepository extends SparkRepository (его тоже заведём). Туда объвим метод List findByNumberBetween(int min,int max)
Теперь перейдем в сам стартер. В пакете starter создадим интерфейс SparkInvocationHandler extends java.lang.reflect.InvoctionHandler, и сразу имплементим его.
Вспомним, что должно быть у нашего InvocationHandler'a: во-первых, класс самой Модели private Class<?> modelClass;
Во-вторых String pathToData.
В третьих - какой-то Эстрактор private DataExtractor dataExtractor Сразу создадим его, это однозначно будет интерфейс,т к экстракторы у нас будут разные - для CSV, JSON, (?)паркет фйла, (?) абро файла, из таблицы...
в-четвертых, послетого как вычиталиданные,нукжноиметь лист каких-то трансфрмаций private List transformationChain Сразу заведём интерфейс SparkTransformation, их у нас тоже будет много разных. И кстати это должен быть не лист,а Мапа, т.к. укаждогометода будет свой список трансформаций private Map<Method, List> ...
И еще должна быть мапа с терминальной операцией на каждый метод private Map<Method, Finalizer> finalizerMap Тоже заведем интерфейс, т.к Finalizer'ов может быть много разных - count, collect, save и т.д.
Ну и тут еще должен сидеть весь Контекст - чтобы можно было вытащить SparkSession, или какие-то другие бины. private ConfigurableAppicationContext context;
38:33 Теперь давайте попытаемся записать метод Invoke(), а откуда это сюда попадет, мы потом разберемся.
На самом деле достаточно большой ценностью этого доклада может быть возможность посмотреть, как писать код. Есть много интересных техник: можно писать сверху вниз, как мы сейчас делаем. Можно писать снизу вверх. Можно писать через тесты, можно писать начиная со стейта.
В данном случае мне было удобней понять, что нужно иметь InvocationHandler'у для того чтобы он мог выполнять свою работу. А теперь я хочу понять, как вот эти все вещи сложатся в цепочку, при помощи которой он сделает то, что нам от него нужно.
Первое, у нас должен быть сейчас DataExtractor, которого мы попросим принести все наши данные, значиту него должен быть такой метод типа readData(), этот метод будет принимать, откуда взять данные (pathToData), и вроде пока больше ничего, потом если что добавим..
Давайте сразу мы напишем этот метод. Он нам должен вернуть ни в коем случае не Лист, некую структуру Spark'a, потому что мы же хотим при помощи спарка делать трансформации. А когда мы вытащим данные (если это collect например), то это уже будет не spark'овский объект, а обычный лист. Но на этапе всех трансформаций мы хотим работать с спарковскими структурами.
В Спарке есть в принципе три вида структур: есть старая структура RDD, которая очень похожа на стрим АПИ, это просто стрим объектов. У нас есть Dataset, с которым мы будем работать сегодня. Если он содержит в своем дженерике Датасет объекта(?), то это примерно как RDD, просто как стрим объектов но если он содержит в себе содержит объект Raw (типа строчка), то это будет DataFrame - то есть такая колоночная структура, и с ней очень очень удобно и эффективно работать
public interface DataExtractor { Dataset readData(String PathToData); }
41:20 Идем обрато в InvocationHandlerImp. Вот мы получили наш первичный Датасет, от срочки(raw). Dataset dataset = dataExtractor.readData(pathToData); Теперь мне надо при помощи всех вот этих трансформаций его там как-то там фильтровать, или там что-то еще сделать. Поэтому мы можем просто проитерироваться по всем нашим <...?>
41:37 идем нашу TransformationChain и говорим, что для данного метода поэтому можем против метода получить цепочку трансформаций (хорошо что нам наш этот тупой гигант передает сюда метод который вызвали) проитерироваться по этой цепочке и попросить чтобы каждая трансформация сделала, допустим transform на этот dataset
List<SparkTransformarion> ... = transformatioChain.get(method);
for (SparkTransformation transformation: ...) {
dataset.transformation.transform(dataset);
}
Для этого у нас должен быть метод transform который принимает dataset и может быть что-то еще, но пока мы не знаем что еще. Поэтому пока оставим так всегда можно попробовать починить.
После того как мы закончили датасет трансформировать, нам надо сделать терминальную операцию, соответственно мы теперь идем в finalazerMap и говорим, что там против этого метода, отобрали финалайзер, и говорим, давай сделай файналайз. или там doAction()
Finalizer finalizer= finalizerMap.get(method);
finalizer.doAction(dataset);
Эта штука нам неизвестно что вернет, потому что финалайзеры могут ничего не возвращать, если они делают save(). Или может возврщать Boolean -типа смог-не смог. Может коллекцию если это collect() может long если это был count(). Ну то что он вернет это просто вернем и мы.Назовем это retVal
Object retVal= finalizer.doAction
И вот его-то и вернет наш InvocationHandler.
43:20 Вот мы сделали то, что должно быть на начальном этапе. И теперь, чтобы понять, что делать дальше, стоит хотя бы чуть-чуть дописать что-то из существующих классов. Например, как у нас будет работать data extractor? Давайте напишем напишем extractor для JSON файлов.
Имплементим JsonDataExtractor, и вот сразу видно чего не хватает DataExtractor'у: он захочет вытащить объект SparkSession в первую очередь. Соответственно, сюда ему надо тоже передавать этот ConfigurableApplicationContext context, из которого мы сможем вытащить бин спарксейшена, и у него есть замечательный метод read(), которому можно сказать json(..указать путь данным) context.getBean(SparkSession.class).read().json(pathToData) и сделать на это return;
(cигнатуру починим в интерфейсе (подозреваю что ее придется не раз чинить)) Это очень простой экстрактор, будут намного более сложные.
Теперь напишем другой extractor для CSV-файлов, он нам пригодится, т.к мы будем с ними работать. Копируем все, Имя CsvDataExtractor, в конце read() будет не json а csv(). Но в CSV есть намного больше нюансов, потому что в json есть схема, поэтому мне нужно будет её применить. Надо будет сказать через такая вот штука в ридере у Спарка - можно настроить всякие разные опции, для этого после read() ставим .option().
Ему мы скажем,что наш csv с колонками ("header", true), и еще скажем применить изначально схему из этих колонок. То есть ставим еще один .option("inferSchema", true), и после всех опций уже .csv() Еще есть энкодеры, но они понадобятся потом, когда буду выкачивать этот коллекции, а пока на этом этапе они не нужны.
46:33 Вот у нас есть такие вот разные экстракторы. Нужный экстрактор будет сидеть у нас в InvocationHansdler'e. Он вытащит датасе, он всеми трансформациями его обработает, файналазйером финализирует, и то что вернётся он вернет. Возвращаюсь в презенташку давайте думать дальше.
Теперь нам нужно создать инфраструктуру для Spark'a. Ну вот эти вот бины типа SparkSession - они же должны каким-то образом в контекст попасть, раз у нас вот эти вот ребята их вытаскивают (invocationHandler.invoke()). То есть в этом контексте на этом этапе уже должен находиться SparkSession, соответственно нам нужно будет зарегистрировать всё что связано со Спарком - это первое. Потом нам надо просканировать все пакеты в которых могут находиться наши интерфейсы которые наследуют SparkRepository. Потом мы на каждый из них нагенерим этих "Крангов", и зарегистрируем в контекст.
47:23 Все просто? казалось почти нечего делать. Но нам надо сначала принять очень сложное решение - мы это будем делать на уровне бинов, или на уровне бин-дефинишенов?
Давайте я напомню что такое бин-дефинишен. Я придумал как объяснить,что такое бин-дефинишенов в одном слайде. Что такое бин, вы все знаете - @Component поставили, вот вам и бин. Его можно инжектить, им можно пользоваться, он может быть синглтоном... Ну это объект,который менеджится Спрингом. А что такое бин-дефинишен?
А бин-дефинишен можно представить примерно вот так вот: это рецепт для приготовления бина. И там будет написано: надо взять бин из такого-то класса, добавить него соль, перец, положить его в кастрюлю.... Ну то есть прописать, какие у него там инит-методы есть, какие у него инжекшены должныбыть и т.д.
СеЙчас мы это все делаем при помощи аннотаций, поэтому бин-дефинишен не содержит такой большой информации, но когда на xml писали, там было столько всякой информации - как настраивать бин... а потом уже кто-то из этих бин-дефинишенов создаёт бины согласно этой инструкции, кидает их в контекст, и потом из контекста их могут все себе инжектить, забирать и т.д.
Давайте думать: у нас есть три таких глобальных способа, куда мы можем воткнуть код, который будет делать всё то что мы сказали - зарегистрировать Спарковские бины, зарегистрировать наши репозитории.. куда мы это все воткнем?
48:48 Первое -Мы можем попытаться начать с Регистрара, про который я говорил на прошлом докладе. Давайте чуть-чуть вспомним, что такое регистрары, для чего они вообще существуют.
Важно понимать, когда они работают, к чему у них есть доступ на том этапе когда они работают, как их можно задекларировать, и вообще для чего они нужны. Попробую тоже объяснить это в одном слайде: примерно вот так:
//на картинке самосвал высыпает рецепты бинов в контекст
Регистрар - это такая штука, который умеет создавать вот эти рецепты приготовления бобов, то есть разные бин-дефинишены, заливает их в контекст... На самом деле даже не в контекст - если вы посмотрите, какой метод вам надо будет имплементировать, если вы пишете свой Регистрар, то тут на этом этапе еще даже нету контекста. Тут есть только Registry, которая в принципе часть контекста в которую мы регистрируем бин-дефинишены, из которых потом будут создаваться бины. Поэтому на данном этапе никаких бинов нету, и их еще нельзя регистрировать. И для того чтобы регистрар начал работать (то есть - как его задекларировать?) - нужно в нашей конфигурации поставить @Import на ваш класс, который является регистраром, то есть импеметирует интерфейс BeanDefinitionRegistrar.
Аннотация @Import обрабатывается Спрингом на очень раннем этапе, потому что через @Import
подтягиваются не только регистрары, а еще всякие Конфигурации, Импорт-селекторы, и соответственно тот кто эту аннотацию считывает, он видит что есть Регистрар, и он его создаст и запустит метод registerBeanDefinitions(...) и передаст ему вот этот объект BeandefinitionRegistry, чтобы он мог прописывать мог прописывать бин-дефинишены, причем еще дают такой полезный объект BeanNameGenerator, т.к у каждого бина же есть еще какое-то имя (id), и соответственно если вы как бы хотите иметь стандартный механизм который эти id будет делать, то вам уже тут в подарок дали генератор. Хотите сами пишете id, хотите им пользуйтесь...
И вот в начале я как раз думал делать через регистрар и, самое смешное что я пришел к выводу что наверно через него - это если не невозможно, то настолько неудобно и сложно, что наверно умные люди так делать не будут, и отказался от регистраров, потому что слишком сложно. А потом выяснилось, что СпрингДата именно им все и делает.
Почему это сложно и неудобно? Смотрите что получается: мне намного удобней работать с бинами: я создаю бин СпаркКонтекста, сразу его в Спринг контекст. Я создаю бин своего репозитория, и сразу пишу его в контекст... Туда можно что-то заинжектить, его можно как-то настроить как объект. Если я буду работать с Бин-дефинишенами, то мне это придется делать на уровне metadata, это очень сильно все усложнит (но мы потом посмотрим,каээто реалищовано в Спринге,и это не настолько сложно, как мне казалось)
/* из другого доклада: Если делать через бин-дефинишн, то надо прописать название класса,изкоторого создадут этот бин. А какое будет название класса, ведьон сгенерится на лету! $$proxy7, что ли? Доаустим даже это можно сделать, но ведь впотом в этот проскси надозасетить invocationHandler! А если мы прописываем бин-дефинишн, то объектбина создаем не мы, а Спринг, как он узнает какой InvocationHandler нужно засесить,они все должны быть разные, и ни в коем слуяае не синглтоны. Кроме того там еще нет контекста, а как быть со спаркСешном, JavaSparkContext и прочими? Все это в комплексе показалось слишком сложно. Намного проще раюотать с объектами,чем писать такой хитрый бин-дефинишн (аналогия с натуральной вакциной и векторной вакциной) */
Но я вам потом покажу под конец, как в Спринге с этим разобрались, и оказалось что это все на самом деле не настолько сложно, как это мне казалось вначале. Хотя по факту разница все равно не очень большая, потому что какая разница у меня будет очень хитро Бин-дефинишен, из которого все создастся как нужно, или я сразу уже зарегистрировал в контекст нужный мне объект? И если так вот задуматься, какая уже такая полезная дополнительная информация есть в этом бин-дефиншене, кроме информации о том классе, который должен создать этот объект? По сути, там я посмотрел, и практически ничего интересного нету. Поэтому я ничего не теряю от того, что я сразу буду создавать бины, и сразу пихать их в контекст. И это нельзя делать на уровне регистрара, это можно делать позже.
Тогда я подумал, что наверное было удобнее всего использовать какон-нибудь Listener, который будет слушать refresh() контекста. Почему так? Потому что ну на refresh() Контекста уже все бины, есть и можно заинжектить все мои Спарк-сейшены куда мне нужно и т.д. н
Но оказалось, что вот так сделать невозможно вообще. Потому что это слишком поздно, потому что - вы понимаете, что все вот эти ваши PersonRepository и CriminalRepository - они так или иначе будут инжектится в какие-то ваши сервисы. Соответственно на этапе когда контекст рефрешнулся, сервисы уже все создались и уже в контексте сидят. Как же мы можем зарегистрировать бины, которые являются репозиториями, которые должны заинжектится в сервисы, если как бы сервисы уже создались? Это уже поздно! Мне надо, чтобы они были созданы сначала. Поэтому от этой идеи просто пришлось отказаться.
Короче мне надоело думать, надо было что-то делать, и я решил использовать ApplicationContextInitializer. Что это такое? Это такая штука, которая работает на очень раннем этапе, когда контекст уже есть но в контексте еще нет бинов. Но их уже можно пихать. Регистрар не может пихать бины в контекст, потому что у него даже нету доступа в контекст, а есть доступ только к Registry. А ApplicationContextInitializer - уже из одного названия понятно, что соответственно можно будет им воспользоваться. Давайте мы попробуем его написать.
Слайд:
BeanDefinitionRegistrar?
слишком сложно
ContextRefreshedEvent?
слишком поздно
ApplicationContextInitializer?
как вариант
53:53
Пишем класс SparkDataApplicationContextInitializer iplements ApplicationContextInitializer
c методом initialize
в нем делаем registerSparkBeans(context)
пишем этот метод. Как регистрируются бины? Сначала надосоздать, SparkSession созлается оч просто: var SparkSession.builder().appName().master().getOrCreate(); в master("local[*]") передаем, что спарк будлет работать локально (на кластере в след раз) appName - нужно для логов Спарка, ибез этой инфы нельзясоздат.
Но, мы хотим ничего хардколить, мы зотим чтобы пользователь пошел в application.properties и написал там то-то типа spark.application.name=blacklist Но проблема,что здесь нет Автокомплита.
Сделаем автокомплит в Идее, для этого напишем фиктивный класс, котороый нужен только для этого. азовем его например SparkPropsHolder над ним поставим @ConfigurationProperties(prefix="spark") а в теле класса пропишем какие у нас бывают проперти: private String appName; ... packagesToScan;
Правда Идея говрит,что этот класс не зарегистрирован в EnabledConfigurationProperties - а нам это ине нао,мы небудем им пользоваться. Там гдемыбудем им пользоваться, еще никакой @Autowired не будет работать. Он нам нужем лишь для тогочтобызапустить maven install
Теперь мы соберем проект, и сгенерируется Json, который умеет читать Идея, чтобы делать автокомплит. За это отвечает sopring-boot-configuration-processor в Поме (это похоже на Ломбок) Теперь, когда автокомплит заработал,этот фейк класс можно дажестереть,т к Json сгенерился. Но оставим, влруг потом захотим добавить в автокомплит что-тоеще
Итак,автокомплит все. Вопрос - откуда придет AppName, настройки? Откуда эти параметры можем взять? Да из Environment же! Который можно вытащить изконтекста.
//см пред листинг
appName(context.getEnvironment().getProperty("spark.app-name"))
/* тут пара слов про Environment, его можно заинжектить, но только не сюда, потому что сюдапока ничего нельзяинжектить,т к инжекшен пока не начал работать. Но вообще если нам нужны какие-то проперти из енвайронмента, можно сделатьнад @Value и инжектить проперти точечно. Еще можно инжектить бины,являющиеся холдерами (которые @ConfigurationProperties), если нам нужна пачка их. Или можно хзаинжектить весь Environment? и потом из него вытаскивать то что надо */
в конце этому всему сделаем .getOrCreate(),после чего у нас создастся Спарк-сешшен.
Потом создадиь JavaSparkContext() - на вскуий случай, он точно пригодится - его вытаскивают из СпаркСешшена
Теперь их надо зарегистрировать -как? Вытаскиваем из Контекста BeanFactory, и просим ее зарегистрировать registerSingleton() - они же синглетоны - и по задумке и по реализации, и вообще работают только в единственном экземпляре на все прилодение,т к если создадим ещеодин СпаркКонтекст, то упадет,потому что на один JVM подсаживать второй СпаркКонтекст это плохо, он тут кластерами управлет...
context.getBeanFactory().regiasterSingleton("sparkSession", sparkSession);
context.getBeanFactory().regiasterSingleton("sparkContext", sparkContext);
Итак,зарегистрировали Бины. Дальше по плану - Просканировать пакеты и найти наследников SparkReposotiory
Для этого созздадим scanner = Reflections() вкоторый передадим пакеты, которые также возьмекм изпроперти "spark.packagesToScan" из Environment. (у СпрингДаты тожеесть такой сканер,называется провайдер)
у сканера возьмем getSubTypesOf(SparkRepository.class).forEach(sparkPerositoryInterface->{ // и вот здесь мы создаем этиг "Крангов", то есть Проксей: Object crang = Proxy.newProxyInstance(любой,например_sparkRepositoryInterface.getClassLoader(), /массив всех интерфейсов/ new Class[] {sparkRepositoryInterface}, /наш InvocationHandler....непонятно, откуда? оставим пока так/ invHandler);
1:06:17
Не выходя из forEach() точно таким же способом регистрируем нажего "Кранга"-проксю. context.getBeanFactory().registerSingleton(Introspector.decapitalize(sparkRepositoryInterface.getSimppleName()) /чтобы перва буква была маленткая/ , crang); });
закрываем forEach
1:09:29 еще раз: мы зарегистрировали бин Cпарка, мы взяли сканер просканировали указанные нам пакеты, нашли все интерфейсы sparkRepository, для каждого интерфейса создали "Кранга" с помощью библиотеки Прокси.
И единственное что мы не знаем - это как мы сюда передадим InvocationHandler, и откуда вы вообще его возьмем, и дальше мы зарегистрировали в контексте синглтон (а все репозитории - синглтоны), название у которого это название репозитория с маленькой буквой. и "кранга", который наш прокси-объект. Пока все понятно.
1:10:06 давайте тогда будем думать над вот этим красным цветом: откуда мы возьмем InvocationHandler? Надо понимать, что InvocationHandler это совсем не простая штука, её нужно серьезно и долго настраивать, причем как настраивать?
Тот кто будет его настраивать, должен анализировать название методов данного интерфейса и вычислять какие надо трансформации, какие файналазйеры, и так далее.
Мы с вами за single responsibility, и однозначно не доверим нашему ContextInitializer'y заниматься такой фигнёй как создание InvocationHandler'a, это явно не его responsibility. Кто это будет делать? Это будет делать естественно SparkInvocationHandlerFactory, к этой фабрике будет обращаться тот кому нужны эти InvocationHandler'ы, их много, и каждый из них заведует своей моделью: вот этот вот InvocationHandler по машинам специалист, он будет InvocationHandler для CarRepository, а этот там UserRepository, а этот ещё в процессе настройки. Но любой может сюда прийти и попросить его.
1:11:30 Давайте подумаем как будет происходить это вот выполнение? Вот, допустим, у нас в репозитории есть метод, который я специально очень длинно назвал, чтобы получше проанализировать как будет идти этот анализ, для того чтобы разобраться как InvocationHandler настроить:
List<User> findByNameOfБабушкаContainsAndAgeLessThanOrderByAgeAndNameSave()
//чтобы было более понятно, на слайде разбито по цветам, мы же поставим пробелы // здесь сделующий слайд
fieldName fieldName fieldName
| | | |
List findBy NameOfБабушка Contains And Age LessThan OrderBy Age And Name Save()
| \ | | / |
Model \ filterTransformation / Finalizer
| \ /
PathToSource '--------- StrategyName ---------'
_ _ _ _ _0 o 0 o 0 o _ _ _ _ Transformation
SparkTransformations Chain
Что тут есть интересного? Во-первых,тут есть модель ///*нам интересно вы им задали где отсюда можно взять модель на скале чтобы пиши когда */// можно взять из своего интерфейса из дженерика, но она нам нужно дальше у нас всегда все будет начинаться с название стратегии.
Стратегия у нас может быть findBy, и может быть orderBy, и в принципе все, в обычной СпрингДате больше никаких стратегий нету. В СпаркДате теоретически со временем может быть может быть такой groupBy запилить, ещё какой-нибудь "xxxBy" придумать. Но пока мы ограничимся двумя.
После названия стратегий у вас идет название проперти, для которого эта стратегия должна применяться. Это может быть проперти из одного слова, может быть из многих слов, "nameOfБабушка" это у нас будет большая проблема.
Дальше у нас идет название фильтра, то есть если мы сейчас находимся в стратегии findBy, то у нее есть своя конвенция:
После findBy всегда идет название филда, а потом идет название конкретного фильтра для стратегии findBy, потом идет либо слово "And", либо конец (этих "And" может быть бесконечное количество).
Если у меня идет слово "And", я понимаю, что стратегия не меняется, мы продолжаем пользоваться стратегией findBy. Теперь у нас идет следующая property и потом соответственно следующее название фильтра, потом опять же будет либо конец, либо And если мы продолжаем в той же стратегии. Либо у нас все переключиться на новую стратегию orderBy понятно у нас есть модель которая берется из PathToSource....
Давайте сразу это сделаем, чтобы было понятно, откуда она будет приходить - на наш класс Криминал, должно быть какая-то такая вот аннотация @Source("data/criminals.csv") 1:13:58 Сделаем такую аннотацию, и у него есть string value. Теперь из модели мы можем вытащить просто source.
//Описание слайда выше:
Дальше, у нас есть все имена филдов, дальше название наших стратегий, дальше у нас некий "паук" который вместо паумтины плетет нашу цепь трансформаций.
Наша фабрика, когда ей нужно будет вычислить следующую трансформацию, она такая - "окей, у нас вначале стоит findBy. эта трансформация из мира фильтров, кто за нее отвечает? (у нас ведь singleResponsibility) К кому мы обратимся для того чтобы он сказал, кукую конкретно трансформацию надо делать? Обращаются к стратегии - вот к этому "паучку", который отвечает за findBy. А он уже может понять, какой нужен фильтр transformation
1:15:23 Он знает, что тут идут property - название филда, потом название фильтра, и в зависимости от этого названия он нужную filterTransformation из себя выплюнет.
И таким образом у меня вот тут эти "паучки", и к ним все время обращаются: иногда к этому паучку который findBy, иногда кдругому, который orderBy возвращает. И они выплевывают вот эти sparkTransformations, из которых у нас получается transformationChain. Ну и в конце у нас тут естественно Файналазайзер, который определяется либо последним словом, либо его отсутствием: если слово отсутствует, значит collect()
1:15:54 Итак, что нужно иметь нашей фабрике для того чтобы она могла InvocationHandler наполнять?
-
Нам нужны все существующие data extractor'ы потому что каждому InvocationHandler'y надо будет положить ДатаЭкстрактор, который связан с тем типом данных, который загружается в его модель.
-
Нужны все виды Finalizer'ов, потому что мы не можем знать какой Finalizer для какого метода и для какого репозитория понадобится.
-
Нам нужны все виды "spider"ов. Пока их два, завтра может быть теоретически больше, которые нам будут выдавать Спарк трансформации...
-
На всякий случай нужно еще весь контекст спринговый, потому что мы видели, что он там используется, передается - поэтому пусть он тоже будет чтобы там SparkSession все могли брать, наверняка всякие dataExtractor'ы и всякие "spider"ы будут тоже пользоваться SparkSession, поэтому они смогут его отсюда забирать.
Короче, у нас получается, что фабрика обложилась вообще всеми друзьями на все случаи жизни - у нее есть все инструменты для того чтобы строить InvocationHandler, и.... Как-то уже возникает вопрос - а как мы всё это настроим?
То есть мы понимаем, что фабрика настраивает InvocationHandler, да. А кто фабрику настроит?
Давайте сначала попробуем ее начать писать, а потом разберемся. Опять же возвращаемся к стилю такому больше по ТДД - сначала пишу метод, а потом смотрю, чего мне не хватает, чтобы этот метод заработал, и вхожу в детали, какие дополнительные компоненты нужны.
Cоздадим этот SparkInvocationHandlerFactory. У него должен быть: (пред слайд:) -Экстрактор -все файналайзеры -все "SPider"'ы которые будут отдавать SparkTransformations -весь контекст на всякий слуяай
//Значит, мы идем наш ApplicationContextInitializer, и говорим что
Начнем с кода. Пишем метод create(), он будет возвращать SparkInvocationHandler. Что мы сюда будем получать? Интерфейс, для которого надо сделать этот InvocationHandler, точнее это будет интерфейс, который который наследует от sparkРепозитори. create(Class<? extends SparkRepository> repoInterface)
Во-первых надо разобраться, откуда выкачивать данные, потому что иначе мы не сможем понять, какой extractor это должен делать. Соответственно, нам нужно вытащить модель, из этой модели мы можем вытащить где лежат даннные: repositoryInterface.getGenericInterfaces()[0] назначим переменную ParameteryzedType genericInterfaces, и закастим в него (непонятно зачем они так сделали,т.е непонятно почему они не сделали еще разные более удобные способы для достаточно стандартных ситуаций) Поясню: например, к нам пришел интерфейс CriminalRepository extends SparkRepository, и нам надо из него выцарапать вот этот дженерик
Теперь мне надо взять этот genericInterfaces.getActualTypeArguments()[0] тоже на нулевом месте. Назначим переменную Type modelClass Вот это класс модели, это надо закастить в Class<?> иначе будет неудобно работать.
Теперь я могу взять этот modelClass и вытащить из него аннотацию @Source (толлько что ее сделали), и взять у неё value(), и это будет у нас String pathToData
1:20:41 что еще нам нужно? Имена филдов. Их однозначно надо будет собрать заранее, потому что все вот эти "спайдеры", которые должны настроить нужную spark трансформацию, должны знать, для каких филдов это делается, и поскольку эта логика будет ни в одном месте использоваться... То есть, мы пришли к первому "спайдеру" и сказали - вот, давай вот у тебя есть "nameOfБабушкаContainsAndAge....", мы не знаем, что тут твое, разбирайся сам. Ну он естественно должен сначала взять название филда. А как он знает, что что это название филда не "name", а именно "nameOfБабушка", как он знает, где оно заканчивается? Поэтому в любом случае нашему "спайдеру" это понадобится.
Спайдер который отвечает за ордер buy который навешивать нам трансформации которая сортировку делают ему тоже нужно вот что "age" есть, и что "name" есть... Всем нужны названия филдов, это я уже сейчас вижу. Поэтому почему бы не вытащить, раз уж все равно мы модель знаем. getDeclaredFields(), вытащили все филды. На самом деле был бы неплохо их как-то отфильтровать давайте наверно сделаем так
Arrays.stream(modelClass......).filter()
....
1:22:08 А воробще, давайте сделаем как в Hibernate - красиво. Человек ведь может написать модель, в которую засунет какой-то филд, который вообще никак не связан с данными? Соответственно, его не надо учитывать. Поэтому мы сделаем такую аннотацию, которая будет называться как? Правильно, @Transient
соответственно можем сказать .filter(field->!field.isAnnotationPresent(Transient.class)) , только наоборот - "not @Transient"
1:22:52 Теперь. Если у меня здесь появится какой-то филд, который будет являться коллекцией - и чуть позже у нас оно появится в нашей модели - у нас могут появиться коллекции. Коллекции мы вообще отдельно будем - у нас они будут ленивые по умолчанию, поэтому на этапе когда мы мапим на модель, мы их будем скипать, поэтому коллекции нам сейчас пока не нужны. Поэтому мы должны сделать еще один фильтр.... ну давайте скажем
!Collection.class.isAssignableFrom(field.getType) - понятна суть?
Вы не можете спросить у какого-то класса, наследуешь ли ты от кого-то. Но мы можем наоборот, мы можем взять папу и сказать - этот чувак тебя наследует или нет. Поэтому я хочу проверить, что тип данного филда не является коллекцией. И после этого я хочу все это собрать в лист .collet(Collectors.toSet())
Вот это всё у нас будет Set fieldNames, т.к. зачем нам Лист, мы можем сделать Сет т.к. они уникальны. Потом можно вытащить все это в какой-то метод, чтобы не было слишком длинно - TODO
1:24:41 Теперь нам надо разобраться какой extractor должен использоваться для того чтобы засетить его в InvocationHandler, чтобы тот воспользовался им, когда ему придется выкачивать данные. Мы уже знаем, что экстракторов у нас есть как минимум 2, а завтра будет 15... А как вообще мы знаем, какой экстрактор использовать? Может быть, со временем эта логика очень сильно усложнится, но на данный момент я могу ограничиться окончанием файла - если это .csv, то у нас csv-экстрактор, если это у наc .json, то это джейсон экстрактор, или там паркет экстрактор.Возникает следующий вопрос: вот эта логика, которая будет подбирать правильный экстрактор в зависимости от окончания файла или в зависимости еще от какой там метаинформации, которая со временем у нас может появиться... Правильно ли, чтобы InvocationHandlerFactory этим занимался? Или мы это можем делегировать какому-нибудь хорошему другу? Давайте делегировать хорошему другу
1:25:35 У нас будет вот такой чувак private DataExtractorResolver extractorResolver, создадим его. Наверное даже это будет не интерфейс, а давайте сразу класс сделаем. У него должен быть метод у него должен быть метод, который принимает pathToData (потом можно будет сделать тобы принимал всю модель).
Как этот метод будет работать? Он будет работать просто - делать сплит по "//."[1]? назоввем это String fileExtension.
B здесь должна быть какая-то мапа (fileExtension, DataExtractor) и соответственно мы тогда можем просто сделать return extractorMap.get(fileExtension)
Может вылететь exception. Почему в Джаве до сих пор нету getOrThrow? я бы здесь кинул кастомный эксепшен, потому что если fileExtension неизвестный, то вместо NPE можно достаточно понятно людям объяснить. в чем дело. TODO
Теперь возникает вопрос -кто и каким образом эту мапу заполнит? Но мы пока концентрируется на коде, проблемы будем решать потом.
1:28:23 Итак мы получили DataExtractor, который поадобится нашему InvocationHandler'у? Дальше ему нужна цепочка трансформаций. Здесь до должны быть вот эти мои spiders. Вспомним картинку - вот моя фабрика, вот у неё вот эта фигня будет отвечать - вот файналайзеры все штуки, и спайдеры все штуки. Давайте сначала сделаем спайдеры, они сложнее.
Поскольку их будет много, то я думаю что здесь будет мапа, в который будет название спайдера (findBy, orderBy и так далее) против спайдера. map<String, TransformationSpider> Созздадим интерфейсTransformationSpider, потому что их будет много, назовем ее spiderMap.
Пока непонятно, откуда она здесь возьмется, тут вообще очень мнрого чего нпонятно, но мы пока хотим код. Главное не потерять мысль, мы сейчас пишем флоу, закончим c флоу - будем разбираться с настройкой объектов. Вообще, если задуматься, то код который мы пишем, бывает как бы двух видов - у нас есть код, когда мы пишем какую-то логику, и код который мы пишем, когда настраиваем наши объекты - вот у нас два вида кода. Не надо щас все это в кучу мешать, поэтому закончим с флоу.
1:30:58 Возвращаемся в create(), Идем в spiderMap...откуда мы его возьмем? Тут уже надо начинать делить все на слова. Чуть-чуть почистим код - вытащим в метод который назовем getFieldNames(modelClass), и Это мы вытащим метод который будет называться getModelClass(repoInterface) //сочетание клавиш Alt+Ctrl+M).
1:31:31 Реально, иногда удобнее писать сначала методы, и потом ими пользоваться. А иногда удобнее сначала писать код, а потом делить это на методы.
//Вопрос из зала - как ты инициализировал переменную? //Alt+Ctrl+B или Alt+Ctrl+F, "если вы хотите подтверждение"
1:32:09 Сделаем Set fieldNames, правда getFieldANmes() возвращает set поэтому добавим в стрим операцию .map(Field::getName)
1:32:59 Хорошо. Теперь наш spiderMap, и вот тут уже мы должны что-то делать с названием метода, тут мы переходим к самому сложному и самому интересному. Подготовка закончилось, мы сделали всё, что мы могли заранее, что не связано с конкретным методам нашего Репозитория.
Мы сказали, что на каждый InvocationHandler приходится одна модель, один pathToData и один DataExtractor. Всё остальное - на методы(? всм, по количеству методов?). Значит мне надо начинать дробить название метода и анализировать его.
Как мы это делаем?.. Мы берем наш repoInterface, говорим "дай мне пожалуйста все свои методы" - мы же все методы хотим проанализировать?
1:33:47 На самом деле нет некоторая дополнительная подготовка еще потребуется - мы же будем заполнять какие-то там коллекции, которые будем сетить. Опять возвращаемся в наш InvocationHandler, там объявлены два поля - мапы transformationChain и finalizerMap. Их надо наполнить и засетить в него. Копиируем их в Фабрику, создаем каждой new HashMap<>(). Воот, теперь мы можем итерироваться и их наполнять.
1:34:28 repoIntergface.getMethods() - получили все методы, итерируемся по методам, теперь для transformatikonChain нужна вот такая штука List для каждого метода, нзовем ее transformations = new ArrayList<() - пустой лист создали, а теперь будем дробить метод на слова и наполнять эти transformations. А последнее слово у нас скажет, какой файналайзер будет.
Нам нужно иметь два списка: один список - это все слова сначала метод на слова поделим... как метод на слова делиться? method.getName().split("(?=\p{Upper})")
1:35:15 как мы будем дробить на слова - тут на самом деле очень у нас большой вопрос. Давайте немножко про это поговорим до того как начать, чтобы потом сделать сразу правильно.Нам нужен какой-то вспомогательный класс который будет уметь это делать, потому что я вижу - эта логика будет повторяться в разных местах.
Но мы сделаем его чуть позже, а сперва допишем Фабрику, для более плавного хода повествования.
Сейчас только надо знать, что в нем будет статичский метод findAndRemoveMatchingPiecesIfExists(), который принимает два списка: в первом - опции, совпадение с которыми мы собственно ищем, и второй - список слов (в нашем случае это все кусочки кусочки длинного названия метода интерфейса SparkRepository, например findByNameOfБабушкаContainsOrderByAgeAndNameSave(), начинающиеся с заглавной буквы). Метод ищет элементы, которые предлагаются в первом списке среди кусочков имени метода, выкидывает те кусочки что нашел, и возвращает найденное. При этом исходный список стал немного короче
1:49:08 Теперь мы можем вернуться в нашу Фабрику, где ранее мы получили с помощью сплита methodWords. Что наша фабрика делает первую очередь - в первую очередь она должна вот это слово "findBy" отцепить - а что это за слово? это название какоq-то стратегии, всегда здесь будет начинаться с названия какого-то "паука", конечно при условии что это например не метод count,потому что в СпрингДате может быть только лишь метод count и всё.
Кстати, с массивом мне будет крайне неудобно работать, тем более что у нас уже по сигнатуре метода который мы придумали должен быть лист, поэтому давайте мы сразу конвертнём это все слова в new ArrayList(asList(....)) Знаете, зачем я делаю new arraylist()? почему не сделать просто asList(..) и все? Да потому что если asList возвращает нам либо immutable, либо unmodifiable листы - ну короче лист из которого ничего выкидывать нельзя, поэтому мне приходится заворачивать их в нормальный лист, ну или можно было сделать collect() и собрать лист по другому, не суть.
Короче, мы говорим - пока у меня в этих methodWords.size()>1 больше чем один, мы должны что-то делать: возьмем например наш CriminalRepository, напишем ему метод long count(). Мы его поделили на слова, у нас получилось ровно одно слово. И нам не надо сейчас вот выискивать какую-то стратегию, потому что если тут одно слово, значит это файналайзер. Потому что файналайзер всегда ставится последним словом, там всегда терминальная операция - save, count, или ничего("") если это collect(). Не может быть такого, чтобы одним словом обозначать какую-то стратегию поиска, так не бывает. Поэтому мне надо искатить findBy, orderBy только в случае, если изначально в методе больше чем одно слово. Поэтому после как мы это определили, мы будем действовать и строить нашу цепочку трансформации выполнять вот этот лист до тех пор, пока у меня не останется ровно одно слово.
1:52:38 теперь мы идем в wordMatcher, говорим ему найти все что использовал... Какой у нас сет опций?Напомню, сейчас мы ищем слово findBy. А findBy это ключ в нашей spiderMap. Здесь будут пары "findBy" - spiderTransformationFindBy, "orderBy" - spiderTransformationOrderBy и т.д. Поэтому мы возьмем из этого spiderMap весь keySet(), и на него будем матчить methodWords. И когда эта строчка отработает, в этом methodWords останется немножечко меньше слов.
1:53:15 После того как мы это сделали, мы идем в этот spipderMap..... ааа неет. Помните пустые кавычки? Смотрите, мы тут в последнем while получаем стрингу, которую в данном случае лучше назвать strategyName, или там spiderName. И у меня есть вот эта ситуация, что может вернуться пустота "". Знаете, что она для меня символизирует? Она означает, что стратегию менять не надо, у меня тут есть очень важная штука - нам нужно иметь для каждого для каждого метода..? или сделать более глобальную переменную?
//..скрип мозгов
1:54:27 попробуем так: идем в InvocationGHandler.create() в for(Method method:methods) первая строчка transformationSpider spider = null;на данныцй момент у нас будет стратегия null. И теперь мы идем там же в while и первой строчкой говорим: если у нас !strategyName.isEmpty() - то есть не пустое - то мы должны заменить стратегию. У меня всегда выставляется spider'y (кстати,переименуем его в currentSpider) начальное значение null, потому что его может и не быть, может и не пригодится - вдруг у нас никаких трансформаций, один только финалайзер. И первый раз он инициализируется только когда мы попадаем в этот while, потому что здесь больше чем одно слово. И тогда мы можем вытащить, что это за слово (помимо последнего).
Итак, возвращаемся к путым кавычкам - в каком случае у меня попадут пустые кавычки? Вслучае "And", понимаете? То есть findBy - такая стратегия у нас есть, orderBy - такой spider в мапе есть. А "And" - у нас такого спайдера нет, и это обозначает, что мы не меняем стратегию, мы остаемся на той же самой стратегии которая у нас была, потому что после вот этого And мы будем продолжать пользоваться тем же самым спайдером, который делает findBy
1:56:26 Значит, если слово какой-то мне пришло, то это - название моей стратегии я могу сказать что currentSpider берем из нашей spiderMap по названию этой стратегии этого currentSpider. После этого мы просим currentSpider'а.... упс, а у нас в нём еще ничегоне написано. 1:56:53 Идем в интерфейс TransformationSpider, напишем ему метод SparkTransformation createTransformation. Принимать он будет все оставшиеся слова. Ну, вот мы findBy уже забрали, а он должен получить все что осталось - мы не знаем сколько ему еще понадобится, может у меня тут nameOfЛюбимаяПрелюбимаяБабушка, 10 слов, и это все название проперти, я не знаю,где они закончится, это не мой responsibility разбираться. Мой responsibility - найти стратегию, а она уж там пусть дальше разбираться, поэтому мы сюда будем явно передавать лист стрингов, и это у нас будет называться remainingWords
1:58:07 Возвоащаемся в Фабрику, и здесь соответственно вызовем createTransformation(methodWords), они все время уменьшаются, это мне дает Трансформацию, я её могу сразу засетить в лист. Я же подготовил лист трансформаций - вот они начинают заполняться transformations.add(currentSpider.createTransformation(methodWords));
1:58:40 Так, закончили мы сетить все эти трансформации, теперь надо файналайзер подбирать. То есть,когда во этот while закончился, это значит что у нас осталось либо одно слово, либо ноль - ну потому что вот этот метод findAndRemoveMatchuingPaices - мы не знаем, сколько слов он выкидывает. Например, у вас последние два слова метода - это "last" и "name", это последние 2 слова. И мы естественно в этот while заходим - и теперьу нас -Опа!-
1:59:28 закончился wh ile мы берем и говорим если Вот этот spider, который отвечает за поиск филдов внутри if(strategyName.isEmpty), он берет этот last name и после этого у нас не осталось слов вообще, поэтому может быть 0, а может быть останется одно слово, типа save или count, которое никто не взял. Поэтому, в зависимости от того, осталось или не осталось, мы решаем какая у нас будет трансформация. Но это после того, как уже while закончился.
1:59:46 После того, как закончился while, сделаем Стрингу, назовем ее finalizerName. Мы сказать что по дефолту это будет "collect". Но если у меня осталось ровно одно слово если стал словно одно слово то тогда это слово и есть название моего финалайзера, и тогда у меня
if(methodWords.size()==1){finalizerName=methodWords.get(0);}
вместо collect. И после этого мы можем пойти в finalizerMap, и по названию нашего finalizer можем получить объект Файналайзера.
2:01:10 Отлично. Теперь надо подготовить мапу для файналайзеров, точно также как мы готовили вот эту мапу methodWords, нам надо для файналайзеров построить мапу, давайте еще один такой сделаем.
2:01:30 Там же, перед первым while сделаем map<....> Только там уже не , там как в InvocationHandler'e это должно выглядеть. Там в нем есть Map<Method, Finalizer> , и нам точно такой же нужен, мы же его подготовим и будем наполнять до того, как... Это делается на каждый метод, также как цепочка трансформацией делается на каждый метод, точно также здесь. Только здесь она будет называться method2Finalizer = newHashMap<>();
2:02:32 делаем method2Finalizer.put(), и первым параметром у нас method и вторым - этот файналайзер,который мы только что нашли, можно его даже заинлйнить сюда.
Теперь надо точно также сделать transformationChain.put(метод, transformations). make sense?
2:03:24 Давайте еще раз оглянемся: мы практически закончили писать логику самого сложного класса (будет только один еще сложнее).
Еще раз: мы идем по всем методам, и для каждого метода мы, при помощи спайдера, который у нас все время будет меняться в зависимости от стратегии (sortBy или OrderBy), мы будем получать трансформации, и будем наполнять цепочку трансформаций трансформациями; когда мы закончим это все делать - это значит, что у меня осталось ли бы ноль либо одно слово в моих словах, это помогает мне понять мой финалайзер, и теперь я могу засетить все трансформации к этому методу и файналайзер этому методу.
2:04:17 //небольшая путаница с мапами
2:05:09 Еще нам надо точно так же как у нас есть все spiders у нас точно так же должна быть мапа названиями фаиналайзеров против всех файналайзеров. Заведем в классе поле private Map<String, Finalizer> finalizerMap, а вот то что для InvocationHandler'a - то будет называться method2Finalizer<method, Finalizer>
2:05:32 Вы понимаете разницу? В InvocationHandler на каждый метод идёт свой файналайзер, а у меня есть все существующие файналайзеры против их названия, и я их типа раздаю - каждому методу клею свой, в зависимости того, на чем там закончилось дело. Поэтому это другая мапа - method2Finalizer.
//Теперь самое время написать WordsMatcher
1:37:25 Вот мы поделили наш метод на слова, назначим String[] methodWords, и нам надо будет постоянно отрезать по одному слову из него. То есть мы сначала отрежем слово findBy, и далее кто-то будет разбираться, что это значит. А весь остаток пойдет в следующую логику, и следующая логика отрежет nameOfБабушка и это пойдет дальше, отрежет contains и пойдет дальше... то есть нам нужен метод, который не просто может сматчить слово, а еще и....
1:38:10 Давайте сразу попытаемся его написать. Вот будет у нас какой-то такой класс который будет вызывать WordsMatcher. Можно в нем прямо статические методы сделать - это будет достаточно стандартная и универсальная штука, потому что у этого WordsMatcher будет две задачи. Никак не получилось соблюсти single responsibility, потому что сайд-эффект одной задачи приводит к другой, а они как раз нужны вместе. Поэтому этому методу надо дать ооочень понятное название,а именно public String findAndRemoveMatchingPeacesIfExists(Set options, )
правильное название этого метода критично - люди должны понимать, что этот метод он не просто находит что сматчилось. Он будет принимать сет из опций, на который надо матчить, и лист кусочков названия метода, записаннного кэмелКейсом.
Проблема в том, что Джава-конвенция прекрасна, но вот тут если мы делим метод просто по словам, то у нас будет отдельное слово Find, отдельное слово By, и Name, Of, Бабушка - это три отдельных слова. Поэтому я должен из этих кусочков собирать, пока я не получу какую-то опцию.
И я решил вынести в статтический метод - потому что я вижу как минимум две ситуации, когда нам это понадобится: в одной ситуации мы с вами хотим найти название моей стратегии (паучка) - findBy или orderBy. Соответственно, сначала кто-то вытащит "find", потом прицепит к нему "by", и когда он соединит их, то поймёт - "О, я что то нашел", и вот он будет искать как раз из опций, которые являются названием стратегий.
У того, кто будет искать название проперти - у него опции это все филды, которые есть у данной модели. Он взял "name", не подошло; взял "of", не подошло; добавил еще "бабушка" - о, появился "nameOfBabushka" - и это сматчилось!
Дополнительная сложность в том, у нас может быть например класс Person у которого есть поле name, и есть поле nameOfБабушка. И очень важно, чтобы не случилось такой ситуации, что он увидел "name", - и такой типа "о все чуваки, я нашёл это "name", есть такое поле". Но если бы он продолжил читать, то он бы увидел, что на самом деле это "nameOfБабушка"... Вот это все надо учесть.
И все слова, которые я уже учёл, из которых я склеил то что сматчилось, мне надо выкинуть из вот этого листа, потому что тот кто следующий будет работать, не хочет чтобы nameOfБабушка осталось, если я его забрал. Поэтому этот метод называется findAndRemoveMatchingPiecesIfExists()
1:42:00
Теперь давайте его писать.. нужен какой-то StringBuilder - мы же будем аппендить то, что у нас будет матчится? И мы можем сразу сюда из этих pieces сделать remove(0), и назовем это match.
Есть шанс, что он же что уже все хорошо, как мы об этом узнаем? Нам надо искать, сколько у нас есть опций среди тех, которые могут сматчиться. То есть мы сделаем
options.stream().filter(optoion->option.toLowerCase().startsWith(match);
(чтобы точно большие-маленьикие буквы нас никак не сбили, потому что в опциях это может быть написан с большой буквы, список может быть с маленькой, потому что там первая буква на большая если в методе, а поэтому тут лучше сделать lowerCase наверняка)
И все эти опции мы можем сразу сколлектить в лист. Если только одна опция подошла, то все хорошо, это у нас будет remainingOptions.
1:44:10 Теперь мы должны посмотреть если у нас remainingOptions.isEmpty() то вернем пустую строку "" эта ситуация не совсем валидная, но все же есть один кейс, когда она как раз очень валидная, чуть позже рассмоотрим его. Но по идее такая штука не должна происходить, потому что всегда какая-то опция должно сматчится.
1:44:51 // основной алгоритм Если мы проскочили этот if, тут у нас уже while. Пока в этих remainingOptions.size()>1 больше чем один, значит мы еще не полностью сматчили.Поскольку мы тут делали startsWith() - например если у меня сейчас в опшенах есть nameOfБабушка, и name, и age, и salary, и еще куча всяких разных других вещей, а я получил кусочки. И в этих кусочках лежит name, потом, of, потом бабушка, потом еще вещи, которые сейчас не нужны. Я взял вытащил первый кусочек name, и сказал - какие у меня есть опции, которые начинаются также? у меня совпало nameOfБабушка и у меня совпало name, соответственно я в ситуации, когда у меня больше чем одна опция. Поэтому я здесь делаю while пока она не останется ровно одна. Пока их больше чем один, мы должны продолжать.
1:45:50 match.append(pieces.remove(0)) - берем здесь кусочки, и добавляем следующее слово из списка. После этого нам надо из remainingOptions выкинуть то что теперь перестало матчиться: сделаем любимый метод который появился в Java8, наконец-то можно из коллекшена выкинуть что-то без итераций, а просто объяснив что конкретно выкинуть
remainingOptions.removeIf(option->!option.toLowerCase().startsWith(match.toString().toLowerCase())
Кстати, выше тоже toLowerCase() надо было поставить, на всякий случай.
1:46:47 Итак, выбросили. И вот так мы будем выбрасывать, выбрасывают, выбрасывать и добавлять - видите, отсюда из remainingOptions оно ремувится, а сюда в match оно аппендится. И у нас получается в начале "name", потом "nameOf", потом "nameOfБабушка", и так до тех пор, пока у меня remainingOptions не прекратит быть больше чем один.
Когда у меня прекратило быть больше чем один, казалось бы мы закончили. что можно return match.toString(), ура, всё. Но смотрите, что может произойти: представьте себе что у меня нету name, а есть только "nameOfБабушка". У меня в remainingOptions очень быстро остался ровно один вариант, и этот вариант "nameOfБабушка", и я уже знаю, что вернуть надо "nameOfБабушка". Но я же пока еще его не склеил, я пока еще только на "name"... Be меня просто после "name" осталась одна опция. Я ее еще правильно не вычислил, мне надо продолжить: "name, Of".. a может вообще написано "nameOfДедушка", что-то чего не существует. То есть мне надо продолжать матчить, пока equals() полностью не совпадёт.
1:48:00 Поэтому мы здесь делаем следующий while. Мы говорим while(remainingOptions.get(0).equalsIgnoreCase(math.toString())) - что надо делать? Надо продолжать делать вот этот match.append(pieces.remove)
1:48:29 И вот теперь, когда мы дошли до вот этого места, и если у меня после вот этого while уже получился "nameOfБабушка", то он никаких дополнительных аппендов делать не будет, потому что у меня уже сейчас единственная опция которая осталась, полностью равна. Там у нас startsWith, а тут у меня полностью равна. Если она полностью равна, то мы сюда не зайдем, но если не равна, значит надо еще накидать каких-то дополнительных слов, чтобы получилось полностью название нашего филда, или название нашего spiderа (стратегии) - вот это теперь законченный wordMatch()
//Видео возвращается к отметке 1:49:00, где автор дописывает фабрику, а у нас она уже есть.
2:06:10 Теперь нам осталось только наполнить InvocationHandler - давайте смотреть, чем надо его наполнять. После того, как у нас вот этот for закончился, это значит, что мы обработали каждый метод. И на каждый метод навесили цепочку трансформаций, и на каждый метод навешали нужный финалайзер, а до этого мы еще там подготовили экстракторы и прочее прочее.
2:06:41 Вернемся теперь в InvocationHander... а как мы вообще, собственно, собираемся его наполнять? Всякие разные мысли типа может быть мы сделаем его бином, и начнем делать инжекшен - мы должны сразу отбросить, потому что он прототайп, он создается постоянно и он настраивается уникальной логикой, которую мы только что написали, поэтому он ни разу не бин. С другой стороны, делать ему конструктор под вот это все дело - как-то очень много будет в конструкторе. С третьей стороны, делать сеттеры... короче. Давайте просто @Builder поставим, и всё.
2:07:21 Забыли создать поле ConfigurableApplicationContext context Теперь наш наша фабрика говорит:
return SparkInvocationHandlerImpl.builder()
.modelClass(modelClass)
.pathToData(pathToData)
.finalizerMap(method2finalizer)
.transformationChain(transformationChain)
.dataExtractor(dataExtractor)
.context(context)
.build()
//builder() берется у класса
Ну вот мы и написали Фабрику, она чуть-чуть еще поменяется но совсем не сильно. TODO Можно потом почистить, вытащить какие-то блоки в методы, чтобы было более красиво и читабельно, но в принципе это - самый длинный метод, еле-еле влезает на ожин экран с 24 шрифтом.
Следующее что мы будем делать - это отвечать на вопрос, как мы можем все это всю эту хрень настроить, так чтобы код наш не стал совсем уже ужасным, и вот на этот вопрос я вам отвечу после 10-минутного перерыва. Мы уже прошли больше половины, то есть где-то в районе часа
//Уходим на перерыв
2:10:15 //пока у нас был перерыв, в чате написали вопросы: Q:фабрика получает имена полей вроде переменная fieldNames, но нигде не использует это скорее всего кусочку логики не хватает? A:потому что мы еще не начали писать spider'ы, они как раз скоро нам понадобятся, когда мы говорим createTransformation и передаем methodWords, то вот эти fieldNames по идее тоже должны передавать, потому что ну вот как сейчас как раз это и сделаем, и увидим.
Тем более, что нам пора уже наконец ответить на вопрос - как мы будем настраивать вот это все? (две мапы и Резолвер). Как мы будем настраивать этих spark transformation?
2:11:22 Вот у нас.. давайте начнем с этого спайдера, который отвечает за фильтры... делаем FilterTransformationSpider, который отвечает за findBy. Первое, что он делает опять же надо смотреть все время на метод, который мы разбираем
findByNameOfБабушкаContainsAndAgeLessThanOrgerByAgeAndNameSave()
ну findBy уже не осталось, вот пришло ему "nameOfБабушкаСontains..." что он делает? он знает,потому что это конвенция СпрингДаты, что первое, что будет это название той колонки или того филда, на которой надо будет применить какую-то из фильтров операций. Соответственно, ему обязательно нужны знать все филды, для того чтобы он мог правильно отрезать при помощи нашего WordMatcher все вот эти вот "nameOfБабушка" и др 2:12:26 поэтому сюда должен кроме листа должен приходить Set fiendNames (потом сигнатуру подправим). И он берет этот WordMatcher и говорит - вот наши опции в которых надо искать (fieldNames), вот наши все оставшиеся слова (remainningWords), и возвращает он fieldName. "nameOfБабушка" сюда пришло, а в листе уже три слова выкинуто. 2:13:18 После того как он это сделал, надо выбрать подходящий фильтр, а для этого надо знать как этот фильтр называется, то есть теперь нам надо отцепить слова Contains или greaterThan или EqualsIgnoreCase, что-то такое. И по этому слову вытащить нужную трансформацию.
Я уже сразу начинаю видеть, что у нас трансформации будут разделены на категории есть подвиды. Отнаследуем интерфейс трансформаций, будет называться FilterTransformation. Их будет очень много разных, поэтому это у нас интерфейс
2:14:17 Теперь у этого Спайдера, которого мы сейчас пишем, у него тоже должно быть мапа название фильтра против какой-то spark-трансформации, которая будет <String, FilterTransformation> transformationMap, тут именно только фильтры просечены будут.
fieldName он взял, теперь, снова при помощи WordMatchera, теперь мне надо из всех ключей выбрать что-то, что подойдет на оставшиеся слова. Если у нас первое доставшееся слово это "Greater", оно не сматчится, потому что здесь есть только "GreaterThan". Но когда отцеспится следующее слово, это сматчится, и после этого в remainingWords у нас не останется ни "Greater" ни "Than", как и должно быть. А сюда (второй вызов WordsMatcher.findAnd...()) придет как раз название трансформации - это значит будет String filterName
2:15:40 После этого мы берём этот transformationMap, вытаскиваем по filterName нужную информацию, ну и в принципе всё, возвращем её.
Правда у нас тут проблема с другим: не очень понятно, где мы будем использовать это fieldName? Но мы поймем это, когда начнем писать вот эти filterTransformation, и тогда у нас опять немного исправиться сигнатура.
2:16:15 Но мне кажется, надо секундочку остановиться и все-таки ответить на вопрос: как же мы будем настраивать всю вот эту вот систему? У нас появляется достаточно много сложных объектов. Фабрика очень сложный объект, но фабрика использует пауков которые тоже сложный объект, а "пауки" будут использовать какие-то тоже сложные объекты. Фабрика использует dataExtractorResolver, который тоже в себе держит какую-то мапу всех экстракторов, которую тоже кто-то должен наполнить.
Короче, я вот может быть как-нибудь не поленюсь и сяду напишу это все руками, чтобы просто посчитать количество строчек.... (TODO?)
Но сейчас мы будем пользоваться Спрингом. Причем как мы можем это сделать? Если просто здесь над Spider поставить @Component - никто не будет на это реагировать - вы же помните, что все вот эти классы, все вот эти фабрики - они у нас строятся на очень очень очень раннем этапе. Первый раз эта фабрика понадобится нам в ApplicationContextInitializer'е, и мне ее надо откуда-то взять, чтобы потом с ее помощью вытаскивать эти InvocationHandler'ы. То есть, у нас где-то здесь должна эта фабрика появится. Но даже если мы каким-то образом сделаем её бином, то мы все равно не сможем сюда заиинжектить, потому что эта штука работает тогда,когда бинов нету, и нету BeanPostProcessor'ов, нету ничего. Получается, что фабрика тоже не может быть бином.
Но при этом если не использовать Spring для того чтобы реализовать все (что мы еще даже не написали), у нас здесь будет просто чертов ад... Поэтому я подумал - а чё бы нам не создать Спринг внутри Спринга? о_О Ну, типа мы создадим отдельный Контекст, просто для того чтобы написать красивый код.
Вы наверно мне скажите, что я совсем уже упоролся, вначале я делал это больше по приколу - создать Спринг внутри Спринга. Причем надо будет не просто создать какой-то дополнительный контекст,а еще потом обязательно придется еще и закрыть, иначе не будет работать, т.к если создать контекст внутри контекста, то когда будет рефрешиться() настоящий контекст, вам упадет эксепшен, что типа уже какие-то ресурсы заюзал, я не могу.
А потом когда я все это написал и попытался представить, как бы это выглядело без Спринга, я понял, что сделал абсолютно правильно, и у меня на это есть два подтверждение: во-первых spring сам делает похожие вещи. Правда, когда они делают контекст внутри контекста, они потом не закрывают внутренний, а мерджат его с настоящим контекстом.
Во-вторых, чтобы было понятно, что даже в нашей ситуации это правильное решение, давайте проведем такую параллель: вспомнил о том что я уже сказал что есть два вида кода который мы пишем: есть код который наша логика, и есть код который настраивает наши объекты. Поскольку мы находимся в объектно-ориентированном мире, мы стараемся все инкапсулировать в объекты, у нас очень много объектов, которые используют другие объекты, все это надо как-то настроить... Количество кода, которое связано с настройками объектов, это будет где-то пятьдесят процентов, если не больше. Особенно если вспомнить старые-старые, проекты когда Спринга никакого не было, и вот это все делали руками, бизнес логика была перемешана с логикой конфигураций... Люди приходили к ситуации, когда чкловеку говорили, что вот надо еще вот такую фичу допилить - "подождите подождите, я новый человек на проекте, чтобы допилить какую-то фичу мне надо что- то где-то менять в конфигурация-в настройках...оно же все развалится! может лучше не будем эту фичу делать?"
то мне очень напоминает вот такую вот загадку представьте что у вас переезд, и у вас есть любимый шкаф, как поступить? разобрать его самому, или можно позвать друга, или можно заплатить деньги специальной команде, чтобы они пришли разобрали-собрали, уговорить начальство, что мне нужно переезжать поближе и тогда я перенесу его сам, не разбирая... Ну и самый классный вариант - выкинуть его и купить новый. Короче, все варинанты плохие. Если вы пишете код без Спнринга, то приходим к ситуации, что вы говорите "на фиг чё-то менять, это скорее всего испортит какие-то другие вещи..." Поэтому будем работать со Спрингом.
Смотрите, что мы для этого сделаем: во первых мне нужно создать какую-то дополнительную конфигурацию, которая не будет использоваться в настоящем контексте, который построится. Тоесть это не та конфигурация которая обычно есть у стартера и всегда импортируется сразу, это будет какая-то отдельная конфигурация Создадим, назовем ее internalСonfig, поставим @Configuration и @ComponentScan (она будет сканировать пакет в котором находится)
2:22:43 Теперь мы можем им воспользоваться: вот этот SparkContextInitializer - он сам в контекст не попадет, но он этот контекст будет строить - для того, чтобы вытащить из этого контекста нашу сумасшедше-сложную фабрику, в которой есть много других сумасшедше-сложных объектов, ради чего ему собственно и нужно создать этот контекст.
Он говорит - давайте, значит, создадим AnnotationConfiogApplicationContext с вот этой вот InternalConfig.class. Назовем это tempContext. Дальше из этого темп-контекста вытащим бин, который будет наша Фабрика (а над самой фабрикой поставим @Component) - и вот мы получим нашу фабрику. И в принципе после этого можно этот контекст спокойненько закрывать! Потому что если я вытащил объект, то он уже настроен, уже пришел, там уже все просечено, можно пользоваться. Откуда он пришёл, живы ли его родители и не сгорел ли его дом, это не интересно - важно, что он уже есть и его настроили. Поэтому контекст можно закрывать.
2:24:25 Так, теперь забавная штука одна... Давайте сразу посмотрим, что нам это даст? Например если посмотреть на нашу фабрику, теперь становится понятно, как неё попадёт например вот эта мапа spiderMap - а попадет она очень просто: у нас эти имплементации TransformationSpider будут помечены аннотацией @Component("findBy") - для фильтров, и например еще сделать для "orderBy", вот такая у нас пока будет конвенция.
2:25:14 Они все у меня попадут в эту мапу, если мы поставим final перед resolver, spiderMap и finalizerMap, и здесь над классом поставим @ReqiredArgsConsatructor. Теперь будет constructor injection то есть у нас есть конструктор под все обязательные поля. При этом поле context ни в коем случае не получится заинжектить - это настоящий контекст (и лучше даже переименовать его в realContext, чтобы было понятно). Его надо будет просетить в КонтекстИнишалайзере после того как получили Фабрику (сначала здесь сделаем ему @Setter)
Итого: мы её взяли, засетили реальный контекст, и все, закрыли временный контекст, он нам не нужен. Не такие уж и сложные телодвижения мне пришлось сделать, для того чтобы у меня появился Спринг внутри Спринга, да?
2:26:15 и тогда я могу сейчас здесь в FilterTransformationSpider та же самая ерунда будет тоже можно поставить final, сделать его @RequiredAgrsContructor, и соответственно если мы будем соблюдать конвенцию что у каждого filterTransformation айдишником будет являться его название, типа "GreaterThan" и т.д, то они все попадут сюда в мапу.
2:26:41 Что меня напрягает? если помните, я несколько раз на предыдущих докладах говорил о том, что очень опасно делать в стартере конфигурацию, которая делает @ComponentScan, потому что не дай бог она своим копонент-сканом найдет что-то, что вообще никак не относятся, или что на самом деле хуже, в данном случае я больше боюсь обратной ситуации - я боюсь что вот эти все мои компоненты случайно кто-то создаст как бины в настоящем контексте - потому что я нахожусь в com.example, настоящий проект тоже будет сканировать com.example, если не дай бог у них появится пакет com.example.starter, и совпадут пакеты....
Еще раз я хочу это за заострить ваше внимание. Вот эти все бины,которые мы сейчас прописывали - они временные бины. И они бины до тех пор, пока вы не закрыли контекст, в котором они собрались. Они являются бинами на то чтобы мне было удобно заинжектить их в мою Фабрику. Но после того как я их заинжектил, мне теперь не нужны ни эти бины,ни весь контекст, мне нужна сама фабрика в которой есть объект, это просто фабрика с объектами.
Если мы допустим ситуацию что в настоящее сканирование (уже основного контекста) тоже попадет каким-то образом этот пакет, то вот эти объекты начнут создаваться, и скорее всего это приведет к тому, что это все еще и грохнется - потому что что-то не заинжетится...в общем, будут какие-то проблемы.
2:28:11 Поэтому мне надо гарантированно поместить все классы, которые вот из временного контекста, в какой-то такой пакет, который абсолютно точно нет никаких шансов, что совпадет с настоящим. На это есть отличная практика java разработчиков как-то назвать пакет, чтобы никогда это не совпало с чужим названием, и чтобы не дай бог туда никто не залез. Назовем его "starter.unsafe" И теперь тогда все мои вот эти вот объекты можно тут а засунуть //а именно: все свзанное с дата-экстракторами и резолвер все связанное с трансформейшеном спайдер файналайзер internalConfig SparkDataApplicationContextInitializer все связанное InvocationHandler пропертиХолдер(для автокомплита) аннотация @Transient WorldMatcher // итого получилось 17 классов
На самом деле, не обязательно все засовывать. Те, которые не являются бинами, можно было оставить сверху - ну то есть например сам ContextInittializer, но мне пофиг.
Q: правильно ли я понимаю, что пакет unsafe нужно потом вынести наружу, потому что текущая иерархия не спасёт от не-сканирования пакета? A: да да? просто я этого не cделал, молодцы, правильно.
2:29:13 Значит, мы обезопасились. Теперь давайте чинить - у нас тут очень много не компилирующегося кода, давайте посмотрим что надо починить, и заодно поставить @Component. JsonDataExtractor это у нас компонент, и у него будет название "json". Также и с CsvDataExtractor - @Component("csv")... Куда они должны заинжектиться? в Резоолвер, идем туда и ствим @Autowired на мапу с ДатаЭкстракторами. Сам Резоолвер тоже должен быть @Component (название не нужно). Всё, сколько у нас решилась проблем!
Пошли обратно в Фабрику. Там у нас всё инжектится, контекст ставится через @Setter фабрика защищен, это все инжектится, это ставится через центр из принципе как бы получается мы закончили никаких настроек нет? мы тут думали щас полчаса настраивать это будет а мы тут бах аннотацию поставили, и все хорошо.
2:30:22 Q: Вопрос из чата: создание внутреннего контекста только для того чтобы не создавать объекты через new? A: Да потому что если я буду все создал через new, все придется настраивать - а вы понимаете, что тут есть две проблемы: первая проблема - это количество кода настроек который постоянно будет увеличиваться. Даже сейчас если написать весь код, чтобы настроить объект, то его будет очень много. А теперь представьте что произойдет через месяца два-три работы, когда много людей придут и начнут писать всякие разные... Ну, допустим Спайдеров много не будет. Но spark transformation'ов будет штук 50... Давайте вот как раз один напишем чтобы понимали, как они устроены.
2:31:13 Идем в FilterTransformation. Мы сказали, что это интерфейс, ему ничего внутри не нужно, он просто наследник, как маркер, чтобы было понятно, что это особый вид трансформации. Давайте писать имплементацию. Какой фильтр напишем? Ну давайте between плопробуем начать
Имплементим, назовем BetweenFilter - название класса мне пофиг, главное чтобы слово between было. Сверхупоставим @Component("between") И здесь в transform() надо уже реализовать логику на Спарке, вот здесь придет программист Спарка и скажет что я должен делать... Сразу видно, что кроме этого dataset'а не хватает названия филдов, напишу запрос, а потому поймем, чего еще не хватает.
Как мы это делаем? Контекст Спарка мне тут не нужен, т к у меня уже есть здесь dataset. Мы хотите что-то фильтровать значит делаем dataset.filter(functions.col(...)) вот эта штука внутри - она уже спарковская: мы берем колонку-датасет от строчек, это dataframe, который как таблица.
Ему нужно название колонки - и вот сразу чувствуется проблема, что сюда ее не передали. Надо, чтобы передали. Причем давайте, чтобы сэкономить время, я сразу скажу, что здесь будет не просто String fieldName, а ясюда передам лист, хотя на самом деле всегда буду брать один - знаете зачем? Потому что я хочу единый API для всех трансформаций. Вспомним конвенцию всех filterTransformation'ов:
List<User> findByNameOfБабушкаContainsAndAgeLessThanOrderByAgeAndNameSave()
Если снова посмотреть этот метод, у вас после названия spider' который отвечает за Contains или GreaterThan или еще что-то - перед ним всегда один филд. И не может быть больше чем один, потому что мы фильтруем по одному какому-то филду, мы можем отфильтровать сразу по двум. Если мы фильтруем по двум, это два фильтра, и у меня при помощи Энда стратегия останется findBy и еще один фильтр накрутится, и потом еще один... Но каждому фильтру нужен ровно один филд.
Казалось бы, наш метод transform должен принимать название одного филда. Но если говорить по трансформации связанные с сортировкой, там немножко другой синтакс у спарка: там нельзя сделать SortBy Name And SortBy Age, там это должно быть одним выражением. И соответственно, тот спайдер, который отвечает за orderBy, будет строить объект который будет называться SortTransformation, и для этого Сорт-трансформашена он передаст не один филд,а все филды. Поэтому давайте мы, чтобы был единый API, скажем что нам нужен List fieldNames. И еще надо будет кое-что... сейчас увидите
2:35:12 Мы всегда берем fieldNames.get(0) Дальше мы пишем between() - здесь не надо дать мин и макс. Откуда я их возьму? Соответственно, тот кто вызовет метод transform(), должен мне здесь передать все аргументы которые могут пригодиться, и вот тут я реально не знаю, сколько это аргументов. Может быть один аргумент, может быть два. вот для between нам нужно 2, а для greaterThan нужен 1. Поэтому у меня есть еще одна штука, которая типа лист объектов - я даженезнаю,кто это,это могут быть Integer, а могут быть Стринги) args. И теперь можно сделать between(args.get(0), args.get(1))
2:36:10 Cразу хочется сказать про очень и очень серьезную проблему, почему вот так ни в коем случае нельзя писать код. Вот я сделал args.get(0), и у меня вот эти аргументы остались в листе. Я им должен был делать remove(). с филдами мне пофиг, это что-то статическое. А аргументами по другому: представьте, что у вас есть вот такой длинный метод, и у вас findBy начал работать, и он....
нет, даже не так. Уже когда вы в InvocationHandler'е запускают - вы же помните, что у нас в InvocationHandler'е есть все аргументы в виде массива. Тут тоже надо бы чуть-чуть починить код, потому что когда мы вызываем трансформацию, мы уже начинаем понимать, что кроме датасета сюда надо и fieldName'ы пихать, и аргументы пихать.. Ну давайте сначала с аргументами закончим.
2:37:19 Если я сюда буду пихать аргументы в виде листа или массива, то тот кто потом будет забирать их - допустим, и вот повезло, он умный человек и взял remove(), потому что он понимает, что тут надо делать remove(), потому что эти аргументы не должны остаться для следующих трансформаций. Но у меня нету гарантий, надо всегда исходить из того что в проект придёт дебил, или в проект придёт очень новый человек, или кто-то спьяну начнет писать код. И я должен сделать максимум, чтобы не дать человеку возможность сделать какую-то очень серьезную ошибку, которую потом будет достаточно непросто найти. Представьте себе, что вот если мы посмотрим на наш метод findByNmberBetween(int min, int max) нашего там какого-н CriminalRepository. Вот у нас есть два параметра - мин и макс, а теперь представьте, что здесь будет еще написано... там, например, Save. Cоответственно это приводит к третьему параметру String path - куда сохранить.
И когда у прокси вызовется findByNumberBetweenSave() то он прокинет в InvocationHandler все эти три аргумента. И я совершенно не хочу чтобы мой BetweenFilter, который эти аргументы типа забрал и засунул их себе в transform, сделал им get() вместо remove(),потму что если он сделал get(), получается что аргументы до сих пор там остались, и когда начнет работать finalizer, который отвечает за Save, ему тоже передают аргументы, а он же не знает, какой номер аргумента ему надо брать? тогда придется откуда-тобрать count и отсчитывать, сколько аргументов я забрал. Аргумент же неможет использоваться вдвух местах, каждый аргумент для каких-то своих целей - для фильтра, сортировки или для файналайзера. Поэтому очень очень плохо оставить здесь лист который дает людям возможность сделать get()? Нам важен порядок, у нас аргументы в определенном порядке, ктокором я их засетил. Я не нашел подходящей, поэтому буду писать свою.
2:39:25 У меня будет вот такая вот штука, я ее назову OrderedBag (сумка с аргументами) значит for berlin сказал значит у него будет
Q: вопрос из чата: а почему не подошла очередь? A: потому там методы непонятно называются, вернее так - там есть еще очень много других методов, я не хочу людям давать никакие методы, кроме двух методов - size() и takeAndRemove().
Давайте мы сделаем красиво, с дженериком внутри, у него соответственно будет что это реализовывать там есть даже мы сделаем конструктор
OrderedBag() удобнее всего будет создавать из массива из массива, потому что вы помните что в InvocationHandler приходит массив аргументов, но я хочу носить их не в массиве, а в "сумке". Поэтому у меня здесь сейчас будет конструктор в нем мы принимаем (T[] args), внутри делаем new ArrayList (на какую-то помните и знаете не ArrayList(AsList(args)))
Ну и метод T takeAndRemove(), принмает ничего, а возвращает он будет list.remove(0) Ну и метод size(), мало ли кому-то понадобится.
2:42:20 Вот такая коллекция. Я хочу, чтобы она приходила во все мои трансформации. в BetweenFilter вместо листа аргументов будет OrderedBag. А как тогда человек ничего не сможет сделать, ему не надо даже номера знать, просто takeAndRemove() и все, ему не нужно думать - может гет сделать, аможет remove сделать? return.