This article was originally posted in my blog
Kotlin is great for creating small command-line utilities, which can be packaged and distributed as normal JAR files. This short tutorial will show you how to:
- Set up a Gradle project that supports Kotlin
- Add a starting function
- Configure your build to call this function when you execute your JAR.
Setting up Kotlin dependencies
apply plugin: 'java'
apply plugin: 'kotlin'
//… other stuff, you typically find in a Gradle build file
dependencies {
// other dependencies …
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
buildscript {
ext.kotlin_version = '1.0.2'
//...
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
You will also want your IDE (I assume you're using IntelliJ or Android Studio) to mark the directory where your Kotlin source code resides as a source directory. Since Kotlin and Java are best friends, it is perfectly fine to keep the same package structure. It is a good practice, though to keep your Kotlin code physically separate from your Java one. Thus, you'd typically have two folders under src
- src/main/java
for Java classes, and src/main/kotlin
for Kotlin ones. Same for tests. Again, in your build.gradle
file, add the following:
sourceSets {
main.java.srcDirs += 'src/main/kotlin/'
test.java.srcDirs += 'src/test/kotlin/'
}
Using IntelliJ, you could rely not he IDE to guide you with all of this, but I wanted to show you the basics, since one may not always rely on the comfort of an IDE. To see that everything is working as it should, go to your project directory and create a new build:
grade clean build
If everything has been set up correctly, you should be able to see a task named compileKotlin
which the build has executed successfully.
Write your first Kotlin program
Unlike Java, Kotlin is friendlier with functions that reside outside of any class scope. You can create a Main
class hosting a main()
function, or you can create a top-level main()
without necessarily wrapping it in a class. Perhaps, you wouldn't be able to find any difference in a such a brief examaple, but I find the possibility to create top-level functions helpful in reducing boilerplate code.
Here is the mandatory HelloWorld example. Create a file with an arbitrary name (say Main
) and an extension .kt
, and write simply:
fun main(args : Array<String>) {
println("Hello, world!")
}
Note that adding a package is optional, as well as ending your lines with semicolons. In order to keep consistency with my Java code though, I'd usually add both, and expect that people I work with, do the same.
Configure your Gradle build to create an executable JAR
The main
function we just added, is enough to test setting up an executable JAR, which one should then be able to call simply by executing:
java -jar <MY_PROJECT_NAME>.jar
If you simply try to build your project and then execute the above command, we would get the following message:
no main manifest attribute <PATH_TO_MY_PROJECT_JAR>.jar
This means that we have to configure jar
task, which Java Gradle builds go through, and tell it which the starting point of our project is. In a Java project, this would be the path to the class where our main()
function resides:
jar {
manifest {
attributes 'Main-Class': 'com.myname.myprojectname.Main'
}
}
Wait a minute? We have defined our main()
function outisde of any class scope. That's true and not entirely true at the same time. Actually, to keep things at the bytecode level consistent, and backwards-compatible with the JVM, the Kotlin compiler adds all top-level functions to respective classes. In our case, the class generated by the Kotlin compiler would have the same name the filename of the file where our function resides, plus the suffix Kt
. This means, for example, that if our file is called Main.kt
, the Kotlin compiler would generate a class with the name MainKt.class
and add it to the generated JAR file. Knowing this, one could rewrite the Gradle configuration above, as follows:
jar {
manifest {
attributes 'Main-Class': 'com.myname.myprojectname.MainKt'
}
}
Note: You can specifiy the name this class should be compiled with, by adding a file-scope annotation on top of your file, even before the package
declaration:
@file:JvmName("MainCls")
This new name can be used within the JAR manifest configuration, as shown above.
Even though we specified our main class correctly in our JAR manifest configuration, if we try to execute our main function using jar -jar
, we will still see an error message:
Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics
at com.preslavrachev.imdbparser.MainKt.main(Main.kt)
Caused by: java.lang.ClassNotFoundException: kotlin.jvm.internal.Intrinsics
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 1 more
Experienced Java developers will quickly recognize this type of exception. By default when Gradle (as well as Maven) packs some Java class files into a JAR file, it is assumed that this JAR file will be referenced by an application, where all of its dependencies are also accessible within the classpath of the loading application. To execute a JAR without having to specifiy the path to itse dependencies, one must tell the build system to take all of this JAR's referenced dependencies and copy them as part of the JAR itself. In the Java community, this is known as a "fat JAR". In a "fat JAR" all of the dependencies end up within the class path of the loading application, so code can be executed without problems. The only downside to creating fat JARs is of course their growing file size (which kind of explains the name), though in most situations, it is not a big concern. In order to tell Gradle to copy all of a JAR's dependencies, one should simply modify the abovementioned JAR task configuration, by adding the following piece of code:
jar {
manifest {
attributes 'Main-Class': 'com.preslavrachev.imdbparser.MainKt'
}
// This line of code recursively collects and copies all of a project's files
// and adds them to the JAR itself. One can extend this task, to skip certain
// files or particular types at will
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
}
Top comments (2)
Use application plugin instead.
Thanks, I corrected it