先试试 Kotlin 混进 Java 项目
xx校xx中心项目的后端是 Java + Springboot 开发。
嗯,很常规的技术栈,也很成熟,大家都很熟悉这一套技术栈下的开发流程。直到有一天,有一位开发人员(没错就是我)脑洞大开,说:“既然 Kotlin 可以和 Java 互操作,为什么不在项目中混用它们呢?享受一下现代编程语言的便利”
IDEA 里一切正常
首先,为了快速验证能不能成功在项目中混用 Java 和 Kotlin,我在项目里使用 Kotlin 写了一个新的Controller,暴露一个 /hello 接口,当调用这个接口时,返回一个简单的字符串 Hello, Kotlin!。大概就是这样:
@RestController
@RequestMapping("/test")
class HelloController {
@GetMapping("/hello")
fun hello(): String {
return "Hello, Kotlin!"
}
}
然后我点击 IDEA 的那个小三角,项目顺利启动。我在浏览器里访问 http://localhost:8080/test/hello,成功返回了 Hello, Kotlin!。
一到服务器就 404
于是我把分支上传到仓库,自动CI/CD结束后,项目被成功部署到服务器,其他成员访问 http://开发域名/test/hello,却返回了 404 Not Found。于是“差评”飞到群里,要求我马上检查一下。
Maven 包里少了 Kotlin 类
我首先检查CI/CD是否成功,答案是肯定的。但是为什么没有这个 /hello 接口呢?我检查项目的CI/CD脚本,发现在脚本中,使用 ./mvnw clean package 打包项目。我意识到,可能是由于开发环境的不同导致的问题,导致实际上在项目部署到服务器后,并没有正确构建 Kotlin 部分的代码。
我在本地执行
mvn clean package
jar tf target/xxx.jar | grep "HelloController"
结果并没有找到 HelloController 类!所以这个问题出在 IDEA 构建和 mvn 构建的差异上!
我没有显示指定 Kotlin 代码的路径! 这导致 Kotlin 编译器找不到 Kotlin 代码,自然也不会编译它。
混合编译顺序没那么自动
接着,我检查 pom.xml 文件中关于编译过程的配置:
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>2.2.21</version>
<configuration>
<jvmTarget>21</jvmTarget>
<compilerPlugins><plugin>spring</plugin></compilerPlugins>
</configuration>
<executions>
<execution>
<id>compile</id>
<goals><goal>compile</goal></goals>
<!-- ❌ 没有指定 phase,默认绑定到 compile 阶段 -->
</execution>
</executions>
</plugin>
嗯,在 compile 阶段编译 Kotlin 代码,看起来相当合理。 但是不合理的地方就在这里 ———— Kotlin 代码的编译在 compile 阶段,这可能与 Java 代码同时编译或在其之后编译。
javac ———— Java 语言的编译器,只能读取已经编译的 .class 文件,不能直接“看懂” .kt 文件。
kotlinc ———— Kotlin 语言的编译器,可以直接读取和解析 .java 文件,并引用未编译的 Java 类。
在一个 Java 和 Kotlin 混合的项目中,可能有这样的依赖情况:
-
场景1: Kotlin代码 → 引用Java代码
-
场景2: Java代码 → 引用Kotlin代码
那么在场景1中,Kotlin 代码引用了 Java 代码,这是没有问题的。因为 Kotlin 编译器可以直接读取和解析 Java 类,而不需要先编译 Java 代码。
但是在场景2中,Java 代码引用了 Kotlin 代码,这就涉及到一个问题:Java 代码在编译时,需要先编译 Kotlin 代码,才能引用 Kotlin 类。这就要求 Kotlin 代码必须在 Java 代码之前编译。
所以,为了确保在 Java 代码中引用 Kotlin 类时,Kotlin 代码已经被编译,我们需要在 Maven 构建中,显式地配置 Kotlin 代码的编译顺序。
把 Kotlin 放到 Java 前面编译
具体来说,我们可以在 pom.xml 中,为 Kotlin 插件添加一个 compile 执行,将其绑定到 compile 阶段之前。这样,在编译 Java 代码时,Kotlin 代码就会先被编译,确保 Java 代码可以引用 Kotlin 类。
<build>
<!-- 显式声明源代码目录 -->
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory>
<plugins>
<!-- 1. Spring Boot 插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- 2. Kotlin 插件:必须在 Java 之前编译 -->
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>2.2.21</version>
<configuration>
<jvmTarget>21</jvmTarget>
<sourceDirs>
<sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
<sourceDir>${project.basedir}/src/main/java</sourceDir>
</sourceDirs>
<compilerPlugins>
<plugin>spring</plugin>
<plugin>lombok</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>2.2.21</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-lombok</artifactId>
<version>2.2.21</version>
</dependency>
</dependencies>
<executions>
<!-- 主代码编译:绑定到 process-sources 阶段 -->
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<!-- 测试代码编译:绑定到 process-test-sources 阶段 -->
<execution>
<id>test-compile</id>
<phase>process-test-sources</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 3. Java 编译器:保持在 compile 阶段 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>21</source>
<target>21</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
对比一下两个不同配置的编译阶段:
| 阶段 | 错误配置 | 正确配置 |
|---|---|---|
| process-sources | (无操作) | ✅ Kotlin 编译 生成 Kotlin .class |
| compile | ❌ Kotlin + Java 同时 或顺序不确定 互相冲突 |
✅ Java 编译 引用 Kotlin .class |
| 结果 | ❌ 类找不到或缺失 | ✅ 所有类正确生成 |
重新打包确认一下
修改完成后,我重新编译项目,再次检查是否包含 Kotlin 类的 .class 文件。发现所有 Kotlin 类都被成功编译,生成了对应的 .class 文件。