7/20/2015

Ehcache Tutorial: Dynamically configuration and Cache Abstraction(Since Spring 3.1)

There are basically two ways to config Ehcache in java project.

1. Dynamically configuration
2. Through XML

Before getting started, we need to add the dependency of Ehcache. For example, if use maven, add below dependency into pom.xml:

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>2.10.0</version>
</dependency>

1. Dynamically configuration

More flexible than XML configuration. But instead, you need to write and manage more code.

Cache Initialization:

public abstract class CommonDao{

    @Value("${cache.enable:true}")
    protected boolean cacheEnable;

    @Value("${cache.timetolive:21600}")
    private int cacheTimeToLive;
    
    @Value("${cache.timetoidle:21600}")
    private int cacheTimeToIdle;
    
    protected CacheManager cacheManager;
    
    protected static final String userCacheName = "userCache";
    protected static final String orgCacheName = "orgCache";

    @PostConstruct
    void initCache(){
        Configuration config = new Configuration();
        CacheConfiguration defaultCacheConfiguration = new CacheConfiguration();
        defaultCacheConfiguration.setEternal(false);
        defaultCacheConfiguration.setTimeToIdleSeconds(cacheTimeToIdle);
        defaultCacheConfiguration.setTimeToLiveSeconds(cacheTimeToLive);
        config.addDefaultCache(defaultCacheConfiguration);
        cacheManager = CacheManager.create(config);
        cacheManager.addCacheIfAbsent(userCacheName);
        cacheManager.addCacheIfAbsent(orgCacheName);
    }
    
    protected void putCacheElement(String cacheName, String key, Object value){
        
        Cache cache = null;
        if(BooleanUtils.isTrue(cacheEnable)){
            cache = cacheManager.getCache(cacheName);
            if(cache != null){
                cache.put(new Element(key, value));
            }
        }
    }
    
    protected void putCacheElement(Cache cache, String key, Object value){
        
        if(BooleanUtils.isTrue(cacheEnable)){
            if(cache != null){
                cache.put(new Element(key, value));
            }
        }
    }
    
    protected Object getCacheElement(String cacheName, String key){
        
        Cache cache = null;
        if(BooleanUtils.isTrue(cacheEnable)){
            cache = cacheManager.getCache(cacheName);
            if(cache != null){
                Element element = cache.get(key);
                return element == null ? null : element.getObjectValue();
            }
        }
        return null;
    }
    
    protected Object getCacheElement(Cache cache, String key){
        
        if(BooleanUtils.isTrue(cacheEnable)){
            if(cache != null){
                Element element = cache.get(key);
                return element == null ? null : element.getObjectValue();
            }
        }
        return null;
    }
    
    protected void removeCacheElement(String cacheName, String key){
        
        Cache cache = null;
        if(BooleanUtils.isTrue(cacheEnable)){
            cache = cacheManager.getCache(cacheName);
            if(cache != null){
                cache.remove(key);
            }
        }
    }
    
    protected void removeCacheElement(Cache cache, String key){
        
        if(BooleanUtils.isTrue(cacheEnable)){
            if(cache != null){
                cache.remove(key);
            }
        }
    }
    
    protected CacheManager getCacheManager(){
        return cacheManager;
    }
}

Dynamically update:

If in the system, you have the requirement to run time update the caching configuration:

Cache cache = dao.getCacheManager().getCache("userCache"); 
CacheConfiguration config = cache.getCacheConfiguration(); 
config.setTimeToIdleSeconds(60); 
config.setTimeToLiveSeconds(120); 
config.setmaxEntriesLocalHeap(10000); 
config.setmaxEntriesLocalDisk(1000000);

Reference:
http://ehcache.org/generated/2.10.0/html/ehc-all/index.html#page/Ehcache_Documentation_Set/co-cfgbasics_dynamically_changing_cache_config.html#wwconnect_header

2. XML Configuration

