Spring Boot is standing on the mountain ...
... his four are debugging.
Inspired by the report of Vladimir Plizgi ( Spring Boot 2: what is not written in the release notes ), I decided to talk about my experience with Spring Boot, its features and the pitfalls encountered on my way.
Spring Data JPA
Vladimir's report is devoted to the migration to Spring Booth 2 and the difficulties that arise. In the first part, it describes the compilation errors, so I will also start with them.
I think many are familiar with the Spring Data framework and its derivatives for various data warehouses. I only worked with one of them - Spring Data JPA. The main interface used in this framework JpaRepository
has undergone significant changes in version 2. *.
Consider the most used methods:
//былоinterfaceJpaRepository<T, ID> {
T findOne(ID id);
List<T> findAll(Iterable<ID> ids);
<S extends T> Iterable<S> save(Iterable<S> entities);
}
//сталоinterfaceJpaRepository<T, ID> {
Optional<T> findById(ID id);
List<T> findAllById(Iterable<ID> ids);
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
}
In fact, there are more changes, and after moving to version 2. * all of them will turn into compilation errors.
When one of our projects raised the issue of migration to Spring Booth 2 and the first steps were taken (replacing the version in pom.xml), the entire project literally blushed: dozens of repositories and hundreds of calls to their methods led to the fact that migration " head on "(using new methods) would hook every second file. Imagine the simplest code:
SomeEntity something = someRepository.findOne(someId);
if (something == null) {
something = someRepository.save(new SomeEntity());
}
useSomething(something);
To get the project ready, you need:
- call another method
- change variable type
something
toOptional<SomeEntity>
- replace check empty link to
Optional::isPresent
or chainOptional::orElse
/Optional::orElseGet
- fix tests
Fortunately, there is a simpler way, since Spring Date provides users with unprecedented opportunities to customize repositories to their needs. All we need is to define (if they are not yet defined) the interface and class that will form the basis of all the repositories of our project. This is done like this:
//основа для всех репозиториев@NoRepositoryBeanpublicinterfaceBaseJpaRepository<T, IDextendsSerializable> extendsJpaRepository<T, ID> {
// определяем метод findOne, подобный существовавшему в версиях 1.*@DeprecatedT findOne(ID id);
// то же для findAll@Deprecated// подсветка в коде как напоминание о необходимости выкашиванияList<T> findAll(Iterable<ID> ids);
}
From this interface we will inherit all our repositories. Now implementation:
publicclassBaseJpaRepositoryImpl<T, IDextendsSerializable> extendsSimpleJpaRepository<T, ID> implementsBaseJpaRepository<T, ID> {
privatefinal JpaEntityInformation<T, ?> entityInfo;
privatefinal EntityManager entityManager;
publicBaseJpaRepositoryImpl(JpaEntityInformation<T, ?> entityInfo, EntityManager entityManager){
super(entityInfo, entityManager);
this.entityInfo = entityInfo;
this.entityManager = entityManager;
}
@Overridepublic T findOne(ID id){
return findById(id).orElse(null); //обеспечиваем совместимость поведения с 1.*
}
@Overridepublic List<T> findAll(Iterable<ID> ids){
return findAllById(ids); // просто передаём в новый метод
}
}
The rest - in the image and likeness. All compilation errors disappear, the code works as before, but only 2 classes are changed, not 200. Now, as the development progresses, you can slowly replace the outdated API with a new one.
Last stroke - we inform Spring that from now on repositories should be built on top of our class:
@Configuration@EnableJpaRepositories(repositoryBaseClass = BaseJpaRepositoryImpl.class)
publicclassSomeConfig{
}
In still waters, bins are found
At the heart of Spring Booth are two concepts - the starter and auto configuration. In life, the consequence of this is an important property — the library that falls into the classpath of the application is viewed by the SAT and if a class is found in it that includes a certain setting, it will turn on without your knowledge or explicit indication. Let us show this with the simplest example.
Many use the gson library to turn objects into JSON strings and vice versa. You can often see this code:
@ComponentpublicclassSerializer{
public <T> String serialize(T obj){
returnnew Gson().toJson(obj);
}
}
With frequent calls, this code will additionally load the garbage collector, which we do not want. Particularly intelligent create one object and use it:
@ConfigurationpublicclassSomeConfig{
@Beanpublic Gson gson(){
returnnew Gson();
}
}
@Component@RequiredArgsConstructorpublicclassSerializer{
privatefinal Gson gson;
public String serialize(T obj){
return gson.toJson(obj);
}
}
... without even realizing that Spring Booth can do everything himself. By default, the SAT comes with a dependency org.springframework.boot:spring-boot-autoconfigure
that includes many classes with the name *AutoConfiguration
, for example, like this:
@Configuration@ConditionalOnClass(Gson.class)
@EnableConfigurationProperties(GsonProperties.class)
publicclassGsonAutoConfiguration{
@Bean@ConditionalOnMissingBeanpublic GsonBuilder gsonBuilder(List<GsonBuilderCustomizer> customizers){
GsonBuilder builder = new GsonBuilder();
customizers.forEach((c) -> c.customize(builder));
return builder;
}
@Bean@ConditionalOnMissingBeanpublic Gson gson(GsonBuilder gsonBuilder){
return gsonBuilder.create();
}
}
Setting is simple as a rail: if there is a class in the application Gson
and there is no manually defined bean of this type, a default implementation will be created. This is a huge power, which allows a single dependency and a couple of annotations to raise a full configuration, which was previously described by XML sheets and numerous hand-written bins.
But therein lies the great deceit of the Security Council. Deceit lies in stealth, because a lot of work happens under the hood according to pre-written scripts. This is good for a typical application, but getting out of the beaten track can cause problems — a convoy shoots without warning. Many bins are created without our knowledge, the example of Gson shows this well. Be careful with your class and your dependencies, for ...
Anything in your classpath can be used against you.
Case of life. There are two applications: in one it is used LdapTemplate
, and in the other it was once used. Both applications depend on the project core
, where some common classes are moved, and the libraries common to both applications were carefully piled in pom.xml.
After some time, from the second project, all uses LdapTemplate
were cut out as unnecessary. But the library org.springramework:spring-ldap
remained in core
. Then he struck Sat and org.springramework.ldap:ldap-core
turned into org.springramework.boot:spring-boot-starter-data-ldap
.
Because of this, interesting messages appeared in the logs:
Multiple Spring Data modules found, entering strict repository configuration mode!
Spring Data LDAP - Could not safely identify store assignment for repository candidate interface ....
Dependence on core
led to the fact that org.springramework.boot:spring-boot-starter-data-ldap
wormed in a project in which the LDAP is not used at all. What do you mean bothered? Got to classpath :). Further, with all the stops:
- Spring Boot notices presence
org.springramework.boot:spring-boot-starter-data-ldap
in classpath - each repository is now checked to see if it is suitable for use with Spring Data JPA or Spring Data LDAP
- reorganization of dependencies and cutting out unnecessary
org.springramework.boot:spring-boot-starter-data-ldap
reduces the launch time of the application by an average of 20 (!) seconds from the total 40-50
An attentive and critical reader will surely ask: why was it even necessary to change org.springramework.ldap:ldap-core
to org.springramework.boot:spring-boot-starter-data-ldap
, if Spring Data LDAP is not used, and only one class is used org.springframework.ldap.LdapTempate
?
Answer: it was a mistake. The fact is that prior to version 2.1.0.M1, auto-tuning for LDAP looked like this
@Configuration@ConditionalOnClass({ContextSource.class})
@EnableConfigurationProperties({LdapProperties.class})
publicclassLdapAutoConfiguration{
privatefinal LdapProperties properties;
privatefinal Environment environment;
publicLdapAutoConfiguration(LdapProperties properties, Environment environment){
this.properties = properties;
this.environment = environment;
}
@Bean@ConditionalOnMissingBeanpublic 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;
}
}
Where it is LdapTemplate
? And it is not :). More precisely, it is, but lies elsewhere:
@Configuration@ConditionalOnClass({LdapContext.class, LdapRepository.class})
@AutoConfigureAfter({LdapAutoConfiguration.class})
publicclassLdapDataAutoConfiguration{
@Bean@ConditionalOnMissingBean({LdapOperations.class})
public LdapTemplate ldapTemplate(ContextSource contextSource){
returnnew LdapTemplate(contextSource);
}
}
Thus, it has been assumed that getting a LdapTemplate
possible appreciation of the conditions in your application @ConditionalOnClass({LdapContext.class, LdapRepository.class})
, it is possible by adding the classpath dependence spring-boot-starter-data-ldap
.
Another possibility: to identify this bin with your hands, which you don’t want (after all, why do we need the Security Council then). Prior to this, we thought after replacing org.springramework.boot:spring-boot-starter-data-ldap
with org.springramework.ldap:ldap-core
.
The problem is solved here: https://github.com/spring-projects/spring-boot/pull/13136 . Major change: Bina's ad LdapTemplate
moved to LdapAutoConfiguration
. Now you can use the LDAP without binding to spring-boot-starter-data-ldap
and manually determining the bean LdapTemplate
.
Podkapotnaya fuss
If you are using Hibernate, you are probably familiar with the open-in-view anti-pattern . We will not dwell on its description, it is described in sufficient detail by reference, and in other sources its harmful effects are described in great detail.
A special flag is responsible for enabling / disabling this mode:
spring:
jpa:
open-in-view: true
In the Security Council version 1. * By default it was enabled, while the user was not informed about this. In versions 2. * it is still included, but now a warning is written to the log:
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.
Initially, there was a request to disable this mode, the epic srach on the topic contains several dozen (!) Detailed comments, including from Oliver Goerke (developer of Spring Dates), Vlad Mihalche (developer of Hiberneit), Phil Web and Vedran Pavic (developers of Spring) with pro and con opinions.
Agreed that the behavior will not change, but a warning will be displayed (which is observed). There is also a fairly common tip to disable this mode:
spring:
jpa:
open-in-view: false
That's all, write about your rakes and interesting features of the Security Council - this is truly an inexhaustible topic.