![](/img/trans.png)
[英]How can I deploy a spring-boot webapp with angular 2 frontend (angular-cli build) to Tomcat?
[英]Spring-Boot: How can I build a runnable jar with SBT?
如何使用 SBT 构建可运行的 jar? 也许 Spring-Boot 不适合 SBT 构建的应用程序? 对此有什么建议吗?
我尝试使用 sbt-assembly 但是当我尝试运行它时它失败了。 请注意, sbt run
有效
mainClass in assembly := Some("com.xagongroup.xagon.app.XagonETL")
assemblyMergeStrategy in assembly := {
case PathList("META-INF", _ @ _*) => MergeStrategy.discard
case _ => MergeStrategy.first
}
堆栈跟踪
org.springframework.beans.factory.BeanDefinitionStoreException: Failed to process import candidates for configuration class [com.x.app.XETL]; nested exception is java.lang.Ille
galArgumentException: No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.
at org.springframework.context.annotation.ConfigurationClassParser.processDeferredImportSelectors(ConfigurationClassParser.java:556)
at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:185)
at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:308)
at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:228)
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:270)
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:93)
at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:687)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:525)
at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:693)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:360)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:303)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1118)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1107)
at com.x.app.XETL$.main(XETL.scala:21)
at com.x.app.XETL.main(XETL.scala)
Caused by: java.lang.IllegalArgumentException: No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.
at org.springframework.util.Assert.notEmpty(Assert.java:277)
at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.getCandidateConfigurations(AutoConfigurationImportSelector.java:153)
at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.selectImports(AutoConfigurationImportSelector.java:95)
at org.springframework.context.annotation.ConfigurationClassParser.processDeferredImportSelectors(ConfigurationClassParser.java:547)
... 15 common frames omitted
我通过移动到 sbt-native-packager 解决了这个问题
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.2.0")
scriptClasspath := Seq("*")
mainClass in Compile := Some("com.x.app.XETL")
enablePlugins(JavaAppPackaging)
sbt universal:stage
target\universal\stage\bin\x.bat
我已经在没有任何插件的情况下解决了这个问题。 似乎这里唯一的技巧是不要丢弃 build.sbt 中的 META-INF/spring.factories 文件
assemblyMergeStrategy in assembly := {
case PathList("META-INF", "spring.factories") => MergeStrategy.filterDistinctLines
case PathList("META-INF", _*) => MergeStrategy.discard
case _ => MergeStrategy.first
}
为了节省别人的时间:
Spring 引导使用特殊的 jar 格式,它具有不同的内部结构,更改为清单并且可以在内部包含 jars,它应该按原样存储在结果档案中(它被称为ZipEntry.STORED
与ZipEntry.DEFLATED
对比)。
我知道的插件(sbt-assembly 或 sbt-native-packager)都不能直接使用或通过配置来完成,所以我只是为此编写了一个自定义任务,它可以 package 我相对简单的项目(没有模块和子项目)。
...usual build folders...
project/
SpringBootJar.scala
build.sbt
请注意在依赖项和springBootJar任务中添加了spring-boot-loader 。
import SpringBootJar.Keys.springBootJar
import SpringBootJar.springBootJarTask
val springBootVersion = "2.7.6"
lazy val myProject = (project in file("."))
.settings(
name := "my-project",
scalaVersion := "3.2.1",
...
libraryDependencies ++= Seq(
"org.springframework.boot" % "spring-boot-starter-parent" % springBootVersion pomOnly(),
"org.springframework.boot" % "spring-boot-starter-jdbc" % springBootVersion,
.... other depenencies ....
"org.springframework.boot" % "spring-boot-loader" % springBootVersion % "compile"
),
springBootJar := springBootJarTask.value
)
import sbt.{Keys, _}
import Keys._
import sbt.io.Using.fileOutputStream
import java.io.BufferedOutputStream
import java.nio.file.Files
import java.util.jar.{Attributes, JarEntry, JarFile, JarOutputStream, Manifest}
import java.util.zip.{CRC32, ZipEntry, ZipOutputStream}
import scala.collection.immutable.TreeSet
object SpringBootJar {
object Keys {
val springBootJar = taskKey[Unit]("Create spring boot fat jar")
}
val springBootJarTask = Def.task {
val log = streams.value.log
// check if spring-boot-loader is already a dependency
libraryDependencies.value.find(_.name == "spring-boot-loader") match {
case Some(_) => log.info("spring-boot-loader: present")
case None => sys.error(s"Consider added spring-boot-loader to libraryDependencies")
}
// name of resulting artifact
val destArtifact = target.value / s"${name.value}-${version.value}-sb.jar"
val classpath = (Runtime / fullClasspath).value
val externalClasspath = (Runtime / externalDependencyClasspath).value
val classDir = (Compile / classDirectory).value
val resourceDir = (Compile / resourceDirectory).value
// create directory structure
val rootDir = IO.createUniqueDirectory(target.value)
val springBootLoaderDir = rootDir / "org" / "springframework" / "boot" / "loader"
val appClassesDir = rootDir / "BOOT-INF" / "classes"
val libsDir = rootDir / "BOOT-INF" / "lib"
// get the location of the spring-boot-loader library on the file system
log.info("Seeking for spring-boot-loader jar")
val springBootLoaderFile = classpath.find(_.data.getName.startsWith("spring-boot-loader")) match {
case Some(af) => af.data
case None => sys.error("Couldn't find spring-boot-loader, interrupting")
}
// create dir structure
log.info("Creating directory structure")
IO.createDirectories(Seq(springBootLoaderDir, appClassesDir, libsDir))
// creating manifest
log.info("Creating MANIFEST")
val manifest = new Manifest()
val mainAttributes = manifest.getMainAttributes
mainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0")
mainAttributes.put(Attributes.Name.SPECIFICATION_TITLE, name.value)
mainAttributes.put(Attributes.Name.SPECIFICATION_VERSION, version.value)
mainAttributes.put(Attributes.Name.SPECIFICATION_VENDOR, organization.value)
mainAttributes.put(Attributes.Name.IMPLEMENTATION_TITLE, name.value)
mainAttributes.put(Attributes.Name.IMPLEMENTATION_VERSION, version.value)
mainAttributes.put(Attributes.Name.IMPLEMENTATION_VENDOR, organization.value)
mainAttributes.put(new Attributes.Name("Start-Class"), (Compile / mainClass).value.getOrElse(""))
mainAttributes.put(Attributes.Name.MAIN_CLASS, "org.springframework.boot.loader.JarLauncher")
// copy all from org/springframework/boot/loader/ to the same path
locally {
log.info("Copying org/springframework/boot/loader/ classes")
val jar = new JarFile(springBootLoaderFile)
val entries = jar.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
if (!entry.isDirectory && entry.getName.startsWith("org/springframework/boot/loader/")) {
val targetPath = entry.getName.stripPrefix("org/springframework/boot/loader/")
val targetFile = new File(springBootLoaderDir, targetPath)
targetFile.getParentFile.mkdirs()
Files.copy(jar.getInputStream(entry), targetFile.toPath)
}
}
jar.close()
}
// copying all own app files
log.info("Copying app classes")
IO.copyDirectory(classDir, appClassesDir)
// copying app resources
log.info("Copying app resources")
IO.copyDirectory(resourceDir, appClassesDir)
// copying all external libs
log.info("Copying libs")
IO.copy(externalClasspath.filterNot(_.data.base.startsWith("spring-boot-loader")).map {
f => f.data -> libsDir / f.data.getName
})
// creating jar
log.info("Creating jar")
jar(
(rootDir ** "*").get.map { file => (file, file.relativeTo(rootDir).get.getPath) },
destArtifact,
manifest,
Some(System.currentTimeMillis())
)
log.info("Cleaning up")
IO.delete(rootDir)
log.info(s"Created: ${destArtifact.getAbsolutePath}")
}
private def jar(sources: Traversable[(File, String)], outputJar: File, manifest: Manifest, time: Option[Long]): Unit = {
val localTime = time.map(t => t - java.util.TimeZone.getDefault.getOffset(t))
if (outputJar.isDirectory)
sys.error("Specified output file " + outputJar + " is a directory")
else {
val outputDir = outputJar.getParentFile match {
case null => new File(".")
case parentFile => parentFile
}
IO.createDirectory(outputDir)
val emptyCRC = new CRC32().getValue // The CRC32 for an empty value, needed to store directories in zip files
withJarOutput(outputJar, manifest, localTime) { output =>
writeZip(sources.toSeq, output, localTime) { (file, name) =>
val entry = new JarEntry(name)
if (file == null || file.isDirectory) {
entry.setSize(0)
entry.setMethod(ZipEntry.STORED)
entry.setCrc(emptyCRC)
} else if (file.ext == "jar") {
val jarBytes = Files.readAllBytes(file.toPath)
entry.setMethod(ZipEntry.STORED)
entry.setSize(jarBytes.length)
entry.setCompressedSize(jarBytes.length)
entry.setCrc({
val crc = new CRC32()
crc.update(jarBytes)
crc.getValue
})
}
entry
}
}
}
}
private def withJarOutput(file: File, manifest: Manifest, time: Option[Long])(f: ZipOutputStream => Unit) = {
fileOutputStream(false)(file) { fileOut =>
val zipOut = {
val os = new JarOutputStream(fileOut)
val e = new ZipEntry(JarFile.MANIFEST_NAME)
e.setTime(time.getOrElse(System.currentTimeMillis))
os.putNextEntry(e)
manifest.write(new BufferedOutputStream(os))
os.closeEntry()
os
}
try
f(zipOut)
finally
zipOut.close()
}
}
private def writeZip(sources: Seq[(File, String)], output: ZipOutputStream, time: Option[Long])(
createEntry: (File, String) => ZipEntry
): Unit = {
val files = sources.flatMap {
case (file, name) => if (file.isFile) (file, normalizeToSlash(name)) :: Nil else Nil
}.sortBy {
case (_, name) => name
}
val now = System.currentTimeMillis
def addDirectoryEntry(file: File, name: String): Unit = {
output.putNextEntry {
val e = createEntry(file, name)
e.setTime(time.getOrElse(now))
e
}
output.closeEntry()
}
def addFileEntry(file: File, name: String): Unit = {
output.putNextEntry {
val e = createEntry(file, name)
e.setTime(time.getOrElse(IO.getModifiedTimeOrZero(file)))
e
}
IO.transfer(file, output)
output.closeEntry()
}
// Calculate directories and add them to the generated Zip
allDirectoryPaths(files).foreach(addDirectoryEntry(null, _))
// Add all files to the generated Zip
files foreach { case (file, name) => addFileEntry(file, name) }
}
private def normalizeToSlash(name: String): String = {
val sep = java.io.File.separatorChar
if (sep == '/') name else name.replace(sep, '/')
}
private def relativeComponents(path: String): List[String] =
path.split("/").toList.dropRight(1)
private def directories(path: List[String]): List[String] =
path.foldLeft(List(""))((e, l) => (e.head + l + "/") :: e)
private def directoryPaths(path: String): List[String] =
directories(relativeComponents(path)).filter(_.length > 1)
private def allDirectoryPaths(files: Iterable[(File, String)]) =
TreeSet[String]() ++ (files flatMap { case (_, name) => directoryPaths(name) })
}
sbt springBootJar
或springBootJar
控制台中的 springBootJar。
这是在项目中用 scala 3.2.1 和 sbt 1.7.1 测试的。 我没有时间把它做成一个插件,我不介意是否有人重用我的发现来制作更通用和随时可用的解决方案。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.