a.  Config the ehcache.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="ehcache.xsd" updateCheck="true" monitoring="autodetect">
    <defaultCache
        eternal="false"
        maxElementsInMemory="1000"
        overflowToDisk="false"
        diskPersistent="false"
        timeToIdleSeconds="3600"
        timeToLiveSeconds="3600"
        memoryStoreEvictionPolicy="LRU" />
    <cache
        name="userCache"
        eternal="false"
        maxElementsInMemory="200"
        overflowToDisk="false"
        diskPersistent="false"
        timeToIdleSeconds="3600"
        timeToLiveSeconds="3600"
        memoryStoreEvictionPolicy="LRU" />
    <cache
        name="orgCache"
        eternal="false"
        maxElementsInMemory="200"
        overflowToDisk="false"
        diskPersistent="false"
        timeToIdleSeconds="3600"
        timeToLiveSeconds="3600"
        memoryStoreEvictionPolicy="LRU" />
</ehcache>

In above example, we configured two cache named "userCache" and "orgCache".
The file can be put in classpath or any location.


Tip:

If the system is developed using Spring, there is another way to configure the cache, which is more preferable. Example below.


b. Create spring context file for Ehcache and added it into spring context.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cache="http://www.springframework.org/schema/cache"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
       http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-4.0.xsd">

    <cache:annotation-driven cache-manager="cacheManager" />

    <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cache-manager-ref="ehcache" />

    <bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:config-location="classpath:ehcache.xml" />
        
    <bean id="realCacheManager" factory-bean="cacheManager" factory-method="getCacheManager" />
    
    <!-- If cache is not configured in ehcache.xml, can also be created here -->
    <bean id="userCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
        <property name="cacheManager" ref="realCacheManager" />
        <property name="eternal" value="false" />
        <property name="maxElementsInMemory" value="${user_cache_max_elements_in_memory}" />
        <property name="overflowToDisk" value="false" />
        <property name="diskPersistent" value="false" />
        <property name="timeToIdle" value="${user_cache_time_to_idle}" />
        <property name="timeToLive" value="${user_cache_time_to_live}" />
        <property name="memoryStoreEvictionPolicy" value="LRU" />
    </bean>

    <bean id="orgCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
        <property name="cacheManager" ref="realCacheManager" />
        <property name="eternal" value="false" />
        <property name="maxElementsInMemory" value="${org_cache_max_elements_in_memory}" />
        <property name="overflowToDisk" value="false" />
        <property name="diskPersistent" value="false" />
        <property name="timeToIdle" value="${org_cache_time_to_idle}" />
        <property name="timeToLive" value="${org_cache_time_to_live}" />
        <property name="memoryStoreEvictionPolicy" value="LRU" />
    </bean>
</beans>

Reference:
http://ehcache.org/generated/2.10.0/html/ehc-all/#page/Ehcache_Documentation_Set%2Fco-cfgbasics_xml_configuration.html%23

c. Cache Abstraction (Since Spring 3.1)

Since 3.1, Spring added support for transparently adding caching into an existing Spring application. 
Similar to other feature in Spring, it supports annotation-based and xml-based configuration. Here we just include example for annotation-based configuration.

@Repository
public interface UserDao extends CrudRepository<User, String> {

    @Cacheable(value="userCache", key="#p0", unless="#result == null")
    Channel findByUserName(String userName);
}
@Repository
public interface OrganizationDao extends CrudRepository<Organization, String> {

    @Cacheable(value="orgCache", key="#p0", unless="#result == null")
    Organization findByName(String name);

    @CacheEvict(value="orgCache", key="#p0")
    Long deleteByName(String name);
}
Tips:

1. The key in above example is using p0 to represent the first param of the method. This is in case:

"Name of any of the method argument. If for some reason the names are not available (ex: no debug information), the argument names are also available under the a<#arg> where #arg stands for the argument index (starting from 0)."

Details on how to customize the configuration will not be included here in this tutorial, because you could almost find everything in the official document. Just do a little bit research based on your own requirement.

Spring expression language can also be used in the configuration.

Reference:


Good luck!


4/24/2015

Runtime Setting of SSL Params when invoking web services(apache cxf client)

By default, when we config the SSL Key for WS call, we use below system parameters:

System.setProperty("javax.net.ssl.keyStoreType", "pkcs12");
System.setProperty("javax.net.ssl.keyStore", "keystore.p12");
System.setProperty("javax.net.ssl.keyStorePassword", "1234");
System.setProperty("javax.net.ssl.trustStore", "cacerts");

But it is not flexible enough if the application has gateway structure and needs different SSL configuration for each different channels. So we need a workaround.

Below example is based on the client stub that generated with Apache CXF.(Recommanded)
Personally I prefer to use Apache CXF to generate client stub instead of Apache Axis. It is more flexible and more user friendly.

