Overview
Trugger is a framework that helps you write code that anyone can read. The idea is to provide a set of fluent interfaces to deal with operations related to reflection (such as creating proxies, reflecting members, instantiating objects).
Trugger is intended to be a base for creating infrastructure code. While it is not an IoC container, for example, it could be a part of the core of an IoC container.
Trugger is developed using Open Source Licenses of:
What a hell does the name trugger mean?
When I was learning java, I choosed "Atatec" (Ataxexe Technology) to be the name of my fictitious company and then I sent a message to my friend Ives:
Atatec
And the response was:
trugger
When I asked him why he came up with this, he said:
Isn't that the name of that special kick in Street Fighter?
The special kick mentioned is the Tatsumaki Senpuu Kyaku and Ken says something like "atatec trugger" (at least in Portuguese) when he does the kick. So the name Trugger became my first choice when I start to write this framework.
How To Use
Just put the jar file on your classpath and you're done. No dependencies are required by Trugger at runtime. If you prefer some build system like Gradle or Maven, trugger is in now in Maven Central repository:
groupId: io.backpackcloud
artifactId: trugger
How To Build
Just make sure you have Maven and execute the mvn install
command.
How To Contribute
Just fork the project, do some stuff and send me a pull request. You can also fire an issue or tell your friends to use Trugger.
Reflection
The Reflection module was the first one to show up in Trugger, it allows simple and much less verbose use of the Reflection API to reflect constructors, methods, fields and generic type declarations.
The base class of this module is Reflection
and you will find a nice fluent interface there with the methods reflect
, invoke
and handle
.
Fields
Single
Reflecting a field is done with the code reflect().field(field name)
. You can also apply a filter by using the method filter
and the target class (or object) by using the method from
. If you need a recursive search through the entire target class hierarchy, just use the method deep
.
Here is some examples (assuming a static import
):
reflect().field("value").from(String.class);
reflect().field("name").deep().from(MyClass.class);
reflect().field("id")
.filter(field -> field.getType().equals(Long.class))
.from(someInstance);
The filter
method receive a java.util.function.Predicate
to stay in touch with Java 8. Also, the Field
is wrapped in a ReflectedField
class to help further operations.
Multiple
If you need to reflect a set of fields, use the reflect().fields()
. The same features of a single reflect are available here.
reflect().fields()
.filter(field -> field.getType().equals(String.class))
.deep()
.from(MyClass.class);
Predicates
There are some builtin predicates in the class FieldPredicates
.
reflect().fields()
.filter(ofType(String.class)) // a static import
.deep()
.in(MyClass.class);
Handling Values
A field can have its value manipulated through the method Reflection#handle
. It will create a handler to set and get the field's value without the verbosity of the Reflection API. To handle static fields, you can call the handler methods directly:
String value = handle(field).getValue();
handle(field).setValue("new value");
For instance fields, just specify an instance using the method on
:
String value = handle(field).on(instance).getValue();
handle(field).on(instance).setValue("new value");
Constructors
Single
To reflect a constructor, use reflect().constructor()
and specify the parameter types and optionally a filter. If the class has only one constructor, it can be reflected without supplying the parameter types.
reflect().constructor()
.withParameters(String.class)
.from(MyClass.class);
reflect().constructor()
.withoutParameters()
.from(MyClass.class);
reflect().constructor()
.filter(c -> c.isAnnotationPresent(SomeAnnotation.class))
.withParameters(String.class)
.from(MyClass.class);
reflect().constructor().from(MyClass.class);
Multiple
A set of constructors can be reflected by using reflect().constructors()
. As in fields, the features in single reflection are present in multiple reflection.
reflect().constructors().from(MyClass.class);
reflect().constructors()
.filter(c -> c.getParameterCount() == 2)
.from(MyClass.class);
Note that in multiple selection you cannot specify the parameter types directly in the fluent interface (for obvious reasons).
Predicates
A few useful predicates are included in the class ConstructorPredicates
.
reflect().constructors()
.filter(annotated()) // a static import
.from(MyClass.class);
Methods
Single
To reflect a single method, just pass it name to Reflection#method
. Filtering is allowed and you can specify parameter types too.
reflect().method("toString").from(Object.class);
reflect().method("remove")
.withParameters(Object.class, Object.class)
.from(Map.class);
reflect().method("foo")
.filter(method -> method.isAnnotationPresent(PostConstruct.class))
.from(instance);
As in field reflection, you can do a deep search with deep
.
reflect().method("toString").deep().from(MyClass.class);
Multiple
A set of methods can be reflected by using Reflection#methods
:
reflect().methods().from(Object.class);
Deep search and filtering are also supported:
reflect().methods()
.filter(method -> method.isAnnotationPresent(PostConstruct.class)
.deep()
.from(MyClass.class);
Predicates
A set of predicates to deal with methods is in MethodPredicates
:
reflect().methods()
.filter(annotatedWith(PostConstruct.class)) // a static import
.deep()
.from(MyClass.class);
Generic Type
Generic declarations in a class are present in the bytecode. Trugger can reflect them by using the method genericType
. Suppose we have this interface:
public interface Repository<T> {
// ... some useful methods here
}
A generic base implementation can use that T:
public class BaseRepository<T> {
private final Class<T> type;
protected BaseRepository() {
this.type = reflect().genericType("T").of(this);
}
}
The constructor was declared protected
to warn that this will only work for subclasses (it is a Java limitation). A workaround to use this trick in a variable-like way is by declaring an anonymous class:
Repository<MyType> repo = new BaseRepository<MyType>(){};
This is an ugly solution, but works. You can also use a subclass as well, it's not too ugly, but can lead to a tons of subclasses in your application.
Proxy Creation
A proxy object is created using a interception handler and optionally a fail handler. The DSL exposed starts at Interception
with the method intercept
and lets you define one or more interfaces to intercept. Additionally, you can set a target and its interfaces will be used. After the behaviour specification, use the method proxy
to create the proxy instance.
SomeInterface proxy = Interception.intercept(SomeInterface.class)
.onCall(context -> logger.info("method intercepted: " + context.method())
.proxy();
proxy.doSomething();
The interception logic happens in the handler passed through the method onCall
. The handler receives a context, which contains all information about the intercepted method. A fail handler can also be set using the method onFail
.
SomeInterface proxy = Interception.intercept(SomeInterface.class)
// sets a target to delegate the call using the context object
.on(instance)
// delegates the call to the target (this is the default behaviour)
.onCall(context -> context.invoke())
// handles any error occurred
.onFail((context, throwable) -> handleTheFail(throwable))
.proxy();
proxy.doSomething();
The fail handler has access to the context so you can delegate the method to the target again (if a timeout occurs, for example).
The Interception Context
The interception context holds everything related to the method interception, included:
- The arguments passed
- The method intercepted
- The declared method intercepted in the target instance
- The proxy instance
- The target instance (may be null if not specified when creating the proxy)
The context can be used to delegate the method call to the target (using invoke
) or to another instance (using invokeOn
). The declared method intercepted can be retrieve by using targetMethod
.
Elements of an Object
What is an Element?
An element is any value that an object holds. It may be accessible through a field, invoking a method (a getter or a setter) or even a specific way like the Map#get
method.
A basic element in Trugger is a Property or a Field. Trugger tries to find a getter and a setter method for the element name and a field with the same name. This allows manipulate private fields and properties in the same way without bothering you with the way of handling the value.
Obtaining an Element
An element is obtained using the method element
in Elements
. The same features of a field reflection is here with the addition of getting an element without specifying a name. A set of predicates are present in ElementPredicates
.
element("value").from(MyClass.class);
element()
.filter(annotatedWith(Id.class)) // static import
.from(MyClass.class);
elements()
.filter(type(String.class) // static import
.from(MyClass.class);
Copying Elements
The elements of an object can be copied to another object, even if they are from different types. The DSL starts at the method copy
:
copy().from(object).to(anotherObject);
This will copy every element. To restrict the copy to non null values, use the notNull
method:
copy().from(object).notNull().to(anotherObject);
You can also apply a function to transform the values before assigning them to the target object (useful when copying values to a different type of object).
copy().from(object)
.map(copy -> copy.value().toString())
.to(anotherObject);
To filter the elements to copy, just give a selector to the copy
method:
copy(elements().filter(annotatedWith(MyAnnotation.class)))
.from(object)
.to(anotherObject);
Or filter the copy directly using filter
copy(elements().filter(annotatedWith(MyAnnotation.class)))
.from(object)
.filter(copy -> copy.dest().isAnnotationPresent(MyAnnotation.class))
.to(anotherObject);
Nested Elements
Nested elements are supported using a "." to separate the elements:
element("address.street").from(Customer.class);
You can use any level of nesting:
element("customer.address.street").from(Response.class);
Custom Elements
Some classes have a custom definition of elements. A Map
has their keys as elements, an Array
has their indexes as elements an so on. Elements are found by an element finder (a class that implements Finder<Element>
) and you can write a custom element finder and register it using the registry available through the method Elements#registry
.
Trugger has custom element finders for a set of java core classes:
Map
: keys are used as the elementsResourceBundle
: keys are used as the elementsProperties
: keys are used as the elementsResultSet
: the column names are used as the elementsAnnotation
: the methods as used as elementsList
: indexes are used as the elements (and also two special names, first and last)Arrays
: indexes are used as the elements (and also two special names, first and last)
It is important to have clear that since this elements are instance specific, the elements should be queried by passing an instance instead of a class for the method from
or an empty list will be returned. For a single elements, you may pass a class or an instance but using an instance is better because you can call the handling methods directly.
You can also use this custom element finders to copy elements easily:
// this will copy every element from the result set to the instance
copy().from(resultSet).to(myEntity);
Utilities
Context Factories
If you need a lightweight component to invoke a constructor with a predicate based logic to resolve the parameter values, you can use the ContextFactory
.
A ContextFactory
is a factory that maps a predicate that evaluates parameters to an object or supplier. After creating a ContextFactory
, you can manipulate the context through the #context
method and create an object with the create
method. A set of predicates can be found in ParameterPredicates
class.
ContextFactory factory = new ContextFactory();
factory.context()
//static imports
.use(myImplementation)
.when(type(MyInterface.class))
.use(someObject)
.when(ofName("component"))
.use(parameter ->
resolve(parameter.getAnnotation(MyAnnotation.class)))
.when(annotatedWith(MyAnnotation.class))
.use(() -> availableWorker())
.when(ofType(MyWorker.class));
The above factory will:
- use
myImplementation
for any parameter of the typeMyInterface
- use
someObject
for any parameter named "component" - use the return of
resolve
with the annotationMyAnnotation
for any parameter annotated withMyAnnotation
- use the return of
availableWorker
to any parameter of typeMyWorker
These steps will be done with every public constructor of a type, if a constructor has one parameter that cannot be resolved to an object, then the next constructor will be used and if there is no more constructors to use, an exception is thrown.
Component Factories
Component factories allows creating components defined by annotations. Suppose you have:
public @interface ComponentClass {
Class<? extends Component> value();
}
@ComponentClass(MyComponentImplementation.class)
public @interface MyComponent {
String name();
}
//inside a class
@MyComponent(name = "myName")
private String aField;
The annotation in aField
can be used to create an instance of MyComponentImplementation
. The context used to create any components are:
- Every property of the annotation with their specific types (in that case, the property
name
with the value "myName" to a parameter namedname
and of typeString
) - The annotation itself with its type (in that case, the
MyComponent
annotation to the typeMyComponent
)
Since the annotation is used as the context, you can have a constructor in the component implementation that receives the annotation instead of its properties. This is useful if you don't want to compile your code with -parameters
parameter.
This behaviour is completely replaceable by using the method configureContextWith
. To add behaviour to the default one, compose the ComponentFactory#defaults
with your behaviour:
factory.configureContextWith(
defaults().andThen(
(context, annotation) -> yourConfigurations
)
);
To instantiate a component, just use a code like this one:
ComponentFactory<ComponentClass, Component> factory =
new ComponentFactory(ComponentClass.class);
// get the annotation from the field
Component component = factory.create(annotation);
Alternatively, you can get a list of components by passing an AnnotatedElement
to the method #createAll
:
Element = Elements.element("aField").from(myObject).orElseThrow(MyException::new);
List<Component> components = factory.createAll(element);
Or creating a single one by passing an AnnotatedElement
to the method #create
:
Element = Elements.element("aField").from(myObject).orElseThrow(MyException::new);
Component component = factory.create(element);