Enterprise Integration Zone is brought to you in partnership with:

Faheem is a software programmer and architect working primarily with the Java stack for the last 8 years. Loves to write about technology. He's interested in messaging systems, distributed systems, cloud computing, NoSQL and web frameworks. Faheem is a DZone MVB and is not an employee of DZone and has posted 16 posts at DZone. You can read more from them at their website. View Full User Profile

Implementing Memcached a Servlet Filter for Spring MVC-Based RESTful Services

06.25.2013
| 12903 views |
  • submit to reddit

I have a number of Spring MVC based RESTful services that return JSON. In 90% of the cases, the state of objects these services return will not change within a 24 hour period. This makes them (the JSON objects) perfect candidates for simple caching enabled by memcached. The idea was to have every request to Spring controllers intercepted, cache key generated  and checked against the cache. If the key and corresponding value (JSON string) is available (a cache hit), it is returned to the caller as-is without making a full round trip to the database. However, if the cache has no entry for the key and hence no corresponding value (a cache miss), the call is forwarded to the controller, which in turn calls the logic to fetch desired object from the database and not only return it to the caller but also update the cache with the returned content.

Keys are generated using the URL of the service in case of GET requests and the URL concatenated with POSTed input (as JSON) in case of POST requests. The resultant strings are encoded with MD5 to come up with a 32 character cache key which is well within the 250 character key length limit of memcached. Performance impact of using MD5 is yet to be evaluated during our load testing cycle.

I started off trying to get hold of JSON response in the postHandle method of a Spring HandlerInterceptor. However since we are using @ResponseBody annotation in our controller, the JSON would be written directly to the stream. The ModelAndView was of course  null because of this reason. If we removed the annotation and returned ModelAndView from the controller, the intended JSON object got enclosed in a map wrapper. A quick question on stack overflow didn’t help as the only suggestion I got was to extract my original object from the map wrapper. I wanted to keep this option (as discussed here as well ) as my last resort.

The solution I eventually came up with involved

  1. Replacing the HandlerInterceptor with Servlet Filters
  2. Using DelegatingFilterProxy to make my filters spring application context aware
  3. Using HttpServletRequestWrapper to get control of the POST request body in the filter on the way in
  4. Using HttpServletResponseWrapper to get control of the response content in the filter on the way out

True, its probably a more complex solution than just overriding MappingJacksonJsonView and extracting my JSON object, but it is more generic as it does not assume that all my content will always be JSON.

Lets first start with the filter definition in the web.xml

<filter>
    <filter-name>cacheFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

...

<filter-mapping>
    <filter-name>cacheFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

A standard filter configuration except for the fact that the filter class is always going to be org.springframework.web.filter.DelegatingFilterProxy. Where do you specify your own class ? As a bean in your spring context xml. The name of the filter and the name of the bean must be the same for the delegation to happen.

<bean id="cacheFilter" class="com.x.x.memcacheFilter">
    <property name="cacheConfig" ref="cacheConfig"/>
</bean>

Using the DelegatingFilterProxy allowed me to use my Filters with Spring. I can inject my dependencies as I would normally. Next, lets look at my MemcacheFilter filter

Memcache Filter Class

public class MemcacheFilter implements Filter {

private static Logger logger = Logger.getLogger(MemcacheFilter.class);

private CacheConfig cacheConfig;

/**
* Memcached lookup is being performed in this method. Firstly, keys are
* generated depending on the request method (GET/POST). Then a cache lookup
* is performed. If a value is obtained, the value is written to the
* response otherwise, the actual target (in this case, Spring's Dispatcher
* Servlet) is called by calling doFilter on the filteChain. The dispatcher
* servlet calls the controller to produce the desire response which is
* intercepted when the doFilter method returns. The Response is added to
* the cache if the reponse code was 200(OK).
*
* @param request
* @param response
* @param filterChain
* @throws IOException
* @throws ServletException
*/
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain filterChain) throws IOException, ServletException {

try {

if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse)) {

        // Wrapping the response in HTTPServletResponseWrapper
        MemcacheResponseWrapper responseWrap = new MemcacheResponseWrapper((HttpServletResponse) response);

        // Wrapping the request in HTTPServletResponseWrapper
        MemcacheRequestWrapper requestWrap = new MemcacheRequestWrapper((HttpServletRequest) request);

        // Get Memcached Client Instance
        MemcachedClient client = cacheConfig.getMemcachedClient();

        Key keyGenerator = getKeyGenerator(requestWrap);

        if (keyGenerator != null) {

                String key = keyGenerator.getKey(requestWrap, cacheConfig);
                String value = (String) client.get(key);

                if (value == null) {
                // cache miss
                logger.info("Cache miss for key " + key);

                // call next filter/actual target for value
                filterChain.doFilter(requestWrap, responseWrap);

                if (responseWrap.getStatus() == HttpServletResponse.SC_OK) {

                   // obtaining response content from
                   // HttpServletResponseWrapper
                   value = responseWrap.getOutputStream().toString();

                   // adding response to cache
                   client.add(key, 0, value);

                   logger.info("Adding response to cache: "+ (value.length() > 50 ? value.substring(0,50) + "..." : value));
                } else {
                   logger.warn("Did not add content to cache as response status is not 200");
                }
        } else {
               // This case is a cache hit
               logger.info("Cache hit for key " + key);

               response.getWriter().println(value);
        }

} else {
        logger.warn("Request skipped because no key generator could be found for the request's method");
        // attempting call to actual target
        filterChain.doFilter(request, response);
}
}
} catch (Exception ex) {
        logger.info("Cache functionality skipped due to exception", ex);

        // attempting call to actual target
        filterChain.doFilter(request, response);
}
}

