Skip to content
Thorben Kuck edited this page Dec 17, 2018 · 3 revisions

NetCom2 has a module, which allows you to drastically reduce boilerplate code required to start a ClientStart/ServerStart.

Requirement

If you want to use the Auto-Module, you have to have at least NetCom2 V.2.0.1.

Explanation

NetCom2 utilizes an object oriented and type-based communication system. The type of the received object triggers a Pipeline. You as a developer have to write one or multiple handlers that are added to the corresponding Pipeline. In the case of creating a ServerStart that handles a TestObject, it looks like this:

int port = ...
ServerStart serverStart = ServerStart.at(port);
serverStart.getCommnuicationRegistration()
     .register(TestObject.class)
     .addFirst((session, testObject) -> {
         // handle the received TestObject
     });

or, if you write it as a Method-Reference:

public class Example {

    private int port = ...

    public void run() {
        ServerStart serverStart = ServerStart.at(port);
        serverStart.getCommnuicationRegistration()
             .register(TestObject.class)
             .addFirst(this::handle);
    }

    public void handle(Session session, TestObject object) {
        // handle the received TestObject
    }
}

Of course, for the ClientStart this is nearly the same. This might not look like much, but imagine now, that you have 4 methods which handle the receiving of 4 different objects. Then you would have this code only to register those objects:

int port = ...
ServerStart serverStart = ServerStart.at(port);
serverStart.getCommnuicationRegistration()
     .register(TestObject.class)
     .addFirst(this::handle);
serverStart.getCommnuicationRegistration()
     .register(TestObject2.class)
     .addFirst(this::handle2);
serverStart.getCommnuicationRegistration()
     .register(TestObject3.class)
     .addFirst(this::handle3);
serverStart.getCommnuicationRegistration()
     .register(TestObject4.class)
     .addFirst(this::handle4);

...

This is a whole lot of repetitive code, but required, because each object has its own Pipeline.

let's again look at the first example:

ServerStart serverStart = ServerStart.at(port);
        serverStart.getCommnuicationRegistration()
             .register(TestObject.class)
             .addFirst(this::handle);

...

public void handle(Session session, TestObject object) {
    // handle the received TestObject
}

We can see, that there are a lot of details shared. The method call to register the object has the same type as the method to handle the receiving of said object. We should bee able to extract this information. In fact, we can, like this:

ServerStart serverStart = ServerStart.at(port);
        serverStart.getCommnuicationRegistration()
             .register(TestObject.class)
             .to(this);

...

@ReceiveHandler
public void handle(Session session, TestObject object) {
    // handle the received TestObject
}

This analyzes the method and determines that the ReceivePipeline has to invoke the method handle. This has some down-sites as you might recall. It uses reflections, which is a more or less bad thing. So, what if we could combine the annotation based approach and remove the need for reflections?

Exactly this is, what the Auto-Module allows us to do.

In conclusion: all we really care for is the method that handles the object. This module allows us, to not write the repetitive code, but to let an annotation processor generate it.

Usage

Let's change the previous example, to utilize the auto-module.

All we really care for is the method that handles the receiving of the Object. For that we annotate the method with the @Register annotation and use the class NetCom2 to create the ServerStart. An annotation processor will generate a wrapper class, that will be collected using the ServiceLoader class. Changed, the previous example will look like this:

public class Example {

    private int port = ...

    public void run() {
        NetCom2.launchServer()
            .use(ObjectRepository.hashingRecursive())
            .at(port)
            .onCurrentThread();
    }

    @Register
    public void handle(Session session, TestObject object) {
        // handle the received TestObject
    }
}

This looks like the same amount of code, but to further extend this example, all we have to do, is to add a new method with a new annotation. Let's write the methods to receive 4 different objects now.

public class Example {

    private int port = ...

    public void run() {
        NetCom2.launchServer()
            .use(ObjectRepository.hashingRecursive())
            .at(port)
            .onCurrentThread();
    }

    @Register
    public void handle(Session session, TestObject object) {
        // handle the received TestObject
    }

    @Register
    public void handle(Session session, TestObject2 object) {
        // handle the received TestObject2
    }

    @Register
    public void handle(Session session, TestObject3 object) {
        // handle the received TestObject3
    }

    @Register
    public void handle(Session session, TestObject4 object) {
        // handle the received TestObject4
    }
}

We do not have to specifically define that those methods have to be put into their corresponding Pipelines. This will be done for us. All we have to do, to add a new Object to be handled, is to add a new Method and annotate it with the @Register annotation.

This has some advantages. It is nearly as fast as writing a custom OnReceive instance. The generated code has to fetch an instance of the type we want this method to be invoked on. But, if this is existing, the added overhead is only one Map#get call. Since the code is generated, we do not need reflections to invoke the method, we invoke it directly. Also, we have to write a lot less code.

Different Factories

The NetCom2 class provides multiple ways of creating ServerStart and ClientStart. First of, it has four Methods:

ServerFactory     factory1 = NetCom2.launchServer();
ClientFactory     factory2 = NetCom2.launchClient();
LazyServerFactory factory3 = NetCom2.createServer();
LazyClientFactory factory4 = NetCom2.createClient();

The ServerFactory and ClientFactory allows you to create and launch a ServerStart and ClientStart, whilst the LazyServerFactory and LazyClientFactory only create a ServerStart and ClientStart.

