DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Composite Requests in Salesforce Are a Great Idea
  • Spring Boot - How To Use Native SQL Queries | Restful Web Services
  • Building REST API Backend Easily With Ballerina Language
  • Aggregating REST APIs Calls Using Apache Camel

Trending

  • Navigating Change Management: A Guide for Engineers
  • Enhancing Business Decision-Making Through Advanced Data Visualization Techniques
  • Using Python Libraries in Java
  • Can You Run a MariaDB Cluster on a $150 Kubernetes Lab? I Gave It a Shot
  1. DZone
  2. Data Engineering
  3. Data
  4. Implementing Memcached a Servlet Filter for Spring MVC-Based RESTful Services

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

By 
Faheem Sohail user avatar
Faheem Sohail
·
Jun. 25, 13 · Tutorial
Likes (1)
Comment
Save
Tweet
Share
22.3K Views

Join the DZone community and get the full member experience.

Join For Free

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);
        }

    }
}




Filter (software) Spring Framework Memcached Cache (computing) REST Web Protocols JSON Requests Database

Published at DZone with permission of Faheem Sohail, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Composite Requests in Salesforce Are a Great Idea
  • Spring Boot - How To Use Native SQL Queries | Restful Web Services
  • Building REST API Backend Easily With Ballerina Language
  • Aggregating REST APIs Calls Using Apache Camel

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!