最近在看 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>