从Scala文件创建一个jar文件

51

我是Scala的新手,不懂Java。我想从一个简单的Scala文件创建一个jar文件。因此,我有了我的HelloWorld.scala,生成一个HelloWorld.jar。

Manifest.mf:

Main-Class: HelloWorld

在控制台中我运行:

fsc HelloWorld.scala
jar -cvfm HelloWorld.jar Manifest.mf HelloWorld\$.class HelloWorld.class
java -jar HelloWorld.jar 
  => "Exception in thread "main" java.lang.NoClassDefFoundError: HelloWorld/jar"

java -cp HelloWorld.jar HelloWorld 
  => Exception in thread "main" java.lang.NoClassDefFoundError: scala/ScalaObject
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:675)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:124)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:260)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:56)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:195)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:316)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:280)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:251)
    at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:374)
    at hoppity.main(HelloWorld.scala)

1
你是否记得在 .jar 文件中包含清单文件了? - millimoose
它说找不到哪个类? - Michael Myers
另外,你是否先将Scala编译成字节码了? - millimoose
好奇。你能运行 java -cp HelloWorld.jar HelloWorld 吗? - Michael Myers
1
您还需要将Scala运行时scala-library.jar包含在CLASSPATH中。 - andri
9个回答

55

样本目录结构:

X:\scala\bin
X:\scala\build.bat
X:\scala\MANIFEST.MF
X:\scala\src
X:\scala\src\foo
X:\scala\src\foo\HelloWorld.scala

HelloWorld.scala:

//file: foo/HelloWorld.scala
package foo {
  object HelloWorld {
    def main(args: Array[String]) {
      println("Hello, world!")
    }
  }
}

MANIFEST.MF:

Main-Class: foo.HelloWorld
Class-Path: scala-library.jar

build.bat:

@ECHO OFF

IF EXIST hellow.jar DEL hellow.jar
IF NOT EXIST scala-library.jar COPY %SCALA_HOME%\lib\scala-library.jar .

CALL scalac -sourcepath src -d bin src\foo\HelloWorld.scala

CD bin
jar -cfm ..\hellow.jar ..\MANIFEST.MF *.*
CD ..

java -jar hellow.jar
为了成功使用-jar开关,您需要在META-INF/MANIFEST.MF文件中有两个条目:主类和任何依赖项的相对URL。文档注释如下:

-jar

执行封装在JAR文件中的程序。第一个参数是JAR文件的名称而不是启动类名。为了使此选项起作用,JAR文件的清单必须包含一个形式为Main-Class:classname的行。在这里,classname标识具有公共静态void main(String [] args)方法的类,该方法作为应用程序的起点。有关使用Jar文件和Jar文件清单的信息,请参见Jar工具参考页面和Java教程的Jar trail。

当您使用此选项时,JAR文件是所有用户类的源,其他用户类路径设置将被忽略。

(注:大多数ZIP应用程序都可以检查JAR文件;批处理脚本中可能会忽略处理目录名称中的空格;Scala代码运行器版本为2.7.4.final。)


为了完整起见,以下是等效的Bash脚本:

#!/bin/bash

if [ ! $SCALA_HOME ]
then
    echo ERROR: set a SCALA_HOME environment variable
    exit
fi

if [ ! -f scala-library.jar ]
then
    cp $SCALA_HOME/lib/scala-library.jar .
fi

scalac -sourcepath src -d bin src/foo/HelloWorld.scala

cd bin
jar -cfm ../hellow.jar ../MANIFEST.MF *
cd ..

java -jar hellow.jar

