niedziela, 29 czerwca 2008

Spring modules cache

Witam ponownie.
Pamiętacie aplikację akwarium? Aplikacja to trochę słowo na wyrost, bo w zasadzie stanowi ją jedna strona, która wylicza ile rybek pluska się w akwarium.

Wszystko działa, ale wyliczanie trochę trwa. Zaraz się tym zajmiemy, korzystając z dodatku do Springa: Spring Modules.
Spring Modules wprowadza między innymi mechanizm pozwalający na zapamiętywanie w pamięci podręcznej wyników wywołań wybranych metod (method cache). W przypadku, gdy któraś z funkcji wykonuje się długo, a dla zadanych parametrów wejściowych jej parametry wyjściowe są zawsze takie same, możemy wyniki jej wykonania przechować w pamięci podręcznej.
Trzeba jednak, trochę uważać. Przy wykorzystaniu mechanizmu method cache, tylko wejście i wyjście danej funkcji się zgadza. Wywołanie funkcji zostaje pominięte, natomiast rezultat jest zwracany z pamięci podręcznej.
Co ciekawe, te wszystkie czary można uruchomić bez napisania jakiejkolwiek linijki kodu w jawie. Trzeba tylko zmienić konfigurację. Ale do rzeczy...
Po uruchomieniu aplikacji akwarium i wpisaniu adresu strony:
http://localhost:8080/aquarium/info.htm
możemy zauważyć, że strona ładuje się kilka sekund. W dodatku zawsze zwraca inny wynik, coś ok 200000.



Odpowiedzialna jest za to klasa

package pl.matt.aquarium.manager.impl;

import pl.matt.aquarium.manager.AquariumManager;

/**
* @author mateusz
*
*/
public class AquariumManagerImpl implements AquariumManager {

/**
* pojemność akwarium w centymetrach sześciennych
*/
private int cubicCentimetres;
/**
* minimalna gęstość rybki
*/
private int minimalDensity;;

@Override
public int getFishCount() {
int count = 0;
for (int index = 0; index < cubicCentimetres; index++) {
if (getDensity(index) > minimalDensity) {
count++;
}
}
return count;
}

/**
* @param centimetreIndex
* @return gęstość wybranego fragmentu akwarium
*/
private int getDensity(int centimetreIndex) {
return (int) (Math.random() * 100);
}

public void setCubicCentimetres(int cubicCentimetres) {
this.cubicCentimetres = cubicCentimetres;
}

public void setMinimalDensity(int minimalDensity) {
this.minimalDensity = minimalDensity;
}

}

a dokładniej jej metoda public int getFishCount().

Pierwsza rzecz, jaką trzeba zrobić, aby dodać Spring Modules do naszego projektu to dodanie nowego repozytorium z którego skorzysta maven2. W tym celu trzeba wprowadzić kilka zmian w pliku settings.xml. Zazwyczaj jest on w katalogu ~/.m2/ lub /etc/maven2/. W sekcji profiles trzeba dopisać nowy profil:

<profiles>
<profile>
<id>standard-extra-repos</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<repositories>
<repository>
<id>jboss</id>
<url>http://repository.jboss.com/maven2</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>


Następnie dodaję kilka nowych zależności do pliku pom.xml

<dependency>
<groupId>org.springmodules</groupId>
<artifactId>spring-modules-cache</artifactId>
<version>0.7</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>apache-oro</groupId>
<artifactId>jakarta-oro</artifactId>
<version>2.0.8</version>
</dependency>


Cały plik pom.xml wygląda następnująco:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>pl.matt</groupId>
<artifactId>aquarium</artifactId>
<packaging>war</packaging>
<version>1.0</version>
<name>aquarium Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.9</version>
</dependency>
<dependency>
<groupId>org.springmodules</groupId>
<artifactId>spring-modules-cache</artifactId>
<version>0.7</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>apache-oro</groupId>
<artifactId>jakarta-oro</artifactId>
<version>2.0.8</version>
</dependency>
</dependencies>
<build>
<finalName>aquarium</finalName>
<directory>/tmp/target</directory>
<outputDirectory>/tmp/target/classes</outputDirectory>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>


generalnie nic ciekawego. Czas wziąć na warsztat serce i mózg aplikacji springowej. W moim przypadku jest to plik springapp-servlet.xml.

Dodaję ziarna cacheManager i cacheProviderFacade określając położenie pliku konfiguracyjnego ehcache.xml.

<bean id="cacheManager"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache.xml" />
</bean>
<bean id="cacheProviderFacade" class="org.springmodules.cache.provider.ehcache.EhCacheFacade">
<property name="cacheManager" ref="cacheManager" />
</bean>


