The nine components in SpringMVC have been shared several times before, and today we continue our analysis of the view parser.

Songo actually shared the view resolver in a previous article, which was to solve the problem of multiple views co-existing. If you haven’t read that article, you can check it out:

  • How can there be multiple view parsers in SpringMVC

ViewResolver is the ViewResolver that we love. If you have used SpringMVC, you know that there is a ViewResolver in SpringMVC. Today we will analyze how the ViewResolver works.

1. An overview

First let’s take a look at what the ViewResolver interface looks like:

public interface ViewResolver {
	@Nullable
	View resolveViewName(String viewName, Locale locale) throws Exception;
}
Copy the code

There’s only one method in this interface, and as you can see, it’s pretty simple, just find the corresponding View and return it based on the View name and Locale.

As shown in the figure, there are four classes directly inherited from the ViewResolver interface, with the following functions:

  • ContentNegotiatingViewResolver: support MediaType views and postfix parser.
  • BeanNameViewResolver: This simply looks up the Bean in the Spring container based on the view name and returns it.
  • AbstractCachingViewResolver: a view of the parser caching function.
  • ViewResolverComposite: This is a composite view parser that can then be used to proxy other view parsers for specific tasks.

Let’s take a look at each of the four view resolvers, starting with the simplest BeanNameViewResolver.

2.BeanNameViewResolver

BeanNameViewResolver (BeanNameViewResolver, BeanNameViewResolver, BeanNameViewResolver);

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws BeansException {
	ApplicationContext context = obtainApplicationContext();
	if(! context.containsBean(viewName)) {return null;
	}
	if(! context.isTypeMatch(viewName, View.class)) {return null;
	}
	return context.getBean(viewName, View.class);
}
Copy the code

First check whether there is a corresponding Bean, and then check whether the Bean type is correct, no problem, directly find the return can be.

3.ContentNegotiatingViewResolver

ContentNegotiatingViewResolver is currently widely used a view of the parser, mainly added support for MediaType. ContentNegotiatingViewResolver this Spring3.0 is introduced in the view of the parser, it is not responsible for a specific view resolution, but according to the current request of MIME types, choose a suitable view from the context of the parser, and will be asked to work entrusted to it.

Here we see first ContentNegotiatingViewResolver# resolveViewName method:

public View resolveViewName(String viewName, Locale locale) throws Exception {
	RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
	List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
	if(requestedMediaTypes ! =null) {
		List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
		View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
		if(bestView ! =null) {
			returnbestView; }}if (this.useNotAcceptableStatusCode) {
		return NOT_ACCEPTABLE_VIEW;
	}
	else {
		return null; }}Copy the code

The code logic here is also simple:

  • The first step is to get the current request object, which can be retrieved directly from the RequestContextHolder. MediaType is then extracted from the current request object.
  • If MediaType is not null, the appropriate View parser is found based on MediaType and the parsed View is returned.
  • If the MediaType is null, as the two cases, if useNotAcceptableStatusCode is true, it returns NOT_ACCEPTABLE_VIEW view, this view is a 406 response, said the client error, The server could not provide a response matching the values specified in the Accept-Charset and Accept-Language headers; If useNotAcceptableStatusCode to false, it returns null.

The getCandidateViews method and getBestView method are the core of the problem. The former method is to get all the candidate views, and the latter method is to choose the best View from these candidate views. Let’s look at them one by one.

GetCandidateViews:

private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
		throws Exception {
	List<View> candidateViews = new ArrayList<>();
	if (this.viewResolvers ! =null) {
		for (ViewResolver viewResolver : this.viewResolvers) {
			View view = viewResolver.resolveViewName(viewName, locale);
			if(view ! =null) {
				candidateViews.add(view);
			}
			for (MediaType requestedMediaType : requestedMediaTypes) {
				List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
				for (String extension : extensions) {
					String viewNameWithExtension = viewName + '. ' + extension;
					view = viewResolver.resolveViewName(viewNameWithExtension, locale);
					if(view ! =null) {
						candidateViews.add(view);
					}
				}
			}
		}
	}
	if(! CollectionUtils.isEmpty(this.defaultViews)) {
		candidateViews.addAll(this.defaultViews);
	}
	return candidateViews;
}
Copy the code

Getting all candidate views is a two-step process:

  1. Call the resolveViewName method in each ViewResolver to load the corresponding View object.
  2. Extract the extension according to MediaType, and then load the View object according to the extension. In practical applications, this step is rarely configured, so the step is basically not loaded out of the View object, mainly rely on the first step.

The first step is to load the View object according to your viewName and the prefix, suffix, templateLocation attributes configured in the ViewResolver. ResolveViewName ->createView->loadView

I will not post the specific implementation method, the only important thing to say is the last loadView method, let’s take a look at this method:

