Skip to content

Uniform Factory implements an Adapter pattern based on Reflection information. Normally, it generates adapters based on annotations. For each target class, it implements the common wrapper interface.

License

antkudruk/uniformfactory

Repository files navigation

Library for Generating Adapters Based On Annotations in Java

UniformFactory is a Java library to generate adapters (wrappers) based on reflection. Adapters are classes implementing a common interface to manipulate different classes the common way.

In the Uniform Factory library, we're using the term wrapper to denote adapter class.

Why do you Need to Generate Adapters in Java?

Supposing you define some annotations to mark arbitrary class members with. The classes may have different structure. You even may be not aware of the classes structure if you publish your functionality as a framework to share it with other developers. The task is just to process the annotated class members.

The common way of doing that is to look through annotated class members each time you use them, find them and process. However, that approach is too cumbersome. It's slow as well because reflective operations are too expensive.

You'd like to process the annotated members more convenient way.

Uniform Factory allows you just to define a common interface for the classes. We'll call the interface wrapper interface. Then you let Uniform Factory automatically generate an implementation of that interface for each origin class. Then you can access annotated members using the common wrapper interface regardless of a class structure.

Uniform Factory works at the bytecode level. Thus, it's significantly faster than iteration over class members to look for the annotated members.

Let's consider the following example.

You defined @Label annotation to mark a member to identify an object. And you defined @Propery annotation to mark an objects named properties. You don't know the origin classes structure. One of the origin classes may look like in the following listing:

@Marker
class OriginImpl {
    @Label
    private String name;
    
    @Property('width')
    private long width;
    
    @Property('height')
    public long getHeight() {
        // ...
    }
}

First, you should define the following interfaces to generate wrapper implementations from:

  • Wrapper interface. It's a common interface to generate the wrapper classes from. A wrapper class is generated for each Origin class.
  • Functional interfaces. As soon as in this example you define an annotation to apply to multiple class members annotated with the same annotation, you have to define a functional interface to access each member.
// Your wrapper interface
interface Wrapper {
    String getName();
    Map<String, Property> getProperties();
}

// Your functional interface.
interface Property {
    Object get();
}

Second, you should perform some settings of Uniform Factory. On this step, you should decide how you're doing to use UniformFactory and set it up. You can find some explanations with examples further.

