tutracker, kotlin, spring boot

В преддверии первого релиза языка kotlin, я хотел бы поделиться с вами опытом создания на нем небольшого проекта. Это будет приложение-сервис, для поиска торрентов в базе rutracker-а. Весь код + бонусный браузерный клиент можно найти здесь. Итак, посмотрим, что же получилось.

Задача


База торрентов раздается в виде набора csv-файлов и периодически обновляется путем добавлением новой версии дампа всей базы в директорию с именем, соответствующим дате создания дампа. В связи с этим, наш небольшой проект будет следить за появлением новых версий (уже скаченных, а клиент, который сам будет скачивать базу мы возможно сделаем в другой раз), разбирать, складывать в базу и предоставлять json rest api для поиска по имени.

Средства


Для быстрого старта возьмем spring boot. У spring boot есть много особенностей, которые могут серьезно осложнить жизнь в больших проектах, но для небольших приложений, как наше, boot — это отличное решение, позволяющее создать конфигурацию для типового набора технологий. Основной способ у boot-а понять, для каких именно технологий создавать бины, — это наличие в classpath ключевых для той или иной технологии классов. Мы добавляем их через подключение зависимостей в maven. В нашем случае boot автоматически сконфигурирует нам соединение с базой (h2) + пул (tomcat-jdbc) и провайдер json (gson). Версии библиотек при подключении зависимости мы не указываем, берем из заранее определенного boot-ом набора — для этого мы указываем в мавене родительский проект spring-boot-starter-parent. Так же мы подключаем spring-boot-starter-web и spring-boot-starter-tomcat, чтобы boot сконфигурировал нам web mvc для нашего будущего rest-а и tomcat в качестве контейнера. Теперь давайте посмотрим на main.
// main.kt
fun main(args: Array<String>) {
    SpringApplication
            .run(MainConfiguration::class.java, *args)
}

И на основную конфигурацию MainConfiguration, которую мы передаем в SpringApplication в качестве источника для бинов.
@Configuration
@Import(JdbcRepositoriesConfiguration::class, ImportConfiguration::class, RestConfiguration::class)
@EnableAutoConfiguration
open class MainConfiguration : SpringBootServletInitializer() {

    override fun configure(builder: SpringApplicationBuilder): SpringApplicationBuilder {
        return builder.sources(MainConfiguration::class.java)
    }

}

Надо заметить, что boot позволяет деплоить получившееся приложение в качестве web-модуля, а не только запускать через main-метод. Для того, чтобы такой подход тоже работал, мы переопределяем метод configure у SpringBootServletInitializer, который будет вызван контейнером при деплое приложения. Так же, обратите внимание, что мы не используем аннотацию @SpringBootApplication на MainConfiguration, но включаем автоконфигурирование напрямую аннотацией @EnableAutoConfiguration. Это я сделал, чтобы не использовать поиск компонентов помеченных аннотацией @Component — все бины, которые мы будем создавать, будут явно создаваться kotlin-конфигурациями. Тут же стоит отметить особенность kotlin-конфигураций — мы вынуждены помечать классы-конфигурации как open (так же как и методы, создающие бины), потому что в kotlin все классы и методы по-умолчанию final, что не позволит spring-у создать для них обертку.

Модель


Модель нашего приложения очень простая и состоит из двух сущностей. Это категория, к которой относится торрент (имеет поле parent, но по факту торрент всегда находится в категории, у которого всего один родитель), и сам торрент.

data class Category(val id:Long, val name:String, val parent:Category?)

data class Torrent(val id:Long,val categoryId:Long, val hash:String, val name:String, val size:Long, val created:Date)


Наши модельные классы, я описал просто как неизменяемые data классы. В этом проекте не используется jpa по этическим причинам и как следствие принципа бритвы Оккама. К тому же orm потребовал бы использования лишних технологий и очевидное проседание производительности. Для мэпинга данных из базы в объекты я буду просто использовать jdbc и jdbctemplate, как вполне достаточный для нашей задачи инструмент.

