Избегайте встроенных дженериков Kotlin

Я пытаюсь создать универсальный класс для обработки инъекций конфигурации в Kotlin и Java.

В основном я хочу, чтобы класс был привязан к классу данных и пути к файлу ресурсов, который должен содержать легко записываемую десериализацию экземпляра класса данных.

То, что я придумал до сих пор, это:

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue

class ResourceLoader()

inline fun <reified T : Any> loadObject(resourcePath: String): T {
    // Load resource file
    val resource = ResourceLoader::class.java.classLoader.getResource(resourcePath)
    val resourceContent = resource.readText()

    // Return deserialized object
    return jacksonObjectMapper().readValue(resourceContent)
}


abstract class ResourceBound<T : Any>(val resourcePath: String) {
    inline fun <reified U : T> getResource(): U {
        return loadObject(this.resourcePath)
    }
}

Таким образом, я могу связать тестовый класс с наличием файла ресурсов, и он потерпит неудачу в великолепной исключительной ситуации, если файл отсутствует или искажен, например:

data class ServiceConfig(val endpoint: String, val apiKey: String)

class TestClassLoadingConfig() : ResourceBound<ServiceConfig>("TestConfig.json") {
    @Test
    fun testThis() {
        val config: ServiceConfig = this.getResource()
        val client = ServiceClient(config.endpoint, config.apiKey)
        ...
    }
}

Единственная проблема в том, что он работает только в Котлине, потому что inline не совместим с java. Итак, как мне обойти это?

В качестве бонуса я хотел бы избавиться от явного объявления типа, чтобы val config: ServiceConfig = this.getResource() могло быть просто val config = this.getResource().


person beruic    schedule 14.08.2018    source источник
comment
Вместо этого вы можете использовать непроверенное приведение, например. просто пишу return jacksonObjectMapper().readValue(resourceContent) as T.   -  person Roland    schedule 14.08.2018
comment
Затем я получаю Cannot use 'T' as reified type parameter. Use a class instead., если использую Kotlin'ized jacksonObjectMapper(). Если вместо этого я попробую ObjectMapper().readValue(resource, object : TypeReference<T>() {}) as T, я получу исключение времени выполнения: java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to dk.whatever...   -  person beruic    schedule 14.08.2018
comment
извините, моя ошибка... при un/marshalling вам нужен конкретный тип... это то, что вы поставили также с TypeReference<T>() {}... по крайней мере, я так думал... если вы просто передаете сам тип (например, dk.whatever.....class) , тогда это работает?   -  person Roland    schedule 14.08.2018
comment
Неа. Сейчас я пытаюсь закодировать это на Java, и я посмотрю, работает ли это на Котлине.   -  person beruic    schedule 14.08.2018
comment
Это все больше похоже на ограничение Джексона. Моя реализация Java также приводит к ClassCastException   -  person beruic    schedule 14.08.2018
comment
Я пока оставлю этот вопрос как есть. К сожалению, я удалил свою реализацию Java, поэтому не могу ею поделиться. На данный момент я буду простым в этом; вместо десериализации в пользовательский класс я всегда буду десериализовать в Map<String, String>.   -  person beruic    schedule 14.08.2018


Ответы (1)


Недавно я наткнулся на ту же проблему и решил ее, передав TypeReference в качестве второго параметра.

Мой базовый класс конфигурации:

package config

import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.module.kotlin.KotlinModule

open class BaseConfig<T>(configFile: String, typeReference: TypeReference<T>) {

    val config: T

    init {
        val mapper = ObjectMapper(YAMLFactory())
        mapper.registerModule(KotlinModule())

        val inputStream = this.javaClass.classLoader
            .getResourceAsStream(configFile)
        config = mapper.readValue(inputStream, typeReference)
    }
}

Пример класса данных:

package config.dto

data class MessageDto(
    val `example messages`: List<String>
)

Пример объекта:

package config

import com.fasterxml.jackson.core.type.TypeReference
import config.dto.MessageDto

object Message: BaseConfig<MessageDto>("messages.yml", object: TypeReference<MessageDto>() {}) {
    val exampleMessages: List<String> = config.`example messages`
}

Пример ямла:

example messages:
  - "An example message"
  - "Another example message"

Это не так лаконично, как хотелось бы, но оно работает и дает мне гибкий способ сопоставления содержимого файлов конфигурации с любым классом данных.

person flesk    schedule 06.05.2019