Ziarno cachingInterceptor pozwala wskazać metody, których wyniki wywołań będą przechowywane w pamięci podręcznej. Wskazuję metody zaczynające się od liter get interfejsu pl.matt.aquarium.manager.AquariumManager. Czyli de facto tylko metodę getFishCount().

<bean id="cachingInterceptor"
class="org.springmodules.cache.interceptor.caching.MethodMapCachingInterceptor">
<property name="cacheProviderFacade" ref="cacheProviderFacade" />
<property name="cachingModels">
<props>
<prop key="pl.matt.aquarium.manager.AquariumManager.get*">cacheName=testCache</prop>
</props>
</property>
</bean>

Wyniki będą przechowywane we fragmencie pamięci nazwanym testCache. Zaraz go zdefiniuję, ale najpierw jeszcze jedno ziarno konfigurujące przechowywanie wyników wywołań metod w pamięci podręcznej:

<bean
class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<list>
<idref local="aquariumManager" />
</list>
</property>
<property name="interceptorNames">
<list>
<value>cachingInterceptor</value>
</list>
</property>
</bean>


Cały plik springapp-servlet.xml wygląda tak:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<!--
the application context definition for the springapp DispatcherServlet
-->
<bean name="/info.htm" class="pl.matt.aquarium.InfoController"
autowire="byName" />

<!-- freemarker config -->
<bean id="freemarkerConfig"
class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/freemarker/" />
</bean>
<!--
View resolvers can also be configured with ResourceBundles or XML
files. If you need different view resolving based on Locale, you have
to use the resource bundle resolver.
-->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
<property name="cache" value="true" />
<property name="prefix" value="" />
<property name="suffix" value=".ftl" />
<!--
if you want to use the Spring FreeMarker macros, set this property to
true
-->
<property name="exposeSpringMacroHelpers" value="true" />
</bean>
<bean id="aquariumManager" class="pl.matt.aquarium.manager.impl.AquariumManagerImpl">
<property name="cubicCentimetres" value="20000000" />
<property name="minimalDensity" value="98" />
</bean>

<!-- cache -->
<bean id="cacheManager"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache.xml" />
</bean>
<bean id="cacheProviderFacade" class="org.springmodules.cache.provider.ehcache.EhCacheFacade">
<property name="cacheManager" ref="cacheManager" />
</bean>
<bean id="cachingInterceptor"
class="org.springmodules.cache.interceptor.caching.MethodMapCachingInterceptor">
<property name="cacheProviderFacade" ref="cacheProviderFacade" />
<property name="cachingModels">
<props>
<prop key="pl.matt.aquarium.manager.AquariumManager.get*">cacheName=testCache</prop>
</props>
</property>
</bean>
<bean
class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<list>
<idref local="aquariumManager" />
</list>
</property>
<property name="interceptorNames">
<list>
<value>cachingInterceptor</value>
</list>
</property>
</bean>
</beans>


Pozostaje jeszcze skonfigurować Ehcache, który będzie u mnie zarządzał pamięcią podręczną. Plik ehcache.xml tworzę w katalogu resources.

<?xml version="1.0" encoding="UTF-8"?>

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="ehcache.xsd">
<diskStore path="java.io.tmpdir/cache" />

<defaultCache
maxElementsInMemory="9999"
eternal="false"
timeToIdleSeconds="777"
timeToLiveSeconds="777"
overflowToDisk="true"
diskSpoolBufferSizeMB="30"
maxElementsOnDisk="10000000"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="600"
memoryStoreEvictionPolicy="LRU"
/>

<cache name="testCache"
maxElementsInMemory="9999"
eternal="false"
timeToIdleSeconds="777"
timeToLiveSeconds="777"
overflowToDisk="true"
diskSpoolBufferSizeMB="30"
maxElementsOnDisk="10000000"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="600"
memoryStoreEvictionPolicy="LRU"
/>

</ehcache>


Plik zawiera konfigurację dwóch fragmentów pamięci podręcznej. Nie wchodząc w szczegóły, konfiguracja fragmentu defaultCache jest obowiązkowa. Natomiast testCache będzie pamiętał wywołania metody getFishCount().
I to tyle. Czas sprawdzić, czy działa. Buduję aplikację, wchodzę na stronę:
http://localhost:8080/aquarium/info.htm
Ładuje się długo. Wchodzę ponownie. Tym razem strona załadowała się błyskawicznie. W dodatku liczba rybek pozostała niezmieniona. Miodzio - i to wszystko bez ani jednej linijki kodu w jawie. Całą aplikację akwarium znajdziesz tutaj.

Oczywiście do konfiguracji mechanizmu method cache można wykorzystać adnotacje, a zamiast Ehcache, JBoss Cache, Java Caching System (JCS) albo OSCache. Takie uroki Springa.

Brak komentarzy: