SpringBoot是基于SpringFramework的企业级应用开发框架,有了SpringFramework的基础,相信理解SpringBoot并不是一件难事。

SpringBoot启动原理

@SpringBootApplication

在SpringBoot项目中,一般情况下都有且仅有一个启动类:

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

其中@SpringBootApplication注解是SpringBoot的核心注解,它其实是一个组合注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
		@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
    // ....
}

其中重要的有三个Annotation:

  1. @Configuration(@SpringBootConfiguration也是应用了@Configuration)
  2. @EnableAutoConfiguration
  3. @ComponentScan

即SpringBoot项目的启动类也可以这么写(优化SpringBoot项目的启动时间就可以将注解拆开来写,缩小包扫描的范围):

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

注解解析

@Configuration

这里的@Configuration与JavaConfig形式的Spring IoC容器的配置类使用的@Configuration注解作用相同,启动类加上这个注解之后,本身就变成了IoC容器的配置类。

@Configuration的注解类标识这个类可以使用Spring IoC容器作为Bean定义的来源。@Bean注解告诉Spring,一个带有@Bean的注解方法将返回一个对象,该对象应该被注册为在Spring应用程序上下文中的Bean。

@ComponentScan

@ComponentScan的功能是自动扫描并加载符合条件的组件(比如@Component和@Repository以及其他衍生注解)或者Bean定义,最终将这些Bean定义加载到IoC容器当中。

可以通过@ComponentScan的一些属性来进行细粒度的定制自动扫描的范围,如果不指定,Spring框架实现默认会从启动类所在的类的包进行扫描,所以启动类最好是放在所有的业务代码的外层,但如果要放在根目录(com包)下需要谨慎,低版本的SpringBoot会有一些兼容性的问题。

@EnableAutoConfiguration

@EnableAutoConfiguration借助@Import的支持,收集和注册特定场景相关的Bean定义,在SpringBoot中还有一些与之类似的注解:

  • @EnableScheduling是通过@Import将Spring调度框架相关的Bean定义都加载到IoC容器
  • @EnableMBeanExport是通过@Import将JMX相关的Bean定义加载到IoC容器
  • @EnableCaching是通过@Impotr将将缓存相关的Bean定义加载到IoC容器

@EnableAutoConfiguration也是通过@Import将所有符合自动装配条件的Bean定义加载到IoC容器,会根据类路径中的jar依赖为项目进行自动配置,如:添加了spring-boot-starter-web依赖,会自动添加Tomcat和Spring MVC的依赖,SpringBoot会对Tomcat和Spring MVC进行自动配置。

@EnableAutoConfiguration作为一个组合注解,其自身定义关键信息如下:

@SuppressWarnings("deprecation")
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    // ...
}

借助AutoConfigurationImportSelector,SpringBoot可以将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器,借助Spring框架的一个工具类SpringFactoriesLoader,@EnableAutoConfiguration可以智能的完成自动配置的功能。

图片

在这其中,SpringFactoriesLoader扮演者十分重要的角色:

public abstract class SpringFactoriesLoader {
    //...
    public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) {
        ...
    }


    public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
        ....
    }
}

配合@EnableAutoConfiguration使用的话,它更多提供了一种配置查找的功能支持,即根据@EnableAutoConfiguration的完整类名org.springframework.boot.autoconfigure.EnableAutoConfiguration作为查找key,获取对应一组的@Configure类。

总的来说,@EnaleAutoConfiguration的作用是从classpath中搜寻所有的META-INF/spring.factories配置文件,并将其中org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的配置项,通过反射实例化为标注了@Configuration的JavaConfig形式的IoC容器配置类,然后加载到IoC容器当中。

启动执行流程

