在不少应用程序中,咱们都须要一个配置类Configuration,一般从一个文本文件中读入配置信息,根据配置调整应用的行为。经过这样的方式,咱们能够用相同的代码来适应不一样的环境,达到灵活性的目标。java
本文探索如何设计好这样的配置类。咱们的重点不在于设计的产物——配置类——自己,而是在设计中的权衡取舍,以及取舍的原则。git
这里咱们以从一个conf.properties文件中读取配置信息为例,以不一样的方式读取配置信息。apache
这个文件的内容以下:编程
birthday=2002-05-11 size=15 closed=true locked = false salary=12.5 name=张三 noneValue=
配置文件是一个标准的属性文件,在一个配置文件中有一系列的配置项,每一个配置项的形式是key=value,其中key为配置项的名称,value为配置项的值。配置项有字符串、数值、布尔、日期等多种类型。api
为了方便,本文直接使用单元测试做为配置类的客户代码,即配置类的消费者。app
如何在应用代码中获取配置信息?如下提供三种方法,咱们随后将分析各自的优缺点。单元测试
JDK中,已经提供了一个Properties类,能够用于从属性文件中读取配置信息。测试
客户代码以下:ui
package yang.yu.configuration; import org.junit.Before; import org.junit.Test; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.util.Properties; import static org.assertj.core.api.Assertions.assertThat; public class PropertiesTest { private String confFile = "/conf.properties"; private Properties properties; @Before public void setUp() throws Exception { properties = new Properties(); try(InputStream in = getClass().getResourceAsStream(confFile)) { properties.load(in); } } @Test public void testGetString() { String name = null; try { String temp = properties.getProperty("name"); name = new String(temp.getBytes("iso-8859-1"), "utf-8"); System.out.println(name); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } assertThat(name).isEqualTo("张三"); } @Test public void testGetStringWithDefault() { String defaultValue = "abc"; String temp = properties.getProperty("notExists"); String name = null; if (temp == null) { name = defaultValue; } else { try { name = new String(temp.getBytes("iso-8859-1"), "utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } assertThat(name).isEqualTo("abc"); } @Test public void testGetInt() { String temp = properties.getProperty("size"); if (temp == null) { throw new ConfigurationKeyNotFoundException(); } int value = -1; try { value = Integer.parseInt(temp); } catch (NumberFormatException e) { throw new RuntimeException("Cannot parse string '" + temp + "' to int"); } assertThat(value).isEqualTo(15); } @Test public void testGetIntWithDefault() { int defaultValue = 0; int value = -1; String temp = properties.getProperty("size"); if (temp == null) { value = defaultValue; } try { value = Integer.parseInt(temp); } catch (NumberFormatException e) { throw new RuntimeException("Cannot parse string '" + temp + "' to int"); } assertThat(value).isEqualTo(15); } ...... }
在这里,咱们用单元测试做为配置的客户代码。咱们经过Properties类从类路径下的conf.properties文件中读取配置信息,而后经过Properties类的getProperty()方法获取相应的配置项。this
虽然从功能上来讲,经过Properties类访问配置项没有任何问题,可是客户代码却很是复杂,下面一一讨论。
1. 繁琐
为了获取一个配置值,必须像下面这样输入不少行代码:
@Test public void testGetIntWithDefault() { int defaultValue = 0; int value = -1; String temp = properties.getProperty("size"); if (temp == null) { value = defaultValue; } try { value = Integer.parseInt(temp); } catch (NumberFormatException e) { throw new RuntimeException("Cannot parse string '" + temp + "' to int"); } assertThat(value).isEqualTo(15); }
由于咱们在获取配置值时,必须进行下面的工做:
2. 重复
因为没有封装,每次访问属性值时都必须输入相似上面的一大段代码,而不仅是一行方法调用。所以,系统中充满着重复的代码。业界已经公认,重复是万恶之源。
3. 僵化
咱们不能灵活应对多种状况,例如是否有缺省值,日期格式是什么,解析失败时是返回缺省值,仍是抛出解析异常等等决策,都直接硬编码到代码之中,不能根据现实须要灵活调整。
4. 脆弱
因为代码复杂,多个关注点相互缠绕,系统很是脆弱,时刻可能由于考虑不周而产生逻辑错误。
直接使用Properties,咱们获得的是一个可读性差、可维护性差、复杂、僵化而脆弱的系统。
针对直接采用Properties作配置类的问题,封装是一个很是有效的解决办法。咱们能够设计一个通用的Configuration类,在该类中封装了异常处理、字符集编码转换、类型转换、缺省值处理等等方面,使得客户代码能够很是简单、直接,系统的可读性、可维护性和可靠性都获得大幅提高。
配置类的代码以下:
package yang.yu.configuration; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import yang.yu.configuration.internal.PropertiesFileUtils; import java.io.*; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Hashtable; import java.util.Properties; public class Configuration { private static final Logger LOGGER = LoggerFactory.getLogger(Configuration.class); private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; private Hashtable<String, String> hashtable; private String dateFormat = DEFAULT_DATE_FORMAT; private boolean defaultWhenParseFailed = false; //当类型转换失败时是否返回缺省值 public static Builder builder() { return new Builder(); } private Configuration(Hashtable<String, String> hashtable) { this.hashtable = hashtable; } public void setDateFormat(String dateFormat) { this.dateFormat = dateFormat; } public void setDefaultWhenParseFailed(boolean defaultWhenParseFailed) { this.defaultWhenParseFailed = defaultWhenParseFailed; } public String getString(String key, String defaultValue) { Assert.notBlank(key, "Key is null or empty!"); String value = hashtable.get(key); return StringUtils.isBlank(value) ? defaultValue : value; } public String getString(String key) { Assert.notBlank(key, "Key is null or empty!"); if (!hashtable.containsKey(key)) { throw new ConfigurationKeyNotFoundException("Configuration key '" + key + "' not found!"); } return hashtable.get(key); } public int getInt(String key, int defaultValue) { if (!hashtable.containsKey(key)) { return defaultValue; } String value = hashtable.get(key); try { return Integer.parseInt(value); } catch (NumberFormatException e) { if (defaultWhenParseFailed) { return defaultValue; } throw new ConfigurationValueParseException("'" + value + "' cannot be parsed to int"); } } public int getInt(String key) { if (!hashtable.containsKey(key)) { throw new ConfigurationKeyNotFoundException("Configuration key '" + key + "' not found!"); } String value = hashtable.get(key); try { return Integer.parseInt(value); } catch (NumberFormatException e) { throw new ConfigurationValueParseException("'" + value + "' cannot be parsed to int"); } } public long getLong(String key, long defaultValue) { if (!hashtable.containsKey(key)) { return defaultValue; } String value = hashtable.get(key); try { return Long.parseLong(value); } catch (NumberFormatException e) { if (defaultWhenParseFailed) { return defaultValue; } throw new ConfigurationValueParseException("'" + value + "' cannot be parsed to long"); } } public long getLong(String key) { if (!hashtable.containsKey(key)) { throw new ConfigurationKeyNotFoundException("Configuration key '" + key + "' not found!"); } String value = hashtable.get(key); try { return Long.parseLong(value); } catch (NumberFormatException e) { throw new ConfigurationValueParseException("'" + value + "' cannot be parsed to long"); } } public double getDouble(String key, double defaultValue) { if (!hashtable.containsKey(key)) { return defaultValue; } String value = hashtable.get(key); try { return Double.parseDouble(value); } catch (NumberFormatException e) { if (defaultWhenParseFailed) { return defaultValue; } throw new ConfigurationValueParseException("'" + value + "' cannot be parsed to double"); } } public double getDouble(String key) { if (!hashtable.containsKey(key)) { throw new ConfigurationKeyNotFoundException("Configuration key '" + key + "' not found!"); } String value = hashtable.get(key); try { return Double.parseDouble(value); } catch (NumberFormatException e) { throw new ConfigurationValueParseException("'" + value + "' cannot be parsed to double"); } } public boolean getBoolean(String key, boolean defaultValue) { if (!hashtable.containsKey(key)) { return defaultValue; } String value = hashtable.get(key); if ("true".equalsIgnoreCase(value)) { return true; } if ("false".equalsIgnoreCase(value)) { return false; } if (defaultWhenParseFailed) { return Boolean.parseBoolean(value); } throw new ConfigurationValueParseException("'" + value + "' cannot be parsed to boolean"); } public boolean getBoolean(String key) { if (!hashtable.containsKey(key)) { throw new ConfigurationKeyNotFoundException("Configuration key '" + key + "' not found!"); } String value = hashtable.get(key); if ("true".equalsIgnoreCase(value)) { return true; } if ("false".equalsIgnoreCase(value)) { return false; } throw new ConfigurationValueParseException("'" + value + "' cannot be parsed to boolean"); } public Date getDate(String key, Date defaultValue) { if (!hashtable.containsKey(key)) { return defaultValue; } String value = hashtable.get(key); try { return new SimpleDateFormat(dateFormat).parse(value); } catch (ParseException e) { if (defaultWhenParseFailed) { return defaultValue; } throw new ConfigurationValueParseException("'" + value + "' cannot be parsed to date"); } } public Date getDate(String key) { if (!hashtable.containsKey(key)) { throw new ConfigurationKeyNotFoundException("Configuration key '" + key + "' not found!"); } String value = hashtable.get(key); try { return new SimpleDateFormat(dateFormat).parse(value); } catch (ParseException e) { throw new ConfigurationValueParseException("'" + value + "' cannot be parsed to date"); } } public static class Builder { private boolean defaultWhenParseFailed = false; //当类型转换失败时是否返回缺省值 private Hashtable<String, String> hashtable = new Hashtable(); private String dateFormat = "yyyy-MM-dd"; private PropertiesFileUtils pfu = new PropertiesFileUtils("utf-8"); public Builder fromClasspath(String confFile) { String path = getClass().getResource(confFile).getFile(); return fromFile(path); } public Builder fromFile(String confFile) { return fromFile(new File(confFile)); } public Builder fromFile(File confFile) { if (!confFile.exists()) { throw new ConfigurationFileNotFoundException(); } if (!confFile.canRead()) { throw new ConfigurationFileReadException("Read configuration file is not permitted!"); } InputStream in = null; try { in = new FileInputStream(confFile); Properties props = new Properties(); props.load(in); hashtable = pfu.rectifyProperties(props); LOGGER.debug("Load configuration from {} at {}", confFile.getAbsolutePath(), new Date()); } catch (IOException e) { throw new ConfigurationFileReadException("Cannot load config file: " + confFile, e); } finally { if (in != null) { try { in.close(); } catch (IOException e) { throw new ConfigurationException("Cannot close input stream.", e); } } } return this; } public Builder dateFormat(String dateFormat) { this.dateFormat = dateFormat; return this; } public Builder defaultWhenParseFailed(boolean defaultWhenParseFailed) { this.defaultWhenParseFailed = defaultWhenParseFailed; return this; } public Configuration build() { if (hashtable.isEmpty()) { throw new ConfigurationException("Configuration source not specified!"); } Configuration result = new Configuration(hashtable); result.setDateFormat(dateFormat); result.setDefaultWhenParseFailed(defaultWhenParseFailed); return result; } } }
经过将异常处理、类型转换、字符编码转换等等功能集中在Configuration类中统一处理,客户端代码卸下这些繁重的、重复性的、易于出错的负担,变得很是简单明了:
package yang.yu.configuration; import org.junit.Before; import org.junit.Test; import java.io.File; import java.util.Calendar; import java.util.Date; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.offset; public class ConfigurationTest { private String confFile = "/conf.properties"; private Configuration instance; //more ///////////////////////////////////////////3. Int型配置项 //////////////////3.1. Int型配置项,完好省值 /** * key存在,value存在,格式正确,应当返回value */ @Test public void get_int_without_defaultValue_happy() { assertThat(instance.getInt("size")).isEqualTo(15); } /** * key存在,value存在,格式不正确,应当抛出ConfigurationValueParseException异常 */ @Test(expected = ConfigurationValueParseException.class) public void get_int_without_defaultValue_and_with_invalid_value() { instance.getInt("name"); } /** * key存在,value不存在,应当抛出ConfigurationValueParseException异常 */ @Test(expected = ConfigurationValueParseException.class) public void get_int_without_defaultValue_and_without_value() { instance.getInt("noneValue"); } /** * key不存在,应当抛出ConfigurationKeyNotFoundException异常 */ @Test(expected = ConfigurationKeyNotFoundException.class) public void get_int_without_defaultValue_and_without_key() { instance.getInt("noneKey"); } /////////////3.2. Int型配置项,有缺省值 /** * key存在, value存在,格式正确,应当返回value */ @Test public void get_int_with_defaultValue_and_with_value() { assertThat(instance.getInt("size", 1000)).isEqualTo(15); } /** * key存在,value存在,格式不正确,defaultWhenParseFailed=true,应当返回缺省值 */ @Test public void get_int_with_defaultValue_and_with_invalid_value_and_defaultWhenParseFailed_is_true() { instance.setDefaultWhenParseFailed(true); assertThat(instance.getInt("name", 1000)).isEqualTo(1000); } /** * key存在,value存在,格式不正确,defaultWhenParseFailed=false,应当抛出ConfigurationValueParseException异常 */ @Test(expected = ConfigurationValueParseException.class) public void get_int_with_defaultValue_and_with_invalid_value_and_defaultWhenParseFailed_is_false() { instance.setDefaultWhenParseFailed(false); instance.getInt("name", 1000); } /** * key存在,value不存在,defaultWhenParseFailed=true,应当返回缺省值 */ @Test public void get_int_with_defaultValue_and_without_value_and_defaultWhenParseFailed_is_true() { instance.setDefaultWhenParseFailed(true); assertThat(instance.getInt("noneValue", 1000)).isEqualTo(1000); } /** * key存在,value不存在,defaultWhenParseFailed=false,应当抛出ConfigurationValueParseException异常 */ @Test(expected = ConfigurationValueParseException.class) public void get_int_with_defaultValue_and_without_value_and_defaultWhenParseFailed_is_false() { instance.setDefaultWhenParseFailed(false); instance.getInt("noneValue", 1000); } /** * key不存在,应当返回缺省值 */ @Test public void get_int_with_defaultValue_and_without_key() { assertThat(instance.getInt("noneKey", 1000)).isEqualTo(1000); } //more }
要获取一个整数值,咱们只须要这样调用:
int size = configuration.getInt("size");
若是想要在配置项不存在时返回缺省值,咱们只须要这样调用:
int size = configuration.getInt("size", 0);
读取配置信息是一个通用性的需求,几乎在每一个项目中都有这样的须要。抽象出Configuration类以后,咱们能够将它做为公司级通用类库,供给全部的项目引用,使得其余项目再也不须要编写重复的配置文件读取代码。
可是,直接使用通用的Configuration类,仍有不尽人意的方面。例如,我要读取Boolean类型的closed配置项,必须这样编写代码:
if (configuration.getBoolean("closed")) { ...... }
这样的语句仍然与个人领域语言隔了一层,是面向实现而不是面向意图的。我真正须要的是这样的代码:
if (configuration.isClosed()) { ...... }
closed再也不是一个由5个字母组成的字符串,而是领域语言isClosed()的一部分。
下面说明怎样设计这样的一个面向客户领域的、特化的Configuration类。
特化的AppConfiguration类采用客户领域的语言编写,将配置项中无心义的字符串key转化为方法名称:
package yang.yu.configuration; import java.util.Date; public class AppConfiguration { private static String confFile = "/conf.properties"; private static final String KEY_BIRTHDAY = "birthday"; private static final String KEY_SIZE = "size"; private static final String KEY_CLOSED = "closed"; private static final String KEY_LOCKED = "locked"; private static final String KEY_SALARY = "salary"; private static final String KEY_NAME = "name"; private static final double HIGH_SALARY_THRESHOLD = 10000; private Configuration configuration; public AppConfiguration() { this.configuration = Configuration.builder() .fromClasspath(confFile) .dateFormat("yyyy-MM-dd") .defaultWhenParseFailed(true) .build(); } public AppConfiguration(Configuration configuration) { this.configuration = configuration; } public Date birthday() { return configuration.getDate(KEY_BIRTHDAY); } public int size() { return configuration.getInt(KEY_SIZE); } public boolean isClosed() { return configuration.getBoolean(KEY_CLOSED); } public boolean isLocked() { return configuration.getBoolean(KEY_LOCKED); } public double salary() { return configuration.getDouble(KEY_SALARY); } public boolean isHighSalaryLevel() { return salary() >= HIGH_SALARY_THRESHOLD; } public String name() { return configuration.getString("name"); } }
客户端代码更加简单、直接,可读性更强:
package yang.yu.configuration; import org.junit.Before; import org.junit.Test; import java.util.Calendar; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.offset; public class AppConfigurationTest { private AppConfiguration instance; @Before public void setUp() throws Exception { instance = new AppConfiguration(); } @Test public void birthday() throws Exception { Date expected = DateUtils.createDate(2002, Calendar.MAY, 11); assertThat(instance.birthday()).isEqualTo(expected); } @Test public void size() throws Exception { assertThat(instance.size()).isEqualTo(15); } @Test public void isClosed() throws Exception { assertThat(instance.isClosed()).isTrue(); } @Test public void isLocked() throws Exception { assertThat(instance.isLocked()).isFalse(); } @Test public void salary() throws Exception { assertThat(instance.salary()).isCloseTo(12.5, offset(0.00001)); } @Test public void isHighSalaryLevel() throws Exception { assertThat(instance.isHighSalaryLevel()).isFalse(); } @Test public void name() throws Exception { assertThat(instance.name()).isEqualTo("张三"); } }
提供应用特定的配置类好处很明显:
1. 经过将配置项从字符串转换成方法名,减小了错误录入的可能
若是咱们这些读取配置:
boolean closed = configuration.getBoolean("closed");
若是不当心,很容易把字符串closed写错。要命的是,这个错误IDE和编译器都没法捕获,只有在运行时才会发现。
这样的配置:
boolean closed = configuration.isClosed();
就不太可能写错,即便写错了,IDE和编译器会替你发现。
2. 面向领域编程
在分析设计的时候,咱们应该经过在软件中采用问题域的词汇来做为软件类、方法、属性、变量的名称来缩小问题域和解决方案域之间的语义距离。
configuration.isClosed();
比起
configuration.getBoolean("closed");
更接近问题域的语言。
3. 支持衍生配置项
在AppConfiguration类中,我建立了一个衍生配置项:
public boolean isHighSalaryLevel() { return salary() >= HIGH_SALARY_THRESHOLD; }
该配置项并不直接存储于conf.properties中。
在这个项目中主要应用了两个设计技巧:
在这个项目中,咱们在两个地方基于不一样的目的应用了封装。
在本项目中,咱们同时提供了通用的配置类Configuration和专门领域的配置类AppConfiguration。Configuration是AppConfiguration的泛化,AppConfiguration是Configuration的特化。
Configuration的目标是重用,所以必须更通常化,能够提供任意多个设置项,提供更多的可配置内容(例如dateFormat, defaultWhenParseFailed等),以即可以灵活应用于多种不一样的环境;AppConfiguration的目标是紧密契合当前项目的须要,所以有一个固定的设置项集合,而且剔除了没必要要的灵活性(dateFormat直接设置为yyyy-MM-dd,defaultWhenParseFailed直接设置为true)。
最重要的是记住:
泛化以扩大外延——在大多数项目中获得重用; 特化以增长内涵——高度契合当前项目的须要。