2021-05-04 13:01:20 -07:00

351 lines
21 KiB
Plaintext

[[features.developing-auto-configuration]]
== Creating Your Own Auto-configuration
If you work in a company that develops shared libraries, or if you work on an open-source or commercial library, you might want to develop your own auto-configuration.
Auto-configuration classes can be bundled in external jars and still be picked-up by Spring Boot.
Auto-configuration can be associated to a "`starter`" that provides the auto-configuration code as well as the typical libraries that you would use with it.
We first cover what you need to know to build your own auto-configuration and then we move on to the <<features#features.developing-auto-configuration.custom-starter,typical steps required to create a custom starter>>.
TIP: A https://github.com/snicoll-demos/spring-boot-master-auto-configuration[demo project] is available to showcase how you can create a starter step-by-step.
[[features.developing-auto-configuration.understanding-auto-configured-beans]]
=== Understanding Auto-configured Beans
Under the hood, auto-configuration is implemented with standard `@Configuration` classes.
Additional `@Conditional` annotations are used to constrain when the auto-configuration should apply.
Usually, auto-configuration classes use `@ConditionalOnClass` and `@ConditionalOnMissingBean` annotations.
This ensures that auto-configuration applies only when relevant classes are found and when you have not declared your own `@Configuration`.
You can browse the source code of {spring-boot-autoconfigure-module-code}[`spring-boot-autoconfigure`] to see the `@Configuration` classes that Spring provides (see the {spring-boot-code}/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories[`META-INF/spring.factories`] file).
[[features.developing-auto-configuration.locating-auto-configuration-candidates]]
=== Locating Auto-configuration Candidates
Spring Boot checks for the presence of a `META-INF/spring.factories` file within your published jar.
The file should list your configuration classes under the `EnableAutoConfiguration` key, as shown in the following example:
[indent=0]
----
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.mycorp.libx.autoconfigure.LibXAutoConfiguration,\
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration
----
NOTE: Auto-configurations must be loaded that way _only_.
Make sure that they are defined in a specific package space and that they are never the target of component scanning.
Furthermore, auto-configuration classes should not enable component scanning to find additional components.
Specific ``@Import``s should be used instead.
You can use the {spring-boot-autoconfigure-module-code}/AutoConfigureAfter.java[`@AutoConfigureAfter`] or {spring-boot-autoconfigure-module-code}/AutoConfigureBefore.java[`@AutoConfigureBefore`] annotations if your configuration needs to be applied in a specific order.
For example, if you provide web-specific configuration, your class may need to be applied after `WebMvcAutoConfiguration`.
If you want to order certain auto-configurations that should not have any direct knowledge of each other, you can also use `@AutoConfigureOrder`.
That annotation has the same semantic as the regular `@Order` annotation but provides a dedicated order for auto-configuration classes.
As with standard `@Configuration` classes, the order in which auto-configuration classes are applied only affects the order in which their beans are defined.
The order in which those beans are subsequently created is unaffected and is determined by each bean's dependencies and any `@DependsOn` relationships.
[[features.developing-auto-configuration.condition-annotations]]
=== Condition Annotations
You almost always want to include one or more `@Conditional` annotations on your auto-configuration class.
The `@ConditionalOnMissingBean` annotation is one common example that is used to allow developers to override auto-configuration if they are not happy with your defaults.
Spring Boot includes a number of `@Conditional` annotations that you can reuse in your own code by annotating `@Configuration` classes or individual `@Bean` methods.
These annotations include:
* <<features#features.developing-auto-configuration.condition-annotations.class-conditions>>
* <<features#features.developing-auto-configuration.condition-annotations.bean-conditions>>
* <<features#features.developing-auto-configuration.condition-annotations.property-conditions>>
* <<features#features.developing-auto-configuration.condition-annotations.resource-conditions>>
* <<features#features.developing-auto-configuration.condition-annotations.web-application-conditions>>
* <<features#features.developing-auto-configuration.condition-annotations.spel-conditions>>
[[features.developing-auto-configuration.condition-annotations.class-conditions]]
==== Class Conditions
The `@ConditionalOnClass` and `@ConditionalOnMissingClass` annotations let `@Configuration` classes be included based on the presence or absence of specific classes.
Due to the fact that annotation metadata is parsed by using https://asm.ow2.io/[ASM], you can use the `value` attribute to refer to the real class, even though that class might not actually appear on the running application classpath.
You can also use the `name` attribute if you prefer to specify the class name by using a `String` value.
This mechanism does not apply the same way to `@Bean` methods where typically the return type is the target of the condition: before the condition on the method applies, the JVM will have loaded the class and potentially processed method references which will fail if the class is not present.
To handle this scenario, a separate `@Configuration` class can be used to isolate the condition, as shown in the following example:
[source,java,indent=0,subs="verbatim"]
----
include::{docs-java}/features/developingautoconfiguration/conditionannotations/classconditions/MyAutoConfiguration.java[]
----
TIP: If you use `@ConditionalOnClass` or `@ConditionalOnMissingClass` as a part of a meta-annotation to compose your own composed annotations, you must use `name` as referring to the class in such a case is not handled.
[[features.developing-auto-configuration.condition-annotations.bean-conditions]]
==== Bean Conditions
The `@ConditionalOnBean` and `@ConditionalOnMissingBean` annotations let a bean be included based on the presence or absence of specific beans.
You can use the `value` attribute to specify beans by type or `name` to specify beans by name.
The `search` attribute lets you limit the `ApplicationContext` hierarchy that should be considered when searching for beans.
When placed on a `@Bean` method, the target type defaults to the return type of the method, as shown in the following example:
[source,java,indent=0,subs="verbatim"]
----
include::{docs-java}/features/developingautoconfiguration/conditionannotations/beanconditions/MyAutoConfiguration.java[]
----
In the preceding example, the `myService` bean is going to be created if no bean of type `MyService` is already contained in the `ApplicationContext`.
TIP: You need to be very careful about the order in which bean definitions are added, as these conditions are evaluated based on what has been processed so far.
For this reason, we recommend using only `@ConditionalOnBean` and `@ConditionalOnMissingBean` annotations on auto-configuration classes (since these are guaranteed to load after any user-defined bean definitions have been added).
NOTE: `@ConditionalOnBean` and `@ConditionalOnMissingBean` do not prevent `@Configuration` classes from being created.
The only difference between using these conditions at the class level and marking each contained `@Bean` method with the annotation is that the former prevents registration of the `@Configuration` class as a bean if the condition does not match.
TIP: When declaring a `@Bean` method, provide as much type information as possible in the method's return type.
For example, if your bean's concrete class implements an interface the bean method's return type should be the concrete class and not the interface.
Providing as much type information as possible in `@Bean` methods is particularly important when using bean conditions as their evaluation can only rely upon to type information that's available in the method signature.
[[features.developing-auto-configuration.condition-annotations.property-conditions]]
==== Property Conditions
The `@ConditionalOnProperty` annotation lets configuration be included based on a Spring Environment property.
Use the `prefix` and `name` attributes to specify the property that should be checked.
By default, any property that exists and is not equal to `false` is matched.
You can also create more advanced checks by using the `havingValue` and `matchIfMissing` attributes.
[[features.developing-auto-configuration.condition-annotations.resource-conditions]]
==== Resource Conditions
The `@ConditionalOnResource` annotation lets configuration be included only when a specific resource is present.
Resources can be specified by using the usual Spring conventions, as shown in the following example: `file:/home/user/test.dat`.
[[features.developing-auto-configuration.condition-annotations.web-application-conditions]]
==== Web Application Conditions
The `@ConditionalOnWebApplication` and `@ConditionalOnNotWebApplication` annotations let configuration be included depending on whether the application is a "`web application`".
A servlet-based web application is any application that uses a Spring `WebApplicationContext`, defines a `session` scope, or has a `ConfigurableWebEnvironment`.
A reactive web application is any application that uses a `ReactiveWebApplicationContext`, or has a `ConfigurableReactiveWebEnvironment`.
The `@ConditionalOnWarDeployment` annotation lets configuration be included depending on whether the application is a traditional WAR application that is deployed to a container.
This condition will not match for applications that are run with an embedded server.
[[features.developing-auto-configuration.condition-annotations.spel-conditions]]
==== SpEL Expression Conditions
The `@ConditionalOnExpression` annotation lets configuration be included based on the result of a {spring-framework-docs}/core.html#expressions[SpEL expression].
[[features.developing-auto-configuration.testing]]
=== Testing your Auto-configuration
An auto-configuration can be affected by many factors: user configuration (`@Bean` definition and `Environment` customization), condition evaluation (presence of a particular library), and others.
Concretely, each test should create a well defined `ApplicationContext` that represents a combination of those customizations.
`ApplicationContextRunner` provides a great way to achieve that.
`ApplicationContextRunner` is usually defined as a field of the test class to gather the base, common configuration.
The following example makes sure that `MyServiceAutoConfiguration` is always invoked:
[source,java,indent=0,subs="verbatim"]
----
include::{docs-java}/features/developingautoconfiguration/testing/MyServiceAutoConfigurationTests.java[tag=runner]
----
TIP: If multiple auto-configurations have to be defined, there is no need to order their declarations as they are invoked in the exact same order as when running the application.
Each test can use the runner to represent a particular use case.
For instance, the sample below invokes a user configuration (`UserConfiguration`) and checks that the auto-configuration backs off properly.
Invoking `run` provides a callback context that can be used with `AssertJ`.
[source,java,indent=0,subs="verbatim"]
----
include::{docs-java}/features/developingautoconfiguration/testing/MyServiceAutoConfigurationTests.java[tag=test-user-config]
----
It is also possible to easily customize the `Environment`, as shown in the following example:
[source,java,indent=0,subs="verbatim"]
----
include::{docs-java}/features/developingautoconfiguration/testing/MyServiceAutoConfigurationTests.java[tag=test-env]
----
The runner can also be used to display the `ConditionEvaluationReport`.
The report can be printed at `INFO` or `DEBUG` level.
The following example shows how to use the `ConditionEvaluationReportLoggingListener` to print the report in auto-configuration tests.
[source,java,indent=0,subs="verbatim"]
----
include::{docs-java}/features/developingautoconfiguration/testing/MyConditionEvaluationReportingTests.java[]
----
[[features.developing-auto-configuration.testing.simulating-a-web-context]]
==== Simulating a Web Context
If you need to test an auto-configuration that only operates in a Servlet or Reactive web application context, use the `WebApplicationContextRunner` or `ReactiveWebApplicationContextRunner` respectively.
[[features.developing-auto-configuration.testing.overriding-classpath]]
==== Overriding the Classpath
It is also possible to test what happens when a particular class and/or package is not present at runtime.
Spring Boot ships with a `FilteredClassLoader` that can easily be used by the runner.
In the following example, we assert that if `MyService` is not present, the auto-configuration is properly disabled:
[source,java,indent=0,subs="verbatim"]
----
include::{docs-java}/features/developingautoconfiguration/testing/MyServiceAutoConfigurationTests.java[tag=test-classloader]
----
[[features.developing-auto-configuration.custom-starter]]
=== Creating Your Own Starter
A typical Spring Boot starter contains code to auto-configure and customize the infrastructure of a given technology, let's call that "acme".
To make it easily extensible, a number of configuration keys in a dedicated namespace can be exposed to the environment.
Finally, a single "starter" dependency is provided to help users get started as easily as possible.
Concretely, a custom starter can contain the following:
* The `autoconfigure` module that contains the auto-configuration code for "acme".
* The `starter` module that provides a dependency to the `autoconfigure` module as well as "acme" and any additional dependencies that are typically useful.
In a nutshell, adding the starter should provide everything needed to start using that library.
This separation in two modules is in no way necessary.
If "acme" has several flavors, options or optional features, then it is better to separate the auto-configuration as you can clearly express the fact some features are optional.
Besides, you have the ability to craft a starter that provides an opinion about those optional dependencies.
At the same time, others can rely only on the `autoconfigure` module and craft their own starter with different opinions.
If the auto-configuration is relatively straightforward and does not have optional feature, merging the two modules in the starter is definitely an option.
[[features.developing-auto-configuration.custom-starter.naming]]
==== Naming
You should make sure to provide a proper namespace for your starter.
Do not start your module names with `spring-boot`, even if you use a different Maven `groupId`.
We may offer official support for the thing you auto-configure in the future.
As a rule of thumb, you should name a combined module after the starter.
For example, assume that you are creating a starter for "acme" and that you name the auto-configure module `acme-spring-boot` and the starter `acme-spring-boot-starter`.
If you only have one module that combines the two, name it `acme-spring-boot-starter`.
[[features.developing-auto-configuration.custom-starter.configuration-keys]]
==== Configuration keys
If your starter provides configuration keys, use a unique namespace for them.
In particular, do not include your keys in the namespaces that Spring Boot uses (such as `server`, `management`, `spring`, and so on).
If you use the same namespace, we may modify these namespaces in the future in ways that break your modules.
As a rule of thumb, prefix all your keys with a namespace that you own (e.g. `acme`).
Make sure that configuration keys are documented by adding field javadoc for each property, as shown in the following example:
[source,java,indent=0,subs="verbatim"]
----
include::{docs-java}/features/developingautoconfiguration/customstarter/configurationkeys/AcmeProperties.java[]
----
NOTE: You should only use plain text with `@ConfigurationProperties` field Javadoc, since they are not processed before being added to the JSON.
Here are some rules we follow internally to make sure descriptions are consistent:
* Do not start the description by "The" or "A".
* For `boolean` types, start the description with "Whether" or "Enable".
* For collection-based types, start the description with "Comma-separated list"
* Use `java.time.Duration` rather than `long` and describe the default unit if it differs from milliseconds, e.g. "If a duration suffix is not specified, seconds will be used".
* Do not provide the default value in the description unless it has to be determined at runtime.
Make sure to <<configuration-metadata#configuration-metadata.annotation-processor,trigger meta-data generation>> so that IDE assistance is available for your keys as well.
You may want to review the generated metadata (`META-INF/spring-configuration-metadata.json`) to make sure your keys are properly documented.
Using your own starter in a compatible IDE is also a good idea to validate that quality of the metadata.
[[features.developing-auto-configuration.custom-starter.autoconfigure-module]]
==== The "`autoconfigure`" Module
The `autoconfigure` module contains everything that is necessary to get started with the library.
It may also contain configuration key definitions (such as `@ConfigurationProperties`) and any callback interface that can be used to further customize how the components are initialized.
TIP: You should mark the dependencies to the library as optional so that you can include the `autoconfigure` module in your projects more easily.
If you do it that way, the library is not provided and, by default, Spring Boot backs off.
Spring Boot uses an annotation processor to collect the conditions on auto-configurations in a metadata file (`META-INF/spring-autoconfigure-metadata.properties`).
If that file is present, it is used to eagerly filter auto-configurations that do not match, which will improve startup time.
It is recommended to add the following dependency in a module that contains auto-configurations:
[source,xml,indent=0,subs="verbatim"]
----
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
----
If you have defined auto-configurations directly in your application, make sure to configure the `spring-boot-maven-plugin` to prevent the `repackage` goal from adding the dependency into the fat jar:
[source,xml,indent=0,subs="verbatim"]
----
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
----
With Gradle 4.5 and earlier, the dependency should be declared in the `compileOnly` configuration, as shown in the following example:
[source,gradle,indent=0,subs="verbatim"]
----
dependencies {
compileOnly "org.springframework.boot:spring-boot-autoconfigure-processor"
}
----
With Gradle 4.6 and later, the dependency should be declared in the `annotationProcessor` configuration, as shown in the following example:
[source,gradle,indent=0,subs="verbatim"]
----
dependencies {
annotationProcessor "org.springframework.boot:spring-boot-autoconfigure-processor"
}
----
[[features.developing-auto-configuration.custom-starter.starter-module]]
==== Starter Module
The starter is really an empty jar.
Its only purpose is to provide the necessary dependencies to work with the library.
You can think of it as an opinionated view of what is required to get started.
Do not make assumptions about the project in which your starter is added.
If the library you are auto-configuring typically requires other starters, mention them as well.
Providing a proper set of _default_ dependencies may be hard if the number of optional dependencies is high, as you should avoid including dependencies that are unnecessary for a typical usage of the library.
In other words, you should not include optional dependencies.
NOTE: Either way, your starter must reference the core Spring Boot starter (`spring-boot-starter`) directly or indirectly (i.e. no need to add it if your starter relies on another starter).
If a project is created with only your custom starter, Spring Boot's core features will be honoured by the presence of the core starter.