After applying Uniform Factory, you can operate the annotated members the following way.

  1. Get the wrapper for the origin object. Depending on whe way you're using UniformFactory, il may look the following:

    • Way 1: Using Maven/Gradle plugin:
    Wrapper wrapper = ((Origin)origin).getWrapper();// get the wrapper built with UniformFactory
    
    • Way 2: Using factory (doesn't require Maven/Gradle plugin)
    Wrapper wrapper = wrapperFactory.get(origin);// get the wrapper built with UniformFactory
    
  2. Operate with the wrapper

    wrapper.getName();
    Property widthProperty = wrapper.getProperties().get("width");
    widthProperty.get();
    

Setting up ClassFactory

An instance of ClassFactory allows you to create adapters for a needed type.

Ways to Use Uniform Factory

You can use Uniform Factory two ways:

  • As a Maven/Gradle plugin
  • Using an object factory. This way doesn't require any plugin
Using Maven/Gradle plugin Using object factory
Works without applying plugin - +
Origin has a reference to its Wrapper + -

Using Object Factory

You can create an instance of object factory.

WrapperFactory<Wrapper> wrapperFactory = classFactory.buildWrapperFactory(); 

After that, you can create an adapter instance for each object:

Wrapper wrapper = wrapperFactory.get(yourObject);

This method doesn't require applying Maven/Gradle plugin. That makes it easier to debug. However, UniformFactory can't change loaded classes format, and therefore, can't introduce a reference to the wrapper into the origin object.

See example for using Object Factory option here

Using Maven/Gradle plugin

Maven/Gradle plugin get applied before a class gets loaded. Thus, Maven/Gradle plugin allows adding fields to the classes (e. g. a reference to the Adapter object)

The plugin does the following:

  • Makes the class implement Origin interface.
  • Created the wrapper property and assigns it with the needed adapter implementation

For instance, this class

@Marker
class Origin {
    //
    // Annotated class members are defined here
    //
}

becomes transformed to this one

@Marker
class Origin implements OriginInterface {
    //
    // Annotated class members are defined here
    // 
    
    private WrapperInterface wrapper = new Wrapper();
    
    @Override
    public WrapperInterface getWrapper() {
        return wrapper;
    }
}

The following tutorial describes how to set up Uniform Factory to generate wrapper classes properly.

Notes on testing

It's difficult to test Maven/Gradle plugin. However, both Plugin and Wrapper Factory use the common classes to generate adapters. Thus, even if you chose Plugin option, you still can test the generated wrapper without applying a plugin.

Just use the method buildWrapperFactory in your unit test in the Plugin: (example from the example Custom method list)

        // when
        Function<Origin1, ? extends Wrapper> meta = testSubject.generateMetaClass(Origin1.class);
        Wrapper w = meta.apply(origin);

Installing Uniform Factory Into Your Project

You can download Uniform Factory into your project from Maven Central.

Here is an example for Gradle:

dependencies {
   compile group: 'com.github.antkudruk', name: 'uniform-factory', version: '0.6.5'
}

and for Maven:

<dependency>
    <groupId>com.github.antkudruk</groupId>
    <artifactId>uniform-factory</artifactId>
    <version>0.6.5</version>
</dependency>

If you choose to apply Uniform Factory with ByteBuddy Gradle Plugin, just import and apply
byte-buddy-gradle-plugin and specify your plugin class.

Here is an example for Gradle:

plugins {
    id 'java'
    id "net.bytebuddy.byte-buddy-gradle-plugin" version "1.12.18"
}

byteBuddy {
    transformation {
        plugin = // Specify reference to your plugin class here, see the next chapter
    }
}

and in Maven:

    <plugin>
        <groupId>net.bytebuddy</groupId>
        <artifactId>byte-buddy-maven-plugin</artifactId>
        <version>1.12.18</version>
        <executions>
            <execution>
                <goals>
                    <goal>transform</goal>
                </goals>
            </execution>
        </executions>
        <configuration>
            <transformations>
                <transformation>
                    <plugin><!-- Specify your plugin class reference here --></plugin>
                </transformation>
            </transformations>
        </configuration>
    </plugin>

Let's take a look at some examples.

Examples

You can find compilable example folder of this project.

Empty Adapter

Here is an example of an empty wrapper. Empty wrapper is an interface that doesn't define any methods. Even though an empty wrapper is practically useless, it's a good point to start.

This plugin example adds an empty wrapper to an object satisfying a special criteria. Furthermore, the plugin makes these object implement the Origin interface. Thus, you can access the wrapper the following way:

Object origin = new OriginImpl();
Wrapper wrapper = ((Origin)origin).getWrapper();

You can find th whole compilable example that implements an empty wrapper here

Using Explicit Interface as an Adapter

Yon can avoid class cast from the previous example. To do it, your domain class should implement the Origin interface. If you mark Origin interface with the @Marker annotation, the standard Uniform Factory plugin is going to add the wrapper into this domain class:

@Marker
public interface Origin {
    default Wrapper getWrapper() {
        throw new RuntimeException("Wrapper method hasn't been implemented.");
    }
}

You can find an example of an explicit interface here

Select Type Criteria

You may specify custom criteria to choose classes to add adapters to. For example, matching class names to a special regular expression. UniformFactory provides a flexible way to select particular classes for that.

Let's implement a plugin to add wrappers to methods that explicitly implement the Origin interface, but using custom class selection criteria.

public class PluginImpl extends WrapperPlugin<Wrapper> {
    public PluginImpl() {
        super(
                Origin.class,
                Wrapper.class,
                // Class selection criteria
                td -> td.getInterfaces()
                        .stream()
                        .map(TypeDefinition::asErasure)
                        .anyMatch(new TypeDescription.ForLoadedType(Origin.class)::equals),
                "examplePlugin",
                ClassFactoryGeneratorImpl.class);
    }
}

See compilable code here

Using Abstract Class as an Adapter

Interfaces are stateless. So you can't have any variables inside interfaces.

But what if you'd like to store a state in your wrapper? For instance, you have to make a cache in your wrapper.

You can use a wrapper class instead of an interface.

Let's consider an example. Your wrapper hs an accumulator. And the accumulator increases by a number from an underlying object. It happens each time you get the accumulator value.

public abstract class Wrapper {

    private int accumulator;

    public int getAccumulated() {
        return accumulator += getDelta();
    }

    public abstract int getDelta();
}

Note that the abstract class must be public.

You can use that kind of wrapper exactly the same way as for the interface case. See compilable exampless and detailed description here

Generate Adapter that Implements Method Singleton

Let's enhance our empty Wrapper class.

Supposing we need to mark different class members to provide an object identity, just like in the following listing:

@Marker
public class Origin1 {
    @Identity
    private Long number = 10L;
}

@Marker
public class Origin2 {
    @Identity
    public String getName() {
        return "name";
    }
}

We need the common interface to get these identities:

    public interface Wrapper {
        String getId();
    }

How can we achieve this behaviour with UniformFactory? You can find a compilable example here

Application: Tree

Let's consider an example. Supposing we have the following structure of objects:

  • Company
  • Department
  • Employee

Each class has a method returning nested object. And each object has a label string to render. We'd like to make a uniform tree structure to use by UI to render.

// Wrapper interface
public interface TreeElement {
    String getLabel();
    List<HasTreeElement> nested();
}

// Origin interface
public interface HasTreeElement {
    TreeElement getTreeElement();
}

You can find an example of a tree here

Generate Wrappers that Provides List Of Methods

We'd like to be able to mark multiple class members with an annotation and work with them. How can we do that?

We can define the common interface containing the method:

public interface Processor {

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD, ElementType.FIELD})
    @interface Process {

    }

    boolean process(String eventName);
}

