О Spring
2017-12-09
Spring — это весна. Spring — это пружина. Spring — это родник. Springfield — это городок, где живут Симпсоны. Плюс ещё стопицот одноимённых городков в Соединённом Королевстве, Австралии и Соединённых Штатах.
А ещё есть Spring Framework. Фреймворк, который знают все явисты. Возникший когда-то как легковесная альтернатива Ынтырпрайзным ЯваБобам (EJB).
Помню, как лет десять назад мы перелезали на Спринг с таких, ныне экзотичных вещей, как Apache Struts, или даже просто месива из сервлетов и JSP. Тогда надо было писать большущие простыни XMLя с описанием всех бинов. Уже тогда надо было подглядывать в исходники Спринга, чтобы понять, как туда воткнуть что-то нестандартное с точки зрения разработчиков фреймворка.
Сейчас в Спринге есть аннотации.
И конфигурацию можно описывать Ява кодом.
Или вообще не описывать,
а просто помечать нужные классы
как @Component
.
Сейчас есть Spring Boot. Вообще крайне странная штуковина.
С одной стороны,
он сильно упрощает начальное создание Спринг приложения.
Это — POM артефакты,
содержащие целые группы спринговых и внешних зависимостей,
с тщательно подобранными
(хочется в это верить)
версиями,
гарантированно работающими друг с другом.
Это — автоматические настройщики,
которые создают бины,
исходя из набора свойств
в application.yml
или application.properties
,
и сканируют пакеты в поисках классов и методов
помеченных хитрыми аннотациями,
чтобы и их настроить.
Это — плугины
к Maven и Gradle,
которые умеют собирать красивые суперджары,
с внедрённым Jetty или Netty для веб приложений.
С другой стороны, Spring Boot весьма усложняет попытки уйти в сторону и сделать что-то непредусмотренное. Притащить другую версию библиотеки почти наверняка не выйдет, потому что у артефакта Spring Boot уже есть своя версия. Причём какая это версия, простого способа узнать нет, ибо там очень много транзитивных зависимостей. Чтобы воткнуть нестандартную конфигурацию, банально несколько подключений к разным MongoDB, придётся отключать автоконфигураторы. Какие именно отключать, придётся находить методом научного тыка, ибо нет простого списка этих конфигураторов с описанием того, за что они отвечают и что делают.
В тех случаях, когда наш любимый, акторный, но пока не очень человеколюбивый фреймворк использовать нельзя, мы берём Spring, Spring Boot и Kotlin. И нормально.
Точка входа в приложение,
Application.kt
,
выглядит забавно.
@SpringBootApplication
@EnableAutoConfiguration(exclude=arrayOf(MongoAutoConfiguration::class, MongoDataAutoConfiguration::class))
@EnableScheduling
@EnableCaching
open class Application
fun main(args: Array<String>) {
SpringApplication.run(Application::class.java, *args)
}
main()
тут получается в классе ApplicationKt
.
Но Спрингу и его Буту нужен
ещё один класс,
открытый, ибо станет бином.
И вот этот класс и становится @SpringBootApplication
.
Современный Спринг
делает страшные вещи с вашими классами.
То,
что будет бином,
т.е. любые классы,
помеченные @SpringBootApplication
, @Configuration
,
@Component
, @Repository
, @Service
, @Controller
,
а также любые методы,
помеченные @Bean
,
в Котлине должны быть open
.
Потому что вы (почти) никогда
не получите на выходе (в IOC)
именно ваш класс.
Вы получите его наследника.
Куда будут аккуратно
засунуты все объявленные @Autowired
и @Value
.
Любой класс,
куда вы навешаете эти аннотации,
станет частью Спринга.
Вам понадобятся спринговые зависимости,
чтобы эти аннотации объявить.
И если этот класс случайно окажется в пакете,
где его найдёт автоконфигурация,
он сразу окажется в IOC.
А если не получится найти все нужные
@Autowired
и @Value
,
то приложение рухнет на старте.
Поэтому мы так не делаем. Мы стараемся делать сервисы и компоненты, которые вообще не зависят от Спринга. Все другие сервисы и компоненты, а также конфигурационные значения, которые нужны для работы этого сервиса, передаются в конструкторе. Как это и положено с нормальными объектами. Куча аргументов конструктора в Котлине — не проблема. Мы просто используем именованные аргументы.
Сами внешние сервисы и компоненты в аргументах конструктора представлены интерфейсами. Интерфейс легко замокать. И можно и нужно протестировать этот компонент как положено, юнит тестами. А ещё типы бинов удобнее представлять интерфейсами, тогда можно тихо и незаметно подменить реализацию. Кстати, Спринг вполне корректно различает генерик интерфейсы, с разными типами переменных типа.
class EntityInsertService(
private val name: String = "",
private val mongoOps: MongoOperations,
private val collectionName: String,
private val executor: Executor
) : IInsertService<Entity> {
override fun insert(data: Entity) {
//...
}
override fun flush() {
//...
}
}
Такие компоненты без аннотаций легко выносятся в (почти) независимые от Спринга библиотеки и переиспользуются между разными приложениями.
А в самом приложении они уже подключаются через объявление конфигурации.
@Configuration
open class InsertServiceConfiguration {
@Value("\${insert.concurrency:2}")
private var concurrency: Int = 2
@Value("\${insert.collection:data}")
private lateinit var collection: String
@Autowired
private lateinit var mongo: MongoOperations
@Bean
open fun insertService(): IInsertService<Entity> {
val executor = Executors.newWorkStealingPool(concurrency)
return EntityInsertService(
name = "entityInsert",
mongoOps = mongo,
collectionName = collection,
executor = executor
)
}
}
Вот эти вот insert.concurrency
—
это проперти приложения.
Как известно,
их можно объявлять в application.properties
.
Но мы,
конечно же,
предпочитаем более сложновложенный
application.yml
.
Из этих пропертей тоже можно конструировать
бины, списки и даже мапы.
Вот вам нестандартная конфигурация с кучей Монг:
mongodb:
connections:
source:
host: 172.31.22.180
port: 27017
database: project
serverSelectionTimeout: 10000 # https://scalegrid.io/blog/understanding-mongodb-client-timeout-options/
connectTimeout: 5000
socketTimeout: 1000
target:
host: localhost
port: 27017
database: test
serverSelectionTimeout: 10000
connectTimeout: 5000
socketTimeout: 1000
writeConcern: acknowledged # http://mongodb.github.io/mongo-java-driver/3
Можно, конечно, наваять @Configuration
класс,
куда внедрить все эти значения через @Value
.
Но мне понадобилось иметь произвольное количество таких подключений.
Их все можно прочитать в мапу с помощью @ConfigurationProperties
.
data class MongoConnectionProperties(
var host: String = "localhost",
var port: Int = 27017,
var database: String = "test",
var serverSelectionTimeout: Int = 0,
var connectTimeout: Int = 5000,
var socketTimeout: Int = 1000,
var writeConcern: String = "acknowledged",
var connectionsPerHost: Int = 10
) {
val writeConcernValue: WriteConcern
get() = WriteConcern.valueOf(writeConcern.toUpperCase())
}
data class MongoConnections(
val connections: MutableMap<String, MongoConnectionProperties> = mutableMapOf()
) {
operator fun get(name: String): MongoConnectionProperties
= connections[name] ?: throw NoSuchElementException("No MongoDB connection mongodb.connections.$name")
}
@Configuration
open class MongoConnectionsConfiguration {
@Bean
@ConfigurationProperties(prefix="mongodb")
open fun allMongoProperties(): MongoConnections {
return MongoConnections()
}
}
Эта мапа должна быть мутабельной,
а объекты должны быть настоящими ява бинами,
с полным набором сеттеров,
чтобы Спринг смог эту мапу заполнить.
Поэтому тут в котлиновых датаклассах сплошные var
и дефолтные значения.
Для итогового набора свойств тоже нужно придумывать свой класс,
нельзя просто вернуть коллекцию,
потому что списки и мапы в качестве типа бина
обрабатываются Спрингом по-особому:
он пытается туда впихнуть все бины из контекста,
подходящие по типу,
а нам это совсем не нужно.
Из бина описания можно сделать настоящий объект MongoTemplate
,
который уже можно использовать.
@Configuration
open class MongoConnectionsConfiguration {
@Bean
@Scope("prototype")
open fun mongoOperations(name: String): MongoOperations {
val properties = allMongoProperties()[name]
val options = MongoClientOptions.builder()
.writeConcern(properties.writeConcernValue)
.serverSelectionTimeout(properties.serverSelectionTimeout)
.connectTimeout(properties.connectTimeout)
.socketTimeout(properties.socketTimeout)
.connectionsPerHost(properties.connectionsPerHost)
.build()
val dbFactory = SimpleMongoDbFactory(
MongoClient(ServerAddress(properties.host, properties.port), options),
properties.database)
return MongoTemplate(dbFactory)
}
}
Правда здесь у нас получается бин,
который прототип,
да ещё принимающий строку параметром.
Получить такие бины из ApplicationContext
труда не составляет.
А вот @Autowired
или @Inject
для них уже не работают.
Поэтому приходится, если нужно, все эти прототипы создавать явно, и засовывать в синглетон коллекцию. Опять своего отдельного типа. Славься Котлин дата классами.
data class MongoOperationsMap(
val mongos: Map<String, MongoOperations>
)
@Configuration
open class MongoConnectionsConfiguration {
@Bean
open fun allMongoOperations() : MongoOperationsMap {
val map: MutableMap<String, MongoOperations> = mutableMapOf()
for ((name, _) in allMongoProperties().connections) {
map.put(name, mongoOperations(name))
}
return MongoOperationsMap(map)
}
}
Ну а дальше этот наш дата класс можно автовайрить куда угодно.
Иногда приходится бороться со Спрингом вообще, и со Спринг Бутом в частности. И хочется выкинуть этот Спринг нафиг. Со всеми его странными и несовместимыми обёртками вокруг обычного Монго драйвера.
Но как представишь, сколько вопросов возникнет без Спринга: Где создавать компоненты? Как их связывать друг с другом? Как автоматически и многопоточно подключаться к какой-нибудь очереди? Какой шедулер взять? Какой кэш взять и как его прикрутить?
И решаешь: пусть Спринг остаётся. Всё же он берёт на себя громадную кучу инфраструктурных проблем. А иногда с ним пободаться даже полезно.