refactor(page): 删除旧版页面文件

This commit is contained in:
2026-08-16 05:31:45 +08:00
parent ca76678afc
commit 1396487af9
88 changed files with 12633 additions and 1081 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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).

View File

@@ -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"`.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.