Итак, мы определили нашу модель, в которой, помимо вполне рядовых полей, стоит обратить внимание на поле hash, которое собственно и является идентификатором торрента в мире общения торрент-клиентов и которого достаточно, чтобы найти (например через dht) счастливых обладателей, раздающих данный торрент и получить у них недостающую информацию (вроде имен файлов), которая и отличает torrent-файл от magnet-ссылки.

Репозитории


Для доступа к данным мы используем небольшую абстракцию, которая позволит нам отделить хранилище данных от его потребителя. Например, из-за специфики данных, мы вполне могли бы использовать просто хранение в памяти и разбор csv-базы при старте, также эта абстракция подошла бы тем, кто особо остро желает использовать jpa, о котором мы говорили чуть выше. Итак, для каждой сущности создаем свой репозиторий, плюс один репозиторий для доступа к актуальной версии базы.

interface CategoryRepository {

    fun contains(id:Long):Boolean

    fun findById(id:Long): Category?

    fun count():Int

    fun clear()

    fun batcher(size:Int):Batcher<Category>

}

interface TorrentRepository {

    fun search(name:String):List<Torrent>

    fun count(): Int

    fun clear()

    fun batcher(size:Int):Batcher<Torrent>

}

interface VersionRepository {

    fun getCurrentVersion():Long?

    fun updateCurrentVersion(version:Long)

    fun clear()

}


Хотелось бы напомнить, если кто-то забыл или не знал, что вопрос после имени типа означает, что значения может не быть, т.е. оно может быть null-ом. Если же вопроса нет, то чаще всего попытка пропихнуть null провалится еще на этапе компиляции. От лирического отступления перейдем к нашим баранам интерфейсам. Интерфейсы специально сделаны минималистичными, чтобы не отвлекать от главного. И в целом их смысл ясен, кроме batcher-ов у первых двух. Опять же из-за специфики, нам нужно один раз записать много данных, а потом они не меняются. Из-за этого для изменения есть только один метод, который предоставляет возможность batch-добавления. Давайте посмотрим на него поближе.

Batcher


Очень простой интерфейс, позволяющий добавлять сущности конкретного типа:
interface Batcher<T> : Closeable {

    fun add(value:T)

}

также, Batcher наследуется от Closable, чтобы можно было отправить на добавление начатую неполную пачку, когда в источнике больше данных нет. Работают они примерно по следующей логике: при создании batcher-а задается размер пачки, при добавлении сущности накапливаются в буффере пока пачка не разрастется до заданного размера, после этого выполняется групповая операция добавления, которая в общем случае работает быстрее, чем набор единичных добавлений. Причем, у категорий Batcher будет с функциональностью добавления только уникальных значений, для торрентов же простая реализация, использующая JdbcTemplate.updateBatch(). Идеального размера для пачки не существует, поэтому эти параметры я вынес в конфигурацию приложения (см. application.yaml)

clear()


Когда я говорил об одном методе, изменяющим данные, я немного слукавил, ведь у всех репозиториев есть метод clear(), который просто удаляет все старые данные перед обработкой новой версии дампа. По факту, используем truncate table ..., потому что delete from… без where работает сильно медленнее, а для нашей ситуации действие аналогичное, если в база не поддерживает операцию truncate, можно просто пересоздать таблицу, что по скорости тоже будет существенно быстрей, чем удаление всех строк.

Интерфейс чтения


Здесь будут только необходимые методы, такие как search() у торрентов, который мы будем использовать для поиска, или findById() у категорий, чтобы собрать полноценный результат при поиске. count() нам нужен только, чтобы вывести в лог сколько мы отпроцессили данных, для дела он не нужен. В реализации для jdbc просто используется JdbcTemplate для выборки и мэпинга, например:
    private val rowMapper = RowMapper { rs: ResultSet, rowNum: Int ->
        Torrent(
                rs.getLong("id"), rs.getLong("category_id"),
                rs.getString("hash"), rs.getString("name"),
                rs.getLong("size"), rs.getDate("created")
        )
    }

    override fun search(name: String): List<Torrent> {
        if(name.isEmpty())
            return emptyList()

        val parts = name.split(" ")

        val whereSql = parts.map { "UPPER(name) like UPPER(?)" }.joinToString(" AND ")

        val parameters = parts.map { it.trim() }.map { "%$it%" }.toTypedArray()

        return jdbcTemplate.query("SELECT id, category_id, hash, name, size, created FROM torrent WHERE $whereSql", rowMapper, *parameters)
    }

