A11.Kotlin与Java项目架构的融合实践

前言

这是一个基于Java 21 的 六边形架构与领域驱动设计的一个通用项目,并且结合现有的最新版本技术架构实现了 领域驱动设计模式和六边形架构模式组件定义. 并且结合微服务,介绍了领域层,领域事件,资源库,分布式锁,序列化,安全认证,日志等,并提供了实现功能. 并且我会以日常发布文章和更新代码的形式来完善它.

开篇

因为Gradle项目支持使用Kotlin DSL,并且Kotlin 是 Java 的超集, 在不考虑Kotlin带有的新特性下,我们以简化项目架构为前提,在以下3个方向使用 Kotlin。

  1. Gradle 使用Kotlin DSL.

  2. Spring Configuration 使用Kotlin.

  3. DTO 使用Kotlin.

为什么选择这3点,在于防止Kotlin入侵领域层, 避免领域层入侵过多的技术栈。又能同时享受Kotlin带来的好处。 这也是我谨慎使用Lombok(现只在测试类中使用)的原因。并在使用中严格遵循以下原则:

  1. Kotlin正向调用Java, 禁止反向调用.

  2. Common模块禁止使用Kotlin.

实践

该部分的改变,将会穿插整个项目中,对非common开头的项目进行改造.

1. Gradle 使用Kotlin DSL

Gradle 脚本可以使用Kotlin DSL,在 build.gradle.kts 中,我们可以使用Kotlin的DSL语法,更直接地理解gradle脚本。

// Root Project中build.gradle
plugins {
    id ("java")
    id("org.jetbrains.kotlin.jvm") version "2.1.0-RC2" apply(false)
    id("org.jetbrains.kotlin.plugin.spring") version "2.1.0-RC2" apply(false)
    id("org.springframework.boot") version "3.3.5" apply(false)
    id("io.spring.dependency-management") version "1.1.6" apply(false)
}

allprojects {

    // 确保 java 插件被正确应用
    apply(plugin = "java")

    java {
        sourceCompatibility = JavaVersion.VERSION_21
        targetCompatibility = JavaVersion.VERSION_21
    }

    tasks.withType<JavaCompile>().configureEach {
        options.compilerArgs.add("--enable-preview")
    }

    tasks.withType<JavaExec>().configureEach {
        jvmArgs("--enable-preview")
    }

    tasks.test {
        useJUnitPlatform()
        jvmArgs("--enable-preview")
    }

    dependencies {

        implementation(platform("com.fasterxml.jackson:jackson-bom:2.18.1"))
        implementation(platform("com.google.guava:guava-bom:33.3.1-jre"))
        implementation(platform("org.slf4j:slf4j-bom:2.1.0-alpha1"))

        implementation("io.vavr:vavr:0.10.4")
        implementation("com.google.guava:guava")
        implementation("org.apache.commons:commons-collections4:4.4")
        implementation("org.apache.commons:commons-lang3:3.14.0")
        implementation("commons-codec:commons-codec:1.16.0")
        implementation("org.jasypt:jasypt:1.9.3")
        implementation("org.slf4j:slf4j-api")
        implementation("ch.qos.logback:logback-core:1.5.11")
        implementation("ch.qos.logback:logback-classic:1.5.11")
        implementation("io.swagger.core.v3:swagger-annotations:2.2.21")

        testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
        testImplementation("net.datafaker:datafaker:2.4.1")

        testCompileOnly("org.projectlombok:lombok:1.18.34")
        testAnnotationProcessor("org.projectlombok:lombok:1.18.34")
    }

    repositories {
        mavenLocal()
        maven { url = uri("https://maven.aliyun.com/nexus/content/groups/public") }
        maven { url = uri("https://repo.maven.apache.org/maven2") }
        maven { url = uri("https://repository.jboss.org/nexus/content/groups/public-jboss") }
        maven { url = uri("https://repo.spring.io/milestone") }
        maven { url = uri("https://repo.spring.io/snapshot") }
        google()
        mavenCentral()
    }

}

其实整个改造是一个很快的过程,如果结合chatgpt等AI的提示下. 几分钟就可以改造完, 发现改动其实变化不大,但是在语法上更加一致性,脚本更加简洁明朗, 可读性更强。

2. Spring Configuration 使用Kotlin

任一项目,可以分为两大类问题: 技术问题和业务问题。 Spring Configuration 是用来解决技术问题,管理项目中的类和对象的依赖及其调用关系等方方面面. 所以Spring的配置可以使用Kotlin。而对业务问题,我们依然保留并保守只使用Java。

