首页 > 编程语言 >Spring MVC 源码分析 - ViewResolver 组件

Spring MVC 源码分析 - ViewResolver 组件

时间:2024-01-10 11:32:55浏览次数:34  
标签:null String viewName Spring ViewResolver 源码 View view

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 接口体系的结构如下:

Spring MVC 源码分析 - ViewResolver 组件_spring

ViewResolver 的实现类比较多,其中 Spring MVC 默认使用 org.springframework.web.servlet.view.InternalResourceViewResolver 这个实现类

Spring Boot 中的默认实现类如下:

Spring MVC 源码分析 - ViewResolver 组件_数组_02

可以看到有三个实现类:

  • 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");
        }
    }
}
  1. 如果“开启”探测功能,则扫描已注册的 ViewResolver 的 Bean 们,添加到 viewResolvers 中,默认开启
  2. 如果“关闭”探测功能,则获得 Bean 名称为 "viewResolver" 对应的 Bean ,将其添加至 viewResolvers
  3. 如果未获得到,则获得默认配置的 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 属性有三个实现类,分别是 BeanNameViewResolverViewResolverCompositeInternalResourceViewResolver

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);
}
  1. 扫描所有 ViewResolver 的 Bean 们 matchingBeans
  1. 情况一,如果 viewResolvers 为空,则将 matchingBeans 作为 viewResolvers
  2. 情况二,如果 viewResolvers 非空,则和 matchingBeans 进行比对,判断哪些未进行初始化,进行初始化
  3. 排序 viewResolvers 数组
  1. 设置 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;
    }
}
  1. 调用 getMediaTypes(HttpServletRequest request) 方法,获得 MediaType 数组,详情见下文
  2. 调用 getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes) 方法,获得匹配的 View 数组,详情见下文
  3. 调用 getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) 方法,筛选出最匹配的 View 对象,如果筛选成功则直接返回,详情见下文
  4. 如果匹配不到 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;
}
  1. 来源一,通过 viewResolvers 解析出 View 数组结果,添加到 List<View> candidateViews 中
  1. 遍历 viewResolvers 数组
  2. 情况一,通过当前 ViewResolver 实现类获得 View 对象,添加到 candidateViews 中
  3. 情况二,遍历入参 List<MediaType> requestedMediaTypes,将带有拓展后缀的类型再通过当前 ViewResolver 实现类获得 View 对象,添加到 candidateViews 中
    2. 获得 MediaType 对应的拓展后缀的数组(默认情况下未配置)
    3. 遍历拓展后缀的数组
    4. 带有拓展后缀的方式,通过当前 ViewResolver 实现类获得 View 对象,添加到 candidateViews 中
  1. 来源二,添加 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;
}
  1. 遍历 candidateView 数组,如果有重定向的 View 类型,则返回它。也就是说,重定向的 View ,优先级更高。
  2. 遍历 MediaType 数组(MediaTy数组已经根据pespecificityquality进行了排序)和 candidateView 数组
  1. 如果 MediaType 类型匹配该 View 对象,则返回该 View 对象。也就是说,优先级的匹配规则,由 ViewResolver 在 viewResolvers 的位置,越靠前,优先级越高。

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 类。

标签:null,String,viewName,Spring,ViewResolver,源码,View,view
From: https://blog.51cto.com/u_15668812/9176045

相关文章

  • 浅谈spring-retry
    使用方法@ComponentpublicclassRetryableXX{ //使用重试框架须知: //该注解无事务性!!!! //该注解是同步操作,重试次数与时间间隔需要慎重考虑!!!! //使用重试注解时,务必匹配@Recover回调方法,否则重试次数会平方!!!! //被调用的重试方法必须是被代理过的,否则重试效果不生效!!!! //匹......
  • 构建高效学习平台:企业培训系统源码深度解析
    企业培训系统是组织中培养和提升员工技能的核心工具。本文将深入探讨企业培训系统的源码,通过关键技术代码解析,揭示其中的设计原理和功能实现,以构建更高效的学习平台。1.环境配置与依赖项安装首先,让我们关注源码的环境配置。以下是一个基本的PythonFlask应用,使用SQLite数据库:#app......
  • 源码开发实践:搭建企业培训APP的技术难题及解决方案
    在企业培训源码开发实践中,各位开发者可能遇到各种各样的问题,本文将深入探讨这些挑战,并提供解决方案,助力你顺利搭建企业培训APP。 1.多平台兼容性企业中员工使用的设备多种多样,包括iOS、Android等不同操作系统。在搭建培训APP时,如何实现多平台兼容性成为一项首要任务。解决方案是采......
  • 交易猫闲鱼链接源码跳转APP+八合一完整源码 转转闲鱼链接源码二手闲置源码
    高仿交易猫转转闲鱼源码搭建教程:添加网站→上传源码→解压源码→导入数据库→修改数据库路径config/Conn.php不用设置什么伪静态源码下载:转转闲鱼交易猫三合一链接源码搭建教程:导入数据库,修改数据库config/conn.php1、一键生成链接卡框分享,效果逼真2、使用简单,支持自定义订单内......
  • 企业培训系统源码:构建智能、可扩展的学习平台
    企业培训系统在现代企业中扮演着至关重要的角色。本文将通过深度解析企业培训系统的源码,介绍如何构建一个智能、可扩展的学习平台,涉及关键技术和代码实例。1.技术栈选择与项目初始化在构建企业培训系统之前,选择适当的技术栈是至关重要的。以下是一个基于Django框架的Python项目初......
  • 最新仿闲鱼链接 转转闲鱼链接二手闲置源码
    二手转转闲鱼平台,完整后台和源码,最新版本,仅限学习,转转闲鱼交易猫三合一链接源码一位兄弟给的源码查看了下无任何加密直接上传服务器解压安装就可以修改数据库config/Conn.php 然后导入数据源码下载:https://pan.baidu.com/s/1ytoKFN0gdcqJoJCh5Pvg6w?pwd=duke ......
  • 购买自主开发体育直播系统源码的重要性,判断是否自研方法
    大型竞体项目的关注度与影响力不断延伸扩散,体育赛事直播平台的开发与运营已成为行业焦点。然而,对于许多企业来说,相比自行开发还是购买体育直播系统源码更有性价比。这其中,选择如“东莞梦幻网络科技”自主研发的一手源码具有不可忽视的重要性,不仅关乎用户体验,更直接影响到平台的生命......
  • SpringBoot集成WebSocket实现消息推送
    一、前言WebSocket是一种新型的通信协议,它可以在客户端和服务端之间实现双向通信,具有低延迟、高效性等特点,适用于实时通信场景。它是一种基于TCP协议实现的全双工通信协议,使用它可以实现实时通信,不必担心HTTP协议的短连接问题。SpringBoot可以很方便的集成WebSocket实现高效实时的......
  • #星计划# 在OpenHarmony上使用网络组件axios与Spring Boot进行前后端交互
    在OpenHarmony上使用网络组件axios与SpringBoot进行前后端交互#jitoa#此博客由金陵科技学院-开放原子开源社李俊杰编写仓库地址:axiosTest·AtomGit_开放原子开源基金会代码托管平台结果演示:在OpenHarmony上使用网络组件axios与SpringBoot进行前后端交互_哔哩哔哩_bilib......
  • springboot通过自定义注解@Log实现日志打印
    springboot通过自定义注解@Log实现日志打印效果图实操步骤注意,本代码在springboot环境下运行,jdk1.81.引入依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency>......