Таким нехитрым способом мы реализуем поиск, который находит название, содержащее все слова запроса. Мы не используем ограничение количества записей, отдаваемых за раз, вроде разбиения по страницам, что безусловно стоило бы сделать в реальном проекте, но для нашего небольшого эксперимента, можно обойтись и без этого. Думаю, здесь же стоит заметить, что такое решение в лоб потребует полного обхода таблицы каждый раз для нахождения всех результатов, что для сравнительно небольшой базы rutracker-а может и так уж много, но для публичного продакшена, конечно, не подошло бы. Для ускорения поиска нужно дополнительное решение в виде индекса, может быть родной полнотекстовый поиск или стороннее решение вроде apache lucene, elasticsearch или многие другие. Создание такого индекса, конечно, увеличит и время создания базы и её размер. Но в нашем приложении мы остановимся на простой выборке с обходом, так как наша система скорее учебная.

Импорт


Большая часть нашей системы — это импорт данных из csv-файлов в наше хранилище. Здесь есть сразу несколько аспектов, на которые стоило бы обратить внимание. Во-первых, наша исходная база хоть и не очень большая, но тем не менее уже такого свойства, когда стоит аккуратно относиться к её размерам — т.е. нужно подумать, как сократить время перенесения данных, вероятно копирование данных в лоб может оказаться долгим. И второе, csv-база денормализована, а мы хотим получить разделение на категорию и торрент. Значит, нужно решить, как мы будем производить это разделение.

Производительность


Начнем с чтения. В моей реализации использован самописный парсер csv на kotlin взятый из другого моего проекта, который немного быстрее и чуть более внимателен к производимому типу исключений, чем существующие на рынке опенсорса, но по сути не меняющий порядок скорости разбора, т.е. можно было бы с тем же успехом взять почти любой парсер, умеющий работать в потоке, например commons-csv.

Теперь запись. Как мы уже видели раньше, я добавил батчеры для того, чтобы сократить накладные расходы на добавление большого числа записей. Для категорий проблема не столько в количестве, сколько в том, что они многократно повторяются. Некоторое количество тестов показало, что проверить наличие перед добавлением в пачку быстрее, чем создавать огромные пачки из запросов типа MERGE INTO. Что и понятно, если учесть, что первым делом проверка идет в уже существующей пачке прямо в памяти, тогда появился специальный batcher проверяющий уникальность.

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

    private fun importCategoriesAndTorrents(directory:Path) = withExecutor { executor ->
        val topCategories = importTopCategories(directory)

        executor
                .invokeAll(topCategories.map { createImportFileWorker(directory, it) })
                .map { it.get() }
    }

    private fun createImportFileWorker(directory: Path, topCategory: CategoryAndFile):Callable<Unit> = Callable {
        val categoryBatcher = categoryRepository.batcher(importProperties.categoryBatchSize)
        val torrentBatcher = torrentRepository.batcher(importProperties.torrentBatchSize)

        (categoryBatcher and torrentBatcher).use {
            parser(directory, topCategory.file).use {
                it
                        .map { createCategoryAndTorrent(topCategory.category, it) }
                        .forEach {
                            categoryBatcher.add(it.category)
                            torrentBatcher.add(it.torrent)
                        }
            }
        }
    }

Для такой работы хорошо подойдет пул с фиксированным количеством потоков. Мы отдаем executor-у сразу все задачи, но выполнять одновременно он будет столько задач, сколько есть потоков в пуле, а по выполнению одной задачи поток будет отдаваться другой. Необходимое количество потоков не угадаешь, но можно подобрать экспериментально. По умолчанию число потоков равняется количеству ядер, что часто бывает не самой плохой стратегией. Так как пул нам нужен только на время импорта, создаем его, отрабатываем и закрываем. Для этого делаем небольшую утилитную inline-функцию withExecutor(), которую мы уже использовали выше:

    private inline fun <R> withExecutor(block:(ExecutorService)->R):R {
        val executor = createExecutor()
        try {
            return block(executor)
        } finally {
            executor.shutdown()
        }
    }

    private fun createExecutor(): ExecutorService = Executors.newFixedThreadPool(importProperties.threads)

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

