主頁 > 後端開發 > 精盡Spring MVC原始碼分析 - ViewResolver 組件

精盡Spring MVC原始碼分析 - ViewResolver 組件

2020-12-24 06:30:15 後端開發

該系列檔案是本人在學習 Spring MVC 的原始碼程序中總結下來的,可能對讀者不太友好,請結合我的原始碼注釋 Spring MVC 原始碼分析 GitHub 地址 進行閱讀

Spring 版本:5.2.4.RELEASE

該系列其他檔案請查看:《精盡 Spring MVC 原始碼分析 - 文章導讀》

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 中,默認開啟

  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 陣列
  2. 設定 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 陣列,詳情見下文

  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_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 添加到 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
  2. 來源二,添加 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 ,優先級更高,
  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 類,?? 實際上,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 物件,保存進入頁面的一些資料

  2. 進行一些準備作業(修復 IE 中存在的 BUG)

  3. 呼叫 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 >
    <!-- 自動給后面 action 的方法 return 的字串加上前綴和后綴,變成一個可用的地址 -->
    <property name="prefix" value="https://www.cnblogs.com/WEB-INF/jsp/" />
    <property name="suffix" value="https://www.cnblogs.com/lifullmoon/p/.jsp" />
</bean>

當回傳的視圖名稱為 login 時,View 物件的 url 就是 /WEB-INF/jsp/login.jsp,呼叫 View 的 render 方法進行頁面渲染時,請求會轉發到這個 url

當然,還有其他的 ViewResolver 實作類,例如 BeanNameViewResolver,目前大多數都是前后端分離的專案,這個組件也許你很少用到

至此,《精盡 Spring MVC 原始碼分析》系列最后一篇檔案已經講述完了,筆者寫的程序中也是懵懵懂懂的狀態,寫完之后才更加的理解,對于 Spring MVC 中大部分的內容都有分析到,你會發現 Spring MVC 原來是這么回事,開心~ ?????? 其中涉及到 Spring 相關內容在努力閱讀中,敬請期待~

希望這系列檔案能夠幫助你對 Spring MVC 的理解,路漫漫其修遠兮~

參考文章:芋道原始碼《精盡 Spring MVC 原始碼分析》

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/239389.html

標籤:Java

上一篇:有沒有大佬IDEA知道調出這個視窗是哪個快捷鍵?

下一篇:Linux部署SpringBoot專案jar包,輸出日志到檔案并追蹤

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more