使用注解处理器生成自己的元数据

您可以使用 spring-boot-configuration-processor jar 轻松地从用 @ConfigurationProperties 注解的项目生成自己的配置元数据文件。这个 jar 包含一个 Java 注解处理器,它会在项目编译时被调用。

配置注解处理器

使用 Maven 构建时,配置编译器插件(3.12.0 或更高版本),将 spring-boot-configuration-processor 添加到注解处理器路径中。

<project>
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<annotationProcessorPaths>
						<path>
							<groupId>org.springframework.boot</groupId>
							<artifactId>spring-boot-configuration-processor</artifactId>
						</path>
					</annotationProcessorPaths>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

使用 Gradle 时,应在 annotationProcessor 配置中声明一个依赖项,如以下示例所示。

dependencies {
	annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
}

如果您正在使用 additional-spring-configuration-metadata.json 文件,应配置 compileJava 任务使其依赖于 processResources 任务,如以下示例所示。

tasks.named('compileJava') {
	inputs.files(tasks.named('processResources'))
}

这个依赖确保了当注解处理器在编译期间运行时,额外的元数据可用。

如果您在项目中使用 AspectJ,需要确保注解处理器只运行一次。有几种方法可以做到这一点。对于 Maven,您可以明确配置 maven-apt-plugin,并将依赖项仅添加到该注解处理器中。您也可以让 AspectJ 插件运行所有处理,并在 maven-compiler-plugin 配置中禁用注解处理,如下所示。

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-compiler-plugin</artifactId>
	<configuration>
		<proc>none</proc>
	</configuration>
</plugin>

如果您在项目中使用 Lombok,需要确保其注解处理器在 spring-boot-configuration-processor 之前运行。对于 Maven,可以使用 Maven compiler plugin 的 annotationProcessors 属性按所需的顺序列出注解处理器。对于 Gradle,在 annotationProcessor 配置中按所需的顺序声明依赖项。

自动元数据生成

该处理器会识别用 @ConfigurationProperties 注解的类和方法。

不支持使用 @ConfigurationProperties 进行元注解的自定义注解。

如果类只有一个带参数的构造函数,则每个构造函数参数都会创建一个属性,除非构造函数使用了 @Autowired 注解。如果类有一个显式地使用了 @ConstructorBinding 注解的构造函数,则该构造函数的每个构造函数参数都会创建一个属性。否则,会通过标准 getter 和 setter 方法来发现属性,并对集合和映射类型进行特殊处理(即使只有 getter 存在也会检测到)。注解处理器也支持使用 @Data@Value@Getter@Setter 等 lombok 注解。

考虑以下示例

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "my.server")
public class MyServerProperties {

	/**
	 * Name of the server.
	 */
	private String name;

	/**
	 * IP address to listen to.
	 */
	private String ip = "127.0.0.1";

	/**
	 * Port to listener to.
	 */
	private int port = 9797;

	// getters/setters ...

	public String getName() {
		return this.name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getIp() {
		return this.ip;
	}

	public void setIp(String ip) {
		this.ip = ip;
	}

	public int getPort() {
		return this.port;
	}

	public void setPort(int port) {
		this.port = port;
	}

}

这暴露了三个属性:my.server.name 没有默认值,而 my.server.ipmy.server.port 分别默认为 "127.0.0.1"9797。字段上的 Javadoc 用于填充 description 属性。例如,my.server.ip 的描述是 "要监听的 IP 地址。"。description 属性只能在类型作为正在编译的源代码可用时填充。当类型仅作为依赖项中的已编译类可用时,不会填充。对于这种情况,应提供手动元数据

@ConfigurationProperties 字段 Javadoc 中,您应该只使用纯文本,因为在添加到 JSON 之前不会对其进行处理。

如果您将 @ConfigurationProperties 与 record class 一起使用,则 record 组件的描述应通过类级别的 Javadoc 标签 @param 提供(record class 中没有显式的实例字段来放置常规的字段级 Javadoc)。

注解处理器采用多种启发式方法从源代码模型中提取默认值。默认值只能在类型作为正在编译的源代码可用时提取。当类型仅作为依赖项中的已编译类可用时,不会提取默认值。此外,默认值必须静态提供。特别是,不要引用在另一个类中定义的常量。此外,注解处理器无法自动检测 Collections 的默认值。

对于无法检测到默认值的情况,应提供手动元数据。考虑以下示例

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "my.messaging")
public class MyMessagingProperties {

	private List<String> addresses = new ArrayList<>(Arrays.asList("a", "b"));

	private ContainerType containerType = ContainerType.SIMPLE;

	// getters/setters ...

	public List<String> getAddresses() {
		return this.addresses;
	}

	public void setAddresses(List<String> addresses) {
		this.addresses = addresses;
	}

	public ContainerType getContainerType() {
		return this.containerType;
	}

	public void setContainerType(ContainerType containerType) {
		this.containerType = containerType;
	}

	public enum ContainerType {

		SIMPLE, DIRECT

	}

}

为了记录上述类中属性的默认值,您可以将以下内容添加到模块的手动元数据

{"properties": [
	{
		"name": "my.messaging.addresses",
		"defaultValue": ["a", "b"]
	},
	{
		"name": "my.messaging.container-type",
		"defaultValue": "simple"
	}
]}
仅需要属性的 name 来记录现有属性的额外元数据。

嵌套属性

注解处理器会自动将内部类视为嵌套属性。与其在命名空间的根目录中记录 ipport,我们可以为其创建一个子命名空间。考虑更新后的示例

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "my.server")
public class MyServerProperties {

	private String name;

	private Host host;

	// getters/setters ...

	public String getName() {
		return this.name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Host getHost() {
		return this.host;
	}

	public void setHost(Host host) {
		this.host = host;
	}

	public static class Host {

		private String ip;

		private int port;

		// getters/setters ...

		public String getIp() {
			return this.ip;
		}

		public void setIp(String ip) {
			this.ip = ip;
		}

		public int getPort() {
			return this.port;
		}

		public void setPort(int port) {
			this.port = port;
		}

	}

}

上面的示例为 my.server.namemy.server.host.ipmy.server.host.port 属性生成元数据信息。您可以在字段或 getter 方法上使用 @NestedConfigurationProperty 注解,以指示应将常规(非内部)类视为嵌套类。

这对集合和映射没有影响,因为这些类型会自动识别,并为每个类型生成一个元数据属性。

添加额外元数据

Spring Boot 的配置文件处理非常灵活,通常情况下,可能存在未绑定到 @ConfigurationProperties bean 的属性。您可能还需要调整现有键的一些属性。为了支持这些情况并允许您提供自定义“提示”,注解处理器会自动将 META-INF/additional-spring-configuration-metadata.json 中的项合并到主元数据文件中。

如果您引用了自动检测到的属性,则如果指定了描述、默认值和弃用信息,它们将被覆盖。如果在当前模块中未识别手动属性声明,则将其添加为新属性。

additional-spring-configuration-metadata.json 文件的格式与常规的 spring-configuration-metadata.json 完全相同。额外属性文件是可选的。如果您没有任何额外属性,则无需添加此文件。