Von Java Legacy zu Kotlin Multi-Plattform – Ein Erfahrungsbericht aus der Praxis

Von Java Legacy zu Kotlin Multi-Plattform – Ein Erfahrungsbericht aus der Praxis

Ein Erfahrungsbericht über eine Migration einer Java Bibliothek zu einer Kotlin Multi-Plattform Bibliothek. Gehalten am 12.06.2018 auf dem Kotlin Meetup Rhein-Main (https://www.meetup.com/de-DE/Kotlin-Rhein-Main/events/250605620/).

E75e669eb121804cbb20052574861288?s=128

Timm Hirsens

June 12, 2018
Tweet

Transcript

  1. Von Java Legacy zu Kotlin Multi-Plattform Ein Erfahrungsbericht aus der

    Praxis Timm Hirsens (developer@dvag) Guido Dolfen (it-architect@dvag)
  2. Agenda • Die Vorgeschichte • Die Challenge • Multi-Plattform •

    Java -> Kotlin • JavaScript • Fazit
  3. Die Vorgeschichte • 2003 – 2005: Entwicklung einer Rule-Engine für

    Plausibilitäten bei der die DSL der Regeln in XML formuliert werden (Java 1.4 -> Java 5) • 2005 – 2015: Pflege, Wartung und Refactoring der Rule-Engine (Java 6, Java 7) • 2015 – 2016: Portierung der Rule-Engine noch Objective-C für die DVAG-App • 2016 – 2018: Erweiterung der Java Rule-Engine um diverse Funktionen und eine neue DSL auf Basis von ANTLR (Java 8) • 2018 – 2019: Portierung der DVAG-App nach Angular
  4. Die Challenge • Bestehende Java Bibliothek muss nach JavaScript oder

    TypeScript portiert werden • Ziel: Single-Source Ansatz um hohe Implementierungsaufwände, Doppelpflege und Auseinanderlaufen der DSL zu vermeiden • Herausforderung: Die Java-Bibliothek muss Bytecode-Kompatibel bleiben • Problem: Es werden mehrere Java Third-Party-Bibliotheken verwenden (SAX-Parser der JDK, ANTLR, JUnit, Hamcrest) • Lösungsansatz: Kotlin Multi-Plattform
  5. Multi-Plattform • Experimentelles Feature in Kotlin 1.2 • Selbe Codebasis

    für JVM und JavaScript • Native soll in Zukunft auch unterstützt werden • Tooling ist noch nicht final, kann sich also noch ändern (tut es auch) • Es wird (aktuell) nur Gradle unterstützt • Standard Bibliothek ist aufgeteilt in • kotlin-stdlib-common • kotlin-stdlib-jvm • kotlin-stdlib-js • kotlin-stdlib-android
  6. Multi-Plattform

  7. Multi-Plattform • Im Common Module nur Zugriff auf Common Standardbibliothek

    • Alles plattformspezifische muss in jeweiligem Module implementiert werden Common JVM JS expectedBy expectedBy
  8. Multi-Plattform • expected und actual als Keywords • typealias für

    den Nachbau bestehender APIs expect class AtomicRef<V>(value: V) { fun get(): V fun set(value: V) fun getAndSet(value: V): V fun compareAndSet(expect: V, update: V): Boolean } actual typealias AtomicRef<V> = java.util.concurrent.atomic.AtomicReference<V> Common JVM JavaScript expect fun formatString(source: String, vararg args: Any): String actual fun formatString(source: String, vararg args: Any) = String.format(source, args) actual fun formatString(source: String, vararg args: Any) = util.format(source, args) Common JVM
  9. Java -> Kotlin: Herangehensweise 1. Anlegen der Kotlin-Klasse in IntelliJ

    2. Kopieren des Java-Codes in die neue Kotlin-Klasse => Automatische Konvertierung durch IntelliJ (Erfolgsquote ca. 70%) 3. Nacharbeit der Compile-Fehler 4. Refactoring zu Kotlin-Style
  10. Java – Static Methods in Utillity-Classes Common-Code: class RundenUtils private

    constructor(){ companion object { @GJvmStatic fun rundenAuf(wert: Double): Long { … } } } expect annotation class GJvmStatic() JAVA-Code: public final class RundenUtils { private RundenUtils() { super(); } public static double rundenAuf(final double wert) { … } } JVM-Code: actual typealias GJvmStatic = JvmStatic JS-Code: actual annotation class GJvmStatic()
  11. Java – Kotlin-Defaults als Overloading Common-Code: class RundenUtils private constructor(){

    companion object { @GJvmStatic @GJvmOverloads fun rundenAuf(wert: Double, stellen: Int = 0): Long { … } } } expect annotation class GJvmOverloads() JAVA-Code: public final class RundenUtils { public static double rundenAuf(final double wert) { rundenAuf(wert, 0); } public static double rundenAuf(final double wert, final int stellen) { … } } JVM-Code: actual typealias GJvmOverloads = JvmOverloads JS-Code: actual annotation class GJvmOverloads()
  12. Java - Date Common-Code: expect class Date expect fun Date.format(pattern

    : String): String expect fun Date.after(date : Date?): Boolean JVM-Code: actual typealias Date = java.util.Date actual fun Date.format(pattern : String): String { val format = SimpleDateFormat(pattern) return format.format(this) } actual fun Date.after(date : Date?): Boolean { return this.after(date) } JS-Code: import moment actual typealias Date = kotlin.js.Date actual fun Date.format(pattern : String): String { return moment(this).format(pattern); } actual fun Date.after(date : Date?): Boolean { return this.getMilliseconds() > getMilliseconds() }
  13. Java - Calendar Common-Code: expect abstract class Calendar { fun

    setTimeInMillis(millis : Long) fun getTimeInMillis() : Long fun set(field : Int, value : Int) } expect fun Calendar.getYear(): Int expect fun Calendar.setYear(year : Int) expect fun newCalendar() : Calendar JVM-Code: actual typealias Calendar = java.util.Calendar actual fun Calendar.getYear(): Int { return get(Calendar.YEAR); } actual fun Calendar.setYear(year: Int) { set(Calendar.YEAR, year) } actual fun newCalendar() : Calendar { return Calendar.getInstance() } JS-Code: actual abstract class Calendar { var date: Date init { this.date = Date() } @JsName("setTimeInMillis") actual fun setTimeInMillis(millis: Long) { this.date = Date(millis) } } @JsName("getYear") actual fun Calendar.getYear(): Int { return this.date.getFullYear() } @JsName("setYear") actual fun Calendar.setYear(year: Int) { this.date = Date(year, date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()) } class GregorianCalendar() : Calendar() actual fun newCalendar(): Calendar { return GregorianCalendar() }
  14. Java – Class und KClass Common-Code: expect class Class<T> {

    fun getSimpleName() : String fun getName() : String fun isAssignableFrom(cls: Class<*>): Boolean } expect fun KClass<*>.toClass() : Class<*> expect fun String.toClass(): Class<*> JVM-Code: actual typealias Class<T> = java.lang. Class<T> actual fun KClass<*>.toClass() : Class<*> { return when { this == kotlin.Byte::class -> Byte::class.java this == kotlin.Short::class -> Short::class.java this == kotlin.Int::class -> Integer::class.java this == kotlin.Long::class -> Long::class.java this == kotlin.Float::class -> Float::class.java this == kotlin.Double::class -> Double::class.java this == kotlin.Boolean::class -> Boolean::class.java else -> this.java } } actual fun String.toClass(): Class<*> { try { return java.lang.Class.forName(this) } catch (e : ClassNotFoundException) { throw IllegalArgumentException( "$this is not a Class",e) } } JS-Code: actual class Class<T> constructor(val name : String) { actual fun getSimpleName(): String { return name } actual fun getName(): String { return name } actual fun isAssignableFrom(cls: Class<*>): Boolean { return name == cls.getName() } } actual fun KClass<*>.toClass(): Class<*> { return Class<Any>(this.simpleName!!) } actual fun String.toClass(): Class<*> { return Class<Any>(this) }
  15. Java – Number-Format & Parse Common-Code: expect fun String.parseNumber(): Number

    expect fun Number.format(pattern: String): String JVM-Code: actual fun String.parseNumber(): Number { try { val numberFormat = NumberFormat.getNumberInstance( Locale.GERMANY) if (this.contains(",")) { numberFormat. maximumFractionDigits = 20 } numberFormat.isGroupingUsed = this.contains(".") return numberFormat.parse( this.trim { it <= ' ' }) } catch (e: ParseException) { throw IllegalArgumentException( "Value out of range. Value:\"$this\"", e) } } actual fun Number.format(pattern: String) : String { val decimalFormat = NumberFormat. getInstance(Locale.GERMAN) as DecimalFormat decimalFormat.applyPattern(pattern) return decimalFormat.format(this) } JS-Code: actual fun String.parseNumber(): Number { TODO(„Find a cool lib for String.parseNumber") … } actual fun Number.format(pattern: String): String { TODO("Find a cool lib for Number.format") … }
  16. Java – Hamcrest • Hamcrest nutzt die assertThat() Assertion mit

    Matchern • Der gebräuchlichste Matcher heißt is() => is ist ein Schlüsselwort in Kotlin • Workaround: • Ersetzen des Matchers is() durch equalTo() im Java-Code • Extension Methods im Hamcrest-Style bauen • Alternativen: • JUnit-Test als Java-Klassen im JVM-Package belassen (Achtung: JS Tests fehlen!) • HamKrest - Hamcrest for Kotlin (https://github.com/npryce/hamkrest) import kotlin.test.assertEquals fun <T> assertThat(actual : T?, expected : T?) { assertEquals(expected, actual); } fun <T> equalTo( expected :T?) : T? { return expected; } fun <T> nullValue() : T? { return null; }
  17. Java 8 – Default-Methods • Kotlin generiert die Default-Methods in

    die implementierenden Klassen • Es können bis Version 1.2.40 keine Java-Interfaces mit Default-Methods gebaut werden • Lösung im Projekt: • Erweiterung des Java-Code um eine abstrakte Klasse mit den Default-Implementierungen • Setzen der Default-Methods im Java-Interface auf @deprecated => Anpassungsaufwand im produktiven Java-Code => Nachteil: Keine Lambdas mehr möglich • Ab Version 1.2.40 @JvmDefault
  18. JavaScript - JavaScript Module aufrufen • Einbindung von JavaScript Modulen:

    (JavaScript aus Kotlin aufrufen) 1. Aufnahme in package.json: antlr4": "^4.7.1", 2. Generierung der Typinformationen aus TypeScript sofern vorhanden (GH: Kotlin/ts2kt) 3. Alternative: Selber schreiben ! @file:JsModule("antlr4") package antlr4 open external class CommonTokenStream(input: Lexer) open external class Lexer(input: String) external object CharStreams { fun fromString(str: String): InputStream } external class InputStream open external class ParserRuleContext { fun getToken(type: Int, i: Int): TerminalNode? = definedExternally […] }
  19. JavaScript - JavaScript Module aufrufen • SAX-Parser gibt es nicht

    in der „Standard-Bibliothek“ von Kotlin JS oder JavaScript • NPM Package kann auch hier Abhilfe schaffen • https://www.npmjs.com/package/sax external open class SAXParser(strict: Boolean, opt: SAXOptions) { open fun onerror(e: Error): Unit = definedExternally open fun ontext(t: String): Unit = definedExternally open fun ondoctype(doctype: String): Unit = definedExternally open fun onprocessinginstruction(node: `T$2`): Unit = definedExternally open fun onopentag(tag: QualifiedTag): Unit = definedExternally open fun onclosetag(tagName: String): Unit = definedExternally open fun onattribute(attr: `T$3`): Unit = definedExternally open fun oncomment(comment: String): Unit = definedExternally open fun onopencdata(): Unit = definedExternally open fun oncdata(cdata: String): Unit = definedExternally open fun onclosecdata(): Unit = definedExternally open fun onend(): Unit = definedExternally [...] }
  20. JavaScript – Kotlin aus JS aufrufen • Integrationstests („Kotlin“ aus

    JavaScript aufrufen) • Package-Namen bleiben auch in Javascript enthalten const parser = require('generic-rules-js').com.dvag.generic.parser; const values = require('generic-rules-js').com.dvag.generic.values; describe('Parser', () => { test('should parse now()', () => { const functionParser = new parser.GXLFunctionParser(); const result = functionParser.parse('now()'); expect(result).not.toBeNull(); expect(result).toBeInstanceOf(values.Now); expect(result.call().date).toBeInstanceOf(Date) }); });
  21. JavaScript - Kotlin aus JS aufrufen • Javascript Tests in

    das Gradle Output Directory kopieren • Test-Runner muss in Gradle konfiguriert werden task runJest(type: NodeTask, dependsOn: [compileTestKotlin2Js, populateNodeModules, copyJsCode, copyJsTestCode]) { script = file('node_modules/jest/bin/jest.js') args = ['build/classes/kotlin/test'] } task copyJsTestCode(type: Copy, dependsOn: compileTestKotlin2Js) { from "${projectDir}/src/test/js" into compileTestKotlin2Js.destinationDir }
  22. JavaScript - Kotlin aus JS aufrufen • JavaScript Funktionen können

    mit einer beliebigen Anzahl von Parametern (von beliebigem Typ) aufgerufen werden • Polymorphie wie in Java oder Kotlin lässt sich damit nicht abbilden so wird aus: einfach: @JSName kann hier wichtig sein fun funWithParam(one: Double) { return 1; } fun funWithParam(one: Long, two: Double) { return 2; } function funWithParam_8eortd$() { return 1; } function funWithParam_fmv235$() { return 2; }
  23. JavaScript - Kotlin aus JS aufrufen • API Design für

    Java-/TypeScript Consumer ist kompliziert • Möglichst auf einfache Datentypen beschränken, aber Vorsicht!
  24. JavaScript - Kotlin aus JS aufrufen TypeError: runtimeEnvironment.get_pdl1vj$ is not

    a function 21 | env = new Map(); 22 | env.set("this", "a"); > 23 | expect(parsedRule.call(env)).toBe(true); sources = { "this": {"a": "1"}, }; env = generic.createMapRuntimeEnvironment(sources); package com.dvag.generic @JsName("createMapRuntimeEnvironment") fun createMapRuntimeEnvironment(jsobj: dynamic): MapRuntimeEnvironment { val newMap = mutableMapOf<String, Any>() createMapFromObject(jsobj, newMap) return MapRuntimeEnvironment(newMap) } fun createMapFromObject(obj: dynamic, newMap: MutableMap<String, Any>) { val keys = js("Object.keys") val mapKeys = keys(obj).unsafeCast<Array<String>>(); mapKeys.forEach { val value = obj[it]; val isObject = js("value.constructor === Object") as Boolean if (isObject) { val map = mutableMapOf<String, Any>() createMapFromObject(value, map) newMap.put(it, map) } else { newMap.put(it, value) } } } /** * Aufruf der Funktion * @param runtimeEnvironment Die Laufzeitumgebung. * @return Das Ergebnis der Funktion. */ @GJsName("call") fun call(runtimeEnvironment: T) : Any?
  25. Fazit • Multiplattform ist ein interessanter Ansatz insbesondere für komplette

    Anwendungen / Apps • Library Entwicklung für Java ist einfacher als für JavaScript • Trotzdem ist die Portierung aus unserer Sicht gut gelungen • JavaScript Debugging ist kompliziert (viel console.log()) • JavaScript Kenntnisse sollten vorhanden sein • Plattformannotationen sollten in die common-stdlib (@JSName, @JvmName, @JvmStatic , usw.) • Alles noch experimentell, aber durchaus schon produktiv nutzbar • Forschen, Ausprobieren und im Zweifelsfalls JetBrains oder die Community kontaktieren (Slack => http://slack.kotlinlang.org/)