The ServerFactory requires one specific thing. Since the ServerStart has to handle the acceptance of the clients, it asks for a way to handle it. You can choose to accept all clients on the current Thread (blocking until the ServerStart terminates) or on a different Thread

(
on=custom ExecutorService,
onNetComThreadPool=NetCom2-Asynch NetComThreadPool,
onThread=a new Thread
)

Not: The acceptance of the Clients will be done as long as the ServerStart is running. Imagine the code like this:

while (serverStart.running()) {
    try {
        serverStart.acceptAllNextClients();
    } catch (ClientConnectionFailedException e) {
        UnhandledExceptionContainer.catching(e);
    }
}

If anything goes wrong inside NetCom2, a NetworkInterfaceFactoryException is thrown. You may catch it if you want to handle errors.

All Factories have to define the ObjectRepository they should use!

ObjectRepository

The ObjectRepository is an interface, which decouples the object the annotated method should be called on, from the generated code.

Here is an example of how a generated class might look like:

class GeneratedOnReceiveTriple implements OnReceiveTriple<TestObject> {
    @Override
    public final void accept(final ConnectionContext connectionContext, final Session session,
				final TestObject variable) {
        // Execute the real method
    }
}

Execute the real method requires an instance of the object. For that, we use the ObjectRepository, like this:

class GeneratedOnReceiveTriple implements OnReceiveTriple<TestObject> {
    private final ObjectRepository objectRepository;

    GeneratedOnReceiveTriple(ObjectRepository objectRepository) {
        this.objectRepository = objectRepository;
    }

    @Override
    public final void accept(final ConnectionContext connectionContext, final Session session,
				final TestObject variable) {
        // Execute the real method
        Example toExecuteOn = objectRepository.get(Example.class);
        toExecuteOn.handle(session, variable);
    }
}

Example is the class, which holds the handle method, which is annotated with the @Register annotation.

No default ObjectRepository is set. Therefor it is required to provide one. Possible default implementations are:

// This will try to invoke default
// constructors and safe created instances.
ObjectRepository.hashingDefault();
// This will try to invoke any Constructor
// as well as Constructors of dependencies.
// Does support constructor dependencies. 
// Afterwards, it safes created instances.
ObjectRepository.hashingRecursive();

You can create any ObjectRepository or add custom instances using the add Method. For example: You might create a ObjectRepository that requests the instance from the Guice-Injector.

Supported Annotations

The following annotations are supported for code generation.

@Register

This annotation allows you to annotate methods, that handle the receiving of objects.

The annotated method can be structured as any OnReceive. It's signature can look like this:

void foo(TestObject object)
void foo(Session session, TestObject object)
void foo(Connection context, Session session, TestObject object)

Arguments

String: name. Defines the class-name, the generate class should have.
boolean: autoLoad: Whether or not the generated class should be collected automatically on creation using the NetCom2-class

Example with default parameter-values
@Register(name="", autoLoad=true)
public void handle(TestObject object) {
    // Received a TestObject
}

@Connect

This annotation allows you to annotate methods, that handle a freshly connected client.

The annotated method has to have exactly one argument of the type Client.

Only viable for the ServerStart!

Arguments

String: name. Defines the class-name, the generate class should have.
boolean: autoLoad: Whether or not the generated class should be collected automatically on creation using the NetCom2-class

Example with default parameter-values
@Connect(name="", autoLoad=true)
public void handle(Client client) {
    // A new client connected
}

@Disconnect

This annotation allows you to annotate methods, that handle a disconnected client.

The annotated method has to have exactly one argument of the type Client.

Arguments

String: name. Defines the class-name, the generate class should have.
boolean: autoLoad: Whether or not the generated class should be collected automatically on creation using the NetCom2-class
boolean: forServer: Whether or not the generated class should be collected if a ServerStart is created
_boolean: forClient: Whether or not the generated class should be collected if a ClientStart is created

Example with default parameter-values
@Disconnect(name="", autoLoad=true, forServer=true, forClient=true)
public void handle(Client client) {
    // A client disconnected
}

@Configure

This annotation allows you to annotate methods, which handle certain object before the ServerStart/ClientStart are launched.

The annotated method has to have exactly one argument, of one of those Types:

  • ServerStart Configure a ServerStart before it is launched.
  • ClientStart Configure a ClientStart before it is launched.
  • CommunicationRegistration Configure a CommunicationRegistration before its corresponding ServerStart/ClientStart is launched.

The annotation processor decides if the generated class should handle ClientStart/ServerStart or both depending on the type of the argument and the configuration within the annotation.

Arguments

String: name. Defines the class-name, the generate class should have.
boolean: autoLoad: Whether or not the generated class should be collected automatically on creation using the NetCom2-class
boolean: forServer: Whether or not the generated class should handle a ServerStart
_boolean: forClient: Whether or not the generated class should handle a ClientStart

Example with default parameter-values
@Configure(name="", autoLoad=true, forServer=true, forClient=true)
public void handle(ServerStart serverStart) {
    // Configure the ServerStart
}

@Configure(name="", autoLoad=true, forServer=true, forClient=true)
public void handle(ClientStart clientStart) {
    // Configure the ClientStart
}

Clone this wiki locally