最近在看 spring boot 的东西,觉得很方便,很好用。对于一个简单的REST服务,都不要自己部署Tomcat了,直接在 IDE 里 run 一个包含 main 函数的主类就可以了。
但是,转念一想,到了真正需要部署应用的时候,不可能通过 IDE 去部署啊。那有没有办法将 spring boot 的项目打包成一个可执行的 jar 包,然后通过 java -jar 命令去启动相应的服务呢?
很明显,是有的。下面,我把我自己的实践过程及遇到的问题,一 一说明一下。
首先,把项目的 POM 配置文件的雏形放上来
PS: (代码我就不放上来了,spring boot 官网上有。我在本文的最下面会给出链接。)
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<artifactId>spring-bootartifactId>
<version>0.1-SNAPSHOTversion>
<name>spring-bootname>
<packaging>jarpackaging>
<parent>
<groupId>org.rainbowgroupId>
<artifactId>springartifactId>
<version>0.1-SNAPSHOTversion>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
dependencies>
project>
这里,我没有使用 spring boot 默认的 parent 工程,而是使用自己项目的 parent 工程,具体请参见 我的另一篇Blog
只要有了上面的这段 pom 配置,你就可以在 IDE 里启动你的应用了。
下面,说明一下,将项目打成 可执行Jar包 所需要的配置。
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<mainClass>org.rainbow.spring.boot.ApplicationmainClass>
configuration>
<executions>
<execution>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
很简单吧?我们只需要添加一个 spring-boot-maven-plugin 插件就可以解决问题了。
加了这个插件之后,我们可以通过下面的方式来将项目打成可执行jar包。
mvn clean package
请注意,从我们上面的配置来看,虽然我们没有明确写出将插件的 repackage 这个 goal 绑定到了 maven 的哪个 life cycle 上,但是插件本身默认将它绑定到了 maven 的 package 上。
所以,只有当我们执行的 maven 命令会触发 package 这个life cycle 时,上面的插件才会被触发。
另外,我们可以在上面的 pom 配置中,去掉下面这段配置:
<executions>
<execution>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
然后,我们可以通过手动来执行插件的 repackage 这个 goal。
mvn clean package spring-boot:repackage
其中,spring-boot 是固定的前缀。
从以上的描述来看,我们一共有两种方式来启用这个插件,任选其一哦。
执行了这个插件之后,你会在 target 目录下发现两个Jar包:
其中,第一个是仅仅包含我们项目源码的 Jar包,它是无法运行的。第二个是经由 spring boot maven plugin 重新包装后的Jar包,这个是可以运行的。可以通过下面的命令来试下:
java -jar xxxxx.jar
然后,你应该会看到下面类似的启动信息:
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/C:/Users/XXXXXXX/Desktop/spring-boot-0.1-SNAPSHOT.jar!/BOOT-INF/classes!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/C:/Users/XXXXXXX/Desktop/spring-boot-0.1-SNAPSHOT.jar!/BOOT-INF/lib/logback-classic-1.1.9.jar!/org/slf4j/impl/StaicLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot ::
2017-02-14 10:47:29.488 INFO 11860 --- [ main] org.rainbow.spring.boot.Application : Starting Application on XXXXXXX-PC with PID 11860 (C:\Users\XXXXXXX\Desktop\spring-boot-0.1-SNAPSHOT.jar started by XXXXXXX in C:\Users\XXXXXXX\Desktop)
2017-02-14 10:47:29.494 INFO 11860 --- [ main] org.rainbow.spring.boot.Application : No active profile set, falling back to default profiles: default
2017-02-14 10:47:29.607 INFO 11860 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@14514713: startup date [Tue Feb 14 10:47:29 CST 2017]; root of context hierarchy
2017-02-14 10:47:31.731 INFO 11860 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration' of type [class org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2017-02-14 10:47:31.849 INFO 11860 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'validator' of type [class org.springframework.validation.beanvalidation.LocalValidatorFactoryBean] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2017-02-14 10:47:32.673 INFO 11860 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2017-02-14 10:47:32.699 INFO 11860 --- [ main] o.apache.catalina.core.StandardService : Starting service Tomcat
2017-02-14 10:47:32.701 INFO 11860 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.11
2017-02-14 10:47:32.848 INFO 11860 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2017-02-14 10:47:32.848 INFO 11860 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 3244 ms
下面说明一下几个注意事项。
第一个是有关于 main 函数的。
我们知道,一个jar包要能够运行,那么必须在其根目录下的 META-INF 目录下的 MANIFEST.MF 文件中声明 Main-Class 这个属性。
对于 spring boot 的项目来说,这一点也是必须的。那么,我们应该如何来声明我们项目中的 main 函数所在的 class 呢?
方法有二。
1. 不作任何声明
即,我们不添加任何的其他声明。这样一来,spring boot maven plugin 在打包时,会自动扫描整个项目的源码,并将扫描到的第一个包含 main 函数的 class 作为Jar包的 Main-Class。
2. 在 plugin 的配置中增加一个配置
<configuration>
<mainClass>org.rainbow.spring.boot.ApplicationmainClass>
configuration>
这样的话,Application 这个class将作为Jar包的 Main-Class。
但是,你会发现,在最终打好的Jar中, Application 这个class,它并不是作为 Main-Class 这个属性的值,而是作为 Start-Class 属性的值。
这个是由 spring boot 自己进行处理的,我们无须过多关注。
(其实,在打好的Jar中,我们去看一下其中的 MANIFEST.MF文件,可以发现,它的 Main-Class 指定的值是 org.springframework.boot.loader.JarLauncher, spring boot 会通过这个类去间接的执行 Start-Class 指定的类,即我们的主类)
第二个问题是关于项目可能会报找不到 spring 的某些 XSD 文件的。
PS:以下篇幅来自 Spring如何加载XSD文件
说明:
这个问题,我在自己的项目中没有遇到,但是在网上看到这个问题的描述及处理。为了防止项目以后遇到问题,我就在此一起列出来。
Start.
问题现象是:
org.xml.sax.SAXParseException: schema_reference.4: Failed to read schema document 'http://www.springframework.org/schema/beans/spring-beans-3.0.xsd', because 1) could not find the document; 2) the document could not be read; 3) the root element of the document is not .
很显然,spring xml配置文件中指定的xsd文件读取不到了,原因多是因为断网或spring的官网暂时无法连接导致的。 你可以通过在浏览器输入xsd文件的URL,如:http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
进行确认。
为什么会这样呢?要想直正弄明白这一问题还需要从spring的XSD文件加载机制谈起。
你必须知道一点:spring在加载xsd文件时总是先试图在本地查找xsd文件(spring的jar包中已经包含了所有版本的xsd文件),如果没有找到,才会转向去URL指定的路径下载。
这是非常合理的做法,并不像看上去的那样,每次都是从站点下载的。
事实上,假如你的所有配置是正确定的,你的工程完全可以在断网的情况下启动而不会报上面的错误。Spring加载xsd文件的类是PluggableSchemaResolver,你可以查看一下它的源码来验证上述说法。
另外,你可以在log4j.xml文件中加入:
<logger name="org.springframework.beans.factory.xml">
<level value="all" />
logger>
通过日志了解spring是何加载xsd文件的。
接下来,问题就是为什么spring在本地没有找到需要的文件,不得不转向网站下载。关于这个问题,其实也非常简单:
在很多spring的jar包里,在META-INF目录下都有一个spring.schemas,这是一个property文件,其内容类似于下面:
http\://www.springframework.org/schema/beans/spring-beans-2.0.xsd=org/springframework/beans/factory/xml/spring-beans-2.0.xsd
http\://www.springframework.org/schema/beans/spring-beans-2.5.xsd=org/springframework/beans/factory/xml/spring-beans-2.5.xsd
http\://www.springframework.org/schema/beans/spring-beans-3.0.xsd=org/springframework/beans/factory/xml/spring-beans-3.0.xsd
....
实际上,这个文件就是spring关于xsd文件在本地存放路径的映射,spring就是通过这个文件在本地(也就是spring的jar里)查找xsd文件的。
那么,查找不到的原因排除URL输入有误之外,可能就是声明的xsd文件版本在本地不存在。
一般来说,新版本的spring jar包会将过去所有版本(应该是自2.0以后)的xsd打包,并在spring.schemas文件中加入了对应项,出现问题的情况往往是声明使用了一个高版本的xsd文件,如3.0,但依赖的spring的jar包却是2.5之前的版本,由于2.5版本自然不可能包含3.0的xsd文件,此时就会导致spring去站点下载目标xsd文件,如遇断网或是目标站点不可用,上述问题就发生了。
但是,在实现开发中,出现上述错误的几率并不高,最常见的导致这一问题的原因其实与使用了一个名为“assembly”的maven打包插件有关。
很多项目需要将工程连同其所依赖的所有jar包打包成一个jar包,maven的assembly插件就是用来完成这个任务的。但是由于工程往往依赖很多的jar包,而被依赖的jar又会依赖其他的jar包,这样,当工程中依赖到不同的版本的spring时,在使用assembly进行打包时,只能将某一个版本jar包下的spring.schemas文件放入最终打出的jar包里,这就有可能遗漏了一些版本的xsd的本地映射,进而出现了文章开始提到的错误。
如果你的项目是打成单一jar的,你可以通过检查最终生成的jar里的spring.schemas文件来确认是不是这种情况。
而关于这种情况,解决的方法一般是推荐使用另外一种打包插件”shade“,它确实是一款比assembly更加优秀的工具,在对spring.schemas文件处理上,shade能够将所有jar里的spring.schemas文件进行合并,在最终生成的单一jar包里,spring.schemas包含了所有出现过的版本的集合!
以上就是spring加载XSD文件的机制和出现问题的原因分析。实际上,我们应该让我们工程在启动时总是加载本地的xsd文件,而不是每次去站点下载,做到这一点就需要你结合上述提及的种种情况对你的工程进行一番检查。
End.
好了,到此,我们了解了这个问题,并且知道了可以使用哪个插件来避免这个问题。那么,下面我们就说一下上面提及到的 shade 插件如何配置吧。
我先直接将配置发上来吧:
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-shade-pluginartifactId>
<executions>
<execution>
<phase>packagephase>
<goals>
<goal>shadegoal>
goals>
<configuration>
<createDependencyReducedPom>truecreateDependencyReducedPom>
<dependencyReducedPomLocation>${project.build.directory}/dependency-reduced-pom.xmldependencyReducedPomLocation>
<minimizeJar>falseminimizeJar>
<promoteTransitiveDependencies>falsepromoteTransitiveDependencies>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.factoriesresource>
transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.handlersresource>
transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.providesresource>
transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.schemasresource>
transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.toolingresource>
transformer>
transformers>
configuration>
execution>
executions>
plugin>
从上面的配置来看,这个插件也是在 maven 的 package 阶段才会被触发,与上面介绍的 spring boot maven plugin 是一样的。
下面重点说明一下 transformer 这个标签的作用。
上面虽然写了 5 个 transformer,但其实都一样,只不过是处理了5个不同的文件而已:
下面 以 META-INF/spring.factories
为例进行说明。
上面的配置就是将所有被项目依赖的Jar包中的 META-INF/spring.factories 文件合并到一份文件中,这份文件将作为最终的 Jar包 中的 META-INF/spring.factories 这个文件。(名称并没有发生变化)。
其实,这个插件还有一个 ManifestResourceTransformer,我们可以通过这个 transformer 来设定 Jar 的Main-Class 等属性,如下:
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>${app.main.class}Main-Class>
<X-Compile-Source-JDK>${maven.compile.source}X-Compile-Source-JDK>
<X-Compile-Target-JDK>${maven.compile.target}X-Compile-Target-JDK>
manifestEntries>
transformer>
这里列出来的属性,都将被写入到 META-INF/MANIFEST.MF 文件中。
不过,需要注意的一点是:虽然可以通过此 transformer 来设定 Jar包的 Main-Class,但是此处设定的值将会被在spring boot maven plugin 设定的 Main-Class 的值所替代掉。因为 spring boot maven plugin 插件是在 apache maven shade plugin 之后执行的。
第三个问题,是关于项目重复引入依赖包的问题。
2017.3.18 补充:
经过最近的测试,我个人觉得,只需要使用 spring-boot-maven 这个插件就可以了。因为这个插件会将所有依赖的 jar 打到最终的jar里去,并不会发生上面问题二中所说的: xld 中元素变少的情况。
而这第三个问题,就是由于上面使用了 shade 插件导致的。所以,如果你只使用了 spring-boot-maven 的插件的话,问题二 和 问题三 都无视吧。。。
如果细心的话,我们会发现上面有这么一段输出:
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/C:/Users/XXXXXXX/Desktop/spring-boot-0.1-SNAPSHOT.jar!/BOOT-INF/classes!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/C:/Users/XXXXXXX/Desktop/spring-boot-0.1-SNAPSHOT.jar!/BOOT-INF/lib/logback-classic-1.1.9.jar!/org/slf4j/impl/StaicLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]
意思就是说,在 classpath 中发现了两个 SLF4J 的绑定:
这么看来,应该是 StaticLoggerBinder.class 被引入了两次。下面,我们看一下经过 spring boot maven 插件打包好的jar包,在解压之后的文件夹结构是怎么样的。请看:
我们看到,BOOT-INF 目录下的 class 和 lib 目录下,几乎所有的依赖都被分别导入了一份。那这个结构的是怎么来的呢?大概下面这样的:
好了,既然现在知道问题发生在哪里了,那就想办法去掉其中的一个呗?那该如何去掉呢?我经过一些调查与测试之后发现,只能在 shade 插件中增加相关配置来过滤掉 class 目录下的重复的类。原因有以下几点:
下面,我们来看下如何配置 maven shade 的插件来避免重复引用依赖的问题:
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-shade-pluginartifactId>
<executions>
<execution>
<phase>packagephase>
<goals>
<goal>shadegoal>
goals>
<configuration>
<createDependencyReducedPom>truecreateDependencyReducedPom>
<dependencyReducedPomLocation>${project.build.directory}/dependency-reduced-pom.xmldependencyReducedPomLocation>
<minimizeJar>falseminimizeJar>
<promoteTransitiveDependencies>falsepromoteTransitiveDependencies>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.factoriesresource>
transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.handlersresource>
transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.providesresource>
transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.schemasresource>
transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.toolingresource>
transformer>
transformers>
<filters>
<filter>
<artifact>*:*artifact>
<includes>
<include>*include>
<include>META-INF/**include>
<include>org/rainbow/**include>
includes>
filter>
filters>
configuration>
execution>
executions>
plugin>
重点是最后面的 filter 属性的配置。我这么配置的作用是:
当然,对于某些特殊的jar包,上面的这个规则列表可能还不完善,需要根据实际情况进行修改。
最后,给出项目的完整 POM 配置:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<artifactId>spring-bootartifactId>
<version>0.1-SNAPSHOTversion>
<name>spring-bootname>
<packaging>jarpackaging>
<parent>
<groupId>org.rainbowgroupId>
<artifactId>springartifactId>
<version>0.1-SNAPSHOTversion>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-loaderartifactId>
<version>1.5.1.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<exclusions>
<exclusion>
<artifactId>logback-classicartifactId>
<groupId>ch.qos.logbackgroupId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-coreartifactId>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-slf4j-implartifactId>
<version>2.7version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-shade-pluginartifactId>
<executions>
<execution>
<phase>packagephase>
<goals>
<goal>shadegoal>
goals>
<configuration>
<createDependencyReducedPom>truecreateDependencyReducedPom>
<dependencyReducedPomLocation>${project.build.directory}/dependency-reduced-pom.xmldependencyReducedPomLocation>
<minimizeJar>falseminimizeJar>
<promoteTransitiveDependencies>falsepromoteTransitiveDependencies>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.factoriesresource>
transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.handlersresource>
transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.providesresource>
transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.schemasresource>
transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.toolingresource>
transformer>
transformers>
<filters>
<filter>
<artifact>*:*artifact>
<includes>
<include>*include>
<include>META-INF/**include>
<include>org/rainbow/**include>
includes>
filter>
filters>
configuration>
execution>
executions>
plugin>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<mainClass>org.rainbow.spring.boot.ApplicationmainClass>
configuration>
<executions>
<execution>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
project>
参考文档