流程概览

  1. 使用SpringApplication的静态的run方法,而这个静态的run方法最终会调用SpringApplication实例的run方法,因此首先需要创建一个SpringApplication对象实例,然后调用这个创建号的SpringApplication的实例方法,在SpringApplication实例初始化的时候,它会提前做几件事情:
    • 根据classpath里面是否存在某个特征类org.springframework.boot.ConfigurableWebApplicationContext来决定是否应该创建一个Web应用使用的ApplicationContext类型
    • 使用SpringFactoriesLoader在应用的classpath中查找并加载所有可能的ApplicationContextInitializer
    • 使用SpringFactories在应用的classpath中查找并加载所有可能的ApplicationListener
    • 推断并设置main方法的定义类
  2. SpringApplication实例初始化完成并且完成设置后,就可以开始执行run方法的逻辑了,首先遍历执行所有通过SpringFactories可以查找到并加载的SpringApplicationRunListener,调用他们的started()方法
  3. 创建并配置当前SpringBoot应用将要使用的Environment(包括要使用的PropertySource以及Profile)、
  4. 调用所有的SpringApplicationRunListener的environmentPrepared()的方法
  5. 如果SpringApplication的showBanner属性被设置为true,则打印banner
  6. 根据用户是否明确设置了applicationContextClass类型以及初始化阶段的推断结果,决定该为当前SpringBoot应用创建什么类型的ApplicationContext并创建完成,然后根据条件决定是否添加ShutdownHook,决定是否使用自定义的BeanNameGenerator,决定是否使用自定义的ResourceLoader,并且将之前准备好的Environment设置给创建好的ApplicationContext使用
  7. ApplicationContext创建好之后,SpringApplication会再次借助Spring-FactoriesLoader,查找并加载classpath中所有可用的ApplicationContext-Initializer,然后遍历调用这些ApplicationContextInitializer的initialize(applicationContext)方法来对已经创建好的ApplicationContext进行进一步的处理
  8. 遍历所有SpringApplicationRunListener的contextPrepared()方法
  9. 最核心的一步,将之前通过@EnableAutoConfiguration获取的所有配置以及其他形式的IoC容器配置加载到已经准备完毕的ApplicationContext
  10. 遍历调用所有SpringApplicationRunListener的contextLoaded()方法
  11. 调用ApplicationContext的refresh()方法,完成IoC容器可用的最后一道工序
  12. 查找当前ApplicationContext中是否注册有CommandLineRunner,如果有,则遍历执行它们
  13. 正常情况下,遍历执行SpringApplicationListener的finished()方法(如果整个过程出现异常,则依然调用所有SpringApplicationRunListener的finished()方法,只不过这种情况下会将一场信息一并传入处理

去掉事件通知点后,整体流程大体如下:

image-20210624210942919

简单一点,也可以将启动流程分为三个部分:

  1. 第一部分进行SpringApplication的初始化模块,配置一些基本的环境变量、资源、构造器、监听器
  2. 第二部分实现了应用具体的启动方案,包括启动流程的监听模块、加载配置环境模块、及核心的创建上下文环境模块
  3. 第三部分是自动化配置模块,该模块是SpringBoot自动配置的核心。

启动详情

首先进入run方法:

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

可以看到调用了重载的run方法:

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

依然是一个静态方法,这里首先要构造一个SpringApplication实例,然后再调用实例的run方法,这里的构造方法也有重载的版本,最终调用的构造方法:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
   this.resourceLoader = resourceLoader;
   Assert.notNull(primarySources, "PrimarySources must not be null");
   this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
   this.webApplicationType = WebApplicationType.deduceFromClasspath();
   setInitializers((Collection) getSpringFactoriesInstances(
         ApplicationContextInitializer.class));
   setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
   this.mainApplicationClass = deduceMainApplicationClass();
}

这里主要是为SpringApplication对象赋一些初值。构造函数执行完毕后,我们回到run方法:

public ConfigurableApplicationContext run(String... args) {
		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);
			ConfigurableEnvironment environment = prepareEnvironment(listeners,
					applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			context = createApplicationContext();
			exceptionReporters = getSpringFactoriesInstances(
					SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			prepareContext(context, environment, listeners, applicationArguments,
					printedBanner);
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass)
						.logStarted(getApplicationLog(), stopWatch);
			}
			listeners.started(context);
			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. 创建了应用的见同期SpringApplicationRunListeners并开始监听

  2. 加载SpringBoot配置环境(ConfigurableEnvironment),如果是通过Web容器发布,会加载StandardEnvironment,其最终也是集成了ConfigurableEnvironment,类图如下

    图片

  3. 配置环境(Environment)加入到监听器对象中(SpringApplicationRunListeners)

  4. 创建run方法的返回对象:ConfigurableApplicationContext(应用配置上下文),创建方法如下:

    	protected ConfigurableApplicationContext createApplicationContext() {
    		Class<?> contextClass = this.applicationContextClass;
    		if (contextClass == null) {
    			try {
    				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);
    			}
    		}
    		return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
    	}
    

    方法会先获取显示设置的应用上下文(appcationContextClass),如果不存在,再加载默认的环境配置(通过是否是web environment判断),默认选择AnnotationConfigApplicationContext注解上下文(通过扫描所有注解类来加载Bean),最后通过BeanUtils实例化上下文对象,并返回。

  5. 回到run方法内,prepareContext方法将listeners、enviroment、applicationArguments、banner等重要组件与上下文对象关联

  6. 加下来的refreshContext(context)方法(方法如下)是实现spring-boot-starter-*等自动化配置的关键,包括spring.factories的加载,bean的实例化等核心工作

    	@Override
    	public void refresh() throws BeansException, IllegalStateException {
    		synchronized (this.startupShutdownMonitor) {
    			// Prepare this context for refreshing.
    			prepareRefresh();
    
    			// Tell the subclass to refresh the internal bean factory.
    			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
    
    			// Prepare the bean factory for use in this context.
    			prepareBeanFactory(beanFactory);
    
    			try {
    				// Allows post-processing of the bean factory in context subclasses.
    				postProcessBeanFactory(beanFactory);
    
    				// Invoke factory processors registered as beans in the context.
    				invokeBeanFactoryPostProcessors(beanFactory);
    
    				// Register bean processors that intercept bean creation.
    				registerBeanPostProcessors(beanFactory);
    
    				// Initialize message source for this context.
    				initMessageSource();
    
    				// Initialize event multicaster for this context.
    				initApplicationEventMulticaster();
    
    				// Initialize other special beans in specific context subclasses.
    				onRefresh();
    
    				// Check for listener beans and register them.
    				registerListeners();
    
    				// Instantiate all remaining (non-lazy-init) singletons.
    				finishBeanFactoryInitialization(beanFactory);
    
    				// Last step: publish corresponding event.
    				finishRefresh();
    			}
    
    			catch (BeansException ex) {
    				if (logger.isWarnEnabled()) {
    					logger.warn("Exception encountered during context initialization - " +
    							"cancelling refresh attempt: " + ex);
    				}
    
    				// Destroy already created singletons to avoid dangling resources.
    				destroyBeans();
    
    				// Reset 'active' flag.
    				cancelRefresh(ex);
    
    				// Propagate exception to caller.
    				throw ex;
    			}
    
    			finally {
    				// Reset common introspection caches in Spring's core, since we
    				// might not ever need metadata for singleton beans anymore...
    				resetCommonCaches();
    			}
    		}
    	}
    

配置结束后,SpringBoot做了一些基本的收尾工作,返回了应用环境上下文。总体而言,SpringBoot的启动,主要创建了配置环境(environment)、事件监听(listener)、应用上下文(applicationContex),并基于以上条件,在容器中开始实例化我们需要的Bean,最终完成SpringBoot的启动,这里面有一个核心就是自动化装配。

自动装配

在整个SpringBoot启动过程中,多出都调用了SpringBoot的自动装配模块

图片

该配置模块主要使用到了SpringFactoriesLoader,即Spring工厂加载器,该对象提供了loadFactoryNames方法,入参为factoryClass和classLoader,即需要传入上图中的工厂类名称对应的类加载器,方法会根据指定的classLoader,加载该类加载器搜索路径下的指定文件,即spring.factories文件,传入的工厂类为接口,而文件中对应的类则是接口的实现类,或最终作为实现类,所以文件中一般为下入这种一对多的类型集合,或者到这些实现类的类名后,loadFactoryNames方法返回类名集合,方法调用方得到这些集合后,再通过反射获取这些类的类对象、构造方法,最终生成实例。

图片

自动配置流程如下:

image-20210624211216303

mybatis-spring-boot-starterspring-boot-starter-web等组件的META-INF文件下均含有spring.factories文件,自动配置模块中,SpringFactoriesLoader收集到文件中的类全名并返回一个类全名的数组,返回的类全名通过反射被实例化,就形成了具体的工厂实例,工厂实例来生成组件具体需要的bean。

之前我们提到了EnableAutoConfiguration注解,其类图如下:

图片

可以发现其最终实现了ImportSelector(选择器)和BeanClassLoaderAware(bean类加载器中间件),重点关注一下AutoConfigurationImportSelectorselectImports方法。

图片

该方法在springboot启动流程——bean实例化前被执行,返回要实例化的类信息列表。我们知道,如果获取到类信息,spring自然可以通过类加载器将类加载到jvm中,现在我们已经通过spring-boot的starter依赖方式依赖了我们需要的组件,那么这些组建的类信息在select方法中也是可以被获取到的,不要急我们继续向下分析。

图片

该方法中的getCandidateConfigurations方法,通过方法注释了解到,其返回一个自动配置类的类名列表,方法调用了loadFactoryNames方法,查看该方法

图片

在上面的代码可以看到自动配置器会根据传入的factoryClass.getName()到项目系统路径下所有的spring.factories文件中找到相应的key,从而加载里面的类。我们就选取这个mybatis-spring-boot-autoconfigure下的spring.factories文件

图片

进入org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration中,主要看一下类头:

图片

发现Spring的@Configuration,俨然是一个通过注解标注的springBean,继续向下看

  • @ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class}):当存在SqlSessionFactory.class, SqlSessionFactoryBean.class这两个类时才解析MybatisAutoConfiguration配置类,否则不解析这一个配置类,make sence,我们需要mybatis为我们返回会话对象,就必须有会话工厂相关类
  • @CondtionalOnBean(DataSource.class):只有处理已经被声明为bean的dataSource
  • @ConditionalOnMissingBean(MapperFactoryBean.class)这个注解的意思是如果容器中不存在name指定的bean则创建bean注入,否则不执行(该类源码较长,篇幅限制不全粘贴)

以上配置可以保证sqlSessionFactory、sqlSessionTemplate、dataSource等mybatis所需的组件均可被自动配置,@Configuration注解已经提供了Spring的上下文环境,所以以上组件的配置方式与Spring启动时通过mybatis.xml文件进行配置起到一个效果。

通过分析我们可以发现,只要一个基于SpringBoot项目的类路径下存在SqlSessionFactory.class, SqlSessionFactoryBean.class,并且容器中已经注册了dataSourceBean,就可以触发自动化配置,意思说我们只要在maven的项目中加入了mybatis所需要的若干依赖,就可以触发自动配置,但引入mybatis原生依赖的话,每集成一个功能都要去修改其自动化配置类,那就得不到开箱即用的效果了。

所以Spring-boot为我们提供了统一的starter可以直接配置好相关的类,触发自动配置所需的依赖(mybatis)如下:

图片

这里是截取的mybatis-spring-boot-starter的源码中pom.xml文件中所有依赖:

图片

因为maven依赖的传递性,我们只要依赖starter就可以依赖到所有需要自动配置的类,实现开箱即用的功能。也体现出Springboot简化了Spring框架带来的大量XML配置以及复杂的依赖管理,让开发人员可以更加关注业务逻辑的开发。

SpringBoot打包

打包过程

在Spring Boot应用中,我们可以约定不同的标识来定义不同的环境。例如 dev 表示开发环境、test表示测试环境,对应的配置文件为application-dev.yamlapplication-test.yaml。我们通过声明spring.profiles.active来激活对应的环境配置,例如激活dev环境时spring.profiles.active=dev。完整的启动命令为:

java -Djava.security.egd=file:/dev/./urandom  -Dspring.profiles.active=dev -jar spring-boot-app.jar 

根据上面的命令编写一个能够适应多环境的Dockerfile

# 引入 openjdk 镜像
FROM adoptopenjdk/openjdk8
# 声明作者
LABEL AUTHOR=felord OG=felord.cn
# 挂载几个有用的文件夹 比如日志
VOLUME ["/tmp","/logs"]
# 声明一个环境参数用来动态启用配置文件 默认dev
ENV ACTIVE=dev
# 暴露端口
EXPOSE 8080
# 复制并修改应用打包后的jar文件名称
ADD /target/flyway-spring-boot-1.0.0.jar app.jar
# 容器启动时第一个运行的命令 用来启动应用
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-Dspring.profiles.active=${ACTIVE}","-jar","app.jar"]

这样打包的Docker镜像就可以通过docker run添加额外的--env ACTIVE=test 来动态的改变环境。单纯的编写Dockerfile不方便我们DevOps。

图片

我们需要能够自动地构建、推送到仓库、拉取镜像、运行一系列流水线操作。好在市面上有很多工具来帮助我们实现这一过程。

构建工具

spring-boot-maven-plugin

这个是Spring Boot官方的插件,在2.x的某个版本提供了Docker镜像构建能力。

<project>
 <build>
  <plugins>
   <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
     <image>
     <name>docker.repo.com/library/${project.artifactId}:${project.version}</name>
      <publish>true</publish>
     </image>
     <docker>
      <publishRegistry>
       <username>user</username>
       <password>secret</password>
       <url>https://docker.repo.com/v1/</url>
       <email>[email protected]</email>
      </publishRegistry>
     </docker>
    </configuration>
   </plugin>
  </plugins>
 </build>
</project>

配置好Docker私仓后就可以通过mvn clean spring-boot:build-image 进行构建镜像了,这种方式好处就是无额外依赖,缺点就是需要从github下载构建元件,网络如果不好就容易失败。

Spotify Maven Plugin

Spotify Maven 插件是一个目前比较普遍的选择。它要求应用程序开发人员编写Dockerfile,并把Dockerfile放在项目src/main/docker目录下。然后你就可以通过引入:

 <plugin>
     <groupId>com.spotify</groupId>
     <artifactId>dockerfile-maven-plugin</artifactId>
     <version>1.4.8</version>
     <configuration>
         <repository>repo.com/${project.artifactId}</repository>
     </configuration>
</plugin>

这个插件提供了mvn dockerfile:buildmvn dockerfile:tagmvn dockerfile:push三个命令分别用来构建、打标签、发布到远端私有仓库,非常简单。

这个是一个非常容易上手的插件,唯一的要求就是需要会编写Dockerfile,对定制化要求高的可以使用这个。

Jib Maven Plugin

它是谷歌开源的OCI镜像打包工具,可以用来打包Docker镜像,大部分情况下已经满足需要。但是如果你要定制化的话还是不容易的,需要阅读官方给的文档。最开始的Dockerfile如果使用JIb的话需要这样配置:

<plugin>
    <groupId>com.google.cloud.tools</groupId>
    <artifactId>jib-maven-plugin</artifactId>
    <version>3.0.0</version>
    <configuration>
        <from>
            <image>adoptopenjdk/openjdk8</image>
        </from>
        <to>
            <image>docker.repo.com/library/${project.artifactId}</image>
            <auth>
                <username>felord</username>
                <password>xxxxxx</password>
            </auth>
            <tags>
                <tag>${project.version}</tag>
            </tags>
        </to>
        <extraDirectories>
            <paths>
                <path>
                    <from>target/${project.artifactId}-${project.version}.jar</from>
                    <includes>*.jar</includes>
                    <into>/app.jar</into>
                </path>
            </paths>
        </extraDirectories>
        <containerizingMode>packaged</containerizingMode>
        <container>
            <volumes>/tmp,/logs</volumes>
            <ports>
                <port>8080</port>
            </ports>
            <environment>
                <active>dev</active>
            </environment>
            <entrypoint>
                java,-Djava.security.egd=file:/dev/./urandom,-Dspring.profiles.active=${active},-jar,/app.jar
            </entrypoint>
            <creationTime>USE_CURRENT_TIMESTAMP</creationTime>
        </container>
    </configuration>
</plugin>

优点是不需要本地Docker环境,而且支持分层构建、镜像瘦身,上手容易;缺点是定制化比较困难。

SpringBoot项目docker打包体积优化

修改之前

最开始使用的dockerfile:

FROM java:8

# 环境变量
ENV WORK_PATH /home/project/cmp
ENV APP_NAME @project.build.finalName@[email protected]@
ENV APP_VERSION @project.version@

