被Kotlin写Web服务行云流水的操作惊惊惊呆了!

3,448 阅读8分钟

前言

前几天升级了新版本的IDEA,本来我是做Java后端,但是也一直想学Kotlin,闲时候也可以做个APP哄女朋友开心,正好新版本的IDEA创建项目的方式有了些变换,选择性更加多了,所以直接选择了Kotlin项目,但是当Next完后还有一步,要你选择版本、模板,模板中有一个Web Server,所以我就好奇选中了它,看看新一代做Web开发是怎么样的,最后IDEA给我生成了以下模板代码,不做任何配置就可以直接运行,也不需要Tomcat什么的。

fun HTML.index() {
    head {
        title("Hello from Ktor!")
    }
    body {
        div {
            +"Hello from Ktor"
        }
    }
}

fun main() {
    embeddedServer(Netty, port = 8080, host = "127.0.0.1") {
        routing {
            get("/") {
                call.respondHtml(HttpStatusCode.OK, HTML::index)
            }
        }
    }.start(wait = true)
}

这代码虽然很多人大致能看懂,能猜出来他的作用,并且这就启动了一个Web服务,端口是8080,访问/的时候会给你返回一个基本的HTML,内容是这样的。

<!DOCTYPE html>
<html>
  <head>
    <title>Hello from Ktor!</title>
  </head>
  <body>
    <div>Hello from Ktor</div>
  </body>
</html>

但是鬼知道为什么可以这样写?Kotlin这些新语法太神奇了!

这又一次颠覆了认知,就好比一个从Servlet转到Spring Boot开发,思想上简直不敢相信,在我们传统的代码中,要创建这样一个简单的功能,要使用到ServerSocket,InputStream、OutputStream,然后把要显示的数据一点点的拼接成HTTP协议格式,最后输出。

如果要在简单一点,可能会使用一些框架,最常见的Tomcat,写一个HTML放在他的Web工作目录,如果是使用SpringBoot,那就更简单了。

但是这样的方式,可是从来没见过的,所以很好奇。为了弄明白这里面的知识,我们需要了解到三个技术点:

  • Kotlin
  • DSL
  • Netty

可能只有DSL不是很熟悉,其他两个已经被各大公众号刷了屏了。

Kotlin

而Kotlin是由JetBrains开发,Kotlin可以编译成Java字节码,也可以编译成JavaScript,语法糖甜的不要不要的,而有几个特性非常喜欢,简直太方便了,比如扩展。

我们也应该知道这样一句话,只要遵循JVM规范,你创造到语言也可以运行在JVM上,Kotlin就是,编译后的class和我们普通java文件编译的一样。

比如下面是通过反编译工具查看kotlin编译后的class,这个程序只做了一行打印。

fun main(args: Array<String>) {
    print("Kotlin")
}
import java.io.PrintStream;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

@Metadata(mv={1, 4, 2}, bv={1, 0, 3}, k=2, d1={"\000\024\n\000\n\002\020\002\n\000\n\002\020\021\n\002\020\016\n\002\b\002\032\031\020\000\032\0020\0012\f\020\002\032\b\022\004\022\0020\0040\003��\006\002\020\005��\006\006"}, d2={"main", "", "args", "", "", "([Ljava/lang/String;)V", "GradleDemo"})
public final class KotlinMainKt
{
  public static final void main(@NotNull String[] args)
  {
    Intrinsics.checkNotNullParameter(args, "args");String str = "Kotlin";int i = 0;System.out.print(str);
  }
}

从中可以看到,默认情况下,这个类被申明了final,所以不能被继承,print("Kotlin")也被翻译成了三句话,checkNotNullParameter方法是检测参数是否为空,为空则抛出异常,后面就是标准打印了。

public class Intrinsics{
 public static void checkNotNullParameter(Object value, String paramName) {
     if (value == null) {
         throwParameterIsNullNPE(paramName);
     }
 }
}

再来看一个牛逼哄哄的扩展,到底生成后是什么样的。

下面这个程序为String类添加一个扩展print函数,这样就可以直接"".print进行打印了,而在方法中使用的this关键字,最后都是被转换成了这个方法参数列表中的第一个参数,而参数值就是""里面的字符串。

但是好奇的是,被编译后有一行int i = 0;,并且没使用过,不知道到底作什么用。

fun String.print() {
    print(this)
}

fun main() {
 "a".print()
}
import java.io.PrintStream;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

@Metadata(mv={1, 4, 2}, bv={1, 0, 3}, k=2, d1={"\000\024\n\000\n\002\020\002\n\000\n\002\020\021\n\002\020\016\n\002\b\003\032\031\020\000\032\0020\0012\f\020\002\032\b\022\004\022\0020\0040\003��\006\002\020\005\032\n\020\006\032\0020\001*\0020\004��\006\007"}, d2={"main", "", "args", "", "", "([Ljava/lang/String;)V", "print", "GradleDemo"})
public final class KotlinMainKt
{
  public static final void print(@NotNull String $this$print)
  {
    Intrinsics.checkNotNullParameter($this$print, "$this$print");
    
    int i = 0;
    
    System.out.print($this$print);
  }
  
