前言
前几天升级了新版本的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)
}
复制代码
如果返回的数据需要通过大量计算得出,那我们最好抽出一个方法,就像下面这样。
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
第二个问题是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指令渲染了。
静态路由
许多应用程序都需要提供文件,例如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")
}
}
}
复制代码
其实还有更加灵活的功能,比如加载的默认文件、压缩等。
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") {
+"艾克!"
}
}
}
}
}
}
复制代码
其他高深的操作还得待挖掘,毕竟他有很大的灵活性。