Spring Boot 使用 Kotlin Script Template 模板引擎kts 开发web应用

Spring Boot 使用 Kotlin Script Template 模板引擎kts 开发web应用

在 Spring Framework 5.0 M4 中引入了一个专门的Kotlin支持。

Kotlin Script based templates

ScriptTemplateView

从4.3版本开始,Spring Framework提供了一个 org.springframework.web.servlet.view.script.ScriptTemplateView 使用支持 JSR-223 的脚本引擎来渲染模板。

...

/**
 * An {@link AbstractUrlBasedView} subclass designed to run any template library
 * based on a JSR-223 script engine.
 *
 * <p>If not set, each property is auto-detected by looking up a single
 * {@link ScriptTemplateConfig} bean in the web application context and using
 * it to obtain the configured properties.
 *
 * <p>Nashorn Javascript engine requires Java 8+, and may require setting the
 * {@code sharedEngine} property to {@code false} in order to run properly. See
 * {@link ScriptTemplateConfigurer#setSharedEngine(Boolean)} for more details.
 *
 * @author Sebastien Deleuze
 * @author Juergen Hoeller
 * @since 4.2
 * @see ScriptTemplateConfigurer
 * @see ScriptTemplateViewResolver
 */
public class ScriptTemplateView extends AbstractUrlBasedView {

    public static final String DEFAULT_CONTENT_TYPE = "text/html";

    private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    private static final String DEFAULT_RESOURCE_LOADER_PATH = "classpath:";


    private static final ThreadLocal<Map<Object, ScriptEngine>> enginesHolder =
            new NamedThreadLocal<>("ScriptTemplateView engines");


    private ScriptEngine engine;

    private String engineName;

    private Locale locale;

    private Boolean sharedEngine;

    private String[] scripts;

    private String renderObject;

    private String renderFunction;

    private Charset charset;

    private String[] resourceLoaderPaths;

    private ResourceLoader resourceLoader;

    private volatile ScriptEngineManager scriptEngineManager;

...

}

这样我们就可以使用 kotlinx.html DSL或简单的Kotlin multiline String插值,编写kts类型安全模板。例如:

import com.easy.kotlin.kotlin_script_template.*

"""
${include("header")}
<p>Locale:
<a href="/?locale=fr">FR</a> |
<a href="/?locale=en">EN</a> |
<a href="/?locale=zh">中文</a>

</p>
<h1>${i18n("title")}</h1>
${include("users", mapOf(Pair("users", users)))}
${include("footer")}
"""


新建一个SpringBoot + Kotlin 工程

首先,我们新建一个SpringBoot + Kotlin 工程,使用对应的版本分别是

  • kotlinVersion = '1.1.2'
  • springBootVersion = '2.0.0.BUILD-SNAPSHOT'

使用gradle构建,配置如下:

buildscript {
    ext {
        kotlinVersion = '1.1.2'
        springBootVersion = '2.0.0.BUILD-SNAPSHOT'
    }
    repositories {
        mavenCentral()
        maven { url "http://dl.bintray.com/kotlin/kotlin-eap-1.1" }
        maven { url "https://repo.spring.io/snapshot" }
        maven { url "https://repo.spring.io/milestone" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
        classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
    }
}

apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'org.springframework.boot'

jar {
    baseName = 'kotlin-script-templating'
    version = '0.0.1-SNAPSHOT'
}

repositories {
    mavenCentral()
    maven { url "http://dl.bintray.com/kotlin/kotlin-eap-1.1" }
    maven { url "https://repo.spring.io/snapshot" }
    maven { url "https://repo.spring.io/milestone" }
}


dependencies {
    compile("org.springframework.boot:spring-boot-starter-web:${springBootVersion}")
    compile("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
    compile("org.jetbrains.kotlin:kotlin-compiler:${kotlinVersion}")
    compile("org.jetbrains.kotlin:kotlin-script-util:${kotlinVersion}") {
        exclude group: "com.jcabi", module: "jcabi-aether"
        exclude group: "org.apache.maven", module: "maven-core"
        exclude group: "org.sonatype.aether", module: "aether-api"
    }
    testCompile("org.springframework.boot:spring-boot-starter-test:${springBootVersion}")
}

工程目录结构

工程目录结构如下:

.
├── README.md
├── build
│   └── kotlin-build
│       └── caches
│           └── version.txt
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── src
    ├── main
    │   ├── java
    │   ├── kotlin
    │   │   └── com
    │   │       └── easy
    │   │           └── kotlin
    │   │               └── kotlin_script_template
    │   │                   ├── Application.kt
    │   │                   ├── Helpers.kt
    │   │                   ├── User.kt
    │   │                   └── ViewController.kt
    │   └── resources
    │       ├── META-INF
    │       │   └── services
    │       │       └── javax.script.ScriptEngineFactory
    │       ├── application.properties
    │       ├── banner.txt
    │       ├── messages.properties
    │       ├── messages_en.properties
    │       ├── messages_fr.properties
    │       ├── messages_zh.properties
    │       ├── scripts
    │       │   └── render.kts
    │       └── templates
    │           ├── footer.kts
    │           ├── header.kts
    │           ├── index.kts
    │           ├── user.kts
    │           └── users.kts
    └── test
        ├── java
        ├── kotlin
        │   └── com
        │       └── easy
        │           └── kotlin
        │               └── kotlin_script_template
        │                   └── ApplicationTests.kt
        └── resources

26 directories, 25 files

配置脚本解析引擎的实现类

在META-INF/services/javax.script.ScriptEngineFactory文件里面加上其脚本解析引擎的实现类

org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory

其中,完成kts模板文件编译解析的类是KotlinJsr223JvmLocalScriptEngineFactory,这个类在kotlin-script-util包里。
因为多了这一层编译解析,感觉速度慢了点。

配置Script Template Engine :


@SpringBootApplication
class Application : WebMvcConfigurerAdapter() {


    /**
     * ScriptTemplateConfigurer
     * scripts/render.kts
     */
    @Bean
    fun kotlinScriptConfigurer(): ScriptTemplateConfigurer {
        val configurer = ScriptTemplateConfigurer()
        configurer.engineName = "kotlin"
        configurer.setScripts("scripts/render.kts")
        configurer.renderFunction = "render"
        configurer.isSharedEngine = false
        return configurer
    }

    @Bean
    fun kotlinScriptViewResolver(): ViewResolver {
        val viewResolver = ScriptTemplateViewResolver()
        viewResolver.setPrefix("templates/")
        viewResolver.setSuffix(".kts")
        return viewResolver
    }

    @Bean
    fun localeResolver() = SessionLocaleResolver().apply {
        setDefaultLocale(Locale.CHINESE)
    }

    @Bean
    fun localeChangeInterceptor() = LocaleChangeInterceptor()

    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(localeChangeInterceptor())
    }

}

其中,LocaleChangeInterceptor() 是Spring框架里面定义的类

org.springframework.web.servlet.i18n.LocaleChangeInterceptor。

public class LocaleChangeInterceptor extends HandlerInterceptorAdapter {

    /**
     * Default name of the locale specification parameter: "locale".
     */
    public static final String DEFAULT_PARAM_NAME = "locale";


    protected final Log logger = LogFactory.getLog(getClass());

    private String paramName = DEFAULT_PARAM_NAME;

    private String[] httpMethods;

    private boolean ignoreInvalidLocale = false;

    private boolean languageTagCompliant = false;

...

从上面的源码,我们可以看出它的默认query参数是locale

渲染脚本render.kts

其中,render.kts代码如下:

import org.springframework.web.servlet.view.script.RenderingContext
import org.springframework.context.support.ResourceBundleMessageSource
import javax.script.*
import org.springframework.beans.factory.getBean

// TODO Use engine.eval(String, Bindings) when https://youtrack.jetbrains.com/issue/KT-15450 will be fixed
fun render(template: String, model: Map<String, Any>, renderingContext: RenderingContext): String {
    val engine = ScriptEngineManager().getEngineByName("kotlin")
    val bindings = SimpleBindings(model)
    var messageSource = renderingContext.applicationContext.getBean<ResourceBundleMessageSource>()
    bindings.put("i18n", { code: String -> messageSource.getMessage(code, null, renderingContext.locale) })
    bindings.put("include", { path: String -> renderingContext.templateLoader.apply("templates/$path.kts") })
    engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE)
    return engine.eval(template) as String
}

模板绑定 ScriptTemplateWithBindings

我们上面在bindings里面添加了

bindings.put("i18n", { code: String -> messageSource.getMessage(code, null, renderingContext.locale) })

在index.kts这样调用

import com.easy.kotlin.kotlin_script_template.*

"""
${include("header")}
<p>Locale:
<a href="/?locale=fr">FR</a> |
<a href="/?locale=en">EN</a> |
<a href="/?locale=zh">中文</a>

</p>
<h1>${i18n("title")}</h1>
${include("users", mapOf(Pair("users", users)))}
${include("footer")}
"""

这个i18n在ScriptTemplateWithBindings类里的扩展函数如下

package com.easy.kotlin.kotlin_script_template

import javax.script.ScriptContext
import javax.script.ScriptEngineManager
import javax.script.SimpleBindings
import kotlin.script.templates.standard.ScriptTemplateWithBindings

fun ScriptTemplateWithBindings.include(path: String, model: Map<String, Any>? = null) :String {
    val engine = ScriptEngineManager().getEngineByName("kotlin")
    var includeBindings = if (model != null) {
        val b = SimpleBindings(LinkedHashMap(model))
        b["include"] = bindings["include"]
        b["i18n"] = bindings["i18n"]
        b
    } else {
        val b = SimpleBindings(bindings)
        b.remove("kotlin.script.history")
        b
    }

    engine.setBindings(includeBindings, ScriptContext.ENGINE_SCOPE)
    val template = (bindings["include"] as (String) -> String).invoke(path)
    return engine.eval(template) as String
}

fun ScriptTemplateWithBindings.i18n(code: String) =
    (bindings["i18n"] as (String) -> String).invoke(code)

fun <T> Iterable<T>.joinToLine(function: (foo: T) -> String): String
  { return joinToString(separator = "\n") { foo -> function.invoke(foo) } }

var ScriptTemplateWithBindings.users: List<User>
  get() = bindings["users"] as List<User>
  set(value) { throw UnsupportedOperationException()}

var ScriptTemplateWithBindings.user: User
  get() = bindings["user"] as User
  set(value) { throw UnsupportedOperationException()}

var ScriptTemplateWithBindings.title: String
  get() = bindings["title"] as String
  set(value) { throw UnsupportedOperationException()}

写实体类Usr,Controller类,最后写Spring Boot main函数:

fun main(args: Array<String>) {
    SpringApplication.run(Application::class.java, *args)
}

运行测试

命令行运行

gradle bootRun

启动运行,访问:http://localhost:8080/

你将看到如下输出页面

螢幕快照 2017-06-05 00.13.24.png
螢幕快照 2017-06-05 00.14.04.png

小结

本章节工程源代码:

https://github.com/EasyKotlin/kotlin-script-templating

使用Kotlin编写Spring Boot应用程序越多,我们越觉得这两种技术有着共同的目标,让我们广大程序员可以使用

  • 富有表达性
  • 简短
  • 可读的代码

来更高效地编写应用程序,而Spring Framework 5 Kotlin支持将这些技术以更加自然,简单和强大的方式来展现给我们。

Kotlin可以用来编写 基于注解的Spring Boot应用程序 ,但作为一种新的 functional and reactive applications 也将是一种很好的尝试,期待未来Spring Framework 5.0 和 Kotlin 结合的开发实践。

参考资料

https://github.com/sdeleuze/kotlin-script-templating

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容