Spring开发,从Kotlin开始

引言

使用Kotlin进行Spring开发已经有几年了,当初SSM的起步学习也是看Java教程然后用Kotlin对应来搭建学习,发现很融洽后,后面就一直用Kotlin在后端进行开发,其中也尝试过ktor和WebFlux这类新兴框架和新技术,在尝试后,kotlin在后端的优势也是更加的显而易见,站在巨人肩膀上的语言确实能带给人更多的乐趣,为了更好推动kotlin的后端发展,觉得有必要写一篇这种引导文章给对kotlin感兴趣的后端开发者们,欢迎更多人了解和尝试kotlin的后端的使用。

环境说明

由于是纯Kotlin项目,所以不打算用Maven构建环境,而是基于Gradle构建。

  • IDEA: 2025.1.1.1
  • Gradle: 8.10
  • JDK: 17
  • MySQL: 8.0.30

环境只是参考,能自己配置对应合适的即可,不了解Gradle也无伤大雅,知道哪个部分是干嘛的就行,后续可以单独学习,不影响项目搭建。

重新SSM的从零搭建

1. 启动IDEA

作为一个IDEA高度依赖患者,为了起步方便我们直接在IDEA中创建我们的Kotlin项目来改造为SSM项目即可

  1. 我们选择Kotlin项目创建即可,构建系统记得选Gradle,然后Gradle的DSL我们选择Kotlin(毕竟纯Kotlin项目)。一定别选错了Kotlin和Groovy这两玩意语法是不同的!!!
  2. JDK版本建议新一点17即可,因为们会用Spring6和较新的Kotlin版本。
  3. 你可以展开下面的advanced settings来设置你用于构建的Gradle所在位置,避免它用默认的gradle下载连接和版本让你等很久。

1.1 自定义我们的Gradle

如果设置了advanced settings可以跳过,当然也可以学习下用于以后别的Gardle项目的配置

设置本地Gradle

如果没advanced settings设置也没关系,我们可以在项目创建后停止下载然后在IDEA菜单的File->Settings中来进行配置

这里的Distribution默认可能是wapper,也就是用的gradle-wrapper.properties中的默认下载连接,切换为我们的local installtion就能选择我们本地的gradle了,

修改下载的镜像

如果你不想手动下载一个Gradle并且解压来指定 ,也能配置我们的gradle-wrapper.properties来切换国内的镜像地址来使用

设置我们的distributionUrl为国内阿里云镜像源https\://mirrors.aliyun.com/macports/distfiles/gradle/gradle-8.10-bin.zip然后重写同步即可

你也可以选择单独使用Gradle进行Init来创建搭建,可能就没使用IDEA这么的方便,不过结果都是一样的都是基于Gradle项目来构建项目

1.2 简单认识一下我们的项目核心文件

下列文件都删减过只留了核心部分

build.gradle.kts

相当于 Maven 的 pom.xml ,我们是一个单体项目不是多模块的所以我们用全局build.gradle.kts就能配置我们的项目构建和依赖相关内容,如果是多模块项目每个模块那么每个模块都有自己的build.gradle.kts,然后可以从全局build.gradle.kts去集成一些公共构建配置和依赖

kotlin 复制代码
//plugins是Gradle的核心部分,通过不同的plugins我们能得到许多的构建任务来让我们进行相关的使用
plugins {
    //使用用于构建kotlin2.1.20的插件 
    kotlin("jvm") version "2.1.20"
}

// 配置项目使用的仓库,这里使用 Maven Central 作为依赖库来源
repositories {
    mavenCentral()
}

// 声明项目的依赖关系,这里添加了 Kotlin 测试框架作为测试依赖
dependencies {
    testImplementation(kotlin("test"))
}

// 配置测试任务,指定使用 JUnit Platform 作为测试平台
tasks.test {
    useJUnitPlatform()
}

// 配置 Kotlin 编译目标为 JVM,并指定使用的 Java 版本为 17
kotlin {
    jvmToolchain(17)
}
settings.gradle.kts

用于设置我们整个项目的多模块相关的依赖关系和引入,主要用于声明子模块,我们目前单体项目所以没有别的模块引用,只有一个项目名称的设置,Gradle会识别src作为项目的源文件入口不用额外配置。

