8.4 Spring Boot集成Kotlin混合Java开发

时间:2022-06-07
本文章向大家介绍8.4 Spring Boot集成Kotlin混合Java开发,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

8.4 Spring Boot集成Kotlin混合Java开发

本章介绍Spring Boot集成Kotlin混合Java开发一个完整的spring boot应用:Restfeel,一个企业级的Rest API接口测试平台(在开源工程restfiddle[1]基础上开发而来)。

系统技术框架

编程语言:Java,Kotlin

数据库:Mongo

Spring框架:Spring data jpa,Spring data mongodb

前端:jquery,requireJS,

工程构建工具:Gradle

Kotlin简介

Kotlin是一种优雅的语言,是JetBrains公司开发的静态类型JVM语言,与Java无缝集成。与Java相比,Kotlin的语法更简洁、更具表达性,而且提供了更多的特性,比如,高阶函数、操作符重载、字符串模板。它与Java高度可互操作,可以同时用在一个项目中。这门语言的目标是:

  • 创建一种兼容Java的语言
  • 编译速度至少同Java一样快
  • 比Java更安全,能够静态检测常见的陷阱。如:引用空指针
  • 比Java更简洁,通过支持 variable type inference,higher-order functions (closures),extension functions,mixins and first-class delegation 等实现。
  • 比最成熟的竞争者Scala还简单

Kotlin不像Scala另起炉灶,将类库,尤其是集合类都自己来了一遍。kotlin是对现有java的增强,通过扩展方法给java提供了很多诸如fp之类的特性,但同时始终保持对java的兼容。这也是是kotlin官网首页重点强调的:

100% interoperable with Java™。

Kotlin和Scala很像,对于用惯了Scala的人来说用起来很顺手,对于喜欢函数式的开发者,Kotlin是个不错的选择。有个小点,像Groovy动态类型就不用说了,Scala函数最后返回值都可以省去return,但是不晓得为啥Kotlin对return情有独钟。

另外,Kotlin可以编译成Java字节码,也可以编译成JavaScript,在没有JVM的环境运行。

Kotlin创建类的方式与Java类似,比如下面的代码创建了一个有三个属性的Person类:

class Person{
    var name: String = ""
    var age: Int = 0
    var sex: String? = null
}

可以看到,Kotlin的变量声明方式略有些不同。在Kotline中,声明变量必须使用关键字var,而如果要创建一个只读/只赋值一次的变量,则需要使用val代替它。

Kotlin对函数式编程的实现恰到好处

一个函数指针的例子:

/**
 * "Callable References" or "Feature Literals", i.e. an ability to pass
 * named functions or properties as values. Users often ask
 * "I have a foo() function, how do I pass it as an argument?".
 * The answer is: "you prefix it with a `::`".
 */

fun main(args: Array<String>) {
    val numbers = listOf(1, 2, 3)
    println(numbers.filter(::isOdd))
}


fun isOdd(x: Int) = x % 2 != 0

运行结果: [1, 3]

再看一个复合函数的例子。看了下面的复合函数的例子,你会发现Kotlin的FP的实现相当简洁。(跟纯数学的表达式,相当接近了)

/**
 * The composition function return a composition of two functions passed to it:
 * compose(f, g) = f(g(*)).
 * Now, you can apply it to callable references.
 */

fun main(args: Array<String>) {
    val oddLength = compose(::isOdd, ::length)
    val strings = listOf("a", "ab", "abc")
    println(strings.filter(oddLength))
}

fun isOdd(x: Int) = x % 2 != 0
fun length(s: String) = s.length

fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
    return { x -> f(g(x)) }
}

运行结果: [a,abc]

简单说明下
val oddLength = compose(::isOdd, ::length)
    val strings = listOf("a", "ab", "abc")
    println(strings.filter(oddLength))

这就是数学中,复合函数的定义:

h = h(f(g))

g: A->B
f: B->C
h: A->C

g(A)=B
h(A) = f(B) = f(g(A)) = C

只是代码中的写法是:

h=compose( f, g )
h=compose( f(g(A)), g(A) )

关于Kotlin,官网有一个非常好的交互式Kotlin学习教程:

http://try.kotlinlang.org/

想深入语言实现细节的,可以直接参考github源码[3]。

Spring Boot集成 Kotlin

1.build.gradle中添加kotlin相关依赖

使用插件

apply {
    plugin "kotlin"
    plugin "kotlin-spring"
    plugin "kotlin-jpa"
    plugin "org.springframework.boot"
    plugin 'java'
    plugin 'eclipse'
    plugin 'idea'
    plugin 'war'
    plugin 'maven'
}

构建时插件依赖

buildscript {
    ext {
        kotlinVersion = '1.1.0'
        springBootVersion = '1.5.2.RELEASE'
    }

    dependencies {
        classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
        classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlinVersion"
        classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlinVersion"
    }

    repositories {
        mavenLocal()
        mavenCentral()
        maven { url "http://oss.jfrog.org/artifactory/oss-release-local" }
        maven { url "http://jaspersoft.artifactoryonline.com/jaspersoft/jaspersoft-repo/" }
        maven { url "https://oss.sonatype.org/content/repositories/snapshots" }

    }
}

编译时jar包依赖