protected View loadView(String viewName, Locale locale) throws Exception {
	AbstractUrlBasedView view = buildView(viewName);
	View result = applyLifecycleMethods(viewName, view);
	return (view.checkResource(locale) ? result : null);
}
Copy the code

In this method, when the View is loaded, it calls its checkResource method to determine whether the View exists, and returns the View if it does, and null if it does not.

This is a very critical step, but our common views handle it differently:

  • FreeMarkerView: Checks honestly.
  • ThymeleafView: This is not checked (Thymeleaf’s entire View architecture is different from FreeMarkerView and JstlView).
  • JstlView: Check results always return true.

At this point, we have all the candidate views, but you need to be aware that the candidate View does not necessarily exist, that the candidate View may not be available in the case of Thymeleaf, and that the candidate View may not actually exist in the case of JstlView.

The getBestView method is then called to find the best View from all the candidate views. The logic of the getBestView method is simple. It looks for the MediaType of all views and matches it with the requested Array of MediaType. The first one that matches is the best View. So it’s possible to pick a view that doesn’t exist at all and end up 404.

This is ContentNegotiatingViewResolver# resolveViewName the working process of the method.

So here also involves a problem, the ViewResolver ContentNegotiatingViewResolver come from? This comes from two sources: default and manually configured. Let’s look at the following initialization code:

@Override
protected void initServletContext(ServletContext servletContext) {
	Collection<ViewResolver> matchingBeans =
			BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
	if (this.viewResolvers == null) {
		this.viewResolvers = new ArrayList<>(matchingBeans.size());
		for (ViewResolver viewResolver : matchingBeans) {
			if (this! = viewResolver) {this.viewResolvers.add(viewResolver); }}}else {
		for (int i = 0; i < this.viewResolvers.size(); i++) {
			ViewResolver vr = this.viewResolvers.get(i);
			if (matchingBeans.contains(vr)) {
				continue;
			}
			String name = vr.getClass().getName() + i;
			obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
		}
	}
	AnnotationAwareOrderComparator.sort(this.viewResolvers);
	this.cnmFactoryBean.setServletContext(servletContext);
}
Copy the code
  1. First I get matchingBeans, which is getting all the view parsers in the Spring container.
  2. If viewResolvers variable is null, that is, the developers didn’t give ContentNegotiatingViewResolver parser configuration view, at this point would give viewResolvers find matchingBeans assignment.
  3. If developers for ContentNegotiatingViewResolver configuration view related to the parser, to check whether these views parser exists in matchingBeans, if not, are initialized.

This is what ContentNegotiatingViewResolver do.

4.AbstractCachingViewResolver

One of the things about views is that they don’t change much once they’re developed, so it’s important to cache them for faster loading. In fact we use most of the views of the parser is support caching function, namely AbstractCachingViewResolver, in fact, there are a lot of place.

Let’s learn about AbstractCachingViewResolver roughly, and then to study its subclasses.

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
	if(! isCache()) {return createView(viewName, locale);
	}
	else {
		Object cacheKey = getCacheKey(viewName, locale);
		View view = this.viewAccessCache.get(cacheKey);
		if (view == null) {
			synchronized (this.viewCreationCache) {
				view = this.viewCreationCache.get(cacheKey);
				if (view == null) {
					view = createView(viewName, locale);
					if (view == null && this.cacheUnresolved) {
						view = UNRESOLVED_VIEW;
					}
					if(view ! =null && this.cacheFilter.filter(view, viewName, locale)) {
						this.viewAccessCache.put(cacheKey, view);
						this.viewCreationCache.put(cacheKey, view); }}}}else{}return(view ! = UNRESOLVED_VIEW ? view :null); }}Copy the code
  1. First, if caching is not enabled, call createView directly to create the view return.
  2. Call the getCacheKey method to get the cached key.
  3. Go to viewAccessCache and look for the cache View, and return it if you find it.
  4. Find a cache View in viewCreationCache, return it if it finds one, call createView to create a new View, and place the View in two cache pools.
  5. There are two cache pools. The difference is that viewAccessCache is of type ConcurrentHashMap and viewCreationCache is of type LinkedHashMap. The former supports concurrent access and is very efficient. The latter limits the maximum number of caches and is less efficient than the former. When the latter cache reaches its maximum, the elements in it are automatically deleted. In the process of deleting its own elements, the corresponding elements in the former viewAccessCache are also deleted.

So there’s another method involved here, createView, and let’s take a look at it a little bit:

@Nullable
protected View createView(String viewName, Locale locale) throws Exception {
	return loadView(viewName, locale);
}
@Nullable
protected abstract View loadView(String viewName, Locale locale) throws Exception;
Copy the code

CreateView calls loadView. LoadView is an abstract method that needs to be subclassed.

This is the lookup process of caching the View.

