当前位置: 首页 > news >正文

Spring MVC 源码- ViewResolver 组件

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 对象*/@NullableView 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 WebApplicationObjectSupportimplements ViewResolver, Ordered, InitializingBean {@Nullableprivate ContentNegotiationManager contentNegotiationManager;/*** ContentNegotiationManager 的工厂,用于创建 {@link #contentNegotiationManager} 对象*/private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean();/*** 在找不到 View 对象时,返回 {@link #NOT_ACCEPTABLE_VIEW}*/private boolean useNotAcceptableStatusCode = false;/*** 默认 View 数组*/@Nullableprivate List<View> defaultViews;/*** ViewResolver 数组*/@Nullableprivate 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、InternalResourceViewResolverif (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 中,说明已经初始化,则直接 continueif (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@Nullablepublic String getContentType() {return null;}@Overridepublic 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 对象不存在,则返回 nullif (!context.containsBean(viewName)) {// Allow for ViewResolver chaining...return null;}// 如果 Bean 对应的 Bean 类型不是 View ,则返回 nullif (!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@Nullablepublic String getContentType() {return null;}@Overridepublic 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) {@Overrideprotected 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 {// 获得缓存 KEYObject 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_VIEWif (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 的类型*/@Nullableprivate Class<?> viewClass;/*** 前缀*/private String prefix = "";/*** 后缀*/private String suffix = "";/*** ContentType 类型*/@Nullableprivate String contentType;private boolean redirectContextRelative = true;private boolean redirectHttp10Compatible = true;@Nullableprivate String[] redirectHosts;/*** RequestAttributes 暴露给 View 使用时的属性*/@Nullableprivate String requestContextAttribute;/** Map of static attributes, keyed by attribute name (String). */private final Map<String, Object> staticAttributes = new HashMap<>();/*** 是否暴露路径变量给 View 使用*/@Nullableprivate Boolean exposePathVariables;@Nullableprivate Boolean exposeContextBeansAsAttributes;@Nullableprivate String[] exposedContextBeanNames;/*** 是否只处理指定的视图名们*/@Nullableprivate 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直接返回viewNamereturn 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;}}// 情况二,直接返回 viewreturn 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());@Nullableprivate Boolean alwaysInclude;public InternalResourceViewResolver() {// 获得 viewClassClass<?> viewClass = requiredViewClass();if (InternalResourceView.class == viewClass && jstlPresent) {viewClass = JstlView.class;}// 设置 viewClasssetViewClass(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";@Nullabledefault 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、LocalizationContextexposeHelpers(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 有进一步的理解,路漫漫其修远兮~

相关文章:

Spring MVC 源码- ViewResolver 组件

ViewResolver 组件ViewResolver 组件&#xff0c;视图解析器&#xff0c;根据视图名和国际化&#xff0c;获得最终的视图 View 对象回顾先来回顾一下在 DispatcherServlet 中处理请求的过程中哪里使用到 ViewResolver 组件&#xff0c;可以回到《一个请求响应的旅行过程》中的 …...

【Hello Linux】初识冯诺伊曼体系

作者&#xff1a;小萌新 专栏&#xff1a;Linux 作者简介&#xff1a;大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;简单介绍冯诺伊曼体系 冯诺伊曼体系 冯诺伊曼体系结构的合理性 我们在Linux的第一篇博客中讲解了第一台计算机的发明是为了解决导弹的…...

mysql索引,主从多个核心主题去探索问题。

网上收集不错的优化方案 事务 mvcc 详讲 详讲 索引 索引概念 MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据 库系统还维护者满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据, 这样就可以在这些数 据…...

前端一面必会面试题(边面边更)

哪些情况会导致内存泄漏 以下四种情况会造成内存的泄漏&#xff1a; 意外的全局变量&#xff1a; 由于使用未声明的变量&#xff0c;而意外的创建了一个全局变量&#xff0c;而使这个变量一直留在内存中无法被回收。被遗忘的计时器或回调函数&#xff1a; 设置了 setInterval…...

【Hello Linux】初识操作系统

作者&#xff1a;小萌新 专栏&#xff1a;Linux 作者简介&#xff1a;大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;简单介绍下操作系统的概念 操作系统 操作系统是什么&#xff1f; 操作系统是管理软硬件资源的软件 为什么要设计操作系统 为什么要设…...

完美的vue3动态渲染菜单路由全程

前言&#xff1a; 首先&#xff0c;我们需要知道&#xff0c;动态路由菜单并非一开始就写好的&#xff0c;而是用户登录之后获取的路由菜单再进行渲染&#xff0c;从而可以起到资源节约何最大程度的保护系统的安全性。 需要配合后端&#xff0c;如果后端的值不匹配&#xff0…...

2023年CDGA考试模拟题库(301-400)

2023年CDGA考试模拟题库(301-400) 300.无附加价值的信息通常也不会被删除,因为:[1分] A.它不应该被移除,所有数据都是有价值的 B.我们可能在以后的某个阶段需更这些信息 C.规程中不明确是否应该保留 D.数据是一种资产它很可能在未来被认为是有价值的 E.规程中不明确哪些是…...

Linux-常见命令

&#x1f69c;关注博主&#xff1a;翻斗花园代码手牛爷爷 &#x1f699;Gitee仓库&#xff1a;牛爷爷爱写代码 目录&#x1f692;xshell热键&#x1f697;Linux基本命令&#x1f697;ls指令&#x1f695;pwd指令&#x1f696;cd指令&#x1f68c;touch指令&#x1f68d;mkdir指…...

2.25测试对象分类

一.按照测试对象划分1.界面测试又称UI测试,按照界面的需求(一般是ui设计稿)和界面的设计规则,对我们软件界面所展示的全部内容进行测试和检查.对于非软件来说:颜色,大小,材质,整体是否美观对于软件来说:输入框,按钮,文字,图片...的尺寸,颜色,形状,整体适配,清晰度等等,2.可靠性…...

【Zabbix实战之部署篇】Zabbix客户端的安装部署方法

【Zabbix实战之部署篇】Zabbix客户端的安装部署方法 一、Zabbix-agent2介绍1.Zabbix-agent2简介2.Zabbix-agent2优点3.主动模式和被动模式二、环境规划1.Zabbix服务器部署链接2.IP规划三、配置客户端系统环境1.关闭selinux2.放行端口或关闭防火墙四、安装zabbix-agent21.下载za…...

【CSS】CSS 层叠样式表 ② ( CSS 引入方式 - 内嵌样式 )

文章目录一、CSS 引入方式 - 内嵌样式1、内嵌样式语法2、内嵌样式示例3、内嵌样式完整代码示例4、内嵌样式运行效果一、CSS 引入方式 - 内嵌样式 1、内嵌样式语法 CSS 内嵌样式 , 一般将 CSS 样式写在 HTML 的 head 标签中 ; CSS 内嵌样式 语法如下 : <head><style …...

MySQL事务与索引

MySQL事务与索引 一、事务 1、事务简介 在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。事务处理可以用来维护数据库的完整性&#xff0c;保证成批的 SQL 语句要么全部执行&#xff0c;要么全部不执行。事务用来管理 insert,update,delete 语句 事务特性…...

【编程入门】应用市场(php版)

背景 前面已输出多个系列&#xff1a; 《十余种编程语言做个计算器》 《十余种编程语言写2048小游戏》 《17种编程语言10种排序算法》 《十余种编程语言写博客系统》 《十余种编程语言写云笔记》 《N种编程语言做个记事本》 目标 为编程初学者打造入门学习项目&#xff0c;使…...

文化:你所在的团队,有多少人敢讲真话?

你好&#xff0c;我是叶芊。 今天我们要讨论的话题是文化&#xff0c;说“文化”这个词你可能会觉得很虚&#xff0c;那我们换个词——“做事风格”&#xff0c;这就和你们团队平时的协作习惯密切相关了。 做事风格&#xff0c;往小了讲&#xff0c;会影响团队成员对开会的认知…...

Linux | 项目自动化构建工具 - make/Makefile

make / Makefile一、前言二、make/Makefile背景介绍1、Makefile是干什么的&#xff1f;2、make又是什么&#xff1f;三、demo实现【见见猪跑&#x1f416;】三、依赖关系与依赖方法1、概念理清2、感性理解【父与子】3、深层理解【程序的翻译环境 栈的原理】四、多学一招&#…...

Spring源码该如何阅读?十年架构师带来的Spring源码解析千万不要错过!

写在前面最近学习了一句话&#xff0c;感觉自己的世界瞬间明朗&#xff0c;不再那么紧张焦虑恐慌&#xff0c;同样推荐给大家&#xff0c;希望我们都终有所得。“如果一个人不是发自内心地想要做一件事情&#xff0c;那么&#xff0c;他是无法改变自己的人生的。” 同样这句话用…...

sonarqube 外部扫描器 go vet golangci-lint 无法导入问题

首先&#xff0c;请看[外部分析报告]各种语言的报告生成 go vet 2> govet-report.out#没有golangci-lint&#xff0c;我从网上找到了 golangci-lint run --out-format checkstyle ./... > golangci-lint-report.xml值得注意的是&#xff0c;貌似不支持目录&#xff0c;仅…...

Tesseract-OCR 控制台怎么使用

Tesseract-OCR 控制台是一个命令行工具&#xff0c;可以在 Windows、Linux、macOS 等操作系统中使用。下面是使用 Tesseract-OCR 控制台进行文字识别的基本步骤&#xff1a;安装 Tesseract-OCR&#xff1a;可以到 Tesseract-OCR 的官方网站&#xff08;https://github.com/tess…...

九龙证券|美股创年内最大周跌幅!美联储官员密集发声!波音重挫近5%

当地时刻2月24日&#xff0c;美股三大指数收盘明显跌落。道指跌1.02%&#xff0c;标普500指数跌1.05%&#xff0c;纳指跌1.69%。 大型科技股普跌&#xff0c;微软、亚马逊跌超2%。波音大跌4.8%&#xff0c;居道指跌幅榜首位&#xff0c;公司因机身部件有问题再次暂停向用户交付…...

C++014-C++字符串

文章目录C014-C字符串字符串目标char[]和stringchar[]char*string字符常量与字符串常量字符串的输入题目描述 字符串输出题目描述在线练习&#xff1a;总结C014-C字符串 在线练习&#xff1a; http://noi.openjudge.cn/ https://www.luogu.com.cn/ 字符串 目标 1、了解字符串…...

Android 架构 MVC MVP MVVM,这一波你应该了然于心

MVC&#xff0c;MVP和MVVM是软件比较常用的三种软件架构&#xff0c;这三种架构的目的都是分离&#xff0c;避免将过多的逻辑全部堆积在一个类中。在Android中&#xff0c;Activity中既有UI的相关处理逻辑&#xff0c;又有数据获取逻辑&#xff0c;从而导致Activity逻辑复杂不单…...

物联网在医疗保健领域的5大创新应用

如今&#xff0c;物联网的发展越来越迅速&#xff0c;我们无法低估物联网在当今世界的重要性。大多数人每天都会使用到物联网设备。例如&#xff0c;当你使用智能手表来跟踪你的锻炼时&#xff0c;你就间接地使用了物联网的功能。由于物联网为世界带来了很多有效的帮助&#xf…...

【一天一门编程语言】Haskell 语言程序设计极简教程

Haskell 语言程序设计极简教程 一、什么是 Haskell Haskell 是一种纯函数式编程语言&#xff0c;它把程序设计抽象化到一个更高的层次&#xff0c;简化程序开发工作量&#xff0c;能够更快更容易地完成任务。 它是一种函数式编程语言&#xff0c;它采用函数式编程方法&#…...

getStaticPaths函数 以及 fallback参数

getStaticPaths是Next.js的一个静态生成API&#xff0c;它用于在构建时确定哪些页面需要被预渲染。它需要返回一个包含params属性的对象数组&#xff0c;其中每个对象都代表一个路径参数集合&#xff0c;可以被预渲染为一个静态页面。如果所有参数都已知&#xff0c;它们将被硬…...

msys2+minGW方案编译ffmpeg的最佳实践

一、Win10 64bit编译环境的建立1&#xff09;从http://www.msys2.org/下载 msys2-x86_64-xxx.exe2&#xff09; 安装msys2到默认路径 C:\msys64\3&#xff09; 运行MSYS2 w644&#xff09;执行 pacman -Syu 更新系统当出现提示时&#xff0c;选择y5) 当窗口关闭时&#xff0c;重…...

理解redis的数据结构

redis为什么快&#xff1f; 首先可以想到内存读写数据本来就快&#xff0c;然后IO复用快&#xff0c;单线程没有静态消耗和锁机制快。 还有就是数据结构的设计快。这是因为&#xff0c;键值对是按一定的数据结构来组织的&#xff0c;操作键值对最终就是对数据结构进行增删改查操…...

Lecture6 逻辑斯蒂回归(Logistic Regression)

目录 1 常用数据集 1.1 MNIST数据集 1.2 CIFAR-10数据集 2 课堂内容 2.1 回归任务和分类任务的区别 2.2 为什么使用逻辑斯蒂回归 2.3 什么是逻辑斯蒂回归 2.4 Sigmoid函数和饱和函数的概念 2.5 逻辑斯蒂回归模型 2.6 逻辑斯蒂回归损失函数 2.6.1 二分类损失函数 2.…...

File类及IO流说明

目录 1.File类说明 (1)构造方法创建文件 (2)创建功能 (3)File类的判断和获取功能 (4)文件删除功能 2.I/O流说明 (1).分类 3.字节流写数据 (1)说明 (2)字节流写数据的三种方式 (3)写入时实现换行和追加写入 (4)异常处理中加入finally实现资源的释放 4.字节流读数据 …...

优秀的网络安全工程师应该有哪些能力?

网络安全工程师是一个各行各业都需要的职业&#xff0c;工作内容属性决定了它不会只在某一方面专精&#xff0c;需要掌握网络维护、设计、部署、运维、网络安全等技能。目前稍有经验的薪资在10K-30K之间&#xff0c;全国的网络安全工程师还处于一个供不应求的状态&#xff0c;因…...

[C++11] auto初始值类型推导

背景&#xff1a;旧标准的auto 在旧标准中&#xff0c;auto代表“具有自动存储期的 局部变量” auto int i 0; //具有自动存储期的局部变量 //C98/03&#xff0c;可以默认写成int i0; static int j 0; //静态类型的定义方法实际上&#xff0c;我们很少使用auto&#xff0c…...