目录

前言

Run()方法

1、实例化SpringApplication对象

1、加载容器

2、装配初始化器

3、装配监听器

 4、加载主类

2、执行Run()方法

1、设置headless

2、启用SpringApplicationListener

3、加载Banner

1、图片Banner

2、文本Banner

4、异常报告类加载

5、准备上下文        

6、刷新上下文

7、系统上下文刷新完成后的监听器

8、执行自定义run方法

 9、监听器

1、listeners.starting();

2、listeners.started(context);

3、listeners.running(context);

总结


前言

        写项目的时候不想知道Run()方法里面都干啥了么?那些Bean还有各种配置啥时候加载进去的?现在看来看一下。

        一个SpringBoot项目的根目录中最重要的一个类就是Application.java,各自有各自的起名习惯,最主要的是这个类中标注了一个注解那就是@SpringBootApplication,随后最重要的就是main方法了。

         既然上次把@SpringBootApplication注解解析完了,这次先看一下main方法。

Run()方法

        点进去Run()方法看一下源码:

	public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }

    public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
		return run(new Class<?>[] { primarySource }, args);
	}

	public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
//1、先实例化SpringApplication对象
//2、再执行run()方法
		return new SpringApplication(primarySources).run(args);
	}

        看到最后一行,一步一步看一下源码:

1、实例化SpringApplication对象

	public SpringApplication(Class<?>... primarySources) {
		this(null, primarySources);
	}

	public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
//获取类加载器,但是这里传进来的是null,所以现在还没有实例化Bean的类加载器
        this.resourceLoader = resourceLoader;
//断言判空校验
		Assert.notNull(primarySources, "PrimarySources must not be null");
//将Application.class(你自己创建的main方法的类加载到主资源集合中)
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
//1、加载容器,这里就相当于一个筛选器,加载不同的Bean需要不同的容器
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
//2、这里就开始使用IOC了,加载META-INF/spring.factories中的初始化器
		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
//3、加载META-INF/spring.factories中的监听器
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
//4、加载主类
		this.mainApplicationClass = deduceMainApplicationClass();
	}

        再根据上述代码一步一步看一下流程:

1、加载容器

        这步我个人这么理解,第一个if

        使用ClassUtils.isPresent()对这三个类进行判断,看能不能加载,相当于一个过滤器,不然这里怎么会写出分叉逻辑,可以返回三个容器类型。

        isPresent()看过这个方法的源码后,你还会发现,我们使用的基本数据类型int,boolean,floot等都是在这里被ClassUtils中的“Map<String, Class<?>> primitiveTypeNameMap”加载起来的,则其他的对象类型String,Integer,Boolean等会被“Map<String, Class<?>> commonClassCache”加载起来。

2、装配初始化器

        上篇文章 SpringBoot自动装配中提到了,有两处有spring.factories,这里加载的是

3、装配监听器

 4、加载主类

        干的工作很简单啊,看一下源码

	this.mainApplicationClass = deduceMainApplicationClass();
	
    private Class<?> deduceMainApplicationClass() {
		try {
//获取栈堆的信息
			StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
			for (StackTraceElement stackTraceElement : stackTrace) {
//循环找出main方法
				if ("main".equals(stackTraceElement.getMethodName())) {
//加载main方法的类
					return Class.forName(stackTraceElement.getClassName());
				}
			}
		}
		catch (ClassNotFoundException ex) {
			// Swallow and continue
		}
		return null;
	}

         到这儿,实例化SpringApplication对象结束,开始执行run()方法。

2、执行Run()方法

        看一下源码,这段有点长,但是没事,一步一步来

	public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch();
    //计时器启动,我们总能看到项目启动时间多少,就是它统计出来的
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
//1、设置系统headless为true
		configureHeadlessProperty();
