OSGi: Configuration Admin und Declarative Services

Zwei sehr interessante Gebiete der OSGi-Umgebung:

  • 104 Configuration Admin Specification: Sie beschreibt, wie man die Konfiguration seiner Anwendung auf die Platte speichern kann
  • 112 Declarative Service Specification: Sie beschriebt, wie man relativ elegant per XML seine Services definieren kann (wobei elegant immer subjektiv ist).

Die beiden Spezifikationen zusammen ergeben eine elegante Möglichkeit Services in Abhängigkeit von der gespeicherten Konfiguration a) zu starten und b) zu konfigurieren.

Das Interface

Als Beispiel soll innerhalb der Konfiguration eine Service für eine Begrüßung implementiert werden. Der Service soll das folgende Interface implementieren:
[code language=“java“ wraplines=“false“]
public interface IGreetingService {

String getLanguage();

String sayHello(String name);
}
[/code]

Die Implementierung (Teil 1)

Innerhalb der Konfiguration soll die Sprache und ein Pattern für die Begrüßung hinterlegt werden. So können wir für jede „neue“ Sprache einfach eine neue Konfiguration anlegen. Die Implementierung kann dann so aussehen:
[code language=“java“ wraplines=“false“]
public class GreetingService implements IGreetingService {

private String pattern;
private String language;

@Override
public String sayHello(String name) {
return MessageFormat.format(pattern, name);
}

@Override
public String getLanguage() {
return language;
}
}
[/code]
Die beiden Variablen pattern und language sollen in der Konfiguration gespeichert werden – wie die aus der Konfiguration in den Service wandern erkläre ich später. Als nächste brauchen wir noch eine XML-Datei, mit der wir den Service definieren:
Die wichtigsten Unterschiede zu einem nicht konfigurierbaren Service liegen in der Kombination der Attribute des
scr:component Elements:

  • enabled=“true“: Die Komponenten muss immer enabled sein, weil sie aus der Konfiguration „bestückt“ werden soll.
  • immediate=“false“: Die Komponente soll nur dann „erzeugt“ werden, wenn wir wirklich ein brauchen, d.h. wenn min. eine entsprechende Konfiguration vorliegt.
  • modified=“modified“: Damit die Komponente benachrichtigt wird, wenn wir die Konfiguration ändern, müssen wir dem Framework die entsprechende Methode mitteilen.
  • configuration-policy=“required“: Es soll für genau eine Konfiguration eine Instanze der Komponente angelegt werden. In diesem Fall stellt die Komponente den IGreetingService zur Verfügung, was bedeutet, das jede Instanz der Komponente gleichzeitig auch als OSGi Service angemeldet wird.
  • factory: Dieses Element wird in unserem Fall nicht genutzt!

Bevor wird das kleine Beispiel starten können müssen wir die Komponente noch in der MANIFEST.MF unter dem Attribute Service-Component eintragen. Weiterhin sollte man sicher stellen, dass die beiden Bundles (Configuration Admin Specification und Declarative Service Specification) für die beiden Services beim Starten mit eingebunden werden. In Equinox sind dies die Bundles org.eclipse.equinox.cm und org.eclipse.equinox.ds. (in anderen Implementierungen werden die Namen ggf. abweichen).

Nach dem ersten Starten macht sich Ernüchterung breit, denn es passiert nichts spektakuläres. Innerhalb der Equinox Konsole zeigen die Befehle ss zwar das unser Bundles korrekt geladen ist. Aber wir können mit services keinen IGreetingService’s finden. Der Befehl list zeigt und aber schon mal etwas:

1 Unsatisfied GreetingService GreetingService-Bundle

Ggf. haben wir beim Starten auch eine Warnung gesehen:

!MESSAGE The ‚GreetingService‘ component’s configuration could not be satisfied because it is required to be initialized by a ConfigurationAdmin service Configuration object, but one was not found.

Diese Informationen bestätigen, was wir schon wissen und was wir als nächste beheben wollen: Es gibt natürlich noch keine Konfiguration für unseren Service. Aus diesem Grund bekommen wir die Warnung und aus dem gleichen Grund ist die Komponente noch Unsatisfied.

Konfiguration anlegen