Разделение


Убедившись, что сущности могут никак друг от друга не зависеть во время импорта, я решил собрать все сущности (категории и торренты) в один проход, заранее создав только категории верхнего уровня (заодно получив информацию о файлах с торрентами), выбрав их за единицу распараллеливания.

Rest


Теперь у нас уже почти всё есть, чтобы добавить контроллер для получения данных поиска по торрентам в виде json. На выходе хотелось бы иметь сгруппированные по категориям торренты. Определим специальный бин, определяющий соотвествующую структуру ответа:
data class CategoryAndTorrents(val category:Category, val torrents:List<Torrent>)

Готово, осталось только запросить торреты, сгруппировать и отсортировать их:
@RequestMapping("/api/torrents")
class TorrentsController(val torrentRepository: TorrentRepository, val categoryRepository: CategoryRepository) {

    @ResponseBody
    @RequestMapping(method = arrayOf(RequestMethod.GET))
    fun find(@RequestParam name:String):List<CategoryAndTorrents> = torrentRepository
            .search(name)
            .asSequence()
            .groupBy { it.categoryId }
            .map { CategoryAndTorrents(categoryRepository.findById(it.key)!!, it.value.sortedBy { it.name }) }
            .sortedBy { it.category.name }
            .toList()

}

Пометив аннотацией @RequestParam параметр name мы ожидаем, что спринг запишет значение request-параметра «name» в параметр нашей функции. Пометив же метод аннотацией @ResponseBody, мы просим спринг преобразовать возвращаемый из метода бин в json.

Немного о DI


Также в предыдущем коде можно заметить, что репозитории приходят конроллеру в конструкторе. Подобным образом сделано и в остальных местах этого приложения: сами бины, создаваемые спрингом не знают о di, а принимают все свои зависимости в конструкторе, даже без всяких аннотаций. Реальная же связь происходит на уровне спринг-конфигурации:

@Configuration
open class RestConfiguration {

    @Bean
    open fun torrentsController(torrentRepository: TorrentRepository, categoryRepository: CategoryRepository):TorrentsController
            = TorrentsController(torrentRepository, categoryRepository)

}

Спринг передает зависимости, созданные другой конфигурацией, в параметры метода, создающего контроллер, — зависимости передаются контроллеру.

Итог


Готово! Запускаем, проверяем (в составе по адресу localhost:8080/ идет javascript-клиент для нашего сервиса, описание которого выходит за рамки этой статьи) — работает! На моей машине импорт идет примерно 80 секунд, вполне неплохо. И запрос на поиск идет еще секунд 5 — не так хорошо, но тоже работает.

О целях


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