Maven Config to generate client stub:

<plugin>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-codegen-plugin</artifactId>
    <version>3.0.4</version>
    <executions>
        <execution>
            <id>generate-sources</id>
            <phase>generate-sources</phase>
            <configuration>
                <sourceRoot>${project.basedir}/src/main/java</sourceRoot>
                <wsdlOptions>
                    <wsdlOption>
                        <wsdl>${project.basedir}/src/main/resources/schemas/sample-ws.wsdl</wsdl>
                        <bindingFiles>
                            <bindingFile>${project.basedir}/src/main/resources/schemas/bindings/binding.xjb</bindingFile>
                        </bindingFiles>
                        <extraargs>
                            <extraarg>-impl</extraarg>
                            <extraarg>-client</extraarg>
                            <extraarg>-verbose</extraarg>
                            <extraarg>-p</extraarg>
                            <extraarg>com.sample.ws</extraarg>
                            <extraarg>-xjc-Xvalue-constructor</extraarg>
                        </extraargs>
                    </wsdlOption>
                </wsdlOptions>
            </configuration>
            <goals>
                <goal>wsdl2java</goal>
            </goals>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>org.jvnet.jaxb2_commons</groupId>
            <artifactId>jaxb2-value-constructor</artifactId>
            <version>3.0</version>
        </dependency>
        <dependency>
            <groupId>org.jvnet.jaxb2_commons</groupId>
            <artifactId>jaxb2-basics</artifactId>
            <version>0.6.4</version>
        </dependency>
    </dependencies>
</plugin>

After the client stub is generated, if we want to make web service call, the client code is like below:
public SampleResponse authenticate(SampleRequest request) {
    
    SampleServiceInterface port = this.getPort();
    return port.authenticate(request);
}

public SampleServiceInterface getPort() {

    SampleServiceInterfaceService ss = new SampleServiceInterfaceService();
    SampleServiceInterface port = ss.getSampleServiceAPI();
    
    return port;
}

To customize the SSL configuration, we need to write the customized SSLSocketFactory. To implement this, we can create one SSLSocketFactoryGenerator, like below:

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.KeyStore;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

import org.apache.commons.lang.StringUtils;


public class SSLSocketFactoryGenerator {

    
    private byte[] keyStore = null;
    private byte[] trustStore = null;
    
    private String alias = null;
    private String keyStoreType = null;
    private String keyStorePassword = null;
    
    private String trustStoreType = null;
    private String trustStorePassword = null;

    public SSLSocketFactoryGenerator(ClientConfig config) {
        
        this.alias = config.getKeyAlias();
        this.keyStore = config.getKeyStoreBytes();
        this.trustStore = config.getTrustStoreBytes();
        this.keyStoreType = config.getKeyStoreType();
        this.keyStorePassword = config.getKeystorePassword();
        this.trustStoreType = config.getTrustStoreType();
        this.trustStorePassword = config.getTruststorePassword();
    }

    public SSLSocketFactory getSSLSocketFactory() throws IOException,
            GeneralSecurityException {

        KeyManager[] keyManagers = getKeyManagers();
        TrustManager[] trustManagers = getTrustManagers();

        SSLContext context = SSLContext.getInstance("SSL");
        context.init(keyManagers, trustManagers, null);

        SSLSocketFactory ssf = context.getSocketFactory();
        return ssf;
    }

    public String getAlias() {
        return alias;
    }

    public void setAlias(String alias) {
        this.alias = alias;
    }

    public byte[] getKeyStore() {
        return keyStore;
    }

    public void setKeyStore(byte[] keyStore) {
        this.keyStore = keyStore;
    }

    public byte[] getTrustStore() {
        return trustStore;
    }

    public void setTrustStore(byte[] trustStore) {
        this.trustStore = trustStore;
    }

    public String getKeyStoreType() {
        return keyStoreType;
    }

    public void setKeyStoreType(String keyStoreType) {
        this.keyStoreType = keyStoreType;
    }

    public String getKeyStorePassword() {
        return keyStorePassword;
    }

    public void setKeyStorePassword(String keyStorePassword) {
        this.keyStorePassword = keyStorePassword;
    }

    public String getTrustStoreType() {
        return trustStoreType;
    }

    public void setTrustStoreType(String trustStoreType) {
        this.trustStoreType = trustStoreType;
    }

