У меня есть несколько сервисов 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);
}
}
}