Beliebte Suchanfragen

Cloud Native

DevOps

IT-Security

Agile Methoden

Java

|
//

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

19.1.2012 | 6 Minuten Lesezeit

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.

1public class PartnerServiceImpl implements PartnerService {
2 
3    private boolean readOnly;
4 
5    private JdbcTemplate jdbcTemplate;
6 
7    public PartnerServiceImpl(JdbcTemplate jdbcTemplate, boolean readOnly) {
8        this.jdbcTemplate = jdbcTemplate;
9        this.readOnly = readOnly;
10    }
11 
12    public void savePartner(Partner partner) {
13        if (readOnly) {
14            throw new IllegalStateException(
15                    "Persisting partner not allowed in read-only mode!");
16        }
17        // save Partner
18    }
19 
20    public Partner getPartner(long id) {
21        return this.jdbcTemplate.queryForObject("SELECT ....",
22                new PartnerRowMapper, id);
23    }
24 
25}

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:

1@Import(HighLevelDataAccessConfig.class)
2@PropertySource("classpath:partner.properties")
3@Configuration
4public class PartnerConfig {
5 
6    @Autowired
7    private Environment environment;
8 
9    @Autowired
10    private HighLevelDataAccessConfig dataAccessConfig;
11 
12    @Bean
13    public PartnerService partnerService() throws Exception {
14        return new PartnerServiceImpl(dataAccessConfig.jdbcTemplate(),
15                environment.getProperty("partner.readonly", boolean.class));
16    }
17 
18}

Hier die neuen Elemente im Einzelnen:

1@PropertySource("classpath:partner.properties")

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

1@Autowired
2    private Environment environment;

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

1environment.getProperty("partner.readonly", boolean.class)
1public class DatabaseReaderDelegate {
2 
3    private JdbcTemplate jdbcTemplate;
4    private String stage;
5    private String runtime;
6    private String application;
7 
8    private static final String SQL = "SELECT p.value FROM PROPERTYTABLE p WHERE stage = ? AND runtime = ? AND application = ? AND key = ?";
9 
10    public DatabaseReaderDelegate(DataSource dataSource, String stage,
11            String runtime, String application) {
12        jdbcTemplate = new JdbcTemplate(dataSource);
13        this.stage = stage;
14        this.runtime = runtime;
15        this.application = application;
16    }
17 
18    public String getProperty(String property) {
19        String value = null;
20        try {
21            value = jdbcTemplate.queryForObject(SQL, String.class, stage,
22                    runtime, application, property);
23        } catch (EmptyResultDataAccessException e) {
24            try {
25                value = jdbcTemplate.queryForObject(SQL, String.class, stage,
26                        runtime, "default", property);
27            } catch (EmptyResultDataAccessException e2) {
28                // nothing to do
29            }
30        }
31        return value;
32    }
33 
34}
35 
36public class DatabasePropertySource extends
37        PropertySource<DatabaseReaderDelegate> {
38 
39    public DatabasePropertySource(DataSource dataSource, String stage,
40            String runtime, String application) {
41        super("database_propertysource", new DatabaseReaderDelegate(dataSource,
42                stage, runtime, application));
43    }
44 
45    @Override
46    public Object getProperty(String key) {
47        return this.source.getProperty(key);
48    }
49 
50}

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.

1public class CustomApplicationContextInitializer implements
2        ApplicationContextInitializer<ConfigurableApplicationContext> {
3    public void initialize(ConfigurableApplicationContext ctx) {
4        String stage = System.getProperty("de.codecentric.stage");
5        String runtime = System.getProperty("de.codecentric.runtime");
6        String application = System.getProperty("de.codecentric.application");
7        String dbURL = System.getProperty("de.codecentric.db.url");
8        String dbUser = System.getProperty("de.codecentric.db.user");
9        String dbPassword = System.getProperty("de.codecentric.db.password");
10        ctx.getEnvironment().setActiveProfiles(runtime);
11        BasicDataSource dataSource = new BasicDataSource();
12        dataSource.setUrl(dbURL);
13        dataSource.setUsername(dbUser);
14        dataSource.setPassword(dbPassword);
15        DatabasePropertySource databasePropertySource = new DatabasePropertySource(
16                dataSource, stage, runtime, application);
17        ctx.getEnvironment().getPropertySources()
18                .addFirst(databasePropertySource);
19    }
20}

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

1ctx.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 ).

1ctx.getEnvironment().getPropertySources()
2                .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:

1<context-param>
2    <param-name>contextInitializerClasses</param-name>
3    <param-value>de.codecentric.CustomApplicationContextInitializer</param-value>
4</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:

1@Profile("websphere")
2@Configuration
3public class JndiDataAccessConfig implements LowLevelDataAccessConfig {
4 
5    @Autowired
6    private Environment env;
7 
8    @Bean
9    public DataSource dataSource() throws Exception {
10        InitialContext initialContext = new InitialContext();
11        return (DataSource) initialContext.lookup(env
12                .getProperty("infrastructure.db.jndi"));
13    }
14 
15    @Bean
16    public PlatformTransactionManager transactionManager() {
17        return new WebSphereUowTransactionManager();
18    }
19 
20}
21 
22@Profile("standalone")
23@Configuration
24public class StandaloneDataAccessConfig implements LowLevelDataAccessConfig {
25 
26    @Autowired
27    private Environment env;
28 
29    @Bean
30    public DataSource dataSource() {
31        BasicDataSource dataSource = new BasicDataSource();
32        dataSource.setUrl(env.getProperty("infrastructure.db.url"));
33        dataSource.setUsername(env.getProperty("infrastructure.db.user"));
34        dataSource.setPassword(env.getProperty("infrastructure.db.password"));
35        return dataSource;
36    }
37 
38    @Bean
39    public PlatformTransactionManager transactionManager() {
40        return new DataSourceTransactionManager(dataSource());
41    }
42 
43}

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.

|

Beitrag teilen

Gefällt mir

0

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.