首先我们添加项目对 KotlinKotlin-Spring 的支持, 只需要在 build.gradle.kts 中添加如下插件. 因为不是每个子模块都需要对Kotlin进行改造,所以设置 apply=false, 当需要的时候,才会引用它.

plugins {
    id("org.jetbrains.kotlin.jvm") version "2.1.0-RC2" apply(false)
    id("org.jetbrains.kotlin.plugin.spring") version "2.1.0-RC2" apply(false)
}

然后在Idea工具, File→Settings→Build, Execution, Deployment→Compiler→Kotlin Compiler(21), 配置好Kotlin的编译参数.

因为Common是公共模块,是一个通用功能组件核心的项目,所以不会考虑对 Common 模块进行 Kotlin 支持. 在sample开头的模块中,我们添加了Kotlin的依赖.

apply(plugin = "org.springframework.boot")
apply(plugin = "io.spring.dependency-management")
apply(plugin = "org.jetbrains.kotlin.jvm") //kotlin
apply(plugin = "org.jetbrains.kotlin.plugin.spring") //kotlin-spring

dependencies {
    implementation(project(":common-serialization-with-jackson"))
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("net.datafaker:datafaker:2.4.1")
}

首先, 我们改造最简单的启动类 Application.java.

package com.iokays.sample

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}

其中 open: 关键字,表示类可以继承,否则只能被final修饰。

然后我们改造 Spring Configuration 配置类 MyDefaultSecurityConfig.java.

@Configuration
@EnableWebSecurity
class MyDefaultSecurityConfig {

    @Bean
    @Throws(Exception::class)
    fun securityFilterChain(http: HttpSecurity, authenticationManager: AuthenticationManager?): SecurityFilterChain {
        http
            .sessionManagement { session -> session.maximumSessions(1) }
            .formLogin(Customizer.withDefaults())
            .authorizeHttpRequests(Customizer { auth ->
                auth
                    .requestMatchers("/login").permitAll()
                    .requestMatchers("/ping").hasAuthority("ADMIN")
                    .anyRequest().authenticated()
            })
            .addFilterBefore(UsernameCaptchaAuthenticationFilter(), UsernamePasswordAuthenticationFilter::class.java)


        //添加 oauth2Login 支持
        http.oauth2Login(Customizer.withDefaults())

        return http.build()
    }

    @Bean
    fun userDetailsService(): UserDetailsService {
        val result = InMemoryUserDetailsManager()
        result.createUser(User.withUsername("admin").password("123456").authorities("ADMIN").build())
        result.createUser(User.withUsername("user").password("123456").authorities("READ").build())
        return result
    }

    @Bean
    fun passwordEncoder(): PasswordEncoder {
        return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    fun authenticationEventPublisher(applicationEventPublisher : ApplicationEventPublisher): AuthenticationEventPublisher {
        return DefaultAuthenticationEventPublisher(applicationEventPublisher);
    }

    @Bean
    fun authenticationManager(userDetailsService: UserDetailsService, passwordEncoder: PasswordEncoder): AuthenticationManager {
        //验证用户密码
        val authenticationProvider = DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder);

        val providerManager = ProviderManager(authenticationProvider);
        providerManager.setEraseCredentialsAfterAuthentication(false);

        return providerManager;
    }
}

这样改造完后,其实和Java 配置并没有什么区别,只是使用Kotlin语法而已。

3. DTO 使用Kotlin

在Java中,我们使用JavaBean来封装数据,在Kotlin中,我们可以使用data class来封装数据。

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

data class 默认实现了equals, hashCode, toString, copy 等方法。 data class 默认实现了所有的属性的getter和setter方法。 data class 默认实现了所有的参数的构造函数.

在部分场景,我们可以使用 Java 的record关键字来替代data class. 结合项目的业务场景, 我们将record class 的应用场景限制在领域层.

未完待续…​

整个体验下来,发现Java与Kotlin的融合后,整体的编译过程变慢了,对于一个通用的框架设计,统一编程语言还是比较重要的,我们可以在自己的业务模块使用使用Kotlin,但是对于Common模块和领域层的使用,还是建议使用Java.

我们介绍了Kotlin(含DSL)在本项目中的应用,并且通过实践,展示了如何将Kotlin与Java项目进行融合。并且极大程度的限制了Kotlin的使用场景, 禁止Kotlin在Common模块和领域层的使用. 促使一个纯碎的JAVA项目也能直接的复用Common模块. 促使业务复杂度不受技术的复杂度影响. 同理,本项目对Lombok也做了这样的限制. 下篇将会介绍…​ (还没想好)