Статьи

Весна из окопов: введение значений свойств в бины конфигурации

Spring Framework имеет хорошую поддержку для вставки значений свойств, найденных в файлах свойств, в классы bean или @Configuration . Однако, если мы добавим отдельные значения свойств в эти классы, мы столкнемся с некоторыми проблемами.

Этот пост в блоге определяет эти проблемы и описывает, как мы можем их решить.

Давайте начнем.

Если вы используете Spring Boot, вы должны использовать его свойства безопасной конфигурации. Вы можете получить больше информации об этом на следующих веб-страницах:

Это просто, но не без проблем

Если мы добавим отдельные значения свойств в наши классы компонентов, мы столкнемся со следующими проблемами:

1. Введение нескольких значений свойств является громоздким

Если мы вводим отдельные значения свойств с помощью аннотации @Value или получаем значения свойств с помощью объекта Environment , внедрение нескольких значений свойств будет затруднительным.

Давайте предположим, что мы должны внедрить некоторые значения свойств в объект UrlBuilder . Этот объект нуждается в трех значениях свойств:

  • Хост сервера ( app.server.host )
  • Порт, который прослушивается сервером ( app.server.port )
  • Используемый протокол ( app.server.protocol )

Эти значения свойств используются, когда объект UrlBuilder создает URL-адреса, которые используются для доступа к различным функциям нашего веб-приложения.

Если мы внедрим эти значения свойств с помощью внедрения конструктора и аннотации @Value , исходный код класса UrlBuilder будет выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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;
    }
}

Дополнительное чтение:

Если мы внедряем эти значения свойств с помощью инжектора конструктора и класса Environment , исходный код класса UrlBuilder выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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);
    }
}

Дополнительное чтение:

Я признаю, что это не выглядит так плохо. Однако, когда число требуемых значений свойств растет и / или наш класс также имеет другие зависимости, внедрение всех из них становится громоздким.

2. Мы должны указать имена свойств более одного раза (или не забывать использовать константы)

Если мы вводим отдельные значения свойств непосредственно в bean-компоненты, которым они нужны, и более чем одному bean-компоненту (A и B) требуется одно и то же значение свойства, первое, что приходит нам в голову, — это указывать имена свойств в обоих классах bean-компонентов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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();
    }
}

Это проблема, потому что

  1. Поскольку мы люди, мы делаем опечатки . Это не большая проблема, потому что мы заметим это при запуске нашего приложения. Тем не менее, это замедляет нас.
  2. Это усложняет обслуживание . Если мы изменим имя свойства, мы должны сделать это для каждого класса, который его использует.

Мы можем решить эту проблему, переместив имена свойств в постоянный класс. Если мы сделаем это, наш исходный код будет выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public final 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();
    }
}

Это решает проблему обслуживания, но только если все разработчики не забывают использовать его. Конечно, мы можем применить это, используя обзоры кода, но это еще одна вещь, которую рецензент должен помнить, чтобы проверять.

3. Добавление логики проверки становится проблемой

Давайте предположим, что у нас есть два класса ( A и B ), которым необходимо значение свойства app.server.protocol . Если мы вводим значение этого свойства непосредственно в компоненты A и B , и мы хотим убедиться, что значение этого свойства равно ‘http’ или ‘https’, мы должны либо

  1. Добавьте логику проверки в оба класса бинов.
  2. Добавьте логику проверки в служебный класс и используйте ее, когда нам нужно проверить, что задан правильный протокол.

Если мы добавим логику проверки в оба класса бинов, исходный код этих классов будет выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
            ));
        }
    }
}

Это проблема обслуживания, потому что классы A и B содержат код для копирования и вставки. Мы можем немного улучшить ситуацию, переместив логику проверки в служебный класс и используя ее при создании новых объектов A и B.

После того, как мы это сделали, наш исходный код выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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();
    }
}

Проблема в том, что нам все еще нужно помнить, чтобы вызывать этот служебный метод. Конечно, мы можем применить это, используя обзоры кода, но еще раз, это еще одна вещь, которую рецензент должен помнить, чтобы проверить.

4. Мы не можем написать хорошую документацию

Мы не можем написать хорошую документацию, которая описывает конфигурацию нашего приложения, потому что мы должны добавить эту документацию к фактическим файлам свойств, использовать вики или написать документ * gasp * Word.

Каждый из этих параметров вызывает проблемы, потому что мы не можем использовать их в то же время, когда пишем код, который требует значений свойств, найденных в наших файлах свойств. Если нам нужно прочитать нашу документацию, нам нужно открыть «внешний документ», и это вызывает переключение контекста, которое может быть очень дорогим .

Давайте двигаться дальше и выясним, как мы можем решить эти проблемы.

Внедрение значений свойств в бины конфигурации

Мы можем решить проблемы, упомянутые ранее, путем введения значений свойств в бины конфигурации. Давайте начнем с создания простого файла свойств для нашего примера приложения.

Создание файла свойств

Первое, что нам нужно сделать, это создать файл свойств. Файл свойств нашего примера приложения называется application.properties и выглядит следующим образом:

1
2
3
4
5
6
app.name=Configuration Properties example
app.production.mode.enabled=false
 
app.server.port=8080
app.server.protocol=http
app.server.host=localhost

Давайте продолжим и настроим контекст приложения нашего примера приложения.

Настройка контекста приложения

Класс конфигурации контекста приложения нашего примера приложения имеет две цели:

  1. Включите Spring MVC и импортируйте его конфигурацию по умолчанию.
  2. Убедитесь, что значения свойств, найденные в файле application.properties , прочитаны и могут быть внедрены в бины Spring.

