Spring MVC 源码- ViewResolver 组件

news2024/9/29 15:26:33

ViewResolver 组件

ViewResolver 组件,视图解析器,根据视图名和国际化,获得最终的视图 View 对象

回顾

先来回顾一下在 DispatcherServlet 中处理请求的过程中哪里使用到 ViewResolver 组件,可以回到《一个请求响应的旅行过程》中的 DispatcherServletrender 方法中看看,如下:

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 根据 viewNamelocale 解析出对应的 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

初始化过程

DispatcherServletinitViewResolvers(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 中,默认开启

  1. 如果“关闭”探测功能,则获得 Bean 名称为 "viewResolver" 对应的 Bean ,将其添加至 viewResolvers

  1. 如果未获得到,则获得默认配置的 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

  1. 情况二,如果 viewResolvers 非空,则和 matchingBeans 进行比对,判断哪些未进行初始化,进行初始化

  1. 排序 viewResolvers 数组

  1. 设置 cnmFactoryBeanservletContext 属性为当前 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 数组,详情见下文

  1. 调用 getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes) 方法,获得匹配的 View 数组,详情见下文

  1. 调用 getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) 方法,筛选出最匹配的 View 对象,如果筛选成功则直接返回,详情见下文

  1. 如果匹配不到 View 对象,则根据 useNotAcceptableStatusCode,返回 NOT_ACCEPTABLE_VIEWnull,其中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 添加到 compatibleMediaTypes 结果中
        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 数组

  1. 情况一,通过当前 ViewResolver 实现类获得 View 对象,添加到 candidateViews

  1. 情况二,遍历入参 List<MediaType> requestedMediaTypes,将带有拓展后缀的类型再通过当前 ViewResolver 实现类获得 View 对象,添加到 candidateViews
    2. 获得 MediaType 对应的拓展后缀的数组(默认情况下未配置)
    3. 遍历拓展后缀的数组
    4. 带有拓展后缀的方式,通过当前 ViewResolver 实现类获得 View 对象,添加到 candidateViews

  1. 来源二,添加 defaultViewscandidateViews

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 ,优先级更高。

  1. 遍历 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 类。实际上,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);
}
  1. 将 Model 对象与请求中的数据进行合并,生成一个 Map 对象,保存进入页面的一些数据

  1. 进行一些准备工作(修复 IE 中存在的 BUG)

  1. 调用 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 有进一步的理解,路漫漫其修远兮~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/370860.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

进程地址空间(虚拟地址空间)

目录 引入问题 测试代码 引入地址空间 故事1&#xff1a; 故事二&#xff1a; 解决问题 为什么有虚拟地址空间 扩展 扩展1&#xff08;没有地址空间&#xff0c;OS如何工作&#xff09; 扩展2 &#xff08;代码只读深入了解&#xff09; 扩展3&#xff08;malloc本质…

0 初识Kotlin

0 基本介绍 相信很多开发者对Kotlin还是比较陌生的。 Kotlin是一种新型的编程语言&#xff0c;由JetBrains公司开发与设计&#xff0c;在2012年开源&#xff0c; 但没引起什么注意。 直到2017年google宣布将Kotlin作为Android开发的首选语言&#xff0c;Kotlin才开始大放异彩。…

基于MATLAB的MIMO预编码设计:优化迫零算法(附完整代码与分析)

目录 一.介绍 二. 对比本方案优化后的迫零算法与原始的迫零算法 三. 源代码 四. 运行结果及分析 4.1 天线数为8 4.2 天线数为128 一.介绍 图中“RF Chain” 全称为Radio Frequency Chain&#xff0c;代表射频链路。 此MIMO预编码包含了基带预编码W&#xff08;改变幅度和…

NVIDIA GPU开源驱动编译学习架构分析

2022年5月&#xff0c;社区终于等到了这一天&#xff0c;NVIDIA开源了他们的LINUX GPU 内核驱动&#xff0c; Linux 内核总设计师 Linus Torvalds 十年前说过的一句话&#xff0c;大概意思是英伟达是LINUX开发者遇到的硬件厂商中最麻烦的一个&#xff0c;说完这句话之后&#x…

20230225英语学习

Is Your Phone Heavier When It’s Full of Data? We’ve Done the Math 从数学角度看&#xff0c;充满数据的手机会更重吗&#xff1f; Here’s a weird question: does your phone weigh more when it’s “full” than when it’s “empty”?It sounds almost ridiculou…

【unity游戏制作-mango的冒险】场景二的镜头和法球特效跟随

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 秩沅 原创 收录于专栏&#xff1a;unity游戏制作 ⭐mango的冒险场景二——镜头和法球特效跟随⭐ 文章目录⭐mango的冒险场景二——镜…

C#的多线程、线程池和Task

线程 被定义为程序的执行路径。每个线程都定义了一个独特的控制流。如果您的应用程序涉及到复杂的和耗时的操作&#xff0c;那么设置不同的线程执行路径往往是有益的&#xff0c;每个线程执行特定的工作。 线程是轻量级进程。一个使用线程的常见实例是现代操作系统中并行编程的…

如何使用Python和ftplib模块连接到FTP服务器并列出远程目录中的文件?

