Eine Fachkomponentenarchitektur mit Spring 3.0/3.1 – Teil 3: Properties

Keine Kommentare

Nach der grundsätzlichen Struktur und den Ressourcen behandle ich nun im dritten Blogpost dieser Reihe das Thema Properties. Dieses mag erst einmal einfach erscheinen, Entwickler mit Spring-Background werden schnell auf den PropertyPlaceHolderConfigurer verweisen und die Sache abhaken, und doch – in dem beschriebenen Umfeld (> 100 Entwickler, viele verschiedene Abteilungen, beliebige Anwendungen können beliebige Fachkomponenten verwenden) gibt es einige Aspekte, die das Ganze verkomplizieren.

Was sind Properties?

Aber treten wir doch mal einen Schritt zurück und schauen uns an, was Properties überhaupt sind. Properties werden dazu verwendet, Konfigurationswerte aus der Anwendung auszulagern, die später durch einen wie auch immer gearteten Außenstehenden gesetzt werden. Dabei gibt es zwei Gruppen von Properties:

  • Properties, die das Anwendungsverhalten bestimmen, unterschiedliche Modi und Ähnliches (Kategorie A).
  • Properties, die Ressourcen konfigurieren, DB-URL, Queue-Namen und Ähnliches (Kategorie B).

Properties sind statisch und ändern sich zur Laufzeit der Anwendung nicht. Für Werte, die dynamisch während der Laufzeit geändert werden müssen, bieten sich andere Konzepte an (Datenbank, JMX).

Das Auslesen von Properties ist Infrastrukturcode und sollte deshalb nicht mit Businesslogik vermischt werden. Im Kontext der Fachkomponentenarchitektur bedeutet das, dass Properties immer im Konfigurationsprojekt ausgelesen und dann per Dependency Injection in den Businesskomponenten gesetzt werden. Ein Beispiel zeigt wahrscheinlich am deutlichsten, wie das gemeint ist.

Sagen wir, der PartnerService hätte ein Flag read-only, also eine Property der Kategorie A, die das Verhalten der Komponente bestimmt.

public class PartnerServiceImpl implements PartnerService {
 
	private boolean readOnly;
 
	private JdbcTemplate jdbcTemplate;
 
	public PartnerServiceImpl(JdbcTemplate jdbcTemplate, boolean readOnly) {
		this.jdbcTemplate = jdbcTemplate;
		this.readOnly = readOnly;
	}
 
	public void savePartner(Partner partner) {
		if (readOnly) {
			throw new IllegalStateException(
					"Persisting partner not allowed in read-only mode!");
		}
		// save Partner
	}
 
	public Partner getPartner(long id) {
		return this.jdbcTemplate.queryForObject("SELECT ....",
				new PartnerRowMapper, id);
	}
 
}

Properties und die Environment Abstraction in Spring 3.1

Das Flag wird nicht direkt im PartnerService ausgelesen, diesen Teil übernimmt die PartnerConfig, die mit der in Spring 3.1 eingeführten Environment Abstraction (siehe auch diesen Artikel) folgendermaßen aussehen würde:

@Import(HighLevelDataAccessConfig.class)
@PropertySource("classpath:partner.properties")
@Configuration
public class PartnerConfig {
 
	@Autowired
	private Environment environment;
 
	@Autowired
	private HighLevelDataAccessConfig dataAccessConfig;
 
	@Bean
	public PartnerService partnerService() throws Exception {
		return new PartnerServiceImpl(dataAccessConfig.jdbcTemplate(),
				environment.getProperty("partner.readonly", boolean.class));
	}
 
}

Hier die neuen Elemente im Einzelnen:

@PropertySource("classpath:partner.properties")

Die Annotation PropertySource bewirkt, dass die Properties im angegebenen Properties-File dem Environment hinzugefügt werden.

	@Autowired
	private Environment environment;

Das Environment steht ab Spring 3.1 immer im ApplicationContext zur Verfügung und kann in Konfigurationsklassen injiziert werden.

environment.getProperty("partner.readonly", boolean.class)

Über das Environment-Objekt kann auf Properties zugegriffen werden. Per Default sind im Environment übrigens alle JVM-Systemproperties und alle Umgebungsvariablen enthalten. In unserem Fall kommen mindestens die Properties aus partner.properties hinzu.

Properties im Enterprise-Umfeld

So weit, so gut. Betrachten wir nun eine Anwendung, die Fachkomponenten verwendet: eine Web-Anwendung, die die Services aus InkassoService als Rest-Service anbietet. Das naheliegende Vorgehen bezüglich Properties wäre es, eine Deployment-Einheit (also ein war oder ear) zu bauen, das die Properties-Files nicht enthält, und später beim Deployment die passenden Properties-Files in einem lib-Verzeichnis des Application Servers unterzubringen, so dass sie dem Klassenpfad hinzugefügt werden. Das kommt jedoch in diesem Fall nicht in Frage und ist darüber hinaus noch unpraktisch:

  • Properties müssen revisionierbar sein: Wenn Properties geändert werden, muss klar sein, wer das wann gemacht hat. Liegen die Properties irgendwo im Filesystem des Application Servers, so ist das nicht kontrollierbar.
  • Build- und Deploymentprozess wird verkompliziert: Woher kommen die Properties? Wer pflegt sie? Wer sorgt dafür, dass beim Deployment die richtigen Properties ins richtige Verzeichnis kopiert werden? Wer räumt die Verzeichnisse auf?
  • Entwickler einer Anwendung muss alle Properties liefern: Der Entwickler der InkassoService-Rest-Anwendung muss alle integrierten Fach- und Infrastrukturkomponenten kennen, um die richtigen Properties-Dateien für die Anwendung benennen und befüllen zu können. Der Vorteil, einfach die InkassoConfig einbinden zu können, ohne zu wissen, welche abhängigen Komponenten noch importiert werden, ist dahin. Properties der Kategorie B jedoch hängen in der Regel nur von der Stage und von der Laufzeitumgebung ab, nicht aber von der konkreten Anwendung. Die Properties könnten also anwendungsübergreifend definiert werden, so dass der Anwendungsentwickler sich nicht mehr darum kümmern muss, diese zu setzen. Properties der Kategorie A dagegen hängen nur von der Anwendung ab, sind aber auch sehr viel seltener. Häufig ist es möglich, Defaults zu definieren, die in den meisten Fällen gültig sind.

Meistens existieren in Unternehmen deshalb individuelle Ersetzungsprozesse, die vom Operations-Team verwaltet werden und im Endeffekt dafür sorgen, dass Properties, die an einer dedizierten Stelle in der Deployment-Einheit verpackt sind, beim Deployment angepasst werden. Die Werte dazu werden vom Operations-Team sicher verwaltet. Die dedizierte Stelle kann zum Beispiel ein eigenes Properties-Jar sein, das alle in der Anwendung verwendeten Properties-Files enthält. Auch hier ist natürlich der Build- und Deploymentprozess etwas komplizierter, und auch der dritte Punkt der obigen Liste greift: der Entwickler muss alle Properties verwalten, die von irgendeiner Komponente in der Anwendung verlangt werden.

Properties und die Fachkomponentenarchitektur

Die Variante, die ich jetzt vorschlage, benötigt weder ein eigenes Property-Jar noch einen besonderen Build- und Deploymentprozess. Die Properties sind revisionierbar und ein Entwickler einer Anwendung muss sich mit den Properties der Kategorie B eingebundener Fachkomponenten nicht befassen.

Jede Fachkomponente packt ihre Properties-Files mit in das auszuliefernde Jar. Diese Dateien enthalten Default-Werte der Properties. Ist es nicht möglich, einen sinnvollen Default anzugeben, so wird die Property weggelassen.
Jede Fachkomponente beschreibt in ihrer API, welche Properties es gibt und gibt dabei auch den Default an.
Properties hängen von drei Dimensionen ab:

  • Stage (dev, integration, production)
  • Laufzeitumgebung (WebSphere, Tomcat, standalone (z.B. JUnit-Integrations-Test))
  • Anwendung (Inkasso-Rest, Partner-Batch etc.)

Für Properties wird eine Datenbank eingerichtet, die die folgenden fünf Spalten hat (für Revisionierbarkeit können weitere hinzukommen):

  • stage
  • runtime
  • application
  • key
  • value

Daten in dieser Datenbank dürfen nur mit speziellen Berechtigungen verändert werden, entweder manuell oder über ein Admin-Tool. Anwendungen haben immer nur lesenden Zugriff.
Bei der Entwicklung einer Fach- oder Infrastrukturkomponente werden für Properties der Kategorie B pro Stage und Laufzeitumgebung Default-Werte in der Datenbank gesetzt. Dabei wird die Spalte application immer mit dem Wert default gefüllt.

Für den Zugriff auf die Properties in der Datenbank entwickeln wir eine eigene PropertySource (siehe diesen Artikel für eine Einführung in PropertySources):

public class DatabaseReaderDelegate {
 
	private JdbcTemplate jdbcTemplate;
	private String stage;
	private String runtime;
	private String application;
 
	private static final String SQL = "SELECT p.value FROM PROPERTYTABLE p WHERE stage = ? AND runtime = ? AND application = ? AND key = ?";
 
	public DatabaseReaderDelegate(DataSource dataSource, String stage,
			String runtime, String application) {
		jdbcTemplate = new JdbcTemplate(dataSource);
		this.stage = stage;
		this.runtime = runtime;
		this.application = application;
	}
 
	public String getProperty(String property) {
		String value = null;
		try {
			value = jdbcTemplate.queryForObject(SQL, String.class, stage,
					runtime, application, property);
		} catch (EmptyResultDataAccessException e) {
			try {
				value = jdbcTemplate.queryForObject(SQL, String.class, stage,
						runtime, "default", property);
			} catch (EmptyResultDataAccessException e2) {
				// nothing to do
			}
		}
		return value;
	}
 
}
 
public class DatabasePropertySource extends
		PropertySource<DatabaseReaderDelegate> {
 
	public DatabasePropertySource(DataSource dataSource, String stage,
			String runtime, String application) {
		super("database_propertysource", new DatabaseReaderDelegate(dataSource,
				stage, runtime, application));
	}
 
	@Override
	public Object getProperty(String key) {
		return this.source.getProperty(key);
	}
 
}

Diese PropertySource benötigt eine DataSource und die Informationen über stage, runtime und application. Wenn eine Property angefragt wird, schaut sie zuerst nach, ob es einen Eintrag speziell für application, stage und runtime gibt und gibt den zurück. Gibt es keinen solchen Eintrag, wird noch geprüft, ob es einen Default-Eintrag für stage und runtime gibt. Gibt es den auch nicht, wird null zurückgegeben, das Zeichen dafür, dass diese PropertySource keinen Wert für die Property liefern kann.
Die DatabasePropertySource wird nun mit Hilfe eines ApplicationContextInitializer am ApplicationContext gesetzt.

public class CustomApplicationContextInitializer implements
		ApplicationContextInitializer<ConfigurableApplicationContext> {
	public void initialize(ConfigurableApplicationContext ctx) {
		String stage = System.getProperty("de.codecentric.stage");
		String runtime = System.getProperty("de.codecentric.runtime");
		String application = System.getProperty("de.codecentric.application");
		String dbURL = System.getProperty("de.codecentric.db.url");
		String dbUser = System.getProperty("de.codecentric.db.user");
		String dbPassword = System.getProperty("de.codecentric.db.password");
		ctx.getEnvironment().setActiveProfiles(runtime);
		BasicDataSource dataSource = new BasicDataSource();
		dataSource.setUrl(dbURL);
		dataSource.setUsername(dbUser);
		dataSource.setPassword(dbPassword);
		DatabasePropertySource databasePropertySource = new DatabasePropertySource(
				dataSource, stage, runtime, application);
		ctx.getEnvironment().getPropertySources()
				.addFirst(databasePropertySource);
	}
}

Neben dem Auslesen der JVM-Properties und des Erstellens der DataSource passieren hier zwei wichtige Dinge:

		ctx.getEnvironment().setActiveProfiles(runtime);

Hier setzen wir den unter runtime eingelesenen Wert als aktives Profil. Das hat zur Folge, dass für Ressourcen die richtigen Konfigurationen herangezogen werden (siehe dazu auch den zweiten Blogpost dieser Reihe).

		ctx.getEnvironment().getPropertySources()
				.addFirst(databasePropertySource);

Und hier wird schließlich unsere DatabasePropertySource als erste PropertySource gesetzt, so dass bei einer Anfrage nach einer Property diese immer zuerst gefragt wird. Erst wenn die DatabasePropertySource keinen Wert liefern kann, werden weitere PropertySources gefragt. Dazu gehören dann in erster Linie die Default-Properties-Files, die mit im Jar der jeweiligen Komponente verpackt sind.
In einer Web-Anwendung kann dieser ApplicationContextInitializer übrigens über einen ServletContext-Parameter eingebunden werden:

<context-param>
    <param-name>contextInitializerClasses</param-name>
    <param-value>de.codecentric.CustomApplicationContextInitializer</param-value>
</context-param>

Für JUnit-Integrations-Tests kann das Spring Testframework so erweitert werden, dass der ApplicationContextInitializer eingebunden wird.
Natürlich gibt es noch Optimierungspotenzial in den vorgestellten Sourcen, so fehlt ein Caching, der runtime – Wert kann sicher irgendwie intelligent ohne JVM-Property ermittelt werden, der application – Wert ist als JVM-Property sogar unpraktisch, wenn man mehrere Anwendungen in einem Server haben möchte, die DataSource könnte auch per JNDI ermittelt werden und für den Fall, dass man keine findet, auf JVM-Properties zurückfallen und so weiter und so fort. Wichtig ist, dass das Konzept klar ist.

Fazit

Das Auslesen von Properties ist Infrastrukturcode und wird deswegen von der Businesslogik separiert, indem Properties in Configuration-Klassen mit Hilfe des Spring-Environments ausgelesen und per Dependency Injection an Businesskomponenten gesetzt werden.
Durch die Erstellung einer eigenen DatabasePropertySource erhalten wir einen einfachen Build- und Deploymentprozess ohne aufwändige Ersetzungen. Properties sind auf einfache Weise revisionierbar. Default-Werte sorgen dafür, dass der Entwickler einer Anwendung im Regelfall keine Properties setzen muss. Er hat aber dennoch alle Freiheiten, Properties nach seinen Wünschen zu überschreiben.
Bei einer Erstellung der web.xml durch einen Maven-Archetype funktioniert das Konzept out-of-the-box.

Vervollständigung des Beispiels

Im vorherigen Blogpost habe ich die LowLevelDataAccess-Konfigurationen ohne Properties vorgestellt. So sehen sie nun mit Properties aus:

@Profile("websphere")
@Configuration
public class JndiDataAccessConfig implements LowLevelDataAccessConfig {
 
	@Autowired
	private Environment env;
 
	@Bean
	public DataSource dataSource() throws Exception {
		InitialContext initialContext = new InitialContext();
		return (DataSource) initialContext.lookup(env
				.getProperty("infrastructure.db.jndi"));
	}
 
	@Bean
	public PlatformTransactionManager transactionManager() {
		return new WebSphereUowTransactionManager();
	}
 
}
 
@Profile("standalone")
@Configuration
public class StandaloneDataAccessConfig implements LowLevelDataAccessConfig {
 
	@Autowired
	private Environment env;
 
	@Bean
	public DataSource dataSource() {
		BasicDataSource dataSource = new BasicDataSource();
		dataSource.setUrl(env.getProperty("infrastructure.db.url"));
		dataSource.setUsername(env.getProperty("infrastructure.db.user"));
		dataSource.setPassword(env.getProperty("infrastructure.db.password"));
		return dataSource;
	}
 
	@Bean
	public PlatformTransactionManager transactionManager() {
		return new DataSourceTransactionManager(dataSource());
	}
 
}

Da es nicht möglich ist, sinnvolle allgemeingültige Defaults für die hier abgefragten Properties anzugeben, wird auch kein Properties-File herangezogen, das sich im Jar befindet. Die Properties müssen sich also in der Datenbank befinden oder durch eine andere, weitere PropertySource hinzugefügt werden.

Wie aufwändig ist es nun, eine Web-Anwendung zu konfigurieren, die die Services aus InkassoService anbietet?
Die Web-Anwendung wird mit dem Maven-Archetype erstellt, der die Konfiguration der DatabasePropertySource in der web.xml bereits enthält.
Es gibt fünf relevante Properties:

  • partner.readonly -> Default false ist in der partner.properties eingetragen, genügt in diesem Fall.
  • infrastructure.db.jndi -> Defaults für alle stages und relevanten runtimes sind in der Datenbank eingetragen, genügt in diesem Fall.
  • infrastructure.db.user -> Defaults für alle stages und relevanten runtimes sind in der Datenbank eingetragen, genügt in diesem Fall.
  • infrastructure.db.url -> Defaults für alle stages und relevanten runtimes sind in der Datenbank eingetragen, genügt in diesem Fall.
  • infrastructure.db.password -> Defaults für alle stages und relevanten runtimes sind in der Datenbank eingetragen, genügt in diesem Fall.

Der Entwickler kann also einfach den InkassoService per InkassoConfig einbinden, ohne noch weiter aktiv werden zu müssen.
Und wenn er doch möchte, kann er für seine Anwendung jede Property überschreiben, indem er einen entsprechenden Datenbankeintrag hinzufügt.

Tobias Flohre

Tobias Flohre arbeitet als Senior-Softwareentwickler/Architekt bei der codecentric AG. Seine Schwerpunkte sind Java-Enterprise-Anwendungen und Architekturen mit JEE/Spring. Er ist Autor diverser Artikel und schreibt regelmäßig Blogbeiträge zu den Themen Architektur und Spring. Zurzeit beschäftigt er sich mit Integrations- und Batch-Themen im Großunternehmen sowie mit modernen Webarchitekturen.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.