ViewResolver 组件
ViewResolver
组件,视图解析器,根据视图名和国际化,获得最终的视图 View 对象
回顾
先来回顾一下在 DispatcherServlet
中处理请求的过程中哪里使用到 ViewResolver
组件,可以回到 《一个请求的旅行过程》 中的 DispatcherServlet
的 render
方法中看看,如下:
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine locale for request and apply it to the response.
// <1> 解析 request 中获得 Locale 对象,并设置到 response 中
Locale locale = (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);
// 获得 View 对象
View view;
String viewName = mv.getViewName();
// 情况一,使用 viewName 获得 View 对象
if (viewName != null) {
// We need to resolve the view name.
// <2.1> 使用 viewName 获得 View 对象
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) { // 获取不到,抛出 ServletException 异常
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
// 情况二,直接使用 ModelAndView 对象的 View 对象
else {
// No need to lookup: the ModelAndView object contains the actual View object.
// 直接使用 ModelAndView 对象的 View 对象
view = mv.getView();
if (view == null) { // 获取不到,抛出 ServletException 异常
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}
// Delegate to the View object for rendering.
// 打印日志
if (logger.isTraceEnabled()) {
logger.trace("Rendering view [" + view + "] ");
}
try {
// <3> 设置响应的状态码
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
// <4> 渲染页面
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "]", ex);
}
throw ex;
}
}
@Nullable
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
Locale locale, HttpServletRequest request) throws Exception {
if (this.viewResolvers != null) {
// 遍历 ViewResolver 数组
for (ViewResolver viewResolver : this.viewResolvers) {
// 根据 viewName + locale 参数,解析出 View 对象
View view = viewResolver.resolveViewName(viewName, locale);
// 解析成功,直接返回 View 对象
if (view != null) {
return view;
}
}
}
return null;
}
如果 ModelAndView 对象不为null
,且需要进行页面渲染,则调用 render
方法,如果设置的 View 对象是 String
类型,也就是 viewName
,则需要调用 resolveViewName
方法,通过 ViewResolver
根据 viewName
和 locale
解析出对应的 View 对象
这是前后端未分离的情况下重要的一个组件
ViewResolver 接口
org.springframework.web.servlet.ViewResolver
,视图解析器,根据视图名和国际化,获得最终的视图 View 对象,代码如下:
public interface ViewResolver {
/**
* 根据视图名和国际化,获得最终的 View 对象
*/
@Nullable
View resolveViewName(String viewName, Locale locale) throws Exception;
}
ViewResolver 接口体系的结构如下:
ViewResolver 的实现类比较多,其中 Spring MVC 默认使用 org.springframework.web.servlet.view.InternalResourceViewResolver
这个实现类
Spring Boot 中的默认实现类如下:
可以看到有三个实现类:
org.springframework.web.servlet.view.ContentNegotiatingViewResolver
org.springframework.web.servlet.view.ViewResolverComposite
,默认没有实现类org.springframework.web.servlet.view.BeanNameViewResolver
org.springframework.web.servlet.view.InternalResourceViewResolver
初始化过程
在 DispatcherServlet
的 initViewResolvers(ApplicationContext context)
方法,初始化 ViewResolver 组件,方法如下:
private void initViewResolvers(ApplicationContext context) {
// 置空 viewResolvers 处理
this.viewResolvers = null;
// 情况一,自动扫描 ViewResolver 类型的 Bean 们
if (this.detectAllViewResolvers) {
// Find all ViewResolvers in the ApplicationContext, including ancestor contexts.
Map<String, ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context,
ViewResolver.class, true, false);
if (!matchingBeans.isEmpty()) {
this.viewResolvers = new ArrayList<>(matchingBeans.values());
// We keep ViewResolvers in sorted order.
AnnotationAwareOrderComparator.sort(this.viewResolvers);
}
}
// 情况二,获得名字为 VIEW_RESOLVER_BEAN_NAME 的 Bean 们
else {
try {
ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class);
this.viewResolvers = Collections.singletonList(vr);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default ViewResolver later.
}
}
// Ensure we have at least one ViewResolver, by registering
// a default ViewResolver if no other resolvers are found.
/**
* 情况三,如果未获得到,则获得默认配置的 ViewResolver 类
* {@link org.springframework.web.servlet.view.InternalResourceViewResolver}
*/
if (this.viewResolvers == null) {
this.viewResolvers = getDefaultStrategies(context, ViewResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("No ViewResolvers declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
- 如果“开启”探测功能,则扫描已注册的 ViewResolver 的 Bean 们,添加到
viewResolvers
中,默认 开启 - 如果“关闭”探测功能,则获得 Bean 名称为 "viewResolver" 对应的 Bean ,将其添加至
viewResolvers
- 如果未获得到,则获得默认配置的 ViewResolver 类,调用
getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface)
方法,就是从DispatcherServlet.properties
文件中读取 ViewResolver 的默认实现类,如下:
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
在 Spring Boot 不是通过这样初始化的,感兴趣的可以去看看
ContentNegotiatingViewResolver
org.springframework.web.servlet.view.ContentNegotiatingViewResolver
,实现 ViewResolver、Ordered、InitializingBean 接口,继承 WebApplicationObjectSupport 抽象类,基于 内容类型 来获取对应 View 的 ViewResolver 实现类。其中, 内容类型 指的是 Content-Type
和拓展后缀
构造方法
public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
implements ViewResolver, Ordered, InitializingBean {
@Nullable
private ContentNegotiationManager contentNegotiationManager;
/**
* ContentNegotiationManager 的工厂,用于创建 {@link #contentNegotiationManager} 对象
*/
private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean();
/**
* 在找不到 View 对象时,返回 {@link #NOT_ACCEPTABLE_VIEW}
*/
private boolean useNotAcceptableStatusCode = false;
/**
* 默认 View 数组
*/
@Nullable
private List<View> defaultViews;
/**
* ViewResolver 数组
*/
@Nullable
private List<ViewResolver> viewResolvers;
/**
* 顺序,优先级最高
*/
private int order = Ordered.HIGHEST_PRECEDENCE;
}
viewResolvers
:ViewResolver 数组。对于来说,ContentNegotiatingViewResolver 会使用这些 ViewResolver们,解析出所有的 View 们,然后基于 内容类型 ,来获取对应的 View 们。此时的 View 结果,可能是一个,可能是多个,所以需要比较获取到 最优 的 View 对象。defaultViews
:默认 View 数组。那么此处的默认是什么意思呢?在viewResolvers
们解析出所有的 View 们的基础上,也会添加defaultViews
到 View 结果中order
:顺序,优先级 最高 。所以,这也是为什么它排在最前面
在上图中可以看到,在 Spring Boot 中 viewResolvers
属性有三个实现类,分别是 BeanNameViewResolver
、ViewResolverComposite
、InternalResourceViewResolver
initServletContext
实现 initServletContext(ServletContext servletContext)
方法,初始化 viewResolvers
属性,方法如下:
在父类 WebApplicationObjectSupport 的父类 ApplicationObjectSupport 中可以看到,因为实现了 ApplicationContextAware 接口,则在初始化该 Bean 的时候会调用
setApplicationContext(@Nullable ApplicationContext context)
方法,在这个方法中会调用initApplicationContext(ApplicationContext context)
这个方法,这个方法又会调用initServletContext(ServletContext servletContext)
方法
@Override
protected void initServletContext(ServletContext servletContext) {
// <1> 扫描所有 ViewResolver 的 Bean 们
Collection<ViewResolver> matchingBeans = BeanFactoryUtils.
beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
// <1.1> 情况一,如果 viewResolvers 为空,则将 matchingBeans 作为 viewResolvers 。
// BeanNameViewResolver、ThymeleafViewResolver、ViewResolverComposite、InternalResourceViewResolver
if (this.viewResolvers == null) {
this.viewResolvers = new ArrayList<>(matchingBeans.size());
for (ViewResolver viewResolver : matchingBeans) {
if (this != viewResolver) { // 排除自己
this.viewResolvers.add(viewResolver);
}
}
}
// <1.2> 情况二,如果 viewResolvers 非空,则和 matchingBeans 进行比对,判断哪些未进行初始化,进行初始化
else {
for (int i = 0; i < this.viewResolvers.size(); i++) {
ViewResolver vr = this.viewResolvers.get(i);
// 已存在在 matchingBeans 中,说明已经初始化,则直接 continue
if (matchingBeans.contains(vr)) {
continue;
}
// 不存在在 matchingBeans 中,说明还未初始化,则进行初始化
String name = vr.getClass().getName() + i;
obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
}
}
// <1.3> 排序 viewResolvers 数组
AnnotationAwareOrderComparator.sort(this.viewResolvers);
// <2> 设置 cnmFactoryBean 的 servletContext 属性
this.cnmFactoryBean.setServletContext(servletContext);
}
-
扫描所有 ViewResolver 的 Bean 们
matchingBeans
- 情况一,如果
viewResolvers
为空,则将matchingBeans
作为viewResolvers
- 情况二,如果
viewResolvers
非空,则和matchingBeans
进行比对,判断哪些未进行初始化,进行初始化 - 排序
viewResolvers
数组
- 情况一,如果
-
设置
cnmFactoryBean
的servletContext
属性为当前 Servlet 上下文
afterPropertiesSet
因为 ContentNegotiatingViewResolver 实现了 InitializingBean 接口,在 Sping 初始化该 Bean 的时候,会调用该方法,完成一些初始化工作,方法如下:
@Override
public void afterPropertiesSet() {
// 如果 contentNegotiationManager 为空,则进行创建
if (this.contentNegotiationManager == null) {
this.contentNegotiationManager = this.cnmFactoryBean.build();
}
if (this.viewResolvers == null || this.viewResolvers.isEmpty()) {
logger.warn("No ViewResolvers configured");
}
}
resolveViewName
实现 resolveViewName(String viewName, Locale locale)
方法,代码如下:
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
// <1> 获得 MediaType 数组
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
if (requestedMediaTypes != null) {
// <2> 获得匹配的 View 数组
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
// <3> 筛选最匹配的 View 对象
View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
// 如果筛选成功,则返回
if (bestView != null) {
return bestView;
}
}
String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ? " given " + requestedMediaTypes.toString() : "";
// <4> 如果匹配不到 View 对象,则根据 useNotAcceptableStatusCode ,返回 NOT_ACCEPTABLE_VIEW 或 null
if (this.useNotAcceptableStatusCode) {
if (logger.isDebugEnabled()) {
logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
}
return NOT_ACCEPTABLE_VIEW;
}
else {
logger.debug("View remains unresolved" + mediaTypeInfo);
return null;
}
}
- 调用
getMediaTypes(HttpServletRequest request)
方法,获得 MediaType 数组,详情见下文 - 调用
getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
方法,获得匹配的 View 数组,详情见下文 - 调用
getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs)
方法,筛选出最匹配的 View 对象,如果筛选成功则直接返回,详情见下文 - 如果匹配不到 View 对象,则根据
useNotAcceptableStatusCode
,返回NOT_ACCEPTABLE_VIEW
或null
,其中NOT_ACCEPTABLE_VIEW
变量,代码如下:
private static final View NOT_ACCEPTABLE_VIEW = new View() {
@Override
@Nullable
public String getContentType() {
return null;
}
@Override
public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
}
};
这个 View 对象设置状态码为 `406`
getMediaTypes
getCandidateViews(HttpServletRequest request)
方法,获得 MediaType 数组,如下:
@Nullable
protected List<MediaType> getMediaTypes(HttpServletRequest request) {
Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
try {
// 创建 ServletWebRequest 对象
ServletWebRequest webRequest = new ServletWebRequest(request);
// 从请求中,获得可接受的 MediaType 数组。默认实现是,从请求头 ACCEPT 中获取
List<MediaType> acceptableMediaTypes = this.contentNegotiationManager.resolveMediaTypes(webRequest);
// 获得可产生的 MediaType 数组
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request);
// 通过 acceptableTypes 来比对,将符合的 producibleType 添加到 mediaTypesToUse 结果数组中
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<>();
for (MediaType acceptable : acceptableMediaTypes) {
for (MediaType producible : producibleMediaTypes) {
if (acceptable.isCompatibleWith(producible)) {
compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible));
}
}
}
// 按照 MediaType 的 specificity、quality 排序
List<MediaType> selectedMediaTypes = new ArrayList<>(compatibleMediaTypes);
MediaType.sortBySpecificityAndQuality(selectedMediaTypes);
return selectedMediaTypes;
}
catch (HttpMediaTypeNotAcceptableException ex) {
if (logger.isDebugEnabled()) {
logger.debug(ex.getMessage());
}
return null;
}
}
getCandidateViews
getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
方法,获得匹配的 View 数组,如下:
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
throws Exception {
// 创建 View 数组
List<View> candidateViews = new ArrayList<>();
// <1> 来源一,通过 viewResolvers 解析出 View 数组结果,添加到 candidateViews 中
if (this.viewResolvers != null) {
Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
// <1.1> 遍历 viewResolvers 数组
for (ViewResolver viewResolver : this.viewResolvers) {
// <1.2> 情况一,获得 View 对象,添加到 candidateViews 中
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
candidateViews.add(view);
}
// <1.3> 情况二,带有拓展后缀的方式,获得 View 对象,添加到 candidateViews 中
for (MediaType requestedMediaType : requestedMediaTypes) {
// <1.3.2> 获得 MediaType 对应的拓展后缀的数组(默认情况下未配置)
List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
// <1.3.3> 遍历拓展后缀的数组
for (String extension : extensions) {
// <1.3.4> 带有拓展后缀的方式,获得 View 对象,添加到 candidateViews 中
String viewNameWithExtension = viewName + '.' + extension;
view = viewResolver.resolveViewName(viewNameWithExtension, locale);
if (view != null) {
candidateViews.add(view);
}
}
}
}
}
// <2> 来源二,添加 defaultViews 到 candidateViews 中
if (!CollectionUtils.isEmpty(this.defaultViews)) {
candidateViews.addAll(this.defaultViews);
}
return candidateViews;
}
-
来源一,通过
viewResolvers
解析出 View 数组结果,添加到List<View> candidateViews
中- 遍历
viewResolvers
数组 - 情况一,通过当前 ViewResolver 实现类获得 View 对象,添加到
candidateViews
中 - 情况二,遍历入参
List<MediaType> requestedMediaTypes
,将带有拓展后缀的类型再通过当前 ViewResolver 实现类获得 View 对象,添加到candidateViews
中
2. 获得 MediaType 对应的拓展后缀的数组(默认情况下未配置)
3. 遍历拓展后缀的数组
4. 带有拓展后缀的方式,通过当前 ViewResolver 实现类获得 View 对象,添加到candidateViews
中
- 遍历
-
来源二,添加
defaultViews
到candidateViews
中
getBestView
getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs)
方法,筛选出最匹配的 View 对象,如下:
@Nullable
private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
// <1> 遍历 candidateView 数组,如果有重定向的 View 类型,则返回它
for (View candidateView : candidateViews) {
if (candidateView instanceof SmartView) {
SmartView smartView = (SmartView) candidateView;
if (smartView.isRedirectView()) {
return candidateView;
}
}
}
// <2> 遍历 MediaType 数组(MediaTy数组已经根据pespecificity、quality进行了排序)
for (MediaType mediaType : requestedMediaTypes) {
// <2> 遍历 View 数组
for (View candidateView : candidateViews) {
if (StringUtils.hasText(candidateView.getContentType())) {
// <2.1> 如果 MediaType 类型匹配,则返回该 View 对象
MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
if (mediaType.isCompatibleWith(candidateContentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Selected '" + mediaType + "' given " + requestedMediaTypes);
}
attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST);
return candidateView;
}
}
}
}
return null;
}
-
遍历
candidateView
数组,如果有 重定向 的 View 类型,则返回它。也就是说, 重定向 的 View ,优先级更高。 -
遍历 MediaType 数组(MediaTy数组已经根据
pespecificity
、quality
进行了排序)和candidateView
数组- 如果 MediaType 类型匹配该 View 对象,则返回该 View 对象。也就是说,优先级的匹配规则,由 ViewResolver 在
viewResolvers
的位置,越靠前,优先级越高。
- 如果 MediaType 类型匹配该 View 对象,则返回该 View 对象。也就是说,优先级的匹配规则,由 ViewResolver 在
BeanNameViewResolver
org.springframework.web.servlet.view.BeanNameViewResolver
,实现 ViewResolver、Ordered 接口,继承 WebApplicationObjectSupport 抽象类,基于 Bean 的名字获得 View 对象的 ViewResolver 实现类
构造方法
public class BeanNameViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered {
/**
* 顺序,优先级最低
*/
private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered
}
resolveViewName
实现 resolveViewName(String viewName, Locale locale)
方法,根据名称获取 View 类型对应的 Bean(View 对象),如下:
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws BeansException {
ApplicationContext context = obtainApplicationContext();
// 如果对应的 Bean 对象不存在,则返回 null
if (!context.containsBean(viewName)) {
// Allow for ViewResolver chaining...
return null;
}
// 如果 Bean 对应的 Bean 类型不是 View ,则返回 null
if (!context.isTypeMatch(viewName, View.class)) {
if (logger.isDebugEnabled()) {
logger.debug("Found bean named '" + viewName + "' but it does not implement View");
}
// Since we're looking into the general ApplicationContext here,
// let's accept this as a non-match and allow for chaining as well...
return null;
}
// 获得 Bean 名字对应的 View 对象
return context.getBean(viewName, View.class);
}
ViewResolverComposite
org.springframework.web.servlet.view.ViewResolverComposite
,实现 ViewResolver、Ordered、InitializingBean、ApplicationContextAware、ServletContextAware 接口,复合的 ViewResolver 实现类
构造方法
public class ViewResolverComposite implements ViewResolver, Ordered, InitializingBean,
ApplicationContextAware, ServletContextAware {
/**
* ViewResolver 数组
*/
private final List<ViewResolver> viewResolvers = new ArrayList<>();
/**
* 顺序,优先级最低
*/
private int order = Ordered.LOWEST_PRECEDENCE;
}
afterPropertiesSet
因为 ViewResolverComposite 实现了 InitializingBean 接口,在 Sping 初始化该 Bean 的时候,会调用该方法,完成一些初始化工作,方法如下:
@Override
public void afterPropertiesSet() throws Exception {
for (ViewResolver viewResolver : this.viewResolvers) {
if (viewResolver instanceof InitializingBean) {
((InitializingBean) viewResolver).afterPropertiesSet();
}
}
}
resolveViewName
实现 resolveViewName(String viewName, Locale locale)
方法,代码如下:
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
// 遍历 viewResolvers 数组,逐个进行解析,但凡成功,则返回该 View 对象
for (ViewResolver viewResolver : this.viewResolvers) {
// 执行解析
View view = viewResolver.resolveViewName(viewName, locale);
// 解析成功,则返回该 View 对象
if (view != null) {
return view;
}
}
return null;
}
AbstractCachingViewResolver
org.springframework.web.servlet.view.AbstractCachingViewResolver
,实现 ViewResolver 接口,继承 WebApplicationObjectSupport 抽象类,提供通用的 缓存 的 ViewResolver 抽象类。对于相同的视图名,返回的是相同的 View 对象,所以通过缓存,可以进一步提供性能。
构造方法
public abstract class AbstractCachingViewResolver extends WebApplicationObjectSupport implements ViewResolver {
/** Default maximum number of entries for the view cache: 1024. */
public static final int DEFAULT_CACHE_LIMIT = 1024;
/** Dummy marker object for unresolved views in the cache Maps. */
private static final View UNRESOLVED_VIEW = new View() {
@Override
@Nullable
public String getContentType() {
return null;
}
@Override
public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
}
};
/** The maximum number of entries in the cache. */
private volatile int cacheLimit = DEFAULT_CACHE_LIMIT; // 缓存上限。如果 cacheLimit = 0 ,表示禁用缓存
/** Whether we should refrain from resolving views again if unresolved once. */
private boolean cacheUnresolved = true; // 是否缓存空 View 对象
/** Fast access cache for Views, returning already cached instances without a global lock. */
private final Map<Object, View> viewAccessCache = new ConcurrentHashMap<>(DEFAULT_CACHE_LIMIT); // View 的缓存的映射
/** Map from view key to View instance, synchronized for View creation. */
// View 的缓存的映射。相比 {@link #viewAccessCache} 来说,增加了 synchronized 锁
@SuppressWarnings("serial")
private final Map<Object, View> viewCreationCache =
new LinkedHashMap<Object, View>(DEFAULT_CACHE_LIMIT, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Object, View> eldest) {
if (size() > getCacheLimit()) {
viewAccessCache.remove(eldest.getKey());
return true;
}
else {
return false;
}
}
};
}
通过 viewAccessCache
属性,提供更快的访问 View 缓存
通过 viewCreationCache
属性,提供缓存的上限的功能
KEY 是通过 getCacheKey(String viewName, Locale locale)
方法,获得缓存 KEY,方法如下:
protected Object getCacheKey(String viewName, Locale locale) {
return viewName + '_' + locale;
}
resolveViewName
实现 resolveViewName(String viewName, Locale locale)
方法,代码如下:
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
// 如果禁用缓存,则创建 viewName 对应的 View 对象
if (!isCache()) {
return createView(viewName, locale);
}
else {
// 获得缓存 KEY
Object cacheKey = getCacheKey(viewName, locale);
// 从 viewAccessCache 缓存中,获得 View 对象
View view = this.viewAccessCache.get(cacheKey);
// 如果获得不到缓存,则从 viewCreationCache 中,获得 View 对象
if (view == null) {
synchronized (this.viewCreationCache) {
// 从 viewCreationCache 中,获得 View 对象
view = this.viewCreationCache.get(cacheKey);
if (view == null) {
// Ask the subclass to create the View object.
// 创建 viewName 对应的 View 对象
view = createView(viewName, locale);
// 如果创建失败,但是 cacheUnresolved 为 true ,则设置为 UNRESOLVED_VIEW
if (view == null && this.cacheUnresolved) {
view = UNRESOLVED_VIEW;
}
// 如果 view 非空,则添加到 viewAccessCache 缓存中
if (view != null) {
this.viewAccessCache.put(cacheKey, view);
this.viewCreationCache.put(cacheKey, view);
}
}
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace(formatKey(cacheKey) + "served from cache");
}
}
return (view != UNRESOLVED_VIEW ? view : null);
}
}
@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;
逻辑比较简单,主要是缓存的处理,需要通过子类去创建对应的 View 对象
UrlBasedViewResolver
org.springframework.web.servlet.view.UrlBasedViewResolver
,实现 Ordered 接口,继承 AbstractCachingViewResolver 抽象类,基于 Url 的 ViewResolver 实现类
构造方法
public class UrlBasedViewResolver extends AbstractCachingViewResolver implements Ordered {
public static final String REDIRECT_URL_PREFIX = "redirect:";
public static final String FORWARD_URL_PREFIX = "forward:";
/**
* View 的类型,不同的实现类,会对应一个 View 的类型
*/
@Nullable
private Class<?> viewClass;
/**
* 前缀
*/
private String prefix = "";
/**
* 后缀
*/
private String suffix = "";
/**
* ContentType 类型
*/
@Nullable
private String contentType;
private boolean redirectContextRelative = true;
private boolean redirectHttp10Compatible = true;
@Nullable
private String[] redirectHosts;
/**
* RequestAttributes 暴露给 View 使用时的属性
*/
@Nullable
private String requestContextAttribute;
/** Map of static attributes, keyed by attribute name (String). */
private final Map<String, Object> staticAttributes = new HashMap<>();
/**
* 是否暴露路径变量给 View 使用
*/
@Nullable
private Boolean exposePathVariables;
@Nullable
private Boolean exposeContextBeansAsAttributes;
@Nullable
private String[] exposedContextBeanNames;
/**
* 是否只处理指定的视图名们
*/
@Nullable
private String[] viewNames;
/**
* 顺序,优先级最低
*/
private int order = Ordered.LOWEST_PRECEDENCE;
}
initApplicationContext
实现 initApplicationContext()
方法,进一步初始化,代码如下:
在父类 WebApplicationObjectSupport 的父类 ApplicationObjectSupport 中可以看到,因为实现了 ApplicationContextAware 接口,则在初始化该 Bean 的时候会调用
setApplicationContext(@Nullable ApplicationContext context)
方法,在这个方法中会调用initApplicationContext(ApplicationContext context)
这个方法,这个方法又会调用initApplicationContext()
方法
@Override
protected void initApplicationContext() {
super.initApplicationContext();
if (getViewClass() == null) {
throw new IllegalArgumentException("Property 'viewClass' is required");
}
}
在子类中会看到 viewClass
属性一般会在构造中法中设置
getCacheKey
重写 getCacheKey(String viewName, Locale locale)
方法,忽略 locale
参数,仅仅使用 viewName
作为缓存 KEY,如下:
@Override
protected Object getCacheKey(String viewName, Locale locale) {
// 重写了父类的方法,去除locale直接返回viewName
return viewName;
}
也就是说,不支持 Locale 特性
canHandle
canHandle(String viewName, Locale locale)
方法,判断传入的视图名是否可以被处理,如下:
protected boolean canHandle(String viewName, Locale locale) {
String[] viewNames = getViewNames();
return (viewNames == null || PatternMatchUtils.simpleMatch(viewNames, viewName));
}
@Nullable
protected String[] getViewNames() {
return this.viewNames;
}
一般情况下,viewNames
指定的视图名们为空,所以会满足 viewNames == null 代码块。也就说,所有视图名都可以被处理
applyLifecycleMethods
applyLifecycleMethods(String viewName, AbstractUrlBasedView view)
方法,代码如下:
protected View applyLifecycleMethods(String viewName, AbstractUrlBasedView view) {
// 情况一,如果 viewName 有对应的 View Bean 对象,则使用它
ApplicationContext context = getApplicationContext();
if (context != null) {
Object initialized = context.getAutowireCapableBeanFactory().initializeBean(view, viewName);
if (initialized instanceof View) {
return (View) initialized;
}
}
// 情况二,直接返回 view
return view;
}
createView
重写 createView(String viewName, Locale locale)
方法,增加了对 REDIRECT、FORWARD 的情况的处理,如下:
@Override
protected View createView(String viewName, Locale locale) throws Exception {
// If this resolver is not supposed to handle the given view,
// return null to pass on to the next resolver in the chain.
// 是否能处理该视图名称
if (!canHandle(viewName, locale)) {
return null;
}
// Check for special "redirect:" prefix.
if (viewName.startsWith(REDIRECT_URL_PREFIX)) { // 如果是 REDIRECT 开头,创建 RedirectView 视图
String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
String[] hosts = getRedirectHosts();
if (hosts != null) {
// 设置 RedirectView 对象的 hosts 属性
view.setHosts(hosts);
}
// 应用
return applyLifecycleMethods(REDIRECT_URL_PREFIX, view);
}
// Check for special "forward:" prefix.
if (viewName.startsWith(FORWARD_URL_PREFIX)) { // 如果是 FORWARD 开头,创建 InternalResourceView 视图
String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
InternalResourceView view = new InternalResourceView(forwardUrl);
// 应用
return applyLifecycleMethods(FORWARD_URL_PREFIX, view);
}
// Else fall back to superclass implementation: calling loadView.
// 创建视图名对应的 View 对象
return super.createView(viewName, locale);
}
loadView
实现 loadView(String viewName, Locale locale)
方法,加载 viewName 对应的 View 对象,方法如下:
@Override
protected View loadView(String viewName, Locale locale) throws Exception {
// <x> 创建 viewName 对应的 View 对象
AbstractUrlBasedView view = buildView(viewName);
// 应用
View result = applyLifecycleMethods(viewName, view);
return (view.checkResource(locale) ? result : null);
}
其中,<x>
处,调用 buildView(String viewName)
方法,创建 viewName
对应的 View 对象,方法如下:
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
Class<?> viewClass = getViewClass();
Assert.state(viewClass != null, "No view class");
// 创建 AbstractUrlBasedView 对象
AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass);
// 设置各种属性
view.setUrl(getPrefix() + viewName + getSuffix());
String contentType = getContentType();
if (contentType != null) {
view.setContentType(contentType);
}
view.setRequestContextAttribute(getRequestContextAttribute());
view.setAttributesMap(getAttributesMap());
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;
}
requiredViewClass
requiredViewClass()
方法,定义了产生的视图,代码如下:
protected Class<?> requiredViewClass() {
return AbstractUrlBasedView.class;
}
InternalResourceViewResolver
org.springframework.web.servlet.view.InternalResourceViewResolver
,继承 UrlBasedViewResolver 类,解析出 JSP 的 ViewResolver 实现类
构造方法
public class InternalResourceViewResolver extends UrlBasedViewResolver {
/**
* 判断 javax.servlet.jsp.jstl.core.Config 是否存在
*/
private static final boolean jstlPresent = ClassUtils.isPresent(
"javax.servlet.jsp.jstl.core.Config", InternalResourceViewResolver.class.getClassLoader());
@Nullable
private Boolean alwaysInclude;
public InternalResourceViewResolver() {
// 获得 viewClass
Class<?> viewClass = requiredViewClass();
if (InternalResourceView.class == viewClass && jstlPresent) {
viewClass = JstlView.class;
}
// 设置 viewClass
setViewClass(viewClass);
}
}
从构造方法中,可以看出,视图名会是 InternalResourceView 或 JstlView 类。 实际上,JstlView 是 InternalResourceView 的子类。
buildView
重写 buildView(String viewName)
方法,代码如下:
@Override
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
// 调用父方法
InternalResourceView view = (InternalResourceView) super.buildView(viewName);
if (this.alwaysInclude != null) {
view.setAlwaysInclude(this.alwaysInclude);
}
// 设置 View 对象的相关属性
view.setPreventDispatchLoop(true);
return view;
}
设置两个属性
View
org.springframework.web.servlet.View
,Spring MVC 中的视图对象,用于视图渲染,代码如下:
public interface View {
String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";
String PATH_VARIABLES = View.class.getName() + ".pathVariables";
String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";
@Nullable
default String getContentType() {
return null;
}
/**
* 渲染视图
*/
void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception;
}
View 接口体系的结构如下:
可以看到 View 的实现类非常多,本文不会详细分析,简单讲解两个方法
在 DispatcherServlet 中会直接调用 View 的 render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
来进行渲染页面
// AbstractView.java
@Override
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
// 合并返回结果,将 Model 中的静态数据和请求中的动态数据进行合并
Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
// 进行一些准备工作(修复 IE 中存在的 BUG)
prepareResponse(request, response);
// 进行渲染
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}
- 将 Model 对象与请求中的数据进行合并,生成一个 Map 对象,保存进入页面的一些数据
- 进行一些准备工作(修复 IE 中存在的 BUG)
- 调用
renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response)
方法,页面渲染,如下:
// InternalResourceView.java
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
// Expose helpers as request attributes, if any.
// 往请求中设置一些属性,Locale、TimeZone、LocalizationContext
exposeHelpers(request);
// Determine the path for the request dispatcher.
// 获取需要转发的路径
String dispatcherPath = prepareForRendering(request, response);
// Obtain a RequestDispatcher for the target resource (typically a JSP).
// 获取请求转发器
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
"]: Check that the corresponding file exists within your web application archive!");
}
// If already included or response already committed, perform include, else forward.
if (useInclude(request, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including [" + getUrl() + "]");
}
rd.include(request, response);
} else {
// Note: The forwarded resource is supposed to determine the content type itself.
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to [" + getUrl() + "]");
}
// 最后进行转发
rd.forward(request, response);
}
}
是不是很熟悉?
通过 Servlet 的 javax.servlet.RequestDispatcher
请求派发着,转到对应的 URL
总结
本文分析了 ViewResolver
组件,视图解析器,根据视图名和国际化,获得最终的视图 View 对象。Spring MVC 执行完处理器后生成一个 ModelAndView 对象,如果该对象不为 null
并且有对应的 viewName
,那么就需要通过 ViewResolver
根据 viewName
解析出对应的 View 对象。
在 Spring MVC 和 Spring Boot 中最主要的还是 InternalResourceViewResolver
实现类,例如这么配置:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!-- 自动给后面 action 的方法 return 的字符串加上前缀和后缀,变成一个可用的地址 -->
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
当返回的视图名称为 login
时,View 对象的 url
就是 /WEB-INF/jsp/login.jsp
,调用 View 的 render
方法进行页面渲染时,请求会转发到这个 url
当然,还有其他的 ViewResolver
实现类,例如 BeanNameViewResolver
,目前大多数都是前后端分离的项目,这个组件也许你很少用到
至此, 《精尽 Spring MVC 源码分析》 系列 最后一篇 文档已经讲述完了,笔者写的过程中也是懵懵懂懂的状态,写完之后才更加的理解,对于 Spring MVC 中大部分的内容都有分析到,你会发现 Spring MVC 原来是这么回事,开心~ 其中涉及到 Spring 相关内容在努力阅读中,敬请期待~
希望这系列文档能够帮助你对 Spring MVC 的理解,路漫漫其修远兮~
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。