mynote
mynote copied to clipboard
重回sbt之路
重回sbt之路
背景
当年考虑团队统一,我们在我们的两个纯scala项目中选择使用了maven而不是sbt作为默认的构建工具,现在主要存在以下几个问题
- maven写play项目的话,涉及到view的修改都需要重新打包,这点使得开发效率非常的低
- 正常打包的情况下,使用maven也比用sbt慢很多,导致每天花了大量时间在构建上
- 最近我们测试环境docker化了,但common的解决方案只是对使用tomcat的war项目,对于我们使用play和akka http的构建用户,还是需要自力更生的
当初弃坑sbt主要还考虑到java用户通常在不同env上使用不同的profile,其他地方都是同名调用,比如可能在test这个profile下有一个url.properties,在prod上有一个同样的文件,而sbt通过Config去隔离env用起来还是很难用的,另外一个问题是当初没找到类似maven中copy dependency这样的插件,而我们spark项目中使用assembly jar的话,其实很不好排查jar的冲突问题,所以我们的做法都是平铺在一个lib目录中,如果能解决这两个问题,及格分就达到了,另外docker的支持首先想到的就是sbt调用shell,然后docker build,后来想了下,我们大sbt肯定会有类似的打包工具的插件,找了下也找到了,所以几个问题都基本解决了,下面详细说下我们对于这几个问题的解决思路
解决profile的问题
在scala用户中,可能的一个做法就是我提供dev.conf
,test.conf
,prod.conf
,并且在打包的时候全变成application.conf
,目录结构看起来像这样
.
├── README.md
├── build.sbt
├── project
│ ├── BuildEnv.scala
│ ├── build.properties
│ ├── plugins.sbt
│ └── project
│ └── target
│ └── config-classes
├── src
│ └── main
│ ├── resources
│ │ ├── dev.conf
│ │ ├── prod.conf
│ │ ├── stage.conf
│ │ └── test.conf
│ └── scala
│ └── Main.scala
└── target
project里面会放一个BuildEnv的插件
object BuildEnvPlugin extends AutoPlugin {
...
override def projectSettings: Seq[Setting[_]] = Seq(
buildEnv := {
sys.props.get("env")
.orElse(sys.env.get("BUILD_ENV"))
.flatMap {
case "prod" => Some(BuildEnv.Production)
case "stage" => Some(BuildEnv.Stage)
case "test" => Some(BuildEnv.Test)
case "dev" => Some(BuildEnv.Developement)
case unkown => None
}
.getOrElse(BuildEnv.Developement)
}
...
此时的 build.sbt会写成
libraryDependencies += "com.typesafe" % "config" % "1.3.0"
enablePlugins(JavaAppPackaging)
mappings in Universal += {
// logic like this belongs into an AutoPlugin
val confFile = buildEnv.value match {
case BuildEnv.Developement => "dev.conf"
case BuildEnv.Test => "test.conf"
case BuildEnv.Stage => "stage.conf"
case BuildEnv.Production => "prod.conf"
}
((resourceDirectory in Compile).value / confFile) -> "conf/application.conf"
}
这样来解决多环境问题,不过公司项目底层的一些库基于spring那一套弄的,所以我们只能解决历史遗留问题
我们的项目结构看起来像这样
├── main
│ ├── resources
│ │ ├── application-context.xml
│ │ └── log4j.properties
│ └── scala
│ ├── com
│ └── org
├── profile
│ ├── dev
│ │ ├── logback.xml
│ │ ├── spark.properties
│ │ ├── spoor.conf
│ │ ├── spoorStream.conf
│ │ └── url.properties
│ ├── prod
│ │ ├── logback.xml
│ │ ├── spark.properties
│ │ ├── spoor.conf
│ │ ├── spoorStream.conf
│ │ └── url.properties
│ └── test
│ ├── logback.xml
│ ├── spark.properties
│ ├── spoor.conf
│ ├── spoorStream.conf
│ └── url.properties
└── test
├── resources
│ ├── collect.conf
│ ├── log4j.properties
│ ├── metrics.properties
│ └── spark.properties
└── scala
└── com
而spring的xml长这样
<bean id="propertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:url.properties</value>
<value>classpath:spark.properties</value>
</list>
</property>
</bean>
此时就要用到
由于我们内部的发布系统依赖一个run.sh的文件,所以刚好可以用来做启动脚本
所以我们的生成project的方法可以这样
def generateProject(name: String, path: Option[String] = None): Project = {
Project(name, file(path.getOrElse(name))).
enablePlugins(JvmPlugin).
enablePlugins(JavaAppPackaging).
enablePlugins(BuildEnvPlugin).
enablePlugins(sbtdocker.DockerPlugin).
settings(basicSettings)
}
def generateProjectWithProfile(name: String, path: Option[String] = None, profilePrefix: String = "src/profile"): Project = {
val suffix = sys.props.get("env").getOrElse("dev")
//get path from env
val realPath = path.getOrElse(name)
val confFiles = IO.listFiles(file(s"$realPath/$profilePrefix/$suffix")).map { confFile ⇒
confFile → s"conf/${confFile.getName}"
}.toSeq
val binFile = IO.listFiles(file(s"$realPath")).find(_.getName == "run.sh")
.map(_ → "run.sh")
val allFiles = binFile.fold(confFiles) { shellMapping ⇒
confFiles :+ shellMapping
}
generateProject(name, path)
.settings(mappings in Universal ++= allFiles)
}
我们目标是将profile里面的文件同名的搬运到打包出来的conf文件夹下面去,这样就可以通过sbt的启动的环境变量去区分对应的profile文件夹
调用逻辑如下
lazy val spoorDashboard = Packaging.generateProjectWithProfile("spoor-dashboard")
.settings(libraryDependencies ++= Seq(
Dependencies.scalaXML, Dependencies.scalaReflect,
Dependencies.akkaHttp, Dependencies.logback
)
).dependsOn(spoorDb, spoorConfig)
.settings(excludeDependencies += "org.slf4j" % "slf4j-log4j12")
.settings(Packaging.dockerSettings)
run.sh如下
➜ spoor-stream git:(docker) cat run.sh
#!/bin/sh
export JAVA_HOME=/usr/local/jdk8
currentdir=`dirname $0`
EXEC_COMMAND="${JAVA_HOME}/bin/java -classpath ${currentdir}/lib/*:${currentdir}/conf com.ximalaya.spoor.stream.StreamBoot"
usage() {
echo "$0 start|stop|restart"
exit 1
}
[ $# -ne 1 ] && usage
start() {
if [ -f ${currentdir}/lib/*spoor-stream*.jar ]
then
echo 'start spoor-stream'
nohup $EXEC_COMMAND > $currentdir/spoor-stream-out.log 2>&1 &
echo 'end of start spoor-stream'
else
echo 'spoor-stream.jar is not exists!'
fi
RETVAL=$?
echo
return $RETVAL
}
stop() {
pid=`ps -ef | grep spoor-stream | grep -v grep | awk '{print $2}'`
echo "stopping..."
kill -9 $pid || failure
echo "done"
RETVAL=$?
echo
return $RETVAL
}
restart(){
stop
start
}
case $1 in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
*)
usage
;;
esac
打完包之后就目录结构如下了
➜ stage git:(docker) tree -L 1
.
├── conf
├── lib
└── run.sh
2 directories, 1 file
➜ stage git:(docker) ls conf
logback.xml spark.properties spoor.conf spoorStream.conf url.properties
➜ stage git:(docker) pwd
/Users/cjuexuan/IdeaProjects/spoor/spoor-stream/target/universal/stage
➜ stage git:(docker)
把这个目录在jenkins同步出去就比较完美解决了
docker 的问题
docker的问题更简单了
lazy val dockerSettings = Seq(
docker := (docker dependsOn stage.in(Universal)).value,
buildOptions in docker := BuildOptions(
cache = false,
removeIntermediateContainers = BuildOptions.Remove.Always,
pullBaseImage = BuildOptions.Pull.Always
),
dockerfile in docker := {
val targetDir = s"/opt/${name.value}"
val mainclass = mainClass.in(Compile, packageBin).value.getOrElse(sys.error("Expected exactly one main class"))
new Dockerfile {
from("harbor.test.ximalaya.com/test/jdk8").entryPoint("java", "-cp", s"$targetDir/lib/*:$targetDir/conf", mainclass)
copy(baseDirectory(_ / "target/universal/stage").value, file(targetDir))
}
},
imageNames in docker := Seq(
sbtdocker.ImageName(
namespace = Some(s"harbor.test.ximalaya.com/test"),
repository = s"${name.value}",
tag = Some("latest")
)
),
dockerPush in docker := {
s"docker push harbor.test.ximalaya.com/test/${name.value}:latest" !
},
dockerPush := (dockerPush dependsOn docker).value
)
把docker这个命令的以及dockerPush,stage挂上依赖关系,最后只需要执行dockerPush就可以了
最后jenkins配置下
copy dependency
用了sbt native packager之后自动解决了这个问题,所以整个过程还是非常完美的
总结
这次切换首先让编译速度飞起来了,第二个顺便解决了docker的问题,总体来说还是比较完美
ps:我们正常项目打包也不会将prod.conf之类的扔到resouce里面去,应该是放到了和main同级的universal中去,这里是因为直接用的sbt native demo的截图
问下, 在使用 sbt docker 插件和使用 shell 脚本相比,为什么会使用 sbt docker 插件?
@timzaak 这个主要考虑到sbt的docker 插件可以帮你生成docker file,也可以和已有的task挂上依赖关系,比如我们将stage和docker build以及docker push挂在一起,只需要执行一个命令就可以了
嗯。 thanks.
请教一下如果自己写一个sbt-profile的插件应该从哪里入手
@Mvpanswer7 稍微封装下sbt-native-packager就可以了