    public String getTrustStorePassword() {
        return trustStorePassword;
    }

    public void setTrustStorePassword(String trustStorePassword) {
        this.trustStorePassword = trustStorePassword;
    }

    private KeyManager[] getKeyManagers() throws IOException,
            GeneralSecurityException {

        String alg = KeyManagerFactory.getDefaultAlgorithm();
        KeyManagerFactory kmFact = KeyManagerFactory.getInstance(alg);

        InputStream fis = new ByteArrayInputStream(getKeyStore());
        
        KeyStore ks = KeyStore.getInstance(getKeyStoreType());
        
        ks.load(fis, getKeyStorePassword().toCharArray());
        fis.close();

        kmFact.init(ks, StringUtils.isEmpty(getKeyStorePassword())?null:getKeyStorePassword().toCharArray());

        KeyManager[] kms = kmFact.getKeyManagers();
        return kms;
    }

    protected TrustManager[] getTrustManagers() throws IOException,
            GeneralSecurityException {

        String alg = TrustManagerFactory.getDefaultAlgorithm();
        TrustManagerFactory tmFact = TrustManagerFactory.getInstance(alg);

        InputStream fis = new ByteArrayInputStream(getTrustStore());
        KeyStore ks = KeyStore.getInstance(getTrustStoreType());
        ks.load(fis, StringUtils.isEmpty(getTrustStorePassword())?null:getTrustStorePassword().toCharArray());
        fis.close();

        tmFact.init(ks);

        TrustManager[] tms = tmFact.getTrustManagers();
        return tms;
    }
}

This is a quite basic sample, for different scenarios, it should be quite easy to make complementary or revision. In above sample, we can create customized SSLSocketFactory by passing in different configuration data(ClientConfig.java).

In this case, we also  need to revise the previous getPort function to use this customized SSLSocketFactory.
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Properties;

import javax.annotation.PostConstruct;
import javax.naming.ConfigurationException;
import javax.xml.ws.BindingProvider;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

public class SampleServiceImpl implements SampleService {

    private static final Logger logger = Logger.getLogger(SampleServiceImpl.class);

    private static final String URI = "/auth";

    protected ClientConfig clientConfig;

    public SampleServiceImpl() throws ConfigurationException, IOException {
        super();
    }

    void loadDefaultIfEmptyClientConfig() throws IOException, ConfigurationException {
        if (this.getClientConfig() != null)
            return;

        Properties properties = new Properties();
        properties.load(this.getClass().getClassLoader().getResourceAsStream("config/sample.properties"));
        setClientConfig(new ClientConfig(properties));
    }

    public SampleServiceImpl(Properties properties) throws ConfigurationException, IOException {
        super();
        this.setClientConfig(new ClientConfig(properties));
    }
    
    public SampleServiceImpl(ClientConfig clientConfig) throws ConfigurationException {
        super();
        this.setClientConfig(clientConfig);
    }

    public SampleServiceInterface getPort(String relUrl) {

        SampleServiceInterfaceService ss = new SampleServiceInterfaceService();
        SampleServiceInterface port = ss.getSampleServiceAPI();
        
        BindingProvider bindingProvider = (BindingProvider) port; 
        try {
            bindingProvider.getRequestContext().put("com.sun.xml.internal.ws.transport.https.client.SSLSocketFactory", 
                    new SSLSocketFactoryGenerator(clientConfig).getSSLSocketFactory());
            
            if(StringUtils.isNotEmpty(relUrl)){
                bindingProvider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, String.format("%s%s", this.clientConfig.getServiceUrl(), relUrl));
            }
        } catch (IOException e) {
            logger.error("Error happened when initializing SSLFactory.", e);
            throw new IllegalArgumentException(e);
        } catch (GeneralSecurityException e) {
            logger.error("Error happened when initializing SSLFactory.", e);
            throw new IllegalArgumentException(e);
        } 
        return port;
    }
    
    public ClientConfig getClientConfig() {
        return clientConfig;
    }

    public void setClientConfig(ClientConfig clientConfig) throws ConfigurationException {

        this.clientConfig = clientConfig;
    }
    
    @Override
    public SampleResponse authenticate(SampleRequest request) {
    
        SampleServiceInterface port = this.getPort();
        return port.authenticate(request);
    }

}
In this case, for each different channels that need different SSL configuration. We can create different service.