kotlin 复制代码
rootProject.name = "SSMBuild"
gralde->gradle-wrapper.properties

也就是我们刚刚说的Gradle构建工具的相关配置,比如上面我们会通过这里去修改Gradle的下载源

2. Hello World

在src->main->kotlin下创建我们的包和Main.kt文件 写一个简单的hello world

kotlin 复制代码
fun main() {
    println("Hello World!")
}

在我们的main左边会有一个绿色的运行按钮,直接点击就可以直接运行。

也可以通过application插件去指定我们的main入口所在位置去手动运行,这里我们就不用过多篇幅去说application插件的使用

3. 引入SSM相关依赖

在项目的build.gradle.kts中的dependencies{......}写入相关的依赖

kotlin 复制代码
plugins {
    //这里的kotlin版本是项目默认的,没进行修改,无影响,可自行查阅jdk版本来修改
    kotlin("jvm") version "2.1.20"
}

repositories {
    mavenCentral()
}

dependencies {
    //我们只引入核心框架,如果觉得版本太高可以自行查阅进行修改,此项目基于spring6x jdk17配置
    //Spring-WebMVC
    //implementation("org.springframework:spring-webmvc:6.1.20")
    //也可以选择分开其中的依赖信息进行引入,而不是直接用:连起来,规则实际上和Maven是一样的
    implementation("org.springframework", "spring-webmvc", "6.1.7")
    //整个DataSource的连接实现
    implementation("org.springframework:spring-jdbc:6.1.7")
    // MyBatis
    implementation("org.mybatis:mybatis-spring:3.0.3")
    implementation("org.mybatis:mybatis:3.5.13")
    // Json库,Jackson有Kotlin的扩展我们直接使用即可,不会出现反射找不到默认空构造函数问题
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.16.0")
    //数据库驱动,使用runtimeOnly表明不参与编译,只在运行时使用,避免依赖污染和增大包体积
    runtimeOnly("com.mysql:mysql-connector-j:8.4.0")
    // 内置Jetty
    implementation("org.eclipse.jetty:jetty-server:11.0.15")
    implementation("org.eclipse.jetty:jetty-servlet:11.0.15")
    //加个SLF4J的实现,避免jetty警告,也为了看日志
    implementation("ch.qos.logback:logback-classic:1.4.14")
    //测试依赖引入,对应maven的<scope>test</scope>,避免打包到项目中
    testImplementation("junit:junit:4.13.2")
    testImplementation("org.springframework:spring-test:6.1.7")
}

tasks.test {
    useJUnitPlatform()
}

kotlin {
    jvmToolchain(17)
}

4. 创建一个简单的数据库

创建一个库叫ssm 创建一张user表

sql 复制代码
create table if not exists ssm.user
(
    user_id  int auto_increment comment '自增的用户ID'
        primary key,
    name     varchar(10) not null comment '用户名字',
    account  varchar(20) not null comment '账号20位长度',
    password char(32)    not null comment '密码MD5加密过,32位固定'
);

5. 创建项目文件

做一个简单的注册登录

项目的基础包已修改了为com.ssm请注意。实际也就构建工具不同,项目文件创建和Java的方式基本没区别,所以才说兼容性好,基本没太高的语言迁移成本。(项目分包纯为了快速,不具备参考性)

我们都用Spring6x了,所以不打算用XML进行开发,使用注解也方便配置,不折腾人.🤗

5.1 一个model包

在com.ssm中创建model包,存放一些实体类

  1. 创建了一个统一的响应对象,虽然没多少内容但是规范点还是好看的😎
kotlin 复制代码
//com.ssm.BaseRes
//ResponseEntity太长了,写个别名吧
typealias HttpRes<T> = ResponseEntity<BaseRes<T>>

data class BaseRes<T>(
    val message: String = "",
    val data: T? = null,
)
  1. 创建一个data class的User映射类,简单直观
kotlin 复制代码
//com.ssm.UserEntity
data class UserEntity(
    var userId: Int? = null,
    var name: String,
    var account: String,
    var password: String
)
  1. 写两个DTO用来接收注册和登录
kotlin 复制代码
//com.ssm.UserDTO
data class UserLoginDTO(
    var account: String,
    var password: String
)
data class UserRegisterDTO(
    var name: String,
    var account: String,
    var password: String
)
  1. 再来个查询的VO
