AndHow Properties

90% of using AndHow is creating and using Properties

AndHow Properties are immutable constants in your code, except their value is loaded when your application starts up. They are always static final and created with a builder() method:

private static final IntProp MY_CONST_INT = IntProp.builder().build();

Properties may be private or public, but they must be static final and the compiler will enforce that. Getting the value of a Property is simple:

Integer theValue = MY_CONST_INT.getValue();

Properties are strongly typed (so IntProp returns an Integer) and there are Property types for most primitive types - see the next section.

Properties can...

  • be declared anywhere in code that a static final variable can be declared. They do not need to be centralized into a configuration class. Best pactice is to declare them where you use them.

  • have constraints such as greaterThan(5) (for a numeric type) or matches("regex expression") (for a String), etc.

  • have configuration values loaded into them during application startup from a number of configuration sources - see the Loaders.

  • be null if no value if found for the property at startup, or may have a default value

Property Types

AndHow has Properties to represent most common value types:

All Property types basically behave the same way:

  • They parse the configured values they receive during application startup into the appropriate type.

  • If no configuration value is found, their value is null unless a default is specified.

  • If the value cannot be parsed to the appropriate type or does not meet the validation requirements defined in the Property declaration, the application startup is stopped with a runtime exception.

One exception to this general behavior is the FlagProp. The FlagProp behaves similar to an on/off switch, thus it is never null and always returns true or false from getValue(). Additionally, the FlagProp behaves like a 'nix flag when used on the command line - see the example in the StdMainStringArgsLoader.

Property value access and security

Property.getValue() is the only way to get a Property's value (other than exports), so the visibility of the Property controls access:

  • If code can see/access a Property, it can read its value

  • If code cannot see/access a Property, it cannot read its value

Reflection can bypass visibility (except non-open modules in JDK 9+) and code can read environment vars and other sources, but many other configuration solutions result in all configuration being effectively public. AndHow Properties let you scope configuration to the classes that need it.

Properties and their values are immutable

During initialization, AndHow will automatically load Property values from multiple configuration sources. Once complete, Property values will not change for the run-life of the application.

Property Names

Properties always have a unique canonical name based on their logical path. A Property named MY_PROP declared in the com.bigcorp.MyClass has the canonical name com.bigcorp.MyClass.MY_PROP. The same pattern continues with nested inner classes or interfaces.

Property names are case-insensitive, so com.bigcorp.MyClass.MY_PROP is the same as COM.BiGcOrP.MyClaSS.my_prOP.

Best Practice: Don't worry about Property names unless you have to

Canonical Properties names are often good enough and will implicitly update when refactoring.

Properties may have In and Out aliases. In aliases are recognized when loading values, in addition to canonical names. Out aliases are alternate names that can be used when exporting. Properties may have multiple In and Out aliases:

LngProp CODE = LngProp.builder().aliasIn("pin")
	.aliasOut("secret").aliasInAndOut("secret_pin").build();

When AndHow loads values, CODE's canonical name, pin and secret_pin will be recognized. If values are exported (such as to a Map to configure another framework), the canonical name, secret and secret_pin will all be names that could be used.

In aliases are useful for Properties that need to be specified from command line, or to match an already existing set of configuration files / sources. Out aliases are useful for exporting to other frameworks or legacy applications that expect specific key names.

Default Values

All Properties can have default values:

IntProp RETRY_CNT = IntProp.builder().defaultValue(1).build();

...But be careful! Its easy for a default value to end up in production. Some Properties have good defaults: report margin, retry counts or log level. Others do not: DB connection string, user name or password. If a Property has no value that is acceptable in all environments, its better to not specify a default and rely on configuration to supply the value.

Don't use a default value for local workstation or test environment configuration values.

As you will see in the testing section, its easy to use separate test configurations. Its also easy to provide local and tier specific configuration (TDB: Write this section).

Properties have lots of configuration options

