Java >> Java Tutorial >  >> Tag >> Spring

Spring from the Trenches:Injizieren von Eigenschaftswerten in Configuration Beans

Spring Framework bietet eine gute Unterstützung für das Einfügen von Eigenschaftswerten, die aus Eigenschaftendateien in Bean oder @Configuration gefunden wurden Klassen. Wenn wir jedoch einzelne Eigenschaftswerte in diese Klassen einfügen, werden wir auf einige Probleme stoßen.

Dieser Blogbeitrag identifiziert diese Probleme und beschreibt, wie wir sie lösen können.

Fangen wir an.

Es ist einfach, aber nicht problemlos

Wenn wir individuelle Eigenschaftswerte in unsere Bean-Klassen einfügen, werden wir auf die folgenden Probleme stoßen:

1. Das Einfügen mehrerer Eigenschaftswerte ist umständlich

Wenn wir individuelle Property-Werte einfügen, indem wir @Value verwenden Anmerkung oder rufen Sie die Eigenschaftswerte mithilfe einer Umgebung ab -Objekt ist das Einfügen mehrerer Eigenschaftswerte umständlich.

Nehmen wir an, dass wir einige Eigenschaftswerte in einen UrlBuilder einfügen müssen Objekt. Dieses Objekt benötigt drei Eigenschaftswerte:

  • Der Host des Servers (app.server.host )
  • Der Port, der vom Server überwacht wird (app.server.port )
  • Das verwendete Protokoll (app.server.protocol )

Diese Eigenschaftswerte werden verwendet, wenn der UrlBuilder Objekt erstellt URL-Adressen, die für den Zugriff auf verschiedene Funktionen unserer Webanwendung verwendet werden.

Wenn wir diese Eigenschaftswerte mithilfe der Konstruktorinjektion und der @Value Annotation, der Quellcode des UrlBuilder Klasse sieht wie folgt aus:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class UrlBuilder {

	private final String host;
	private final String port;
	private final String protocol;

	@Autowired
	public UrlBuilder(@Value("${app.server.protocol}") String protocol,
                         @Value("${app.server.host}") String serverHost,
                         @Value("${app.server.port}") int serverPort) {
        this.protocol = protocol.toLowercase();
        this.serverHost = serverHost;
        this.serverPort = serverPort;
    }
}

Wenn wir diese Eigenschaftswerte mithilfe der Konstruktorinjektion und der Umgebung einfügen Klasse, der Quellcode des UrlBuilder Klasse sieht wie folgt aus:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Component
public class UrlBuilder {

	private final String host;
	private final String port;
	private final String protocol;

	@Autowired
	public UrlBuilder(Environment env) {
        this.protocol = env.getRequiredProperty("app.server.protocol").toLowercase();
        this.serverHost = env.getRequiredProperty("app.server.host");
        this.serverPort = env.getRequiredProperty("app.server.port", Integer.class);
    }
}

Ich gebe zu, das sieht gar nicht so schlecht aus. Wenn jedoch die Anzahl der erforderlichen Eigenschaftswerte wächst und/oder unsere Klasse auch andere Abhängigkeiten hat, ist es umständlich, sie alle einzufügen.

2. Wir müssen die Eigenschaftsnamen mehr als einmal angeben (oder denken Sie daran, Konstanten zu verwenden)

Wenn wir einzelne Eigenschaftswerte direkt in die Beans einfügen, die sie benötigen, und mehr als eine Bean (A und B) denselben Eigenschaftswert benötigen, fällt uns als erstes ein, die Eigenschaftsnamen in beiden Bean-Klassen anzugeben:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class A {

	private final String protocol;

	@Autowired
	public A(@Value("${app.server.protocol}") String protocol) {
		this.protocol = protocol.toLowercase();
    }
}

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class B {

	private final String protocol;

	@Autowired
	public B(@Value("${app.server.protocol}") String protocol) {
		this.protocol = protocol.toLowercase();
    }
}

Dies ist ein Problem, weil

  1. Weil wir Menschen sind, machen wir Tippfehler . Dies ist kein großes Problem, da wir es bemerken werden, wenn wir unsere Anwendung starten. Trotzdem bremst es uns aus.
  2. Es erschwert die Wartung . Wenn wir den Namen einer Eigenschaft ändern, müssen wir diese Änderung an jeder Klasse vornehmen, die sie verwendet.