//2、加载SpringApplicationListener和上面初始化加载可不一样啊,上面的是ApplicationListener
		SpringApplicationRunListeners listeners = getRunListeners(args);
    //系统启动前-的监听,这个可以实现ApplicationListener<ApplicationStartingEvent>来自定义监听器
		listeners.starting();
		try {
    //初始化参数对象,args就是main方法里的参数
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    //准备环境变量和各种默认的转换器和格式器,意思就是把电脑中的环境变量加载进来
    //还有我们常用的那些日期格式,字符格式转换的类加载进来,所以说这一步做的还挺多
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
    //spring.beaninfo.ignore 的配置用来决定是否跳过 BeanInfo 类的扫描,如果设置为 true,则跳过。
			configureIgnoreBeanInfo(environment);
//3、这个就是系统启动时出现的“SPRINGBOOT”图形,这个可以看下源码
			Banner printedBanner = printBanner(environment);
    //创建AnnotationConfigReactiveWebServerApplicationContext,这块儿有些复杂,先不说
			context = createApplicationContext();
//4、异常报告类加载
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
//5、准备上下文
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
//6、刷新上下文
			refreshContext(context);
    //刷新后的操作
			afterRefresh(context, applicationArguments);
    //结束计时
			stopWatch.stop();
    //打印启动时间
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
//7、系统上下文刷新完成后的监听器
			listeners.started(context);
//8、执行自定义run方法
			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;
	}

1、设置headless

        看一下这个方法干啥的,源码:

	private static final String SYSTEM_PROPERTY_JAVA_AWT_HEADLESS = "java.awt.headless";

	private void configureHeadlessProperty() {
		System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, System.getProperty(
				SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
	}

        看到把这个全局静态变量设置为ture,那么“java.awt.headless”这个是干什么的?

        这个属性是用来禁用图形功能。在一些服务器环境或无图形界面的操作系统上,启用图形功能可能会导致不必要的资源消耗和性能下降。因此,将 java.awt.headless 属性设置为 true 可以使得 Java 应用程序可以在无图形界面的环境中运行,降低功耗。

        除了上面这种启动方式,也可以在application.yml中这样设置打开:

java:
  awt:
    headless: true

2、启用SpringApplicationListener

	private SpringApplicationRunListeners getRunListeners(String[] args) {
		Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
		return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
				SpringApplicationRunListener.class, types, this, args));
	}

3、加载Banner

	private Banner printBanner(ConfigurableEnvironment environment) {
		......
		if (this.bannerMode == Mode.LOG) {
//这里就是加载Banner的方法
			return bannerPrinter.print(environment, this.mainApplicationClass, logger);
		}
		return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
	}


	public Banner print(Environment environment, Class<?> sourceClass, Log logger) {
//获取Banner
		Banner banner = getBanner(environment);
		......
		return new PrintedBanner(banner, sourceClass);
	}


	private Banner getBanner(Environment environment) {
		Banners banners = new Banners();
//1、可以放图片Banner哦!!
		banners.addIfNotNull(getImageBanner(environment));
//2、文字的Banner
		banners.addIfNotNull(getTextBanner(environment));
		......
	}

1、图片Banner

    static final String BANNER_IMAGE_LOCATION_PROPERTY = 
"spring.banner.image.location";

    static final String[] IMAGE_EXTENSION = { "gif", "jpg", "png" };

    private Banner getImageBanner(Environment environment) {
		String location = environment.getProperty(BANNER_IMAGE_LOCATION_PROPERTY);
		......
        for (String ext : IMAGE_EXTENSION) {
			Resource resource = this.resourceLoader.getResource("banner." + ext);
			if (resource.exists()) {
				return new ImageBanner(resource);
			}
		}
	}

        spring.banner.image.location属性用于指定自定义启动横幅(Banner)的图片文件位置。

        1、可以将自定义的启动横幅图片放置在应用程序的src/main/resources/目录下,但必须是"gif", "jpg", "png" 中的一种:

src/main/resources/banner.png

       2、 也可以放在yml文件中:

# application.yml

spring:
  banner:
    image:
      location: classpath:banner.png

        这样,在启动 Spring Boot 应用程序时,控制台将显示指定位置的 banner.png 图片作为启动横幅,而不是默认的文本横幅。请确保图片文件存在,并且格式正确,以便正确显示在控制台中。

2、文本Banner

    static final String BANNER_LOCATION_PROPERTY = "spring.banner.location";

    static final String DEFAULT_BANNER_LOCATION = "banner.txt";	

    private Banner getTextBanner(Environment environment) {
		String location = environment.getProperty(BANNER_LOCATION_PROPERTY,
				DEFAULT_BANNER_LOCATION);
        ......
	}

         spring.banner.location属性用于指定自定义启动横幅(Banner)的文本文件位置。

        1、可以将自定义的启动横幅图片放置在应用程序的src/main/resources/目录下

