Search code examples
springkotlindto

Is it possible to Autowire a KSP generated class without writing any additional boilerplate?


I have an interface for creating Dto/Domain mappers using Konvert, which uses KSP.

@Konverter
interface SimpleNoteMapper {
  fun toDto(note: SimpleNote): SimpleNoteDto
  fun toDomain(note: SimpleNoteDto): SimpleNote
}

The generated class is:

public object SimpleNoteMapperImpl : SimpleNoteMapper {
  @GeneratedKonverter(priority = 5_000)
  override fun toDto(note: SimpleNote): SimpleNoteDto = SimpleNoteDto(
    id = note.id,
    name = note.name
  )

  @GeneratedKonverter(priority = 5_000)
  override fun toDomain(note: SimpleNoteDto): SimpleNote = SimpleNote(
    id = note.id,
    name = note.name
  )
}

I want to inject the generated class using @Autowired into my Controller.

@RestController
class SimpleNotesController {
  @Autowired
  lateinit var simpleNoteMapper: SimpleNoteMapper
  [...]
}

But the error I get, is:

Field simpleNoteMapper in com.project.example.simplenotes.api.controller.SimpleNotesController required a bean of type 'com.project.example.simplenotes.api.mapper.SimpleNoteMapper' that could not be found.

The injection point has the following annotations:
    - @org.springframework.beans.factory.annotation.Autowired(required=true)


Action:

Consider defining a bean of type 'com.project.example.simplenotes.api.mapper.SimpleNoteMapper' in your configuration.

I understand that I'd probably need to be able to add an annotation to that generated class, right? But that's not possible because it's generated. I can solve this by creating a Configuration for this mapper:

@Configuration
class MapperConfiguration {
  @Bean
  fun simpleNoteMapper(): SimpleNoteMapper {
    return SimpleNoteMapperImpl
  }
}

The application works, but I am unsure whether there isn't some way to remove this boilerplate, because I don't feel it's a good idea to create a configuration for each generated class. So I am looking for something that will simplify this to one annotation or to one class that would be able to cover all the Mappers. Thanks for reading.


Solution

  • While it it seems there is some Spring behavior that automatically matches interfaces to implementations (e.g. InterfaceA matches to InterfaceAImpl), it doesn't work in this case, because Konvert is using object instead of class in the generated code (possibly).

    Fortunately, Konvert has plugins, that solve this problem.

    I had to add:

        implementation("io.mcarle:konvert-spring-annotations:2.4.0")
        ksp("io.mcarle:konvert-spring-injector:2.4.0")
    

    to my build.gradle.kts file and then add the @KComponent annotation to the mapper interface:

    @Konverter
    @KComponent
    interface SimpleNoteMapper {
      fun toDto(note: SimpleNote): SimpleNoteDto
      fun toDomain(note: SimpleNoteDto): SimpleNote
    }
    

    This generates a Spring-aware Mapper that can be injected without additional boilerplate.