Then we make our Wrapper interface return an element of that functional interface:

public interface Wrapper {
    List<Processor> getProcessors();
}

You can find a compilable example here: here

Custom MetaclassGenerator to Generate Wrappers in Java

In the previous examples we used only default implementation DefaultMetaClassFactory, implementing interface MetaClassFactory. MetaClassFactory instance is a singleton per application.

Supposing, in the previous task we'd like to store a link to each object

Let's define custom MetaClassFactory.

public class ClassFactoryGeneratorImpl implements MetaClassFactory<Wrapper> {

    private final ClassFactory<Wrapper> classFactory;

    public ClassFactoryGeneratorImpl() throws NoSuchMethodException {
        this.classFactory = new ClassFactory.Builder<>(Wrapper.class)
                .addMethodList(
                        Wrapper.class.getMethod("getProcessors"),
                        boolean.class
                )
                .setMarkerAnnotation(Processor.Process.class)
                .setFunctionalInterface(Processor.class)

                .addResultTranslator(void.class, t -> true)
                .addResultTranslator(Long.class, t -> t >= 0)
                .addResultTranslator(String.class, "yes"::equalsIgnoreCase)
                .addResultTranslator(Boolean.class, t -> t)

                .parameterSource(String.class, 0)
                .applyTo(new AnyParameterFilter())
                .addTranslator(Integer.class, Integer::parseInt)
                .finishParameterDescription()

                .endMethodDescription()

                .build();
    }