我严格按照这些说明操作(复制和粘贴并设置目录,如您所建议),但我收到以下错误:java -jar hellow.jar Exception in thread "main" java.lang.NoClassDefFoundError: scala/ScalaObject at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:676) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:124) at java.net.URLClassLoader.defineClass(URLClassLoader.java:260)。树形结构:http://pastebin.com/3k8rCpxf - I82Much
@I82Much - 我没有看到你的树形结构有任何明显的问题;我建议解压 hellow.jar 并验证清单文件是否具有正确的依赖关系。 - McDowell
最近我遇到了类似的问题 - 只有当我在Class-Path中写入库的绝对路径时,问题才消失了(~没有起作用)。 - xhudik
抱歉问个新手问题:我猜这是使用 sbt 的替代方案?(因为 sbt 无法产生我想要的结果) - John Nicholas
2
@JohnNicholas 我认为这个答案比sbt更早。 - McDowell

10

因为Scala脚本需要安装Scala库,所以您必须将Scala运行时与您的JAR文件一起包含。

有许多策略可以实现这一点,例如jar jar,但最终您遇到的问题是启动的Java进程找不到Scala JAR文件。

对于一个简单的独立脚本,我建议使用jar jar,否则您应该开始考虑依赖管理工具,或要求用户在JDK中安装Scala。


是的,从许可证的角度来看,重新打包JAR文件不应该是一个问题:http://www.scala-lang.org/node/146 - McDowell

5

我最终使用了 sbt assembly,它非常易于使用。我在项目根目录下的project/目录中添加了一个名为assembly.sbt的文件,并加入了一行代码(注意您的版本可能需要更改)。

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.11.2")

然后只需在 sbt 中运行 assembly 任务:

> assembly

或者在项目根目录下执行 'sbt assembly'。
$ sbt assembly

它将首先运行您的测试,然后将生成新的jar文件到目录target/(假设我的build.sbt已经列出了所有的依赖项)。
在我的情况下,我只需将该.jar文件设置为可执行,重命名以去掉扩展名,它就可以准备好进行发布了!
此外,如果您正在制作一个命令行工具,请不要忘记添加man页面(我讨厌没有适当manpages或带有多页纯文本文档的脚本,它甚至没有为您输入分页器)。

4
我是一个能够翻译文本的有用助手。
我不想解释为什么和如何,而只是展示解决方案(通过Linux Ubuntu命令行在我的情况下有效):
1)
mkdir scala-jar-example
cd scala-jar-example

2)
nano Hello.scala
object Hello extends App   {  println("Hello, world")   }

3)
第三点)
nano build.sbt
import AssemblyKeys._

assemblySettings

name := "MyProject"

version := "1.0"

scalaVersion := "2.11.0"

3)

mkdir project
cd project 
nano plugins.sbt
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.9.1")

4)

cd ../   
sbt assembly

5)

java -jar target/target/scala-2.11/MyProject-assembly-1.0.jar
>> Hello, world

4

我尝试复制MyDowell的方法。最终我成功了。然而,我发现答案虽然正确,但对于新手来说有点太复杂了(特别是目录结构过于复杂)。

我可以用非常简单的方法来复制这个结果。首先,只需要一个包含三个文件的目录:

helloworld.scala
MANIFEST.MF
scala-library.jar

helloworld.scala

object HelloWorld
{
  def main(args: Array[String])
  {
    println("Hello, world!")
  }
}

MANIFEST.MF:

Main-Class: HelloWorld
Class-Path: scala-library.jar

首先编译helloworld.scala:

scalac helloworld.scala

然后创建jar文件:

\progra~1\java\jdk18~1.0_4\bin\jar -cfm helloworld.jar MANIFEST.MF .

现在你可以通过以下方式运行它:

java -jar helloworld.jar

我发现这个简单的解决方案,因为原始的解决方案没有起作用。 后来我发现这不是因为它是错误的,而是因为一个微不足道的错误:如果我没有在MANIFEST.MF中的第二行上使用换行符关闭该行,则该行将被忽略。 在尝试了其他所有方法之后,这花费了我一个小时的时间,最终找到这个非常简单的解决方案。

我遇到了“找不到或加载主类HelloWorld”的问题。你需要在jar命令中包含构建文件吗?并且你能否在回答中添加scala-library.jar的位置? - A1rPun

