OSGi provides a standard configuration mechanism called
config admin. This allows the location of configuration information to be decoupled from the code that requires the configuration. Configuration is passed through to services via a Map
or Hashtable
, and they can configure themselves appropriately.
As with other parts in OSGi, this can also be dynamically updated. When the configuration source changes; an event can flow through to the service or component to allow it to reconfigure itself.
Config admin itself is an OSGi service, and it may be supplied by different configuration agents. The de facto standard is Apache Felix's FileInstall, which can also be used to install bundles into an OSGi runtime.
FileInstall is available from the Apache Felix site at http://felix.apache.org as well as Maven Central. Search for org.apache.felix.fileinstall
at http://search.maven.org and download the latest Jar. It can be imported into Eclipse as a plug-in project with File | Import | Plug-in Development | Plug-ins and Fragments to enable it to run in a test runtime.
The system property felix.fileinstall.dir
must be specified to use FileInstall. It defaults to ./load
from the current working directory, but for the purpose of testing, this can be specified by adding a VM argument in the launch configuration that appends -Dfelix.fileinstall.dir=/tmp/config
or some other location. This can be used to test modifications to configuration later.
To configure services, ConfigAdmin needs to be installed into the runtime as well. The two standard implementations of these are Felix ConfigAdmin and Equinox Config Admin. The latter does not come with Eclipse by default, and the Felix version is available from Maven Central and should be preferred. Search for org.apache.felix.configadmin
at http://search.maven.org, download the latest Jar, and then import this as a plug-in project to Eclipse with File | Import | Plug-in Development | Plug-ins and Fragments so that it can be used as a bundle in the OSGi framework.
A component created by Declarative Services can have configurations passed in a Map
. A component can have an activate
method, which is called after the component's dependencies have become available (along with a corresponding deactivate
method). There is also a modified
method, which can be used to respond to changes in configuration without stopping and restarting the component.
To configure the TimeZonesProvider
with config admin, add a configure
method that takes a Map
of values. If it's non-null
and there is a key max
, then parse it as an int
and use that as the max
value. Use this to set a limit on the number of time zones returned in the getTimeZones
method:
private long max = Long.MAX_VALUE; public Map<String, Set<ZoneId>> getTimeZones() { ... .filter(s -> s.contains("/")) // with / in them .limit(max) // return this many only .map(ZoneId::of) // convert to ZoneId ... } public void configure(Map<String, Object> properties) { max = Long.MAX_VALUE; if (properties != null) { String maxStr = (String) properties.get("max"); if (maxStr != null) { max = Long.parseLong(maxStr); } } }
To ensure that the method gets called, modify the service component document to add the activate="configure"
and modified="configure"
attributes:
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" modified="configure" activate="configure" name="TimeZonesProvider">
Finally, create a properties file with the contents max=1
called TimeZonesProvider.cfg
, and place it in the location of the felix.fileinstall.dir
.
Now when the application is run, the configuration should be loaded and configure the TimeZonesProvider
, such that when the time zones are requested, it shows a maximum of one value.
If nothing is seen, verify that the felix.fileinstall.dir
is specified correctly using props | grep felix
from the OSGi console. Also verify that the Felix fileinstall
and configadmin
bundles are started. Finally, verify that the methods in the component are public void
and are defined correctly in the component config.
A service factory can be used to create services on demand, rather than being provided up front. OSGi defines a number of different service factories that have different behaviors.
Ordinarily services published into the registry are shared between all bundles. OSGi R6 adds a service.scope
property, and uses the singleton
value to indicate that the same instance is shared between all bundles.
Service factories allow multiple instances to be created, and there are three different types:
ServiceFactory
, which creates a new instance per bundle (registered withservice.scope=bundle
in OSGi R6)ManagedServiceFactory
, which uses config admin to create instances per configuration/pid (registered withservice.scope=bundle
in OSGi R6)PrototypeServiceFactory
, which allows multiple instances per bundle (newly added in OSGi R6 registered withservice.scope=prototype
)
The ServiceFactory
allows a per-client bundle instance to be created, to avoid bundles sharing state. When a client bundle requests a service, if the bundle has already requested the service then the same instance is returned; if not, a service is instantiated. When the client bundle goes away, so does the associated service instance.
A ManagedServiceFactory
provides a means to instantiate multiple services instead of a single service per component. Multiple instances of a service can be created, each with their own configuration using service.pid-somename.cfg
. Each bundle shares the instances of these services, but other client bundles will instantiate their own. Like ServiceFactory
, if the service has been requested before, the same bundle will be returned.
The PrototypeServiceFactory
was added in OSGi R6 (available since Eclipse Luna) as a means of providing a bundle with multiple instances of the same service. Instead of caching the previously delivered service per bundle, a new one is instantiated each time it is looked up. The client code can use BundleContext.getServiceObjects(ref) .getService()
to acquire a service through the PrototypeServiceFactory
. This allows stateful services to be created.
As an example, consider an EchoServer
that listens on a specific ServerSocket
port. This can be run on zero or many ports at the same time. This code will be used by the next section, and simply creates a server running on a port and sets up a single thread to accept client connections and echo back what is typed. The code here is presented without explanation other than its purpose, and will be used to create multiple instances of this service in the next section.
When this is instantiated on a port (for example, when new EchoServer(1234)
is called) it will be possible to telnet
to the localhost
on port 1234
and have content echoed back as it is typed. To close the connection, use Ctrl + ] and then type close
:
public class EchoServer implements Runnable { private ServerSocket socket; private boolean running = true; private Thread thread; public EchoServer(int port) throws IOException { this.socket = new ServerSocket(port); this.thread = new Thread(this); this.thread.setDaemon(true); this.thread.start(); } public void run() { try { byte[] buffer = new byte[1024]; while (running) { Socket client = null; try { client = socket.accept(); InputStream in = client.getInputStream(); OutputStream out = client.getOutputStream(); int read; while (running && (read = in.read(buffer)) > 0) { out.write(buffer, 0, read); out.flush(); } } catch (InterruptedIOException e) { running = false; } catch (Exception e) { } finally { safeClose(client); } } } finally { safeClose(socket); } } public void safeClose(Closeable closeable) { try { if (closeable != null) { closeable.close(); } } catch (IOException e) { } } public void stop() { running = false; this.thread.interrupt(); } }
Create an EchoServiceFactory
that implements ManagedServiceFactory
, and register it as a managed service factory in a component:
<?xml version="1.0" encoding="UTF-8"?> <scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="EchoServiceFactory"> <implementation class="com.packtpub.e4.timezones.internal.EchoServiceFactory"/> <service> <provide interface="org.osgi.service.cm.ManagedServiceFactory"/> </service> <property name="service.pid" type="String" value="com.packtpub.e4.timezones.internal.EchoServiceFactory"/> </scr:component>
The EchoServiceFactory
is responsible for managing the children that it creates and, since they will be using threads, appropriately stopping them afterwards. The ManagedServiceFactory
has three methods; getName
, which returns the name of the service, and updated
and deleted
methods for reacting to configurations coming and going. To track them, create an instance variable in the EchoServiceFactory
called echoServers
which is a map of pid
to EchoServer
instances:
public class EchoServiceFactory implements ManagedServiceFactory { private Map<String, EchoServer> echoServers = new TreeMap<String, EchoServer>(); public String getName() { return "Echo service factory"; } public void updated(String pid, Dictionary<String, ?> props) throws ConfigurationException { } public void deleted(String pid) { } }
The updated
method will do two things; it will determine if a port
is present in the properties, and if so, instantiate a new EchoServer
on the given port. If not, it will deconfigure the service:
public void updated(String pid, Dictionary<String, ?> properties) throws ConfigurationException { if (properties != null) { String portString = properties.get("port").toString(); try { int port = Integer.parseInt(portString); System.out.println("Creating echo server on port " + port); echoServers.put(pid, new EchoServer(port)); } catch (Exception e) { throw new ConfigurationException("port", "Cannot create a server on port " + portString, e); } } else if (echoServers.containsKey(pid)) { deleted(pid); } }
If an error occurs while creating the service (because the port number isn't specified, isn't a valid integer, or is already in use), an exception will be propagated back to the runtime engine, which will be appropriately logged.
The deleted
method removes it if present, and stops it:
public void deleted(String pid) { System.out.println("Removing echo server with pid " + pid); EchoServer removed = echoServers.remove(pid); if (removed != null) { removed.stop(); } }
Now that the service is implemented, how is it configured? Unlike singleton configurations, the ManagedServiceFactory
expects the pid
to be a prefix of the name, followed by a dash (-
) and then a custom suffix.
Ensure that the timezones
bundle is started, and that the EchoServiceFactory
is registered and waiting for configurations to appear:
osgi> ss | grep timezones 13 ACTIVE com.packtpub.e4.timezones._1.0.0.qualifier osgi> start 13 osgi> bundle 13 | grep service.pid {org.osgi.service.cm.ManagedServiceFactory}={ service.pid=com.packtpub.e4.timezones.internal.EchoServiceFactory}
Now create a configuration file in the Felix install directory com.packtpub.e4.timezones.internal.EchoServiceFactory.cfg
with the content port=1234
. Nothing happens.
Now rename the file to something with an -extension
on the end, such as -1234
. The suffix can be anything, but conventionally naming it for the type of instance being created (in this case, a service listening on port 1234
) makes it easier to keep track of the services. For example, create com.packtpub.e4.timezones.internal.EchoServiceFactory-1234.cfg
with contents port=1234
in the configuration directory. When this happens, a service will be created:
Creating new echo server on port 1234
Telnetting to this port can see the output being returned:
$ telnet localhost 1234 Connected to localhost. Escape character is '^]'. hello hello world world ^] telnet> close Connection closed by foreign host.
Creating a new service pid will start a new service; create a new file called com.packtpub.e4.timezone.internal.EchoServiceFactory-4242.cfg
with the contents port=4242
. A new service should be created:
Creating new echo server on port 4242
Test this by running telnet localhost 4242
. Does this echo back content as well?
Finally, remove the service configuration for port 1234
. This can be done by either deleting the configuration file, or simply renaming it with a different extension:
Removing echo server
Verify that the service has stopped:
$ telnet localhost 1234 Trying 127.0.0.1... telnet: unable to connect to remote host