GithubHelp home page GithubHelp logo

spring-the-ripper-borisov's Introduction

Spring-потрошитель by E Borosov

part1

add spring-contet to pom new package quoters new interface Quoter with void sayQuote() new TerminatorQuoter impl Q property message with setter sayQuote(){sout(message)}

идем в xml делаем там бин TerminatorQuoter в нем проперти со зачением "I'll be back"

теперь надо написать какой-н тест. У нас не подключем JUnit, поэтому напишем Main (не делайте так никогда) созаем Main, там создаем контект из XML, вытаскиваем бин...можно по интерфейсу,а можно по классу

^сейчас вытащим по классу, чтобы потом объяснить,чтоэто неправильно ..запускакем проверяем

/* тут описывает, как Фабрика создает объекты и складывает их в контейнер причем если синглетоны создаются и складываются сразу, то прототайпы -только когда они нужны (лениво), отдает их и про них забывает. Поэтому при прописывании дестрой-метода для бина, он будет работать только для синглонов, т.к. прототайпы не хранятся в контейнере.

ЧАСТЬ 2. BeanPostProcessor

при создании, перед тем как попасть в контейнер, все бины проходят через BeanPostProcessor, являя собой пример дизайн-паттерна "Chain Of Responsibility".

Напишем свой БПП. Допутим, мы хотим кастомизировать Спринг и обучить его каким-то своим анотациям.

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

допустим унас в Терминаторе будет int repeat (сколько разонбудетповторятл цитату). Поставим на него @InjectRandomInt(min = 2, max = 7)

создадим аннотацию ( не забываем про @Retention поменять на рантайм. По дефолту Retention стоит "класс",это означает что она попадет в рантайм, но считать через рефлекшн ее нельзя. Это нужно для AST-трансформаций, байткод-инструментирования. Пример RetentionPolicy.COMPILE это аннотация @Override, которая еужна только накомпиляции никак не попадает в байткод ) также напишем параметры int min() и max()

ну и теперь самое интересное - нужно написать поддержку нашей аннотации. напишем класс, который будет отвечать за нее InjectRandomIntAnnotationBeanPostprocrssor implements BeanPostprocessor (обратим внимание на конвеции построения названий классов)

Этот интерфейс имеет 2 метода: Object postProcessBeforeInitialization(Object bean, String beanName) и
Object postProcessAfterInitialization(Object bean, String beanName)

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

в методе ...BeforeInitiallization() возьмем все поля бина, проитерируемся по ним, и для каждого проверим наличие @InjectRandomInt если !=null, возьмем min и max, и сгенерируем случайный int. Теперь этот int надо засунуть в это поле. Во-первых, скорее всегополе private, значит надо сделать ему setAccessible(true) Во-вторых надо сделать field.set().. но мы так делать не будем, т.к надо обрабатывать эксепшны. Причем мы не можемделать throws, т.к. мы имплементим чужой интерфейс (а если он не кидает Э., то и мы не можем). А try-catch некрасиво. Поэтому воспользуемся библиотекой ReflectionUtils, которая есть у Спринга, которая умеет делать всеобычные рефлекшены,но без try-catch (просто обворачивает их и прячет в RuntimeException)

Хорошо, мы написали, акак теперьсделать, чтобы Спринг узнал про это класс? Надо просто прописать его в контекст. Практически все то надо добавить в Спринг, надо приписать в контекст. Можно разными способами - java, аннотацией, но раз мы с xmlnj укажем его в xml как бин (id не нужен)

..проверяем, работает

ЧАСТЬ 3 init-метод

между Before и After работает init-метод есть разные способы прописать его для бина. Если мы на аннотациях,но можно прописать аннотацию @PostConstruct мы с XML, поэтому пропишем через аттрибут в теге

зачем вообще они нужны? вроде бы есть конструктор если в конструкторе попытатьсяползоваться чем-то,чтотнастраивает Спринг... щас попробуем: создадим в Терминаторе конструктор, и напечатаем в нем значение параметра repeat он еще не проинициализировался, поэтому в нашем случае напечатает 0, а в случае объекта был бы NPE

Процесс создания объекта в Спринге лишем всякой магии: просканировался XML, созздались BeanDefinitions, спринг понял что надосоздать синглетон TerminatorQuoter, при помощи рефлекшна он запустил его конструктор, объект создался, ипослеэтого Спринг его настраивает. Соответственно,когдамы в конструкторе пытаемся обратиться к тому, что еще ддолден настроить Спринг - а их еще нету. Получаем либо 0, либо NPE

Поэтому вместо тогочтобы пользоваться конструктором,надо написать public void init(){sout(repeat);} А в конструкторе будем просто печатать фразу "фаза 1". А в init() добавим "Phase 2" (а всего рассмотрим 3 фазы конструктора)

Если просто поставить аннотацию @PostProcessor, ничего не заработает. Потому что в Контексте мы не добавили класс обработчи ктаких аннотаций. Можно прописать CommonAnnotationBeanPostProcessor, тогда заработает

(у меня аннотация @PostConstruct не знакома IDE (?))

другой способ это включить <context:annotation-config /> внутри тега дело в том, что кроме тех BPP,которые мы напишем сами и добавим в контекст, есть еще 5-6 уще существующих BPP, которые относятся к известынм стандартным аннотациям- @Async, @Transactional, @Scheduled, @Inject, @Autowired и т.д. Ради каждой прописывать в контекст свой BPP это дрочь, поэтому придумали неймспейсы, обычно вместо этого пишут <context:annotation-config />, и это прячет кусок XML,который добавляетв контекст все эти BPP. Или можно сделать <context:component-scan base-package="org.example"/> , икроме того,что просканируются пакеты, еще и добавятся в контекст все эти BPP.

подведем итоги (но сначала надо понять, почему личноу меня IDE не знает @PostConstruct?) Разобрался, дело в том что Борисов пишет на javа8, а я на java11. Аннотация @PostConstruct является частью javaEE, и начиная с java9 ее убрали, поэтому надо прописать зависимость

<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>

..запускаем, все работает

ЧАСТЬ 4, postProcessor AFTER init

Так зачем же 2 прохода по BeanPostProcessor'ам? напишем ПостПроцессор, который будет делать профайлинг, который можно будет отключать через JMX-console

Другими словами, мы хотим, чтобы профилировались все классы, над которыми стоит аннотация @Profiling (сделаем её). Тоесть,выводилось время, за которое метод работает.

То есть последовательность такая: у нас будет BeanPostProcessor, который будет получать bean от фабрики, спрашивать - не стоит ли над классом аннотация, если да, то ему придется в каждый метод данного бина дописывать логику профайлинга. Сама логига не сложная: запомнить время до,запустить метод, замерить после окончания, разницу вывести на экран. Но как добавитьлогику в объект?

В Java нужно будет налету сгенерировать новый класс. Какой это должен быть класс? Очевидно такой, чтобы никто не заметил подмены. Поэтому новый класс,который сгенерится налету, должен либо наследоваться от ориг класса, либо имплементировать его интерфейсы. Перый подход - CGLIB, второй - DynamicProxy. CGLIB считается,что хуже, т.к. не получится отнаследовать FINAL методы и классы, и к тому же работает медленнее. Поэтому Спринг всегда предпочитает идти через интерфейсы. Кстаи, через И работают и аспекты (^Спринг АОП) Еслиже нет интерфейсов,то Спринг идет через CGLIB (кстати, cglib сейчас есть почти во всех проектах)

Итак, если наш БПП будет подменять класс, то что будетв нашем Бпп, который рассчитывает искллючительно на то что getClass() вернет оригинальный класс, в котором все поля аннотированы ориг аннотиями? Если же у нас класс сгенерится на лету, то никакой метадаты там не будет, никааих анотаций. Аннотация @Inherited не поможет. Есть конечно AOPUtils, но с ними возникают различные сложности... Поэтому было бы правильно, и по конвенциям Спригна, не смешивать, а делать все прокси-вещи на этапе postProcessAfterInitialization() - это дает нам уверенность, что @PostConstruct всегда работает на оригинальный метод, до того как все прокси на него накрутились.

создадим класс ProfilingHandlerBeanPostProcessor в нем мапу<стринг,класс> с именем бина... кстати имя бина всегда сохраняется. Поэтому один из способов- это на этапе BEFORE откладывать в сторонку, запоминать имена тех бинов, для которых что-то надо сделать, а на этапе AFTER делать это

в BEFORE возвращаем бин, а перед этим вытащим у него getClass(), и если этот класс аннотирован @Proifiling то кладем этот класс в мапу по имени (приходитв BEFORE)

а на этапе AFTER вытаскиваем из мапы бинКласс по имени (приходит в AFTER), и если этот бинКласс !=null,значит мы егозапомнили, значит над ним стояла аанотация,и значит надотвозвращать не оригбин, а прокси. Как? Rcxfcnm.?yfv самим не придется генерить класс на лету, с 1999 годавджаве есть dynamic proxy.

retirn Proxy.newProxyInstance(...) -принимает класслоадер (возьмем из бинКласса) списокинтерфейсов и InvocationHandler - объекткоторый инкапс логику,котораябызывает исходный метод и модифицирует его new InvoHandler(..) { тут можно сделать какие-то деиствия, и вернуть принимаемыйМетод.invoke() }

ЧАСТЬ 5, профайлинг

для интереса мы сейчаас сделаем класс ProfilingController c булеан enabled,и мы сделоаем так,чтбы его можно было вклюбчать-выключать извне с помощью консоли MBean (не связано соСпрингом). Это можетбыть полезно, т.к. нам не надо всегда профилировать, но очень классноиметь возможность включить профилирование в рантайме. //такмкод 36:00

идем в наш ProfilingHandlerBeanPostProcessor, он будет у себя держать этот контроллер

и теперь нам надо наптсать логику каждого метода каждого класса, который сгенерится на лету, который имплементирует интерфейсы оригинального класса (то есть логику для динамик прокси)

во-первых печатаем слово "профилирую..." потом замерить наносекунды long start = System.nanoTime() потом вызвать оригинальный метод.invoke(bean, args) правда нам нужно что-тотвернуть из хэндлеа,поэтому назначим ему Object returnVal = ... потом замерить наносекунды after и вывести разницу after-before

и после этого вернуть returnVal

только один момент - этовсе должно раюотатьтолько в том случае,если контороддер enabled Завернем в if, иначе просто вернем метод.инвок

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

ЧАСТЬ 6 MBean server (не связаносо Спрингом)

Как теперь нам сделать, чтобы этот флажок можно было через какой-нибудть JMX-console включать-выключать? Есть такая штука как MBean (название не связано со Спрингом. Когда-то на нем построилицелый ^JBoss...) Есть такия штука как MBean-сервер,он подлнимается всегда когда полднимается java-процесс И все объекты, которые в нем зарегистрированы, через JMX-консоль можно их менять, запускать их методы Конывенция очень старая, поэтому немногодурацкая: надоиимплементировать интерфейс с тем же названием,что и класс,но в конце MBean ... implements ProfilerControllerMBean создаем этот интерфейс и внем указываем те методы,корторые мы хотим,чтобы были доступны через JMX-console в нашем случае это setEnabled()

Теперь про MBean. То что мы сделали интерфейс,а тееперь его надо зарегистрировать в MBean-сервере. Где нам его зарегистрировать? например,в конструкторе напишем в ппостПроцессоре пустой конструктор, там:

BeanServer platformMBeanServer = ManagementFactory.getPlatformBeamServer(); platformMBeanServer.registerMBean(controller, new ObjectName("profiling", "name", "controller")); objectName состоит из частой: 1я доен,т е попочкавктоторой это будет находиться, и ключ-значение параметра - у нас "name" и "controller"

тут может быть куча проблем - не имплинтерфейс, мбин уже зареган, куча эксепшнов, просто бросим их все вместе throws Exception

теперь наш постПроцессор надо зарегистрировать в контексте (xml )

ну и еще чтобы увидеть результат, поменяем main() чтобы цитаты выводились постоянно

завернем getBean() в while(true ), там добавим Thread.sleep(100)

// К ЭТОМУ МОМЕНТУ В КОДЕ НАКОПИЛОСЬ НЕСКОЛЬКО БАГОВ,
// ИСПРАВИМ ИХ, ПРЕЖДЕ ЧЕМ ИДТИ ДАЛЕЕ

    1. "не удается создать бин", вложеный эксепшн - NPE
    ...забыл проинициализиорлвать new HashMap<>()

    2. забыл поставить аннотацию @Profiling - поcтавил на класс TerminatorQuoter

    3. все сломалось на вызове метода прокси - в InvocationHandler я вызываю method.invoke на переданный в него proxy,а надо бы на изначальный bean

//ПРОДОЛЖИМ

// таймкод 44:40 ..запукскаем... упало, почему?

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

^это тот момент, когда мы в main после после создания контиекста из xml вытаскиваем бин. Должныбы по интерфейсу, а вытаскиваем по классу

сделаем брейкпоинт в main на sayQuote() есть нескоько полезных методов, которые помогают хорошо дебажить (откр окно "expression evaluation" Alt+F8...почему-то не рабоиает в моей IDE) первый -getBeanDefinitionNames() оттого что поставили config, появилось много бинов- autoweiredAnnotatonionBeanProce

теперь разбираемся с MBean... Идём в bin/jconsole. Находим там наш аттрибут - controller/enabled, ставим ему true -

...класс, всё работает! профилируется.

Итак,сейчас мыможемнад любым свимбиновставить аннотацию @Profilinfg о онбудетпрофилироваться,когдамыбудем вклпрофилироване извне придумали своюаннотацию, придумалисвой БинПостПроцессор,который ее обраытывает на этапе создаиябина, и БПП может не просто "подкруьить" бин, вообщепоменять логику его класса длячегонадо зать dynamicProxy и CGLib

ЧАСТЬ 7. Трёхфазовый конструктор и ContextListener

Рассмотрим еще один компонент ApplicationListener. Он умеет слушать контекст Спринга, т.е.всеивенты которые с ним происходят, а именно: -ContextStartedEvent -ContextStoppedEvent -ContextRefreshedEvent -ContextClosedEvent Самый интересный для нас - это ContextRefreshed (а вовсе не ContextStarted, т.к. starte означает,что Контекст только начал свое построение. А когда он это построение заканчивает, он делает refresh)

Допустим, у нас есть некий сервис, у которого есть метод warmCache(), который разогревает кэш, то есть лезет в БД, наполняет свои листы, и только тогда он готов к работе. Вопрос, где нам этот метод вызывать? Точно не в конструкторе,т к там еще ничего нету, бин не настррен, и в БД не может сходить, т к ни dataSource не проинжектился, ничего... Пробуем в ПостКонстракт. Работать-то будет, но транзакции на этапе постКонстпакт еще не существуют, они еще не настроены... Почему? Разберемся. Кто настраивает транзакции? У нас метод warmCache(), над которым стоит @Transactional - мы хотим чтобы все медтоды были транзакционными... но за эту аннотациюотвечачет БПП - а в какой момент он запихает в наш класс логику, связанную с транзакцией? Очевидно, что после того, как отработает @PostConstruct. Вспромнимпро 2 этапа: сначала postProcessBeforeInitialization, потом PostConstruct, а потом postProcessAfeterInitialization, на котором настраиваются прокси, а значит - наш постКонстракт отрабюотает Дотого, как настоилиись прокси, включая те, которые отвечаютза транзакции.

Что дедать? Получается, мы хотим иметь третью фазу конструктора...

Вернемся к нашему примеру. Мы совсем не будем вызывать sayQuote() из main, а вместо этого напишем над ним аннотацию @PostConstruct в ТЕрминаторе. Это не совсем правильно - иметь сразу два постКонстракта, но раюбтаеть будет.

//В КОД

Также назначим вКонтроллере enabled=true, чтобы всегда раюотало профилирование

запускаем - и профилирования нет! хотим убедиться,чтодело именно вЭтом(?) Идемобратно в main и вручнуювызовем sayQuote() Теперь, во второй раз, оно появилось

Почему? Потому чтона этапе постКонстракт никаких проски еще нет

Поэтому мы спейчас придумаем еще одну аннотацию @PostProxy ставим ее над sayQuote() выведем в sayQuote() текст "3я фаза"

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

Пишем этот PostPoxyInvokerContextListener он имплементирует ApplicationListener причем обратим внимание на возможность параметризовать его Зачем нам это? Делов том,что эвентов есть 5 видов, а мы слушаем только один Нам необязательно делать instanceOf и т.д. поэтому мы поставим дженерик на автоматом оверрайдится метод

что мы можем из этого ивента вытащить? ApplicationContext, и положить его всторонку из Контекста вытащим имена всех своих бинов,и тожеотложить getBeanDefinitionNames() сейчас мы по ним все пройдпмся и провенрим на наличие аннотации @PostProxy пройдемпо именам for вопрос: можемли мы сейчас вытаскивать бин по имени,и делать getClass? Нет,т.к нас проски, а в них ничего инетересного

Более того, это еще и неправильно. Представим,что мы определили Бин как ленивый то есть, что он должен создаться лишь в момент, когда его запросят. А мы сейчас будем проходить по всем бинам, чтобы проверить, у кого есть метод аннотированный @PostProxy, чтобы сейчас его запустить. Полуается, придется создать все бины, в том чисте тот ленывый, а это противоречит б-логике. Поэтому неправилдьно вытаскивать бины, а правильно вытаскивать только их definition'ы, и в них искать то что нам надо.

Для этого нужно обратиться к главной фабрике Спринга, только она умеет делать getBeanDefinition()

для этого еенужно инжектнуть @Autowired private ConfigurableListableBeanFactory factory;

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

Из этого beanDefinition можем вытащить оригинальное название класса getBeanClassName() теперьпо этому имени можем получить объект класса Class.forName(originalClassName) (Экспешн через try-catch) у этого originalClassa возьмем все методы [], пройдемсяпо ним,и если метод аннотирован @PostProxy, то этот метод надо запустить...но как? просто method.invoke() не сработает, потому что сейчас мы ищем метод в оригинальном классе, а бин с которым мы работаем, создан из прокси (сработает только если наши прокси через CGLib, а через dynamicProxy не будет. А в Спрингше как раз dynamicProxy) что же нам запускать? надо вытащить метод у текущего класса (который прокси) дляэтого надо вытащить сам бин context.getBean(name) потом вытащим из него класс, и из него вытащим метод по имени и сигнатуре bean.getClass().getMethod(method.getName, method.getParameterTypes) Новый эксепшен апкастим до Exception в try-catch и теперь наконец-то мы можем запукстить этот currentMethod на наш бин (без аргументов - почему?)

надо проверить-запустить идем вконтекст, регистрируем наш новый спринговый компонент

запускакем

уберем все лишнее в main()

видим все 3 фазы

Подведем итоги 3хфазового конструктора: 1я фаза - java конструктор 2яфаза - @PostConstruct 3яфаза - @AfterProxy, которую можно сделать через ContextListener

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

//КОНЕЦ 1го видео

// так, почему-то в factory инжектится null...

дело в том,что поддержка аннотации @Autowired не входит в пакет CommonAnnotations, поэтому при конфигурировании xml ее надо вручную дописывать в контекст:

ЛЕКЦИЯ 2

ЧАСТЬ 8 BeanFactoryPostProcessor

Что такое BFPP? Они позволяют подкрутить BeanDefinition до того, как бин создастся Например, выставить проперти. Пример, ситуация: у нас есть бин Login, у него имя и пароль мы не хотим сейчас хардкодить пароль, а хотим написать ${password}, но когда будет создаваться бин, вместо него будет использоваться настоящее значение. Вот за это и отвечает BFPP, который на этапе обработки дефинишнов сможет этот бинДефинишн подкрутиьт

Кроме этого, BFPP может подкрутить и саму BeanFactory перед тем, как она начнет работать. BFPP это интерфейс, у которого есть один-единственный метод, с доступом к BeanFactory

postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)

Разберем на следующем примере: В мире Java существует аннотация @Deprecated. У нее @RetentionPolicy -рантайм Обычно как используется эта аннотация? ее пишут, когда уже есть какой-то класс на замену устаревшему Так почему бы новую имплементацию не засунуть в саму аннотацию обязательным параметром?

Допустим, наш Терминатор устарел. Напишем ему аннотацию @DeprecatedClass(newImpl = T1000.class), и именно для нее мы напишем поддержку,чтобыиспользовать ее в таким формате. Создадим класс аннотации, внутри проперти newImpl()

Создадим T1000 extends TerminatorQuoter, переопределим метод sayQuote() который будет говорить другое

Теперь нам надо,чтобы все бины, помеченные @DeprecatedClass заменились на указанные в параметре implClass, и в этом нам поможет BeanFactoryPostProcessor, т.к рассмотренные ранее инструменты, такие как BPP, нам бы не помогли, потому что надо поменять имплементацию в бинДефинишенах на новую, еще до того как Фабрика начнет создавать объекты.

Это не очень сложно. Пишем класс DeprecationHndlerBeanFactoryPostProcessor implements BeanFactoryPostProcessor в методе postProcessBeanFactory берем фабрику, которая в негоприходит, и берем getBeanDefinitionNames() для каждого имени берем getBeanDefinition(..) изкоторого вытаскиваем getBeanClassName() делаем ему Class.forName(...) +эксепшн вытаскаваем из полученного класса аннотацию @DeprecatedClass Если она != null значит на его beanDefinition надо установить setBeanClassNAme(annotation.newImpl().getName()) Всё! регистрируем в xml Запускаем - ничего не происходит Потому что не стоит @PostConstruct сделаем просто через getBean() в Main Правда, надо еще сделать чтобы T1000 implemetnts Quoter По хорошему, надобыло бынаприсать рекурсивный обход Когда мы делаем прокси, мы не ищем повсем классам,которые насдледуются от этого интерфейса метод getInterfaces возвращает только те интерфейсы,которые имплементит именно наш класс А если мы наследуемся от какого-то другого класса, у которого тоже есть интерфейсы, то надоделать рекурсивный обход, а сейчас некогда. Лучше скажем T1000 implements Quoter (..есть какойто чудо-метод который может так делать,но его никто не помнит)

Проверяем... "Я жидкий"! Круто, а ведь у нас даже такого бина нету. Поэтому если так делать в продакшене, то за это могут уволить))

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

9 ПРОМЕЖУТОЧНЫЕ ИТОГИ, способы определения бинов/создания контекста

Итоговый алгоритм Спрингана текущий момент: -парсинг xml -создались beanDefinitions -BeanFactoryPostProcessor'ы подкрутили бинДефинишены -создалить постпроцессоры -пошелпроцесс создания объектов -Все готовые объекты сидядв хэшМапе,которая называется "контейнер"

Спринг 1 работал с XML Спринг 2.5 - с аннотациями

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

Почти для всего, что возможноделоать в xml, можносделать через анотации: Деалаем бин - ставим @Component Хотим сделать prototype - пишем аннотацию @Scope Хотим lazy - @Lazy хотим чтобы он зависел отдругого бина- @DependsOn

Итак,длятого чтобы ктотопросканипровал пакеты, в которых лежат классы,аннотированные @Component, есть 2 варианта:

  1. <context:component-scan base-package="com...." />
  2. Если вдруг так получается, что в контексте только одна строчка - см выше, то есть смысл отказаться от xml, а создать контекст через new AnnotationConfigApplicationContext("com...")

Как это работает? Есть компонент ClassPathBeanDefinitionScanner, он сканирует пакет и ищетвсе бины,аннотированные @Coimponent илилюбой другой аннотацией,связанной с @Component Он не является BPP или BFPP, а является ResourceLoaderAware

Создает дополнительные BeanDefinition из всех классов, аннгтированных @COmponent или @Servicebkb @Repository... или люборй другой аннотации, которая где-то там в глубине держит @Component

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

За парсинг java-конфигов отвечает AnnotatedBeanDefinitionReader, этот класс не имеет отношения к интерфейсу BeanDefinitionReader, а является частью ApplicationContext-а. Тоесть, когда мы создаем AnnotationConfigApplicationContext и передаем один или несколько JavaConfig'ов, у него внутри есть AnotatedBeanDefinitionReader ,но онделает нескотлько другие вещи. Чем отличается java-конфиг от xml или от groovy-скрипта? Тем,что это преждевсегоофмф-файл,на него накручивается прокси, и каждый разкогданужен бин,происоли делегацияв наш метод,который мы прописали в этом java-конфиге

Пример java--конфига

@Configuration
@ComponentScan("root")
public class JavaConfig{
    @Bean
    public CoolDao dao(){
        return new CoolDaoImpl();    
    }

    @Bean(initMethod = "init")
    @Scope(BeanDefinition.SCOPE_PROTOTYPE)
    public CoolSrevice coolService() {
        CoolServiceImpl servise = new CoolServiceImpl();
        service.setDao(dao());
        return service;
    }
}

у нас тут класс, аннотированный @Сonfiguration, под @ComponentScan указаны пакеты, которые мы хотим дополнительно просканировать, бины с указанием @Scope и инит-метода, и внутри пишем ту логику,которая будет запускаться каждый раз при создании бина... Апоскольку CoolService это прототайп, токаждый раз когдаСпрингбудет егосоздавать, Спринг будет делегитровать в эту логику. Это выглядит как будто это метод java,но это не метод, а определение бина, которое просто имеет формат java-метода... Java-конфиги обрабатывает ConfigurationClassPostProcessor - это особый BFPP. Его, как BFPP, регистрирует AnnotationConfigApplicationContext, и он создает бин-дифинишны по @Bean, а также ^относится к @Import (импорт другой конфиги), @ImportResource (импорт xml), @ComponentScan (где будет задействован тот же самый "крот" - ClassPathBeanDefinitionScanner)

Spring 4 принес с собой конфигурацию на groovy, и на нее можно смело перезодить- она заменит всё Вот так прописывается бин: (где-то сверхустоит импорт) beans{ myDao(DaoImpl)
}

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

beans{
    myDao(DaoImpl)      

    coolSerevice(coolServiceImpl) {bean ->
        bean.scope = 'prototype'
        dao = myDao
    }
}

тут испольуется лямбда-выражение (на Груви это называется closure)

создается это так:

new GenericGroovyApplicationContext("context.groovy")

в java- конфиге есть много волшебны аанотаций,а в xml есть много неймспейсов - например, <component-scan..>, а на 2014 год в груви еще ничегоэтого не было ^проверить,что появилось

и еще проблема (на 2014 год), что если в примере выше переставитть метстами эти объявления этих двух бинов, то не будет работать, т.к.скрипт читает скрипт по порядку.. ^ узнать, как это решается в 2024?

ЧАСТЬ 10 - Пишем свой тип контекста

//таймкод 20:00

на самом деле не совсем свой - мы просто вернем к жизни забытый "старый" способ конфигурирования

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

в папке resources создаем файл context.properties

стркутура файла такая:

quoter.(class) = quoters.TerminatorQuoter
quoter.message = Get DAAWN!

Теперь сделаем класс PropertyFileApplicationContext extends GenericApplicationContext

внемпишем конструктор, принимаюий String fileName в нём,с помощью new PropertiesBeanDefinitionReader(this) - принимает тот контекст, для которого мы хотим регистрировать бины,то есть у нас this У этого ридера мы можем загрузить все бин-дефинишны из проперти-файла reader.loadBeanDefinitions(fileName) - возвращает количество дефинишенов выведем на экран "found " + i бинов

сделаем прямо тут main в нём new PropertyFileApplicationContext("context.properties"); вытащим из него бин Quoter.class, и запустим у него sayQuote()

...запускаем, нашел 1 бин, но поскольку у нас repeat=0, то цитата не выводится в те времена ничего не знали об аннотациях, поэтому наш @InjectRandomInt не работает, проинжетируем repeat вручную в проперти-файл

quoter.repeat = 1

вот теперь упало, но по достаточно смешной причине: как и xml, проперти-конфигурация работает толтько если на инжектируемом поле есть сеттер (^запомнить) добавим сеттер в поле repeat в Терминаторе

...запускакем, забыл вызвать refresh() в конструкторе Контекста ...Также в Проперти-файле имя интерфейса Quoter.(class) надо было указывать с маленткой буквы ...также забыл указать полное имя пакета в проперти-файле ...все работает.

в будущем можно было бы продолжить разработку этого контекста: добавить аннотации, ProperyFileProcessor'ы и др.

^единственное, что у автора срабатывает 1я и 3я фаза коструктора, а у меня только 1я...

ЧАСТЬ11, Как обновлять прототайпы в синглетоне

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

Делаем новый пакет screensaver в нем 2 класса, первый - ColorFrame extends JFrame в нем контструктор и поле Color color, аннотированное @Autowired в конструкторе setSize(200,200); setVisible(); setDefaultCloseOperation(EXIT_ON_CLOSE); метод void showOnRandomPlace(), который будет менять setLocation(random.nextInt(1200), ...) и getContentPane.setBackground(color) -который в нас инжектят и repaint()

теперь надо прописать бин,возпользуемся JavaConfig для разнообразия а также потому что нам надо прописать еще один бин Color. а это класс не наш,мы не можем в нем поставить аннотацию @Component.... Тут автор заскучал и решил зарегистрировать наш ColorFrame с помощью аннотации @Service

делаем класс Сonfig, над ним @Configuration и @ComponentScan(basePackages = "screensaver") внутри пропиываем @Bean public Color color(){ Random random = new Random() return new Color(random.nextInt(255),random.nextInt(255),random.nextInt(255)); }

кстати,это яркий пример,чем хорош java-конфиг. В xml таксделать сложнее... конечно можно было бы прикрутить FactoryBean (^как?), но это не очень красиво Кроме того, здесь мы пишем с помощью IDE

Прямо здесьсделаем main() в нем AnnotationConfigApplicationContest(Config.class), который заодно попросит "крота" ClassPathBeandefinitionScanner просканировать весь пакет, и там найдет там еще один бин, аннотированный аннотацией @Component (^ может он имет в виду @Service ?) и теперь из этого контекста в бесконечном цикле будем вытаскивать getBean(ColorFrame.class).showOnRandonPlace() ну и небольшой sleep чтобы сильно не мелькало

...запускаем, что-то не меняется цвет

очевидно, дело в скоупах, надо поставитт @Prototype на Color в конфиге Ну и на всякий случай поставим @Prototype на ColorFrame

...запускаем, теперь цвет меняется, но тоже что-то не то :)))

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

запускаем, все равно не работает нормально - не обновляет цвет. Дело в том, что .... (^кто?) принимает скоуп того бина, который в него инжектится у нас один раз создается Фрейм, потому что он синглетон И мы каждый раз делаем ему ^лукап (то есть context.getBean(ColorFrame)) Мы всегда получаем тот же объект, а значит нет никакой причины, почему нужно по новому создавать и обновлять Color, пусть даже он и является прототайпом (и это очень типичный вопрос на интервью - "а как вы обновляете прототайпы в синглетоне?")

Это не очень распространенная задача, но иногда она встречается в проде. Есть несколько вариантов Самый неправильный это вместо Колора инжектить Контекст, и из него доставать Color - каждый раз новый. Работать будет, но это ужасно. Мы ведь хотим получать удовольствие при написании юнит-теста. А здесь наш Фрейм, который часть бизнес-логики, не имеющий отношения к Спрингу, начинает зависеть от всего Контекста, всего лишь из-за того, что еу нужен какой-то там цвет... Лучше уж вообще не пользоваться Спрингом, чем делать вот так...

Есть решение получше, но оно тоже не самое оптимальное. Это в Конфиге при задании скоупа Цвета после скоупНейма указать ProxyMode = ...TARGET_CLASS) Теперьмыснова,как и ранее, просто инжектим Color, и все будет работать. Но у этого решения есть своя дорогая "цена": каждый раз при обращении к Color, даже если дважды в одном методе,каждый раз мы получаем новый бин. Это далеко не всегда нужно...

Теперь - самое правильное решение. Это лукап-метод, то есть в классе ColorFrame мы в setBackground(getColor) вместо создания new Color(...) мы обращаемся в некий метод getColor(), а этот метод делаем абстрактным! После чего весь класс ColorFrame становится абстрактным.

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

А вот там где мы прописываем ему бин (в java-конфиге), мы уже можем прописать реализацию этого абстрактного класса, для чего надо прописать абстрактный метод. Здесь в Конфиге у нас ужеесть естественный доступ к Color, поэтому мы переопределяем абстрактный метод getColor и в нем возвращаем наш цвет:

@Bean public ColorFrame frame(){ return new ColorFrame(){ @Override getColor(){ return color();
} } }

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

В отличие от предыдущего способа, когда при каждом лобращении к бину, Color тоже каждый раз берется из Контекста. А здесь мы можем этот момент контроллировать, и создаем новый color именно в тот момент, когда вызываем метод getColor(). После этого мы можем например его сохранить,и какое-то время использовать, а чтобы потом получить новый, снова вызовем этот метод.

сделаем sleep побольше... запускаем, все работает. если же выставить над Color скоуп "singleton", цвет меняться не будет.

ЧАСТЬ 12: написание кастомного скоупа.

Теперь предположим, что нам надо менять цвет каждые 3 секунды. И теперь прототайп нам не очень подходит. Нужен какой-то другой скоуп, т.к по лукапу всегда дает новый бин. А мы хотим новыйс бин кажлые 3 секунды. Укажем в аннотации новый скоуп - "periodical"

Чтобы написать свой собственный скоуп, создадим новый класс PeriodicalScopeConfigurer implements Scope (не путать с классом одноименной аннотации) оверрайдятся методы.. Из них нам сейчас интересен только метод get() Когда мы позже зарегистрируем наш кастомный скоуп как частью Спринга, тогда каждый раз, как Спрингу надо будет знать, что делать когда он встретит скоуп periodical, он будет делегировать этот вопрос в метод get(), и уже непосредственно в нём мы решаем, отдавать нам новый бин, либо как-то кешировать.

Надо создать какую-то мапу, в которой ключом явл имя бина, а значением - пара из самого объекта бина, и сколько времени он существует (local daytime) Map<String, Pair<LocalTime, Object>>

Теперь метод get. Первое, что мы будем делать - это проверять containsKey(name), тогда вытаскиваем пару, и надопосмотреть, сколько времени этот бин был создан: если давно, то надо получить новый объект,заменить его в мапе, и вернуть тому, кто попросил. Давность определим через LocalTime.now().getSecond() - pair.getKey().getSecond(), Tсли давность >3, то кладем в мапу (name, new Pair(now(), objectFactory.getObject(?)))

Если же ключа в мапе нет, то создается новый объект, и запоминается, который сейчас момент времени. И потом в конце возвращаем map.get(name).getValue()

Теперь этот скоуп-конфигюрер (^больше подходит слово "resolver") надо зарегистрировать. То есть - сказать Контексту, что он отвечает за стринг "periodical" в аннотации @Scope. В какой момент надо это сделать? до создания контекста, т.е нам поможет BeanFactoryPostProcessor

Пишем класс CustomScopeRegistryBeanFactoryPostProcessor implements BFPP это класс будет регистрировать любой кастомный скоуп.

@override-метод принимает beanFactory, у которой есть метод registerScope() который принимает имя скоупа и new Конфигюрер

И вот этот BFPP надопрописать как бин. тк у нас java-конфиг, просто положим егов пакет screensaver и напишем анотацию @Component

...запускаем, работает.

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

spring-the-ripper-borisov's People

Contributors

varrivoda avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.