001/* 002 * Copyright 2002-2019 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.servlet.view; 018 019import java.util.LinkedHashMap; 020import java.util.Locale; 021import java.util.Map; 022import java.util.concurrent.ConcurrentHashMap; 023 024import javax.servlet.http.HttpServletRequest; 025import javax.servlet.http.HttpServletResponse; 026 027import org.springframework.lang.Nullable; 028import org.springframework.util.Assert; 029import org.springframework.web.context.support.WebApplicationObjectSupport; 030import org.springframework.web.servlet.View; 031import org.springframework.web.servlet.ViewResolver; 032 033/** 034 * Convenient base class for {@link org.springframework.web.servlet.ViewResolver} 035 * implementations. Caches {@link org.springframework.web.servlet.View} objects 036 * once resolved: This means that view resolution won't be a performance problem, 037 * no matter how costly initial view retrieval is. 038 * 039 * <p>Subclasses need to implement the {@link #loadView} template method, 040 * building the View object for a specific view name and locale. 041 * 042 * @author Rod Johnson 043 * @author Juergen Hoeller 044 * @see #loadView 045 */ 046public abstract class AbstractCachingViewResolver extends WebApplicationObjectSupport implements ViewResolver { 047 048 /** Default maximum number of entries for the view cache: 1024. */ 049 public static final int DEFAULT_CACHE_LIMIT = 1024; 050 051 /** Dummy marker object for unresolved views in the cache Maps. */ 052 private static final View UNRESOLVED_VIEW = new View() { 053 @Override 054 @Nullable 055 public String getContentType() { 056 return null; 057 } 058 @Override 059 public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) { 060 } 061 }; 062 063 /** Default cache filter that always caches. */ 064 private static final CacheFilter DEFAULT_CACHE_FILTER = (view, viewName, locale) -> true; 065 066 067 /** The maximum number of entries in the cache. */ 068 private volatile int cacheLimit = DEFAULT_CACHE_LIMIT; 069 070 /** Whether we should refrain from resolving views again if unresolved once. */ 071 private boolean cacheUnresolved = true; 072 073 /** Filter function that determines if view should be cached. */ 074 private CacheFilter cacheFilter = DEFAULT_CACHE_FILTER; 075 076 /** Fast access cache for Views, returning already cached instances without a global lock. */ 077 private final Map<Object, View> viewAccessCache = new ConcurrentHashMap<>(DEFAULT_CACHE_LIMIT); 078 079 /** Map from view key to View instance, synchronized for View creation. */ 080 @SuppressWarnings("serial") 081 private final Map<Object, View> viewCreationCache = 082 new LinkedHashMap<Object, View>(DEFAULT_CACHE_LIMIT, 0.75f, true) { 083 @Override 084 protected boolean removeEldestEntry(Map.Entry<Object, View> eldest) { 085 if (size() > getCacheLimit()) { 086 viewAccessCache.remove(eldest.getKey()); 087 return true; 088 } 089 else { 090 return false; 091 } 092 } 093 }; 094 095 096 /** 097 * Specify the maximum number of entries for the view cache. 098 * Default is 1024. 099 */ 100 public void setCacheLimit(int cacheLimit) { 101 this.cacheLimit = cacheLimit; 102 } 103 104 /** 105 * Return the maximum number of entries for the view cache. 106 */ 107 public int getCacheLimit() { 108 return this.cacheLimit; 109 } 110 111 /** 112 * Enable or disable caching. 113 * <p>This is equivalent to setting the {@link #setCacheLimit "cacheLimit"} 114 * property to the default limit (1024) or to 0, respectively. 115 * <p>Default is "true": caching is enabled. 116 * Disable this only for debugging and development. 117 */ 118 public void setCache(boolean cache) { 119 this.cacheLimit = (cache ? DEFAULT_CACHE_LIMIT : 0); 120 } 121 122 /** 123 * Return if caching is enabled. 124 */ 125 public boolean isCache() { 126 return (this.cacheLimit > 0); 127 } 128 129 /** 130 * Whether a view name once resolved to {@code null} should be cached and 131 * automatically resolved to {@code null} subsequently. 132 * <p>Default is "true": unresolved view names are being cached, as of Spring 3.1. 133 * Note that this flag only applies if the general {@link #setCache "cache"} 134 * flag is kept at its default of "true" as well. 135 * <p>Of specific interest is the ability for some AbstractUrlBasedView 136 * implementations (FreeMarker, Tiles) to check if an underlying resource 137 * exists via {@link AbstractUrlBasedView#checkResource(Locale)}. 138 * With this flag set to "false", an underlying resource that re-appears 139 * is noticed and used. With the flag set to "true", one check is made only. 140 */ 141 public void setCacheUnresolved(boolean cacheUnresolved) { 142 this.cacheUnresolved = cacheUnresolved; 143 } 144 145 /** 146 * Return if caching of unresolved views is enabled. 147 */ 148 public boolean isCacheUnresolved() { 149 return this.cacheUnresolved; 150 } 151 152 /** 153 * Sets the filter that determines if view should be cached. 154 * Default behaviour is to cache all views. 155 * @since 5.2 156 */ 157 public void setCacheFilter(CacheFilter cacheFilter) { 158 Assert.notNull(cacheFilter, "CacheFilter must not be null"); 159 this.cacheFilter = cacheFilter; 160 } 161 162 /** 163 * Return filter function that determines if view should be cached. 164 * @since 5.2 165 */ 166 public CacheFilter getCacheFilter() { 167 return this.cacheFilter; 168 } 169 170 @Override 171 @Nullable 172 public View resolveViewName(String viewName, Locale locale) throws Exception { 173 if (!isCache()) { 174 return createView(viewName, locale); 175 } 176 else { 177 Object cacheKey = getCacheKey(viewName, locale); 178 View view = this.viewAccessCache.get(cacheKey); 179 if (view == null) { 180 synchronized (this.viewCreationCache) { 181 view = this.viewCreationCache.get(cacheKey); 182 if (view == null) { 183 // Ask the subclass to create the View object. 184 view = createView(viewName, locale); 185 if (view == null && this.cacheUnresolved) { 186 view = UNRESOLVED_VIEW; 187 } 188 if (view != null && this.cacheFilter.filter(view, viewName, locale)) { 189 this.viewAccessCache.put(cacheKey, view); 190 this.viewCreationCache.put(cacheKey, view); 191 } 192 } 193 } 194 } 195 else { 196 if (logger.isTraceEnabled()) { 197 logger.trace(formatKey(cacheKey) + "served from cache"); 198 } 199 } 200 return (view != UNRESOLVED_VIEW ? view : null); 201 } 202 } 203 204 private static String formatKey(Object cacheKey) { 205 return "View with key [" + cacheKey + "] "; 206 } 207 208 /** 209 * Return the cache key for the given view name and the given locale. 210 * <p>Default is a String consisting of view name and locale suffix. 211 * Can be overridden in subclasses. 212 * <p>Needs to respect the locale in general, as a different locale can 213 * lead to a different view resource. 214 */ 215 protected Object getCacheKey(String viewName, Locale locale) { 216 return viewName + '_' + locale; 217 } 218 219 /** 220 * Provides functionality to clear the cache for a certain view. 221 * <p>This can be handy in case developer are able to modify views 222 * (e.g. FreeMarker templates) at runtime after which you'd need to 223 * clear the cache for the specified view. 224 * @param viewName the view name for which the cached view object 225 * (if any) needs to be removed 226 * @param locale the locale for which the view object should be removed 227 */ 228 public void removeFromCache(String viewName, Locale locale) { 229 if (!isCache()) { 230 logger.warn("Caching is OFF (removal not necessary)"); 231 } 232 else { 233 Object cacheKey = getCacheKey(viewName, locale); 234 Object cachedView; 235 synchronized (this.viewCreationCache) { 236 this.viewAccessCache.remove(cacheKey); 237 cachedView = this.viewCreationCache.remove(cacheKey); 238 } 239 if (logger.isDebugEnabled()) { 240 // Some debug output might be useful... 241 logger.debug(formatKey(cacheKey) + 242 (cachedView != null ? "cleared from cache" : "not found in the cache")); 243 } 244 } 245 } 246 247 /** 248 * Clear the entire view cache, removing all cached view objects. 249 * Subsequent resolve calls will lead to recreation of demanded view objects. 250 */ 251 public void clearCache() { 252 logger.debug("Clearing all views from the cache"); 253 synchronized (this.viewCreationCache) { 254 this.viewAccessCache.clear(); 255 this.viewCreationCache.clear(); 256 } 257 } 258 259 260 /** 261 * Create the actual View object. 262 * <p>The default implementation delegates to {@link #loadView}. 263 * This can be overridden to resolve certain view names in a special fashion, 264 * before delegating to the actual {@code loadView} implementation 265 * provided by the subclass. 266 * @param viewName the name of the view to retrieve 267 * @param locale the Locale to retrieve the view for 268 * @return the View instance, or {@code null} if not found 269 * (optional, to allow for ViewResolver chaining) 270 * @throws Exception if the view couldn't be resolved 271 * @see #loadView 272 */ 273 @Nullable 274 protected View createView(String viewName, Locale locale) throws Exception { 275 return loadView(viewName, locale); 276 } 277 278 /** 279 * Subclasses must implement this method, building a View object 280 * for the specified view. The returned View objects will be 281 * cached by this ViewResolver base class. 282 * <p>Subclasses are not forced to support internationalization: 283 * A subclass that does not may simply ignore the locale parameter. 284 * @param viewName the name of the view to retrieve 285 * @param locale the Locale to retrieve the view for 286 * @return the View instance, or {@code null} if not found 287 * (optional, to allow for ViewResolver chaining) 288 * @throws Exception if the view couldn't be resolved 289 * @see #resolveViewName 290 */ 291 @Nullable 292 protected abstract View loadView(String viewName, Locale locale) throws Exception; 293 294 295 /** 296 * Filter that determines if view should be cached. 297 * 298 * @author Sergey Galkin 299 * @author Arjen Poutsma 300 * @since 5.2 301 */ 302 @FunctionalInterface 303 public interface CacheFilter { 304 305 /** 306 * Indicates whether the given view should be cached. 307 * The name and locale used to resolve the view are also provided. 308 * @param view the view 309 * @param viewName the name used to resolve the {@code view} 310 * @param locale the locale used to resolve the {@code view} 311 * @return {@code true} if the view should be cached; {@code false} otherwise 312 */ 313 boolean filter(View view, String viewName, Locale locale); 314 } 315 316}