Directly inherited AbstractCachingViewResolver view parser has four kinds: ResourceBundleViewResolver, XmlViewResolver, UrlBasedViewResolver and ThymeleafViewResolver, of which the first two from Spring5.3 start has been abandoned, So songo won’t say much here, but let’s focus on the latter two.

4.1 UrlBasedViewResolver

UrlBasedViewResolver overrides getCacheKey, createView, loadView;

getCacheKey

@Override
protected Object getCacheKey(String viewName, Locale locale) {
	return viewName;
}
Copy the code

The parent getCacheKey is viewName + ‘_’ + locale, now viewName.

createView

@Override
protected View createView(String viewName, Locale locale) throws Exception {
	if(! canHandle(viewName, locale)) {return null;
	}
	if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
		String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
		RedirectView view = new RedirectView(redirectUrl,
				isRedirectContextRelative(), isRedirectHttp10Compatible());
		String[] hosts = getRedirectHosts();
		if(hosts ! =null) {
			view.setHosts(hosts);
		}
		return applyLifecycleMethods(REDIRECT_URL_PREFIX, view);
	}
	if (viewName.startsWith(FORWARD_URL_PREFIX)) {
		String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
		InternalResourceView view = new InternalResourceView(forwardUrl);
		return applyLifecycleMethods(FORWARD_URL_PREFIX, view);
	}
	return super.createView(viewName, locale);
}
Copy the code
  1. The canHandle method is first called to determine whether the logical view here is supported.
  2. Next check whether the logical view name prefix is correctredirect:If yes, it indicates that this is a redirected view, and the RedirectView is constructed for processing.
  3. Next check whether the logical view name prefix is correctforward:If yes, it indicates that this is a server jump and InternalResourceView is constructed to process it.
  4. If none of the preceding is true, the createView method of the parent class is called to build the view, which eventually calls the loadView method of the subclass.

loadView

@Override
protected View loadView(String viewName, Locale locale) throws Exception {
	AbstractUrlBasedView view = buildView(viewName);
	View result = applyLifecycleMethods(viewName, view);
	return (view.checkResource(locale) ? result : null);
}
Copy the code

There are three things going on here:

  1. Call the buildView method to build the View.
  2. Call the applyLifecycleMethods method to initialize the View.
  3. Check whether the car View exists and return.

The third step, which is fairly straightforward, is to check that the view file exists, which is checked by Jsp view parsers and Freemarker view parsers, but not by Thymeleaf (see: How to have multiple view parsers in SpringMVC). I’m going to focus on the first two steps, and Songo is going to talk to you about them, but there are two more methods we’re going to use: buildView and applyLifecycleMethods.

4.1.1 buildView

This method is used to build the view:

protected AbstractUrlBasedView buildView(String viewName) throws Exception {
	AbstractUrlBasedView view = instantiateView();
	view.setUrl(getPrefix() + viewName + getSuffix());
	view.setAttributesMap(getAttributesMap());
	String contentType = getContentType();
	if(contentType ! =null) {
		view.setContentType(contentType);
	}
	String requestContextAttribute = getRequestContextAttribute();
	if(requestContextAttribute ! =null) {
		view.setRequestContextAttribute(requestContextAttribute);
	}
	Boolean exposePathVariables = getExposePathVariables();
	if(exposePathVariables ! =null) {
		view.setExposePathVariables(exposePathVariables);
	}
	Boolean exposeContextBeansAsAttributes = getExposeContextBeansAsAttributes();
	if(exposeContextBeansAsAttributes ! =null) {
		view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);
	}
	String[] exposedContextBeanNames = getExposedContextBeanNames();
	if(exposedContextBeanNames ! =null) {
		view.setExposedContextBeanNames(exposedContextBeanNames);
	}
	return view;
}
Copy the code
  1. We first call the instantiateView method and build a View object to return based on the viewClass we provided when configuring the View parser.
  2. Configure the URL for the view, which is prefix +viewName+ suffix, where prefix suffix is provided when configuring the view parser.
  3. Similarly, if the user provides a Content-Type when configuring the View parser, set it to the View object.
  4. Configure the attribute name for requestContext.
  5. Configure exposePathVariables, that is, through@PathVaribaleAnnotate tag parameter information.
  6. Configure exposeContextBeansAsAttributes, said if I can use the Bean in the container, in View of the parameters, we can provide in the configuration View parser.
  7. Configure exposedContextBeanNames, which beans from the container can be used in the View, which we can provide when configuring the View parser.

That’s it, the view is built, isn’t it very easy?

4.1.2 applyLifecycleMethods

protected View applyLifecycleMethods(String viewName, AbstractUrlBasedView view) {
	ApplicationContext context = getApplicationContext();
	if(context ! =null) {
		Object initialized = context.getAutowireCapableBeanFactory().initializeBean(view, viewName);
		if (initialized instanceof View) {
			return(View) initialized; }}return view;
}
Copy the code

This is the initialization of the Bean, nothing to say.

There are many subclasses of UrlBasedViewResolver, including two representative ones. Of InternalResourceViewResolver is we use JSP are respectively used as well as when we use a Freemarker FreeMarkerViewResolver, due to the two we are common, So Songo tells you a little bit more about these two components here.

4.2 InternalResourceViewResolver

This view parser may be used when we use JSPS.

InternalResourceViewResolver mainly do four things:

  1. Views are specified through the requiredViewClass method.
@Override
protectedClass<? > requiredViewClass() {return InternalResourceView.class;
}
Copy the code
  1. The requiredViewClass method is called in the constructor to determine the view, and if JSTL is introduced in the project, the view is adjusted to JstlView.
  2. Rewrote the instantiateView method to instantiate different views based on actual conditions:
@Override
protected AbstractUrlBasedView instantiateView(a) {
	return (getViewClass() == InternalResourceView.class ? new InternalResourceView() :
			(getViewClass() == JstlView.class ? new JstlView() : super.instantiateView()));
}
Copy the code

InternalResourceView or JstlView is initialized according to the actual situation, or a method of the parent class is called to initialize the View.

  1. The buildView method has also been rewritten as follows:
@Override
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
	InternalResourceView view = (InternalResourceView) super.buildView(viewName);
	if (this.alwaysInclude ! =null) {
		view.setAlwaysInclude(this.alwaysInclude);
	}
	view.setPreventDispatchLoop(true);
	return view;
}
Copy the code

Call the parent class method to build InternalResourceView, and then configure alwaysInclude, which indicates whether to allow the use of forward as well as include. The last setPreventDispatchLoop method is to prevent loops.

4.3 FreeMarkerViewResolver

FreeMarkerViewResolver and even a AbstractTemplateViewResolver between UrlBasedViewResolver AbstractTemplateViewResolver is simpler, There are only five additional attributes that Songo already talked about when he shared Freemarker usage (see: Spring Boot + Freemarker squigginess!). , here again and everyone wordy:

  1. ExposeRequestAttributes: whether to exposeRequestAttributes for View use.
  2. AllowRequestOverride: Allows parameters in the RequestAttributes to override parameters in the Model with the same name as data in the Model.
  3. ExposeSessionAttributes: whether to exposeSessionAttributes to View for use.
  4. AllowSessionOverride: Allows parameters in SessionAttributes to override parameters in the Model with the same name as data in the Model.
  5. ExposeSpringMacroHelpers: Whether to expose RequestContext for SpringMacro to use.

This is AbstractTemplateViewResolver characteristics, relatively simple, and then to see FreeMarkerViewResolver.

public class FreeMarkerViewResolver extends AbstractTemplateViewResolver {
	public FreeMarkerViewResolver(a) {
		setViewClass(requiredViewClass());
	}
	public FreeMarkerViewResolver(String prefix, String suffix) {
		this(a); setPrefix(prefix); setSuffix(suffix); }@Override
	protectedClass<? > requiredViewClass() {return FreeMarkerView.class;
	}
	@Override
	protected AbstractUrlBasedView instantiateView(a) {
		return (getViewClass() == FreeMarkerView.class ? new FreeMarkerView() : super.instantiateView()); }}Copy the code

FreeMarkerViewResolver’s source code is simple: configure the prefixes, override the requiredViewClass method to provide FreeMarkerView, override the instantiateView method to instantiate the View.

ThymeleafViewResolver inherited from AbstractCachingViewResolver, specific work processes and in front of about the same, so here is not to do too much introduction. Note that the ThymeleafViewResolver#loadView method does not check for the existence of a view template, so it is possible to end up returning a view that does not exist (see how to have multiple view parsers in SpringMVC).

5.ViewResolverComposite

ViewResolverComposite: ViewResolverComposite: ViewResolverComposite: The ViewResolverComposite is used to proxy other viewResolvers, except that the ViewResolverComposite does some initialization for other viewResolvers. ApplicationContext and servletContext are configured for the corresponding ViewResolver. The code here is relatively simple, so I won’t post it. Finally, in the ViewResolverComposite#resolveViewName method, we iterate through the other view parsers:

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
	for (ViewResolver viewResolver : this.viewResolvers) {
		View view = viewResolver.resolveViewName(viewName, locale);
		if(view ! =null) {
			returnview; }}return null;
}
Copy the code

6. Summary

Today, I mainly talked with my friends about the workflow of the view parser in SpringMVC. Combining with Songgo’s previous article on how there are multiple view parsers in SpringMVC, I believe you will have a better understanding of the view parser in SpringMVC.

Well, today we talk about so much ~