Package initialization
Packages are used in Java to group all classes belonging to a module in one place. Only the interface to the module should be public, all other classes and methods remain private.
In many cases, a module has dependencies on other modules or services. Now you could check before each use if all dependencies are fulfilled, i.e. the corresponding modules are available. However, this is a laborious approach.
Automatic dependency injection, such as in Spring Boot, would be a good start.
But this is not an option in a dynamic system like
Our answer here is to use service or package starters. In those we define a list of dependencies. The system checks whether our dependencies are met or not every time the service environment is changed. If they are fulfilled, but our package is not started yet, the package will be started. If they are not fulfilled, but our package is already started, it will be stopped.
Package starters are often the only classes that are public in a package. The actual implementation of the functions remains hidden. APIs of the packages are published as interfaces in the service registry. This ensures that no one communicates bypassing the APIs.
Dependencies in unit tests are resolved by calling the start method of the corresponding package starter.
Getting started
We want to create a target, which must be registered in a namespace. To get the namespace, we need the namespace registry. To do this, we create a package private interface in a new package that defines our dependencies. That might be a little overkill, but it's the principle.
interface IDependencies { INamespaceRegistry getNamespaceRegistry(); }
We now define a package starter that implements the interface IServiceStarter as well as our interface IDependencies.
public final class CPackageImplPingTarget implements IServiceStarter, IDependencies { private IService mService; private INamespaceRegistry mNamespaceRegistry; @Override public void getDependencies(@NotNull final IServiceDependencyList aDependencyList) { aDependencyList.add(INamespaceRegistry.class); } @Override public void start(@NotNull final IServiceRegistry aServiceRegistry) throws Exception { if (mService == null) { mNamespaceRegistry = aServiceRegistry.getServiceOrThrow(INamespaceRegistry.class); mService = new CPingTarget(this); mService.activate(aServiceRegistry); } } @Override public void stop(@NotNull final IServiceRegistry aServiceRegistry) throws Exception { if (mService != null) { mService.deactivate(aServiceRegistry); mService = null; } } @Override public INamespaceRegistry getNamespaceRegistry() { return mNamespaceRegistry; } }
You can now easily see the three methods of the IServiceStarter as well as the one method of the
interface IDependencies
.
The only dependency entered in the list is the interface INamespaceRegistry.
Additional dependencies may be added as development progresses.
These would then have to be entered in the dependency list as well as in the interface IDependencies
.
The IDependencies
interface is passed to all classes in the package so that the dependencies are
available everywhere.
If our dependency is fulfilled, here the service INamespaceRegistry is available, the system calls the
start()
method.
We can now fetch the required services.
Then we create our required classes, here the target CPingTarget
.
CPingTarget
implements the IService interface.
This is not mandatory, but it simplifies our setup, since this interface consists of an activate()
and
deactivate()
method.
We can conveniently call these in the start()
and stop()
handlers.
Our target is still missing.
The target
final class CPingTarget extends CTarget implements IService { private final IDependencies mDependencies; /** * Constructor. */ public CPingTarget(@NotNull final IDependencies aDependencies) { mDependencies = aDependencies; addMessageHandler(CRecordPing.ID, this::asyncPing); } @Override public void activate(@NotNull final IServiceRegistry aServiceRegistry) throws Exception { final INamespace namespace = mDependencies.getNamespaceRegistry() .getNamespace(CWellKnownNID.SYSTEM); namespace.getTargetRegistry() .registerTarget(this); } @Override public void deactivate(@NotNull final IServiceRegistry aServiceRegistry) { deregisterTarget(); } private boolean asyncPing(@NotNull final CEnvelope aEnvelope, @NotNull final CRecord aRecord) throws CException { if (aEnvelope.isAnswer()) { return false; } else { final long loop = CRecordPing.getLoop(aRecord, 0); CRecordPing.setLoopMirror(aRecord, loop); aEnvelope.setResultSuccess(); return true; } } }
The activate()
and deactivate()
methods can be seen very nicely here.
In the activate method, the namespace "SYSTEM" is fetched via the namespace registry. Our target is registered in its target registry. After that it can send and receive messages.
In the deactivate method our target is de-registered.
Consuming a message
By the way, you can also see here how a target consumes a message.
A message handler is registered in the constructor (asyncPing
).
We have made it a practice to always start message processing methods with async.... .
The asyncPing
method here only responds to requests, responses are discarded.
We get an argument of the message from the record (getLoop
) and copy it to another slot
(setLoopMirror
).
As a result, we set the value Success.
Register the service starter
The service starter must still be made known to the system so that it is also started. This is usually done in the class that implements the plugin interface and thus initializes all services in a plugin JAR.
public final class CModuleMyPlugin implements IPlugIn { @Override public void startPlugin(@NotNull final URL aUrl, @NotNull final ClassLoader aClassLoader, @NotNull final IServiceRegistry aServiceRegistry) { aServiceRegistry.addStarter(new CPackageImplPingTarget()); // Initialize more packages here } }
That's about it. The whole thing is relatively meaningless, since targets implement similar things via the base CTarget class. But it shows how a package is organized, how a target is registered and deregistered, and how a message is received.