ftp服务可以用在以下一些使用场景&#xff1a; 文件共享&#xff1a;使用Python和FTP服务器可以轻松地搭建一个文件共享服务&#xff0c;使得用户可以上传和下载文件&#xff0c;从而促进协作和信息共享。 数据备份&#xff1a;FTP可以用于将数据备份到另一个服务器或云存储中…

Git ---- GitHub 操作

Git ---- GitHub 操作1. 创建远程仓库2. 远程仓库操作1. 创建爱你远程仓库别名2. 推送本地分支到远程仓库3. 克隆远程仓库到本地4. 邀请加入团队5. 拉取远程库内容3. 跨团队协作4. SSH 免密登录GitHub 网址&#xff1a;https://github.com/ Ps&#xff1a;全球最大同性交友网站…

实现弹窗功能并修改其中一个系数

把鼠标放在number-info上面,会是一个delon/chart的类库,可以在NG-ALAIN上找到阅读NG ALAIN的图表,以及number-info样式,数据文本 它拥有[title] [subtitle]两个可以是TemplateRef类型的,而template可以在里面放一些东西,比如按钮,所以可以放一个修改按钮 这里刚开始把template放…

学习 Python 之 Pygame 开发魂斗罗(三)

学习 Python 之 Pygame 开发魂斗罗&#xff08;三&#xff09;继续编写魂斗罗1. 角色站立2. 角色移动3. 角色跳跃4. 角色下落继续编写魂斗罗 在上次的博客学习 Python 之 Pygame 开发魂斗罗&#xff08;二&#xff09;中&#xff0c;我们完成了角色的创建和更新&#xff0c;现…

MySQL高级第一讲

目录 一、MySQL高级01 1.1 索引 1.1.1 索引概述 1.1.2 索引特点 1.1.3 索引结构 1.1.4 BTREE结构(B树) 1.1.5 BTREE结构(B树) 1.1.6 索引分类 1.1.7 索引语法 1.1.8 索引设计原则 1.2 视图 1.2.1 视图概述 1.2.2 创建或修改视图 1.3 存储过程和函数 1.3.1 存储过…

openresty的部署、nginx高速缓存的配置、nginx日志的可视化

文章目录一、openresty1.OpenResty简介2.OpenResty的技术3.OpenResty的优势4.openresty部署实验二、nginx配置高效缓存三、nginx日志可视化一、openresty 1.OpenResty简介 OpenResty官网 http://openresty.org/cn/ OpenResty是一个基于 Nginx 与 Lua 的高性能 Web 平台&#x…

shell基础学习

文章目录查看shell解释器写hello world多命令处理执行变量常用系统变量自定义变量撤销变量静态变量变量提升为全局环境变量特殊变量$n$#$* $$?运算符:条件判断比较流程控制语句ifcasefor 循环while 循环read读取控制台输入基本语法:函数系统函数basenamedirname自定义函数shel…

FL StudioV21电脑版水果编曲音乐编辑软件

这是一款功能十分丰富和强大的音乐编辑软件&#xff0c;能够帮助用户进行编曲、剪辑、录音、混音等操作&#xff0c;让用户能够全面地调整音频。FL水果最新版是一款专业级别的音乐编曲软件&#xff0c;集合更多的编曲功能为一身&#xff0c;可以进行录音、编辑、制作、混音、调…

计算机网络(六): HTTP,HTTPS,DNS,网页解析全过程

文章目录一、HTTP头部包含的信息通用头部请求头部响应头部实体头部二、Keep-Alive和非Keep-Alive的区别三、HTTP的方法四、HTTP和HTTPS建立连接的过程4.1 HTTP4.2 HTTPS五、HTTP和HTTPS的区别六、HTTPS的加密方式七、cookie和sessionsessioncookie八、HTTP状态码状态码200&…

【微信小程序】-- WXML 模板语法 - 数据绑定(九)

&#x1f48c; 所属专栏&#xff1a;【微信小程序开发教程】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &…

DPDK系列之四DPDK整体框架分析说明

一、网络发展和DPDK 在上篇分析过网络应用对DPDK出现的影响。而具体体现在技术上&#xff0c;从最简单来看就是从C10K到c100K甚至更多。而相应的计算的发展也从挖掘单CPU的性能发展到了瓶颈&#xff0c;同样&#xff0c;对于网络设备也遇到了类似的问题。而目前解决问题的方法…

MySQL到Elasticsearch实时同步构建数据检索服务的选型与思考[转载]

前言 本文具体探讨 MySQL 数据实时同步到 Elasticsearch (以下简称 ES ) 技术方案和思考&#xff0c;同时使用一定篇幅介绍一些前置知识&#xff0c;从理论到实践&#xff0c;让读者更好的理解这块内容和相关问题。包括&#xff1a; 为什么我们要将数据从 MySQL 实时同步到 ES …

Day899.Join语句优化 -MySQL实战

Join语句优化 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于Join语句优化的内容。 join 语句的两种算法&#xff0c;分别是 Index Nested-Loop Join(NLJ) 和 Block Nested-Loop Join(BNL)。 发现在使用 NLJ 算法的时候&#xff0c;其实效果还是不错的&#xff0c…