src/main/resources/custom-banner.txt

       2、 也可以放在yml文件中:

# application.yml

spring:
  banner:
    location: classpath:custom-banner.txt

        banner.txt这个则是默认的文本,和上面的使用方式一样,只不过文件名字是:banner.txt。

4、异常报告类加载

        加载spring.factories中的Erro Reporters

5、准备上下文        

	private void prepareContext(ConfigurableApplicationContext context,
			ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments, Banner printedBanner) {
        context.setEnvironment(environment);
		postProcessApplicationContext(context);
		applyInitializers(context);
		listeners.contextPrepared(context);
        ......
	}

让我们逐行解释这段代码:

1、`context.setEnvironment(environment);`:设置应用程序的上下文环境。`environment` 包含了应用程序的配置信息和属性。

2、`postProcessApplicationContext(context);`:对应用程序的上下文进行后处理。这里可以进行一些定制化的操作,例如注册 Bean 或者添加自定义的配置。

3. `applyInitializers(context);`:应用初始化器。上文我们说的加载的初始化器可以在此处被调用。

4. `listeners.contextPrepared(context);`:通知所有的应用程序监听器,上下文已经被准备好了。这些监听器可以在应用程序的不同运行阶段做出操作。

6、刷新上下文

        应用程序上下文会经历一系列的初始化和刷新过程,包括 Bean 的实例化、依赖注入、后置处理器的应用、事件发布等。这是整个应用程序上下文初始化的核心步骤,确保应用程序上下文得到正确初始化和配置。

7、系统上下文刷新完成后的监听器

        `listeners.started(context)` 是用来通知应用程序启动监听器(ApplicationStartedEvent 监听器)的一个方法调用。它表示应用程序的上下文已经启动并刷新完成,在此时会触发 `ApplicationStartedEvent` 事件。

        `ApplicationStartedEvent` 事件是一种通知机制,用于在系统特定时间点广播事件,允许其他组件或代码对这些事件进行响应。例如初始化某些组件、执行定时任务、发送通知等。我们自己也可以编写一个实现了 `ApplicationListener<ApplicationStartedEvent>` 接口的监听器,并在其中实现相应的逻辑。

以下是一个简单的示例,展示如何创建一个应用程序启动监听器并处理 `ApplicationStartedEvent` 事件:

import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class MyApplicationStartedListener implements ApplicationListener<ApplicationStartedEvent> {

    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        // 在应用程序启动后执行一些逻辑
        System.out.println("Application is started. Do something here...");
    }
}

        我创建了一个名为 `MyApplicationStartedListener` 的监听器,并实现了 `ApplicationListener<ApplicationStartedEvent>` 接口。在 `onApplicationEvent` 方法中,我们可以编写需要在应用程序启动后执行的逻辑。

当应用程序上下文启动并刷新完成时,Spring Boot 会自动触发 `ApplicationStartedEvent` 事件,而 `MyApplicationStartedListener` 监听器将接收到这个事件,并执行其中的逻辑。

8、执行自定义run方法

        callRunners(context, applicationArguments)是用来调用应用程序的 `ApplicationRunner` 和 `CommandLineRunner` 的方法。

private void callRunners(ApplicationContext context, ApplicationArguments args) {
    List<Object> runners = new ArrayList<>();
//获取所有实现ApplicationRunner接口的类
    runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
//获取所有实现CommandLineRunner接口的类
    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);
        }
    }
}

        实现`ApplicationRunner` 和 `CommandLineRunner` 两个接口,用于在 Spring Boot 启动完成后执行一些自定义操作。例如加载初始化数据、执行定时任务、配置某些组件等。

        但是这两个接口的作用略有不同:
        `ApplicationRunner` 接口:定义了一个 `run` 方法,该方法在应用程序启动后被调用,传递一个 `ApplicationArguments` 对象,用于接收命令行参数。
        `CommandLineRunner` 接口:也定义了一个 `run` 方法,该方法在应用程序启动后被调用,传递一个 `String` 数组,用于接收命令行参数。

        在上述代码中,通过

context.getBeansOfType(ApplicationRunner.class)