    @Override
    public <O> Function<O, ? extends Wrapper> generateMetaClass(Class<O> originClass) {
        try {
            Constructor<? extends Wrapper> wrapperConstructor = classFactory
                    .build(new TypeDescription.ForLoadedType(originClass))
                    .load(DefaultMetaClassFactory.class.getClassLoader())
                    .getLoaded()
                    .getConstructor(originClass);

            return new WrapperObjectGenerator<>(wrapperConstructor);
        } catch (ClassGeneratorException | NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

    public static class WrapperObjectGenerator<O> extends DefaultMetaClassFactory.WrapperObjectGenerator<O, Wrapper> {

        WrapperObjectGenerator(Constructor<? extends Wrapper> wrapperConstructor) {
            super(wrapperConstructor);
        }

        @Override
        public Wrapper apply(O t) {
            Wrapper w = super.apply(t);
            CallableObjectsRegistry.INSTANCE.addObject(w);
            return w;
        }
    }
}

It's a good practice to move event notification functionality to a separate class (named CallableObjectsRegistry for instance). Let's implement it.

public class CallableObjectsRegistry {

    public static final CallableObjectsRegistry INSTANCE = new CallableObjectsRegistry();

    private final WeakHashMap<Wrapper, Object> object = new WeakHashMap<>();

    void addObject(Wrapper wrapper) {
        object.put(wrapper, null);
    }

    public boolean call(String eventName) {
        return object.keySet().stream()
                .map(Wrapper::getProcessors)
                .flatMap(Collection::stream)
                .map(t -> t.process(eventName))
                .reduce(true, (a, b) -> a & b );
    }
}

In case you're not familiar with Weak References, in a nutshell java.util.WeakHashMap allows to store references as keys and avoid holding objects in the memory after all the hard references to the objects are removed.

You can call assertTrue(CallableObjectsRegistry.INSTANCE.call(EVENT_TYPE_STRING)); to trigger the events and cause event handler methods to be invoked..

After that you can define classes processing

@Marker
public class Origin2 {

    private final Function<String, String> consumerString;
    private final Function<Integer, Boolean> consumerInteger;

    public Origin2(Function<String, String> consumerString, Function<Integer, Boolean> consumerInteger) {
        this.consumerString = consumerString;
        this.consumerInteger = consumerInteger;
    }

    @Processor.Process
    public String processString(String event) {
        return consumerString.apply(event);
    }

    @Processor.Process
    public Boolean processInteger(Integer event) {
        return consumerInteger.apply(event);
    }
}

Generate Adapters with Map of Methods

In the previous example, we just took all annotated class members. But what if we'd like to use some additional information?

We can use a map instead of a list. UniformFactory takes keys from the annotation parameters and generate values implementing functional interface:

public interface Coordinate {
    long getCoordinate(Long scale);
}

public interface PointWrapper {
    Map<String, Coordinate> getCoords();
}

You can find a compilable example here

Using Multiple Wrappers

Uniform Factory can generate multiple wrappers for one object.

Where it may be convenient?

Adapters generated by UniformFactory are stateless. But what if you're going to enhance your objects with state? For instance, with cache.

You can use two wrappers:

  • An adapter generated by Uniform Factory
  • Your cache object that works with the adapter

The example of code for multiple adapters at one origin class may be found here

Setting up a Field in the Origin Class

You can do setting up the field marked with an annotation exactly the same way as MethodSingleton does. See the example here

Setting up Multiple Fields

UniformFactory may implement adapters for multiple fields in the origin class for you. See the example here

Translating parameters and result

Translating result

Let's take a look at the following example. We have a wrapper containing two methods. Both methods return the same type and consume the same types, like in the following example.

public interface Wrapper {
    String processFirst(Integer scale);
    String processSecond(Integer scale);
}

To follow DRY (Don't Repeat Yourself) principle, it's better to use the common result and parameter translators to avoid adding them twice. setResultMapper and setMapper methods will help you to use specified mappers.

public class ClassFactoryGeneratorImpl extends DefaultMetaClassFactory<Wrapper> {

    private static ParameterMappersCollection<Integer> parameterMapper = new ParameterMappersCollection<>(Integer.class)
            .add(new TypeDescription.ForLoadedType(String.class), Object::toString)
            .add(new TypeDescription.ForLoadedType(Long.class), Integer::longValue);

    private static ResultMapperCollection<String> resultMapperCollection = new ResultMapperCollection<>(String.class)
            .addMapper(Long.class, Object::toString)
            .addMapper(int.class, Object::toString);

    public ClassFactoryGeneratorImpl() throws NoSuchMethodException {
        super(new ClassFactory.Builder<>(Wrapper.class)

                .addMethodSingleton(FirstMethodMarker.class, Wrapper.class.getMethod("process", Integer.class), String.class)
                .setResultMapper(resultMapperCollection)
                .parameterSource(Integer.class, 0)
                .applyTo(new AnyParameterFilter())
                .setMapper(parameterMapper)
                .finishParameterDescription()
                .endMethodDescription()

                .addMethodSingleton(SecondMethodMarker.class, Wrapper.class.getMethod("processSecond", Integer.class), String.class)
                .setResultMapper(resultMapperCollection)
                .parameterSource(Integer.class, 0)
                .applyTo(new AnyParameterFilter())
                .setMapper(parameterMapper)
                .finishParameterDescription()
                .endMethodDescription()

                .build());
    }
}

History

version Description
0.2.2 Added pure ByteBuddy implementation
0.3.0 Gave up builder experiments
0.4.0 Added an opportunity to implement custom method map
0.4.1 Clean up exceptions
0.5.1 Allowed subclasses in element factories. Cleaned up method
collections
0.5.2 Cleaned up messages in some exceptions.
Cleaned up method descriptors and builders from spare properties.
0.5.3 Small cleanup
0.6.0 Fixed bug for parameter mapper super types.
Added automated boxing of primitives.
Got rid of default translators in children mappers
0.6.1 Fixed a bug in selecting parameter by type
0.6.2 Verbose a non-intuitive message 'Invisible return type interface
Enabled getOrigin method in any Wrapper interface
0.6.3 Enabled multiple lists in the same wrapper and multiple maps in
the same wrapper
0.6.4 Modification of DynamicType.Builder in the pure ByteBuddy
implementation (e. g. adding required dynamic types)
Added required dynamic types to typeConstant BbImplementation
0.6.5 Enhanced adapters creating without Maven or Gradle plugin

License

Copyright 2020 - Present Anton Kudruk

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

About

Uniform Factory implements an Adapter pattern based on Reflection information. Normally, it generates adapters based on annotations. For each target class, it implements the common wrapper interface.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages