28. září 2007

Fulltextové vyhledávání (Lucene, Compass)

V dnešní době je celkem běžný požadavek na fulltextové vyhledávání v aplikaci. Uživatel aplikace si již nechce pamatovat všechny ty možné atributy a vybírat možné hodnoty - on se prostě potřebuje dostat k cíli na základě toho co zná a už bez ohledu na vlastní uložení dat.
Pokud se někdo rozhodne zahrnout fulltextové vyhledávání do své aplikace, tak první co bude muset vyřešit je, zda použít vestavěnou podporu fulltextu v databázi či vybrat nějakou knihovnu, která bude fulltext implementovat. Pro obě volby existují určité výhody a nevýhody.
Mezi výhodami řešení pomocí knihoven se často uvádí:

  • výkonnější než databázové řešení

  • plně konfigurovatelné

  • plně pod kontrolou

  • databázové řešení lze použít pouze pro omezený doménový model, kdežto knihovna v tomto ohledu žádné omezení nemá


Mezi výhody databázového řešení patří:
  • většinou automaticky součástí databázových serverů

  • snadné a lehké použití bez nějakého velkého nastavování

  • někdy i lepší vlastnosti než řešení pomocí knihovny, např. podpora češtiny


Pro vysvětlení posledního bodu bych rád dodal některá fakta: já osobně používám knihovnu Apache Lucene (verze 2.0). K hodnocení této knihovny se dostanu dále v článku, ale co už bych chtěl uvést již zde, je horší podpora češtiny. Není problém samozřejmě ukládat český text s diakritikou a i vyhledávat s diakritikou, ale již není možné počítat s ohýbáním slov. To je možné ve většině případů řešit pomocí zástupného znaku *, ale není to úplně stoprocentní. Na druhou stranu např. Microsoft nabízí český a slovenský jazykový modul do svého SQL serveru, který tuto funkcionalitu nabízí. Žádnou osobní zkušenost s tím ale nemám.

Apache Lucene a Spring modules

Jak už jsem zmínil, tak ve svém projektu používám knihovnu Apache Lucene. Nejsem odborník přes fulltextové techniky, ale myslím si, že Lucene nabízí jedno z nejlepších (možná i nejlepší) řešení pro fulltextové vyhledávání na platformě Java. Další důvod, proč jsem toto řešení kdysi vybral bylo množství dokumentace a informací na webu a také podpora ve Spring modules, což zase posouvá práci s touto knihovnou o úroveň výše. S knihovnou Lucene resp. Spring modules jsem naprosto spokojený - co se týče nastavení datového modelu, co se týče použití (ale to hlavně díky Spring modules) a také výkonnost se ukazuje jako velice dobrý. Aplikaci teď budeme nasazovat do pilotního provozu u zákazníka, takže na závěrečné zhodnocení je ještě čas.

Compass

Apache Lucene a Spring modules bude znát asi většina lidí, ale již méně je známý Compass - Java Search Engine Framework. Já osobně jsem si této knihovny všimnul na konferenci Spring One a dokonce jsem našel ke stažení vlastní prezentaci.
Nemám s tímto frameworkem zatím žádnou osobní zkušenost, ale některé klíčové vlastnosti jsou velice zajímavé:
  • Framework je postaven nad Apache Lucene. Compass lze používat nejen jako nástavbu nad Lucene, která řeší některé jeho nedostatky jako např. chybějící podpora transakcí, příliš nízkoúrovňové API, nepříliš rychlá aktualizace indexů a možnost ukládání indexů pouze na souborový systém (pozn. nízkoúrovňové API a ukládání indexů řeší již Spring modules).

  • Integrace se Springem a Hibernate

  • Compass umožňuje si stávající datový model (nebo jeho část) napamovat pro účely fulltextového vyhledávání. K tomuto je možné použít konfiguraci pomocí XML nebo anotací.

  • Změna datového modelu se může automaticky promítat v indexech.


