Verano provides convenient way to wire up dependencies without using reflections, type castings, annotations or component scan. It takes different approach to classic DI containers and relies on wiring dependencies through a main entry point of an application. To simplify/enhance this wiring process it provides a set of objects through user can declare dependencies and conditions.
Core features:
Every part of the framework is open for extension and modification due to its OOP nature. It was built using objects only. There is no single if/for/while/throw/catch or similar statement present in the codebase.
Let's create a very simple model which prints items in an order.
public class MyOrder implements Order {
private final Items items;
public UserOrder(Items items) {
this.items = items;
}
@Override
public void showItem(String id) {
this.items.printItem(id);
}
}
public class RealItems implements Items {
@Override
public void printItem(String id) {
System.out.println(String.format("Real item %s", id));
}
}
public class TestItems implements Items {
@Override
public void printItem(String id) {
System.out.println(String.format("Test item %s", id));
}
}
We have two implementations of Items
, one for test environment and one
for production. In order to inject the right implementation into MyOrder
we
will create ItemsComponent
and OrderComponent
using Verano's component
system.
public class ItemsComponent extends VrComponent<Items> {
public ItemsComponent(AppContext context) {
super(context,
new VrInstance<>(
() -> new RealItems(),
new ProfileWire("prod")
),
new VrInstance<>(
() -> new TestItems(),
new ProfileWire("test")
)
);
}
}
public class OrderComponent extends VrComponent<Order> {
public OrderComponent(AppContext context) {
super(context,
new VrInstance<>(
() -> new MyOrder(new ItemsComponent(context).instance())
)
);
}
}
Finally we hook things up int the main class and start the application with argument --profile=prod or --profile=test.
public class Main {
public static void main(String[] args) throws Exception {
AppContext context = new VrAppContext(args);
Order order = new OrderComponent(context).instance();
order.showItem("123");
}
}
OTUPUT:
"Real item 123" => profile=prod
"Test item 123" => profile=test
Note that OrderComponent
does not need to know how to construct Items
it just uses ItemsComponent
and let that component build it.
You can use following pom.xml template for running verano:
<project>
<dependencies>
<dependency>
<groupId>hr.com.vgv</groupId>
<artifactId>verano</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>YourMainClass</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin-->
</plugins>
</build>
</project>
Component is a base building block for managing instances. It acts as a factory class that determines which implementation is suitable for wiring based on users input. The difference between classic factory class is that it is not globally accessible and the logic of choosing the right instance is hidden from users.
Verano provides two types of components, VrComponent
which manages all its
instances like singletons and VrRefreshableComponent
for which user can
control instance lifecycle.
In order to create a component simply extend VrComponent
and provide desired
implementations.
public class ItemsComponent extends VrComponent<Items> {
public ItemsComponent(AppContext context) {
super(context,
new VrInstance<>(
() -> new MyItems()
)
);
}
}
public class Main {
public static void main(String[] args) throws Exception {
AppContext context = new VrAppContext(args);
Items items = new ItemsComponent(context).instance();
}
}
Calling method instance
will return singleton instance.
In order to gain direct control of an instance lifecycle extend VrRefreshableComponent
and define instances using VrCloseableInstance
:
public class ItemsComponent extends VrRefreshableComponent<Items> {
public ItemsComponent(AppContext context) {
super(context,
new VrCloseableInstance<>(
() -> new MyItems() // implements Closeable
)
);
}
}
public class Main {
public static void main(String[] args) throws Exception {
AppContext context = new VrAppContext(args);
VrRefreshableComponent<Items> component = new ItemsComponent(context);
Items items = component.instance();
Items refreshed = component.refreshed(); // creates new MyItems instance
// and closes previous one
}
}
For wiring instances conditionally, Verano provides you ProfileWire
and QualifierWire
.
ProfileWire
condition wiring based on profile set through command line interface
via argument --profile=${profile}
.
With QualifierWire
user can specify, by a name, which instance will be used.
Lets observe the following example using both of those wires:
public class ItemsComponent extends VrComponent<Items> {
public ItemsComponent(AppContext context) {
super(context,
new VrInstance<>(
() -> new RealItems(),
new ProfileWire("prod"),
new QualifierWire("realItems")
),
new VrInstance<>(
() -> new TestItems(),
new ProfileWire("test"),
new QualifierWire("testItems")
)
);
}
}
public class Main {
public static void main(String[] args) throws Exception {
AppContext context = new VrAppContext(args);
VrComponent<Items> component = new ItemsComponent(context);
Items items = component.instance(); // Returns instance based on profile
// specified in command line
Items testItems = component.with(new QualifierWire("test")).instance();
// Returns TestItems instance
}
}
If we run this application with parameter --profile=prod the first instance
retrieved will be RealItems
and the second TestItems
.
We can specify qualifiers for each class through qualifiers.xml
file.
Let's create the file in resource folder with the following content:
<?xml version="1.0" encoding="UTF-8"?>
<classes>
<class name="com.example.ItemsComponent">
<qualifier>realItems</qualifier>
</class>
</classes>
public class Main {
public static void main(String[] args) throws Exception {
AppContext context = new VrAppContext(args);
Items items = new ItemsComponent(context).instance(); // RealItems instance
}
}
You can externalise configuration property files and make them available when
specific profile is set. This functionality is very similar to Spring profiles.
Base configuration should be stored in a resource folder as app.properties
file and the rest of the property files should follow app-${profile}.properties
convention.
Verano will read property file that matches current active profile and use
app.properties as baseline.
Suppose we have to property files app.properties
and app-test.properties
.
They both have property db.url
defined with different values.
This is how we could fetch that property:
public class Main {
public static void main(String[] args) throws Exception {
AppContext context = new VrAppContext(args);
Props props = new AppPropsOf(context);
System.out.println("DB url: " + props.value("db.url");
}
}
If we run the app with argument --profile=test
the value from app-test.properties
is
printed. If we omit the profile argument than default value from app.properties
will be
printed.
All components in the system can be reinitialised when some of configuration files are changed.
In order to trigger this functionallity we need to specify it in ApplicationContext
.
VrAppContext
is consisted of application properties, command line porperties and and properties
connected with qualifiers.
public class Main {
public static void main(String[] args) throws Exception {
AppContext context = new VrAppContext(
new MapEntry<>("app", new RefreshableProps(new AppProps(args), "pathToAppProperties")),
new MapEntry<>("cli", new CliProps(args)),
new MapEntry<>("qualifiers", new RefreshableProps(new QualifiersProps(), "pathToQualifiersXml"))
);
}
}
Once application properties or qualifiers.xml are changed, components will be reinitialised and
next method call instance
on a component will result in a new instance creation using refreshed properties.
Following the previous example we can swap implementations at runtime by modifying qualifiers.xml file and specifying different qualifier for a component to use.