Wir können dieses Problem beheben, indem wir die Eigenschaftsnamen in eine konstante Klasse verschieben. Wenn wir dies tun, sieht unser Quellcode wie folgt aus:

public final class PropertyNames {

	private PropertyNames() {}
	
	public static final String PROTOCOL = "${app.server.protocol}";
}

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class A {

	private final String protocol;

	@Autowired
	public A(@Value(PropertyNames.PROTOCOL) String protocol) {
		this.protocol = protocol.toLowercase();
    }
}

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class B {

	private final String protocol;

	@Autowired
	public B(@Value(PropertyNames.PROTOCOL) String protocol) {
		this.protocol = protocol.toLowercase();
    }
}

Dies behebt das Wartungsproblem, aber nur, wenn alle Entwickler daran denken, es zu verwenden. Wir können dies natürlich erzwingen, indem wir Code-Reviews verwenden, aber dies ist eine weitere Sache, an die der Reviewer denken muss.

3. Das Hinzufügen von Validierungslogik wird zu einem Problem

Nehmen wir an, wir haben zwei Klassen (A und B ), die den Wert von app.server.protocol benötigen Eigentum. Wenn wir diesen Eigenschaftswert direkt in die A einfügen und B Beans, und wir wollen sicherstellen, dass der Wert dieser Eigenschaft 'http' oder 'https' ist, müssen wir entweder

  1. Fügen Sie die Validierungslogik zu beiden Bean-Klassen hinzu.
  2. Fügen Sie die Validierungslogik zu einer Utility-Klasse hinzu und verwenden Sie sie, wenn wir überprüfen müssen, ob das richtige Protokoll angegeben ist.

Wenn wir die Validierungslogik zu beiden Bean-Klassen hinzufügen, sieht der Quellcode dieser Klassen wie folgt aus:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class A {

	private final String protocol;

	@Autowired
	public A(@Value("${app.server.protocol}") String protocol) {
		checkThatProtocolIsValid(protocol);
		this.protocol = protocol.toLowercase();
    }
	
	private void checkThatProtocolIsValid(String protocol) {
		if (!protocol.equalsIgnoreCase("http") && !protocol.equalsIgnoreCase("https")) {
			throw new IllegalArgumentException(String.format(
				"Protocol: %s is not allowed. Allowed protocols are: http and https.",
				protocol
			));
		}
	}
}

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class B {

	private final String protocol;

	@Autowired
	public B(@Value("${app.server.protocol}") String protocol) {
		checkThatProtocolIsValid(protocol);
		this.protocol = protocol.toLowercase();
    }
	
	private void checkThatProtocolIsValid(String protocol) {
		if (!protocol.equalsIgnoreCase("http") && !protocol.equalsIgnoreCase("https")) {
			throw new IllegalArgumentException(String.format(
				"Protocol: %s is not allowed. Allowed protocols are: http and https.",
				protocol
			));
		}
	}
}

Dies ist ein Wartungsproblem, weil A und B Klassen enthalten Copy-Paste-Code. Wir können die Situation etwas verbessern, indem wir die Validierungslogik in eine Utility-Klasse verschieben und sie verwenden, wenn wir ein neues A erstellen und B Objekte.

Nachdem wir dies getan haben, sieht unser Quellcode wie folgt aus:

public final class ProtocolValidator {

	private ProtocolValidator() {}
	
	public static void checkThatProtocolIsValid(String protocol) {
		if (!protocol.equalsIgnoreCase("http") && !protocol.equalsIgnoreCase("https")) {
			throw new IllegalArgumentException(String.format(
				"Protocol: %s is not allowed. Allowed protocols are: http and https.",
				protocol
			));
		}
	}
}

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class A {

	private final String protocol;

	@Autowired
	public A(@Value("${app.server.protocol}") String protocol) {
		ProtocolValidator.checkThatProtocolIsValid(protocol);
		this.protocol = protocol.toLowercase();
    }
}

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class B {

	private final String protocol;

	@Autowired
	public B(@Value("${app.server.protocol}") String protocol) {
		ProtocolValidator.checkThatProtocolIsValid(protocol);
		this.protocol = protocol.toLowercase();
    }
}

Das Problem ist, dass wir immer noch daran denken müssen, diese Utility-Methode aufzurufen. Wir können dies natürlich erzwingen, indem wir Codeüberprüfungen verwenden, aber noch einmal, dies ist eine weitere Sache, die der Überprüfer überprüfen muss.

4. Wir können keine gute Dokumentation schreiben

Wir können keine gute Dokumentation schreiben, die die Konfiguration unserer Anwendung beschreibt, da wir diese Dokumentation zu den eigentlichen Eigenschaftendateien hinzufügen, ein Wiki verwenden oder ein *keuch* Word-Dokument schreiben müssen.

Jede dieser Optionen verursacht Probleme, da wir sie nicht gleichzeitig mit dem Schreiben von Code verwenden können, der Eigenschaftswerte aus unseren Eigenschaftendateien benötigt. Wenn wir unsere Dokumentation lesen müssen, müssen wir "ein externes Dokument" öffnen und dies verursacht einen Kontextwechsel, der sehr teuer sein kann.

Lassen Sie uns weitermachen und herausfinden, wie wir diese Probleme lösen können.

Einfügen von Eigenschaftswerten in Konfigurations-Beans

Wir können die zuvor erwähnten Probleme lösen, indem wir die Eigenschaftswerte in Konfigurations-Beans einfügen. Beginnen wir damit, eine einfache Eigenschaftendatei für unsere Beispielanwendung zu erstellen.

Erstellen der Eigenschaftendatei

Das erste, was wir tun müssen, ist, eine Eigenschaftendatei zu erstellen. Die Eigenschaftendatei unserer Beispielanwendung heißt application.properties , und es sieht so aus:

app.name=Configuration Properties example
app.production.mode.enabled=false

app.server.port=8080
app.server.protocol=http
app.server.host=localhost

Fahren wir fort und konfigurieren den Anwendungskontext unserer Beispielanwendung.

Konfigurieren des Anwendungskontexts

Die Anwendungskontext-Konfigurationsklasse unserer Beispielanwendung hat zwei Ziele:

  1. Aktivieren Sie Spring MVC und importieren Sie die Standardkonfiguration.
  2. Stellen Sie sicher, dass die Eigenschaftswerte aus application.properties gefunden werden Datei gelesen und kann in Spring Beans injiziert werden.

Wir können sein zweites zweites Ziel erreichen, indem wir diesen Schritten folgen:

  1. Konfigurieren Sie den Spring-Container so, dass er alle Pakete scannt, die Bean-Klassen enthalten.
  2. Stellen Sie sicher, dass die Eigenschaftswerte aus application.properties gefunden werden Datei gelesen und der Spring Umgebung hinzugefügt .
  3. Stellen Sie sicher, dass ${…} Platzhalter aus @Value gefunden Anmerkungen werden durch Eigenschaftswerte ersetzt, die aus der aktuellen Umgebung von Spring gefunden wurden und seine PropertySources .

Der Quellcode der WebAppContext-Klasse sieht wie folgt aus:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@ComponentScan({
        "net.petrikainulainen.spring.trenches.config",
        "net.petrikainulainen.spring.trenches.web"
})
@EnableWebMvc
@PropertySource("classpath:application.properties")
public class WebAppContext {

	/**
	 * Ensures that placeholders are replaced with property values
	 */
    @Bean
    static PropertySourcesPlaceholderConfigurer propertyPlaceHolderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

Unser nächster Schritt besteht darin, die Konfigurations-Bean-Klassen zu erstellen und die aus unserer Eigenschaftendatei gefundenen Eigenschaftswerte in sie einzufügen. Lassen Sie uns herausfinden, wie wir das tun können.

Erstellen der Konfigurations-Bean-Klassen

Lassen Sie uns zwei Konfigurations-Bean-Klassen erstellen, die im Folgenden beschrieben werden:

  • Die Webeigenschaften Die Klasse enthält die Eigenschaftswerte, die das verwendete Protokoll, den Host des Servers und den vom Server überwachten Port konfigurieren.
  • Die Anwendungseigenschaften Die Klasse enthält die Eigenschaftswerte, die den Namen der Anwendung konfigurieren und angeben, ob der Produktionsmodus aktiviert ist. Es hat auch einen Verweis auf WebProperties Objekt.

Zuerst , müssen wir die WebProperties erstellen Klasse. Wir können dies tun, indem wir diesen Schritten folgen:

  1. Erstellen Sie die Webeigenschaften Klasse und kommentieren Sie es mit @Component Anmerkung.
  2. Endgültiges Protokoll hinzufügen , serverHost und serverPort Felder zur erstellten Klasse.
  3. Fügen Sie die Eigenschaftswerte mithilfe der Konstruktorinjektion in diese Felder ein und stellen Sie sicher, dass der Wert des Protokolls Feld muss entweder 'http' oder 'https' sein (Groß-/Kleinschreibung ignorieren).
  4. Fügen Sie Getter hinzu, die verwendet werden, um die tatsächlichen Eigenschaftswerte zu erhalten.

Der Quellcode der WebProperties Klasse sieht wie folgt aus:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public final class WebProperties {

    private final String protocol;

    private final String serverHost;

    private final int serverPort;

    @Autowired
    public WebProperties(@Value("${app.server.protocol}") String protocol,
                         @Value("${app.server.host}") String serverHost,
                         @Value("${app.server.port}") int serverPort) {
        checkThatProtocolIsValid(protocol);

        this.protocol = protocol.toLowercase();
        this.serverHost = serverHost;
        this.serverPort = serverPort;
    }

    private void checkThatProtocolIsValid(String protocol) {
        if (!protocol.equalsIgnoreCase("http") && !protocol.equalsIgnoreCase("https")) {
            throw new IllegalArgumentException(String.format(
                    "Protocol: %s is not allowed. Allowed protocols are: http and https.",
                    protocol
            ));
        }
    }

    public String getProtocol() {
        return protocol;
    }

    public String getServerHost() {
        return serverHost;
    }

    public int getServerPort() {
        return serverPort;
    }
}

Zweiter , Wir müssen die ApplicationProperties implementieren Klasse. Wir können dies tun, indem wir diesen Schritten folgen:

  1. Erstellen Sie die ApplicationProperties Klasse und kommentieren Sie sie mit @Component Anmerkung.
  2. Endgültigen Namen hinzufügen , productionModeEnabled und webProperties Felder zur erstellten Klasse.
  3. Fügen Sie die Eigenschaftswerte und die WebProperties ein Bean in die ApplicationProperties Bean durch Konstruktorinjektion.
  4. Fügen Sie Getter hinzu, die zum Abrufen der Feldwerte verwendet werden.

Der Quellcode der ApplicationProperties Klasse sieht wie folgt aus:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public final class ApplicationProperties {

    private final String name;

    private final boolean productionModeEnabled;

    private final WebProperties webProperties;

    @Autowired
    public ApplicationProperties(@Value("${app.name}") String name,
                                 @Value("${app.production.mode.enabled:false}") boolean productionModeEnabled,
                                 WebProperties webProperties) {
        this.name = name;
        this.productionModeEnabled = productionModeEnabled;
        this.webProperties = webProperties;
    }

    public String getName() {
        return name;
    }

    public boolean isProductionModeEnabled() {
        return productionModeEnabled;
    }

    public WebProperties getWebProperties() {
        return webProperties;
    }
}

Fahren wir fort und finden Sie heraus, was die Vorteile dieser Lösung sind.

Wie hilft uns das?

Wir haben jetzt die Bean-Klassen erstellt, die die in application.properties gefundenen Eigenschaftswerte enthalten Datei. Diese Lösung mag wie ein Overengineering erscheinen, aber sie hat die folgenden Vorteile gegenüber der traditionellen und einfachen Methode:

1. Wir können nur eine Bean statt mehrerer Eigenschaftswerte einfügen

Wenn wir die Eigenschaftswerte in eine Konfigurations-Bean einfügen und dann diese Konfigurations-Bean in den UrlBuilder einfügen Klasse durch Konstruktorinjektion, sieht ihr Quellcode wie folgt aus:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class UrlBuilder {

	private final WebProperties properties;

	@Autowired
	public UrlBuilder(WebProperties properties) {
		this.properties = properties;
    }
}

Wie wir sehen können, macht dies unseren Code sauberer (insbesondere wenn wir Konstruktorinjektion verwenden).

2. Wir müssen die Eigenschaftsnamen nur einmal angeben

Wenn wir die Eigenschaftswerte in die Konfigurations-Beans einfügen, müssen wir die Eigenschaftsnamen nur an einer Stelle angeben. Das bedeutet, dass