kotlin 复制代码
//com.ssm.UserVO
data class UserSummaryVO(
    var userId: Int,
    var name: String,
    var account: String,
)

5.2 写个mapper操作表

在com.ssm中创建mapper包,进行eneity的映射操作

kotlin 复制代码
@Mapper
interface UserMapper {

    @Insert("INSERT INTO user(name,account,password) VALUES(#{name}, #{account}, #{password})")
    fun insertUser(user: UserEntity): Int

    @Select("SELECT * FROM user WHERE account = #{account}")
    fun getUserByAccount(account: String): UserEntity?

    @Select("SELECT * FROM user WHERE name = #{name}")
    fun getUserByName(name: String): UserEntity?

    @Select("SELECT * FROM user WHERE user_id = #{userId}")
    fun getUserById(userId: Int): UserEntity?
}

5.3 必不可少的service

创建个user的Service,不写啥impl了,多余的公式化操作了😇

kotlin 复制代码
//com.ssm.service.UserService
@Service
class UserService(private val userMapper: UserMapper) {

    fun register(userRegister: UserRegisterDTO): HttpRes<Boolean> {
        userMapper.getUserByAccount(userRegister.account)?.also {
            return ResponseEntity.status(HttpStatus.CONFLICT).body(BaseRes("账号已存在", false))
        }
        userMapper.getUserByName(userRegister.name)?.also {
            return ResponseEntity.status(HttpStatus.CONFLICT).body(BaseRes("用户名称已存在", false))
        }
        val encryptedPassword = md5(userRegister.password)

        return UserEntity(
            name = userRegister.name,
            account = userRegister.account,
            password = encryptedPassword
        ).let { newUser ->
            userMapper.insertUser(newUser)
            ResponseEntity.ok(BaseRes("注册成功", true))
        }
    }

    fun login(userLogin: UserLoginDTO): HttpRes<Int> {
        return userMapper.getUserByAccount(userLogin.account)?.let { user ->
            val encryptedPassword = md5(userLogin.password)
            if (user.password == encryptedPassword) {
                ResponseEntity.ok(BaseRes("登录成功", user.userId))
            } else {
                ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(BaseRes("密码错误", null))
            }
        } ?: ResponseEntity.status(HttpStatus.NOT_FOUND).body(BaseRes("用户不存在", null))
    }

    fun getUserById(userId: Int): HttpRes<UserSummaryVO> {
        return userMapper.getUserById(userId)?.let { user ->
            val userSummaryVO = UserSummaryVO(userId = user.userId!!, name = user.name, account = user.account)
            ResponseEntity.ok(BaseRes("查询成功", userSummaryVO))
        } ?: ResponseEntity.status(HttpStatus.NOT_FOUND).body(BaseRes("用户不存在"))
    }

    private fun md5(input: String): String {
        val md = MessageDigest.getInstance("MD5")
        val digest = md.digest(input.toByteArray())
        return digest.joinToString(separator = "") { "%02x".format(it) }
    }
}

5.4 别忘记写controller

创建一个简单的controller,就直接用函数响应的语法糖了

kotlin 复制代码
//com.ssm.controller.UserController
@RestController
@RequestMapping("/user")
class UserController(
    private val userService: UserService,
) {

    @PostMapping("/register")
    fun register(
        @RequestBody userRegisterDTO: UserRegisterDTO
    ): HttpRes<Boolean> = userService.register(userRegisterDTO)

    @PostMapping("/login")
    fun login(
        @RequestBody userLoginDTO: UserLoginDTO
    ): HttpRes<Int> = userService.login(userLoginDTO)

    @GetMapping("/{userId}")
    fun getUserById(
        @PathVariable userId: Int
    ): HttpRes<UserSummaryVO> = userService.getUserById(userId)
}

5.5 再来个AppConfig

写一个AppConfig来扫描我们上面的相关Bean和配置数据库连接

