Spring Konfiguráció részletesen

Ebben a fejezetben a Spring nyújtotta konfigurációs lehetőségekkel ismerkedünk meg mélyrehatóan.

Property Editors

A PropertyEditor interface lehetővé teszi, hogy egy property értékét String-be és String-ből konvertáljuk. Ez azért fontos témakör, mivel a legtöbb konfiguráció, melyet írunk alapvetően String típusban fogalmazódik meg (pl.: property fájlban definiált property-k). A Spring által adott PropertyEditor-ok a spring-beans modulon belül találhatóak meg, de fontos lehet tudni, hogy maga a PropertyEditor nem a Spring által kitalált dolog, hanem a JavaBeans API része. Itt sok-sok beépített PropertyEditor-t találhatunk, mint például:

  • ByteArrayPropertyEditor
  • CharacterEditor
  • CurrencyEditor
  • CustomBooleanEditor
  • CustomDateEditor
  • FileEditor

csak hogy néhányat említsünk.

Példa a dátum konverziók megadásához:

1
2
3
4
5
6
7
8
9
public static class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {

    @Override
    public void registerCustomEditors(PropertyEditorRegistry registry) {

        SimpleDateFormat dateFormatter = new SimpleDateFormat("MM/dd/yyyy");
        registry.registerCustomEditor(Date.class, new CustomDateEditor(dateFormatter, true));
    }
}

A fenti kódot a bean-ünknél elhelyezve máris aktiválódik a konverziós logika. A dátum alapú PropertyEditor-ok alapból nincsenek bekapcsolva, de a többi igen. A fenti kód a CustomDateEditor-t használta.

Amennyiben saját custom property editort szeretnénk létrehozni, akkor a PropertyEditorSupport osztályból kell származnia (getAsText() és setAsText() metódusokat kell implementálnia) a saját editornak és az osztály nevének az Editor postfix-szel kell végződnie!

A PropertyEditor-ok használatát részletesebben lásd itt.

Az ApplicationContext további képességei

Az eddig ismertetett képességek leginkább a BeanFactory érdemei voltak. Az ApplicationContext azonban további feature-öket is szolgáltat, mint például:

  • Többnyelvűség
  • Esemény publikálás
  • Erőforráskezelés
  • További életciklus interfészek
  • Továbbfejlesztett automatikus konfiguráció (infrastrukturális komponensekre)

Többnyelvűség (i18n)

A többnyelvűség támogatásához az ApplicationContext implementálja a MessageSource interface-t, mely segítségével az alkalmazásunk képes lesz hozzáférni String alapú erőforrásokhoz, melyeket üzeneteknek (message) nevezünk, és melyek több nyelvi megfelelőt adnak meg. A többnyelvűség támogatásakor üzenetek listáját tároljuk, melyek kulcsokban adják meg az adott nyelvi megfelelőt.

Ugyan a többnyelvűséghez nem lenne szükséges az ApplicationContext használata, de mivel az nagyban segíti az üzenetek betöltését és azok elérését alkalmazás szerte, így ajánlott a használata.

A Spring 3 MessageSource implementációt kínál a számunkra:

  • StaticMessageSource
  • ResourceBundleMessageSource
  • ReloadableResourceBundleMessageSource

A StaticMessageSource használata nem javasolt éles környezetben, hiszen kívülről nem konfigurálható (általában erre pedig szükség van). A ResourceBundleMessageSource az üzenetek betöltését a ResourceBundle által végzi. A ReloadableResourceBundleMessageSource ugyanerre képes, de támogatja a forrásállományok ütemezett újratöltését is.

A fenti implementációk mindegyike implementálja a HierarchicalMessageSource interface-t is, mely segítségével a MessageSource példányokat egymásba is ágyazhatjuk, mely az egyik kulcsa annak, ahogy az ApplicationContext kezeli a MessageSource példányokat.

A Spring Boot tovább egyszerűsíti a többnyelvű alkalmazásunk írását, melyről a gyakorlati anyagok között lesz részletesen szó.

Alkalmazás események

Az ApplicationContext támogatja az események publikálását és kezelését is, melyben ő maga a bróker szerepét tölti be (ő regisztrálja az eseményeket és továbbítja a megfelelő helyekre, hogy ott kezelhessük azokat).

Az alapszabályok:

  • Az események származzanak az ApplicationEvent osztályból!
  • Az esemény kiváltója injektáljon egy ApplicationEventPublisher objektumot!
  • Az eseménykezelő implementálja az ApplicationListener interface-t!