Мы можем выполнить свою вторую вторую цель, выполнив следующие шаги:

  1. Настройте контейнер Spring для сканирования всех пакетов, содержащих классы bean-компонентов.
  2. Убедитесь, что значения свойств, найденные в файле application.properties , прочитаны и добавлены в среду Spring.
  3. Убедитесь, что заполнители $ {…}, найденные в аннотациях @Value , заменены значениями свойств, найденными в текущей среде Spring и ее PropertySources .

Исходный код класса WebAppContext выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
    PropertySourcesPlaceholderConfigurer propertyPlaceHolderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

Дополнительное чтение:

Нашим следующим шагом будет создание классов bean-компонентов конфигурации и добавление в них значений свойств, найденных в нашем файле свойств. Давайте выясним, как мы можем это сделать.

Создание классов бинов конфигурации

Давайте создадим два класса bean-компонентов конфигурации, которые описаны ниже:

  • Класс WebProperties содержит значения свойств, которые настраивают используемый протокол, хост сервера и порт, который прослушивается сервером.
  • Класс ApplicationProperties содержит значения свойств, которые настраивают имя приложения и определяют, включен ли производственный режим. Он также имеет ссылку на объект WebProperties .

Сначала мы должны создать класс WebProperties . Мы можем сделать это, выполнив следующие действия:

  1. Создайте класс WebProperties, аннотируйте его аннотацией @Component .
  2. Добавьте итоговые поля protocol , serverHost и serverPort в созданный класс.
  3. Вставьте значения свойств в эти поля, используя конструктор, и убедитесь, что значение поля протокола должно быть http или https (игнорируйте регистр).
  4. Добавьте получатели, которые используются для получения фактических значений свойств.

Исходный код класса WebProperties выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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;
    }
}

Во-вторых , мы должны реализовать класс ApplicationProperties . Мы можем сделать это, выполнив следующие действия:

  1. Создайте класс ApplicationProperties и аннотируйте его аннотацией @Component .
  2. Добавьте окончательное имя , productionModeEnabled и поля webProperties к созданному классу.
  3. Вставьте значения свойств и компонент WebProperties в компонент ApplicationProperties с помощью внедрения конструктора.
  4. Добавьте геттеры, которые используются для получения значений поля.

Исходный код класса ApplicationProperties выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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;
    }
}

Давайте продолжим и выясним, каковы преимущества этого решения.

Как это помогает нам?

Теперь мы создали классы bean-компонентов, которые содержат значения свойств, найденные в файле application.properties . Это решение может показаться слишком сложным, но оно обладает следующими преимуществами по сравнению с традиционным и простым способом:

1. Мы можем ввести только один компонент вместо нескольких значений свойств

Если мы вставим значения свойств в компонент конфигурации, а затем внедрим этот компонент конфигурации в класс UrlBuilder с помощью внедрения конструктора, его исходный код будет выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
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;
    }
}

Как мы видим, это делает наш код чище ( особенно если мы используем инжектор конструктора ).

2. Мы должны указать имена свойств только один раз

Если мы вводим значения свойств в бины конфигурации, мы должны указывать имена свойств только в одном месте. Это значит, что

  • Наш кодекс следует принципу разделения интересов . Имена свойств находятся в компонентах конфигурации, а другие компоненты, которым требуется эта информация, не знают, откуда она берется. Они просто используют это.
  • Наш кодекс следует принципу « не повторяйся» Поскольку имена свойств указываются только в одном месте (в компонентах конфигурации), наш код проще поддерживать.

Кроме того, (IMO) наш код выглядит намного чище:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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. Мы должны написать логику валидации только один раз

Если мы добавим значения свойств в бины конфигурации, мы можем добавить логику проверки в бины конфигурации, и другие бины не должны знать об этом. Этот подход имеет три преимущества:

  • Наш кодекс следует принципу разделения интересов, потому что логика проверки находится в бинах конфигурации (где она принадлежит). Другие бобы не должны знать об этом.
  • Наш кодекс следует принципу «не повторяй себя», потому что логика валидации находится в одном месте.
  • Нам не нужно забывать вызывать логику проверки при создании новых объектов bean-компонентов, потому что мы можем применять правила проверки при создании bean-компонентов конфигурации.

Кроме того, наш исходный код выглядит намного чище:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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. Мы можем получить доступ к документации из нашей IDE

Мы можем задокументировать конфигурацию нашего приложения, добавив комментарии Javadoc к нашим компонентам конфигурации. После того, как мы это сделали, мы можем получить доступ к этой документации из нашей IDE, когда пишем код, которому нужны эти значения свойств. Нам не нужно открывать другой файл или читать вики-страницу. Мы можем просто продолжить писать код и избежать затрат на переключение контекста .

Давайте продолжим и подведем итог тому, что мы узнали из этого блога.

Резюме

Этот пост научил нас внедрять значения свойств в бины конфигурации:

  • Помогает нам следовать принципу разделения интересов. Вещи, которые касаются свойств конфигурации и проверки значений свойств, инкапсулированы в наших компонентах конфигурации. Это означает, что компоненты, использующие эти компоненты конфигурации, не знают, откуда поступают значения свойств и как они проверяются.
  • Помогает нам следовать принципу «не повторяйся сам», потому что 1) Мы должны указывать имена свойств только один раз и 2) Мы можем добавить логику проверки в бины конфигурации.
  • Облегчает доступ к нашей документации.
  • Делает наш код легче писать, читать и поддерживать.

Однако это не помогает нам выяснить конфигурацию времени выполнения нашего приложения. Если нам нужна эта информация, мы должны прочитать файл свойств, найденный на нашем сервере. Это громоздко

Мы решим эту проблему в моем следующем сообщении в блоге.