Комментарии (32)


  1. ragimovich
    08.01.2016 23:17
    -11

    Горшочек, не вари.

    Серия №1. http://habrahabr.ru/post/273777/
    Серия №2. http://habrahabr.ru/post/274449/


    1. terryP
      08.01.2016 23:41
      +5

      Так там же php, а не java.


  1. fwizard
    09.01.2016 01:56
    +1

    вопрос только один: ЗАЧЕМ? Есть же Tribler


    1. fogone
      09.01.2016 12:31
      +3

      вопрос как раз в другом: как tribler помогает в изучении технологий и программированию вообще?


      1. fwizard
        09.01.2016 15:51

        Если рассматривать просто как практику программирования, я ничего против не имею ) Но зачем делать то что никогда не кому не пригодится.


        1. fogone
          09.01.2016 18:10
          +1

          Само приложение — скорее всего не пригодится, но могут пригодиться знания и опыт.


  1. KorP
    09.01.2016 02:10
    -11

    args: Array<String>

    эм… так массив или строка? строка массивов? массив строк?


    1. asm0dey
      09.01.2016 07:49

      Массив строк.


    1. stack_trace
      09.01.2016 11:38
      +4

      Строка массивов — это как?


      1. mukizu
        10.01.2016 17:24
        +3

        "[[],[],[],[],[[],[]],[[[[],[],[]]]]]"


  1. bsideup
    09.01.2016 13:19
    -1

    Про Spring Boot и большие приложения просто так ляпнули, чтобы умней казаться?:) Может всё же добавите аргументацию, а то прочтёт Вас джуниор какой-нибудь, и будет потом ходить и рассказывать всем что бут не годится для больших приложений. Хотя, есть подозрение что и Вы это просто где-то подхватили


    1. fogone
      09.01.2016 15:21
      +3

      Вы этот комент ляпнули, чтобы показать, что умнее автора? Может вы почитаете внимательное и поймете, что во-первых, я не утверждаю, что бут для больших проектов не годится, что было бы неправдой, ибо занимаюсь как раз таким проектом, а лишь сказал, что он может осложнить жизнь, знание о чем, я «подхватил» из собственного опыта. И во-вторых, если бы вы не писали свой комментарий в непотребных для этого сообщества выражениях, я даже ответил бы вам по существу.


  1. iimuhin
    09.01.2016 16:51
    +1

    Спасибо за пример проектика на Котлине. Интересно было почитать код. Многое взял себе на заметку. Один только utils.kt чего стоит. Да и DateFormat в ThreadLocal я в первый раз вижу — класс!
    1) Вы давно уже с Котлином работаете?
    2) Какие еще проекты на Котлине можете посоветовать для углубленного изучения паттернов котлиновских?
    3) Зачем написан собственный парсер CSV?
    4) «У spring boot есть много особенностей, которые могут серьезно осложнить жизнь в больших проектах». Инетересно было бы узнать об этих особенностях.


    1. iimuhin
      09.01.2016 17:12

      Уточнение про 1-й вопрос. Интересует не только время, но и число на Котлине сделанных проектов.


    1. fogone
      09.01.2016 18:54

      Здорово, я рад, что статья оказалась полезной.
      1) На работе я использую котлин не так давно и сравнительно немного, но в собственных нуждах постоянно и уже очень давно. Даже сам удивился насколько, когда увидел, что прошлая моя статья про котлин написана в 2012 году. Количество кода большое, но опенсорса не так много — у меня наверное можно только посмотреть прототип системы на базе libgdx на рективной тяге, там конечно кода сильно больше, но много и не очень качественного, который в планах в будущем улучшить.
      2) Сейчас пишется достаточно много кода на котлине, но к сожалению какого-то конкретного проекта именно для изучения паттернов сейчас выделить не смогу.
      3) В одном из рабочих проектов, нужен был парсер и до некоторого момента меня устраивала реализация от apache, но у него имелась проблема, связанная с тем, что у него все проблемы были IOException. А мне нужна была более тонкая обработка. В итоге получился парсер, который работает даже чуть быстрее, чем аналоги и с той системой ошибок, что была нужна мне. К тому же это был первый опыт написания кода такого рода на котлине.
      4) Подробно расписывать долго, но если вкратце, то основная проблема бута в том, что он очень многие вещи делает неявно. От этого большой проект становится сложнее, чем мог бы быть. Из за этого приходится отказываться от многих «фишек» бута, которые для небольшого проекта позволяют очень серьёзно сократить количество беготни с конфигурированием стандартных наборов технологий. К тому же в больших проектах часто нужно какое-то специфическое поведение, которое идет в разрез с автоконфигурированием, от которого в итоге в большинстве мест пришлось отказаться, но время на попытку его использования было потрачено. Думаю, что начиная проект сразу со бутом, проблем будет меньше просто потому, что отказ от каких-то его особенностей будет происходить постепенно и по необходимости, но если ты пытаешься мигрировать большое приложение, то все эти «особенности» вылезают сразу. Отдельная песня — это спрингбутовый класслоадер (дело даже не столько в самом класслоадере, сколько в самом подходе хранения jar-ов внутри другого jar-а), который доставил нам большое количество приятных минут проведенных в отладке. И хотя класслоадеры — это всегда тонкая тема, нам в итоге пришлось отказаться от использования executable jar, из за особенностей взаимодействия с ним используемых библиотек. А мой пулреквест, который прикрывал небольшую дыру в JarURLConnection, в 1.3 так и не попал, что тоже грустно.


      1. GreyCat
        10.01.2016 12:53
        +1

        > 2) Сейчас пишется достаточно много кода на котлине, но к сожалению какого-то конкретного проекта именно для изучения паттернов сейчас выделить не смогу.

        Кода пишется просто дико много, аж настолько, что Kotlin до сих пор отсутствует в районе сравнивалки openhub, а детальный просмотр приведенной ссылке на github показывает, что всего за прошлый месяц что-либо на Kotlin коммитили аж целых 14 разработчиков. Из этих 14 более-менее живых проектов именно на Kotlin там примерно половина, остальное — либо примеры и пробы пера, либо поддержка-Kotlin-как-еще-одного-таргета-в-системе-сборки, либо плагины IntelliJ.


        1. fogone
          10.01.2016 16:20

          Возможно, по ссылке «много кода» вы ожидаете гигабайты кода, но на мой взгляд то, что сейчас уже его активно использует сама JetBrains, делают специфические расширения такие проекты как rx, и создаются специфичные фреймворки вроде wasabi (при том, что релиза языка еще не было), уже позволяет сказать, что кода пишется достаточно много. Хотя в сравнении с java, конечно, этот объем ничтожен.


  1. iimuhin
    09.01.2016 17:41
    +1

    $.getJSON("/api/torrents?name=" + $('#search-text').val(), {}).done(function (response) {
    

    Вроде как надо же значение параметра name эскейпить по urlEncode? А для этого есть:

    $.getJSON("/api/torrents", { name: $('#search-text').val() }).done(function (response) {
    


    1. fogone
      09.01.2016 18:58

      Честно говоря, клиент я просто слепил на колене, потому что не собирался про него писать и уж тем более показывать его как пример разработки. Он был нужен, чтобы просто проверить работу сервиса. К слову, я сначала именно второй вариант использовал, но что-то не взлетело и я не желая тратить на это время, просто перенес в значение прямо в урл.


  1. iimuhin
    09.01.2016 17:59

    И еше вопрос, если можно: Почему не пользуетесь @ComponentScan, а объявляете все бины вручную?


    1. fogone
      09.01.2016 19:13
      +1

      Это вообще халиварная тема, но вот именно в этом приложении котлин наложил отпечаток своими отличиями от java. Сканирование компонентов вынуждает нас использовать инжекшн в поля, а в котлине это выглядит следующим образом:

      @Autowired
      private lateinit var someService:SomeService;
      
      

      что делает немного больно мои глазам, к тому же вынуждает объявлять поля для инжекшена как var, что несколько «неаккуратненько». Во-вторых, конфигурирование всего в java-конфигах дает большую гибкость при инстанцировании бинов. Для сканированных же компонентов, мы можем только задать опциональность их добавления в конфигурацию по профилю или какому-то другому критерию. Ну и в-третьих, сами бины в таком подходе почти всегда получаются свободными от контейнера, что дает некоторый плюс к удобству тестирования.


      1. fogone
        09.01.2016 19:20
        +1

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


        1. Borz
          09.01.2016 20:13

          если необходимо кастомизировать аннотированный бин, его же можно «выкинуть» из сканера фильтром и прописать как надо. Ну, или наложить Bean*Processor на него, например BeanPostProcessor


          1. fogone
            09.01.2016 20:32

            Конечно, можно выкинуть фильтром или сделать профили, или кондишены, и это отлично работает, но сложнее и менее явно, чем написать java-код вроде такого:

            @Bean
            public SomeInterface someInterface(OurConfig config) {
                return config.getSomeProperty() == "someValue" ? new FirstImplementation() : new SecondImplementation();
            }
            

            пример, конечно утрирован, но смысл понятен. Особенно неприятно становится, когда нужно иметь несколько конфигураций с разными наборами бинов. Но хочу еще раз повториться, я ни в коем случае не отказываю подходу со сканированием компонентов в праве на жизнь. Более того, в проекте над которым я работаю сейчас, он активно используется. Так же я еще нашел достаточно удобным сканирование только избранных компонентов, например web-контроллеров и только в рамках определенного пакета.


            1. Borz
              09.01.2016 20:39

              зачем вам DI при таком, кхм, объявлении бинов?

              я бы вместо параметра-флага someValue в конфиге указал бы имя класса для бина и создал бы из него бин через org.springframework.beans.BeanUtils#instantiate*


              1. fogone
                09.01.2016 21:08

                зачем вам DI при таком, кхм, объявлении бинов?

                DI в таком подходе есть, просто он выше — на уровне конфигурации. А на уровне сервисов DI в таком подходе и не нужен.

                я бы вместо параметра-флага someValue в конфиге указал бы имя класса для бина и создал бы из него бин через org.springframework.beans.BeanUtils#instantiate*

                если конфиг совсем одевелопереный, то можно и так сделать, а если это какой-нибудь параметр devMode, который аффектит сразу серию бинов разным образом? А еще часто бывает в больших системах, что необходимые настройки связаны с бизнес-логикой и из за этого параметр влияет сразу на много бинов.


                1. Borz
                  09.01.2016 21:25

                  параметр devMode, это две конфигурации для озвученного вами Spring Boot — одна для devMode=true, другая для devMode=false, сделанная через профиль.

                  А если это иные флаги (не devMode), то делается два бина с @Conditional. Я к тому, что я бы не стал через «условие? один_класс: другой_класс» создавать бин


                  1. fogone
                    09.01.2016 21:45

                    На @Conditional сделан весь бут, что и делает его использование таким неявным. А на счет devMode — тут вопрос неоднозначный, потому что так же как и параметры бизнес-логики он может влиять на такие нюансы конфигурации разных сущностей, что к ним не так просто подлезть просто объявив разные конфигурации, к тому же получится, что в одну конфигурацию будут напиханы разные никак друг с другом не связаные бины. И не совсем понимаю, чем ситуация с созданием бина по условию хуже, чем испольование @Conditional? Вот, возьмем реальную ситуацию, мне нужно было по параметру использовать кешированный вариант бина и обычный. Для этого в конфигурации я написал что-то вроде:

                        @Bean
                        public open fun headerRepository(sourceConfig: SourceConfig, fileSystem:FileSystem): HeaderRepository {
                            val repository = FileHeaderRepository(fileSystem, sourceConfig.sourceDirectory)
                    
                            return if (sourceConfig.cacheHeaders) CachedHeaderRepositoryWrapper(repository) else repository
                        }
                    

                    наверняка, я смог бы решить это и другим способом, но зачем, если это можно сделать так просто?


      1. iimuhin
        09.01.2016 23:28

        Спасибо за ответы. Я буквально на днях открыл для себя Котлин и должен признаться, что приятно удивлен.
        Кстати, я только что попробовал на одном из примеров отпробовать иньекцию и очень даже всё отлично отработало:

        @RestController
        class GreetingController @Autowired constructor (val greetingService: GreetingService) {
        ...
        

        и лэйтиниты не понадобились.

        Начиная с 4-го спринга рекомендуется именно иньекция через конструктор.


        1. fogone
          09.01.2016 23:52

          Да, как раз хотел про это написать, это вполне рабочий способ, если используешь скан, но он не очень хорошо подходит для способа о котором писал я — ведь в java-конфиге конструктор вызываешь руками.


          1. iimuhin
            10.01.2016 00:13
            +1

            Похоже мы как-то друг друга не понимэ. Ваше утверждение «Сканирование компонентов вынуждает нас использовать инжекшн в поля» неверно. Сканирование не вынуждает использовать иньекцию в поля. Конструкторная инъекция работает с тем же успехом (как я только что отпробовал).


            1. fogone
              10.01.2016 00:18

              Ну да, я об этом и хотел сказать своим комментарием выше — что это моё утверждение — неверное. Оно актуально только для определения бинов через джаваконфиг.