dependencies {
    compile("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
    compile("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
    compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.8.4")

    ...

}
2.配置源码目录sourceSets
sourceSets {
    main {
        kotlin { srcDir "src/main/kotlin" }
        java { srcDir "src/main/java" }
    }
    test {
        kotlin { srcDir "src/test/kotlin" }
        java { srcDir "src/test/java" }
    }
}
```

指定kotlin,java源码放置目录。让java的归java,Kotlin的归Kotlin。这样区分开来。这个跟scala的插件实现方式上有点区别。scala的插件,是允许你把scala,java代码随便放,插件会自动寻找scala代码编译。


整个工程大目录如下图所示:



![](http://upload-images.jianshu.io/upload_images/1233356-20285f52eb50f486.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)




#####3.写Kotlin代码

Entity实体类

```
package com.restfeel.entity

import org.bson.types.ObjectId
import org.springframework.data.mongodb.core.mapping.Document
import java.util.*
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
import javax.persistence.Version

@Document(collection = "blog") // 如果不指定collection,默认遵从命名规则
class Blog {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    var id: String = ObjectId.get().toString()
    @Version
    var version: Long = 0
    var title: String = ""
    var content: String = ""
    var author: String = ""
    var gmtCreated: Date = Date()
    var gmtModified: Date = Date()
    var isDeleted: Int = 0 //1 Yes 0 No
    var deletedDate: Date = Date()
    override fun toString(): String {
        return "Blog(id='$id', version=$version, title='$title', content='$content', author='$author', gmtCreated=$gmtCreated, gmtModified=$gmtModified, isDeleted=$isDeleted, deletedDate=$deletedDate)"
    }

}


```

Service类

```
package com.restfeel.service

import com.restfeel.entity.Blog
import org.springframework.data.mongodb.repository.MongoRepository
import org.springframework.data.mongodb.repository.Query
import org.springframework.data.repository.query.Param

interface BlogService : MongoRepository<Blog, String> {

    @Query("{ 'title' : ?0 }")
    fun findByTitle(@Param("title") title: String): Iterable<Blog>

}

```


Controller类

```
package com.restfeel.controller

import com.restfeel.entity.Blog
import com.restfeel.service.BlogService
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.context.annotation.ComponentScan
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.stereotype.Controller
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseBody
import java.util.*
import javax.servlet.http.HttpServletRequest

/**
 * 文章列表,写文章的Controller
 * @author Jason Chen  2017/3/31 01:10:16
 */

@Controller
@EnableAutoConfiguration
@ComponentScan
@Transactional(propagation = Propagation.REQUIRES_NEW)
class BlogController(val blogService: BlogService) {
    @GetMapping("/blogs.do")
    fun listAll(model: Model): String {
        val authentication = SecurityContextHolder.getContext().authentication
        model.addAttribute("currentUser", if (authentication == null) null else authentication.principal as UserDetails)
        val allblogs = blogService.findAll()
        model.addAttribute("blogs", allblogs)
        return "jsp/blog/list"
    }

    @PostMapping("/saveBlog")
    @ResponseBody
    fun saveBlog(blog: Blog, request: HttpServletRequest):Blog {
        blog.author = (request.getSession().getAttribute("currentUser") as UserDetails).username
        return blogService.save(blog)
    }

    @GetMapping("/goEditBlog")
    fun goEditBlog(@RequestParam(value = "id") id: String, model: Model): String {
        model.addAttribute("blog", blogService.findOne(id))
        return "jsp/blog/edit"
    }

    @PostMapping("/editBlog")
    @ResponseBody
    fun editBlog(blog: Blog, request: HttpServletRequest) :Blog{
        blog.author = (request.getSession().getAttribute("currentUser") as UserDetails).username
        blog.gmtModified = Date()
        blog.version = blog.version + 1
        return blogService.save(blog)
    }

    @GetMapping("/blog")
    fun blogDetail(@RequestParam(value = "id") id: String, model: Model): String {
        model.addAttribute("blog", blogService.findOne(id))
        return "jsp/blog/detail"
    }

    @GetMapping("/listblogs")
    @ResponseBody
    fun listblogs(model: Model) = blogService.findAll()

    @GetMapping("/findBlogByTitle")
    @ResponseBody
    fun findBlogByTitle(@RequestParam(value = "title") title: String) = blogService.findByTitle(title)

}

```



对应的前端代码,这里就不多说了。可以参考工程源代码[4]。



SpringBoot的启动类: 我们用Kotlin写SpringBoot的启动类:

```
package com.restfeel

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.SpringApplication
import org.springframework.core.env.Environment

/**
 * Created by jack on 2017/3/29.
 * @author jack
 * @date 2017/03/29
 */
@RestFeelBoot
class RestFeelApplicationKotlin : CommandLineRunner {
    @Autowired
    private val env: Environment? = null

    override fun run(vararg args: String?) {
        println("RESTFEEL 启动完毕")
        println("应用地址:" + env?.getProperty("application.host-uri"))
    }
}

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



```



其中,@RestFeelBoot是自定义注解,代码如下:

```
package com.restfeel;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
 * Created by jack on 2017/3/23.
 *
 * @author jack
 * @date 2017/03/23
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Configuration
@EnableAutoConfiguration
@ComponentScan
public @interface RestFeelBoot {
    Class<?>[] exclude() default {};
}

```


#####运行测试

命令行输入gradle bootRun,运行应用。 访问 http://127.0.0.1:5678/blogs.do 

文章列表
![](http://upload-images.jianshu.io/upload_images/1233356-365b79713f187cc3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)




写文章

![](http://upload-images.jianshu.io/upload_images/1233356-87929f0b83a43ad8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

阅读文章



![](http://upload-images.jianshu.io/upload_images/1233356-4b12e505588c6b3f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)




##小结

本章示例工程源代码:

https://github.com/Jason-Chen-2017/restfeel/tree/restfeel_kotlin_2017.5.6




参考资料:
1.https://github.com/AnujaK/restfiddle
2.http://www.jianshu.com/c/498ebcfd27ad
3.https://github.com/JetBrains/kotlin
4.https://github.com/Jason-Chen-2017/restfeel/tree/restfeel_kotlin_2017.5.6