/**
* Factory method that returns KeyGenerator based on the request method.
*
* @param httpRequest
* @return
*/
private Key getKeyGenerator(HttpServletRequest httpRequest) {

Key keyGenerator = null;

   if (httpRequest.getMethod().equalsIgnoreCase("GET")) {
       keyGenerator = new GetRequestKey();
   } else if (httpRequest.getMethod().equalsIgnoreCase("POST")) {
       keyGenerator = new PostRequestKey();
   }

return keyGenerator;
}

public void init(FilterConfig arg0) throws ServletException {
    logger.debug("init");
}

public CacheConfig getCacheConfig() {
    return cacheConfig;
}

public void setCacheConfig(CacheConfig cacheConfig) {
    this.cacheConfig = cacheConfig;
}

public void destroy() {
    logger.debug("destroy");
}

}

1. I first wrap my request and response objects in the following statements. I have had to create the wrappers as well. Will get to those later.

// Wrapping the response in HTTPServletResponseWrapper
MemcacheResponseWrapper responseWrap = new MemcacheResponseWrapper((HttpServletResponse) response);

// Wrapping the request in HTTPServletResponseWrapper
MemcacheRequestWrapper requestWrap = new MemcacheRequestWrapper((HttpServletRequest) request);

2. Next, I have one of my injected classes, CacheConfig, provide me with a memcache client which I will use later to look up the cache.

// Get Memcached Client Instance
MemcachedClient client = cacheConfig.getMemcachedClient();

3. I make a call to a function that tells me which key generator I should use, a GET one or a POST one depending on the request method.

Key keyGenerator = getKeyGenerator(requestWrap);
/**
* Factory method that returns KeyGenerator based on the request method.
*
* @param httpRequest
* @return
*/
private Key getKeyGenerator(HttpServletRequest httpRequest) {

Key keyGenerator = null;

if (httpRequest.getMethod().equalsIgnoreCase("GET")) {
keyGenerator = new GetRequestKey();
} else if (httpRequest.getMethod().equalsIgnoreCase("POST")) {
keyGenerator = new PostRequestKey();
}

return keyGenerator;
}

4. Check for a cache hit using the Key returned by the Key Generator. If its a miss, call next filter or target to compute actual value, get value from the response wrapper, and add it to the cache.

