… его четверо отлаживают.
Вдохновлённый докладом Владимира Плизги (Spring Boot 2: чего не пишут в release notes) я решил рассказать о своём опыте работы со Спринг Бут, его особенностях и подводных камнях, встретившихся на моём пути.
Spring Data JPA
Доклад Владимира посвящён миграции на Спринг Бут 2 и сложностям, которые при этом возникают. В первой части он описывает ошибки компиляции, поэтому я тоже начну с них.
Думаю, многие знакомы с каркасом Spring Data и его производными для различных хранилищ данных. Я работал только с одной из них — Spring Data JPA. Основной интерфейс, используемый в этом фреймворке — JpaRepository, претерпел значительных изменений в версиях 2.*.
Рассмотрим наиболее используемые методы:
//было interface JpaRepository<T, ID> { T findOne(ID id); List<T> findAll(Iterable<ID> ids); <S extends T> Iterable<S> save(Iterable<S> entities); } //стало interface JpaRepository<T, ID> { Optional<T> findById(ID id); List<T> findAllById(Iterable<ID> ids); <S extends T> Iterable<S> saveAll(Iterable<S> entities); }
На самом деле изменений больше, и после переезда на версию 2.* все они превратятся в ошибки компиляции.
Когда на одном из наших проектов встал вопрос о миграции на Спринг Бут 2 и были сделаны первые шаги (замена версии в pom.xml) весь проект в прямом смысле слова покраснел: десятки репозиториев и сотни обращений к их методам привели к тому, что миграция "в лоб" (использование новых методов) зацепила бы каждый второй файл. Представьте простейший код:
SomeEntity something = someRepository.findOne(someId); if (something == null) { something = someRepository.save(new SomeEntity()); } useSomething(something);
Чтобы проект просто собрался необходимо:
- вызывать другой метод
- сменить тип переменной
somethingнаOptional<SomeEntity> - заменить проверку пустой ссылки на
Optional::isPresentили цепочкуOptional::orElse/Optional::orElseGet - поправить тесты
К счастью, есть способ проще, благо Спринг Дата предоставляет пользователям ранее невиданные возможности по подгонке репозиториев под свои нужды. Всё что нам нужно — определить (если они ещё не определены) интерфейс и класс, которые лягут в основу всех репозиториев нашего проекта. Делается это вот так:
//основа для всех репозиториев @NoRepositoryBean public interface BaseJpaRepository<T, ID extends Serializable> extends JpaRepository<T, ID> { // определяем метод findOne, подобный существовавшему в версиях 1.* @Deprecated T findOne(ID id); // то же для findAll @Deprecated // подсветка в коде как напоминание о необходимости выкашивания List<T> findAll(Iterable<ID> ids); }
От этого интерфейса будем наследовать все наши репозитории. Теперь реализация:
public class BaseJpaRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements BaseJpaRepository<T, ID> { private final JpaEntityInformation<T, ?> entityInfo; private final EntityManager entityManager; public BaseJpaRepositoryImpl(JpaEntityInformation<T, ?> entityInfo, EntityManager entityManager) { super(entityInfo, entityManager); this.entityInfo = entityInfo; this.entityManager = entityManager; } @Override public T findOne(ID id) { return findById(id).orElse(null); //обеспечиваем совместимость поведения с 1.* } @Override public List<T> findAll(Iterable<ID> ids) { return findAllById(ids); // просто передаём в новый метод } }
Остальное — по образу и подобию. Все ошибки компиляции исчезают, код работает как прежде, а изменено всего 2 класса, а не 200. Теперь можно по ходу разработки неспешно заменять устаревшее АПИ на новое, благо "Идея" заботливо закрасит жёлтым все вызовы отмеченных @Deprecated методов.
Последний мазок — сообщаем Спрингу, что отныне репозитории должны строиться поверх нашего класса:
@Configuration @EnableJpaRepositories(repositoryBaseClass = BaseJpaRepositoryImpl.class) public class SomeConfig{ }
В тихом омуте бины водятся
В основе Спринг Бута лежит два понятия — стартер и автоконфигурация. В жизни следствием этого является важное свойство — библиотека, попавшая в classpath приложения, просматривается СБ-ом и если в ней найден класс, включающий некую настройку, то она включится без вашего ведома и явного указания. Покажем это на простейшем примере.
Многие используют библиотеку gson для превращения объектов в JSON-строки и наоборот. Часто можно увидеть такой код:
@Component public class Serializer { public <T> String serialize(T obj) { return new Gson().toJson(obj); } }
При частых обращениях этот код дополнительно нагрузит сборщик мусора, чего нам не хочется. Особо умные создают один объект и используют его:
@Configuration public class SomeConfig { @Bean public Gson gson() { return new Gson(); } } @Component @RequiredArgsConstructor public class Serializer { private final Gson gson; public String serialize(T obj) { return gson.toJson(obj); } }
… даже не догадываясь, что Спринг Бут может сделать всё сам. По умолчанию СБ поставляется вместе с зависимостью org.springframework.boot:spring-boot-autoconfigure, включающей множество классов с именем *AutoConfiguration, например, такой:
@Configuration @ConditionalOnClass(Gson.class) @EnableConfigurationProperties(GsonProperties.class) public class GsonAutoConfiguration { @Bean @ConditionalOnMissingBean public GsonBuilder gsonBuilder(List<GsonBuilderCustomizer> customizers) { GsonBuilder builder = new GsonBuilder(); customizers.forEach((c) -> c.customize(builder)); return builder; } @Bean @ConditionalOnMissingBean public Gson gson(GsonBuilder gsonBuilder) { return gsonBuilder.create(); } }
Настройка незатейлива как рельс: при наличии в приложении класса Gson и отсутствии вручную определённого бина этого типа будет создана реализация по умолчанию. В этом — огромная мощь, позволяющая одной зависимостью и парой аннотаций поднять полноценную конфигурацию, на описание которой раньше уходили простыни XML-а и многочисленные рукописные бины.
Но в этом и большое коварство СБ. Коварство заключается в скрытности, ведь много работы происходит под капотом по заранее написанным сценариям. Это хорошо для типового приложения, но выход с проторённой дорожки может вызвать проблемы — конвой стреляет без предупреждения. Множество бинов создаётся без нашего ведома, пример с Гсоном хорошо это показывает. Будьте осторожны с класспасом и вашими зависимостями, ибо...
Всё, что попадёт в ваш classpath, может быть использовано против вас
Случай из жизни. Есть два приложения: в одном используется LdapTemplate, а в другом он когда-то использовался. Оба приложения зависят от проекта core, куда вынесены некоторые общие классы, а также в pom.xml были заботливо сложены общие для обоих приложений библиотеки.
Спустя некоторое время из второго проекта все использования LdapTemplate были за ненадобностью выпилены. Но библиотека org.springramework:spring-ldap осталась в core. Потом грянул СБ и org.springramework.ldap:ldap-core превратился в org.springramework.boot:spring-boot-starter-data-ldap.
Из-за этого в логах появились интересные сообщения:
Multiple Spring Data modules found, entering strict repository configuration mode! Spring Data LDAP - Could not safely identify store assignment for repository candidate interface ....
Зависимость от core привела к тому, что org.springramework.boot:spring-boot-starter-data-ldap затесался в проект, в котором ЛДАП вообще не используется. Что значит затесался? Попал в classpath :). Далее со всеми остановками:
- Spring Boot замечает наличие
org.springramework.boot:spring-boot-starter-data-ldapв classpath-е - каждый репозиторий теперь проверяется на предмет того, годится ли он для использования со Spring Data JPA или Spring Data LDAP
- реорганизация зависимостей и выпиливание ненужного
org.springramework.boot:spring-boot-starter-data-ldapуменьшает время запуска приложения в среднем на 20 (!) секунд из общих 40-50
Внимательный и критически настроенный читатель наверняка спросит: а зачем вообще понадобилось менять org.springramework.ldap:ldap-core на org.springramework.boot:spring-boot-starter-data-ldap, если Spring Data LDAP не используется, а используется только один класс org.springframework.ldap.LdapTempate?
Ответ: это была ошибка. Дело в том, что до версии 2.1.0.M1 автонастройка для ЛДАП-а выглядела примерно вот так
@Configuration @ConditionalOnClass({ContextSource.class}) @EnableConfigurationProperties({LdapProperties.class}) public class LdapAutoConfiguration { private final LdapProperties properties; private final Environment environment; public LdapAutoConfiguration(LdapProperties properties, Environment environment) { this.properties = properties; this.environment = environment; } @Bean @ConditionalOnMissingBean public ContextSource ldapContextSource() { LdapContextSource source = new LdapContextSource(); source.setUserDn(this.properties.getUsername()); source.setPassword(this.properties.getPassword()); source.setBase(this.properties.getBase()); source.setUrls(this.properties.determineUrls(this.environment)); source.setBaseEnvironmentProperties(Collections.unmodifiableMap(this.properties.getBaseEnvironment())); return source; } }
Где же LdapTemplate? А его нет :). Точнее он есть, но лежит в другом месте:
@Configuration @ConditionalOnClass({LdapContext.class, LdapRepository.class}) @AutoConfigureAfter({LdapAutoConfiguration.class}) public class LdapDataAutoConfiguration { @Bean @ConditionalOnMissingBean({LdapOperations.class}) public LdapTemplate ldapTemplate(ContextSource contextSource) { return new LdapTemplate(contextSource); } }
Таким образом, было сделано предположение, что получить LdapTemplate в своём приложении можно удовлетворением условия @ConditionalOnClass({LdapContext.class, LdapRepository.class}), что возможно при добавлении в classpath зависимости spring-boot-starter-data-ldap.
Другая возможность: определить этот бин руками, чего не хочется (ведь зачем нам тогда СБ). До этого додумались уже после замены org.springramework.boot:spring-boot-starter-data-ldap на org.springramework.ldap:ldap-core.
Проблема решена здесь: https://github.com/spring-projects/spring-boot/pull/13136. Основное изменение: объявление бина LdapTemplate переехало в LdapAutoConfiguration. Теперь можно использовать ЛДАП без привязки к spring-boot-starter-data-ldap и ручного определения бина LdapTemplate.
Подкапотная возня
Если вы используете Hibernate, то наверняка знакомы с антипаттерном open-in-view. На его описании останавливаться не будем, по ссылке он описан достаточно подробно, да и в других источниках его вредное воздействие описано весьма подробно.
За включение/выключение этого режима отвечает специальный флаг:
spring: jpa: open-in-view: true
В СБ версии 1.* по умолчанию он был включен, при этом пользователю об этом ничего не сообщалось. В версиях 2.* он по прежнему включен, но теперь в журнал пишется предупреждение:
WARN spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning.
Изначально существовал запрос на отключение этого режима, былинный срач по теме содержит несколько десятков (!) развёрнутых комментариев в т.ч. от Оливера Гёрке (разработчик Спринг Даты), Влада Михалче (разработчик Хибернейта), Фила Веба и Ведран Павича (разработчики Спринга) с мнениями "за" и "против".
Сошлись на том, что поведение меняться не будет, но будет выводится предупреждение (что и наблюдается). Также существует довольно распространённый совет отключать этот режим:
spring: jpa: open-in-view: false
Вот собственно и всё, пишите о ваших граблях и интересных особенностях СБ — это поистине неисчерпаемая тема.