context.getBeansOfType(CommandLineRunner.class)

        分别获取了所有实现的 `ApplicationRunner` 和 `CommandLineRunner` 的类,然后对类进行排序,按照顺序执行这些实例的 `run` 方法。

下面举个例子:

假设有一个简单的学生管理系统。在应用程序启动时,我们希望自动向数据库中插入一些初始化的学生数据,并打印出命令行参数。

首先,我们需要创建一个学生实体类和一个学生数据初始化服务类。

1、学生实体类 `Student.java`:

public class Student {
    private Long id;
    private String name;
    private int age;

    // 省略构造函数、getter 和 setter 方法
}

2、学生数据初始化服务类 `StudentDataService.java`:

@Service
public class StudentDataService {
    public void initializeData() {
        // 在这里执行初始化数据的逻辑,将学生数据插入数据库
        // 简化起见,这里不连接真实数据库,只在控制台打印初始化信息
        System.out.println("Initializing student data...");
    }
}

        接下来,我们需要创建一个实现 `ApplicationRunner` 接口的初始化器,或者一个实现 `CommandLineRunner` 接口的初始化器。这里我都创建出来看一下,这两个初始化器会在应用程序启动时被调用,并执行相应的逻辑。

3. 实现 `ApplicationRunner` 的初始化器 `StudentDataApplicationRunner.java`:

@Component
public class StudentDataApplicationRunner implements ApplicationRunner {
    @Autowired
    private StudentDataService studentDataService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 在应用程序启动后执行的逻辑,插入学生数据
        studentDataService.initializeData();
    }
}

4. 实现 `CommandLineRunner` 的初始化器 `CommandLineAppRunner.java`:

@Component
public class CommandLineAppRunner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        // 在应用程序启动后执行的逻辑,打印命令行参数
        System.out.println("Command line arguments: " + Arrays.toString(args));
    }
}

        现在,当应用程序启动后,`StudentDataApplicationRunner` 和 `CommandLineAppRunner` 这两个初始化器会自动被调用。`StudentDataApplicationRunner` 会执行 `StudentDataService` 中的初始化数据逻辑,而 `CommandLineAppRunner` 则会打印命令行参数。

 9、监听器

        上文第二大点“执行run()方法”中,如果仔细看,源码中提到了三个监听器分别是:

1、listeners.starting();

2、listeners.started(context);

3、listeners.running(context);

1、listeners.starting();

        是用来通知系统启动前的监听器(ApplicationStartingEvent 监听器)的一个方法调用。它表示系统即将开始启动,在此时会触发 ApplicationStartingEvent 事件。使用方法和上文第7小点一样哈,只不过要实现ApplicationListener<ApplicationStartingEvent>接口

2、listeners.started(context);

        是用来通知系统启动监听器(ApplicationStartedEvent 监听器)的一个方法调用。它表示系统的上下文已经启动并刷新完成,在此时会触发 ApplicationStartedEvent 事件。使用方法和上文第7小点一样哈,只不过要实现ApplicationListener<ApplicationStartedEvent>接口

3、listeners.running(context);

        是用来通知系统运行监听器(ApplicationRunningEvent 监听器)的一个方法调用。它表示系统的上下文已经启动并正在运行中,在此时会触发ApplicationRunningEvent事件。使用方法和上文第7小点一样哈,只不过要实现ApplicationListener<ApplicationRunningEvent>接口

        这三个监听器一定要区分开,是不一样的哦,它们都是可以扩展的,我们在系统不同的启动阶段可以做不同的监听器做自定义骚操作!

总结

        这里我们就分析完了Run()方法的运行流程,在这里面有很多的可以自定义扩展的,下面的这些:ApplicationContextInitializer、ApplicationListener、banner、exceptionReporter、ApplicationRunner、CommandLineRunner,还有listeners.starting()、listeners.started(context)、listeners.running(context),这些都是可以自定义进行个性化扩展的,对这些都熟悉后,自己去对SpringBoot启动优化时间、DIY监听器,都是可以做的,分析不易,有帮助的话点个赞!

最后我还整理汇总了⼀些 Java ⾯试相关的⾼质量 PDF 资料和免费Idea账号

公众号:Java小白,回复“⾯试” 和“idea破解”即可获取!

Logo

长江两岸老火锅,共聚山城开发者!We Want You!

更多推荐