Статьи

Реализация Memcached фильтра сервлетов для сервисов RESTful на основе Spring MVC

У меня есть несколько сервисов RESTful на основе Spring MVC, которые возвращают JSON. В 90% случаев состояние объектов, возвращаемых этими службами, не изменится в течение 24 часов. Это делает их (объекты JSON) идеальными кандидатами для простого кэширования, включенного memcached. Идея состояла в том, чтобы перехватывать каждый запрос к контроллерам Spring, генерировать ключ кеша и проверять его по кешу. Если ключ и соответствующее значение (строка JSON) доступны (попадание в кэш), они возвращаются вызывающей стороне как есть, не совершая полного обращения к базе данных. Однако, если в кеше нет записи для ключа и, следовательно, нет соответствующего значения (пропуск кеша), вызов перенаправляется в контроллер, который, в свою очередь, вызывает логику для получения нужного объекта из базы данных, а не только возвращает его в вызывающий, но также обновить кэш с возвращенным содержимым.

Ключи генерируются с использованием URL-адреса службы в случае запросов GET и URL-адреса, объединенного с вводом POST (как JSON) в случае запросов POST. Результирующие строки кодируются с помощью MD5 для получения 32-символьного ключа кэша, который находится в пределах ограничения длины ключа 250 символов в memcached. Влияние производительности на использование MD5 еще предстоит оценить в ходе нашего цикла нагрузочных испытаний.

Я начал с попытки получить ответ JSON в методе PostHandle Spring HandlerInterceptor. Однако, поскольку мы используем аннотацию @ResponseBody в нашем контроллере, JSON будет записан непосредственно в поток. ModelAndView, конечно, был нулевым по этой причине. Если мы удалили аннотацию и вернули ModelAndView из контроллера, намеченный объект JSON был заключен в оболочку карты. Быстрый вопрос о переполнении стека не помог, так как единственное предложение, которое я получил, было извлечь мой оригинальный объект из оболочки карты. Я хотел сохранить эту опцию (как обсуждено здесь также) как мое последнее средство.

Решение, которое я в итоге придумала

  1. Замена HandlerInterceptor сервлетными фильтрами
  2. Использование DelegatingFilterProxy, чтобы мои фильтры создавали контекст приложения
  3. Использование HttpServletRequestWrapper для получения контроля над телом запроса POST в фильтре на пути к
  4. Использование HttpServletResponseWrapper для получения контроля над содержимым ответа в фильтре на выходе

Правда, это, вероятно, более сложное решение, чем просто переопределение MappingJacksonJsonView и извлечение моего объекта JSON, но оно более общее, так как не предполагает, что весь мой контент всегда будет JSON.

Давайте сначала начнем с определения фильтра в 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>

Стандартная конфигурация фильтра, за исключением того факта, что класс фильтра всегда будет org.springframework.web.filter.DelegatingFilterProxy. Где вы указываете свой собственный класс? Как боб в вашем весеннем контексте xml. Имя фильтра и имя компонента должны совпадать, чтобы произошло делегирование.

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

Использование DelegatingFilterProxy позволило мне использовать мои фильтры с Spring. Я могу ввести свои зависимости, как обычно. Далее, давайте посмотрим на мой фильтр MemcacheFilter

Класс фильтра Memcache

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. Сначала я обертываю свои объекты запроса и ответа в следующие утверждения. Я должен был также создать обертки. Доберусь до тех позже.

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

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

2. Затем у меня есть один из моих внедренных классов, CacheConfig, предоставляющий мне клиент memcache, который я буду использовать позже для поиска в кэше.

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

3. Я вызываю функцию, которая сообщает мне, какой генератор ключей мне следует использовать: GET или POST в зависимости от метода запроса.

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. Проверьте наличие попадания в кэш, используя ключ, возвращаемый генератором ключей. Если это не так, вызовите следующий фильтр или цель для вычисления фактического значения, получите значение из оболочки ответа и добавьте его в кэш.

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. Если попадание в кеш, просто получите возвращаемое кешированное значение

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

Давайте посмотрим на каждого из оболочек. Я не буду вдаваться в подробности того, как каждый из них работает.

Запросить класс Wrapper

По пути исходное содержимое POST извлекается из запроса и помещается в буфер строк. В фильтр это содержимое возвращается через метод toString () класса WrappedInputStream, тогда как впоследствии вызываемый контроллер вызывает метод read.

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

    }
}

Класс Обертки Ответа

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

    }
}