  • Unser Kodex folgt dem Grundsatz der Trennung von Anliegen. Die Eigenschaftsnamen werden von den Konfigurations-Beans gefunden, und die anderen Beans, die diese Informationen benötigen, wissen nicht, woher sie kommen. Sie benutzen es einfach.
  • Unser Kodex folgt dem Prinzip „Wiederhole dich nicht“. Da die Eigenschaftsnamen nur an einer Stelle (in den Konfigurations-Beans) angegeben werden, ist unser Code einfacher zu warten.

Außerdem sieht (IMO) unser Code auch viel sauberer aus:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class A {

	private final String protocol;

	@Autowired
	public A(WebProperties properties) {
		this.protocol = properties.getProtocol();
    }
}

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class B {

	private final String protocol;

	@Autowired
	public B(WebProperties properties) {
		this.protocol = properties.getProtocol();
    }
}

3. Wir müssen die Validierungslogik nur einmal schreiben

Wenn wir Eigenschaftswerte in die Konfigurations-Beans einfügen, können wir die Validierungslogik zu den Konfigurations-Beans hinzufügen, und die anderen Beans müssen nichts davon wissen. Dieser Ansatz hat drei Vorteile:

  • Unser Code folgt dem Separation-of-Concern-Prinzip, da die Validierungslogik in den Konfigurations-Beans gefunden wird (wo sie hingehört). Die anderen Bohnen müssen davon nichts wissen.
  • Unser Code folgt dem „Don’t repeat yourself“-Prinzip, da die Validierungslogik an einem Ort gefunden wird.
  • Wir müssen nicht daran denken, die Validierungslogik aufzurufen, wenn wir neue Bean-Objekte erstellen, weil wir Validierungsregeln erzwingen können, wenn die Konfigurations-Beans erstellt werden.

Außerdem sieht unser Quellcode auch viel sauberer aus:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class A {

	private final String protocol;

	@Autowired
	public A(WebProperties properties) {
		this.protocol = properties.getProtocol();
    }
}

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class B {

	private final String protocol;

	@Autowired
	public B(WebProperties properties) {
		this.protocol = properties.getProtocol();
    }
}

4. Wir können von unserer IDE aus auf die Dokumentation zugreifen

Wir können die Konfiguration unserer Anwendung dokumentieren, indem wir unseren Konfigurations-Beans Javadoc-Kommentare hinzufügen. Nachdem wir dies getan haben, können wir über unsere IDE auf diese Dokumentation zugreifen, wenn wir Code schreiben, der diese Eigenschaftswerte benötigt. Wir müssen keine weitere Datei öffnen oder eine Wiki-Seite lesen. Wir können einfach weiter Code schreiben und die Kosten für den Kontextwechsel vermeiden.

Fahren wir fort und fassen zusammen, was wir aus diesem Blogpost gelernt haben.

Zusammenfassung

Dieser Blogbeitrag hat uns gelehrt, dass das Einfügen von Eigenschaftswerten in Konfigurations-Beans:

  • Hilft uns, das Prinzip der Trennung von Anliegen einzuhalten. Die Dinge, die Konfigurationseigenschaften und die Validierung der Eigenschaftswerte betreffen, sind in unseren Konfigurations-Beans gekapselt. Das bedeutet, dass die Beans, die diese Konfigurations-Beans verwenden, nicht wissen, woher die Eigenschaftswerte kommen oder wie sie validiert werden.
  • Hilft uns dabei, dem „Don’t repeat yourself“-Prinzip zu folgen, weil 1) wir die Eigenschaftsnamen nur einmal angeben müssen und 2) wir die Validierungslogik zu den Konfigurations-Beans hinzufügen können.
  • Erleichtert den Zugriff auf unsere Dokumentation.
  • Erleichtert das Schreiben, Lesen und Verwalten unseres Codes.

Es hilft uns jedoch nicht, die Laufzeitkonfiguration unserer Anwendung herauszufinden. Wenn wir diese Informationen benötigen, müssen wir die auf unserem Server gefundene Eigenschaftendatei lesen. Das ist umständlich.

Wir werden dieses Problem in meinem nächsten Blogbeitrag lösen.

P.S. Sie können die Beispielanwendung dieses Blogbeitrags von Github erhalten.


Java-Tag