У меня есть несколько сервисов 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 был заключен в оболочку карты. Быстрый вопрос о переполнении стека не помог, так как единственное предложение, которое я получил, было извлечь мой оригинальный объект из оболочки карты. Я хотел сохранить эту опцию (как обсуждено здесь также) как мое последнее средство.
Решение, которое я в итоге придумала
- Замена HandlerInterceptor сервлетными фильтрами
- Использование DelegatingFilterProxy, чтобы мои фильтры создавали контекст приложения
- Использование HttpServletRequestWrapper для получения контроля над телом запроса POST в фильтре на пути к
- Использование 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); } } }