Als nächste müssen wir eine (oder mehrere) Konfiguration(en) für unseren GreetingService. In OSGi wird dies mit dem ConfigurationAdmin-Service durchgeführt:
[code language=“java“ wraplines=“false“]
Configuration config = configurationAdmin.createFactoryConfiguration("GreetingService");
Properties props = new Properties();
props.put("language", language);
props.put("pattern", pattern);
config.update(props);
[/code]
Die Daten für den Service übergeben wir wie üblich über eine Map mit den beiden Schlüssel language und pattern. An dieser Stelle sind zwei Dinge wichtig:

  • wir müssen über den Service eine FactoryConfiguration erzeugen und dem entsprechend die Methode createFactoryConfiguration nutzen. Auf keinen Fall dürfen wir zum Erzeugen getConfiguration benutzen!
  • Als factoryPid müssen wir den Namen (Attribute name) der Komponente aus der XML-Datei benutzen.

Daten aus der Konfiguration in den Service leiten

Als nächstes müssen wir noch die Daten aus der Konfiguration den den Service bekommen. An dieser Stelle müssen wir die aus der Declarative Service Specification schon bekannten Methoden bedienen:
[code language=“java“ wraplines=“false“]
protected void modified(ComponentContext cc) {
language = (String) cc.getProperties().get("language");
pattern = (String) cc.getProperties().get("pattern");
}

protected void activate(final ComponentContext cc) {
language = (String) cc.getProperties().get("language");
pattern = (String) cc.getProperties().get("pattern");
}

protected void deactivate(final ComponentContext cc) {
// nothing to do
}
[/code]
In den Methoden modified und activate lesen wir die Daten der Konfiguration aus und setzen die entsprechenden Variablen des Service.

  • deactivate: Diese Methode wir immer aufgerufen, wenn eine Konfiguration für diesen Service gelöscht wird. In der Regel sollen hier Ressourcen freigegeben werden
  • activate: Diese Methode wir immer aufgerufen, wenn der Service das erste mal erzeugt wird.
  • modified: Diese Methode wir immer aufgerufen, wenn sich die Konfiguration zu diesem Service ändert.

Konfiguration ändern

Das Ändern einer Konfiguration funktioniert analog zur Änderung von „normalen“ Konfigurationen.
[code language=“java“ wraplines=“false“]
String filter = "(&(service.factoryPid=GreetingService)(language=" + language + "))";
Configuration[] cList = configurationAdmin.listConfigurations(filter);
for (Configuration c : cList) {
Dictionary props = c.getProperties();
props.put("pattern", pattern);
c.update(props);
}
[/code]
In diesem Fall können wir über den Filter nach der entsprechenden Sprache suchen und die Konfiguration aktualisieren. In diesem Fall wird die modified-Methode des entsprechenden Service gerufen.

Konfiguration löschen

Auch das löschen funktioniert wie bei „normalen“ Konfigurationen.
[code language=“java“ wraplines=“false“]
String filter = "(&(service.factoryPid=GreetingService)(language=" + language + "))";
Configuration[] cList = configurationAdmin.listConfigurations(filter);
for (Configuration c : cList) {
c.delete(m);
}
[/code]
In diesem Fall wird die deactivate-Methode des entsprechenden Service gerufen.

Zusammenfassung

Mit den hier gezeigten Beispiel kann man jetzt über den ConfigurationAdmin-Service entsprechend viele Konfigurationen erzeugen. Die OSGi-Umgebung erzeugt für jede Konfiguration einen entsprechenden Service und ruft, je nach Kontext, activate, modified oder deactivate auf. Weiterhin übernimmt OSGi das Speichern der Konfiguration und das laden der selbigen beim Starten.

Der schwierige Punkt bei dem Zusammensetzen der Configuration Admin Specification mit der Declarative Service Specification waren:

  • Ein Bug in der Equinox-Implementierung, wenn man dynamische Bundles nutzt und die Reihenfolge der Aktivierung eine ganz spezielle ist — der Bug war extrem schwer zu finden und wurde von den Equinox-Leuten im 3.6 Release behoben.
  • Eine korrekte XML für die Komponente zu erstellen, weil die Doku an dieser Stelle etwas undeutlich ist.