Прежде всего, поделитесь всеми предыдущими статьями, ставьте лайки, добавляйте в избранное и пересылайте три раза подряд. >>>>😜😜😜
Сборник статей:🎁nuggets.capable/post/694164…
Github :👉github.com/black-ant
Резервное копирование CASE:👉git ee.com/ant black/wipe…
Введение
Это одно, чтобы говорить о основном потоке приложения Springboot.
Ввод основного потока SpringApplication очень прост:
@SpringBootApplication
public class BaseApplication {
public static void main(String[] args) {
SpringApplication.run(BaseApplication.class, args);
}
}
1 > 使用 @SpringBootApplication 注解,标明是 Spring Boot 应用。通过它,可以开启自动配置的功能。
2 > main 方法 : 调用 SpringApplication#run(Class<?>... primarySources) 方法,启动 Spring Boot 应用
Давайте пошагово рассмотрим эту запись, что именно делается в этом процессе, который в основном включает в себя следующие вещи:
- Создать и запустить прослушиватель
- Создать среду из аргументов
- Создание контекста через среду
- обновить контекст
- Конечно, он также напечатает баннер
блок-схема
2. Анализ процесса
2.1 Основной процесс SpringApplication
Основной процесс в основном находится в SpringApplication.class, давайте посмотрим на этот процесс:
Свойства SpringApplication
F- resourceLoader
?- 资源加载器
F- primarySources
?- 主要的 Java Config 类的数组
F- webApplicationType
?- 调用 WebApplicationType#deduceFromClasspath() 方法,通过 classpath ,判断 Web 应用类型。
F- listeners
?- ApplicationListener 数组。
F- mainApplicationClass
?- 调用 #deduceMainApplicationClass() 方法,获得是调用了哪个 #main(String[] args) 方法
// 提供了三种 ApplicationContext 加载类 , 这个后续会用上
String DEFAULT_CONTEXT_CLASS = "org.springframework.context.annotation.AnnotationConfigApplicationContext";
String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext";
String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext";
// 默认Banner 地址 -> "banner.txt"
String BANNER_LOCATION_PROPERTY_VALUE = SpringApplicationBannerPrinter.DEFAULT_BANNER_LOCATION;
Основной процесс метода запуска SpringApplication
M1_01- run()
T-> 计时 ,创建 StopWatch 并且启动 , 用于记录启动的时长
-> configureHeadlessProperty() : 配置 headless 属性
?- Headless模式是系统的一种配置模式。在该模式下,系统缺少了显示设备、键盘或鼠标
?- 该模式下可以创建轻量级组件 , 收集字体等前置工作
- getRunListeners : 获取 SpringApplicationRunListeners ,并且开启监听 listeners.starting()
- 1 创建 ApplicationArguments 对象
- 2 prepareEnvironment 加载属性配置(传入 listener + arguments ) -> M20_01
?- 执行完成后,所有的 environment 的属性都会加载进来 (application.properties等)
- 3 打印banner(printBanner)
- 4 创建Spring 容器(createApplicationContext)
- 准备异常对象(getSpringFactoriesInstances.SpringBootExceptionReporter )
- 5 调用所有初始化类的 initialize 方法(prepareContext) , 初始化Spring 容器
- 6 刷新容器(refreshContext) , 执行 Spring 容器的初始化的后置逻辑(afterRefresh)
T-> 计时完成
- 7 通知 SpringApplicationRunListener , 执行异常处理等收尾
Псевдокод основного процесса SpringApplication
public ConfigurableApplicationContext run(String... args) throws Exception {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 调用 M1_21 获取 ConfigurableEnvironment
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
// PS:M1_01_03
context = createApplicationContext();
// 从 factories 中获取 Exception M1_11
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// M1_35 : 为 Context 添加属性
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// M1_50 : 刷新容器Bean
refreshContext(context);
afterRefresh(context, applicationArguments);
// 计时结束
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
// 执行 implements ApplicationRunne 对象
callRunners(context, applicationArguments);
}catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
2.2 Подмодуль: Работа с окружающей средой
M1_21- prepareEnvironment
- getOrCreateEnvironment -> M21_01
-
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// 内部通过 WebApplicationType 生成不同的 Environment (可以set 自己的 Environment) -> M1_23
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 重写此方法以完全控制环境自定义,或者重写上述方法之一以分别对属性源或概要文件进行细粒度控制。 -> M1_24
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 对 configurationProperties 属性进行处理 -> M2_01
ConfigurationPropertySources.attach(environment);
// listener 处理
listeners.environmentPrepared(environment);
// 将 environment 绑定到 SpringApplication
bindToSpringApplication(environment); -> M1_25
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
M1_23- getOrCreateEnvironment
- 通过 webApplicationType , 创建三种不同的 Environment
// M1_23 伪代码
private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
switch (this.webApplicationType) {
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
}
}
M1_24- configureEnvironment
- 获取 ApplicationConversionService 的实现类
- 调用 configurePropertySources(environment, args) , 在此应用程序环境中添加、删除或重新排序任何PropertySources。
- MutablePropertySources sources = environment.getPropertySources();
?- 获取前面的 MutablePropertySources
- sources.addLast : 添加 defaultProperties
- 如果 args > 0 , 且可以添加 commonLine , 则添加CommandLineProperties
?- 其中会判断属性 commandLineArgs 是否会存在 ,存在则常用替换方式
// M1_24 伪代码
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
if (this.addConversionService) {
ConversionService conversionService = ApplicationConversionService.getSharedInstance();
environment.setConversionService((ConfigurableConversionService) conversionService);
}
configurePropertySources(environment, args);
configureProfiles(environment, args);
}
C2- ConfigurationPropertySources.
M2_01- attach(environment);
- sources = ((ConfigurableEnvironment) environment).getPropertySources() : 获取 MutablePropertySources
- attached = sources.get(ATTACHED_PROPERTY_SOURCE_NAME) : 获取 PropertySource
- 如果 attached 为null 或者 不等于 sources , 则将 sources 替换原先的 attached
// Binder : 从一个或多个容器绑定对象的容器对象
M1_25- bindToSpringApplication
C3- Binder
M3_01- bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context,boolean allowRecursiveBinding, boolean create)
- context.clearConfigurationProperty() : 清空原属性
- handler.onStart(name, target, context) : BindHandler 开始绑定
- bindObject : 将 属性绑定到对象
- handleBindResult : 返回 绑定结果
private <T> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context,boolean allowRecursiveBinding, boolean create) {
try {
Bindable<T> replacementTarget = handler.onStart(name, target, context);
if (replacementTarget == null) {
return handleBindResult(name, target, handler, context, null, create);
}
target = replacementTarget;
Object bound = bindObject(name, target, handler, context, allowRecursiveBinding);
return handleBindResult(name, target, handler, context, bound, create);
}catch (Exception ex) {
return handleBindError(name, target, handler, context, ex);
}
}
Конфигурация configureIgnoreBeanInfo
// 其中仅设置了2个属性 :
// 属性一 : spring.beaninfo.ignore , 用于
environment.getProperty("spring.beaninfo.ignore", Boolean.class, Boolean.TRUE)
// 设置二 : 设置到 System 中
System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME, ignore.toString())
//那么该属性是为了干什么 ? -> spring.beaninfo.ignore
当值为 true 时 , 意味着跳过对 BeanInfo 类的搜索 .
如果经历了对不存在的 BeanInfo 类的重复 ClassLoader 访问,可以考虑将这个标志切换为“ true”,以防在启动或延迟加载时这种访问开销很大
但是现阶段所有 BeanInfo 元数据类,默认值是"false"
2.3 Печать баннера
Просто из любопытства, зайди и посмотри
M1_28- printBanner : 准备 Banner 类
// 代码详情
private Banner printBanner(ConfigurableEnvironment environment) {
if (this.bannerMode == Banner.Mode.OFF) {
return null;
}
ResourceLoader resourceLoader = (this.resourceLoader != null)
? this.resourceLoader
: new DefaultResourceLoader(getClassLoader());
SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
if (this.bannerMode == Mode.LOG) {
return bannerPrinter.print(environment, this.mainApplicationClass, logger);
}
// 核心代码 -> 调用类 SpringBootBanner
return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}
// 核心类 SpringBootBanner
C- SpringBootBanner
M- printBanner
- printStream.println(line) : 逐行打印那个 Spring
- 打印版本号
?- :: Spring Boot :: (v2.3.1.RELEASE)
// ps : 点进去就知道了 , 逐行打印
- обеспечивает переключатель
- Создайте SpringApplicationBannerPrinter,
- Вызов print генерирует объект Banner
2.4 логика createApplicationContext
Логика создания ассоциированной логики ApplicationContext, где кратко:
C- SpringApplication
M1_30- createApplicationContext
- Class<?> contextClass = this.applicationContextClass;
IF- contextClass为null
- 根据 webApplicationType 类型,获得 ApplicationContext 类型
- AnnotationConfigServletWebServerApplicationContext
- AnnotationConfigApplicationContext
- AnnotationConfigReactiveWebServerApplicationContext
- 根据 contextClass 创建 ApplicationContext 对象
// M1_30 伪代码
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
// 根据不同 webApplicationType 准备不同的 ContextClass
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
// 反射构造器方法获得 context 实现类
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}
// ApplicationContext 核心流程值得深入 , 后面开一个单章来详细是说
PS: M1_01_03 генерирует ConfigurableApplicationContext
PS: Архитектура контекста приложения
Вот разница между реактивным
Как показано на рисунке, Reactive — важный стек технологий в Spring.Reactive можно использовать для создания реактивных, отказоустойчивых, отказоустойчивых и управляемых сообщениями реактивных систем корпоративного уровня..
Webflux не является заменой Spring MVC, Его основное приложение все еще находится на асинхронной модели программирования безблокированной программирования. Приложение с помощью WebFlux имеет более короткое время отклика, меньшие потоки для начала и меньше используемых ресурсов памяти.При этом, чем больше задержка, тем очевиднее преимущества WebFlux..
Подробнее см. в этом документе @blog.CSDN.net/U010862794/…
2.5 Промежуточные операции
M1_11 Получить класс обработки SpringBootExceptionReporter
getSpringFactoriesInstances(SpringBootExceptionReporter.class,new Class[] { ConfigurableApplicationContext.class }, context);
// Spring.factories
# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers
M1_12 callRunners: что делать
Этот метод в основном предназначен для запуска ApplicationRunner.
// callRunners 主流程
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
@Component
public class SourceTestLogic implements ApplicationRunner {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public void run(ApplicationArguments args) throws Exception {
logger.info("------> run <-------");
}
}
// 简单点说 , ApplicationRunner 初始化启动就是这里做的
2.6 Три обработки контекста
Вторичная обработка контекста делится на три этапа:
- prepareContext(context, environment, listeners, applicationArguments, printedBanner);
- Установите предварительное свойство Context
- refreshContext(context);
- Внедрение Beans, регистрация слушателей, инициализация и публикация событий
- afterRefresh(context, applicationArguments);
- В настоящее время пустая реализация
Основной процесс prepareContext
// prepareContext(context, environment, listeners, applicationArguments, printedBanner);
M1_35- prepareContext : 准备 ApplicationContext 对象,主要是初始化它的一些属性
-> 1 设置 context 的 environment 属性
-> 2 调用 #postProcessApplicationContext(ConfigurableApplicationContext context) 方法,
?- 设置 context 的一些属性 -> M1_36
-> 3 调用 #applyInitializers(ConfigurableApplicationContext context) 方法,
?- 初始化 ApplicationContextInitializer -> applyInitializers
-> 4 调用 SpringApplicationRunListeners#contextPrepared
?- 通知 SpringApplicationRunListener 的数组,Spring 容器准备完成
-> 5 设置 beanFactory 的属性
-> 6 调用 #load(ApplicationContext context, Object[] sources) 方法,加载 BeanDefinition 们
?-
-> 创建 BeanDefinitionRegistry 对象
-> 设置 loader 属性
-> 执行BeanDefine 加载
// M1_35 伪代码
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
// 为容器设置 environment
context.setEnvironment(environment);
// 设置容器 classloader 和 conversionService , 即容器中类的加载工具
postProcessApplicationContext(context);
// 在刷新上下文之前,将任何ApplicationContextInitializers应用于该上下文
applyInitializers(context);
// listeners 执行 , 在创建并准备好ApplicationContext之后调用,但在加载源之前调用。
listeners.contextPrepared(context);
if (this.logStartupInfo) {
// 打印启动日志配置
logStartupInfo(context.getParent() == null);
// 打印 active profile 信息
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
// beanFactory 注入相关Bean
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
// 添加一个新的BeanFactoryPostProcessor
// 该新的BeanFactoryPostProcessor将在刷新之前应用于此应用程序上下文的内部Bean工厂,然后再评估任何Bean定义。
// 在上下文配置期间调用。
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
// -> M1_38
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}
M1_36- postProcessApplicationContext : 在ApplicationContext中应用任何相关的后处理
- beanNameGenerator 存在则设置到 context.getBeanFactory().registerSingleton 中
- resourceLoader 存在且 为 GenericApplicationContext类型 , 则 setResourceLoader
- resourceLoader 存在且 为 resourceLoader 则 setClassLoader
- addConversionService 为 true 则设置到 BeanFactory 中
// M1_36 伪代码
protected void postProcessApplicationContext(ConfigurableApplicationContext context) {
if (this.beanNameGenerator != null) {
context.getBeanFactory().registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR,this.beanNameGenerator);
}
if (this.resourceLoader != null) {
if (context instanceof GenericApplicationContext) {
((GenericApplicationContext) context).setResourceLoader(this.resourceLoader);
}
if (context instanceof DefaultResourceLoader) {
((DefaultResourceLoader) context).setClassLoader(this.resourceLoader.getClassLoader());
}
}
if (this.addConversionService) {
context.getBeanFactory().setConversionService(ApplicationConversionService.getSharedInstance());
}
}
M1_37- applyInitializers
FOR- (ApplicationContextInitializer initializer : getInitializers()) : for 循环处理 Initializers
- Assert判断 是否为 ApplicationContextInitializer 的实例
- initializer.initialize(context) 初始化对象
M1_38- load
- 这里主要是创建 BeanDefinitionLoader
protected void load(ApplicationContext context, Object[] sources) {
// 创建 BeanDefinitionLoader , 直接 new 的
BeanDefinitionLoader loader = createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources);
if (this.beanNameGenerator != null) {
// beanName 生成类
loader.setBeanNameGenerator(this.beanNameGenerator);
}
if (this.resourceLoader != null) {
loader.setResourceLoader(this.resourceLoader);
}
if (this.environment != null) {
loader.setEnvironment(this.environment);
}
// -> M5_01
loader.load();
}
C5- BeanDefinitionLoader
M5_01- loader
?- 这里会循环调用 load source
// M5_01 伪代码
for (Object source : this.sources) {
count += load(source);
}
процесс обновления
C- AbstractApplicationContext
M1_50- refreshContext(context);
- refresh(context);
- 如果有 ShutdownHook (关闭钩子) , 则注册 registerShutdownHook
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 准备此上下文以进行刷新。
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// 告诉子类刷新内部bean工厂
prepareBeanFactory(beanFactory);
try {
// 允许在上下文子类中对bean工厂进行后处理
postProcessBeanFactory(beanFactory);
// 调用在上下文中注册为bean的工厂处理器
// IOC 的主要逻辑就在其中
invokeBeanFactoryPostProcessors(beanFactory);
// 注册拦截Bean创建的Bean处理器
registerBeanPostProcessors(beanFactory);
// 初始化此上下文的消息源
initMessageSource();
// 初始化此上下文的事件多主控器.
initApplicationEventMulticaster();
// 初始化特定上下文子类中的其他特殊bean.
onRefresh();
// 检查侦听器bean并注册它们.
registerListeners();
// 实例化所有剩余的(非lazy-init)单例.
finishBeanFactoryInitialization(beanFactory);
//最后一步:发布相应的事件
finishRefresh();
}catch (BeansException ex) {
// 销毁已创建的单件以避免资源悬空
destroyBeans();
// 重置“活动”标志.
cancelRefresh(ex);
// 将异常传播到调用方.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}
// 这一部分也是 AbstractApplicationContext 的主要流程 , 放在后续 ApplicationContext 单章说
процесс afterRefresh
PS: это пустая реализация
M1_60- afterRefresh
2.7 Обработка слушателя
Когда Приложение запускается,Слушатель включает в себя в общей сложности четыре операции :
- getRunListeners
- listener.starting
- listener.started
- listener.running
С моим скудным уровнем английского здесь может быть продолженное и прошедшее время, хахахаха
Можно видеть, что цель первого шага и четвертого шага относительно ясна, в основном, чтобы посмотреть на второй шаг.
getRunListeners
C- SpringApplication
M- getRunListeners
- getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args)
// 可以看到 , 从 Factories 中获取的 , 先阶段只有一个 EventPublishingRunListener
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
listener.starting
//这里的 starting 是 EventPublishingRunListener 运行
// 具体事件的处理逻辑 , 我们后续文档继续深入
C- EventPublishingRunListener
M- starting()
- this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
C- AbstractApplicationEventMulticaster
?- 将所有事件多播给所有注册的侦听器,并在调用线程中调用它们 , 简单点说就是事件群发
listeners.started(context)
// 这里的事件就更多了
for (SpringApplicationRunListener listener : this.listeners) {
listener.started(context);
}
这里还是 EventPublishingRunListener
@Override
public void started(ConfigurableApplicationContext context) {
context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context));
AvailabilityChangeEvent.publish(context, LivenessState.CORRECT);
}
listener.running
// 发布 ReadyEvent
context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context));
AvailabilityChangeEvent.publish(context, ReadinessState.ACCEPTING_TRAFFIC);
Место ограничено, поэтому я не буду подробно рассматривать какие именно реализации.
2.8 Другая обработка
private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception,
Collection<SpringBootExceptionReporter> exceptionReporters, SpringApplicationRunListeners listeners) {
try {
try {
// 处理退出码
handleExitCode(context, exception);
if (listeners != null) {
listeners.failed(context, exception);
}
}finally {
reportFailure(exceptionReporters, exception);
if (context != null) {
context.close();
}
}
}catch (Exception ex) {
logger.warn("Unable to close ApplicationContext", ex);
}
ReflectionUtils.rethrowRuntimeException(exception);
}
3. Резюме
В этой статье кратко рассматривается общий процесс SpringBoot.Из-за ограниченного места есть несколько моментов, которые пока не включены и будут добавлены позже.
- Аннотация @SpringApplication
- SpringapplationContext целая история
- Логика загрузки Listener
обновить запись
- v20210804: обновить макет, оптимизировать структуру