EXPOSE 8080

#VOLUME
VOLUME ["/home/project", "/tmp/data"]

#COPY
COPY $APP_NAME $WORK_PATH/

# WORKDIR
WORKDIR $WORK_PATH

RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' >/etc/timezone

COPY SIMSUN.TTC  /usr/share/fonts/SIMSUN.TTC
RUN cd  /usr/share/fonts
RUN fc-cache -fsv

# ENTRYPOINT
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom"]

CMD ["-jar", "-Xmx512m", "@project.build.finalName@[email protected]@"]

由于这里直接使用java8的镜像,本身就有600多M,加上业务模块本身的大小,大约有了800多M,这样每次打包构建,非常的耗时,也很占用磁盘空间。

image-20210624210110566

使用Alpine镜像

Alpine Linux操作系统是一个面向安全的轻型 Linux 发行版,Alpine Docker 镜像也继承了Alpine Linux发行版的这些优势,相比于其他Docker镜像,它的容量非常小,并且拥有自己的包管理机制,可以使用apk 包管理器替换 apt 工具,例如:

$ apk add --no-cache <package>

这里我们使用Alpine提供的docker镜像:

image-20210624210052799

因此这里我们使用Alpine镜像,并且将RUN的指令合并在一起,减少构建的层数,修改之后的dockerfile,:

FROM openjdk:8-jdk-alpine

# 环境变量
ENV WORK_PATH /home/project/cmp
# 这里的都是maven内置的变量
ENV APP_NAME @project.build.finalName@[email protected]@

COPY $APP_NAME $WORK_PATH/

