2. Adding Features to the Config
In the previous step, we defined a basic configuration. However, real-world configurations are rarely that simple. They require default values, validation constraints, collections, and nested structures.
Let's expand our simple config into a more robust AdvancedConfig.
Building the Config
Here is our expanded interface:
package me.bristermitten.mittenlib.docs.tutorial;
import me.bristermitten.mittenlib.config.Config;
import me.bristermitten.mittenlib.config.Source;
import me.bristermitten.mittenlib.config.validation.Min;
import me.bristermitten.mittenlib.config.validation.NotBlank;
import java.util.List;
@Config(requireDynamicInitialization = false)
@Source("config.yml")
public interface AdvancedConfig {
@NotBlank
String host();
@Min(1)
default int port() {
return 3306;
}
default List<String> motd() {
return List.of("Welcome to the server!");
}
DatabaseConfig database();
@Config
interface DatabaseConfig {
String username();
@Nullable String password();
}
}
Let's break down the new features we added.
1. The @Source Annotation
@SourceThis annotation actually binds the config to a real file on the filesystem. When this annotation is present, MittenLib will automatically:
- Perform some extra validation on the config to ensure that a default config can be generated.
- Add a constant
me.bristermitten.mittenlib.config.Configurationfield to the implementation, storing the target file name. - Set up Guice bindings for the config
Automated Guice Features
The generated Guice bindings handle the following automatically:
- Create a default config file if it doesn't exist OR load a handwritten default from the JAR.
- Automatically read the config file and deserialize it into the config object.
- Live-reload the config file when it changes!
When a config has a @Source, MittenLib requires it to be dynamically initializable (meaning every field has a default or is @Nullable). This allows the library to create a default config file if it's missing. If it's not, MittenLib will throw a compile-time error by default.
If you don't want this, and instead want to hand-write the default, you can set requireDynamicInitialization = false, which disables this check. You MUST have a default config file present in your plugin's JAR if you take this approach, or an error will be thrown on startup.
For the sake of this example, we set requireDynamicInitialization = false because host and database do not have defaults.
2. Defaults and Nullable Fields
By default, all fields in a MittenLib configuration are required. If a user omits them from the config.yml, the application will fail to start.
There are two ways to disable this:
Nullable Fields
If you want to allow a field to be omitted from the config, you can mark it as @Nullable. We do this on the password() property in DatabaseConfig.
Defaults
We can also provide a default value for a field, which will be used if it's not present in the config.
- Interface Configs
- Class Configs
Default values can be defined with a default method, as demonstrated with the port() method.
If the user doesn't specify a port, the loader will automatically use the value returned by return 3306;.
This means we can use standard Java code to provide backup default values!
Classes also support default values with standard field initializers, e.g.:
@Config class DemoDTO {
int port = 3306;
}
Class default values are a little less powerful and more janky. The class must have a public no-arg constructor, and the default values have to be constant expressions. This is one of the reasons why interfaces are preferred.
2. Validation (@NotBlank, @Min)
MittenLib has built-in support for automatically validating your config.
@NotBlankensures thehoststring isn't empty or whitespace.@Min(1)ensures theportis a positive number.
If any of these constraints fail during loading, MittenLib will collect all the errors and present them in a user-friendly way.
3. Collections (List<String>)
MittenLib supports a subset of standard Java collections out of the box (List, and Map), which work in the way you'd expect.
4. Nested Configurations
As your configuration grows, it's helpful to organize it into subsections. You can do this by defining another @Config type (like DatabaseConfig above) and using it like normal. This also allows reusing the same type for multiple subsections.
Using other configurations as properties translates to a nested structure in the YAML/JSON file.
Example
The config class we've written handles files with this structure:
host:
port: 3306
motd:
- Welcome to the server!
database:
username:
password:
What's Next?
We now have a robust, validated, and feature-rich configuration definition. The final step is learning how to actually load this configuration into your application and use it.