  public static final void main(@NotNull String[] args)
  {
    Intrinsics.checkNotNullParameter(args, "args");
    print("a");
  }
}

只能说太方便了。

DSL

DSL叫做领域特定语言,指的是专注于某个应用程序领域的计算机语言,比如用来显示网页的HTML。说真的,我也是前几天才知道这词。不做过多的解释。

Netty

java网络开源框架,Netty就是简化了网络应用的编程开发过程。

kotlinx.html

这个库是本文的重点。转到: github.com/Kotlin/kotl…

官方是这样解释的:此库提供了DSL,可以在JVM上构建HTML,以更好地进行Web的Kotlin编程。

巴拉巴拉他说了一堆,也没看懂,咱们还是看几个例子吧。

启动Web服务

embeddedServer(Netty, port = 8080, host = "127.0.0.1") {
    routing {
        get("/") {
            call.respondHtml(HttpStatusCode.OK, HTML::index)
        }
    }
}.start(wait = true)

embeddedServer其实就是EmbeddedServerKt类里面的静态方法。然后创建了一个嵌入式服务器Netty的实例,并在端口8080上进行侦听,通过routing定义几个访问路由,这里只定义了/,然后返回上面HTML.index方法定义的HTML代码。

返回文本

如果只想简单返回一些文本,可以直接调用respondText方法。

fun main() {
    embeddedServer(Netty, port = 8080, host = "127.0.0.1") {
        routing {
            get("/") {
                call.respondHtml(HttpStatusCode.OK, HTML::index)
            }
            get("helloworld"){
                call.respondText { "helloworld" }
            }
        }
    }.start(wait = true)

}

image.png

如果返回的数据需要通过大量计算得出,那我们最好抽出一个方法,就像下面这样。

fun helloworld(): String {
    var list = arrayListOf<String>()
    for (index in 1..10) {
        list.add("第:" + index);
    }

    return list.toString();
}

fun main() {
    embeddedServer(Netty, port = 8080, host = "127.0.0.1") {
        routing {
            get("helloworld") {
                call.respondText { helloworld() }
            }
        }
    }.start(wait = true)

}

HTML模板

只返回字符明显是不够的,我们还要显示一个完整的网页给用户,这里使用Apache FreeMarker,他是一个JVM 上的模板引擎。

但是集成他我踩了很多坑,不像SpringBoot那样简单,更何况现在网上多这样的资料少之又少,所以无处参考,摸索了很长时间才摸索出来。

第一次应该是版本问题,启动报错,后来换了ktor版本和kotlin版本才解决,报错信息如下。

io.ktor.server.engine.ApplicationEngineEnvironmentReloading.getDevelopmentMode()Z

Ktor是一个框架,可以构建Web应用程序,HTTP服务,移动和浏览器应用程序。IDEA中搜索ktor插件,安装好之后,在新建项目的时候,左边就会出现ktor,可以像创建SpringBoot项目一样,选择项目所需要用到的到依赖,但是这里我并没有用这个插件,其实手动加几个依赖道理也是一样的。

另外推荐使用ktor插件,下面是创建项目时候的引导页。另外也可以使用Tomcat

image.png

第二个问题是ClassNotFoundException,这个错误是类未找到,kotlin编译后的类名会在类名后面加上Kt,所以在application.conf文件下要写清楚。

Exception in thread "main" java.lang.ClassNotFoundException: Module function cannot be found for the fully qualified name 'com.hxl.Server.module'

以下是application.conf中内容,7070告诉Netty启动7070端口,modules的具体作用是什么不太清楚,反正按照官网的意思是要指定一个函数的入口。另外官网对他的解释大概是这样的:由于Ktor支持两种创建服务器的方法,一种是使用EmbeddedServer方法,一种是EngineMain,如果使用EngineMain启动服务器,则Ktor将从应用程序资源中的application.conf文件中加载配置设置,该文件至少包含要使用到的属性,这个属性就是用于指定要加载的模块ktor.application.modules。

application.conf位于项目的resources下,内容如下。

ktor {
    deployment {
        port = 7070
    }
    application {
          modules = [ com.hxl.ServerKt.module ]
    }
}

所以要在application.con中配置,全是乌龟的屁股,龟锭!!

首先加入依赖。

dependencies {
    testImplementation(kotlin("test-junit"))
    implementation("io.ktor:ktor-server-netty:1.5.4")
    implementation("io.ktor:ktor-html-builder:1.4.0")
    implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.2")
    implementation ("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.32")
    implementation("io.ktor:ktor-freemarker:1.5.4")
}
package com.hxl

import freemarker.cache.*
import io.ktor.application.*
import io.ktor.freemarker.*
import io.ktor.response.*
import io.ktor.routing.*

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)


