refactor(page): 删除旧版页面文件
This commit is contained in:
138
.agents/skills/kotlin-tooling-java-to-kotlin/SKILL.md
Normal file
138
.agents/skills/kotlin-tooling-java-to-kotlin/SKILL.md
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: kotlin-tooling-java-to-kotlin
|
||||
description: >
|
||||
Use when converting Java source files to idiomatic Kotlin, when user mentions
|
||||
"java to kotlin", "j2k", "convert java", "migrate java to kotlin", or when
|
||||
working with .java files that need to become .kt files. Handles framework-aware
|
||||
conversion for Spring, Lombok, Hibernate, Jackson, Micronaut, Quarkus, Dagger/Hilt,
|
||||
RxJava, JUnit, Guice, Retrofit, and Mockito.
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: JetBrains
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Java to Kotlin Conversion
|
||||
|
||||
Convert Java source files to idiomatic Kotlin using a disciplined 4-step conversion
|
||||
methodology with 5 invariants checked at each step. Supports framework-aware conversion
|
||||
that handles annotation site targets, library idioms, and API preservation.
|
||||
|
||||
## Workflow
|
||||
|
||||
```dot
|
||||
digraph j2k_workflow {
|
||||
rankdir=TB;
|
||||
"User specifies files" -> "Step 0: Scan & Detect";
|
||||
"Step 0: Scan & Detect" -> "Load framework guides";
|
||||
"Load framework guides" -> "Step 1: Convert";
|
||||
"Step 1: Convert" -> "Step 2: Write .kt";
|
||||
"Step 2: Write .kt" -> "Step 3: Git rename";
|
||||
"Step 3: Git rename" -> "Step 4: Verify";
|
||||
"Step 4: Verify" -> "Next file?" [label="pass"];
|
||||
"Step 4: Verify" -> "Fix issues" [label="fail"];
|
||||
"Fix issues" -> "Step 1: Convert";
|
||||
"Next file?" -> "Step 0: Scan & Detect" [label="batch: yes"];
|
||||
"Next file?" -> "Done" [label="no more files"];
|
||||
}
|
||||
```
|
||||
|
||||
## Step 0: Scan & Detect Frameworks
|
||||
|
||||
Before converting, scan the Java file's import statements to detect which frameworks
|
||||
are in use. Load ONLY the matching framework reference files to keep context focused.
|
||||
|
||||
### Framework Detection Table
|
||||
|
||||
| Import prefix | Framework guide |
|
||||
|---|---|
|
||||
| `org.springframework.*` | [SPRING.md](references/frameworks/SPRING.md) |
|
||||
| `lombok.*` | [LOMBOK.md](references/frameworks/LOMBOK.md) |
|
||||
| `javax.persistence.*`, `jakarta.persistence.*`, `org.hibernate.*` | [HIBERNATE.md](references/frameworks/HIBERNATE.md) |
|
||||
| `com.fasterxml.jackson.*` | [JACKSON.md](references/frameworks/JACKSON.md) |
|
||||
| `io.micronaut.*` | [MICRONAUT.md](references/frameworks/MICRONAUT.md) |
|
||||
| `io.quarkus.*`, `javax.enterprise.*`, `jakarta.enterprise.*` | [QUARKUS.md](references/frameworks/QUARKUS.md) |
|
||||
| `dagger.*`, `dagger.hilt.*` | [DAGGER-HILT.md](references/frameworks/DAGGER-HILT.md) |
|
||||
| `io.reactivex.*`, `rx.*` | [RXJAVA.md](references/frameworks/RXJAVA.md) |
|
||||
| `org.junit.*`, `org.testng.*` | [JUNIT.md](references/frameworks/JUNIT.md) |
|
||||
| `com.google.inject.*` | [GUICE.md](references/frameworks/GUICE.md) |
|
||||
| `retrofit2.*`, `okhttp3.*` | [RETROFIT.md](references/frameworks/RETROFIT.md) |
|
||||
| `org.mockito.*` | [MOCKITO.md](references/frameworks/MOCKITO.md) |
|
||||
|
||||
If `javax.inject.*` is detected, check for Dagger/Hilt vs Guice by looking for other
|
||||
imports from those frameworks. If ambiguous, load both guides.
|
||||
|
||||
## Step 1: Convert
|
||||
|
||||
Apply the conversion methodology from [CONVERSION-METHODOLOGY.md](references/CONVERSION-METHODOLOGY.md).
|
||||
|
||||
This is a 4-step chain-of-thought process:
|
||||
1. **Faithful 1:1 translation** — exact semantics preserved
|
||||
2. **Nullability & mutability audit** — val/var, nullable types
|
||||
3. **Collection type conversion** — Java mutable → Kotlin types
|
||||
4. **Idiomatic transformations** — properties, string templates, lambdas
|
||||
|
||||
Five invariants are checked after each step. If any invariant is violated, revert
|
||||
to the previous step and redo.
|
||||
|
||||
Apply any loaded framework-specific guidance during step 4 (idiomatic transformations).
|
||||
|
||||
## Step 2: Write Output
|
||||
|
||||
Write the converted Kotlin code to a `.kt` file with the same name as the original
|
||||
Java file, in the same directory.
|
||||
|
||||
## Step 3: Preserve Git History
|
||||
|
||||
To preserve `git blame` history, use a two-phase approach:
|
||||
|
||||
```bash
|
||||
# Phase 1: Rename (creates rename tracking)
|
||||
git mv src/main/java/com/example/Foo.java src/main/kotlin/com/example/Foo.kt
|
||||
git commit -m "Rename Foo.java to Foo.kt"
|
||||
|
||||
# Phase 2: Replace content (tracked as modification, not new file)
|
||||
# Write the converted Kotlin content to Foo.kt
|
||||
git commit -m "Convert Foo from Java to Kotlin"
|
||||
```
|
||||
|
||||
If the project keeps Java and Kotlin in the same source root (e.g., `src/main/java/`),
|
||||
rename in place:
|
||||
|
||||
```bash
|
||||
git mv src/main/java/com/example/Foo.java src/main/java/com/example/Foo.kt
|
||||
```
|
||||
|
||||
If the project does not use Git, simply write the `.kt` file and delete the `.java` file.
|
||||
|
||||
## Step 4: Verify
|
||||
|
||||
After conversion, verify using [checklist.md](assets/checklist.md):
|
||||
- Attempt to compile the converted file
|
||||
- Run existing tests
|
||||
- Check annotation site targets
|
||||
- Confirm no behavioral changes
|
||||
|
||||
## Batch Conversion
|
||||
|
||||
When converting multiple files (a directory or package):
|
||||
|
||||
1. **List all `.java` files** in the target scope
|
||||
2. **Sort by dependency order** — convert leaf dependencies first (files that don't
|
||||
import other files in the conversion set), then work up to files that depend on them
|
||||
3. **Convert one file at a time** — apply the full workflow (steps 0-4) for each
|
||||
4. **Track progress** — report which files are done, which remain
|
||||
5. **Handle cross-references** — after converting a file, update imports in other Java
|
||||
files if needed (e.g., if a class moved packages)
|
||||
|
||||
For large batches, consider converting in packages (bottom-up from leaf packages).
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
See [KNOWN-ISSUES.md](references/KNOWN-ISSUES.md) for:
|
||||
- Kotlin keyword conflicts (`when`, `in`, `is`, `object`)
|
||||
- SAM conversion ambiguity
|
||||
- Platform types from Java interop
|
||||
- `@JvmStatic` / `@JvmField` / `@JvmOverloads` usage
|
||||
- Checked exceptions and `@Throws`
|
||||
- Wildcard generics → Kotlin variance
|
||||
@@ -0,0 +1,60 @@
|
||||
# Post-Conversion Verification Checklist
|
||||
|
||||
Use this checklist after converting each Java file to Kotlin.
|
||||
|
||||
## Compilation & Tests
|
||||
- [ ] The `.kt` file compiles without errors
|
||||
- [ ] All existing tests still pass
|
||||
- [ ] No new compiler warnings introduced
|
||||
|
||||
## Semantic Correctness
|
||||
- [ ] No new side-effects or behavioural changes
|
||||
- [ ] All public API signatures preserved (method names, parameter types, return types)
|
||||
- [ ] Exception behaviour unchanged (same exceptions thrown in same conditions)
|
||||
|
||||
## Annotations
|
||||
- [ ] All annotations preserved from the original Java code
|
||||
- [ ] Annotation site targets correct (`@field:`, `@get:`, `@set:`, `@param:`)
|
||||
- [ ] No annotations accidentally dropped during conversion
|
||||
|
||||
## Imports & Package
|
||||
- [ ] Package declaration matches original
|
||||
- [ ] All imports carried forward (except Java types that shadow Kotlin builtins)
|
||||
- [ ] No new imports added unnecessarily
|
||||
|
||||
## Documentation
|
||||
- [ ] All Javadoc converted to KDoc format
|
||||
- [ ] `{@code ...}` → backtick code in KDoc
|
||||
- [ ] `{@link ...}` → `[...]` KDoc links
|
||||
- [ ] `<p>` paragraph tags → blank lines
|
||||
- [ ] `@param`, `@return`, `@throws` tags preserved
|
||||
- [ ] Class-level and method-level documentation preserved
|
||||
|
||||
## Nullability & Mutability
|
||||
- [ ] Non-null types used only where provably non-null
|
||||
- [ ] Nullable types (`?`) used for all Java types that could be null
|
||||
- [ ] `val` used for all immutable variables/properties
|
||||
- [ ] `var` used only for mutable variables/properties
|
||||
|
||||
## Collections
|
||||
- [ ] `MutableList`/`MutableSet`/`MutableMap` for Java's mutable collections
|
||||
- [ ] `List`/`Set`/`Map` only where Java used immutable wrappers
|
||||
|
||||
## Kotlin Idioms
|
||||
- [ ] Getters/setters replaced with Kotlin properties where appropriate
|
||||
- [ ] String concatenation replaced with string templates where clearer
|
||||
- [ ] Elvis operator used where appropriate
|
||||
- [ ] `when` expression used instead of `switch`
|
||||
- [ ] Smart casts used after `is` checks (no explicit casts)
|
||||
|
||||
## Framework-Specific (check applicable items)
|
||||
- [ ] **Spring**: Classes that need proxying are `open`; `@Bean` methods are `open`
|
||||
- [ ] **Lombok**: All Lombok annotations removed; replaced with Kotlin equivalents
|
||||
- [ ] **Hibernate/JPA**: Entities are `open` (not data classes); no-arg constructor provided
|
||||
- [ ] **Jackson**: `@field:` and `@get:` annotation site targets correct
|
||||
- [ ] **RxJava**: Reactive types correctly mapped to Coroutines/Flow
|
||||
- [ ] **Mockito**: `when` keyword escaped or replaced with MockK equivalent
|
||||
|
||||
## Git History
|
||||
- [ ] File renamed via `git mv` (not delete + create)
|
||||
- [ ] Rename commit separate from content change commit
|
||||
@@ -0,0 +1,352 @@
|
||||
# Conversion Methodology
|
||||
|
||||
You are a senior Kotlin engineer and Java-Kotlin JVM interop specialist. Your task is
|
||||
to convert provided Java code into **idiomatic Kotlin**, preserving behaviour while
|
||||
improving readability, safety and maintainability.
|
||||
|
||||
## The 4-Step Precognition Process
|
||||
|
||||
Before emitting any code, run through the provided Java input and perform these 4 steps
|
||||
of thinking. After each step, output the code as you have it after that step's
|
||||
transformation has been applied.
|
||||
|
||||
### Step 1: Faithful 1:1 Translation
|
||||
|
||||
Convert the Java code 1 to 1 into Kotlin, prioritising faithfulness to the original
|
||||
Java semantics, to replicate the Java code's functionality and logic exactly.
|
||||
|
||||
**Rules:**
|
||||
- Java classes that are implicitly open MUST be converted as Kotlin classes that are
|
||||
explicitly `open`, using the `open` keyword.
|
||||
- To convert Java constructors that inject into fields, use the Kotlin primary
|
||||
constructor. Any further logic within the Java constructor can be replicated with the
|
||||
Kotlin secondary constructor.
|
||||
|
||||
### Step 2: Nullability & Mutability
|
||||
|
||||
Check that mutability and nullability are correctly expressed in your Kotlin conversion.
|
||||
Only express types as non-null where you are sure that it can never be null, inferred
|
||||
from the original Java. Use `val` instead of `var` where you see variables that are
|
||||
never modified.
|
||||
|
||||
**Rules:**
|
||||
- If you see a logical assertion that a value is not null (e.g., `Objects.requireNonNull`),
|
||||
this shows that the author has considered that the value can never be null. Use a
|
||||
non-null type in this case, and remove the logical assertion.
|
||||
- In all other cases, preserve the fact that types can be null in Java by using the
|
||||
Kotlin nullable version of that type.
|
||||
|
||||
### Step 3: Collection Type Conversion
|
||||
|
||||
Convert datatypes like collections from their Java variants to the Kotlin variants.
|
||||
|
||||
**Rules:**
|
||||
- For Java collections like `List` that are mutable by default, always use the Kotlin
|
||||
`MutableList`, unless you see explicitly that the Java code uses an immutable wrapper
|
||||
(e.g., `Collections.unmodifiableList()`) — in this case, use the Kotlin `List` (and
|
||||
so on for other collections like `Set`, `Map` etc.)
|
||||
|
||||
### Step 4: Idiomatic Transformations
|
||||
|
||||
Introduce syntactic transformations to make the output truly idiomatic.
|
||||
|
||||
**Rules:**
|
||||
- Where getters and setters are defined as methods in Java, use the Kotlin syntax to
|
||||
replace these methods with a more idiomatic version.
|
||||
- Lambdas should be used where they can simplify code complexity while replicating the
|
||||
exact behaviour of the previous code.
|
||||
|
||||
## The 5 Invariants
|
||||
|
||||
In each stage of your chain of thought, the following invariants must hold.
|
||||
|
||||
**Invariant 1:** No new side-effects or behaviour.
|
||||
|
||||
**Invariant 2:** Preserve all annotations and targets exactly.
|
||||
- Annotations must target the backing field in Kotlin where they targeted the field in
|
||||
Java. Use annotation site targets: `@field:`, `@get:`, `@set:`, `@param:`.
|
||||
|
||||
**Invariant 3:** Preserve the package declaration and all imports.
|
||||
- Carry forwards every single import, adding no new imports. Only remove imports where
|
||||
they would shadow Kotlin names (e.g., `java.util.List` shadows Kotlin's `List`).
|
||||
|
||||
**Invariant 4:** Preserve all Javadoc comments.
|
||||
- In step 1, convert any Javadoc comments to KDoc comments.
|
||||
- In all other steps, carry forwards the KDoc comments, preserving information where
|
||||
structure changes.
|
||||
|
||||
**Invariant 5:** Ensure the output result is in Kotlin.
|
||||
- The emitted code must be syntactically valid Kotlin.
|
||||
|
||||
**After each step**, go through each of these invariants, listing the ones that no
|
||||
longer hold. If any exist, revert to the previous step and recalculate from there.
|
||||
|
||||
---
|
||||
|
||||
## Example 1: Utility Class with Nullability
|
||||
|
||||
### Java Input
|
||||
|
||||
```java
|
||||
package com.acme.util;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Utility for printing a greeting that includes the current date.
|
||||
*/
|
||||
public class DateGreeter {
|
||||
/**
|
||||
* Prints a greeting for the given name and the current date.
|
||||
*
|
||||
* @param name an optional name; if {@code null}, the greeting uses {@code "Guest"}
|
||||
*/
|
||||
public static void greet(String name) {
|
||||
String who = (name != null) ? name : "Guest";
|
||||
System.out.println("Hello, " + who + " - today is " + LocalDate.now());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Conversion Walkthrough
|
||||
|
||||
**Step 1** — Faithful translation. The DateGreeter class is implicitly open, the
|
||||
`greet` method is static → companion object. Javadoc converted to KDoc.
|
||||
|
||||
```kotlin
|
||||
package com.acme.util
|
||||
|
||||
import java.time.LocalDate
|
||||
import java.util.Objects
|
||||
|
||||
/**
|
||||
* Utility for printing a greeting that includes the current date.
|
||||
*/
|
||||
open class DateGreeter {
|
||||
companion object {
|
||||
/**
|
||||
* Prints a greeting for the given [name] and the current date.
|
||||
*
|
||||
* @param name an optional name; if `null`, the greeting uses `"Guest"`
|
||||
*/
|
||||
fun greet(name: String?) {
|
||||
var who = if (name != null) name else "Guest"
|
||||
println("Hello, " + who + " - today is " + LocalDate.now())
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Invariants check: All OK.
|
||||
|
||||
**Step 2** — The `String?` is correct since the author checks for null. The `who`
|
||||
variable is only read, so change `var` → `val`.
|
||||
|
||||
```kotlin
|
||||
// Same as above but with:
|
||||
val who = if (name != null) name else "Guest"
|
||||
```
|
||||
|
||||
Invariants check: All OK.
|
||||
|
||||
**Step 3** — No collections in this code. No changes.
|
||||
|
||||
Invariants check: All OK.
|
||||
|
||||
**Step 4** — The `greet` function is not tied to any state of DateGreeter, so move it
|
||||
to a top-level function. Use string templates and Elvis operator.
|
||||
|
||||
```kotlin
|
||||
package com.acme.util
|
||||
|
||||
import java.time.LocalDate
|
||||
import java.util.Objects
|
||||
|
||||
/**
|
||||
* Prints a greeting for the given [name] and the current date.
|
||||
*
|
||||
* @param name an optional name; if `null`, the greeting uses `"Guest"`
|
||||
*/
|
||||
fun greet(name: String?) {
|
||||
println("Hello, ${name ?: "Guest"} - today is ${LocalDate.now()}")
|
||||
}
|
||||
```
|
||||
|
||||
Invariants check: All OK.
|
||||
|
||||
---
|
||||
|
||||
## Example 2: Domain Model with Annotations
|
||||
|
||||
### Java Input
|
||||
|
||||
```java
|
||||
package com.acme.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Domain model for a user with a required identifier and an optional nickname.
|
||||
* <p>
|
||||
* The {@code id} is serialized as {@code "id"} and is required.
|
||||
* The {@code nickname} may be absent.
|
||||
*/
|
||||
public class User {
|
||||
/**
|
||||
* Stable, non-null identifier serialized as {@code "id"}.
|
||||
*/
|
||||
@JsonProperty("id")
|
||||
private final String id;
|
||||
|
||||
/**
|
||||
* Optional nickname for display purposes.
|
||||
*/
|
||||
@Nullable
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* Creates a user with the given non-null identifier.
|
||||
*
|
||||
* @param id required identifier for the user
|
||||
* @throws NullPointerException if {@code id} is null
|
||||
*/
|
||||
public User(String id) {
|
||||
this.id = Objects.requireNonNull(id, "id");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier serialized as {@code "id"}.
|
||||
*
|
||||
* @return the user id
|
||||
*/
|
||||
@JsonProperty("id")
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the optional nickname.
|
||||
*
|
||||
* @return the nickname or {@code null} if absent
|
||||
*/
|
||||
@Nullable
|
||||
public String getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the optional nickname.
|
||||
*
|
||||
* @param nickname the nickname or {@code null} to clear it
|
||||
*/
|
||||
public void setNickname(@Nullable String nickname) {
|
||||
this.nickname = nickname;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Conversion Walkthrough
|
||||
|
||||
**Step 1** — Faithful translation. Class is implicitly open → `open class`.
|
||||
`@JsonProperty("id")` on the field → `@field:JsonProperty("id")`.
|
||||
`@JsonProperty("id")` on the getter → `@get:JsonProperty("id")` when converted to
|
||||
property later. Keep explicit getters/setters at this step for faithfulness.
|
||||
|
||||
```kotlin
|
||||
package com.acme.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import javax.annotation.Nullable
|
||||
import java.util.Objects
|
||||
|
||||
/**
|
||||
* Domain model for a user with a required identifier and an optional nickname.
|
||||
*
|
||||
* The `id` is serialized as `"id"` and is required.
|
||||
* The `nickname` may be absent.
|
||||
*/
|
||||
open class User {
|
||||
|
||||
/**
|
||||
* Stable, non-null identifier serialized as `"id"`.
|
||||
*/
|
||||
@field:JsonProperty("id")
|
||||
private val id: String
|
||||
|
||||
/**
|
||||
* Optional nickname for display purposes.
|
||||
*/
|
||||
@field:Nullable
|
||||
private var nickname: String? = null
|
||||
|
||||
/**
|
||||
* Creates a user with the given non-null identifier.
|
||||
*
|
||||
* @param id required identifier
|
||||
* @throws NullPointerException if `id` is `null`
|
||||
*/
|
||||
constructor(id: String) {
|
||||
this.id = Objects.requireNonNull(id, "id")
|
||||
}
|
||||
|
||||
@get:JsonProperty("id")
|
||||
fun getId(): String { return id }
|
||||
|
||||
@Nullable
|
||||
fun getNickname(): String? { return nickname }
|
||||
|
||||
fun setNickname(@Nullable nickname: String?) { this.nickname = nickname }
|
||||
}
|
||||
```
|
||||
|
||||
Invariants check: All OK.
|
||||
|
||||
**Step 2** — `id` is non-null by design (`Objects.requireNonNull` enforces it).
|
||||
`nickname` is nullable (`@Nullable`). No val/var changes needed beyond what's already
|
||||
done. Code unchanged.
|
||||
|
||||
Invariants check: All OK.
|
||||
|
||||
**Step 3** — No collections. No changes.
|
||||
|
||||
Invariants check: All OK.
|
||||
|
||||
**Step 4** — Idiomatic Kotlin:
|
||||
1. Primary constructor with `id` as a `val` property. Apply both `@field:JsonProperty`
|
||||
and `@get:JsonProperty` to match both Java annotation targets.
|
||||
2. Convert `nickname` getter/setter → Kotlin property with `@field:Nullable` and
|
||||
`@get:Nullable`.
|
||||
3. Drop `Objects.requireNonNull` — Kotlin's type system enforces non-null.
|
||||
4. Preserve all imports even if now unused (invariant 3).
|
||||
|
||||
```kotlin
|
||||
package com.acme.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import javax.annotation.Nullable
|
||||
import java.util.Objects
|
||||
|
||||
/**
|
||||
* Domain model for a user with a required identifier and an optional nickname.
|
||||
*
|
||||
* The `id` is serialized as `"id"` and is required.
|
||||
* The `nickname` may be absent.
|
||||
*
|
||||
* @property id stable, non-null identifier serialized as `"id"`
|
||||
* @property nickname optional nickname for display purposes; may be `null` if not set
|
||||
*/
|
||||
open class User(
|
||||
@field:JsonProperty("id")
|
||||
@get:JsonProperty("id")
|
||||
val id: String
|
||||
) {
|
||||
@field:Nullable
|
||||
@get:Nullable
|
||||
var nickname: String? = null
|
||||
}
|
||||
```
|
||||
|
||||
Invariants check: All OK.
|
||||
@@ -0,0 +1,358 @@
|
||||
# Known Issues and Common Pitfalls
|
||||
|
||||
A reference of common issues encountered during Java-to-Kotlin conversion, with solutions.
|
||||
|
||||
### Kotlin Keyword Conflicts
|
||||
|
||||
Java identifiers that are reserved keywords in Kotlin will cause compilation errors after conversion.
|
||||
|
||||
**Affected keywords:** `when`, `in`, `is`, `object`, `fun`, `val`, `var`, `typealias`, `as`
|
||||
|
||||
**Solution:** Backtick-escape them in Kotlin:
|
||||
|
||||
```java
|
||||
// Java
|
||||
public void when(String event) { ... }
|
||||
public boolean in(List<String> items) { ... }
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Kotlin — backtick-escaped
|
||||
fun `when`(event: String) { ... }
|
||||
fun `in`(items: List<String>): Boolean { ... }
|
||||
```
|
||||
|
||||
When the API is internal (not exposed to other modules), prefer renaming the identifier to a non-keyword alternative instead of using backticks. For example, rename `when` to `onEvent` or `in` to `contains`.
|
||||
|
||||
### SAM Conversion Ambiguity
|
||||
|
||||
When a Java method has overloads that each accept a different SAM (Single Abstract Method) interface, Kotlin's trailing lambda syntax becomes ambiguous. The compiler cannot determine which SAM interface the lambda should implement.
|
||||
|
||||
```java
|
||||
// Java — overloaded method accepting different SAM types
|
||||
public class TaskExecutor {
|
||||
void submit(Runnable task) { ... }
|
||||
void submit(Callable<String> task) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Kotlin — WRONG: ambiguous, won't compile
|
||||
executor.submit { doWork() }
|
||||
|
||||
// Kotlin — CORRECT: explicit SAM constructor
|
||||
executor.submit(Runnable { doWork() })
|
||||
executor.submit(Callable { computeResult() })
|
||||
```
|
||||
|
||||
Use explicit SAM constructor calls whenever there are overloaded methods accepting different functional interfaces.
|
||||
|
||||
### Platform Types
|
||||
|
||||
Java types without nullability annotations (`@Nullable`, `@NotNull`, `@NonNull`) become "platform types" (`T!`) in Kotlin. Platform types bypass Kotlin's null-safety system — they are neither nullable nor non-null, and null checks are deferred to runtime.
|
||||
|
||||
```java
|
||||
// Java — no nullability annotations
|
||||
public String getName() { return name; }
|
||||
public List<String> getItems() { return items; }
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Kotlin — BAD: platform types left in converted code
|
||||
val name = obj.name // inferred as String! — unsafe
|
||||
val items = obj.items // inferred as List<String!>! — unsafe
|
||||
|
||||
// Kotlin — GOOD: explicit nullability based on code analysis
|
||||
val name: String = obj.name // if provably non-null
|
||||
val name: String? = obj.name // if could be null
|
||||
val items: List<String> = obj.items // if neither list nor elements are null
|
||||
```
|
||||
|
||||
Always add explicit type declarations to eliminate platform types. Analyze the Java source code, documentation, and call sites to determine the correct nullability.
|
||||
|
||||
### @JvmStatic / @JvmField / @JvmOverloads
|
||||
|
||||
When converted Kotlin code is still called from Java, use JVM interop annotations to maintain a clean Java API:
|
||||
|
||||
**`@JvmStatic`** — Makes companion object functions accessible as static methods from Java:
|
||||
|
||||
```kotlin
|
||||
class Config {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getInstance(): Config = ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// Java callers can use: Config.getInstance()
|
||||
// Without @JvmStatic they would need: Config.Companion.getInstance()
|
||||
```
|
||||
|
||||
**`@JvmField`** — Exposes a property as a direct field rather than through getter/setter:
|
||||
|
||||
```kotlin
|
||||
class Constants {
|
||||
companion object {
|
||||
@JvmField
|
||||
val DEFAULT_TIMEOUT = 30_000L
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// Java callers can use: Constants.DEFAULT_TIMEOUT
|
||||
// Without @JvmField they would need: Constants.Companion.getDEFAULT_TIMEOUT()
|
||||
```
|
||||
|
||||
**`@JvmOverloads`** — Generates Java overloads for functions with default parameters:
|
||||
|
||||
```kotlin
|
||||
@JvmOverloads
|
||||
fun connect(host: String, port: Int = 443, secure: Boolean = true) { ... }
|
||||
```
|
||||
|
||||
```java
|
||||
// Java sees three overloads:
|
||||
// connect(String host)
|
||||
// connect(String host, int port)
|
||||
// connect(String host, int port, boolean secure)
|
||||
```
|
||||
|
||||
### Checked Exceptions
|
||||
|
||||
Kotlin does not have checked exceptions. When Kotlin code is called from Java, the Java compiler will not know about thrown exceptions unless annotated with `@Throws`:
|
||||
|
||||
```kotlin
|
||||
// Without @Throws, Java callers cannot catch IOException in a catch block
|
||||
// (the Java compiler will say "exception is never thrown in the corresponding try block")
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun readFile(path: String): String {
|
||||
return File(path).readText()
|
||||
}
|
||||
```
|
||||
|
||||
Add `@Throws` to every Kotlin function that throws checked exceptions and is called from Java code.
|
||||
|
||||
### Wildcard Generics
|
||||
|
||||
Java wildcard types map to Kotlin's variance annotations:
|
||||
|
||||
| Java | Kotlin | Description |
|
||||
|------|--------|-------------|
|
||||
| `? extends T` | `out T` | Covariance (producer) |
|
||||
| `? super T` | `in T` | Contravariance (consumer) |
|
||||
| Raw type `List` | `List<Any?>` | Add explicit type parameter |
|
||||
|
||||
```java
|
||||
// Java
|
||||
public void process(List<? extends Number> numbers) { ... }
|
||||
public void addAll(List<? super Integer> target) { ... }
|
||||
public void legacy(List items) { ... } // raw type
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Kotlin
|
||||
fun process(numbers: List<out Number>) { ... }
|
||||
fun addAll(target: MutableList<in Int>) { ... }
|
||||
fun legacy(items: List<Any?>) { ... } // explicit type parameter
|
||||
```
|
||||
|
||||
For raw types, analyze the code to determine the most specific type parameter rather than defaulting to `Any?`.
|
||||
|
||||
### Static Members
|
||||
|
||||
Java's `static` keyword has no direct equivalent in Kotlin. Use the following mappings:
|
||||
|
||||
**Static methods** — Use companion object functions, or top-level functions if they don't need class state:
|
||||
|
||||
```java
|
||||
// Java
|
||||
public class StringUtils {
|
||||
public static String capitalize(String s) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Kotlin — top-level function (preferred when no class state needed)
|
||||
fun capitalize(s: String): String { ... }
|
||||
|
||||
// Kotlin — companion object (when logically tied to the class)
|
||||
class StringUtils {
|
||||
companion object {
|
||||
fun capitalize(s: String): String { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Static constants** — Use `const val` for compile-time constants (primitives and String), `val` for object constants:
|
||||
|
||||
```kotlin
|
||||
class HttpStatus {
|
||||
companion object {
|
||||
const val OK = 200 // primitive — const val
|
||||
const val NOT_FOUND_MESSAGE = "Not Found" // String — const val
|
||||
val DEFAULT_HEADERS = mapOf("Accept" to "application/json") // object — val
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Static initializers** — Use companion object `init {}` block or top-level code:
|
||||
|
||||
```kotlin
|
||||
class Registry {
|
||||
companion object {
|
||||
private val handlers = mutableMapOf<String, Handler>()
|
||||
init {
|
||||
handlers["default"] = DefaultHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Synchronized Blocks
|
||||
|
||||
Java's `synchronized` constructs map to Kotlin as follows:
|
||||
|
||||
**Synchronized blocks** — Use Kotlin's `synchronized()` function:
|
||||
|
||||
```java
|
||||
// Java
|
||||
synchronized (lock) {
|
||||
sharedState.update();
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Kotlin
|
||||
synchronized(lock) {
|
||||
sharedState.update()
|
||||
}
|
||||
```
|
||||
|
||||
**Synchronized methods** — Use the `@Synchronized` annotation:
|
||||
|
||||
```java
|
||||
// Java
|
||||
public synchronized void update() { ... }
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Kotlin
|
||||
@Synchronized
|
||||
fun update() { ... }
|
||||
```
|
||||
|
||||
### Anonymous Inner Classes
|
||||
|
||||
**Single Abstract Method (SAM) interfaces** — Convert to lambda syntax:
|
||||
|
||||
```java
|
||||
// Java
|
||||
executor.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
doWork();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Kotlin
|
||||
executor.submit(Runnable { doWork() })
|
||||
```
|
||||
|
||||
**Multiple methods or abstract classes** — Use `object` expression:
|
||||
|
||||
```java
|
||||
// Java
|
||||
view.addListener(new ViewListener() {
|
||||
@Override
|
||||
public void onOpen() { ... }
|
||||
@Override
|
||||
public void onClose() { ... }
|
||||
});
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Kotlin
|
||||
view.addListener(object : ViewListener {
|
||||
override fun onOpen() { ... }
|
||||
override fun onClose() { ... }
|
||||
})
|
||||
```
|
||||
|
||||
### Array Handling
|
||||
|
||||
Java arrays map to Kotlin types as follows:
|
||||
|
||||
| Java | Kotlin | Notes |
|
||||
|------|--------|-------|
|
||||
| `String[]` | `Array<String>` | Reference type arrays |
|
||||
| `int[]` | `IntArray` | Primitive array (not `Array<Int>`) |
|
||||
| `long[]` | `LongArray` | Primitive array |
|
||||
| `double[]` | `DoubleArray` | Primitive array |
|
||||
| `boolean[]` | `BooleanArray` | Primitive array |
|
||||
| `Object[]` | `Array<Any?>` | |
|
||||
| `new int[10]` | `IntArray(10)` | Array creation |
|
||||
| `new String[10]` | `arrayOfNulls<String>(10)` | Nullable element array |
|
||||
| `String... args` | `vararg args: String` | Varargs parameter |
|
||||
|
||||
Using `Array<Int>` instead of `IntArray` causes boxing overhead — always use the specialized primitive array types.
|
||||
|
||||
### Ternary Operator
|
||||
|
||||
Kotlin has no ternary operator. Use `if`/`else` as an expression:
|
||||
|
||||
```java
|
||||
// Java
|
||||
String label = (count > 0) ? "Items: " + count : "Empty";
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Kotlin
|
||||
val label = if (count > 0) "Items: $count" else "Empty"
|
||||
```
|
||||
|
||||
### instanceof
|
||||
|
||||
Java's `instanceof` maps to Kotlin's `is` keyword. Kotlin supports smart casting, so an explicit cast after an `is` check is unnecessary:
|
||||
|
||||
```java
|
||||
// Java
|
||||
if (shape instanceof Circle) {
|
||||
Circle circle = (Circle) shape;
|
||||
double area = circle.getArea();
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Kotlin — smart cast, no explicit cast needed
|
||||
if (shape is Circle) {
|
||||
val area = shape.area // shape is automatically cast to Circle
|
||||
}
|
||||
```
|
||||
|
||||
### try-with-resources
|
||||
|
||||
Java's try-with-resources maps to Kotlin's `.use {}` extension function:
|
||||
|
||||
```java
|
||||
// Java
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
|
||||
String line = reader.readLine();
|
||||
process(line);
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Kotlin
|
||||
BufferedReader(FileReader(path)).use { reader ->
|
||||
val line = reader.readLine()
|
||||
process(line)
|
||||
}
|
||||
```
|
||||
|
||||
The `.use {}` function works on any `Closeable` or `AutoCloseable` instance and guarantees the resource is closed even if an exception is thrown.
|
||||
@@ -0,0 +1,160 @@
|
||||
# Dagger / Hilt Conversion Guide
|
||||
|
||||
## When This Applies
|
||||
|
||||
This guide applies when the Java source contains imports matching `dagger.*` or
|
||||
`dagger.hilt.*`. This covers Dagger 2, Hilt for Android, and Hilt Jetpack integrations.
|
||||
|
||||
## Key Rules
|
||||
|
||||
### 1. @Inject constructor syntax
|
||||
|
||||
Kotlin places `@Inject` before the `constructor` keyword in the primary constructor:
|
||||
|
||||
```kotlin
|
||||
class Foo @Inject constructor(private val bar: Bar)
|
||||
```
|
||||
|
||||
### 2. @Module classes with @Provides methods
|
||||
|
||||
Keep `@Provides` methods `open`, or use `object` for modules that contain only
|
||||
`@JvmStatic` provides methods (companion object pattern):
|
||||
|
||||
```kotlin
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build()
|
||||
}
|
||||
```
|
||||
|
||||
### 3. @Binds abstract methods
|
||||
|
||||
`@Binds` methods work in abstract classes exactly as in Java. Convert the abstract
|
||||
class directly — no special Kotlin considerations.
|
||||
|
||||
### 4. Hilt Android annotations
|
||||
|
||||
`@HiltAndroidApp`, `@AndroidEntryPoint`, `@HiltViewModel` — preserve these exactly
|
||||
on Application, Activity, Fragment, and ViewModel classes.
|
||||
|
||||
### 5. Scoping annotations
|
||||
|
||||
`@Singleton`, `@ActivityScoped`, `@ViewModelScoped`, `@FragmentScoped` — preserve
|
||||
exactly. No annotation site target is needed.
|
||||
|
||||
### 6. @AssistedInject / @AssistedFactory
|
||||
|
||||
`@AssistedInject` replaces `@Inject` on the constructor. `@Assisted` parameters
|
||||
appear alongside regular injected parameters in the primary constructor:
|
||||
|
||||
```kotlin
|
||||
class PlayerViewModel @AssistedInject constructor(
|
||||
@Assisted private val playerId: String,
|
||||
private val repository: PlayerRepository
|
||||
) : ViewModel()
|
||||
```
|
||||
|
||||
### 7. @Component / @Subcomponent interfaces
|
||||
|
||||
Convert directly to Kotlin interfaces. Dagger's annotation processing works
|
||||
identically with Kotlin interfaces via kapt or KSP.
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Hilt ViewModel with @Inject Constructor and a @Module
|
||||
|
||||
**Java:**
|
||||
|
||||
```java
|
||||
package com.acme.feature;
|
||||
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.hilt.InstallIn;
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
import dagger.hilt.components.SingletonComponent;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
@HiltViewModel
|
||||
public class UserProfileViewModel extends ViewModel {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final AnalyticsTracker analyticsTracker;
|
||||
|
||||
@Inject
|
||||
public UserProfileViewModel(UserRepository userRepository, AnalyticsTracker analyticsTracker) {
|
||||
this.userRepository = userRepository;
|
||||
this.analyticsTracker = analyticsTracker;
|
||||
}
|
||||
|
||||
public LiveData<User> getUser(String userId) {
|
||||
analyticsTracker.trackProfileView(userId);
|
||||
return userRepository.getUser(userId);
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent.class)
|
||||
public class AnalyticsModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public AnalyticsTracker provideAnalyticsTracker(Application app) {
|
||||
return new AnalyticsTracker(app);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Kotlin:**
|
||||
|
||||
```kotlin
|
||||
package com.acme.feature
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@HiltViewModel
|
||||
class UserProfileViewModel @Inject constructor(
|
||||
private val userRepository: UserRepository,
|
||||
private val analyticsTracker: AnalyticsTracker
|
||||
) : ViewModel() {
|
||||
|
||||
fun getUser(userId: String): LiveData<User> {
|
||||
analyticsTracker.trackProfileView(userId)
|
||||
return userRepository.getUser(userId)
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AnalyticsModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAnalyticsTracker(app: Application): AnalyticsTracker {
|
||||
return AnalyticsTracker(app)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key changes:
|
||||
- `@Inject` moves before the `constructor` keyword in the primary constructor.
|
||||
- Constructor parameters become `private val` in the primary constructor.
|
||||
- The module class becomes an `object` since it contains only static-like provides methods.
|
||||
- `SingletonComponent.class` becomes `SingletonComponent::class` (Kotlin class reference).
|
||||
- Java getter method `getUser` becomes a regular function `getUser` (no `get` prefix
|
||||
convention change needed here since it takes a parameter).
|
||||
@@ -0,0 +1,165 @@
|
||||
# Guice Conversion Guide
|
||||
|
||||
## When This Applies
|
||||
|
||||
This guide applies when the Java source contains imports matching `com.google.inject.*`.
|
||||
This covers Google Guice core, Guice multibindings, and Guice servlet.
|
||||
|
||||
## Key Rules
|
||||
|
||||
### 1. @Inject constructor syntax
|
||||
|
||||
Kotlin places `@Inject` before the `constructor` keyword in the primary constructor:
|
||||
|
||||
```kotlin
|
||||
class Foo @Inject constructor(private val bar: Bar)
|
||||
```
|
||||
|
||||
### 2. @Provides methods in Modules
|
||||
|
||||
Keep `@Provides` methods as regular functions. Guice modules extend `AbstractModule`,
|
||||
so override `configure()` as usual.
|
||||
|
||||
### 3. Module.configure() override
|
||||
|
||||
Override `configure()` in Kotlin. Use Guice's binding DSL with Kotlin class references:
|
||||
|
||||
```kotlin
|
||||
bind(Foo::class.java).to(FooImpl::class.java)
|
||||
```
|
||||
|
||||
### 4. @Named qualifier — annotation site targets
|
||||
|
||||
In Kotlin, `@Named` on constructor parameters needs a site target to reach the
|
||||
parameter (not the field or property). Use `@param:Named` for constructor injection:
|
||||
|
||||
```kotlin
|
||||
class Foo @Inject constructor(
|
||||
@param:Named("primary") private val dataSource: DataSource
|
||||
)
|
||||
```
|
||||
|
||||
When used on function parameters (e.g., in `@Provides` methods), no site target
|
||||
is needed.
|
||||
|
||||
### 5. @Singleton scope
|
||||
|
||||
Preserve `@Singleton` exactly. It can be placed on the class declaration or in
|
||||
module bindings via `.in(Singleton::class.java)`.
|
||||
|
||||
### 6. Provider<T>
|
||||
|
||||
`Provider<T>` can stay as-is for lazy or scoped injection. Where the only purpose
|
||||
is deferred initialization, Kotlin's `lazy` delegation can be used as an alternative
|
||||
outside of Guice-managed contexts.
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Guice Module with Bindings and an Injected Class
|
||||
|
||||
**Java:**
|
||||
|
||||
```java
|
||||
package com.acme.config;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Provides;
|
||||
import com.google.inject.Singleton;
|
||||
import com.google.inject.name.Named;
|
||||
|
||||
public class AppModule extends AbstractModule {
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(CacheService.class).to(RedisCacheService.class);
|
||||
bind(NotificationService.class).to(EmailNotificationService.class).in(Singleton.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public HttpClient provideHttpClient(@Named("baseUrl") String baseUrl) {
|
||||
return new HttpClient(baseUrl);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
package com.acme.service;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
|
||||
public class OrderService {
|
||||
|
||||
private final CacheService cacheService;
|
||||
private final HttpClient httpClient;
|
||||
private final String region;
|
||||
|
||||
@Inject
|
||||
public OrderService(CacheService cacheService, HttpClient httpClient, @Named("region") String region) {
|
||||
this.cacheService = cacheService;
|
||||
this.httpClient = httpClient;
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
public Order findById(Long id) {
|
||||
return cacheService.getOrFetch(id, () -> httpClient.get("/orders/" + id, Order.class));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Kotlin:**
|
||||
|
||||
```kotlin
|
||||
package com.acme.config
|
||||
|
||||
import com.google.inject.AbstractModule
|
||||
import com.google.inject.Provides
|
||||
import com.google.inject.Singleton
|
||||
import com.google.inject.name.Named
|
||||
|
||||
class AppModule : AbstractModule() {
|
||||
|
||||
override fun configure() {
|
||||
bind(CacheService::class.java).to(RedisCacheService::class.java)
|
||||
bind(NotificationService::class.java).to(EmailNotificationService::class.java).`in`(Singleton::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideHttpClient(@Named("baseUrl") baseUrl: String): HttpClient {
|
||||
return HttpClient(baseUrl)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
package com.acme.service
|
||||
|
||||
import com.google.inject.Inject
|
||||
import com.google.inject.name.Named
|
||||
|
||||
class OrderService @Inject constructor(
|
||||
private val cacheService: CacheService,
|
||||
private val httpClient: HttpClient,
|
||||
@param:Named("region") private val region: String
|
||||
) {
|
||||
|
||||
fun findById(id: Long): Order? {
|
||||
return cacheService.getOrFetch(id) { httpClient.get("/orders/$id", Order::class.java) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key changes:
|
||||
- `@Inject` moves before the `constructor` keyword in the primary constructor.
|
||||
- Constructor parameters become `private val` in the primary constructor.
|
||||
- `@Named("region")` uses `@param:Named` site target so the annotation reaches the
|
||||
constructor parameter rather than the Kotlin property.
|
||||
- `.in(Singleton.class)` becomes `` .`in`(Singleton::class.java) `` — `in` is a
|
||||
reserved keyword in Kotlin and must be escaped with backticks.
|
||||
- The lambda in `getOrFetch` uses Kotlin's trailing lambda syntax instead of an
|
||||
anonymous inner class.
|
||||
- String concatenation `"/orders/" + id` becomes a string template `"/orders/$id"`.
|
||||
@@ -0,0 +1,227 @@
|
||||
# Hibernate / JPA Conversion Guide
|
||||
|
||||
## When This Applies
|
||||
|
||||
Detected when imports match any of:
|
||||
- `javax.persistence.*`
|
||||
- `jakarta.persistence.*`
|
||||
- `org.hibernate.*`
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **Do NOT use data classes for JPA entities.** Data classes generate `equals`/`hashCode`
|
||||
based on all properties, which breaks Hibernate's identity semantics and proxy creation.
|
||||
|
||||
2. **Keep entity classes `open`.** Hibernate creates proxies via subclassing. Kotlin classes
|
||||
are `final` by default, so you must use `open` explicitly (or use the `allopen` compiler
|
||||
plugin with JPA annotation support).
|
||||
|
||||
3. **Provide a no-argument constructor** if Hibernate requires one for proxy creation. Use a
|
||||
secondary constructor or default values for all primary constructor parameters.
|
||||
|
||||
4. **Annotation site targets matter:**
|
||||
- `@Id`, `@Column`, `@GeneratedValue` on fields → use `@field:Id`, `@field:Column`, etc.
|
||||
in Kotlin, OR place annotations on constructor parameters with `@field:` site target.
|
||||
- `@ManyToOne`, `@OneToMany`, `@JoinColumn` → same `@field:` targeting.
|
||||
|
||||
5. **Lazy loading considerations:** `@ManyToOne(fetch = FetchType.LAZY)` requires the entity
|
||||
class to be open for proxy creation. `@OneToMany` with lazy collections work with Kotlin's
|
||||
`MutableList`.
|
||||
|
||||
6. **`@Embeddable` classes**: Can be data classes (they don't need proxies).
|
||||
|
||||
7. **`@MappedSuperclass`**: Must be `open abstract class` in Kotlin.
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: JPA Entity with @Id, @Column, and Relationships
|
||||
|
||||
**Java:**
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "username", nullable = false, unique = true)
|
||||
private String username;
|
||||
|
||||
@Column(name = "email")
|
||||
private String email;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "department_id")
|
||||
private Department department;
|
||||
|
||||
protected User() {}
|
||||
|
||||
public User(String username, String email, Department department) {
|
||||
this.username = username;
|
||||
this.email = email;
|
||||
this.department = department;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public String getUsername() { return username; }
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
public Department getDepartment() { return department; }
|
||||
public void setDepartment(Department department) { this.department = department; }
|
||||
}
|
||||
```
|
||||
|
||||
**Kotlin:**
|
||||
```kotlin
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
open class User(
|
||||
|
||||
@field:Column(name = "username", nullable = false, unique = true)
|
||||
open val username: String,
|
||||
|
||||
@field:Column(name = "email")
|
||||
open var email: String? = null,
|
||||
|
||||
@field:ManyToOne(fetch = FetchType.LAZY)
|
||||
@field:JoinColumn(name = "department_id")
|
||||
open var department: Department? = null
|
||||
|
||||
) {
|
||||
@field:Id
|
||||
@field:GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
open var id: Long? = null
|
||||
protected set
|
||||
|
||||
protected constructor() : this(username = "")
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: @Embeddable Value Object
|
||||
|
||||
**Java:**
|
||||
```java
|
||||
@Embeddable
|
||||
public class Address {
|
||||
|
||||
@Column(name = "street")
|
||||
private String street;
|
||||
|
||||
@Column(name = "city")
|
||||
private String city;
|
||||
|
||||
@Column(name = "zip_code")
|
||||
private String zipCode;
|
||||
|
||||
protected Address() {}
|
||||
|
||||
public Address(String street, String city, String zipCode) {
|
||||
this.street = street;
|
||||
this.city = city;
|
||||
this.zipCode = zipCode;
|
||||
}
|
||||
|
||||
public String getStreet() { return street; }
|
||||
public String getCity() { return city; }
|
||||
public String getZipCode() { return zipCode; }
|
||||
}
|
||||
```
|
||||
|
||||
**Kotlin:**
|
||||
```kotlin
|
||||
@Embeddable
|
||||
data class Address(
|
||||
|
||||
@field:Column(name = "street")
|
||||
val street: String = "",
|
||||
|
||||
@field:Column(name = "city")
|
||||
val city: String = "",
|
||||
|
||||
@field:Column(name = "zip_code")
|
||||
val zipCode: String = ""
|
||||
)
|
||||
```
|
||||
|
||||
`@Embeddable` classes can safely be data classes because Hibernate does not proxy them.
|
||||
Default values satisfy the no-arg constructor requirement.
|
||||
|
||||
### Example 3: Entity with @ManyToOne and @OneToMany
|
||||
|
||||
**Java:**
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "departments")
|
||||
public class Department {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "name", nullable = false)
|
||||
private String name;
|
||||
|
||||
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<User> users = new ArrayList<>();
|
||||
|
||||
protected Department() {}
|
||||
|
||||
public Department(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public String getName() { return name; }
|
||||
public List<User> getUsers() { return users; }
|
||||
|
||||
public void addUser(User user) {
|
||||
users.add(user);
|
||||
user.setDepartment(this);
|
||||
}
|
||||
|
||||
public void removeUser(User user) {
|
||||
users.remove(user);
|
||||
user.setDepartment(null);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Kotlin:**
|
||||
```kotlin
|
||||
@Entity
|
||||
@Table(name = "departments")
|
||||
open class Department(
|
||||
|
||||
@field:Column(name = "name", nullable = false)
|
||||
open val name: String = ""
|
||||
|
||||
) {
|
||||
@field:Id
|
||||
@field:GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
open var id: Long? = null
|
||||
protected set
|
||||
|
||||
@field:OneToMany(mappedBy = "department", cascade = [CascadeType.ALL], orphanRemoval = true)
|
||||
open val users: MutableList<User> = mutableListOf()
|
||||
|
||||
protected constructor() : this(name = "")
|
||||
|
||||
fun addUser(user: User) {
|
||||
users.add(user)
|
||||
user.department = this
|
||||
}
|
||||
|
||||
fun removeUser(user: User) {
|
||||
users.remove(user)
|
||||
user.department = null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key points in this example:
|
||||
- `cascade` array syntax uses Kotlin's `[CascadeType.ALL]` instead of Java's `{CascadeType.ALL}`.
|
||||
- The collection is typed as `MutableList` to allow Hibernate to manage the relationship.
|
||||
- The class and its properties are `open` so Hibernate can create proxies.
|
||||
- The no-arg constructor delegates to the primary constructor with default values.
|
||||
@@ -0,0 +1,248 @@
|
||||
# Jackson Conversion Guide
|
||||
|
||||
## When This Applies
|
||||
|
||||
Detected when imports match `com.fasterxml.jackson.*`.
|
||||
|
||||
## Key Rules
|
||||
|
||||
1. **Annotation site targets**:
|
||||
- `@JsonProperty` on a Java field → `@field:JsonProperty` in Kotlin.
|
||||
- `@JsonProperty` on a Java getter → `@get:JsonProperty` in Kotlin.
|
||||
- When converting to Kotlin properties, apply BOTH `@field:` and `@get:` targets to
|
||||
match Java's dual annotation on field + getter.
|
||||
|
||||
2. **@JsonCreator**: Java's `@JsonCreator` static factory or constructor → Kotlin primary
|
||||
constructor. The `@JsonCreator` annotation is often unnecessary on Kotlin's primary
|
||||
constructor if using the Jackson Kotlin module, but preserve it for safety.
|
||||
|
||||
3. **@JsonIgnore**: Preserve exactly. Use `@get:JsonIgnore` or `@field:JsonIgnore`
|
||||
depending on original target.
|
||||
|
||||
4. **@JsonDeserialize / @JsonSerialize**: Preserve exactly with correct site targets.
|
||||
|
||||
5. **@JsonInclude**: Preserve on class or property level.
|
||||
|
||||
6. **@JsonFormat**: Preserve with `@field:JsonFormat` site target.
|
||||
|
||||
7. **Jackson Kotlin Module**: Note that projects using Jackson with Kotlin should add
|
||||
`jackson-module-kotlin` for proper Kotlin support (data classes, default values,
|
||||
nullable types). This is NOT something to add during conversion — just note it if
|
||||
missing.
|
||||
|
||||
8. **Builder pattern with @JsonPOJOBuilder**: Replace with primary constructor +
|
||||
`@JsonCreator` if converting to data class. Otherwise preserve.
|
||||
|
||||
---
|
||||
|
||||
## Example 1: DTO with Various Jackson Annotations
|
||||
|
||||
### Java Input
|
||||
|
||||
```java
|
||||
package com.acme.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/**
|
||||
* Data transfer object for an order summary.
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class OrderSummaryDto {
|
||||
|
||||
@JsonProperty("order_id")
|
||||
private final String orderId;
|
||||
|
||||
@JsonProperty("total_amount")
|
||||
private final double totalAmount;
|
||||
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
|
||||
private final String createdDate;
|
||||
|
||||
@JsonIgnore
|
||||
private String internalNote;
|
||||
|
||||
public OrderSummaryDto(String orderId, double totalAmount, String createdDate) {
|
||||
this.orderId = orderId;
|
||||
this.totalAmount = totalAmount;
|
||||
this.createdDate = createdDate;
|
||||
}
|
||||
|
||||
@JsonProperty("order_id")
|
||||
public String getOrderId() {
|
||||
return orderId;
|
||||
}
|
||||
|
||||
@JsonProperty("total_amount")
|
||||
public double getTotalAmount() {
|
||||
return totalAmount;
|
||||
}
|
||||
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
|
||||
public String getCreatedDate() {
|
||||
return createdDate;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public String getInternalNote() {
|
||||
return internalNote;
|
||||
}
|
||||
|
||||
public void setInternalNote(String internalNote) {
|
||||
this.internalNote = internalNote;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Kotlin Output
|
||||
|
||||
```kotlin
|
||||
package com.acme.dto
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.annotation.JsonFormat
|
||||
|
||||
/**
|
||||
* Data transfer object for an order summary.
|
||||
*
|
||||
* @property orderId unique identifier for the order, serialized as `"order_id"`
|
||||
* @property totalAmount total monetary amount, serialized as `"total_amount"`
|
||||
* @property createdDate date the order was created, formatted as `yyyy-MM-dd`
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
open class OrderSummaryDto(
|
||||
@field:JsonProperty("order_id")
|
||||
@get:JsonProperty("order_id")
|
||||
val orderId: String?,
|
||||
|
||||
@field:JsonProperty("total_amount")
|
||||
@get:JsonProperty("total_amount")
|
||||
val totalAmount: Double,
|
||||
|
||||
@field:JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
|
||||
@get:JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
|
||||
val createdDate: String?
|
||||
) {
|
||||
@field:JsonIgnore
|
||||
@get:JsonIgnore
|
||||
var internalNote: String? = null
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- `@JsonInclude` stays at class level — no site target needed.
|
||||
- `@JsonProperty` gets both `@field:` and `@get:` to match the Java field + getter
|
||||
annotations.
|
||||
- `@JsonFormat` also gets both `@field:` and `@get:` since Java had it on both.
|
||||
- `@JsonIgnore` gets both `@field:` and `@get:` to suppress serialization fully.
|
||||
|
||||
---
|
||||
|
||||
## Example 2: Class with @JsonCreator Factory Method
|
||||
|
||||
### Java Input
|
||||
|
||||
```java
|
||||
package com.acme.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Immutable configuration entry deserialized from JSON.
|
||||
*/
|
||||
public class ConfigEntry {
|
||||
|
||||
private final String key;
|
||||
private final String value;
|
||||
private final boolean enabled;
|
||||
|
||||
@JsonCreator
|
||||
public static ConfigEntry create(
|
||||
@JsonProperty("key") String key,
|
||||
@JsonProperty("value") String value,
|
||||
@JsonProperty("enabled") boolean enabled) {
|
||||
return new ConfigEntry(key, value, enabled);
|
||||
}
|
||||
|
||||
private ConfigEntry(String key, String value, boolean enabled) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
@JsonProperty("key")
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@JsonProperty("value")
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@JsonProperty("enabled")
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Kotlin Output
|
||||
|
||||
```kotlin
|
||||
package com.acme.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
/**
|
||||
* Immutable configuration entry deserialized from JSON.
|
||||
*
|
||||
* @property key the configuration key
|
||||
* @property value the configuration value
|
||||
* @property enabled whether this entry is active
|
||||
*/
|
||||
data class ConfigEntry @JsonCreator constructor(
|
||||
@field:JsonProperty("key")
|
||||
@get:JsonProperty("key")
|
||||
val key: String?,
|
||||
|
||||
@field:JsonProperty("value")
|
||||
@get:JsonProperty("value")
|
||||
val value: String?,
|
||||
|
||||
@field:JsonProperty("enabled")
|
||||
@get:JsonProperty("enabled")
|
||||
val enabled: Boolean
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Factory method preserved for documentation; the primary constructor
|
||||
* with [JsonCreator] handles deserialization directly.
|
||||
*/
|
||||
@JsonCreator
|
||||
@JvmStatic
|
||||
fun create(
|
||||
@JsonProperty("key") key: String?,
|
||||
@JsonProperty("value") value: String?,
|
||||
@JsonProperty("enabled") enabled: Boolean
|
||||
): ConfigEntry = ConfigEntry(key, value, enabled)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- The Java `@JsonCreator` static factory is converted to a Kotlin primary constructor
|
||||
with `@JsonCreator`. The companion object factory is preserved for backward
|
||||
compatibility but the primary constructor handles deserialization.
|
||||
- The class becomes a `data class` since it is immutable and value-oriented.
|
||||
- `@JsonCreator` is kept on the primary constructor for safety, ensuring Jackson can
|
||||
deserialize even without the Jackson Kotlin module.
|
||||
- String parameters remain nullable (`String?`) since Java strings are nullable by
|
||||
default and there is no `@NonNull` or `Objects.requireNonNull` evidence.
|
||||
@@ -0,0 +1,193 @@
|
||||
# JUnit / TestNG Conversion Guide
|
||||
|
||||
## When This Applies
|
||||
|
||||
Detected when imports match `org.junit.*` or `org.testng.*`.
|
||||
|
||||
## Key Rules
|
||||
|
||||
### 1. JUnit 4 to Kotlin (with JUnit 5)
|
||||
|
||||
| JUnit 4 | Kotlin (JUnit 5 / kotlin.test) |
|
||||
|---|---|
|
||||
| `@Test` | `@Test` (from `kotlin.test` or `org.junit.jupiter.api`) |
|
||||
| `@Before` | `@BeforeEach` (JUnit 5) or `@BeforeTest` (kotlin.test) |
|
||||
| `@After` | `@AfterEach` (JUnit 5) or `@AfterTest` (kotlin.test) |
|
||||
| `@BeforeClass` | `@BeforeAll` in companion object with `@JvmStatic` |
|
||||
| `@AfterClass` | `@AfterAll` in companion object with `@JvmStatic` |
|
||||
| `@RunWith` | `@ExtendWith` (JUnit 5) |
|
||||
| `@Ignore` | `@Disabled` (JUnit 5) |
|
||||
| `@Rule` / `@ClassRule` | `@ExtendWith` or `@RegisterExtension` |
|
||||
| `Assert.assertEquals(expected, actual)` | `assertEquals(expected, actual)` (kotlin.test) |
|
||||
| `Assert.assertTrue(condition)` | `assertTrue(condition)` (kotlin.test) |
|
||||
| `@Test(expected = X.class)` | `assertFailsWith<X> { }` (kotlin.test) or `assertThrows<X> { }` (JUnit 5) |
|
||||
|
||||
### 2. JUnit 5 stays mostly the same
|
||||
|
||||
JUnit 5 annotations (`@Test`, `@BeforeEach`, `@AfterEach`, etc.) remain unchanged.
|
||||
Focus on Kotlin idioms in the test body:
|
||||
|
||||
- `assertThrows<ExceptionType> { code }` — uses reified generics, no `.class` needed.
|
||||
- Test classes and methods do not need to be `public` — Kotlin's default visibility
|
||||
is public, which satisfies JUnit's requirements.
|
||||
- Test methods do not need `open` unless using a framework that subclasses the test
|
||||
(e.g., certain Spring test configurations).
|
||||
|
||||
### 3. TestNG to Kotlin
|
||||
|
||||
| TestNG | Kotlin (JUnit 5) |
|
||||
|---|---|
|
||||
| `@Test` | `@Test` |
|
||||
| `@BeforeMethod` | `@BeforeEach` |
|
||||
| `@AfterMethod` | `@AfterEach` |
|
||||
| `@BeforeClass` | `@BeforeAll` with `@JvmStatic` in companion object |
|
||||
| `@AfterClass` | `@AfterAll` with `@JvmStatic` in companion object |
|
||||
| `@DataProvider` | `@ParameterizedTest` + `@MethodSource` |
|
||||
|
||||
### 4. Assertion style
|
||||
|
||||
Prefer `kotlin.test` assertions (`assertEquals`, `assertTrue`, `assertFailsWith`)
|
||||
for portability across test frameworks. They delegate to the underlying framework
|
||||
at runtime.
|
||||
|
||||
### 5. Backtick method names
|
||||
|
||||
Kotlin allows backtick-quoted method names for readable test names:
|
||||
```kotlin
|
||||
@Test
|
||||
fun `should return empty list when no users exist`() { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example: JUnit 4 Test Class to Kotlin with JUnit 5
|
||||
|
||||
### Java Input
|
||||
|
||||
```java
|
||||
package com.acme.service;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.junit.BeforeClass;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* Tests for the UserService class.
|
||||
*/
|
||||
public class UserServiceTest {
|
||||
|
||||
private static DatabaseConnection db;
|
||||
private UserService userService;
|
||||
|
||||
@BeforeClass
|
||||
public static void setupDatabase() {
|
||||
db = DatabaseConnection.create("test");
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
userService = new UserService(db);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
db.clearTestData();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindById() {
|
||||
User user = userService.findById(1L);
|
||||
assertNotNull(user);
|
||||
assertEquals("Alice", user.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindAllReturnsNonEmptyList() {
|
||||
List<User> users = userService.findAll();
|
||||
assertNotNull(users);
|
||||
assertTrue(users.size() > 0);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testFindByIdWithNegativeIdThrows() {
|
||||
userService.findById(-1L);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Kotlin Output
|
||||
|
||||
```kotlin
|
||||
package com.acme.service
|
||||
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Tests for the UserService class.
|
||||
*/
|
||||
class UserServiceTest {
|
||||
|
||||
companion object {
|
||||
private lateinit var db: DatabaseConnection
|
||||
|
||||
@BeforeAll
|
||||
@JvmStatic
|
||||
fun setupDatabase() {
|
||||
db = DatabaseConnection.create("test")
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var userService: UserService
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
userService = UserService(db)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
db.clearTestData()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should find user by id`() {
|
||||
val user = userService.findById(1L)
|
||||
assertNotNull(user)
|
||||
assertEquals("Alice", user.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return non-empty list from findAll`() {
|
||||
val users = userService.findAll()
|
||||
assertNotNull(users)
|
||||
assertTrue(users.isNotEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should throw IllegalArgumentException for negative id`() {
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
userService.findById(-1L)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- JUnit 4 `@Before` / `@After` → JUnit 5 `@BeforeEach` / `@AfterEach`.
|
||||
- `@BeforeClass` static method → `@BeforeAll` + `@JvmStatic` inside `companion object`.
|
||||
- `@Test(expected = ...)` → `assertFailsWith<ExceptionType> { }` with reified generics.
|
||||
- Static assertions become kotlin.test top-level function imports.
|
||||
- Test method names use backtick syntax for readability.
|
||||
- `users.size() > 0` becomes idiomatic `users.isNotEmpty()`.
|
||||
- The `db` field uses `lateinit var` since it is initialized in `@BeforeAll`.
|
||||
@@ -0,0 +1,237 @@
|
||||
# Lombok Conversion Guide
|
||||
|
||||
## When This Applies
|
||||
|
||||
Detected when imports match `lombok.*`.
|
||||
|
||||
## Core Rule
|
||||
|
||||
**Remove ALL Lombok annotations entirely.** Do not convert Lombok to Lombok — convert
|
||||
to idiomatic Kotlin equivalents. Lombok has no place in Kotlin code.
|
||||
|
||||
## Annotation Conversion Table
|
||||
|
||||
| Lombok Annotation | Kotlin Equivalent |
|
||||
|---|---|
|
||||
| `@Getter` / `@Setter` | Kotlin properties (val/var) — automatic |
|
||||
| `@Data` | `data class` with primary constructor properties |
|
||||
| `@Value` (Lombok) | `data class` with `val` properties (immutable) |
|
||||
| `@Builder` | Default parameter values, or named arguments. For complex builders, use Kotlin builder DSL |
|
||||
| `@NoArgsConstructor` | Secondary no-arg constructor, or default values for all params |
|
||||
| `@AllArgsConstructor` | Primary constructor (Kotlin default) |
|
||||
| `@RequiredArgsConstructor` | Primary constructor with only required (non-default) params |
|
||||
| `@ToString` | `data class` auto-generates toString, or manual `override fun toString()` |
|
||||
| `@EqualsAndHashCode` | `data class` auto-generates, or manual `override fun equals/hashCode` |
|
||||
| `@Slf4j` / `@Log` / `@Log4j2` | Companion object with logger (see example below) |
|
||||
| `@Cleanup` | Kotlin's `.use {}` extension function |
|
||||
| `@SneakyThrows` | Kotlin has no checked exceptions — just remove it |
|
||||
| `@Synchronized` | Kotlin's `@Synchronized` annotation |
|
||||
| `@With` | `data class` `.copy()` method |
|
||||
| `@Accessors(chain = true)` | Kotlin's `apply {}` block |
|
||||
|
||||
## Key Rules
|
||||
|
||||
1. **@Slf4j** — Convert to a companion object with an explicit logger:
|
||||
```kotlin
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(MyClass::class.java)
|
||||
}
|
||||
```
|
||||
|
||||
2. **@Data with JPA entities** — Do NOT use `data class` for JPA entities. Use regular
|
||||
`open class` with properties instead. Data classes break Hibernate proxies.
|
||||
|
||||
3. **@Builder** — Prefer default parameter values. Only create an explicit builder
|
||||
pattern if the Java code has complex builder logic beyond simple setters.
|
||||
|
||||
4. **Lombok `val`** — Replace with Kotlin's `val` (they serve the same purpose).
|
||||
|
||||
---
|
||||
|
||||
## Example 1: @Data Class with @Builder
|
||||
|
||||
### Java Input
|
||||
|
||||
```java
|
||||
package com.acme.model;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Represents a customer order with shipping details.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class Order {
|
||||
private String orderId;
|
||||
private String customerName;
|
||||
private int quantity;
|
||||
private boolean expedited;
|
||||
}
|
||||
```
|
||||
|
||||
### Kotlin Output
|
||||
|
||||
```kotlin
|
||||
package com.acme.model
|
||||
|
||||
/**
|
||||
* Represents a customer order with shipping details.
|
||||
*/
|
||||
data class Order(
|
||||
val orderId: String?,
|
||||
val customerName: String?,
|
||||
val quantity: Int = 0,
|
||||
val expedited: Boolean = false
|
||||
)
|
||||
```
|
||||
|
||||
**What changed:**
|
||||
- `@Data` → `data class` with primary constructor properties.
|
||||
- `@Builder` → default parameter values. Callers use named arguments:
|
||||
`Order(orderId = "123", customerName = "Alice", quantity = 2)`.
|
||||
- All Lombok imports removed.
|
||||
- Fields become `val` properties (immutable by default; use `var` only if mutation is
|
||||
required by the original code).
|
||||
- Reference types are nullable (`String?`) because Java fields default to `null` unless
|
||||
proven otherwise.
|
||||
|
||||
---
|
||||
|
||||
## Example 2: @Slf4j Annotated Service Class
|
||||
|
||||
### Java Input
|
||||
|
||||
```java
|
||||
package com.acme.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Service that processes incoming payment requests.
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class PaymentService {
|
||||
|
||||
private final PaymentGateway gateway;
|
||||
private final NotificationSender notifier;
|
||||
|
||||
/**
|
||||
* Processes a payment for the given amount.
|
||||
*
|
||||
* @param amount the payment amount in cents
|
||||
* @return true if the payment succeeded
|
||||
*/
|
||||
public boolean processPayment(long amount) {
|
||||
log.info("Processing payment of {} cents", amount);
|
||||
try {
|
||||
gateway.charge(amount);
|
||||
notifier.sendConfirmation(amount);
|
||||
log.info("Payment of {} cents succeeded", amount);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("Payment failed for amount {}", amount, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Kotlin Output
|
||||
|
||||
```kotlin
|
||||
package com.acme.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
* Service that processes incoming payment requests.
|
||||
*/
|
||||
open class PaymentService(
|
||||
private val gateway: PaymentGateway,
|
||||
private val notifier: NotificationSender
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(PaymentService::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a payment for the given amount.
|
||||
*
|
||||
* @param amount the payment amount in cents
|
||||
* @return true if the payment succeeded
|
||||
*/
|
||||
fun processPayment(amount: Long): Boolean {
|
||||
log.info("Processing payment of {} cents", amount)
|
||||
return try {
|
||||
gateway.charge(amount)
|
||||
notifier.sendConfirmation(amount)
|
||||
log.info("Payment of {} cents succeeded", amount)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
log.error("Payment failed for amount {}", amount, e)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**What changed:**
|
||||
- `@Slf4j` → companion object with `LoggerFactory.getLogger(...)`.
|
||||
- `@RequiredArgsConstructor` → primary constructor with `val` parameters.
|
||||
- Lombok imports replaced with `org.slf4j.LoggerFactory`.
|
||||
- `try/catch` used as an expression (idiomatic Kotlin).
|
||||
- Class is `open` because Java classes are implicitly open.
|
||||
|
||||
---
|
||||
|
||||
## Example 3: @Value (Lombok) Immutable Class
|
||||
|
||||
### Java Input
|
||||
|
||||
```java
|
||||
package com.acme.config;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
/**
|
||||
* Immutable configuration for connecting to a database.
|
||||
*/
|
||||
@Value
|
||||
public class DatabaseConfig {
|
||||
String host;
|
||||
int port;
|
||||
String databaseName;
|
||||
boolean useSsl;
|
||||
}
|
||||
```
|
||||
|
||||
### Kotlin Output
|
||||
|
||||
```kotlin
|
||||
package com.acme.config
|
||||
|
||||
/**
|
||||
* Immutable configuration for connecting to a database.
|
||||
*/
|
||||
data class DatabaseConfig(
|
||||
val host: String?,
|
||||
val port: Int,
|
||||
val databaseName: String?,
|
||||
val useSsl: Boolean
|
||||
)
|
||||
```
|
||||
|
||||
**What changed:**
|
||||
- `@Value` → `data class` with `val` properties (all immutable).
|
||||
- Lombok's `@Value` makes the class final, and Kotlin `data class` is also final by
|
||||
default — so the semantics match.
|
||||
- All Lombok imports removed.
|
||||
- Auto-generated `equals()`, `hashCode()`, `toString()`, and `copy()` come from
|
||||
`data class` for free.
|
||||
- Reference types are nullable (`String?`) since the original Java fields have no
|
||||
nullability annotations.
|
||||
@@ -0,0 +1,120 @@
|
||||
# Micronaut Conversion Guide
|
||||
|
||||
## When This Applies
|
||||
|
||||
This guide applies when the Java source contains imports matching `io.micronaut.*`.
|
||||
This covers Micronaut HTTP, Micronaut Data, and Micronaut Security.
|
||||
|
||||
## Key Rules
|
||||
|
||||
### 1. Constructor injection is the default
|
||||
|
||||
Micronaut uses compile-time dependency injection via constructor injection by default.
|
||||
This maps naturally to Kotlin's primary constructor. Remove `@Inject` when there is
|
||||
only one constructor — Micronaut discovers it automatically.
|
||||
|
||||
### 2. Stereotype annotations
|
||||
|
||||
`@Singleton`, `@Controller`, `@Client`, `@Repository` — preserve these exactly.
|
||||
No annotation site target is needed.
|
||||
|
||||
### 3. @Value annotation
|
||||
|
||||
Escape `$` in Kotlin to prevent string template interpretation:
|
||||
|
||||
```kotlin
|
||||
@Value("\${config.key}") val configKey: String
|
||||
```
|
||||
|
||||
### 4. @Inject field injection → constructor injection
|
||||
|
||||
Replace `@Inject` on fields with constructor parameters in Kotlin's primary constructor.
|
||||
This eliminates `lateinit var` and makes dependencies immutable.
|
||||
|
||||
### 5. AOP interceptors require open classes
|
||||
|
||||
Classes using AOP annotations (`@Around`, `@Introduction`, `@Cacheable`) must be `open`
|
||||
in Kotlin because Micronaut generates subclass proxies for them at compile time.
|
||||
|
||||
### 6. Bean factories
|
||||
|
||||
`@Factory` classes and their `@Bean`-annotated methods should be `open` so Micronaut
|
||||
can manage their lifecycle through subclassing.
|
||||
|
||||
### 7. @ConfigurationProperties
|
||||
|
||||
Convert to a class with mutable properties. Use `lateinit var` for required `String`
|
||||
properties and `var` with defaults for primitives. The class must be `open`.
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Micronaut Controller with Constructor Injection
|
||||
|
||||
**Java:**
|
||||
|
||||
```java
|
||||
package com.acme.web;
|
||||
|
||||
import io.micronaut.http.annotation.Controller;
|
||||
import io.micronaut.http.annotation.Get;
|
||||
import io.micronaut.http.annotation.PathVariable;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
@Controller("/api/orders")
|
||||
public class OrderController {
|
||||
|
||||
private final OrderService orderService;
|
||||
private final InventoryClient inventoryClient;
|
||||
|
||||
@Inject
|
||||
public OrderController(OrderService orderService, InventoryClient inventoryClient) {
|
||||
this.orderService = orderService;
|
||||
this.inventoryClient = inventoryClient;
|
||||
}
|
||||
|
||||
@Get("/{id}")
|
||||
public OrderDto getOrder(@PathVariable Long id) {
|
||||
return orderService.findById(id);
|
||||
}
|
||||
|
||||
@Get
|
||||
public List<OrderDto> listOrders() {
|
||||
return orderService.findAll();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Kotlin:**
|
||||
|
||||
```kotlin
|
||||
package com.acme.web
|
||||
|
||||
import io.micronaut.http.annotation.Controller
|
||||
import io.micronaut.http.annotation.Get
|
||||
import io.micronaut.http.annotation.PathVariable
|
||||
|
||||
@Controller("/api/orders")
|
||||
class OrderController(
|
||||
private val orderService: OrderService,
|
||||
private val inventoryClient: InventoryClient
|
||||
) {
|
||||
|
||||
@Get("/{id}")
|
||||
fun getOrder(@PathVariable id: Long): OrderDto? {
|
||||
return orderService.findById(id)
|
||||
}
|
||||
|
||||
@Get
|
||||
fun listOrders(): List<OrderDto> {
|
||||
return orderService.findAll()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key changes:
|
||||
- `@Inject` is removed — Micronaut auto-discovers the single constructor.
|
||||
- The `jakarta.inject.Inject` import is removed because it is no longer referenced.
|
||||
- Constructor parameters become `private val` in the primary constructor.
|
||||
- Return type `OrderDto` becomes `OrderDto?` where the service may return null.
|
||||
@@ -0,0 +1,253 @@
|
||||
# Mockito to MockK Conversion Guide
|
||||
|
||||
## When This Applies
|
||||
|
||||
Detected when imports match `org.mockito.*`.
|
||||
|
||||
While Mockito works in Kotlin, MockK is the idiomatic Kotlin mocking library. Convert
|
||||
to MockK when the project already uses MockK or is doing a full Kotlin migration. If
|
||||
the project wants to keep Mockito, convert only the Java syntax to Kotlin syntax using
|
||||
the `mockito-kotlin` helper library.
|
||||
|
||||
## Key Rules
|
||||
|
||||
### 1. MockK conversion table
|
||||
|
||||
| Mockito | MockK |
|
||||
|---|---|
|
||||
| `Mockito.mock(Foo.class)` | `mockk<Foo>()` |
|
||||
| `@Mock Foo foo` | `@MockK lateinit var foo: Foo` (with `@ExtendWith(MockKExtension::class)`) |
|
||||
| `when(foo.bar()).thenReturn(x)` | `every { foo.bar() } returns x` |
|
||||
| `when(foo.bar()).thenThrow(e)` | `every { foo.bar() } throws e` |
|
||||
| `when(foo.bar()).thenAnswer { }` | `every { foo.bar() } answers { }` |
|
||||
| `doNothing().when(foo).bar()` | `justRun { foo.bar() }` |
|
||||
| `verify(foo).bar()` | `verify { foo.bar() }` |
|
||||
| `verify(foo, times(2)).bar()` | `verify(exactly = 2) { foo.bar() }` |
|
||||
| `verify(foo, never()).bar()` | `verify(exactly = 0) { foo.bar() }` |
|
||||
| `ArgumentCaptor<T>` | `slot<T>()` and `capture(slot)` |
|
||||
| `any()` | `any()` |
|
||||
| `eq(x)` | `eq(x)` (often not needed — MockK matches exact values by default) |
|
||||
| `Mockito.spy(obj)` | `spyk(obj)` |
|
||||
| `@InjectMocks` | No direct equivalent — use constructor injection |
|
||||
| `verifyNoMoreInteractions(foo)` | `confirmVerified(foo)` |
|
||||
|
||||
### 2. Coroutine support in MockK
|
||||
|
||||
For suspending functions, use `coEvery` and `coVerify` instead of `every` and `verify`:
|
||||
```kotlin
|
||||
coEvery { foo.suspendBar() } returns x
|
||||
coVerify { foo.suspendBar() }
|
||||
```
|
||||
|
||||
### 3. Keeping Mockito (syntax-only conversion)
|
||||
|
||||
If keeping Mockito, use the `mockito-kotlin` library (`org.mockito.kotlin`) for
|
||||
Kotlin-friendly wrappers:
|
||||
- `mock<Foo>()` instead of `Mockito.mock(Foo::class.java)` — uses reified generics.
|
||||
- `whenever(foo.bar())` instead of `` Mockito.`when`(foo.bar()) `` — avoids backtick-
|
||||
escaping `when` (it is a Kotlin keyword).
|
||||
- `argumentCaptor<T>()` — type-safe captor via reified generics.
|
||||
- `any()` — properly handles Kotlin's non-null types.
|
||||
|
||||
### 4. Relaxed mocks
|
||||
|
||||
MockK supports relaxed mocks that return default values without explicit stubbing:
|
||||
`mockk<Foo>(relaxed = true)`. This has no direct Mockito equivalent (Mockito's
|
||||
`RETURNS_DEFAULTS` is the closest).
|
||||
|
||||
---
|
||||
|
||||
## Example 1: Converting to MockK
|
||||
|
||||
### Java Input
|
||||
|
||||
```java
|
||||
package com.acme.service;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
/**
|
||||
* Tests for OrderService using Mockito mocks.
|
||||
*/
|
||||
public class OrderServiceTest {
|
||||
|
||||
private UserRepository userRepository;
|
||||
private OrderRepository orderRepository;
|
||||
private OrderService orderService;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
userRepository = mock(UserRepository.class);
|
||||
orderRepository = mock(OrderRepository.class);
|
||||
orderService = new OrderService(userRepository, orderRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateOrderForUser() {
|
||||
User user = new User(1L, "Alice");
|
||||
when(userRepository.findById(1L)).thenReturn(user);
|
||||
|
||||
orderService.createOrder(1L, "ITEM-100");
|
||||
|
||||
ArgumentCaptor<Order> captor = ArgumentCaptor.forClass(Order.class);
|
||||
verify(orderRepository).save(captor.capture());
|
||||
assertEquals("ITEM-100", captor.getValue().getItemCode());
|
||||
assertEquals(1L, captor.getValue().getUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetOrderCount() {
|
||||
when(orderRepository.countByUserId(anyLong())).thenReturn(5);
|
||||
|
||||
int count = orderService.getOrderCount(1L);
|
||||
|
||||
assertEquals(5, count);
|
||||
verify(orderRepository).countByUserId(1L);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Kotlin Output (MockK)
|
||||
|
||||
```kotlin
|
||||
package com.acme.service
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Tests for OrderService using MockK mocks.
|
||||
*/
|
||||
class OrderServiceTest {
|
||||
|
||||
private val userRepository = mockk<UserRepository>()
|
||||
private val orderRepository = mockk<OrderRepository>()
|
||||
private val orderService = OrderService(userRepository, orderRepository)
|
||||
|
||||
@Test
|
||||
fun `should create order for user`() {
|
||||
val user = User(1L, "Alice")
|
||||
every { userRepository.findById(1L) } returns user
|
||||
every { orderRepository.save(any()) } returns Unit
|
||||
|
||||
orderService.createOrder(1L, "ITEM-100")
|
||||
|
||||
val orderSlot = slot<Order>()
|
||||
verify { orderRepository.save(capture(orderSlot)) }
|
||||
assertEquals("ITEM-100", orderSlot.captured.itemCode)
|
||||
assertEquals(1L, orderSlot.captured.userId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return order count`() {
|
||||
every { orderRepository.countByUserId(any()) } returns 5
|
||||
|
||||
val count = orderService.getOrderCount(1L)
|
||||
|
||||
assertEquals(5, count)
|
||||
verify { orderRepository.countByUserId(1L) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- `mock(Foo.class)` → `mockk<Foo>()` using reified generics.
|
||||
- `@Before` setUp is eliminated — mocks are initialized inline with property
|
||||
declarations. This works because MockK mocks do not require a runner.
|
||||
- `when(...).thenReturn(...)` → `every { ... } returns ...`.
|
||||
- `ArgumentCaptor` → `slot<T>()` with `capture(slot)`, accessed via `slot.captured`.
|
||||
- `anyLong()` → `any()` (MockK's `any()` handles all types).
|
||||
- `verify(foo).bar()` → `verify { foo.bar() }`.
|
||||
|
||||
---
|
||||
|
||||
## Example 2: Keeping Mockito (mockito-kotlin syntax)
|
||||
|
||||
### Java Input
|
||||
|
||||
```java
|
||||
package com.acme.service;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Tests for PricingService using Mockito.
|
||||
*/
|
||||
public class PricingServiceTest {
|
||||
|
||||
private PriceRepository priceRepository;
|
||||
private PricingService pricingService;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
priceRepository = mock(PriceRepository.class);
|
||||
pricingService = new PricingService(priceRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetPrice() {
|
||||
when(priceRepository.findPriceByItemCode("ITEM-1")).thenReturn(9.99);
|
||||
double price = pricingService.getPrice("ITEM-1");
|
||||
assertEquals(9.99, price, 0.001);
|
||||
verify(priceRepository).findPriceByItemCode("ITEM-1");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Kotlin Output (mockito-kotlin)
|
||||
|
||||
```kotlin
|
||||
package com.acme.service
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.verify
|
||||
import org.mockito.kotlin.whenever
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Tests for PricingService using Mockito.
|
||||
*/
|
||||
class PricingServiceTest {
|
||||
|
||||
private val priceRepository = mock<PriceRepository>()
|
||||
private val pricingService = PricingService(priceRepository)
|
||||
|
||||
@Test
|
||||
fun `should return price for item`() {
|
||||
whenever(priceRepository.findPriceByItemCode("ITEM-1")).thenReturn(9.99)
|
||||
|
||||
val price = pricingService.getPrice("ITEM-1")
|
||||
|
||||
assertEquals(9.99, price, 0.001)
|
||||
verify(priceRepository).findPriceByItemCode("ITEM-1")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- `mock(Foo.class)` → `mock<Foo>()` from `org.mockito.kotlin` (reified generics).
|
||||
- `when(...)` → `whenever(...)` to avoid backtick-escaping the `when` keyword.
|
||||
- `verify` stays the same — `org.mockito.kotlin.verify` wraps Mockito's verify.
|
||||
- The `setUp` method is eliminated — mocks are initialized inline.
|
||||
- `assertEquals` with a delta parameter works the same way from kotlin.test.
|
||||
@@ -0,0 +1,138 @@
|
||||
# Quarkus Conversion Guide
|
||||
|
||||
## When This Applies
|
||||
|
||||
This guide applies when the Java source contains imports matching `io.quarkus.*`,
|
||||
`javax.enterprise.*`, or `jakarta.enterprise.*`. This covers Quarkus REST, Quarkus CDI,
|
||||
and Panache ORM.
|
||||
|
||||
## Key Rules
|
||||
|
||||
### 1. CDI beans need a no-arg constructor
|
||||
|
||||
The CDI specification requires beans to have a no-arg constructor (package-private or
|
||||
public). In Kotlin, satisfy this by giving all constructor parameters default values,
|
||||
or by adding a secondary no-arg constructor.
|
||||
|
||||
### 2. Scope annotations
|
||||
|
||||
`@ApplicationScoped`, `@RequestScoped`, `@Dependent` — preserve these exactly.
|
||||
Beans with these scopes must have a no-arg constructor accessible to CDI.
|
||||
|
||||
### 3. @Inject field injection → constructor injection
|
||||
|
||||
Replace `@Inject` on fields with an `@Inject`-annotated primary constructor in Kotlin.
|
||||
CDI requires the `@Inject` annotation on the constructor when multiple constructors
|
||||
exist. With a single constructor, Quarkus discovers it automatically.
|
||||
|
||||
### 4. REST endpoint annotations
|
||||
|
||||
`@Path`, `@GET`, `@POST`, `@PUT`, `@DELETE`, `@Produces`, `@Consumes` — preserve
|
||||
these exactly. No annotation site target is needed.
|
||||
|
||||
### 5. Panache entities
|
||||
|
||||
Panache entities must remain `open` — do NOT use `data class`. Extend `PanacheEntity`
|
||||
(auto-generated Long ID) or `PanacheEntityBase` (custom ID type). Keep fields as
|
||||
`open` mutable properties because Panache enhances field access at build time.
|
||||
|
||||
### 6. @ConfigProperty
|
||||
|
||||
Use on constructor parameters with a default value to satisfy CDI's no-arg
|
||||
constructor requirement:
|
||||
|
||||
```kotlin
|
||||
@ConfigProperty(name = "app.greeting") val greeting: String = ""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: REST Resource with CDI Injection
|
||||
|
||||
**Java:**
|
||||
|
||||
```java
|
||||
package com.acme.web;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.util.List;
|
||||
|
||||
@Path("/api/products")
|
||||
@ApplicationScoped
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class ProductResource {
|
||||
|
||||
@Inject
|
||||
ProductService productService;
|
||||
|
||||
@Inject
|
||||
PricingService pricingService;
|
||||
|
||||
@GET
|
||||
public List<ProductDto> listProducts() {
|
||||
return productService.findAll();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
public ProductDto getProduct(@PathParam("id") Long id) {
|
||||
return productService.findById(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Kotlin:**
|
||||
|
||||
```kotlin
|
||||
package com.acme.web
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.ws.rs.GET
|
||||
import jakarta.ws.rs.Path
|
||||
import jakarta.ws.rs.PathParam
|
||||
import jakarta.ws.rs.Produces
|
||||
import jakarta.ws.rs.core.MediaType
|
||||
|
||||
@Path("/api/products")
|
||||
@ApplicationScoped
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
class ProductResource @Inject constructor(
|
||||
private val productService: ProductService,
|
||||
private val pricingService: PricingService
|
||||
) {
|
||||
|
||||
// No-arg constructor required by CDI — default values satisfy this
|
||||
constructor() : this(
|
||||
productService = ProductService(),
|
||||
pricingService = PricingService()
|
||||
)
|
||||
|
||||
@GET
|
||||
fun listProducts(): List<ProductDto> {
|
||||
return productService.findAll()
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
fun getProduct(@PathParam("id") id: Long): ProductDto? {
|
||||
return productService.findById(id)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key changes:
|
||||
- `@Inject` field injection is replaced by an `@Inject`-annotated primary constructor.
|
||||
- A secondary no-arg constructor is added to satisfy the CDI specification. In practice,
|
||||
CDI will use the `@Inject` constructor — the no-arg constructor exists only to pass
|
||||
validation.
|
||||
- Constructor parameters become `private val` in the primary constructor.
|
||||
- Return type `ProductDto` becomes `ProductDto?` where the service may return null.
|
||||
@@ -0,0 +1,151 @@
|
||||
# Retrofit / OkHttp Conversion Guide
|
||||
|
||||
## When This Applies
|
||||
|
||||
Detected when imports match `retrofit2.*` or `okhttp3.*`.
|
||||
|
||||
## Key Rules
|
||||
|
||||
### 1. Interface declarations
|
||||
|
||||
Retrofit service interfaces convert directly — Kotlin interfaces are structurally
|
||||
identical to Java interfaces for this purpose.
|
||||
|
||||
### 2. Call\<T\> to suspend functions
|
||||
|
||||
Replace `Call<T>` return types with `suspend fun` returning `T` directly. This requires
|
||||
the Retrofit coroutine adapter (built-in since Retrofit 2.6.0). The `Callback<T>`
|
||||
async pattern is eliminated entirely.
|
||||
|
||||
### 3. Annotation preservation
|
||||
|
||||
All Retrofit annotations transfer directly with no changes:
|
||||
- HTTP method annotations: `@GET`, `@POST`, `@PUT`, `@DELETE`, `@PATCH`, `@HTTP`
|
||||
- Header annotations: `@Headers`, `@Header`, `@HeaderMap`
|
||||
- Parameter annotations: `@Path`, `@Query`, `@QueryMap`, `@Body`, `@Field`,
|
||||
`@FieldMap`, `@Part`, `@PartMap`
|
||||
- `@FormUrlEncoded`, `@Multipart`, `@Streaming`
|
||||
|
||||
### 4. Response\<T\> handling
|
||||
|
||||
For endpoints where HTTP status codes matter, keep `Response<T>` as the return type
|
||||
with `suspend fun`. For simple cases where only the body is needed, return `T` directly
|
||||
and let Retrofit throw on non-2xx responses.
|
||||
|
||||
### 5. OkHttpClient.Builder
|
||||
|
||||
Java builder chains convert directly. Use `.apply {}` or `.also {}` for grouping
|
||||
related configuration:
|
||||
|
||||
```kotlin
|
||||
val client = OkHttpClient.Builder().apply {
|
||||
connectTimeout(30, TimeUnit.SECONDS)
|
||||
readTimeout(30, TimeUnit.SECONDS)
|
||||
addInterceptor(loggingInterceptor)
|
||||
}.build()
|
||||
```
|
||||
|
||||
### 6. Interceptor SAM conversion
|
||||
|
||||
Java `Interceptor` anonymous classes become Kotlin SAM lambdas:
|
||||
`Interceptor { chain -> chain.proceed(chain.request()) }`
|
||||
|
||||
### 7. Request/Response body handling
|
||||
|
||||
`RequestBody.create(mediaType, content)` → `content.toRequestBody(mediaType)` when
|
||||
using the `okhttp3-kotlin-extensions` artifact (or `okhttp-bom` with Kotlin extensions).
|
||||
|
||||
---
|
||||
|
||||
## Example: Retrofit Interface with Coroutine Support
|
||||
|
||||
### Java Input
|
||||
|
||||
```java
|
||||
package com.acme.api;
|
||||
|
||||
import java.util.List;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Headers;
|
||||
import retrofit2.http.PATCH;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
/**
|
||||
* Retrofit service interface for the Users API.
|
||||
*/
|
||||
public interface UserApi {
|
||||
|
||||
@GET("users")
|
||||
Call<List<UserDto>> getUsers(@Query("page") int page, @Query("limit") int limit);
|
||||
|
||||
@GET("users/{id}")
|
||||
Call<UserDto> getUserById(@Path("id") long id);
|
||||
|
||||
@POST("users")
|
||||
@Headers("Content-Type: application/json")
|
||||
Call<UserDto> createUser(@Body CreateUserRequest request);
|
||||
|
||||
@PATCH("users/{id}")
|
||||
Call<UserDto> updateUser(@Path("id") long id, @Body UpdateUserRequest request);
|
||||
|
||||
@DELETE("users/{id}")
|
||||
Call<Void> deleteUser(@Path("id") long id);
|
||||
}
|
||||
```
|
||||
|
||||
### Kotlin Output
|
||||
|
||||
```kotlin
|
||||
package com.acme.api
|
||||
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.PATCH
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
/**
|
||||
* Retrofit service interface for the Users API.
|
||||
*/
|
||||
interface UserApi {
|
||||
|
||||
@GET("users")
|
||||
suspend fun getUsers(@Query("page") page: Int, @Query("limit") limit: Int): List<UserDto>
|
||||
|
||||
@GET("users/{id}")
|
||||
suspend fun getUserById(@Path("id") id: Long): UserDto
|
||||
|
||||
@POST("users")
|
||||
@Headers("Content-Type: application/json")
|
||||
suspend fun createUser(@Body request: CreateUserRequest): UserDto
|
||||
|
||||
@PATCH("users/{id}")
|
||||
suspend fun updateUser(@Path("id") id: Long, @Body request: UpdateUserRequest): UserDto
|
||||
|
||||
@DELETE("users/{id}")
|
||||
suspend fun deleteUser(@Path("id") id: Long): Response<Unit>
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- `Call<T>` is removed — each method becomes a `suspend fun` returning `T` directly.
|
||||
Retrofit 2.6.0+ supports this natively without an additional adapter.
|
||||
- `Call<Void>` becomes `Response<Unit>`. `Unit` is Kotlin's equivalent of `Void`.
|
||||
`Response<Unit>` is used here to allow checking the HTTP status code on delete.
|
||||
- `Call` and `Callback` imports are removed since they are no longer referenced.
|
||||
- All HTTP method and parameter annotations (`@GET`, `@POST`, `@Path`, `@Query`,
|
||||
`@Body`, `@Headers`, etc.) are preserved exactly as-is.
|
||||
- Java `int` → Kotlin `Int`, Java `long` → Kotlin `Long`.
|
||||
- The `public` modifier on the interface is removed — Kotlin's default visibility
|
||||
is public.
|
||||
@@ -0,0 +1,181 @@
|
||||
# RxJava to Coroutines/Flow Conversion Guide
|
||||
|
||||
## When This Applies
|
||||
|
||||
Detected when imports match `io.reactivex.*` or `rx.*`. This is a significant paradigm
|
||||
shift — RxJava reactive types map to Kotlin coroutines and Flow.
|
||||
|
||||
## Key Rules
|
||||
|
||||
### 1. Dependency setup
|
||||
|
||||
Add `kotlinx-coroutines-core` and `kotlinx-coroutines-rx3` (or `kotlinx-coroutines-rx2`)
|
||||
as dependencies if performing a gradual migration. The bridge library provides extension
|
||||
functions like `asFlow()` and `asObservable()` for interop at module boundaries.
|
||||
|
||||
### 2. Type mapping
|
||||
|
||||
| RxJava | Kotlin |
|
||||
|---|---|
|
||||
| `Observable<T>` | `Flow<T>` |
|
||||
| `Flowable<T>` | `Flow<T>` (backpressure is built-in) |
|
||||
| `Single<T>` | `suspend fun`: T |
|
||||
| `Maybe<T>` | `suspend fun`: T? |
|
||||
| `Completable` | `suspend fun` returning `Unit` |
|
||||
| `Disposable` | `Job` (from coroutines) |
|
||||
| `CompositeDisposable` | `CoroutineScope` (structured concurrency) |
|
||||
|
||||
### 3. Operator mapping
|
||||
|
||||
| RxJava | Kotlin Flow |
|
||||
|---|---|
|
||||
| `subscribeOn(Schedulers.io())` | `flowOn(Dispatchers.IO)` |
|
||||
| `observeOn(AndroidSchedulers.mainThread())` | `flowOn(Dispatchers.Main)` or collect on Main |
|
||||
| `flatMap` | `flatMapMerge` or `flatMapConcat` |
|
||||
| `map` | `map` (same) |
|
||||
| `filter` | `filter` (same) |
|
||||
| `zip` | `combine` or `zip` |
|
||||
| `merge` | `merge` |
|
||||
| `concat` | `flatMapConcat` |
|
||||
| `onErrorReturn` | `catch { emit(default) }` |
|
||||
| `doOnNext` | `onEach` |
|
||||
| `subscribe()` | `collect {}` in a coroutine scope |
|
||||
|
||||
### 4. Error handling
|
||||
|
||||
RxJava's `onError` callback maps to Flow's `catch` operator or a try-catch block
|
||||
wrapping the `collect` call. In suspend functions (replacing `Single`/`Completable`),
|
||||
use standard try-catch.
|
||||
|
||||
### 5. Backpressure
|
||||
|
||||
Flow has built-in backpressure via suspension. There is no need for a separate
|
||||
`Flowable` type — all `Flow` instances support backpressure by default.
|
||||
|
||||
### 6. Threading
|
||||
|
||||
`flowOn` changes the upstream dispatcher (analogous to `subscribeOn`). Collection
|
||||
always happens on the caller's dispatcher. To collect on a specific dispatcher,
|
||||
launch the collecting coroutine in the desired scope.
|
||||
|
||||
### 7. Lifecycle and cancellation
|
||||
|
||||
RxJava's `Disposable` / `CompositeDisposable` pattern is replaced by structured
|
||||
concurrency. Cancelling a `CoroutineScope` cancels all child coroutines and flow
|
||||
collections automatically.
|
||||
|
||||
---
|
||||
|
||||
## Example: Converting an Observable Chain to Flow
|
||||
|
||||
### Java Input
|
||||
|
||||
```java
|
||||
package com.acme.data;
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
/**
|
||||
* Repository that streams user data from a remote source.
|
||||
*/
|
||||
public class UserRepository {
|
||||
|
||||
private final UserApi api;
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
public UserRepository(UserApi api) {
|
||||
this.api = api;
|
||||
}
|
||||
|
||||
public Observable<List<User>> getActiveUsers() {
|
||||
return api.getAllUsers()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map(users -> filterActive(users))
|
||||
.doOnNext(users -> logCount(users))
|
||||
.onErrorReturn(throwable -> Collections.emptyList());
|
||||
}
|
||||
|
||||
public void observeUsers(UserCallback callback) {
|
||||
disposables.add(
|
||||
getActiveUsers()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
users -> callback.onUsers(users),
|
||||
error -> callback.onError(error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
private List<User> filterActive(List<User> users) {
|
||||
return users.stream().filter(User::isActive).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void logCount(List<User> users) {
|
||||
System.out.println("Active users: " + users.size());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Kotlin Output
|
||||
|
||||
```kotlin
|
||||
package com.acme.data
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Repository that streams user data from a remote source.
|
||||
*/
|
||||
class UserRepository(
|
||||
private val api: UserApi
|
||||
) {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
|
||||
fun getActiveUsers(): Flow<List<User>> =
|
||||
api.getAllUsers()
|
||||
.map { users -> users.filter { it.isActive } }
|
||||
.onEach { users -> println("Active users: ${users.size}") }
|
||||
.catch { emit(emptyList()) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
||||
fun observeUsers(callback: UserCallback) {
|
||||
scope.launch {
|
||||
getActiveUsers().collect { users ->
|
||||
callback.onUsers(users)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- `Observable<List<User>>` becomes `Flow<List<User>>`.
|
||||
- `subscribeOn(Schedulers.io())` becomes `flowOn(Dispatchers.IO)` at the end of the
|
||||
chain (it affects all upstream operators).
|
||||
- `CompositeDisposable` is replaced by a `CoroutineScope` with `SupervisorJob`.
|
||||
Calling `scope.cancel()` cancels all active collections.
|
||||
- `doOnNext` becomes `onEach`.
|
||||
- `onErrorReturn` becomes `catch { emit(emptyList()) }`.
|
||||
- `observeOn(AndroidSchedulers.mainThread())` is unnecessary because `scope` already
|
||||
uses `Dispatchers.Main`, and `collect` runs on the collector's dispatcher.
|
||||
- Java streams (`filter` + `collect`) become Kotlin's `filter` directly on the list.
|
||||
@@ -0,0 +1,238 @@
|
||||
# Spring Framework Conversion Guide
|
||||
|
||||
## When This Applies
|
||||
|
||||
This guide applies when the Java source contains imports matching `org.springframework.*`.
|
||||
This covers Spring Boot, Spring MVC, Spring Data, and Spring Security.
|
||||
|
||||
## Key Rules
|
||||
|
||||
### 1. SpringApplication.run — spread CLI args
|
||||
|
||||
In Kotlin, `String[]` varargs must be spread with the `*` operator.
|
||||
|
||||
- Java: `SpringApplication.run(App.class, args);`
|
||||
- Kotlin: `SpringApplication.run(App::class.java, *args)`
|
||||
|
||||
### 2. Constructor injection over @Autowired
|
||||
|
||||
Kotlin's primary constructor makes constructor injection natural. When a class has a
|
||||
single constructor, Spring auto-discovers it — remove `@Autowired`.
|
||||
|
||||
### 3. Stereotype annotations
|
||||
|
||||
`@Component`, `@Service`, `@RestController`, and `@Repository` target the class.
|
||||
Preserve these annotations exactly. No annotation site target is needed.
|
||||
|
||||
### 4. @Value annotation
|
||||
|
||||
Use `@Value` on constructor parameters. Escape `$` in SpEL expressions to prevent
|
||||
Kotlin string template interpretation:
|
||||
|
||||
```kotlin
|
||||
@Value("\${app.name}") val appName: String
|
||||
```
|
||||
|
||||
### 5. @ConfigurationProperties
|
||||
|
||||
Convert to a `data class` only if the properties are immutable. For mutable
|
||||
configuration, use a regular class with `lateinit var`.
|
||||
|
||||
### 6. Spring Data repositories
|
||||
|
||||
Interface declarations convert directly. Replace `Optional<T>` return types with
|
||||
nullable `T?` in Kotlin for idiomatic usage.
|
||||
|
||||
### 7. @RequestMapping / @GetMapping / @PostMapping etc.
|
||||
|
||||
Preserve exactly. Where Java uses array initializer syntax for annotation parameters,
|
||||
use `arrayOf()` in Kotlin.
|
||||
|
||||
### 8. @Transactional
|
||||
|
||||
Preserve exactly. The class must remain `open` because Spring creates proxies via
|
||||
subclassing. Do not make `@Transactional` classes `final`.
|
||||
|
||||
### 9. @Bean methods in @Configuration classes
|
||||
|
||||
`@Bean` methods must be `open` so that Spring can override them in CGLIB proxies.
|
||||
Alternatively, apply the `allopen` compiler plugin with a Spring preset, which makes
|
||||
annotated classes and their members open automatically.
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Spring Boot Application Main Class
|
||||
|
||||
**Java:**
|
||||
|
||||
```java
|
||||
package com.acme;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Kotlin:**
|
||||
|
||||
```kotlin
|
||||
package com.acme
|
||||
|
||||
import org.springframework.boot.SpringApplication
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
@SpringBootApplication
|
||||
open class Application
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<Application>(*args)
|
||||
}
|
||||
```
|
||||
|
||||
Key changes:
|
||||
- `main` becomes a top-level function (no companion object needed).
|
||||
- `runApplication<T>` is a Spring Boot Kotlin extension that replaces
|
||||
`SpringApplication.run(T::class.java, *args)`.
|
||||
- The `*args` spread operator is required for the varargs parameter.
|
||||
|
||||
---
|
||||
|
||||
### Example 2: REST Controller with Constructor Injection
|
||||
|
||||
**Java:**
|
||||
|
||||
```java
|
||||
package com.acme.web;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@Autowired
|
||||
public UserController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public UserDto getUser(@PathVariable Long id) {
|
||||
return userService.findById(id);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<UserDto> getAllUsers() {
|
||||
return userService.findAll();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Kotlin:**
|
||||
|
||||
```kotlin
|
||||
package com.acme.web
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
class UserController(
|
||||
private val userService: UserService
|
||||
) {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
fun getUser(@PathVariable id: Long): UserDto? {
|
||||
return userService.findById(id)
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
fun getAllUsers(): List<UserDto> {
|
||||
return userService.findAll()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key changes:
|
||||
- `@Autowired` is removed — Spring auto-discovers the single constructor.
|
||||
- The `Autowired` import is removed because it is no longer referenced.
|
||||
- Constructor parameter becomes a `private val` in the primary constructor.
|
||||
- Return type `UserDto` becomes `UserDto?` where the service may return null.
|
||||
|
||||
---
|
||||
|
||||
### Example 3: @ConfigurationProperties Class
|
||||
|
||||
**Java:**
|
||||
|
||||
```java
|
||||
package com.acme.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "app.mail")
|
||||
public class MailProperties {
|
||||
|
||||
private String host;
|
||||
private int port = 587;
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
public String getHost() { return host; }
|
||||
public void setHost(String host) { this.host = host; }
|
||||
|
||||
public int getPort() { return port; }
|
||||
public void setPort(int port) { this.port = port; }
|
||||
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
}
|
||||
```
|
||||
|
||||
**Kotlin (mutable config with lateinit var):**
|
||||
|
||||
```kotlin
|
||||
package com.acme.config
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "app.mail")
|
||||
open class MailProperties {
|
||||
lateinit var host: String
|
||||
var port: Int = 587
|
||||
lateinit var username: String
|
||||
lateinit var password: String
|
||||
}
|
||||
```
|
||||
|
||||
Key changes:
|
||||
- Getters and setters are replaced by Kotlin properties.
|
||||
- `lateinit var` is used for required `String` properties that Spring populates
|
||||
after construction.
|
||||
- `port` keeps its default value and uses a regular `var` (`lateinit` does not
|
||||
support primitive types).
|
||||
- The class is `open` so that Spring can create a CGLIB proxy for it.
|
||||
Reference in New Issue
Block a user