Properties can have validation, description, defaults, and more. Rather than attempt to describe them all, see the examples below.

Properties behave like static finals except...

AndHow Properties work like static final variables whose value is assigned at startup. This is true in amost all situations except one: A Property's getValue() method cannot be called inside a static initializer block. Doing so will cause a startup error that AndHow will tell you about.

Groups

A Group is just the AndHow term for the class or interface containing Properties. Some AndHow operations and annotations apply to Groups, as you will see in the examples below.

Property Example 1

package org.example;

import org.yarnandtail.andhow.GroupInfo;
import org.yarnandtail.andhow.property.*;

public class TransactionManager {

	@GroupInfo(name="Connection Config", 
		desc = "Config's an http service connection")
	interface Connection {
		StrProp BASE_URL = StrProp.builder().notNull()
			.aliasIn("url").startsWith("http://").endsWith("/")
			.desc("Base url for a service request").build();
		IntProp RETRY_CNT = IntProp.builder().defaultValue(1)
			.aliasIn("retry").aliasIn("retry_cnt")
			.greaterThanOrEqualTo(0).lessThan(10)
			.desc("# of request retries. 0 = no retry.").build();
	}

}

Properties and Groups can have description to help make code self-documenting. AndHow uses all the Property metadata to generate rich configuration templates for your Properties, as in the 2nd tab. The template serves as both documentation and an initial configuration file, and is created when startup fails due to validation error, or it can be run manually by setting the built-in org.yarnandtail.andhow.Options.CREATE_SAMPLES to true, e.g.:

java -DAHForceCreateSamples MyMainClass

-D sets a Java system property and AHForceCreateSamples is an In alias for CREATE_SAMPLES, as you can see at the bottom of the configuration template. It is also a 'flag' property (FlagProp), so just being present is enough to set it true, similar to other command line switches.

The 3rd tab shows example error messages for invalid configuration values. These informative messages would be part of a RuntimeException, thrown to prevent startup with invalid configuration.

Property Example 2

package org.example;

import org.yarnandtail.andhow.GroupInfo;
import org.yarnandtail.andhow.property.*;

import java.math.BigDecimal;
import java.time.LocalDateTime;

public class ReportGenerator {

	@GroupInfo(name="Record filter", desc="Filters are AND'ed together")
	private interface Filter {
		StrProp REGION = StrProp.builder()
			.oneOfIgnoringCase("EAST", "WEST").build();
		StrProp ZIP = StrProp.builder().matches("\\d{5}(\\-\\d{4})?")
			.desc("Zipcode w optional plus 4 (12345 or 12345-1234)").build();
		LocalDateTimeProp START_TIME = LocalDateTimeProp.builder()
			.defaultValue(LocalDateTime.parse("2010-01-01T00:00"))
			.desc("Include records after this date-time").build();
		BigDecProp MIN_SALE = BigDecProp.builder()
			.defaultValue(BigDecimal.TEN).greaterThanOrEqualTo(BigDecimal.ZERO)
			.desc("Min sale amount to include").build();
	}

	private interface Format {
		DblProp MARGIN = DblProp.builder().defaultValue(1d)
			.greaterThan(.25d).desc("Margin in inches").build();
		BolProp WITH_HEADERS = BolProp.builder().defaultValue(true).build();
	}
}

Just like constants, Properties can (and should) be declared where they are be used to create natural scope: If a secret is only needed by one class, don't make it visible to the entire application. Avoid placing Properties in a central 'Config' class.

Best Practice: Declare properties in the class or interface where they are used

Place related sets of Properties in nested interfaces.

The Properties in the example above configure a Report class, and related Properties have been nested into interfaces. This creates logical, canonical names for Properties: The purpose of ZIP is easy to understand when it's inside Report.Filter.

Nesting into interfaces also takes advantage of the Java language to save some typing: Variables declared in an interface are implicitly static final. (Note: Java 11+ is required to use a private interface as in the example)

Last updated