前言
如果有一段程序需要在SpringBoot框架启动之后立马执行,这就需要借助启动加载器来完成这个需求
计时器介绍
StopWatch是位于org.springframework.util包下的一个工具类,通过它可方便的对程序部分代码进行计时(ms级别),适用于同步单线程代码块。
StopWatch 主要是spring为了计算加载耗时产生的。 其内部包含一个list(代表任务的个数),当开始调用start方法后,然后调用stop,都会记录当前时间和start时间的耗时,然后封装成一个任务对象加入到StopWatch内部的list中,其中start和stop方法必须成对出现。
public ConfigurableApplicationContext run(String... args) { // 计时工具 StopWatch stopWatch = new StopWatch(); stopWatch.start(); ...... stopWatch.stop(); ...... return context; }StopWatch源代码
public class StopWatch { /** * Identifier of this stop watch. * Handy when we have output from multiple stop watches * and need to distinguish between them in log or console output. */ //用来区分不同的StopWatch private final String id; //标记每次统计完一个任务耗时之后,需不需要将这次任务保存起来,即需不需要保存每次任务执行的信息 private boolean keepTaskList = true; //需要执行的任务集合 private final List<TaskInfo> taskList = new LinkedList<>(); /** Start time of the current task. */ //当前任务开始时间 private long startTimeMillis; /** Name of the current task. */ @Nullable //当前正在执行的任务名 private String currentTaskName; @Nullable //最近一次的任务信息 private TaskInfo lastTaskInfo; //此计时器一共执行过多少次任务 private int taskCount; /** Total running time. */ //执行过任务之后,一共统计过多少耗时 private long totalTimeMillis; /** * Construct a new stop watch. Does not start any task. */ public StopWatch() { this(""); } /** * Construct a new stop watch with the given id. * Does not start any task. * @param id identifier for this stop watch. * Handy when we have output from multiple stop watches * and need to distinguish between them. */ public StopWatch(String id) { this.id = id; } /** * Return the id of this stop watch, as specified on construction. * @return the id (empty String by default) * @since 4.2.2 * @see #StopWatch(String) */ public String getId() { return this.id; } /** * Determine whether the TaskInfo array is built over time. Set this to * "false" when using a StopWatch for millions of intervals, or the task * info structure will consume excessive memory. Default is "true". */ public void setKeepTaskList(boolean keepTaskList) { this.keepTaskList = keepTaskList; } /** * Start an unnamed task. The results are undefined if {@link #stop()} * or timing methods are called without invoking this method. * @see #stop() */ public void start() throws IllegalStateException { start(""); } /** * Start a named task. The results are undefined if {@link #stop()} * or timing methods are called without invoking this method. * @param taskName the name of the task to start * @see #stop() */ public void start(String taskName) throws IllegalStateException { if (this.currentTaskName != null) { throw new IllegalStateException("Can't start StopWatch: it's already running"); } this.currentTaskName = taskName; this.startTimeMillis = System.currentTimeMillis(); } /** * Stop the current task. The results are undefined if timing * methods are called without invoking at least one pair * {@code start()} / {@code stop()} methods. * @see #start() */ public void stop() throws IllegalStateException { if (this.currentTaskName == null) { throw new IllegalStateException("Can't stop StopWatch: it's not running"); } long lastTime = System.currentTimeMillis() - this.startTimeMillis; this.totalTimeMillis += lastTime; this.lastTaskInfo = new TaskInfo(this.currentTaskName, lastTime); if (this.keepTaskList) { this.taskList.add(this.lastTaskInfo); } ++this.taskCount; this.currentTaskName = null; } /** * Return whether the stop watch is currently running. * @see #currentTaskName() */ public boolean isRunning() { return (this.currentTaskName != null); } /** * Return the name of the currently running task, if any. * @since 4.2.2 * @see #isRunning() */ @Nullable public String currentTaskName() { return this.currentTaskName; } /** * Return the time taken by the last task. */ public long getLastTaskTimeMillis() throws IllegalStateException { if (this.lastTaskInfo == null) { throw new IllegalStateException("No tasks run: can't get last task interval"); } return this.lastTaskInfo.getTimeMillis(); } /** * Return the name of the last task. */ public String getLastTaskName() throws IllegalStateException { if (this.lastTaskInfo == null) { throw new IllegalStateException("No tasks run: can't get last task name"); } return this.lastTaskInfo.getTaskName(); } /** * Return the last task as a TaskInfo object. */ public TaskInfo getLastTaskInfo() throws IllegalStateException { if (this.lastTaskInfo == null) { throw new IllegalStateException("No tasks run: can't get last task info"); } return this.lastTaskInfo; } /** * Return the total time in milliseconds for all tasks. */ public long getTotalTimeMillis() { return this.totalTimeMillis; } /** * Return the total time in seconds for all tasks. */ public double getTotalTimeSeconds() { return this.totalTimeMillis / 1000.0; } /** * Return the number of tasks timed. */ public int getTaskCount() { return this.taskCount; } /** * Return an array of the data for tasks performed. */ public TaskInfo[] getTaskInfo() { if (!this.keepTaskList) { throw new UnsupportedOperationException("Task info is not being kept!"); } return this.taskList.toArray(new TaskInfo[0]); } /** * Return a short description of the total running time. */ public String shortSummary() { return "StopWatch '" + getId() + "': running time (millis) = " + getTotalTimeMillis(); } /** * Return a string with a table describing all tasks performed. * For custom reporting, call getTaskInfo() and use the task info directly. */ public String prettyPrint() { StringBuilder sb = new StringBuilder(shortSummary()); sb.append('\n'); if (!this.keepTaskList) { sb.append("No task info kept"); } else { sb.append("-----------------------------------------\n"); sb.append("ms % Task name\n"); sb.append("-----------------------------------------\n"); NumberFormat nf = NumberFormat.getNumberInstance(); nf.setMinimumIntegerDigits(5); nf.setGroupingUsed(false); NumberFormat pf = NumberFormat.getPercentInstance(); pf.setMinimumIntegerDigits(3); pf.setGroupingUsed(false); for (TaskInfo task : getTaskInfo()) { sb.append(nf.format(task.getTimeMillis())).append(" "); sb.append(pf.format(task.getTimeSeconds() / getTotalTimeSeconds())).append(" "); sb.append(task.getTaskName()).append("\n"); } } return sb.toString(); } /** * Return an informative string describing all tasks performed * For custom reporting, call {@code getTaskInfo()} and use the task info directly. */ @Override public String toString() { StringBuilder sb = new StringBuilder(shortSummary()); if (this.keepTaskList) { for (TaskInfo task : getTaskInfo()) { sb.append("; [").append(task.getTaskName()).append("] took ").append(task.getTimeMillis()); long percent = Math.round((100.0 * task.getTimeSeconds()) / getTotalTimeSeconds()); sb.append(" = ").append(percent).append("%"); } } else { sb.append("; no task info kept"); } return sb.toString(); } /** * Inner class to hold data about one task executed within the stop watch. */ public static final class TaskInfo { private final String taskName; private final long timeMillis; TaskInfo(String taskName, long timeMillis) { this.taskName = taskName; this.timeMillis = timeMillis; } /** * Return the name of this task. */ public String getTaskName() { return this.taskName; } /** * Return the time in milliseconds this task took. */ public long getTimeMillis() { return this.timeMillis; } /** * Return the time in seconds this task took. */ public double getTimeSeconds() { return (this.timeMillis / 1000.0); } } }通过start与stop方法分别记录开始时间与结束时间,其中在记录结束时间时,会维护一个链表类型的tasklist属性,从而使该类可记录多个任务,最后的输出也仅仅是对之前记录的信息做了一个统一的归纳输出,从而使结果更加直观的展示出来。
使用步骤
start方法
stop方法
StopWatch优缺点:
优点:
- spring自带工具类,可直接使用
- 代码实现简单,使用更简单
- 统一归纳,展示每项任务耗时与占用总时间的百分比,展示结果直观,性能消耗相对较小,并且最大程度的保证了start与stop之间的时间记录的准确性
- 可在start时直接指定任务名字,从而更加直观的显示记录结果
缺点:
- 一个StopWatch实例一次只能开启一个task,不能同时start多个task,并且在该task未stop之前不能start一个新的task,必须在该task stop之后才能开启新的task,若要一次开启多个,需要new不同的StopWatch实例
- 代码侵入式使用,需要改动多处代码
启动加载案例演示
ApplicationRunner与CommandLineRunner
我们可以实现 ApplicationRunner 或 CommandLineRunner 接口, 这两个接口工作方式相同,都只提供单一的run方法,该方法在SpringApplication.run(…)完成之前调用,我们先来看看这两个接口:
@FunctionalInterface public interface CommandLineRunner { /** * Callback used to run the bean. * @param args incoming main method arguments * @throws Exception on error */ void run(String... args) throws Exception; } @FunctionalInterface public interface ApplicationRunner { /** * Callback used to run the bean. * @param args incoming application arguments * @throws Exception on error */ void run(ApplicationArguments args) throws Exception; }都只提供单一的run方法,接下来我们来看看具体的使用
ApplicationRunner
构造一个类实现ApplicationRunner接口
@Component @Order(1) public class ApplicationRunner1 implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { System.out.println("\u001B[32m[>>> startup ApplicationRunner1 <<<]"); } }很简单,首先要使用@Component将实现类加入到Spring容器中,如果有多个的话通过@Order(1)进行排序,然后实现其run方法实现自己的初始化数据逻辑就可以了
CommandLineRunner
对于这两个接口而言,我们可以通过Order注解或者使用Ordered接口来指定调用顺序, @Order() 中的值越小,优先级越高
@Component @Order(1) public class CommandLineRunner1 implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println("\u001B[32m[>>> startup runner1 <<<]"); } }同样需要加入到Spring容器中,CommandLineRunner的参数是最原始的参数,没有进行任何处理,ApplicationRunner的参数是ApplicationArguments,是对原始参数的进一步封装,如果有多个的话通过@Order(1)进行排序
ApplicationRunner和CommandLineRunner排序规则
- 通过Order指定顺序
- Order值相同ApplicationRunner的实现优先执行
启动加载原理解析
从SpringApplication.run方法的第8步callRunners开始
public ConfigurableApplicationContext run(String... args) { ``` // 第八步:执行Runners callRunners(context, applicationArguments); ``` return context; } private void callRunners(ApplicationContext context, ApplicationArguments args) { List<Object> runners = new ArrayList<>(); //获取容器中所有的ApplicationRunner的Bean实例 runners.addAll(context.getBeansOfType(ApplicationRunner.class).values()); //获取容器中所有的CommandLineRunner的Bean实例 runners.addAll(context.getBeansOfType(CommandLineRunner.class).values()); //对runners集合进行排序 AnnotationAwareOrderComparator.sort(runners); for (Object runner : new LinkedHashSet<>(runners)) { if (runner instanceof ApplicationRunner) { //执行ApplicationRunner的run方法 callRunner((ApplicationRunner) runner, args); } if (runner instanceof CommandLineRunner) { //执行CommandLineRunner的run方法 callRunner((CommandLineRunner) runner, args); } } }很明显,是直接从Spring容器中获取ApplicationRunner和CommandLineRunner的实例,并调用其run方法,这也就是为什么我要使用@Component将ApplicationRunner和CommandLineRunner接口的实现类加入到Spring容器中了。
ApplicationRunner和CommandLineRunner实现类的差异点
- 执行优先级差异
- run方法入参不一致
ApplicationRunner和CommandLineRunner实现类的相同点
- 调用点一样
- 实现方法名一样
参考: https://blog.csdn.net/gxs1688/article/details/87185030
https://www.cnblogs.com/java-chen-hao/p/11835120.html