if (keyGenerator != null) {

String key = keyGenerator.getKey(requestWrap, cacheConfig);
String value = (String) client.get(key);

if (value == null) {
// cache miss
logger.info("Cache miss for key " + key);

// call next filter/actual target for value
filterChain.doFilter(requestWrap, responseWrap);

if (responseWrap.getStatus() == HttpServletResponse.SC_OK) {

// obtaining response content from
// HttpServletResponseWrapper
value = responseWrap.getOutputStream().toString();

// adding response to cache
client.add(key, 0, value);

logger.info("Adding response to cache: "+ (value.length() > 50 ? value.substring(0,50) + "..." : value));
}

5. If its a cache hit, just get return cached value

else {
// This case is a cache hit
logger.info("Cache hit for key " + key);
response.getWriter().println(value);
}

Lets take a look at each of the Wrappers. I am not going into a a lot of detail into how each of these work.

Request Wrapper Class

On the way in, the original POST content is extracted from the request and put in a String Buffer. To the filter, this content is returned via the toString() method of the WrappedInputStream class whereas  the subsequently called controller calls the read method.

public class MemcacheRequestWrapper extends HttpServletRequestWrapper {

    protected ServletInputStream stream;
    protected HttpServletRequest origRequest = null;
    protected BufferedReader reader = null;

    public MemcacheRequestWrapper(HttpServletRequest request)
    throws IOException {

        super(request);
        origRequest = request;

    }

    public ServletInputStream createInputStream() throws IOException {
        return (new WrappedInputStream(origRequest));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (reader != null) {
            throw new IllegalStateException("getReader() has already been called for this request");
        }

        if (stream == null) {
            stream = createInputStream();
        }

        return stream;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        if (reader != null) {
            return reader;
        }

        if (stream != null) {
            throw new IllegalStateException("getReader() has already been called for this request");
        }

        stream = createInputStream();
        reader = new BufferedReader(new InputStreamReader(stream));

        return reader;
    }

    private class WrappedInputStream extends ServletInputStream {

        private StringBuffer originalInput = new StringBuffer();
        private HttpServletRequest originalRequest;
        private ByteArrayInputStream byteArrayInputStream;

        public WrappedInputStream(HttpServletRequest request) throws IOException {
            this.originalRequest = request;

            BufferedReader bufferedReader = null;
            try {
                InputStream inputStream = request.getInputStream();
                if (inputStream != null) {
                    bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                    char[] charBuffer = new char[128];
                    int bytesRead = -1;
                    while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {

                        originalInput.append(charBuffer, 0, bytesRead);
                    }
                }
                byteArrayInputStream = new ByteArrayInputStream(originalInput.toString().getBytes());
                } catch (IOException ex) {
                throw ex;
                } finally {
                if (bufferedReader != null) {
                    try {
                        bufferedReader.close();
                        } catch (IOException ex) {
                        throw ex;
                    }
                }
            }
        }

        @Override
        public String toString() {
            return this.originalInput.toString();
        }

        @Override
        public int read() throws IOException {
            return byteArrayInputStream.read();
        }

    }
}

Response Wrapper Class

The response wrapper is similar to the request wrapper. Instead of the read method, there is a write method, called by the controller when its writing JSON content. This is stored in the wrapper and called in the filter.

public class MemcacheResponseWrapper extends HttpServletResponseWrapper {

    protected ServletOutputStream stream;
    protected PrintWriter writer = null;
    protected HttpServletResponse origResponse = null;
    private int httpStatus = 200;

    public MemcacheResponseWrapper(HttpServletResponse response) {
        super(response);
        response.setContentType("application/json");
        origResponse = response;
    }

    public ServletOutputStream createOutputStream() throws IOException {
        return (new WrappedOutputStream(origResponse));
    }

    public ServletOutputStream getOutputStream() throws IOException {
        if (writer != null) {
            throw new IllegalStateException("getWriter() has already been called for this response");
        }

        if (stream == null) {
            stream = createOutputStream();
        }

        return stream;
    }

    public PrintWriter getWriter() throws IOException {
        if (writer != null) {
            return writer;
        }

        if (stream != null) {
            throw new IllegalStateException("getOutputStream() has already been called for this response");
        }

        stream = createOutputStream();
        writer = new PrintWriter(stream);

        return writer;
    }

    @Override
    public void sendError(int sc) throws IOException {
        httpStatus = sc;
        super.sendError(sc);
    }

    @Override
    public void sendError(int sc, String msg) throws IOException {
        httpStatus = sc;
        super.sendError(sc, msg);
    }

    @Override
    public void setStatus(int sc) {
        httpStatus = sc;
        super.setStatus(sc);
    }

    public int getStatus() {
        return httpStatus;
    }

    private class WrappedOutputStream extends ServletOutputStream {

        private StringBuffer originalOutput = new StringBuffer();
        private HttpServletResponse originalResponse;

        public WrappedOutputStream(HttpServletResponse response) {
            this.originalResponse = response;
        }

        @Override
        public String toString() {
            return this.originalOutput.toString();
        }

        @Override
        public void write(int arg0) throws IOException {

            originalOutput.append((char) arg0);
            originalResponse.getOutputStream().write(arg0);
        }

    }
}




Published at DZone with permission of Faheem Sohail, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Lajos Papp replied on Tue, 2013/06/25 - 3:11pm

Did you considered to use Spring's Cache Abstraction: http://static.springsource.org/spring/docs/3.1.0.M1/spring-framework-reference/html/cache.html

By default they offer 2 implementations:

  •  jdk's ConcurrentHashMap based: quite simple, but no external dependencies
  •  ehcache based
There is even an opensource memcached based implementation: https://code.google.com/p/simple-spring-memcached/

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.