Pokud někdo máte zkušenost s jazykovým modulem do MS SQL nebo jiných databází či přímo s Compassem, budu jedině rád, když se podělíte o vaši zkušenost.

23. září 2007

Ukázka konfigurace Acegi security

Pro tento článek jsem vybral konfigurační soubor Acegi security pro náš jeden projekt. Rád bych pár slovy popsal jednotlivé body konfigurace a částečně tím prezentoval možnosti této knihovny. Já osobně považuji Acegi security za nejlepší knihovnu pro řešení bezpečnostních problémů spojených s vývojem aplikací, tedy hlavně s autentizací a autorizací uživatelů.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<!--
- Acegi Security configuration.
-->
<beans>

Definice filtrů pro různé adresy. Zmínil bych zde jen dvě důležité věci - pořadí filtrů je velice důležité a je možné definovat různé filtry pro různé URL adresy. Zápis je pomocí ANT stylu.

<!-- Zakladni bean s definici pouzitych filtru.
Poradi pouzitych filtru je dulezite! -->
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/remoting/**=basicProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
/**=concurrentSessionFilter,httpSessionContextIntegrationFilter,logoutFilter,authenticationProcessingFilter,securityContextHolderAwareRequestFilter,anonymousProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
</value>
</property>
</bean>

ConcurrentSessionFilter aktualizuje informace o session a tím pádem je možné kontrolovat vypršení session pro daného uživatele.

<!-- Filter required by concurrent session handling package -->
<bean id="concurrentSessionFilter" class="org.acegisecurity.concurrent.ConcurrentSessionFilter">
<property name="expiredUrl" value="${acegi.expiredUrl}"/>
<property name="sessionRegistry" ref="sessionRegistry"/>
</bean>

HttpSessionContextIntegrationFilter se stará o přenos SecurityContext mezi jednotlivými HTTP voláními.

<!-- HttpSessionContextIntegrationFilter is responsible for storing a SecurityContext between HTTP requests -->
<bean id="httpSessionContextIntegrationFilter" class="org.acegisecurity.context.HttpSessionContextIntegrationFilter"/>

LogoutFilter slouží k odhlášení přihlášeného uživatele (vymazání kontextu z registru). Definice obsahuje URL adresu, kam se má uživatel přesměrovat po odhlášení a seznam handlerů (implementace LogoutHandler), které se mají provést. Nezbytným handlerem je SecurityContextLogoutHandler, který vymazává informace o přihlášeném uživateli z registru. Kromě toho jsem si vytvořil handler pro účely logování.

<!-- Logs a principal out.
Use <a href="j_acegi_logout">Logout</a> on the page. -->
<bean id="logoutFilter" class="org.acegisecurity.ui.logout.LogoutFilter">
<constructor-arg value="${acegi.urlAfterLogout}"/> <!-- URL redirected to after logout -->
<constructor-arg>
<list>
<bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler"/>
<bean class="cz.anect.securitymodule.ldap.LogoutHandlerImpl"/>
</list>
</constructor-arg>
</bean>

Filter pro zpracování požadavku na přihlášení. Standardně je možné použít AuthenticationProcessingFilter. Já jsem si standardní filtr upravil, protože jsem požadoval zablokování účtu po několika špatných pokusech o přihlášení. Tuto implementaci jsem popisoval v tomto článku.

<!-- Processes an authentication form. -->
<bean id="authenticationProcessingFilter" class="cz.anect.securitymodule.DisableAuthenticationProcessingFilter">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="authenticationFailureUrl" value="${acegi.authenticationFailureUrl}"/>
<property name="defaultTargetUrl" value="/"/>
<property name="filterProcessesUrl" value="/j_acegi_security_check"/>
<property name="disabledAccountManager" ref="disabledAccountManager"/>
</bean>

BasicProcessingFilter zpracovává BASIC hlavičky z HTTP požadavků. Tento filtr používám pouze pro relativní adresy začínající "remoting". Na této adrese jsou totiž publikované vzdálené služby (remote services) a já pomocí Acegi zajišťuji autorizovaný přístup k těmto službám.
  
<bean id="basicProcessingFilter" class="org.acegisecurity.ui.basicauth.BasicProcessingFilter">
<property name="authenticationManager"><ref bean="authenticationManager"/></property>
<property name="authenticationEntryPoint"><ref bean="authenticationEntryPoint"/></property>
</bean>

<bean id="authenticationEntryPoint" class="org.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint">
<property name="realmName" value="MIS_REALM"/>
</bean>

<!-- A Filter which populates the ServletRequest with a new request wrapper. -->
<bean id="securityContextHolderAwareRequestFilter" class="org.acegisecurity.wrapper.SecurityContextHolderAwareRequestFilter"/>

AnonymousProcessingFilter slouží k vytváření anonymních uživatelů (= uživatelů, kteří nejsou přihlášeni). To má tu velkou výhodu, že já si zde mohu nadefinovat parametry anonymního uživatele a pak v aplikaci s tím pracovat jako s jakýmkoliv dalším uživatelem. Nepřihlášení uživatel tedy není null objekt, ale normální uživatel se specifickými vlastnostmi.

<bean id="anonymousProcessingFilter" class="org.acegisecurity.providers.anonymous.AnonymousProcessingFilter">
<property name="key" value="anonymKey"/>
<property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/>
</bean>

ExceptionTranslationFilter zpracovává Java výjimky a vytváří odpovídající HTTP odpovědi.

<!-- Handles any AccessDeniedException and AuthenticationException thrown within the filter chain. -->
<bean id="exceptionTranslationFilter" class="org.acegisecurity.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint">
<bean class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl" value="${acegi.loginFormUrl}"/>
<property name="forceHttps" value="true"/>
</bean>
</property>
<property name="accessDeniedHandler">
<bean class="cz.anect.securitymodule.ldap.CustomAccessDeniedHandlerImpl">
<property name="errorPage" value="${acegi.accessDeniedUrl}"/>
</bean>
</property>
</bean>

FilterSecurityInterceptor řídí přístup k jednotlivým URL adresám. Nastavení URL adres mám v odděleném properties souboru, který uvedu na konci.

<!-- protect web URIs -->
<bean id="filterInvocationInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="objectDefinitionSource">
<value>${acegi.urlMapping}</value>
</property>
</bean>

ProviderManager prochází zaregistrované providery a snaží se získat odpověď na autentikační požadavek (např. přihlášení uživatele k aplikaci). Jednotlivé providery se procházejí v uvedeném pořadí a to do té doby, než nejaký provider vrátí odpověď.
    
<!-- authority providers -->
<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref bean="daoAuthenticationProvider"/>
<ref bean="ldapAuthProvider"/>
<bean class="org.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider">
<property name="key" value="anonymKey"/>
</bean>
</list>
</property>
</bean>

DaoAuthenticationProvider je provider pro přístup k datům, které jsou uloženy v databázi nebo v paměti. Tento provider toho sám o sobě moc neumí, proto je potřeba zaregistrovat userDetailsService. Já tento provider používám pouze pro účely autorizace přístupu k publikovaným službám (=remote services).

<bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="userDetailsService"/>
</bean>

InMemoryDaoImpl je nejjednodušší implementace UserDetailsService a umožňuje mi nadefinovat si uživatele např. pomocí properties souborů. Jak jsem již zmiňoval v minulém bodě, tak tento provider resp. UserDetailService používám pouze pro oveření přístupu ke vzdáleným službám. V properties souboru si nadefinuji fiktivní uživatele pro jednotlivé klienty vzdálených služeb a mám jednoduchý způsob, jak mohu ověřovat přístup.

<!-- In-memory implementation; all information is loaded from properties file -->
<bean id="userDetailsService" class="org.acegisecurity.userdetails.memory.InMemoryDaoImpl">
<property name="userProperties">
<bean class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="location" value="classpath:/config/common/users.properties"/>
</bean>
</property>
</bean>

DefaultInitialDirContextFactory slouží pro připojení k LDAP serveru. Jednotlivé hodnoty jsou uloženy v properties souboru, který uvedu na konci článku.

<!-- =========================== LDAP ========================== -->
<!-- Pripojeni k LDAP serveru -->
<bean id="initialDirContextFactory" class="org.acegisecurity.ldap.DefaultInitialDirContextFactory">
<constructor-arg value="${ldap.providerUrl}"/>
<property name="managerDn" value="${ldap.managerDn}"/>
<property name="managerPassword" value="${ldap.password}"/>
</bean>

LdapAuthenticationProvider je základní provider pro integraci s LDAP serverem. Nastavení LDAP provideru spočívá ve dvou hlavních věcech - jak bude probíhat autentikace uživatele a jak se budou načítat role k uživatelům. Tyto věci jsou závislé na struktuře LDAP serveru a proto je nutné toto nastavení upravit dle aktuální podoby. S ohledem na specifické požadavky autentikace na základě různých atributů v LDAP serveru jsem si vytvořil vlastní implementaci LDAP provideru.

<bean id="ldapAuthProvider" class="cz.anect.securitymodule.ldap.CustomLdapAuthenticationProvider">
<constructor-arg>
<!-- autentifikace uzivatele -->
<bean class="org.acegisecurity.providers.ldap.authenticator.BindAuthenticator">
<constructor-arg><ref local="initialDirContextFactory"/></constructor-arg>
<property name="userDnPatterns">
<list>
<value>${ldap.userDnPatterns1}</value>
<value>${ldap.userDnPatterns2}</value>
<value>${ldap.userDnPatterns3}</value>
<value>${ldap.userDnPatterns4}</value>
<value>${ldap.userDnPatterns5}</value>
<value>${ldap.userDnPatterns6}</value>
<value>${ldap.userDnPatterns7}</value>
<value>${ldap.userDnPatterns8}</value>
<value>${ldap.userDnPatterns9}</value>
<value>${ldap.userDnPatterns10}</value>
<value>${ldap.userDnPatterns11}</value>
<value>${ldap.userDnPatterns12}</value>
<value>${ldap.userDnPatterns13}</value>
<value>${ldap.userDnPatterns14}</value>
<value>${ldap.userDnPatterns15}</value>
<value>${ldap.userDnPatterns16}</value>
<value>${ldap.userDnPatterns17}</value>
<value>${ldap.userDnPatterns18}</value>
<value>${ldap.userDnPatterns19}</value>
<value>${ldap.userDnPatterns20}</value>
</list>
</property>
</bean>
</constructor-arg>
<constructor-arg>
<!-- nacteni roli k uzivateli -->
<bean class="org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator">
<constructor-arg><ref local="initialDirContextFactory"/></constructor-arg>
<constructor-arg value="${ldap.groupSearchBase}"/>
<property name="groupSearchFilter" value="${ldap.groupSearchFilter}"/>
<property name="groupRoleAttribute" value="${ldap.groupRoleAttribute}"/>
<property name="rolePrefix" value="${ldap.rolePrefix}"/>
<property name="convertToUpperCase" value="${ldap.convertToUpperCase}"/>
<property name="searchSubtree" value="true"/>
</bean>
</constructor-arg>
</bean>
<!-- =========================== LDAP ========================== -->

AffirmativeBased je jednoduchou implementací AccessDecisionManager. AccessDecisionManager rozhoduje o tom, zda přihlášený uživatel má dostatečná práva pro přístup k cílovému zdroji (to může být např. webová stránky na určité adrese, to může být metoda v Java rozhraní apod.). Pro můj konkrétní případ se kontroluje role daného uživatele (RoleVoter) a (pokud není první kontrola úspěšná) speciální atributy IS_AUTHENTICATED_FULLY nebo IS_AUTHENTICATED_REMEMBERED nebo IS_AUTHENTICATED_ANONYMOUSLY (AuthenticatedVoter) , které mohu použít v definici přístupu ke stránkám.

<!--
AffirmativeBased implementation will grant access if one or more ACCESS_GRANTED votes were
received (ie a deny vote will be ignored, provided there was at least one grant vote).
In other words principal must have corresponding ROLE and particular level of authentication.
-->
<bean id="accessDecisionManager" class="org.acegisecurity.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions" value="false"/>
<property name="decisionVoters">
<list>
<!-- class will vote if any ConfigAttribute begins with ROLE_. -->
<bean class="org.acegisecurity.vote.RoleVoter"/>
<bean class="org.acegisecurity.vote.AuthenticatedVoter"/>
</list>
</property>
</bean>

MethodSecurityInterceptor slouží k ověřování přístupu na úrovni Java kódu.

<!-- method authorization in FACADE LAYER -->
<bean id="facadeSecurity" class="org.acegisecurity.intercept.method.aopalliance.MethodSecurityInterceptor">
<property name="validateConfigAttributes"><value>true</value></property>
<property name="authenticationManager"><ref bean="authenticationManager"/></property>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="objectDefinitionSource">
<value>
cz.anect.mis.web.facade.DocumentFacade.addDocuments=ROLE_UZIVATELE, ROLE_SPRAVCIGLOBALNI, ROLE_SPRAVCILOKALNI, ROLE_ADDDOCUMENT_WITHOUTEDIT, ROLE_ADDDOCUMENT_WITHEDIT
cz.anect.mis.web.facade.DocumentFacade.addDocumentData=ROLE_ADDDOCUMENTDATA
</value>
</property>
</bean>

<!-- auto proxy for beans which we want to intercepts by security interceptor -->
<bean id="autoProxySecurityCreator" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="interceptorNames">
<list>
<idref bean="facadeSecurity"/>
</list>
</property>
<property name="beanNames">
<list>
<idref bean="DocumentFacade"/>
</list>
</property>
</bean>

Registrace listenerů pro účely logování a zachycení neúspěšných pokusů o přihlášení.
    
<!-- This bean is optional; it isn't used by any other bean as it only listens and logs -->
<bean id="loggerListener" class="cz.anect.securitymodule.CustomLoggerListener"/>

<bean id="failureListener" class="cz.anect.securitymodule.AuthentificationFailureListener">
<property name="disabledAccountManager" ref="disabledAccountManager"/>
</bean>

<bean id="disabledAccountManager" class="cz.anect.securitymodule.BasicDisabledAccountManager">
</bean>

<bean id="sessionRegistry" class="org.acegisecurity.concurrent.SessionRegistryImpl"/>

</beans>

Nyní ještě uvedu použité properties soubory.
###############################################
# property file: acegi_base.properties #
# format : key = value #
# Zakladni nastaveni pro Acegi knihovnu #
###############################################

acegi.expiredUrl =/expired.jsp
acegi.urlAfterLogout =/index.jsp
acegi.authenticationFailureUrl =/login.jsp?login_error=1
acegi.loginFormUrl =/login.jsp
acegi.accessDeniedUrl =/accessDenied.jsp



<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>
Mapovani mezi URLs a rolemi.
Poradi je dulezite, konstanty jsou definovany v AuthenticatedVoter:
IS_AUTHENTICATED_FULLY or IS_AUTHENTICATED_REMEMBERED or IS_AUTHENTICATED_ANONYMOUSLY.
!!! prvni dva radky nechat beze zmeny !!!
Dalsi radky jsou ve formatu: url = jmeno role (popr. vyse uvedene promenne)
</comment>
<entry key="acegi.urlMapping">
<![CDATA[
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/adddocument.*=ROLE_UZIVATELE, ROLE_SPRAVCIGLOBALNI, ROLE_SPRAVCILOKALNI
/adddocumentas.*=ROLE_UZIVATELE, ROLE_SPRAVCIGLOBALNI, ROLE_SPRAVCILOKALNI
/adddocumentdataonly.*=ROLE_ADDDOCUMENTDATA
/adddocumenttodata.*=IS_AUTHENTICATED_FULLY
/analogdocstoreadmin.*=IS_AUTHENTICATED_FULLY
/newdigiuser.*=IS_AUTHENTICATED_FULLY

/remoting/*=ROLE_REMOTE_SERVICES, ROLE_BINDED_APPLICATION
]]>
</entry>
</properties>


###########################################################
# property file: users.properties #
# format : key (username) = value (password, roles #
# List of technical users for accessing remote services #
###########################################################

system1=673yuededjei3yuy3bhyu3,ROLE_REMOTE_SERVICES
system2=eddjeihyu63jeui3j7337,ROLE_REMOTE_SERVICES


###############################################
# property file: ldap.properties #
# format : key = value #
# Udaje pro pripojeni k LDAP serveru #
###############################################

#-------------------- pripojeni k LDAPu
#This should be in the form ldap://monkeymachine.co.uk:389/dc=acegisecurity,dc=org
ldap.providerUrl =ldap://
#directory user to authenticate (including base DN)
ldap.managerDn =
ldap.password =

#-------------------- autentizace uzivatele
#DN sablona pro vyhledavani uzivatele (max. 20 polozek)
ldap.userDnPatterns1 =uid={0},ou=uzivatele,ou=302
ldap.userDnPatterns2 =uid={0},ou=uzivatele,ou=311
ldap.userDnPatterns3 =uid={0},ou=uzivatele,ou=321
ldap.userDnPatterns4 =uid={0},ou=uzivatele,ou=331
ldap.userDnPatterns5 =uid={0},ou=uzivatele,ou=341
ldap.userDnPatterns6 =uid={0},ou=uzivatele,ou=342
ldap.userDnPatterns7 =uid={0},ou=uzivatele,ou=351
ldap.userDnPatterns8 =uid={0},ou=uzivatele,ou=353
ldap.userDnPatterns9 =uid={0},ou=uzivatele,ou=361
ldap.userDnPatterns10 =uid={0},ou=uzivatele,ou=362
ldap.userDnPatterns11 =uid={0},ou=uzivatele,ou=371
ldap.userDnPatterns12 =uid={0},ou=uzivatele,ou=373
ldap.userDnPatterns13 =uid={0},ou=uzivatele,ou=381
ldap.userDnPatterns14 =uid={0},ou=uzivatele,ou=391
ldap.userDnPatterns15 =uid={0},ou=uzivatele,ou=partneri
ldap.userDnPatterns16 =uid={0},ou=uzivatele,ou=verejnost
ldap.userDnPatterns17 =
ldap.userDnPatterns18 =
ldap.userDnPatterns19 =
ldap.userDnPatterns20 =

#-------------------- nacteni roli k uzivateli
#kontejner pro vyhledavani roli k teto aplikaci (DN bez zakladniho DN)
ldap.groupSearchBase =
#jmeno atributu u role, ktery obsahuje odkazy na uzivatele v dane skupine
ldap.groupSearchFilter =(roleOccupant={0})
#jmeno atributu, ktery se pouzije pro ziskani nazvu role
ldap.groupRoleAttribute =cn
#prefix ke jmenu role v LDAPu
ldap.rolePrefix =ROLE_
#maji se jmena roli prevadet na velka pismena? true|false
ldap.convertToUpperCase =true

19. září 2007

Přehled článků a knih o Springu

Kamarád se na mě obrátil s prosbou, zda bych mu mohl poslat nějaké odkazy ke Springu, že by se rád s tím seznámil. Řekl jsem si, že když už mu budu něco posílat, tak to můžu dát rovnou na blog.
V posledních dvou letech jsem si přidal do oblíbených pár článků a přečetl jsem nějaké knížky. Tyto informace bych chtěl zde tedy publikovat.

Články


Knihy

  • Agile Java Development with Spring, Hibernate and Eclipse - tuto knížku jsem četl nedávno a nedala mi nic nového. Jsem z ní spíše zklamaný, protože jsem měl pocit, že autor tam toho chtěl uvést co nejvíce, ale nic pořádně. Měl jsem také dojem, že kdybych začínal od začátku, tak nemám šanci se to podle toho naučit. Takže tuto knížku moc nedoporučuji.

  • Spring in Action - tuto knížku jsem celou nečetl, spíše jsem jí v rychlosti prošel. Pro začátečníky mi přijde dobrá, protože obsahuje celkem srozumitelný výklad o vlastnostech Springu včetně příkladů. Na druhou stranu knížka obsahuje skoro to samé jako referenční dokumentace ke Springu.

  • POJO in Action - tato knížka není jen o Springu, ale i přesto do tohoto seznamu patří. Kniha obsahuje spoustu zajímavých informací a příkladů a je z ní cítit, že autor zkušenosti má a více o čem píše. Tuto knížku vřele doporučuji.

  • J2EE Design and Development - tato knížka je taková moje programovací bible. Sice byla vydána před pár lety, ale pořád většina uvedených myšlenek je aktuální. V této knížce uvádí zakladatel Springu Rod Johnson základní myšlenky pro vývoj aplikací, podle kterých pak vlastně vznikl samotný Spring.


Pokud někdo máte jiné zajímavé odkazy na články nebo jste četly nějaké zajímavé knížky, tak budu jedině rád, když je uvedete v komentářích k tomuto článku.

7. září 2007

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

Aby byla implementace více prostředí ve Springu kompletní, tak ještě zbývá vyřešit nastavení logování (konkrétně Log4j) pro jednotlivá prostředí. Logger se spouští odděleně od vlastní inicializace Springu a samozřejmě by se měl spouštět jako první. Možná si někdo řekne, že už je to přehnané mít více nastavení loggerů, ale my jsme ty požadavky měly - produkční logger posílá chyby mailem, logger pro vývoj pouze loguje na konzoli a logger pro akceptační testy loguje do souboru. Zde je vidět, že potřeba více loggerů opravdu je.

Tento článek se odvolává na věci uvedené v předchozích článcích na toto téma - Více prostředí pomocí Springu, úvod a Více prostředí pomocí Springu, implementace.

Zjištění cesty ke konfiguraci loggeru

Nastavení loggeru bude umístěno v souboru log4j.xml v adresářích představující jednotlivá prostředí (DEVELOP, ANECT, apod.). Podle volby prostředí v konf. souboru config.properties je potřeba zjistit cestu ke správné konfiguraci loggeru. Do třídy EnvironmentUtils přidáme následující metodu:
  /**
* Method returns location of log4j configuration file.
* @return path to log4j conf. file
*/
public static String getLog4jConfigLocation() {
return "classpath:/config/env/" + getEnvironment() + "/log4j.xml";
}

Vlastní implementace Log4jConfigListener

Známe cestu ke konfiguraci a potřebujeme podle toho upravit standardní Log4jConfigListener.
/**
* Custom listener for log4j inicialization.
*
* This listener is aware of different environments.
*
* @author pjuza@anect.com
*/
public class EnvironmentAwareLog4jConfigListener extends Log4jConfigListener {

/**
* This method is overridden because we need to have environment aware initialization.
* From this reason we need to use custom Log4j configurer.
*/
@Override
public void contextInitialized(ServletContextEvent event) {
ServletContext servletContext = event.getServletContext();

String location = EnvironmentUtils.getLog4jConfigLocation();

// Write log message to server log.
servletContext.log("Initializing Log4J from [" + location + "]");

//Expose the web app root system property.
WebUtils.setWebAppRootSystemProperty(servletContext);

try {
Log4jConfigurer.initLogging(location);
}
catch (FileNotFoundException ex) {
servletContext.log("Log4J config file [" + location + "] not found", ex);
}
}
}
Na závěr je ještě nutné upravit konfiguraci web.xml.
  <listener>
<listener-class>cz.anect.mis.utils.config.EnvironmentAwareLog4jConfigListener</listener-class>
</listener>

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é :-).