refactor(page): 删除旧版页面文件
This commit is contained in:
@@ -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