1. 概述

Java 与 Kotlin 都是运行在 JVM 上的语言。虽然两者语法和设计哲学有所不同,但在 Java EE 容器中使用 Kotlin 时,会遇到一些典型的兼容性问题。

本文将介绍在 Java EE 环境中使用 Kotlin 时常见的挑战,并提供解决方案。通过一个简单的 CRUD 示例应用,演示如何在 Java EE 容器中顺利使用 Kotlin 编写企业级应用。

2. 面临的挑战

Kotlin 与 Java 最大的不同在于其语言设计范式。比如:

  • Kotlin 类默认是 final 的,不能被继承。
  • Kotlin 的数据类(data class)需要显式提供无参构造器以适配 JPA、Jackson 等框架。
  • 依赖注入(DI)在 Kotlin 中初始化方式略有不同。
  • 使用 Arquillian 进行集成测试时也存在兼容性问题。

这些问题虽然不是大问题,但需要我们有意识地进行适配,才能顺利构建 Kotlin + Java EE 应用。

提示:目前 Arquillian 已停止更新,与 JUnit5 和新版本 Wildfly 不兼容,使用时需要注意版本选择。

3. 依赖配置

我们首先在 pom.xml 中添加 Java EE 的依赖:

<dependency>
    <groupId>jakarta.platform</groupId>
    <artifactId>jakarta.jakartaee-api</artifactId>
    <version>11.0.0-M1</version>
    <scope>provided</scope>
</dependency>

由于我们仅在编译阶段使用该依赖,所以设置为 provided

4. JPA 实体类

我们定义一个 Student 数据类,同时作为 JPA 实体类和 DTO 使用:

@Entity
data class Student constructor (
    @SequenceGenerator(name = "student_id_seq", sequenceName = "student_id_seq", allocationSize = 1)
    @GeneratedValue(generator = "student_id_seq", strategy = GenerationType.SEQUENCE)
    @Id
    var id: Long?,

    var firstName: String,
    var lastName: String
) {
    constructor() : this(null, "", "")
    constructor(firstName: String, lastName: String) : this(null, firstName, lastName)
}

Kotlin 的 data class 会自动生成 equals(), hashCode(), toString() 等方法,非常适合用作数据容器。

⚠️ 注意:为了兼容 JPA 或 Jackson,必须显式定义无参构造函数。Kotlin 的主构造函数不能满足框架的实例化需求。

5. 业务服务类

我们使用 @Stateless 注解定义一个无状态 EJB,处理 CRUD 逻辑:

@Stateless
open class StudentService {

    @PersistenceContext
    private lateinit var entityManager: EntityManager

    open fun create(student: Student) = entityManager.persist(student)

    open fun read(id: Long): Student? = entityManager.find(Student::class.java, id)

    open fun update(student: Student) = entityManager.merge(student)

    open fun delete(id: Long) = entityManager.remove(read(id))
}

由于 Kotlin 默认类是 final 的,我们需要使用 open 关键字显式允许继承,以便容器创建代理类进行注入。

我们使用 lateinit 关键字延迟初始化 EntityManager,这是 Kotlin 中处理依赖注入的常用方式。

6. REST 接口

我们定义一个 REST 接口类,暴露 CRUD 操作:

@ApplicationPath("/")
class ApplicationConfig : Application() {
    override fun getClasses() = setOf(StudentResource::class.java)
}

接口资源类:

@Path("/student")
open class StudentResource {

    @Inject
    private lateinit var service: StudentService

    @POST
    open fun create(student: Student): Response {
        service.create(student)
        return Response.ok().build()
    }

    @GET
    @Path("/{id}")
    open fun read(@PathParam("id") id: Long): Response {
        val student  = service.read(id)
        return Response.ok(student, MediaType.APPLICATION_JSON_TYPE).build()
    }

    @PUT
    @Path("/{id}")
    open fun update(@PathParam("id") id: Long, student: Student): Response {
        service.update(student)
        return Response.ok(student, MediaType.APPLICATION_JSON_TYPE).build()
    }

    @DELETE
    @Path("/{id}")
    open fun delete(@PathParam("id") id: Long): Response {
        service.delete(id)
        return Response.noContent().build()
    }
}

与服务类类似,我们也需要将类和方法标记为 open,以支持容器代理注入。

7. 使用 Arquillian 进行集成测试

Arquillian 是 Java EE 中常用的集成测试框架。虽然它已不再更新,但仍可用于旧项目。

我们使用 ShrinkWrap 打包部署测试 WAR 包:

@RunWith(Arquillian.class)
public class StudentResourceIntegrationTest {

    @Deployment
    public static WebArchive createDeployment() {
        JavaArchive[] kotlinRuntime = Maven.configureResolver()
          .workOffline()
          .withMavenCentralRepo(true)
          .withClassPathResolution(true)
          .loadPomFromFile("pom.xml")
          .resolve("org.jetbrains.kotlin:kotlin-stdlib")
          .withTransitivity()
          .as(JavaArchive.class);

        return ShrinkWrap.create(WebArchive.class, "kotlin.war")
          .addPackages(true, Filters.exclude(".*Test*"), "com.baeldung.jeekotlin")
          .addAsLibraries(kotlinRuntime)
          .addAsResource("META-INF/persistence.xml")
          .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml");
    }
    
    @Test
    @RunAsClient
    public void when_post__then_return_ok(@ArquillianResource URL url)
      throws URISyntaxException, JsonProcessingException {
        String student = new ObjectMapper()
          .writeValueAsString(new Student("firstName", "lastName"));
          WebTarget webTarget = ClientBuilder.newClient().target(url.toURI());

        Response response = webTarget
          .path("/student")
          .request(MediaType.APPLICATION_JSON)
          .post(Entity.json(student));

        assertEquals(200, response.getStatus());
    }
}

我们在测试中使用 @ArquillianResource 获取部署后的 URL,构造 HTTP 请求验证 REST 接口功能是否正常。

8. 总结

本文通过一个完整的 CRUD 示例,展示了如何在 Java EE 容器中使用 Kotlin 构建企业级应用。虽然 Kotlin 与 Java 在语法和设计上存在差异,但通过一些技巧(如 openlateinit、显式无参构造函数等),可以很好地兼容 Java EE 的各种框架。

完整源码可在 GitHub 仓库 获取。


原始标题:Java EE Application with Kotlin