Pixie is a tiny 100k jar that handles configuration, dependency injection and events. It can handle any scenario where you would use reflection to instantiate a Java object.
<dependency>
<groupId>org.tomitribe.pixie</groupId>
<artifactId>pixie</artifactId>
<version>2.0</version>
</dependency>
Let’s imagine we want to use Pixie to configure and build two classes we’ve created called Person
and Address
.
Our first step is to annotate the constructor parameters with Pixie annotations. Pixie will map the configuration to our constructor based on the names we use.
import org.tomitribe.pixie.Component;
import org.tomitribe.pixie.Default;
import org.tomitribe.pixie.Name;
import org.tomitribe.pixie.Param;
public class Person {
private final String name;
private final Integer age;
private final Address address;
public Person(@Name final String name,
@Param("age") final Integer age,
@Component("address") final Address address) {
this.name = name;
this.age = age;
this.address = address;
}
//...
}
public class Address {
private final String street;
private final String city;
private final State state;
private final int zipcode;
private final String country;
public Address(@Param("street") final String street,
@Param("city") final String city,
@Param("state") final State state,
@Param("zipcode") final int zipcode,
@Param("country") @Default("USA") final String country) {
this.street = street;
this.city = city;
this.state = state;
this.zipcode = zipcode;
this.country = country;
}
//...
}
public enum State {
WI, MN, CA;
}
We can use the Pixie System
to configure and build the above objects via either a Properties file defintion or in code via a Builder API
Our properties file might look like so. The file name and location can be anything as it’s our
job to read this into a java.util.Properties
instance.
jon = new://org.example.Person
jon.age = 46
jon.address = @home
home = new://org.example.Address
home.street = 823 Roosevelt Street
home.city = River Falls
home.state = WI
home.zipcode = 54022
home.country = USA
The following code would build a Pixie System
via a java.util.Properties
instance and lookup
the Person
instance by type.
import java.util.Properties;
import org.example.Person;
import org.tomitribe.pixie.System;
//...
public static void main(String[]) {
final Properties properties = new Properties();
properties.load(...); // read the properties file
final System system = new System(properties);
final Person person = system.get(Person.class);
assertEquals("jane", person.getName();
final Address address = person.getAddress();
assertNotNull(address);
assertEquals("820 Roosevelt Street", address.getStreet());
}
Alternatively, we can skip the properties and build our System
in code fluently.
import java.util.Properties;
import org.example.Person;
import org.tomitribe.pixie.System;
//...
public static void main(String[]) {
final System system = System.builder()
.definition(Person.class, "jane")
.param("age", 37)
.comp("address", "home")
.definition(Address.class, "home")
.param("street", "820 Roosevelt Street")
.param("city", "River Falls")
.param("state", "WI")
.param("zipcode", "54022")
.build();
final Person person = system.get(Person.class);
//...
}
Pixie supports constructor injection. Each parameter the constructor Pixie will use must be annotated with either @Param
, @Component
, @Event
or @Name
.
Annotation | Purpose | Example Usage |
---|---|---|
|
Maps a constructor parameter to a config property |
|
|
Provides a default value if the property is missing |
|
|
Injects a dependent object built by or given to Pixie |
|
|
Allows a property to be |
|
|
Injects the component’s name from the config |
|
|
Injects a |
|
|
Marks a method as an event listener |
|
All the above annotations are in the org.tomitribe.pixie
package.
Purpose: Binds a constructor parameter to a configuration property.
Usage: Pixie will automatically inject values from a properties file or the builder API.
public final class User {
private final String username;
private final int age;
public User(@Param("username") final String username, @Param("age") final int age) {
this.username = username;
this.age = age;
}
}
Maps to a properties file entry:
user=new://org.example.User
user.username=alice
user.age=30
Any Java type that can be created from a String
is supported. Pixie will inspect the java class and look for one of the following:
-
Public constructor with a single parameter of type
String
-
Public static method with a single parameter of type
String
returning an instance of the type
Purpose: Specifies a default value for a constructor parameter if it is not set in the configuration.
public final class Address {
private final String country;
public Address(@Param("country") @Default("USA") final String country) {
this.country = country;
}
}
If country
is missing from the config, "USA"
is used. Applies to both @Param
and @Component
. When used on @Component
it implies the name of the component that should be injected.
Purpose: Indicates that a constructor parameter should be injected as a component dependency.
public final class ShoppingCart {
private final PaymentProcessor paymentProcessor;
public ShoppingCart(@Component("processor") final PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
}
Pixie can resolve this reference by name or by type.
cart=new://org.example.ShoppingCart
cart.processor=@stripe
With the above configuration Pixie will look in the System
for an object with the
name stripe
and inject it as the value of the processor
when constructing the ShoppingCart
.
A ConstructionFailedException
will be thrown if no object with that name is found or if the object found is of the wrong type.
cart=new://org.example.ShoppingCart
In the above configuration the processor
name has not be specified. In this situation, Pixie will
look in the System
for any object with the the type PaymentProcessor
and inject it as the value of the processor
when constructing the ShoppingCart
.
If there are multiple instances of PaymentProcessor
they will be sorted in descending order by name and the first will be picked.
A ConstructionFailedException
will be thrown if no objects with that type are found.
The @Component
annotation can be used to resolve components which are added directly to the Pixie System
.
jane=new://org.example.Person\n" +
jane.age = 37
jane.address=@home
In the above properties, the Person object has a @Component
reference to Address
called home
which is not defined.
The home
instance can be added directly to the Pixie System
before we load the properties.
final Properties properties = //...
final System system = new System();
system.add("home", new Address("820 Roosevelt Street","River Falls", State.WI, 54022, "USA"));
system.load(properties);
final Person person = system.get(Person.class);
assertNotNull(person.getAddress());
In the above code we’ve directly created the Address
instance and handed it to Pixie System
with the name home
.
Purpose: Marks a constructor parameter as optional (can be null
if not configured).
public final class Notification {
private final String message;
private final String footer;
public Notification(@Param("message") final String message, @Nullable @Param("footer") final String footer) {
this.message = message;
this.footer = footer;
}
}
If footer
is missing from the config, it will be null
instead of throwing an error.
Purpose: Injects the component’s name from the configuration.
public final class Service {
private final String serviceName;
public Service(@Name final String serviceName) {
this.serviceName = serviceName;
}
}
If configured as myService = new://com.foo.Service
, the constructor will receive "myService"
.
Purpose: Injects an event consumer (Consumer<T>
) into a component so it can fire events.
public final class OrderService {
private final Consumer<OrderPlaced> orderPlacedEvent;
public OrderService(@Event final Consumer<OrderPlaced> orderPlacedEvent) {
this.orderPlacedEvent = orderPlacedEvent;
}
public void placeOrder(final String orderId) {
orderPlacedEvent.accept(new OrderPlaced(orderId));
}
}
Pixie will inject a Consumer<OrderPlaced>
that calls System.fire(event)
which will
invoke all observer methods in all components in the System
.
Purpose: Marks a method as an event listener.
public final class OrderListener {
public void onOrderPlaced(@Observes final OrderPlaced event) {
System.out.println("Order placed: " + event.getOrderId());
}
}
When OrderPlaced
is fired, this method will be called automatically.
It is possible to listen for events by any assignable type, even java.lang.Object
public final class EverythingListener {
public void onEvent(@Observes final Object event) {
System.out.println("Event observed: " + event);
}
}
Pixie provides strict validation to ensure configuration correctness and prevent common issues with properties files.
All properties in Pixie are case insensitive, meaning users will not encounter failures due to incorrect capitalization. For example, the following entries are treated as equivalent:
user.name=Alice
User.Name=Alice
USER.NAME=Alice
Regardless of how the property is written, it will be correctly matched and retrieved.
Pixie enforces strict validation of configuration properties to prevent misconfigurations:
-
If a property is specified in the configuration file but does not exist in the corresponding class, Pixie will throw an exception at startup.
-
This ensures that typos or removed properties do not lead to silent failures.
For example, given the following properties file:
app.mode=production
app.timeout=5000
If the app.timeout
property is removed from the Java class but remains in the configuration file, Pixie will fail fast with an error, preventing users from relying on "dead" properties.