fun Application.module(testing: Boolean = false) {
    install(FreeMarker) {
        templateLoader = ClassTemplateLoader(this::class.java.classLoader, "templates")
    }
    routing {
        get("/index") {
            val sampleUser = User(1, "艾克")
            call.respond(FreeMarkerContent("index.ftl", mapOf("user" to sampleUser)))
        }
    }
}

data class User(val id: Int, val name: String)

上面Application.module按Kotlin的语法应该是个扩展函数。

然后新建resources/templates/index.ftl文件,内容如下:

<html>
<body>
<h1>Hello, ${user.name}!</h1>
</body>
</html>

用过jsp、thyemleaf等模板引擎的对上面语法应该不陌生,就不解释了。剩下的工作就是编写html/css,以及用各种FreeMarker指令渲染了。

image.png

静态路由

许多应用程序都需要提供文件,例如css,javascript,图像等,Ktor借助static功能简化了整个过程。

可以使用static+staticRootFolder+files实现,如果想使用绝对路径,就需要使用staticRootFolder定义,如果想使用相对路径,可以直接使用files()定义,这个相对是相对的项目路径。

比如下面定义前缀static,绝对地址是系统的/home/HouXinLin/test路径,但是光使用staticRootFolder不行,没找到原因,可能也是乌龟的屁股吧,必须在下面加一个files,所以最终的完整路径是/home/HouXinLin/test/public,这样在浏览器访问/static/xxx.xx就可以访问此路径的文件。

package com.hxl

import freemarker.cache.*
import io.ktor.application.*
import io.ktor.freemarker.*
import io.ktor.http.content.*
import io.ktor.routing.*
import io.ktor.http.content.staticRootFolder;
import java.io.File

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

fun Application.module(testing: Boolean = false) {
    routing {
        static("/static") {
            staticRootFolder = File("/home/HouXinLin/test")
            files("public")
        }
    }
}

image.png

其实还有更加灵活的功能,比如加载的默认文件、压缩等。

HTML DSL

剩下最后了,使用DSL写HTML,我们可以在Kotlin中编写纯HTML,将变量值放到HTML内容中,甚至可以使用模板构建复杂的HTML布局,比如下面这一段。

get("/") {
    val name = "艾克"
    call.respondHtml {
        head {
            title {
                +name
            }
        }
        body {
            h1 {
                +"Hello $name!"
            }
        }
    }
}

实际就会给浏览器发送以下内容

<head>
    <title>艾克</title>
</head>
<body>
    <h1>Hello 艾克!</h1>
</body>

并且,还可以增加css

get("/styles.css") {
    call.respondCss {
        body {
            backgroundColor = Color.darkBlue
            margin(0.px)
        }
        rule("h1.page-title") {
            color = Color.white
        }
    }
}

来一个完整的例子,在这个例子中,首先定义了一个路由styles.css,他返回的css中会设置body的颜色是黑色,h1的字体是白色。另一个路由是返回一个html,头部标签中引入了styles.css,并且body中只有一个h1元素。

但是首先要加入以下依赖。

repositories {
    jcenter()
    mavenCentral()
    maven ( url = "https://kotlin.bintray.com/ktor")
    maven ( url = "https://kotlin.bintray.com/kotlin-js-wrappers")

}
dependencies {
    testImplementation(kotlin("test-junit"))
    implementation("io.ktor:ktor-server-netty:1.5.4")
    implementation("io.ktor:ktor-html-builder:1.4.0")
    implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.2")
    implementation("org.jetbrains:kotlin-css-jvm:1.0.0-pre.31-kotlin-1.2.41")
    implementation ("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.32")
}

package com.hxl

import freemarker.cache.*
import io.ktor.application.*
import io.ktor.freemarker.*
import io.ktor.html.*
import io.ktor.http.*
import io.ktor.routing.*
import io.ktor.response.*
import kotlinx.css.*
import kotlinx.html.body
import kotlinx.html.h1
import kotlinx.html.head
import kotlinx.html.link

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)
suspend inline fun ApplicationCall.respondCss(builder: CSSBuilder.() -> Unit) {
    this.respondText(CSSBuilder().apply(builder).toString(), ContentType.Text.CSS)
}


fun Application.module(testing: Boolean = false) {
    routing {
        get("/styles.css") {
            call.respondCss {
                body {
                    backgroundColor = kotlinx.css.Color.black
                    margin(0.px)
                }
                rule("h1.title") {
                    color = kotlinx.css.Color.white
                }
            }
        }
        get("/") {
            call.respondHtml {
                head {
                    link(rel = "stylesheet", href = "/styles.css", type = "text/css")
                }
                body {
                    h1(classes = "title") {
                        +"艾克!"
                    }
                }
            }
        }
    }
}

image.png

其他高深的操作还得待挖掘,毕竟他有很大的灵活性。