kotlin 复制代码
//com.ssm.config.AppConfig
/**
 * 用于定义 Spring 应用上下文中的核心配置。
 *
 * 标记为open,由于spring需要对配置类进行动态代理,但是Kotlin的class和函数默认是final的
 * 包含数据源配置、MyBatis工厂配置,并启用组件扫描以加载 Controller 和 Service.
 * 同时扫描 MyBatis Mapper 接口。
 */
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = ["com.ssm.controller", "com.ssm.service"])
@MapperScan("com.ssm.mapper")
open class AppConfig {

    /**
     * 配置数据源 Bean。
     *
     * 使用 DriverManagerDataSource 实现,适用于开发和测试环境,不推荐用于生产。
     *
     * @return 配置好的 [DataSource] 实例
     */
    @Bean
    open fun dataSource(): DataSource {
        val dataSource = DriverManagerDataSource()
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver")
        dataSource.url = "jdbc:mysql://localhost:3306/ssm?useSSL=false&serverTimezone=UTC"
        dataSource.username = "root"
        dataSource.password = "你自己的数据库密码"
        return dataSource
    }

    /**
     * 配置 MyBatis SqlSessionFactory。
     *
     * SqlSessionFactory 是 MyBatis 的核心对象,用于创建 SqlSession。
     *
     * @return 配置好的 [SqlSessionFactoryBean] 实例
     */
    @Bean
    open fun sqlSessionFactory(): SqlSessionFactoryBean {
        val sqlSessionFactoryBean = SqlSessionFactoryBean()
        sqlSessionFactoryBean.setDataSource(dataSource())
        sqlSessionFactoryBean.setTypeAliasesPackage("com.ssm.model") // 设置实体类包路径,用于别名映射
        return sqlSessionFactoryBean
    }
}

5.6 设置下日志库

src->main->resources下建一个logback.xml用来配置logback库,我们只需要一些核心的级别日志

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %cyan([%thread]) %green(%logger{36}) - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>

    <logger name="org.eclipse.jetty" level="WARN" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>

    <logger name="ch.qos.logback" level="WARN" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>

</configuration>

6. 启动启动!

创建程序入口,因为我们引入了Jetty依赖,所以就直接声明服务启动就行,用IDEA去配置外置的启动容器麻烦了(当然不是懒,是因为截图太占位置了),也为了后面的打包😀

kotlin 复制代码
//com.ssm.Main
fun main(): Unit = with(Server(1028)) {//创建服务监听1028端口
    // 1. 创建并配置 Spring 应用上下文
    val springContext = AnnotationConfigWebApplicationContext().apply {
        register(AppConfig::class.java) // 注册 AppConfig 作为 Spring 配置来源
    }
    // 2. 创建并配置 Servlet 上下文处理器
    handler = ServletContextHandler().apply {
        contextPath = "/" // 设置 Web 应用的上下文路径为根路径 "/"
        // 添加 ContextLoaderListener,初始化 Spring 容器
        addEventListener(ContextLoaderListener(springContext))
        // 添加 Spring MVC 的核心 DispatcherServlet,并映射到所有 URL ("/*")
        addServlet(ServletHolder(DispatcherServlet(springContext)), "/*")
    }
    // 3. 启动 Jetty 服务器并阻塞主线程
    start() // 启动 Jetty 服务器
    join()  // 阻塞当前线程,直到服务器停止
}

点击main左边的绿色按钮后查看下控制台输出

如果左边没启动识别可以右键文件去run,或去IDEA的Configuration手动配置启动

看看你的启动后的控制台是不是下图这样的(左边文件结构可以自己对照下)

看到我们的Completed initialization就证明我们启动成功了,接下来就是测下API了

7. 测下API

我们用ApiFox测试下这三个API,这里我就写了一个结果,其余的没问题,大家可以自行测试

避免冗余篇幅,单测可以自行编写试试,和Java的使用方式没任何区别

8. 来打一个Jar包

8.1 配置构建任务

我们现在成功运行后只是利用IDEA来构建运行,那么我们最终要独立出来运行肯定还是要打个包的, 细心的小伙伴肯定注意到IDEA右边Gradle部分的任务里面有一个Jar任务,当然那个任务我们需要自己再加以改造,这也是Gradle强大的地方,提供了很多预制的构建任务并且我们可以自定义内容。

build.gradle.kts文件中自定义jar包打包任务的运行规则,然后同步Gradle。

