bean注入属性
Spring Framework对将从属性文件中找到的属性值注入到bean或@Configuration类中提供了很好的支持。 但是,如果将单个属性值注入这些类中,则会遇到一些问题。
这篇博客文章指出了这些问题,并描述了我们如何解决它们。
让我们开始吧。
如果使用Spring Boot,则应使用其Typesafe配置属性。 您可以从以下网页获取有关此的更多信息:
- Spring Boot参考手册的23.7节Typesafe配置属性
- @EnableConfigurationProperties批注的Javadoc
- @ConfigurationProperties批注的Javadoc
- 在Spring Boot中使用@ConfigurationProperties
很简单,但并非没有问题
如果将单个属性值注入到我们的bean类中,我们将面临以下问题:
1.注入多个属性值很麻烦
如果我们通过使用@Value批注注入单个属性值,或者通过使用Environment对象来获取属性值,则注入多个属性值会很麻烦。
假设我们必须向UrlBuilder对象注入一些属性值。 该对象需要三个属性值:
- 服务器的主机( app.server.host )
- 服务器监听的端口( app.server.port )
- 使用的协议( app.server.protocol )
当UrlBuilder对象构建用于访问Web应用程序不同功能的URL地址时,将使用这些属性值。
如果我们使用构造函数注入和@Value注释注入这些属性值,则UrlBuilder类的源代码如下所示:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;@Component
public class UrlBuilder {private final String host;private final String port;private final String protocol;@Autowiredpublic UrlBuilder(@Value("${app.server.protocol}") String protocol,@Value("${app.server.host}") String serverHost,@Value("${app.server.port}") int serverPort) {this.protocol = protocol.toLowercase();this.serverHost = serverHost;this.serverPort = serverPort;}
}
补充阅读:
- @Value注释的Javadoc
如果我们通过使用构造函数注入和Environment类注入这些属性值,则UrlBuilder类的源代码如下所示:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;@Component
public class UrlBuilder {private final String host;private final String port;private final String protocol;@Autowiredpublic UrlBuilder(Environment env) {this.protocol = env.getRequiredProperty("app.server.protocol").toLowercase();this.serverHost = env.getRequiredProperty("app.server.host");this.serverPort = env.getRequiredProperty("app.server.port", Integer.class);}
}
补充阅读:
- 环境接口的Javadoc
我承认这看起来还不错。 但是,当所需属性值的数量增加和/或我们的类也具有其他依赖项时,将所有这些属性注入都是很麻烦的。
2.我们必须指定多个属性名称(或记住使用常量)
如果我们将单个属性值直接注入需要它们的bean中,并且多个bean(A和B)需要相同的属性值,那么我们想到的第一件事就是在两个bean类中都指定属性名称:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class A {private final String protocol;@Autowiredpublic A(@Value("${app.server.protocol}") String protocol) {this.protocol = protocol.toLowercase();}
}import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class B {private final String protocol;@Autowiredpublic B(@Value("${app.server.protocol}") String protocol) {this.protocol = protocol.toLowercase();}
}
这是一个问题,因为
- 因为我们是人类,所以我们会打错字 。 这不是一个大问题,因为在启动应用程序时我们会注意到它。 但是,它使我们放慢了速度。
- 这使维护更加困难 。 如果更改属性的名称,则必须对使用该属性的每个类进行此更改。
我们可以通过将属性名称移到常量类来解决此问题。 如果这样做,我们的源代码如下所示:
public final PropertyNames {private PropertyNames() {}public static final String PROTOCOL = "${app.server.protocol}";
}import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class A {private final String protocol;@Autowiredpublic A(@Value(PropertyNames.PROTOCOL) String protocol) {this.protocol = protocol.toLowercase();}
}import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class B {private final String protocol;@Autowiredpublic B(@Value(PropertyNames.PROTOCOL) String protocol) {this.protocol = protocol.toLowercase();}
}
这可以解决维护问题,但前提是所有开发人员都记得要使用它。 我们当然可以通过使用代码审阅来强制执行此操作,但这是审阅者必须记住要检查的另一件事。
3.添加验证逻辑成为问题
假设我们有两个类( A和B ),它们需要app.server.protocol属性的值。 如果我们将此属性值直接注入到A和B Bean中,并且想要确保该属性的值为'http'或'https',则必须
- 将验证逻辑添加到两个bean类中。
- 将验证逻辑添加到实用程序类中,并在需要验证是否给出了正确的协议时使用它。
如果我们将验证逻辑添加到两个bean类,则这些类的源代码如下所示:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class A {private final String protocol;@Autowiredpublic A(@Value("${app.server.protocol}") String protocol) {checkThatProtocolIsValid(protocol);this.protocol = protocol.toLowercase();}private void checkThatProtocolIsValid(String protocol) {if (!protocol.equalsIgnoreCase("http") && !protocol.equalsIgnoreCase("https")) {throw new IllegalArgumentException(String.format("Protocol: %s is not allowed. Allowed protocols are: http and https.",protocol));}}
}import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class B {private final String protocol;@Autowiredpublic B(@Value("${app.server.protocol}") String protocol) {checkThatProtocolIsValid(protocol);this.protocol = protocol.toLowercase();}private void checkThatProtocolIsValid(String protocol) {if (!protocol.equalsIgnoreCase("http") && !protocol.equalsIgnoreCase("https")) {throw new IllegalArgumentException(String.format("Protocol: %s is not allowed. Allowed protocols are: http and https.",protocol));}}
}
这是一个维护问题,因为A和B类包含复制粘贴代码。 通过将验证逻辑移至实用程序类并在创建新的A和B对象时使用它,可以稍微改善这种情况。
完成此操作后,我们的源代码如下所示:
public final class ProtocolValidator {private ProtocolValidator() {}public static void checkThatProtocolIsValid(String protocol) {if (!protocol.equalsIgnoreCase("http") && !protocol.equalsIgnoreCase("https")) {throw new IllegalArgumentException(String.format("Protocol: %s is not allowed. Allowed protocols are: http and https.",protocol));}}
}import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class A {private final String protocol;@Autowiredpublic A(@Value("${app.server.protocol}") String protocol) {ProtocolValidator.checkThatProtocolIsValid(protocol);this.protocol = protocol.toLowercase();}
}import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class B {private final String protocol;@Autowiredpublic B(@Value("${app.server.protocol}") String protocol) {ProtocolValidator.checkThatProtocolIsValid(protocol);this.protocol = protocol.toLowercase();}
}
问题在于,我们仍然必须记住要调用此实用程序方法。 我们当然可以通过使用代码审阅来强制执行此操作,但是再次重申,审阅者必须记住要检查的另一件事。
4.我们不能编写好的文档
我们无法编写描述应用程序配置的好的文档,因为我们必须将此文档添加到实际的属性文件中,使用Wiki或编写* gasp * Word文档。
这些选项中的每个选项都会引起问题,因为我们不能在编写需要从属性文件中找到属性值的代码时同时使用它们。 如果需要阅读文档,则必须打开“外部文档”,这会导致上下文切换非常昂贵 。
让我们继续前进,找出如何解决这些问题。
将属性值注入配置Bean
通过将属性值注入配置Bean中,我们可以解决前面提到的问题。 首先,为示例应用程序创建一个简单的属性文件。
创建属性文件
我们要做的第一件事是创建一个属性文件。 我们的示例应用程序的属性文件称为application.properties ,其外观如下:
app.name=Configuration Properties example
app.production.mode.enabled=falseapp.server.port=8080
app.server.protocol=http
app.server.host=localhost
让我们继续并配置示例应用程序的应用程序上下文。
配置应用程序上下文
我们的示例应用程序的应用程序上下文配置类有两个目标:
- 启用Spring MVC并导入其默认配置。
- 确保已读取从application.properties文件中找到的属性值,并且可以将其注入到Spring Bean中。
通过执行以下步骤,我们可以实现第二个目标:
- 配置Spring容器以扫描所有包含bean类的软件包。
- 确保从application.properties文件中找到的属性值已读取并添加到Spring Environment中 。
- 确保从@Value批注中找到的$ {…}占位符替换为从当前Spring Environment及其PropertySources中找到的属性值。
WebAppContext类的源代码如下所示:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;@Configuration
@ComponentScan({"net.petrikainulainen.spring.trenches.config","net.petrikainulainen.spring.trenches.web"
})
@EnableWebMvc
@PropertySource("classpath:application.properties")
public class WebAppContext {/*** Ensures that placeholders are replaced with property values*/@BeanPropertySourcesPlaceholderConfigurer propertyPlaceHolderConfigurer() {return new PropertySourcesPlaceholderConfigurer();}
}
补充阅读:
- @ComponentScan批注的Javadoc
- @PropertySource批注的Javadoc
- Spring框架参考手册的5.13.4 @PropertySource部分
- PropertySourcesPlaceholderConfigurer类的Javadoc
下一步是创建配置Bean类,并将从属性文件中找到的属性值注入到它们中。 让我们找出如何做到这一点。
创建配置Bean类
让我们创建以下描述的两个配置bean类:
- WebProperties类包含用于配置使用的协议,服务器的主机以及服务器侦听的端口的属性值。
- ApplicationProperties类包含用于配置应用程序名称并标识是否启用生产模式的属性值。 它还具有对WebProperties对象的引用。
首先 ,我们必须创建WebProperties类。 我们可以按照以下步骤进行操作:
- 创建WebProperties类,并使用@Component注释对其进行注释。
- 将最终协议 , serverHost和serverPort字段添加到创建的类中。
- 通过使用构造函数注入将属性值注入这些字段,并确保协议字段的值必须为“ http”或“ https”(忽略大小写)。
- 添加用于获取实际属性值的吸气剂。
WebProperties类的源代码如下所示:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;@Component
public final class WebProperties {private final String protocol;private final String serverHost;private final int serverPort;@Autowiredpublic WebProperties(@Value("${app.server.protocol}") String protocol,@Value("${app.server.host}") String serverHost,@Value("${app.server.port}") int serverPort) {checkThatProtocolIsValid(protocol);this.protocol = protocol.toLowercase();this.serverHost = serverHost;this.serverPort = serverPort;}private void checkThatProtocolIsValid(String protocol) {if (!protocol.equalsIgnoreCase("http") && !protocol.equalsIgnoreCase("https")) {throw new IllegalArgumentException(String.format("Protocol: %s is not allowed. Allowed protocols are: http and https.",protocol));}}public String getProtocol() {return protocol;}public String getServerHost() {return serverHost;}public int getServerPort() {return serverPort;}
}
其次 ,我们必须实现ApplicationProperties类。 我们可以按照以下步骤进行操作:
- 创建ApplicationProperties类,并使用@Component注释对其进行注释。
- 将最终名称 , productionModeEnabled和webProperties字段添加到创建的类。
- 通过使用构造函数注入,将属性值和WebProperties Bean注入到ApplicationProperties Bean中。
- 添加用于获取字段值的吸气剂。
ApplicationProperties类的源代码如下所示:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;@Component
public final class ApplicationProperties {private final String name;private final boolean productionModeEnabled;private final WebProperties webProperties;@Autowiredpublic ApplicationProperties(@Value("${app.name}") String name,@Value("${app.production.mode.enabled:false}") boolean productionModeEnabled,WebProperties webProperties) {this.name = name;this.productionModeEnabled = productionModeEnabled;this.webProperties = webProperties;}public String getName() {return name;}public boolean isProductionModeEnabled() {return productionModeEnabled;}public WebProperties getWebProperties() {return webProperties;}
}
让我们继续研究该解决方案的好处。
这对我们有什么帮助?
现在,我们创建了包含从application.properties文件中找到的属性值的Bean类。 该解决方案似乎是一项过度的工程设计,但与传统的简单方法相比,它具有以下优点:
1.我们只能注入一个Bean而不是多个属性值
如果我们将属性值注入配置Bean,然后通过使用构造函数注入将该配置Bean注入UrlBuilder类,则其源代码如下所示:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class UrlBuilder {private final WebProperties properties;@Autowiredpublic UrlBuilder(WebProperties properties) {this.properties = properties;}
}
如我们所见,这使我们的代码更整洁( 尤其是在使用构造函数注入的情况下 )。
2.我们只需指定一次属性名称
如果将属性值注入到配置Bean中,则只能在一个地方指定属性名称。 这意味着
- 我们的代码遵循关注点分离原则。 可从配置Bean中找到属性名称,而其他需要此信息的Bean不知道其来源。 他们只是使用它。
- 我们的代码遵循“ 不要重复自己”的原则。 因为属性名称仅在一个位置(在配置Bean中)指定,所以我们的代码更易于维护。
另外,(IMO)我们的代码看起来也更加简洁:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class A {private final String protocol;@Autowiredpublic A(WebProperties properties) {this.protocol = properties.getProtocol();}
}import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class B {private final String protocol;@Autowiredpublic B(WebProperties properties) {this.protocol = properties.getProtocol();}
}
3.我们只需要编写一次验证逻辑
如果将属性值注入到配置Bean中,则可以将验证逻辑添加到配置Bean中,而其他Bean不必知道它。 这种方法具有三个好处:
- 我们的代码遵循关注点分离原则,因为可以从配置bean(它所属的地方)中找到验证逻辑。 其他的豆子不必知道。
- 我们的代码遵循“不要重复自己”的原则,因为验证逻辑是从一个地方找到的。
- 创建新的bean对象时,我们不必记住要调用验证逻辑,因为在创建配置bean时我们可以执行验证规则。
同样,我们的源代码看起来也更加干净:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class A {private final String protocol;@Autowiredpublic A(WebProperties properties) {this.protocol = properties.getProtocol();}
}import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class B {private final String protocol;@Autowiredpublic B(WebProperties properties) {this.protocol = properties.getProtocol();}
}
4.我们可以从IDE中访问文档
我们可以通过向配置bean中添加Javadoc注释来记录应用程序的配置。 完成此操作后,当我们编写需要这些属性值的代码时,可以从IDE中访问此文档。 我们不需要打开其他文件或阅读维基页面。 我们可以简单地继续编写代码并避免上下文切换的开销 。
让我们继续并总结从这篇博客文章中学到的知识。
摘要
这篇博客文章告诉我们将属性值注入配置Bean:
- 帮助我们遵循关注点分离原则。 有关配置属性和属性值验证的内容封装在我们的配置Bean中。 这意味着使用这些配置bean的bean不知道属性值来自何处或如何验证它们。
- 帮助我们遵循“不要重复自己”的原则,因为1)我们只需指定一次属性名称,然后2)可以将验证逻辑添加到配置bean。
- 使我们的文档更易于访问。
- 使我们的代码更易于编写,阅读和维护。
但是,这无助于我们弄清应用程序的运行时配置。 如果我们需要此信息,则必须读取从服务器中找到的属性文件。 这很麻烦。
我们将在我的下一篇博客文章中解决此问题。
- PS:您可以从Github获得此博客文章的示例应用程序 。
翻译自: https://www.javacodegeeks.com/2015/04/spring-from-the-trenches-injecting-property-values-into-configuration-beans.html
bean注入属性