# WORKDIR
WORKDIR $WORK_PATH
# 根据实际需求添加字体
COPY simsun.ttc  /usr/share/fonts/simsun.ttc
# 为了解决docker容器内的时间和宿主机时间不一致的问题,并且安装了fontconfig,方便安装字体
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone && apk add --update ttf-dejavu fontconfig && rm -rf /var/cache/apk/*

# ENTRYPOINT
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom"]

CMD ["-jar", "-Xmx512m", "@project.build.finalName@[email protected]@"]

分层构建镜像

使用了Alpine镜像之后,项目的体积有了比较明显的改善,但是每次打包的时候还是会全量构建项目中的所有内容,构建的时间还是比较长,这里我们使用分层的机制来进行打包,这里要注意的是,使用的SpringBoot的版本必须要大于2.3.X。

FROM openjdk:8-jdk-alpine as builder
ENV APP_NAME @project.build.finalName@[email protected]@
WORKDIR application
# 注意这里对应的是编译后的dockerfile的目录,如果对应不上,可能会提示找不到文件
COPY $APP_NAME application.jar
# 指定构建Jar的模式,并从Jar包中提取构建镜像所需的内容
RUN java -Djarmode=layertools -jar application.jar extract

FROM openjdk:8-jdk-alpine
WORKDIR application
# 拷贝字体,这里安装宋体字体,alpine镜像会自动检测/usr/share/fonts是否含有字体
COPY simsun.ttc  /usr/share/fonts/simsun.ttc
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/application/ ./
ENV TZ="Asia/Shanghai"
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 设置虚拟机参数
ENV JVM_OPTS="-XX:MaxRAMPercentage=80.0"
ENV JAVA_OPTS=""
ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS org.springframework.boot.loader.JarLauncher"]

配置pom.xml文件:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <!--引用本地Jar包-->
    <configuration>
        <includeSystemScope>true</includeSystemScope>
        <layers>
            <enabled>
                true
            </enabled>
        </layers>
    </configuration>
</plugin>

Maven中内置变量

  • ${basedir} 项目根目录
  • ${project.build.directory} 构建目录,缺省为target
  • ${project.build.outputDirectory} 构建过程输出目录,缺省为target/classes
  • ${project.build.finalName} 产出物名称,缺省为{project.artifactId}-${project.version}
  • ${project.packaging} 打包类型,缺省为jar
  • ${project.xxx} 当前pom文件的任意节点的内容

可能存在的问题

  1. 如果本身项目SpringBoot版本较低,不建议升级,推荐通过使用Alpine镜像以及优化dockerfile写法减少镜像体积,特别的,如果是使用的Spring Cloud,升级会出现组件版本不兼容的情况,可能需要升级诸多依赖的版本,并且需要投入精力进行测试验证。

  2. 如果项目不需要字体,可以跳过:

    RUN apk add --update ttf-dejavu fontconfig && rm -rf /var/cache/apk/*
    
  3. 如果项目中只需要宋体,可以只复制字体文件到镜像内,而不需要安装fontconfig,alpine镜像会自动检测/usr/share/fonts是否含有字体:

    COPY simsun.ttc  /usr/share/fonts/simsun.ttc
    
  4. 如果项目中既需要宋体又需要其他字体(例如图片验证码),这是时候,拷贝宋体字体文件和安装fontconfig都需要:

    COPY simsun.ttc  /usr/share/fonts/simsun.ttc
    RUN apk add --update ttf-dejavu fontconfig && rm -rf /var/cache/apk/*
    
  5. 如果镜像打包的体积还是过大,可以使用docker history image_name --no-trunc=true命令来查看构建的详情。

附完整的pom文件:

  <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <includes>
                    <include>application-${profiles.active}.yml</include>
                    <include>bootstrap.yml</include>
                    <include>application.yml</include>
                </includes>
            </resource>
            <!--这里实我们的dockerfile文件所在的目录-->
            <resource>
                <directory>src/main/docker</directory>
                <filtering>true</filtering>
                <includes>
                    <include>**/Dockerfile</include>
                </includes>
                <targetPath>../docker</targetPath>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <includes>
                    <include>static/**</include>
                    <include>**/*.jar</include>
                    <include>bootstrap.yml</include>
                    <include>*.yml</include>
                    <include>*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                    <include>**/*.json</include>
                    <include>**/*.ftl</include>
                </includes>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <!--引用本地Jar包-->
                <configuration>
                    <includeSystemScope>true</includeSystemScope>
                    <layers>
                        <enabled>
                            true
                        </enabled>
                    </layers>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <!-- 打包跳过测试 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
            <!-- 避免font文件的二进制文件格式压缩破坏 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <configuration>
                    <nonFilteredFileExtensions>
                        <nonFilteredFileExtension>woff</nonFilteredFileExtension>
                        <nonFilteredFileExtension>woff2</nonFilteredFileExtension>
                        <nonFilteredFileExtension>eot</nonFilteredFileExtension>
                        <nonFilteredFileExtension>ttf</nonFilteredFileExtension>
                        <nonFilteredFileExtension>svg</nonFilteredFileExtension>
                    </nonFilteredFileExtensions>
                    <delimiters>
                        <delimiter>@</delimiter>
                    </delimiters>
                    <useDefaultDelimiters>false</useDefaultDelimiters>
                </configuration>
            </plugin>
            <plugin>
                <groupId>com.spotify</groupId>
                <artifactId>docker-maven-plugin</artifactId>
                <version>1.1.0</version>
                <executions>
                    <execution>
                        <id>build-image</id>
                        <phase>package</phase>
                        <goals>
                            <goal>build</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>push-image</id>
                        <phase>package</phase>
                        <goals>
                            <goal>push</goal>
                        </goals>
                        <configuration>
                            <imageName>
                                ${docker.repostory}/${docker.registry.name}/${project.artifactId}:${profiles.active}-${project.version}
                            </imageName>
                        </configuration>
                    </execution>
                </executions>
                <configuration>
                    <serverId>harbor</serverId>
                    <dockerDirectory>${project.build.directory}/docker</dockerDirectory>
                    <imageName>
                        ${docker.repostory}/${docker.registry.name}/${project.artifactId}
                    </imageName>
                    <imageTags>
                        <!--docker的tag为项目版本号、latest-->
                        <imageTag>${profiles.active}-${project.version}</imageTag>
                    </imageTags>
                    <resources>
                        <rescource><!-- 将打包文件放入dockerDirectory指定的位置 -->
                            <targetPath>/</targetPath>
                            <directory>${project.build.directory}</directory>
                            <include>${project.artifactId}-${project.version}.jar</include>
                        </rescource>
                    </resources>
                </configuration>
            </plugin>
        </plugins>
    </build>