3. září 2007

Více prostředí pomocí Springu, implementace

Tímto článkem navazuji na první úvodní část Více prostředí pomocí Springu, úvod. Bez zbytečných úvodních slov se pustím do popisu implementace.

Vhodně zvolená adresářová struktura

Spring framework nabízí rozsáhlé možnosti konfigurace. Já jsem v mém řešení využil hlavně tyto vlastnosti:
  • PropertyPlaceholderConfigurer - patří mezi dobré zvyklosti, že různé parametry k nastavení (např. připojení k DB, nastavení Hibernate, cesty k adresářům) se nastavují přes properties soubory. Z pohledu konfigurace to má tu velkou výhodu, že já mohu mít společnou konfiguraci pro všechny prostředí, ale konkrétní parametry konfigurace (např. parametry pro připojení do DB) mohu mít definovány v každém prostředí zvlášť.
  • Dědičnost bean - definici beany si mohu označit jako abstraktní s tím, že až bude jasné přesné nastavení (tedy pro určité prostředí), tak se beana donastaví
  • Collection merging - tato vlastnost souvisí s předchozí vlastností a umožňuje mi spojovat kolekce z rodiče a potomků. Zase s ohledem na konfiguraci prostředí si např. nadefinuji společný propertyConfigurer a v jednotlivých prostředích ho rozšířím o další properties soubory.
  • Lazy inicializace - díky této vlastnosti mohu mít beany, které se budou inicializovat pouze tehdy, když budou potřeba. Zase bych uvedl příklad použití: jedna z našich aplikací má lokální a centrální část. Aplikace to byla vyvíjena jako jedna, ale s tím rozdílem, že v každé částí běžely jiné služby.
Příklad vhodné adresářové struktury může být například následující:
src
config
env
ANECT
DEVELOP
CUSTOMER
TEST
java
config
common
Adresář common obsahuje společnou konfiguraci, jednotlivé podadresáře v adresáři env představují konkrétní prostředí. Názvy adresářů pro jednotlivá prostředí musí odpovídat názvům položek v enum tříde:
/**
* Enum of all environments.
* Enum names have to correspond with names of directories.
*
* @author pjuza
*/
public enum Environment {
DEVELOP("development"),
CUSTOMER("Customer deployment"),
ANECT("ANECT testing deployment");

private final String description;

Environment(String description) {
this.description = description;
}

public String getDescription() {
return description;
}
}

Načítání konf. souborů pro dané prostředí

Informace o prostředí, které se má spouštět nastavuji v properties souuboru:
#environment ('ANECT', 'DEVELOP', 'CUSTOMER')
environment = DEVELOP

#version
version = 0.5

Pro načítání konf. souborů jsem si vytvořil třídu EnvironmentUtils, která mi načítá soubory z jednotlivých adresářů.
/**
* Util class for choosing right configuration files for selected
* application part and environment.
*
* @author pjuza
*/
public class EnvironmentUtils {
protected final static Log logger = LogFactory.getLog(EnvironmentUtils.class);

private static final String ENV_PROP = "environment";
private static final String VERSION_PROP = "version";

private static ResourceBundle configProps;

static {
//load config properties
try {
configProps = ResourceBundle.getBundle("config/config");
}
catch (Exception e) {
logger.fatal("Error occured during application initialization.", e);
}

logger.info("We're running in version: " + getVersion());
}


/**
* Method returns conf. files for application initialization.
* @return conf. files
*/
public static String[] getConfigLocations() {
List files = new ArrayList();
// our regular application context files
files.add("classpath:/config/common/sp_*.xml");
// our env. specific context files
files.add("classpath:/config/env/" + getEnvironment() + "/sp_*.xml");

return files.toArray(new String[]{});
}

/**
* Method returns right environment.
* @return environment
*/
public static Environment getEnvironment() {
String envValue = configProps.getString(ENV_PROP);
Environment env = Environment.valueOf(envValue);

logger.info("We're running in environment: " + env.getDescription());

return env;
}


/**
* Method gets version of application.
* @return version
*/
public static String getVersion() {
return configProps.getString(VERSION_PROP);
}
}
Všimněte si, že soubory se načítají v pořadí od obecných nastavení (adresář common) směrem ke konkrétnímu nastavení pro dané prostředí. Také se mi vyplatilo si zavést nějakou konvenci pro pojmenování konf. souborů.

Úprava inicializace Springu

Poslední co nám zbývá je upravit inicializaci Springu tak, aby načítal konf. soubory dle naší implementace v EnvironmentUtils. Proto je potřeba si napsat vlastní implementaci ContextLoaderListener a ContextLoader. Zde uvádím jejich implementace:
/**
* Custom context loader listener.
*
* @author pjuza
*/
public class EnvironmentAwareContextLoaderListener extends ContextLoaderListener {

@Override
protected ContextLoader createContextLoader() {
return new EnvironmentAwareContextLoader();
}
}

/**
* Custom context loader for loading specific configuration files according to
* environment.
*
* @author pjuza
*/
public class EnvironmentAwareContextLoader extends ContextLoader {
protected final static Log logger = LogFactory.getLog(EnvironmentAwareContextLoader.class);

@Override
protected WebApplicationContext createWebApplicationContext(ServletContext servletContext,
ApplicationContext parent)
throws BeansException {
Class contextClass = determineContextClass(servletContext);

if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
}

ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
wac.setParent(parent);
wac.setServletContext(servletContext);

// Start change:
wac.setConfigLocations(EnvironmentUtils.getConfigLocations());
// End change

wac.refresh();
return wac;
}
}
Toto je základní implementace, kterou je možné dále rozšiřovat dle potřeb. Jak už jsem naznačoval, naše poslední aplikace měla dvě části, takže načítání konf. souborů bylo ještě komplikovanější. Ale ve Springu není nic nemožné :-).

2 komentáře:

Anonymní řekl(a)...

Zaujímavé riešenie, ktoré mi ukázalo ďalšie možnosti Springu. Vdaka.
PS: Čo tak to vylepšiť prekrytím metódy customizeContext?

public class EnvironmentAwareContextLoader extends ContextLoader {
@Override
protected void customizeContext(ServletContext servletContext, ConfigurableWebApplicationContext applicationContext) {
applicationContext.setConfigLocations(EnvironmentUtils.getConfigLocations());
}
}

Petr Jůza řekl(a)...

Řešení je postavené na tom, že dle cílového prostředí si načtu ty správné konf. soubory.
Podle JavaDoc metoda customizeContext umožňuje upravit context, ale až po tom, co se načetly konf. soubory, takže bych to asi neměnil :).