4

你也可以使用Maven和maven-scala-plugin。一旦你设置好了Maven,只需执行mvn package,它就会为你创建jar包。


1
生成的JAR文件不包含scala-library.jar。 - Simon Morgan
2
@simon - 这取决于你是否将scala-library添加到pom文件的依赖项中。 - xhudik

2
我修改了bash脚本,增加了一些智能功能,包括自动生成清单。
该脚本假定主对象与其所在的文件同名(区分大小写)。同时,当前目录名称必须等于主对象名称或者主对象名称应作为命令行参数提供。请从项目的根目录启动此脚本,并根据需要修改顶部的变量。
请注意,该脚本将生成bin和dist文件夹,并且会删除bin中的任何现有内容。

#!/bin/bash

SC_DIST_PATH=dist
SC_SRC_PATH=src
SC_BIN_PATH=bin
SC_INCLUDE_LIB_JAR=scala-library.jar
SC_MANIFEST_PATH=MANIFEST.MF
SC_STARTING_PATH=$(pwd)

if [[ ! $SCALA_HOME ]] ; then
    echo "ERROR: set a SCALA_HOME environment variable"
    exit 1
fi

if [[ ! -f $SCALA_HOME/lib/$SC_INCLUDE_LIB_JAR ]] ; then
    echo "ERROR: Cannot find Scala Libraries!"
    exit 1
fi

if [[ -z "$1" ]] ; then
    SC_APP=$(basename $SC_STARTING_PATH)
else
    SC_APP=$1
fi

[[ ! -d $SC_DIST_PATH ]] && mkdir $SC_DIST_PATH

if [[ ! -d $SC_BIN_PATH ]] ; then
    mkdir "$SC_BIN_PATH"
else
    rm -r "$SC_BIN_PATH"
    if [[ -d $SC_BIN_PATH ]] ; then
        echo "ERROR:  Cannot remove temp compile directory:  $SC_BIN_PATH"
        exit 1
    fi
    mkdir "$SC_BIN_PATH"
fi

if [[ ! -d $SC_SRC_PATH ]] || [[ ! -d $SC_DIST_PATH ]] || [[ ! -d $SC_BIN_PATH ]] ; then
    echo "ERROR: Directory not found!:  $SC_SRC_PATH or $SC_DIST_PATH or $SC_BIN_PATH"
    exit 1
fi

if [[ ! -f $SC_DIST_PATH/$SC_INCLUDE_LIB_JAR ]] ; then
    cp "$SCALA_HOME/lib/$SC_INCLUDE_LIB_JAR" "$SC_DIST_PATH"
fi

SCALA_MAIN=$(find ./$SC_SRC_PATH -name "$SC_APP.scala")
COMPILE_STATUS=$?
SCALA_MAIN_COUNT=$(echo "$SCALA_MAIN" | wc -l)

if [[ $SCALA_MAIN_COUNT != "1" ]] || [[ ! $COMPILE_STATUS == 0 ]] ; then
    echo "Main source file not found or too many exist!:  $SC_APP.scala"
    exit 1
fi

if [[ -f $SC_DIST_PATH/$SC_APP.jar ]] ; then
    rm "$SC_DIST_PATH/$SC_APP.jar"  
    if [[ -f $SC_DIST_PATH/$SC_APP.jar ]] ; then
        echo "Unable to remove existing distribution!:  $SC_DIST_PATH/$SC_APP.jar"
        exit 1
    fi
fi

