Ignore non-default candidates in type-based matching

Closes gh-22403
This commit is contained in:
Andy Wilkinson
2024-08-20 11:40:04 +01:00
parent 8183c5b23d
commit 2ecb4ebd49
18 changed files with 271 additions and 179 deletions

View File

@@ -25,6 +25,7 @@ import java.lang.annotation.Target;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
@@ -69,30 +70,37 @@ public @interface ConditionalOnBean {
/**
* The class types of beans that should be checked. The condition matches when beans
* of all classes specified are contained in the {@link BeanFactory}. Beans that are
* not autowire candidates are ignored.
* not autowire candidates or that are not default candidates are ignored.
* @return the class types of beans to check
* @see Bean#autowireCandidate()
* @see BeanDefinition#isAutowireCandidate
* @see Bean#defaultCandidate()
* @see AbstractBeanDefinition#isDefaultCandidate
*/
Class<?>[] value() default {};
/**
* The class type names of beans that should be checked. The condition matches when
* beans of all classes specified are contained in the {@link BeanFactory}. Beans that
* are not autowire candidates are ignored.
* are not autowire candidates or that are not default candidates are ignored.
* @return the class type names of beans to check
* @see Bean#autowireCandidate()
* @see BeanDefinition#isAutowireCandidate
* @see Bean#defaultCandidate()
* @see AbstractBeanDefinition#isDefaultCandidate
*/
String[] type() default {};
/**
* The annotation type decorating a bean that should be checked. The condition matches
* when all the annotations specified are defined on beans in the {@link BeanFactory}.
* Beans that are not autowire candidates are ignored.
* Beans that are not autowire candidates or that are not default candidates are
* ignored.
* @return the class-level annotation types to check
* @see Bean#autowireCandidate()
* @see BeanDefinition#isAutowireCandidate
* @see Bean#defaultCandidate()
* @see AbstractBeanDefinition#isDefaultCandidate
*/
Class<? extends Annotation>[] annotation() default {};

View File

@@ -25,6 +25,7 @@ import java.lang.annotation.Target;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
@@ -70,20 +71,24 @@ public @interface ConditionalOnMissingBean {
/**
* The class types of beans that should be checked. The condition matches when no bean
* of each class specified is contained in the {@link BeanFactory}. Beans that are not
* autowire candidates are ignored.
* autowire candidates or that are not default candidates are ignored.
* @return the class types of beans to check
* @see Bean#autowireCandidate()
* @see BeanDefinition#isAutowireCandidate
* @see Bean#defaultCandidate()
* @see AbstractBeanDefinition#isDefaultCandidate
*/
Class<?>[] value() default {};
/**
* The class type names of beans that should be checked. The condition matches when no
* bean of each class specified is contained in the {@link BeanFactory}. Beans that
* are not autowire candidates are ignored.
* are not autowire candidates or that are not default candidates are ignored.
* @return the class type names of beans to check
* @see Bean#autowireCandidate()
* @see BeanDefinition#isAutowireCandidate
* @see Bean#defaultCandidate()
* @see AbstractBeanDefinition#isDefaultCandidate
*/
String[] type() default {};
@@ -105,10 +110,13 @@ public @interface ConditionalOnMissingBean {
/**
* The annotation type decorating a bean that should be checked. The condition matches
* when each annotation specified is missing from all beans in the
* {@link BeanFactory}. Beans that are not autowire candidates are ignored.
* {@link BeanFactory}. Beans that are not autowire candidates or that are not default
* candidates are ignored.
* @return the class-level annotation types to check
* @see Bean#autowireCandidate()
* @see BeanDefinition#isAutowireCandidate
* @see Bean#defaultCandidate()
* @see AbstractBeanDefinition#isDefaultCandidate
*/
Class<? extends Annotation>[] annotation() default {};

View File

@@ -24,6 +24,7 @@ import java.lang.annotation.Target;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
@@ -53,14 +54,16 @@ public @interface ConditionalOnSingleCandidate {
/**
* The class type of bean that should be checked. The condition matches if a bean of
* the class specified is contained in the {@link BeanFactory} and a primary candidate
* exists in case of multiple instances. Beans that are not autowire candidates are
* ignored.
* exists in case of multiple instances. Beans that are not autowire candidates or
* that are not default candidates are ignored.
* <p>
* This attribute may <strong>not</strong> be used in conjunction with
* {@link #type()}, but it may be used instead of {@link #type()}.
* @return the class type of the bean to check
* @see Bean#autowireCandidate()
* @see BeanDefinition#isAutowireCandidate
* @see Bean#defaultCandidate()
* @see AbstractBeanDefinition#isDefaultCandidate
*/
Class<?> value() default Object.class;
@@ -68,13 +71,15 @@ public @interface ConditionalOnSingleCandidate {
* The class type name of bean that should be checked. The condition matches if a bean
* of the class specified is contained in the {@link BeanFactory} and a primary
* candidate exists in case of multiple instances. Beans that are not autowire
* candidates are ignored.
* candidates or that are not default candidates are ignored.
* <p>
* This attribute may <strong>not</strong> be used in conjunction with
* {@link #value()}, but it may be used instead of {@link #value()}.
* @return the class type name of the bean to check
* @see Bean#autowireCandidate()
* @see BeanDefinition#isAutowireCandidate
* @see Bean#defaultCandidate()
* @see AbstractBeanDefinition#isDefaultCandidate
*/
String type() default "";

View File

@@ -42,6 +42,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.SingletonBeanRegistry;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfigurationMetadata;
import org.springframework.boot.autoconfigure.condition.ConditionMessage.Style;
import org.springframework.context.annotation.Bean;
@@ -217,8 +218,8 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat
Map<String, BeanDefinition> typeMatchedDefinitions = getBeanDefinitionsForType(classLoader,
considerHierarchy, beanFactory, type, parameterizedContainers);
Set<String> typeMatchedNames = matchedNamesFrom(typeMatchedDefinitions,
(name, definition) -> !beansIgnoredByType.contains(name) && !ScopedProxyUtils.isScopedTarget(name)
&& (definition == null || definition.isAutowireCandidate()));
(name, definition) -> isCandidate(name, definition, beansIgnoredByType)
&& !ScopedProxyUtils.isScopedTarget(name));
if (typeMatchedNames.isEmpty()) {
result.recordUnmatchedType(type);
}
@@ -230,8 +231,7 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat
Map<String, BeanDefinition> annotationMatchedDefinitions = getBeanDefinitionsForAnnotation(classLoader,
beanFactory, annotation, considerHierarchy);
Set<String> annotationMatchedNames = matchedNamesFrom(annotationMatchedDefinitions,
(name, definition) -> !beansIgnoredByType.contains(name)
&& (definition == null || definition.isAutowireCandidate()));
(name, definition) -> isCandidate(name, definition, beansIgnoredByType));
if (annotationMatchedNames.isEmpty()) {
result.recordUnmatchedAnnotation(annotation);
}
@@ -262,6 +262,18 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat
return matchedNames;
}
private boolean isCandidate(String name, BeanDefinition definition, Set<String> ignoredBeans) {
return (!ignoredBeans.contains(name))
&& (definition == null || (definition.isAutowireCandidate() && isDefaultCandidate(definition)));
}
private boolean isDefaultCandidate(BeanDefinition definition) {
if (definition instanceof AbstractBeanDefinition abstractBeanDefinition) {
return abstractBeanDefinition.isDefaultCandidate();
}
return true;
}
private Set<String> getNamesOfBeansIgnoredByType(ClassLoader classLoader, ListableBeanFactory beanFactory,
boolean considerHierarchy, Set<String> ignoredTypes, Set<Class<?>> parameterizedContainers) {
Set<String> result = null;

View File

@@ -261,6 +261,25 @@ class ConditionalOnBeanTests {
.run((context) -> assertThat(context).doesNotHaveBean("bar"));
}
@Test
void conditionalOnBeanTypeIgnoresNotDefaultCandidateBean() {
this.contextRunner.withUserConfiguration(NotDefaultCandidateConfiguration.class, OnBeanClassConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean("bar"));
}
@Test
void conditionalOnBeanNameMatchesNotDefaultCandidateBean() {
this.contextRunner.withUserConfiguration(NotDefaultCandidateConfiguration.class, OnBeanNameConfiguration.class)
.run((context) -> assertThat(context).hasBean("bar"));
}
@Test
void conditionalOnAnnotatedBeanIgnoresNotDefaultCandidateBean() {
this.contextRunner
.withUserConfiguration(AnnotatedNotDefaultCandidateConfig.class, OnAnnotationConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean("bar"));
}
private Consumer<ConfigurableApplicationContext> exampleBeanRequirement(String... names) {
return (context) -> {
String[] beans = context.getBeanNamesForType(ExampleBean.class);
@@ -356,6 +375,16 @@ class ConditionalOnBeanTests {
}
@Configuration(proxyBeanMethods = false)
static class NotDefaultCandidateConfiguration {
@Bean(defaultCandidate = false)
String foo() {
return "foo";
}
}
@Configuration(proxyBeanMethods = false)
@ImportResource("org/springframework/boot/autoconfigure/condition/foo.xml")
static class XmlConfiguration {
@@ -569,6 +598,16 @@ class ConditionalOnBeanTests {
}
@Configuration(proxyBeanMethods = false)
static class AnnotatedNotDefaultCandidateConfig {
@Bean(defaultCandidate = false)
ExampleBean exampleBean() {
return new ExampleBean("value");
}
}
@TestAnnotation
static class ExampleBean {

View File

@@ -363,6 +363,25 @@ class ConditionalOnMissingBeanTests {
.run((context) -> assertThat(context).hasBean("bar"));
}
@Test
void typeBasedMatchingIgnoresBeanThatIsNotDefaultCandidate() {
this.contextRunner.withUserConfiguration(NotDefaultCandidateConfig.class, OnBeanTypeConfiguration.class)
.run((context) -> assertThat(context).hasBean("bar"));
}
@Test
void nameBasedMatchingConsidersBeanThatIsNotDefaultCandidate() {
this.contextRunner.withUserConfiguration(NotDefaultCandidateConfig.class, OnBeanNameConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean("bar"));
}
@Test
void annotationBasedMatchingIgnoresBeanThatIsNotDefaultCandidateBean() {
this.contextRunner
.withUserConfiguration(AnnotatedNotDefaultCandidateConfig.class, OnAnnotationConfiguration.class)
.run((context) -> assertThat(context).hasBean("bar"));
}
private Consumer<ConfigurableApplicationContext> exampleBeanRequirement(String... names) {
return (context) -> {
String[] beans = context.getBeanNamesForType(ExampleBean.class);
@@ -607,6 +626,16 @@ class ConditionalOnMissingBeanTests {
}
@Configuration(proxyBeanMethods = false)
static class NotDefaultCandidateConfig {
@Bean(defaultCandidate = false)
String foo() {
return "foo";
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = "foo")
static class HierarchyConsidered {
@@ -780,6 +809,16 @@ class ConditionalOnMissingBeanTests {
}
@Configuration(proxyBeanMethods = false)
static class AnnotatedNotDefaultCandidateConfig {
@Bean(autowireCandidate = false)
ExampleBean exampleBean() {
return new ExampleBean("value");
}
}
@TestAnnotation
static class ExampleBean {

View File

@@ -184,6 +184,17 @@ class ConditionalOnSingleCandidateTests {
});
}
@Test
void singleCandidateMultipleCandidatesOneDefaultCandidate() {
this.contextRunner
.withUserConfiguration(AlphaConfiguration.class, BravoNonDefaultConfiguration.class,
OnBeanSingleCandidateConfiguration.class)
.run((context) -> {
assertThat(context).hasBean("consumer");
assertThat(context.getBean("consumer")).isEqualTo("alpha");
});
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnSingleCandidate(String.class)
static class OnBeanSingleCandidateConfiguration {
@@ -303,4 +314,14 @@ class ConditionalOnSingleCandidateTests {
}
@Configuration(proxyBeanMethods = false)
static class BravoNonDefaultConfiguration {
@Bean(defaultCandidate = false)
String bravo() {
return "bravo";
}
}
}

View File

@@ -110,43 +110,41 @@ See xref:reference:data/sql.adoc#data.sql.datasource[] in the "`Spring Boot Feat
[[howto.data-access.configure-two-datasources]]
== Configure Two DataSources
If you need to configure multiple data sources, you can apply the same tricks that were described in the previous section.
You must, however, mark one of the `DataSource` instances as `@Primary`, because various auto-configurations down the road expect to be able to get one by type.
To define an additional `DataSource`, an approach that's similar to the previous section can be used.
A key difference is that the `DataSource` `@Bean` must be declared with `defaultCandidate=false`.
This prevents the auto-configured `DataSource` from backing off.
To allow the additional `DataSource` to be injected where it's needed, also annotate it with `@Qualifier` as shown in the following example:
If you create your own `DataSource`, the auto-configuration backs off.
In the following example, we provide the _exact_ same feature set as the auto-configuration provides on the primary data source:
include-code::MyAdditionalDataSourceConfiguration[]
include-code::MyDataSourcesConfiguration[]
To consume the additional `DataSource`, annotate the injection point with the same `@Qualifier`.
TIP: `firstDataSourceProperties` has to be flagged as `@Primary` so that the database initializer feature uses your copy (if you use the initializer).
Both data sources are also bound for advanced customizations.
For instance, you could configure them as follows:
The auto-configured and additional data sources can be configured as follows:
[configprops%novalidate,yaml]
----
spring:
datasource:
url: "jdbc:mysql://localhost/first"
username: "dbuser"
password: "dbpass"
configuration:
maximum-pool-size: 30
app:
datasource:
first:
url: "jdbc:mysql://localhost/first"
username: "dbuser"
password: "dbpass"
configuration:
maximum-pool-size: 30
second:
url: "jdbc:mysql://localhost/second"
username: "dbuser"
password: "dbpass"
max-total: 30
url: "jdbc:mysql://localhost/second"
username: "dbuser"
password: "dbpass"
max-total: 30
----
You can apply the same concept to the secondary `DataSource` as well, as shown in the following example:
More advanced, implementation-specific, configuration of the auto-configured `DataSource` is available through the `spring.datasource.configuration.*` properties.
You can apply the same concept to the additional `DataSource` as well, as shown in the following example:
include-code::MyCompleteDataSourcesConfiguration[]
include-code::MyCompleteAdditionalDataSourceConfiguration[]
The preceding example configures two data sources on custom namespaces with the same logic as Spring Boot would use in auto-configuration.
Note that each `configuration` sub namespace provides advanced settings based on the chosen implementation.
The preceding example configures the additional data source with the same logic as Spring Boot would use in auto-configuration.
Note that the `app.datasource.configuration.*` properties provide advanced settings based on the chosen implementation.
@@ -295,26 +293,28 @@ You can disable or tune this behavior by registering a `HibernatePropertiesCusto
To take full control of the configuration of the `EntityManagerFactory`, you need to add a `@Bean` named '`entityManagerFactory`'.
Spring Boot auto-configuration switches off its entity manager in the presence of a bean of that type.
NOTE: When you create a bean for `LocalContainerEntityManagerFactoryBean` yourself, any customization that was applied during the creation of the auto-configured `LocalContainerEntityManagerFactoryBean` is lost.
Make sure to use the auto-configured `EntityManagerFactoryBuilder` to retain JPA and vendor properties.
This is particularly important if you were relying on `spring.jpa.*` properties for configuring things like the naming strategy or the DDL mode.
[[howto.data-access.use-multiple-entity-managers]]
== Using Multiple EntityManagerFactories
If you need to use JPA against multiple data sources, you likely need one `EntityManagerFactory` per data source.
If you need to use JPA against multiple datasources, you likely need one `EntityManagerFactory` per datasource.
The `LocalContainerEntityManagerFactoryBean` from Spring ORM allows you to configure an `EntityManagerFactory` for your needs.
You can also reuse `JpaProperties` to bind settings for each `EntityManagerFactory`, as shown in the following example:
You can also reuse `JpaProperties` to bind settings for a second `EntityManagerFactory`.
Building upon xref:how-to:data-access.adoc#howto.data-access.configure-two-datasources[the example for configuring a second `DataSource`], a second `EntityManagerFactory` can be defined as shown in the following example:
include-code::MyEntityManagerFactoryConfiguration[]
include-code::MyAdditionalEntityManagerFactoryConfiguration[]
The example above creates an `EntityManagerFactory` using a `DataSource` bean named `firstDataSource`.
The example above creates an `EntityManagerFactory` using the `DataSource` bean qualified with `@Qualifier("second")`.
It scans entities located in the same package as `Order`.
It is possible to map additional JPA properties using the `app.first.jpa` namespace.
It is possible to map additional JPA properties using the `app.jpa` namespace.
The use of `@Bean(defaultCandidate=false)` allows the `secondJpaProperties` and `secondEntityManagerFactory` beans to be defined without interfering with auto-configured beans of the same type.
NOTE: When you create a bean for `LocalContainerEntityManagerFactoryBean` yourself, any customization that was applied during the creation of the auto-configured `LocalContainerEntityManagerFactoryBean` is lost.
Make sure to use the auto-configured `EntityManagerFactoryBuilder` to retain JPA and vendor properties.
This is particularly important if you were relying on `spring.jpa.*` properties for configuring things like the naming strategy or the DDL mode.
You should provide a similar configuration for any additional data sources for which you need JPA access.
You should provide a similar configuration for any more additional data sources for which you need JPA access.
To complete the picture, you need to configure a `JpaTransactionManager` for each `EntityManagerFactory` as well.
Alternatively, you might be able to use a JTA transaction manager that spans both.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,35 +16,20 @@
package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration(proxyBeanMethods = false)
public class MyDataSourcesConfiguration {
public class MyAdditionalDataSourceConfiguration {
@Bean
@Primary
@ConfigurationProperties("app.datasource.first")
public DataSourceProperties firstDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
@ConfigurationProperties("app.datasource.first.configuration")
public HikariDataSource firstDataSource(DataSourceProperties firstDataSourceProperties) {
return firstDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("app.datasource.second")
@Qualifier("second")
@Bean(defaultCandidate = false)
@ConfigurationProperties("app.datasource")
public BasicDataSource secondDataSource() {
return DataSourceBuilder.create().type(BasicDataSource.class).build();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -24,33 +23,20 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration(proxyBeanMethods = false)
public class MyCompleteDataSourcesConfiguration {
public class MyCompleteAdditionalDataSourceConfiguration {
@Bean
@Primary
@ConfigurationProperties("app.datasource.first")
public DataSourceProperties firstDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
@ConfigurationProperties("app.datasource.first.configuration")
public HikariDataSource firstDataSource(DataSourceProperties firstDataSourceProperties) {
return firstDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("app.datasource.second")
@Qualifier("second")
@Bean(defaultCandidate = false)
@ConfigurationProperties("app.datasource")
public DataSourceProperties secondDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties("app.datasource.second.configuration")
@Qualifier("second")
@Bean(defaultCandidate = false)
@ConfigurationProperties("app.datasource.configuration")
public BasicDataSource secondDataSource(
@Qualifier("secondDataSourceProperties") DataSourceProperties secondDataSourceProperties) {
return secondDataSourceProperties.initializeDataSourceBuilder().type(BasicDataSource.class).build();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.boot.docs.howto.dataaccess.usemultipleentitymanagers
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
@@ -28,19 +29,21 @@ import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
@Configuration(proxyBeanMethods = false)
public class MyEntityManagerFactoryConfiguration {
public class MyAdditionalEntityManagerFactoryConfiguration {
@Bean
@ConfigurationProperties("app.jpa.first")
public JpaProperties firstJpaProperties() {
@Qualifier("second")
@Bean(defaultCandidate = false)
@ConfigurationProperties("app.jpa")
public JpaProperties secondJpaProperties() {
return new JpaProperties();
}
@Bean
public LocalContainerEntityManagerFactoryBean firstEntityManagerFactory(DataSource firstDataSource,
JpaProperties firstJpaProperties) {
EntityManagerFactoryBuilder builder = createEntityManagerFactoryBuilder(firstJpaProperties);
return builder.dataSource(firstDataSource).packages(Order.class).persistenceUnit("firstDs").build();
@Qualifier("second")
@Bean(defaultCandidate = false)
public LocalContainerEntityManagerFactoryBean secondEntityManagerFactory(@Qualifier("second") DataSource dataSource,
@Qualifier("second") JpaProperties jpaProperties) {
EntityManagerFactoryBuilder builder = createEntityManagerFactoryBuilder(jpaProperties);
return builder.dataSource(dataSource).packages(Order.class).persistenceUnit("second").build();
}
private EntityManagerFactoryBuilder createEntityManagerFactoryBuilder(JpaProperties jpaProperties) {

View File

@@ -16,6 +16,6 @@
package org.springframework.boot.docs.howto.dataaccess.usemultipleentitymanagers;
class Order {
public class Order {
}

View File

@@ -20,7 +20,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@Configuration(proxyBeanMethods = false)
@EnableJpaRepositories(basePackageClasses = Order.class, entityManagerFactoryRef = "firstEntityManagerFactory")
@EnableJpaRepositories(basePackageClasses = Order.class, entityManagerFactoryRef = "entityManagerFactory")
public class OrderConfiguration {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,34 +16,20 @@
package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources
import com.zaxxer.hikari.HikariDataSource
import org.apache.commons.dbcp2.BasicDataSource
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.jdbc.DataSourceBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
@Configuration(proxyBeanMethods = false)
class MyDataSourcesConfiguration {
class MyAdditionalDataSourceConfiguration {
@Bean
@Primary
@ConfigurationProperties("app.datasource.first")
fun firstDataSourceProperties(): DataSourceProperties {
return DataSourceProperties()
}
@Bean
@Primary
@ConfigurationProperties("app.datasource.first.configuration")
fun firstDataSource(firstDataSourceProperties: DataSourceProperties): HikariDataSource {
return firstDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource::class.java).build()
}
@Bean
@ConfigurationProperties("app.datasource.second")
@Qualifier("second")
@Bean(defaultCandidate = false)
@ConfigurationProperties("app.datasource")
fun secondDataSource(): BasicDataSource {
return DataSourceBuilder.create().type(BasicDataSource::class.java).build()
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,39 +16,27 @@
package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources
import com.zaxxer.hikari.HikariDataSource
import org.apache.commons.dbcp2.BasicDataSource
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
@Configuration(proxyBeanMethods = false)
class MyCompleteDataSourcesConfiguration {
class MyCompleteAdditionalDataSourceConfiguration {
@Bean
@Primary
@ConfigurationProperties("app.datasource.first")
fun firstDataSourceProperties(): DataSourceProperties {
return DataSourceProperties()
}
@Bean
@Primary
@ConfigurationProperties("app.datasource.first.configuration")
fun firstDataSource(firstDataSourceProperties: DataSourceProperties): HikariDataSource {
return firstDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource::class.java).build()
}
@Bean
@ConfigurationProperties("app.datasource.second")
@Qualifier("second")
@Bean(defaultCandidate = false)
@ConfigurationProperties("app.datasource")
fun secondDataSourceProperties(): DataSourceProperties {
return DataSourceProperties()
}
@Bean
@ConfigurationProperties("app.datasource.second.configuration")
@Qualifier("second")
@Bean(defaultCandidate = false)
@ConfigurationProperties("app.datasource.configuration")
fun secondDataSource(secondDataSourceProperties: DataSourceProperties): BasicDataSource {
return secondDataSourceProperties.initializeDataSourceBuilder().type(BasicDataSource::class.java).build()
}

View File

@@ -18,6 +18,7 @@ package org.springframework.boot.docs.howto.dataaccess.usemultipleentitymanagers
import javax.sql.DataSource
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder
@@ -29,21 +30,23 @@ import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter
@Suppress("UNUSED_PARAMETER")
@Configuration(proxyBeanMethods = false)
class MyEntityManagerFactoryConfiguration {
class MyAdditionalEntityManagerFactoryConfiguration {
@Bean
@ConfigurationProperties("app.jpa.first")
fun firstJpaProperties(): JpaProperties {
@Qualifier("second")
@Bean(defaultCandidate = false)
@ConfigurationProperties("app.jpa")
fun secondJpaProperties(): JpaProperties {
return JpaProperties()
}
@Bean
@Qualifier("second")
@Bean(defaultCandidate = false)
fun firstEntityManagerFactory(
firstDataSource: DataSource?,
firstJpaProperties: JpaProperties
@Qualifier("second") dataSource: DataSource,
@Qualifier("second") jpaProperties: JpaProperties
): LocalContainerEntityManagerFactoryBean {
val builder = createEntityManagerFactoryBuilder(firstJpaProperties)
return builder.dataSource(firstDataSource).packages(Order::class.java).persistenceUnit("firstDs").build()
val builder = createEntityManagerFactoryBuilder(jpaProperties)
return builder.dataSource(dataSource).packages(Order::class.java).persistenceUnit("second").build()
}
private fun createEntityManagerFactoryBuilder(jpaProperties: JpaProperties): EntityManagerFactoryBuilder {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,37 +21,41 @@ import java.sql.SQLException;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link MyCompleteDataSourcesConfiguration}.
* Tests for {@link MyCompleteAdditionalDataSourceConfiguration}.
*
* @author Stephane Nicoll
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest
@Import(MyCompleteDataSourcesConfiguration.class)
@Import(MyCompleteAdditionalDataSourceConfiguration.class)
class MyCompleteDataSourcesConfigurationTests {
@Autowired
private ApplicationContext context;
@Autowired
private DataSource dataSource;
@Autowired
@Qualifier("second")
private DataSource secondDataSource;
@Test
void validateConfiguration() throws SQLException {
assertThat(this.context.getBeansOfType(DataSource.class)).hasSize(2);
DataSource dataSource = this.context.getBean(DataSource.class);
assertThat(this.context.getBean("firstDataSource")).isSameAs(dataSource);
assertThat(dataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:");
DataSource secondDataSource = this.context.getBean("secondDataSource", DataSource.class);
assertThat(secondDataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:");
assertThat(this.context.getBean("dataSource")).isSameAs(this.dataSource);
assertThat(this.dataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:");
assertThat(this.context.getBean("secondDataSource")).isSameAs(this.secondDataSource);
assertThat(this.secondDataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:");
}
}

View File

@@ -22,39 +22,44 @@ import javax.sql.DataSource;
import org.apache.commons.dbcp2.BasicDataSource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link MyDataSourcesConfiguration}.
* Tests for {@link MyAdditionalDataSourceConfiguration}.
*
* @author Stephane Nicoll
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = { "app.datasource.second.url=jdbc:h2:mem:bar;DB_CLOSE_DELAY=-1",
"app.datasource.second.max-total=42" })
@Import(MyDataSourcesConfiguration.class)
@SpringBootTest(properties = { "app.datasource.url=jdbc:h2:mem:bar;DB_CLOSE_DELAY=-1", "app.datasource.max-total=42" })
@Import(MyAdditionalDataSourceConfiguration.class)
class MyDataSourcesConfigurationTests {
@Autowired
private ApplicationContext context;
@Autowired
private DataSource dataSource;
@Autowired
@Qualifier("second")
private DataSource secondDataSource;
@Test
void validateConfiguration() throws SQLException {
assertThat(this.context.getBeansOfType(DataSource.class)).hasSize(2);
DataSource dataSource = this.context.getBean(DataSource.class);
assertThat(this.context.getBean("firstDataSource")).isSameAs(dataSource);
assertThat(dataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:");
BasicDataSource secondDataSource = this.context.getBean("secondDataSource", BasicDataSource.class);
assertThat(secondDataSource.getUrl()).isEqualTo("jdbc:h2:mem:bar;DB_CLOSE_DELAY=-1");
assertThat(secondDataSource.getMaxTotal()).isEqualTo(42);
assertThat(this.context.getBean("dataSource")).isSameAs(this.dataSource);
assertThat(this.dataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:");
assertThat(this.context.getBean("secondDataSource")).isSameAs(this.secondDataSource);
assertThat(this.secondDataSource).extracting((dataSource) -> ((BasicDataSource) dataSource).getUrl())
.isEqualTo("jdbc:h2:mem:bar;DB_CLOSE_DELAY=-1");
assertThat(this.secondDataSource).extracting((dataSource) -> ((BasicDataSource) dataSource).getMaxTotal())
.isEqualTo(42);
}
}