A11.Kotlin与Java项目架构的融合实践
前言
这是一个基于Java 21 的 六边形架构与领域驱动设计的一个通用项目,并且结合现有的最新版本技术架构实现了 领域驱动设计模式和六边形架构模式组件定义. 并且结合微服务,介绍了领域层,领域事件,资源库,分布式锁,序列化,安全认证,日志等,并提供了实现功能. 并且我会以日常发布文章和更新代码的形式来完善它.
开篇
Spring Boot 中文参考文档: https://www.iokays.com/spring-boot/features/kotlin.html
因为Gradle项目支持使用Kotlin DSL,并且Kotlin 是 Java 的超集, 在不考虑Kotlin带有的新特性下,我们以简化项目架构为前提,在以下3个方向使用 Kotlin。
-
Gradle 使用Kotlin DSL.
-
Spring Configuration 使用Kotlin.
-
DTO 使用Kotlin.
为什么选择这3点,在于防止Kotlin入侵领域层, 避免领域层入侵过多的技术栈。又能同时享受Kotlin带来的好处。 这也是我谨慎使用Lombok(现只在测试类中使用)的原因。并在使用中严格遵循以下原则:
-
Kotlin正向调用Java, 禁止反向调用.
-
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。
首先我们添加项目对 Kotlin
和 Kotlin-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也做了这样的限制. 下篇将会介绍… (还没想好)