if [[ ! -f $SC_MANIFEST_PATH ]] ; then
    LEN_BASE=$(echo $(( $(echo "./$SC_SRC_PATH" |wc -c) - 0 )))
    SC_MAIN_CLASS=$(echo $SCALA_MAIN |cut --complement -c1-$LEN_BASE)
    SC_MAIN_CLASS=${SC_MAIN_CLASS%%.*}
    SC_MAIN_CLASS=$(echo $SC_MAIN_CLASS |awk '{gsub( "/", "'"."'"); print}')

    echo $(echo "Main-Class: "$SC_MAIN_CLASS) > $SC_MANIFEST_PATH
    echo $(echo "Class-Path: "$SC_INCLUDE_LIB_JAR) >> $SC_MANIFEST_PATH
fi

scalac -sourcepath $SC_SRC_PATH -d $SC_BIN_PATH $SCALA_MAIN
COMPILE_STATUS=$?

if [[ $COMPILE_STATUS != "0" ]] ; then
    echo "Compile Failed!"
    exit 1
fi

cd "$SC_BIN_PATH"
jar -cfm ../$SC_DIST_PATH/$SC_APP.jar ../$SC_MANIFEST_PATH *
COMPILE_STATUS=$?
cd "$SC_STARTING_PATH"

if  [[ $COMPILE_STATUS != "0" ]] || [[ ! -f $SC_DIST_PATH/$SC_APP.jar ]] ; then
    echo "JAR Build Failed!"
    exit 1
fi

echo " "
echo "BUILD COMPLETE!... TO LAUNCH:  java -jar $SC_DIST_PATH/$SC_APP.jar"
echo " "

2

有一件事情可能会引起类似的问题(虽然这不是上面初始问题的原因),那就是Java虚拟机似乎要求main方法返回void。在Scala中,我们可以编写类似于下面的代码(注意在main定义中的等号符号):

object MainProgram {

  def main(args: Array[String]) = {
    new GUI(args)
  }
}

在这里,main实际上返回了一个GUI对象(即它不是void),但当我们使用scala命令启动程序时,程序将可以正常运行。

如果我们将此代码打包成一个jar文件,并将MainProgram作为主类,Java虚拟机会抱怨没有主函数,因为我们的主函数的返回类型不是void(我认为这种抱怨有些奇怪,因为返回类型不是签名的一部分)。

如果我们在main的头部中省略了等号,或者明确声明其为Unit,则不会出现任何问题。


0
如果您不想使用sbt工具,我建议使用makefile。
以下是一个示例,其中foo包被替换为foo.bar.myApp以保证完整性。
makefile
NAME=HelloWorld
JARNAME=helloworld

PACKAGE=foo.bar.myApp
PATHPACK=$(subst .,/,$(PACKAGE))

.DUMMY: default
default: $(NAME)

.DUMMY: help
help:
    @echo "make [$(NAME)]"
    @echo "make [jar|runJar]"
    @echo "make [clean|distClean|cleanAllJars|cleanScalaJar|cleanAppJar]"

.PRECIOUS: bin/$(PATHPACK)/%.class

bin/$(PATHPACK)/%.class: src/$(PATHPACK)/%.scala
    scalac -sourcepath src -d bin $<

scala-library.jar:
    cp $(SCALA_HOME)/lib/scala-library.jar .

.DUMMY: runjar
runJar: jar
    java -jar $(JARNAME).jar

.DUMMY: jar
jar: $(JARNAME).jar

MANIFEST.MF:
    @echo "Main-Class: $(PACKAGE).$(NAME)" > $@
    @echo "Class-Path: scala-library.jar" >> $@

$(JARNAME).jar: scala-library.jar bin/$(PATHPACK)/$(NAME).class \
                                MANIFEST.MF
    (cd bin && jar -cfm ../$(JARNAME).jar ../MANIFEST.MF *)

%: bin/$(PATHPACK)/%.class
    scala -cp bin $(PACKAGE).$@

.DUMMY: clean
clean:
    rm -R -f bin/* MANIFEST.MF

cleanAppJar:
    rm -f $(JARNAME).jar

cleanScalaJar:
    rm -f scala-library.jar

cleanAllJars: cleanAppJar cleanScalaJar

distClean cleanDist: clean cleanAllJars

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接