kotlin 复制代码
plugins{.......}
repositories{.......}
dependencies{.......}
tasks.jar {
    manifest {
        // 设置启动的主类,Kotlin类编译后都是xxxKt的形式
        attributes(mapOf("Main-Class" to "com.ssm.MainKt"))
    }

    // 将所有运行时依赖(JAR 文件)的内容合并到最终的 JAR 包中。
    from(configurations.runtimeClasspath.get().map {
        // 如果是目录就直接包含,如果是 JAR 文件就解压其内容再包含。
        if (it.isDirectory) it else zipTree(it)
    })

    // 处理重复文件:当多个依赖库包含同名文件时(如 META-INF/LICENSE),
    // 告诉 Gradle 排除重复项,只保留第一次遇到的文件,以避免构建失败。
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE

    // 将项目编译后的代码(.class 文件)添加到最终的 JAR 包中。
    from(sourceSets.main.get().output)
}

8.2 运行构建任务

运行任务的几种方式

  • 点击右边的jar任务运行
  • 使用项目的gradlew脚本运行jar任务

会使用gradle-wrapper.properties中的配置的Gradle地址下来来构建

bash 复制代码
./gradlew jar
  • 使用全局的gradle运行脚本

这样做你得本机有Gradle,并且配置环境变量中来使用,并且打包用的也是你本机的这个版本

bash 复制代码
gradle jar

8.3 运行jar包

打包后在我们的build->libs文件夹中就能找到咯,直接java -jar即可运行

开箱即用的SpringBoot

Springboot的构建就很方便了,IDEA专业版直接提供了SpringBoot的Kotlin工程快速创建,其创建和开发流程和Java无区别就不展开说明了,如果没有使用IDEA专业版也可以自行去官网构建网站去创建,也提供了Kotlin的选择。当然也可以自行挑战下从SSM转型为SpringBoot工程,换依赖和文件配置都和Java相同。

Kotlin的SpringBoot开发和Java无任何区别,目前也没遇到比较困难的兼容性问题,基本都是一些序列化和Open问题,当然都有对应的解决方案,Kotlin在SpringBoot中已经很成熟了,也欢迎大家尝试。

后记

从Spring6开始Spring就开始对Kotlin进行了相关的适配,最近KotlinConf也提到了Kotlin将和Spring进行合作共创Spring生态,这些也证明Kotlin的优势和好处,近几年国外和国内的Kotlin后端应用也说明了Kotlin后端确实能有一席之地,Kotlin也不单单局限于SSM,Kotlin自己的后端生态框架比如Ktorm,Ktor,Exposed这些都是不错的选择。还有WebFlux这种和Kotlin设计天然契合的新兴技术,这些对Kotlin在后端领域的未来是充满潜力的。它不仅是一个站在巨人肩膀上的语言,更是一个拥有独特生态和理念的平台,有望为开发者带来更高的效率和更好的体验。欢迎大家来共创Kotlin后端发展。

相关推荐
叫我阿柒啊2 小时前
从Java全栈到云原生:一场技术深度对话
java·spring boot·docker·微服务·typescript·消息队列·vue3
杨杨杨大侠2 小时前
实战案例:商品详情页数据聚合服务的技术实现
java·spring·github
杨杨杨大侠2 小时前
实战案例:保险理赔线上审核系统的技术实现
java·spring·github
计算机毕设定制辅导-无忧学长2 小时前
MQTT 与 Java 框架集成:Spring Boot 实战(一)
java·网络·spring boot
叫我阿柒啊2 小时前
从Java全栈到Vue3实战:一次真实面试的深度复盘
java·spring boot·微服务·vue3·响应式编程·前后端分离·restful api
泉城老铁3 小时前
Spring Boot中实现多线程分片下载
java·spring boot·后端
泉城老铁3 小时前
Spring Boot中实现多文件打包下载
spring boot·后端·架构
泉城老铁3 小时前
Spring Boot中实现大文件分片下载和断点续传功能
java·spring boot·后端
友莘居士3 小时前
长流程、复杂业务流程分布式事务管理实战
spring boot·rocketmq·saga·复杂流程分布式事务·长流程
百思可瑞教育3 小时前
Spring Boot 参数校验全攻略:从基础到进阶
运维·服务器·spring boot·后端·百思可瑞教育·北京百思教育