How to add client side (browser) caching

Modern web development tend to use fronted frameworks with API rest calls to hybris backend. Such approach skips hybris CMS caching system and can lead to performance issues. One of the possible improvement would be to utilize caching on browser side and in such way decrease amount of requests send to server by browser.

Web browsers uses Cache-Control HTTP header to identify how long response could be stored in an internal cache. Spring MVC provides HandlerInterceptor interface, which allows to add common handler behavior for each request. Exists WebContentInterceptor implementation, which adds Cache-Control headers. Unfortunately, spring mvc implementation ignores hybris cms cache region settings and can’t be configured to skip adding of Cache-Control header for 301 Redirect responses.

In below implementation of WebContentInterceptor, which uses hybris cms cache region settings to cache HTTP GET methods with 200 response code, adding of Cache-Control header is moved into postHandle from preHandle, because preHandle is executed before Controller code, and in Controller could be present redirect. postHandle is executed after Contoller but before View(JSP) rendering, which suits our needs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

package com.blog.storefront;

import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import de.hybris.platform.servicelayer.config.ConfigurationService;
import org.apache.log4j.Logger;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.WebContentInterceptor;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static org.apache.log4j.Logger.getLogger;

public class HybrisWebContentInterceptor extends WebContentInterceptor {

    private static final Logger LOG = getLogger(HybrisWebContentInterceptor.class);
    private static final String CMS_CACHE_ENABLED_KEY = "cms.cache.enabled";

    private final Supplier<Boolean> USE_CACHE = Suppliers.memoizeWithExpiration(getCMSCacheConfigurationValue(), 1, TimeUnit.MINUTES);

    private ConfigurationService configurationService;

    public HybrisWebContentInterceptor(ConfigurationService configurationService, Map<String, CacheControl> cacheControlMap) {
        super();
        this.configurationService = configurationService;

        for (Map.Entry<String, CacheControl> entry : cacheControlMap.entrySet()) {
            addCacheMapping(entry.getValue(), entry.getKey());
        }
        setSupportedMethods(HttpMethod.GET.name());
    }


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException {
        // We don't want to add cache control for 301 redirects, that`s why logic is moved in postHandle.
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        if (isNotRedirectView(request, modelAndView)) {
            addCacheControlHeader(request, response, handler);
        }
    }

    public void addCacheControlHeader(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // ! We don't want to cache 301 redirects
        if (USE_CACHE.get() && HttpStatus.OK.value() == response.getStatus()) {
            try {
                super.preHandle(request, response, handler);
            } catch (ServletException e) {
                LOG.warn(e.toString());
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Exception:", e);
                }
            }
        }
    }

    private boolean isNotRedirectView(HttpServletRequest request, ModelAndView modelAndView) {
        return modelAndView != null && isNotIncludeRequest(request) && isNotRedirectView(modelAndView);
    }


    private boolean isNotIncludeRequest(final HttpServletRequest request) {
        return request.getAttribute("javax.servlet.include.request_uri") == null;
    }

    private boolean isNotRedirectView(final ModelAndView modelAndView) {
        final String viewName = modelAndView.getViewName();
        return viewName != null && !viewName.startsWith("redirect:");
    }


    private Supplier<Boolean> getCMSCacheConfigurationValue() {
        return () -> configurationService.getConfiguration().getBoolean(CMS_CACHE_ENABLED_KEY, false);
    }

}

Instance of handler should be configured via spring and added to list of MVC interceptors:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

<mvc:interceptors>
    <!-- Browser side caching for FE components -->
    <ref bean="cacheControlInterceptor"/>
</mvc:interceptors>

<alias name="hybrisCacheControlInterceptor" alias="cacheControlInterceptor"/>
<bean id="hybrisCacheControlInterceptor" class="com.blog.storefront.HybrisWebContentInterceptor">
<constructor-arg ref="configurationService"/>
<constructor-arg ref="cacheControlMapping"/>
</bean>

<util:map id="cacheControlMapping" key-type="java.lang.String"
          value-type="org.springframework.http.CacheControl">
<entry key="/" value-ref="cmsRegionTTLCacheControl"/>
<entry key="/**/some/html/page/**" value-ref="cmsRegionTTLCacheControl"/>
</util:map>

<bean id="cmsRegionTTLCacheControl" class="org.springframework.http.CacheControl" factory-method="maxAge">
<constructor-arg index="0" value="${regioncache.cmsregion.ttl}"/>
<constructor-arg index="1">
    <value type="java.util.concurrent.TimeUnit">SECONDS</value>
</constructor-arg>
</bean>

But for requests wrapped with @ResponseBody annotation Cache-Control header would not be added, because in postHandle response would come as committed, so changes would not be applied. Such behavior is expected according to github issue 13864 and according to issue 15486 for such case must be used ResponseBodyAdvice. So we need to implement ResponseBodyAdvice to add Cache-Control header for Controllers wrapped with @ResponseBody and still use HandlerInterceptor for other Controllers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.blog.storefront;

import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@ControllerAdvice
public class CacheControlResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    private HybrisWebContentInterceptor hybrisWebContentInterceptor;

    public CacheControlResponseBodyAdvice(HybrisWebContentInterceptor hybrisWebContentInterceptor) {
        this.hybrisWebContentInterceptor = hybrisWebContentInterceptor;
    }

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (serverHttpResponse instanceof ServletServerHttpResponse && serverHttpRequest instanceof ServletServerHttpRequest) {
            HttpServletResponse response = ((ServletServerHttpResponse) (serverHttpResponse)).getServletResponse();
            HttpServletRequest request = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest();

            hybrisWebContentInterceptor.addCacheControlHeader(request, response, this);
        }

        return o;
    }

}

And bean definition:

1
2
3
4

<bean id="cacheControlResponseBodyAdvice" class="com.blog.storefront.CacheControlResponseBodyAdvice">
    <constructor-arg ref="hybrisCacheControlInterceptor"/>
</bean>
comments powered by Disqus