最近在看 spring boot 的东西,以为很方便,很好用。对于一个简单的REST服务,都不要本身部署Tomcat了,直接在 IDE 里 run 一个包含 main 函数的主类就能够了。html
可是,转念一想,到了真正须要部署应用的时候,不可能经过 IDE 去部署啊。那有没有办法将 spring boot 的项目打包成一个可执行的 jar 包,而后经过 java -jar 命令去启动相应的服务呢?java
很明显,是有的。下面,我把我本身的实践过程及遇到的问题,一 一说明一下。web
首先,把项目的 POM 配置文件的雏形放上来
PS: (代码我就不放上来了,spring boot 官网上有。我在本文的最下面会给出连接。)spring
<?xml version="1.0" encoding="UTF-8"?> <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.0</modelVersion> <artifactId>spring-boot</artifactId> <version>0.1-SNAPSHOT</version> <name>spring-boot</name> <packaging>jar</packaging> <parent> <groupId>org.rainbow</groupId> <artifactId>spring</artifactId> <version>0.1-SNAPSHOT</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> </project>
这里,我没有使用 spring boot 默认的 parent 工程,而是使用本身项目的 parent 工程,具体请参见 个人另外一篇Blogshell
只要有了上面的这段 pom 配置,你就能够在 IDE 里启动你的应用了。apache
下面,说明一下,将项目打成 可执行Jar包 所须要的配置。浏览器
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>org.rainbow.spring.boot.Application</mainClass> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
很简单吧?咱们只须要添加一个 spring-boot-maven-plugin 插件就能够解决问题了。markdown
加了这个插件以后,咱们能够经过下面的方式来将项目打成可执行jar包。app
mvn clean package
请注意,从咱们上面的配置来看,虽然咱们没有明确写出将插件的 repackage 这个 goal 绑定到了 maven 的哪一个 life cycle 上,可是插件自己默认将它绑定到了 maven 的 package 上。maven
因此,只有当咱们执行的 maven 命令会触发 package 这个life cycle 时,上面的插件才会被触发。
另外,咱们能够在上面的 pom 配置中,去掉下面这段配置:
<executions> <execution> <goals> <goal>repackage</goal> </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.Application</mainClass> </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 <xsd:schema>.
很显然,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.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <createDependencyReducedPom>true</createDependencyReducedPom> <dependencyReducedPomLocation>${project.build.directory}/dependency-reduced-pom.xml</dependencyReducedPomLocation> <minimizeJar>false</minimizeJar> <promoteTransitiveDependencies>false</promoteTransitiveDependencies> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.factories</resource> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.handlers</resource> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.provides</resource> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.schemas</resource> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.tooling</resource> </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.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <createDependencyReducedPom>true</createDependencyReducedPom> <dependencyReducedPomLocation>${project.build.directory}/dependency-reduced-pom.xml</dependencyReducedPomLocation> <minimizeJar>false</minimizeJar> <promoteTransitiveDependencies>false</promoteTransitiveDependencies> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.factories</resource> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.handlers</resource> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.provides</resource> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.schemas</resource> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.tooling</resource> </transformer> </transformers> <!-- use filter to include only the needed files --> <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 配置:
<?xml version="1.0" encoding="UTF-8"?> <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.0</modelVersion> <artifactId>spring-boot</artifactId> <version>0.1-SNAPSHOT</version> <name>spring-boot</name> <packaging>jar</packaging> <parent> <groupId>org.rainbow</groupId> <artifactId>spring</artifactId> <version>0.1-SNAPSHOT</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-loader</artifactId> <version>1.5.1.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>logback-classic</artifactId> <groupId>ch.qos.logback</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.7</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <createDependencyReducedPom>true</createDependencyReducedPom> <dependencyReducedPomLocation>${project.build.directory}/dependency-reduced-pom.xml</dependencyReducedPomLocation> <minimizeJar>false</minimizeJar> <promoteTransitiveDependencies>false</promoteTransitiveDependencies> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.factories</resource> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.handlers</resource> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.provides</resource> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.schemas</resource> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.tooling</resource>