Az egyedi események alapértelmezetten szinkron módon jönnek létre.

Nézzünk is egy példát egy egyszerű eseményre:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class CustomEvent extends ApplicationEvent {
    private String message;

    public CustomEvent(Object source, String message) {
        super(source);
        this.message = message;
    }
    public String getMessage() {
        return message;
    }
}

Az üzenet kiváltója létrehoz egy CustomEvent objektumot, majd publikája, hogy minden érdeklődő (listener) megkaphassa azt. Ehhez injektáljunk a bean-be egy ApplicationEventPublisher objektumot, melynek publishEvent() metódusával publikálhatjuk az eseményt!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Component
public class CustomEventPublisher {

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    public void publishCustomEvent(final String message) {
        CustomEvent customEvent = new CustomEvent(this, message);
        applicationEventPublisher.publishEvent(customEvent);
    }
}

Megjegyzés

Alternatívaként a bean implementálhatja az ApplicationEventPublisherAware interface-t.

Az eseménykezelőnek implementálnia kell az ApplicationListener interfészt, melynek az onApplicationEvent() metódusát kell kifejtenünk! Az eseménykezelő metódus generikus paramétert vár, így jelen esetben CustomEvent-et.

1
2
3
4
5
6
7
8
9
@Component
public class CustomEventListener implements ApplicationListener<CustomEvent> {

    @Override
    public void onApplicationEvent(CustomEvent event) {
        System.out.println("Received custom event: " + event.getMessage());
    }

}

Mivel az ApplicationEventPublisher-ben létrejövő üzenet szinkron, így a publishCustomEvent() metódus futása egészen addig blokkolt, ameddig minden eseménykezelő le nem kezeli az eseményt.

Megjegyzés

Spring 4.2-től az eseménykezelőknek nem kell implementálnia az ApplicationListener interfészt, hanem elhelyezhetjük a @EventListener annotációt is a megfelelő metóduson. Ilyenkor figyeljünk a paraméter típusára, illetve arra, hogy a metódus láthatósága public legyen!

Erőforrások kezelése

A Springben sokszor kell erőforrás fájlokhoz hozzáférnünk (például konfigurációs adatok, képek). A Spring egy egységes mechanizmust biztosít az erőforrások kezelésére, mindezt protokoll-független módon (egy fájlt a lokális fájlrendszerről ugyanúgy kezelhetünk, mint egy távoli fájlt, vagy a classpath-ban lévő fájlt).

Az egységes kezelés megvalósításának egyik alappillére a org.springframework.core.io.Resource interfész, mely többek között a következő funkcionalitásokat adja:

  • contentLength()
  • exists()
  • getDescription()
  • getFile()
  • getFileName()
  • getURI()
  • getURL()
  • isOpen()
  • isReadable()
  • lastModified()

A Resource interfészhez több implementációt is találhatunk:

  • FileSystemResource
  • ClassPathResource
  • UrlResource

A Spring keretrendszer a Resource példányokat nem direktben hozza létre, hanem a ResourceLoader interfészt használja, illetve annak alapértelmezett megvalósítását: DefaultResourceLoader,. Amikor mi magunk akarunk erőforrásokat kezelni, akkor még csak nem is ezt ajánlott használnunk, hanem magát az ApplicationContext-et! Példa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class ResourceExample {

    public static void main(String... args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();

        File file = File.createTempFile("test", ".txt");
        file.deleteOnExit();

        Resource res1 = ctx.getResource("file://" + file.getPath());
        System.out.println(res1.getFilename());

        Resource res2 = ctx.getResource("classpath:test.txt");
        Resource res3 = ctx.getResource("http://www.google.com");
    }
}

A fenti példában minden getResource() hívásban egy-egy URI-t adunk át, melyeknél a file: és a http: protokollt is használtuk., azonban a classpath: prtokoll megadás egy Spring specifikus megadás, melynek hatására a kért erőforrást a classpath-ban keresi a rendszer.

A Resource-ok injektálását a @Value is támogatja:

1
2
@Value("classpath:test.txt")
Resource resource1;

Profilok

Amennyiben definiáltuk a profilhoz tartozó beállításokat, akkor nincs más dolgunk, mint aktiválni valamelyik profilt. Ezt többféleképpen is megtehetjük:

  • WebApplicationInitializer-en keresztül:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    @Configuration
    public class MyWebApplicationInitializer implements WebApplicationInitializer {
    
        @Override
        public void onStartup(ServletContext servletContext) throws ServletException {
    
            servletContext.setInitParameter(
            "spring.profiles.active", "dev");
        }
    }
    
  • ConfigurableEnvironment-en keresztül
    1
    2
    3
    4
    @Autowired
    private ConfigurableEnvironment env;
    ...
    env.setActiveProfiles("dev");
    
  • Context paraméter a web.xml-ben
    1
    2
    3
    4
    <context-param>
        <param-name>spring.profiles.active</param-name>
        <param-value>dev</param-value>
    </context-param>
    
  • JVM System Parameter
    1
    -Dspring.profiles.active=dev
    
  • Környezeti változó (Unix):
    1
    export spring_profiles_active=dev
    
  • Maven profile:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <profiles>
        <profile>
            <id>dev</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <spring.profiles.active>dev</spring.profiles.active>
            </properties>
        </profile>
        <profile>
            <id>prod</id>
            <properties>
                <spring.profiles.active>prod</spring.profiles.active>
            </properties>
        </profile>
    </profiles>
    
    Ebben az esetben az application.properties-be átvezethetjük az aktív maven profile-t, ha a következőt használjuk:
    1
    spring.profiles.active=@spring.profiles.active@
    

    de ehhez az is kell, hogy aktiváljuk a resource filtering-et:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
        ...
    </build>
    
    Ez a változók behelyettesítése miatt szükséges.

    A package előállításakor ilyenkor használhatjuk a -P paramétert. Példuál "prod" package előállításához:

    1
    mvn clean package -Pprod
    

  • @ActiveProfile: tesztek írásakor használható

Bármelyik beant, amely nem tartozik egyetlen profilhoz sem, az alapértelmezett (default) profilhoz rendeljük hozzá. Ha nincs aktív profil kiválasztva, akkor az alapértelmezettet használja a rendszer, de az alapértelmezést meg is lehet változtatni spring.profiles.default property beállításával.

Arra is lehet igény, hogy az éppen aktív profilt forráskódból elérhessük. Erre is több módszert lehet használni:

  • Environment (a követekző alfejezetben mutatjuk meg az Envrionment-et) Az Environment injektálása után az rendelkezésre bocsájtja a profilokat, melyekete a environment.getActiveProfiles() hívással kérhetünk le.
  • spring.profiles.active: A kiválasztott profil nevét a spring.profiles.active property injektálásával is megkaphatjuk:
    1
    2
    @Value("${spring.profiles.active}")
    private String activeProfile;
    

    Amennyiben több aktív profil is van, akkor ezeket vesszővel elválasztva adja vissza a rendszer. A fenti kódnál azonban probléma fog fellépni (a context-et nem tudja betölteni a rendszer), ha nincs megadva aktív profil (IllegalArgumentException keletkezik). Ennek kezelésére használjuk a már bemutatott default érték használatát:

    1
    2
    @Value("${spring.profiles.active:}")
    private String activeProfile;
    

    A kettőspont után most nincs semmi, így üres string-et kapunk eredményül.

Spring Boot esetén az eddig ismertetett profil megoldások mind használhatóak, illetve még ennél több is. A spring.profiles.active property-t használhatjuk az property állományunkban (alapértelmezetten az application.properties-ben), hogy megadjuk a használni kívánt profilt.

A legfontosabb újítás azonban a profil-specifikus property fájlok használata. Fontos a nevezéktan, amelyet használnunk kell:

1
application-{profile}.properties

Abban az esetben, ha profil specifikus property állományunk is van, akkor a Spring betölti az alap application.properties állományt, illetve az aktív profilokhoz tartozó property állományokat is.

Environment és PropertySource

Az Environment interface egy absztrakciós szint, ami segíti a környezeti információk elérését az éppen aktuálisan futó Spring alkalmazásban. Ahogy azt az előző alfejeztben láthattuk az Environment segítségével elérhetjük az aktív profilt is, de segítséget nyújt abban is, hogy az összes property-t (pl.: az alkalmazás mappája, adatbázis csatlakozási információk, stb.) ezen keresztül érjük el. Az Environment interface egységes formába gyúrja a rendszer property-ket, környezeti változókat és az alkalmazás property-jeit, azaz ezeket mind elérhetjük az Environment-ből. A PropertySource interface a kulcs=érték alakú állományok reprezentálását segíti, lehet az egy Properties típusú objektum, vagy egy Map vagy bármi egyéb.

Nézzünk is egy példát:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class EnvAndPropertySourceExample {

    public static void main(String... args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ConfigurableEnvironment env = ctx.getEnvironment();

        System.out.println("user.home: " + System.getProperty("user.home"));
        System.out.println("JAVA_HOME: " + System.getenv("JAVA_HOME"));
        System.out.println("user.home: " + env.getProperty("user.home"));
        System.out.println("JAVA_HOME: " + env.getProperty("JAVA_HOME"));
        ctx.close();
    }
}

Kimenet

1
2
3
4
5
6
user.home: C:\Users\Zoltán Tóth
JAVA_HOME: c:\Program Files\Java\jdk-11.0.6\
11:33:39.574 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Found key 'user.home' in PropertySource 'systemProperties' with value of type String
user.home: C:\Users\Zoltán Tóth
11:33:39.580 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Found key 'JAVA_HOME' in PropertySource 'systemEnvironment' with value of type String
JAVA_HOME: c:\Program Files\Java\jdk-11.0.6\

Az ApplciationContext-től el tudjuk kérni a hozzá tartozó Environment-et (ConfigurableEnvironment implementáció használható).

A ConfigurableEnvironment-től el is kérhetjük a PropertySources objektumot, melyen keresztül módosíthatjuk is a property-ket, vagy akár sajátot is hozzáadhatunk. Példa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class EnvAndPropertySourceExample {

    public static void main(String... args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ConfigurableEnvironment env = ctx.getEnvironment();
        MutablePropertySources propertySources = env.getPropertySources();

        Map<String,Object> appMap = new HashMap<>();
        appMap.put("app.home", "application_home");
        propertySources.addLast(new MapPropertySource("test_prop_source", appMap));

        System.out.println("app.home: " + env.getProperty("app.home"));
        ctx.close();
    }
}

Kimenet

1
2
11:33:39.581 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Found key 'app.home' in PropertySource 'test_prop_source' with value of type String
app.home: application_home

A fenti kódban az Environment-től elkérjük az összes PropertySource-ot, melyet általában egy PropertySources interfész mögé szokás rejteni (a MutablePropertySources egy alapértelmezett implementációja). Ezután egy Map alapú property megadással bővítjük a PropertySources tárházat.

Ha egy property többféleképpen is meg van adva akkor ezek feloldási sorrendje:

  • Rendszer property-k (aktuális JVM-re)
  • Környezeti változók
  • Alkalmazás által definiált property-k

Ennek tükrében, például a user.home környezeti változót hiába írnánk felül az általunk adott Map-ben, a rendszer akkor is a System property-k közül venné azt. Azonban a sorrend megváltoztatható:

1
propertySources.addLast(new MapPropertySource("test_prop_source", appMap));
helyett használjuk a

1
propertySources.addFirst(new MapPropertySource("test_prop_source", appMap));

hívást!

Ilyenkor a listában hamarabb fog szerepelni az általunk definiált test_prop_source PropertySource, így abban keres először a rendszer.

Ezen háttérismeretek után nézzük meg, hogy még magasabb absztrakciós szinten, hogyan használhatjuk a property-ket!

A property-ket általában jó ha kiszervezzük egy külső állományba (például egy *.properties állományba). Az alkalmazásunk nagy valószínűséggel tartalmazni fog egy Java konfigurációs fájlt, melyre minden további nélkül rárakhatjuk a @PropertySource annotációt, melynek hatására a property fájlt betölti a rendszer és hozzáadja az aktuális az Environment-hez.

1
2
3
4
5
@Configuration
@PropertySource("classpath:foo.properties")
public class TestApp {
    //...
}

Egyszerre akár több PropertySource is megadható (mivel rajta van a @Repeatable annotáció; csak 8-as Java-val használható):

1
2
3
4
5
6
@Configuration
@PropertySource("classpath:foo.properties")
@PropertySource("classpath:bar.properties")
public class TestApp {
    //...
}

Ezzel egyenértékű a következő megadás, mely nem korlátozódik 8-as Java-ra:

1
2
3
4
5
6
7
8
@Configuration
@PropertySources({
    @PropertySource("classpath:foo.properties"),
    @PropertySource("classpath:bar.properties")
})
public class TestApp {
    //...
}

Property ütközéskor a legutolsónak megadott PropertySource élvez előnyt.

Megjegyzés

Ne felejtsük el, hogy Spring Boot-ban az alapértelmezett property állomány az application.properties!!!

Teszteléshez a Spring Boot az src/test/resources-ba helyezett property fájlokat is betölti, így felülírva az alapértelmezett application.properties-ben megadott értékeket.


Utolsó frissítés: 2021-05-31 18:06:31