`
sha0k
  • 浏览: 84892 次
  • 性别: Icon_minigender_1
  • 来自: 西安
社区版块
存档分类
最新评论

Spring MVC 源码学习札记(一)DispatcherServlet阅读

 
阅读更多

马上过年了~闲来无事,阅读Spring源码提升自己,做事要有个流程,大家都知道Spring MVC的请求处理流程

 

springmvc

请求进来后必须经由Front controller,而在spring mvc中Front controller就是DispatcherServlet

在我看来,DispatcherServlet作为分发请求交给Controller处理的spring mvc门卫,就是阅读spring mvc源代码的入口。

什么都是由浅入深,看不懂的我搁着  先看能看懂的,对spring mvc用透彻了再回头看看不懂的,我相信能更理解它

/**
	 * Initialize the ViewResolvers used by this class.
	 * <p>If no ViewResolver beans are defined in the BeanFactory for this
	 * namespace, we default to InternalResourceViewResolver.
	 */
	private void initViewResolvers(ApplicationContext context) {
		this.viewResolvers = null;

		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<ViewResolver>(matchingBeans.values());
				// We keep ViewResolvers in sorted order.
				OrderComparator.sort(this.viewResolvers);
			}
		}
		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.
		if (this.viewResolvers == null) {
			this.viewResolvers = getDefaultStrategies(context, ViewResolver.class);
			if (logger.isDebugEnabled()) {
				logger.debug("No ViewResolvers found in servlet '" + getServletName() + "': using default");
			}
		}
	}

  上面这段代码从字面和注释意思可以看出是初始化ViewResolver,从ApplicationContext中获取

OrderComparator.sort(this.viewResolvers);

  这一段根据经验可以想象出当获取了所有的viewResolver后,对其进行排序,根据当然就是它的order属性,在***-servlet.xml中,配置多个viewResolver时,我们经常会设置order,以保证查找的优先顺序。

/** Detect all ViewResolvers or just expect "viewResolver" bean? */
	private boolean detectAllViewResolvers = true;

  在DispatcherServlet中是否查找ApplicationContext的所有viewResolver,这个属性默认为true

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.
			}
		}

  如果非true则表示不会查找所有viewResolver,那么根据VIEW_RESOLVER_BEAN_NAME = "viewResolver"去查找对应的单个viewResolver。

 

if (this.viewResolvers == null) {
			this.viewResolvers = getDefaultStrategies(context, ViewResolver.class);
			if (logger.isDebugEnabled()) {
				logger.debug("No ViewResolvers found in servlet '" + getServletName() + "': using default");
			}
		}

  如果上面的true和false的情况都未找到对应viewResolver,就使用默认的ViewResolver.class 即 InternalResourceViewResolver。

  下面来说请求分发的最重要的方法,这里会有很多难点,但是不怕,最重要是走通整个过程:

/**
	 * Process the actual dispatching to the handler.
	 * <p>The handler will be obtained by applying the servlet's HandlerMappings in order.
	 * The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters
	 * to find the first that supports the handler class.
	 * <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers
	 * themselves to decide which methods are acceptable.
	 * @param request current HTTP request
	 * @param response current HTTP response
	 * @throws Exception in case of any kind of processing failure
	 */
	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		int interceptorIndex = -1;

		try {
			ModelAndView mv;
			boolean errorView = false;

			try {
				processedRequest = checkMultipart(request);

				// Determine handler for the current request.
				mappedHandler = getHandler(processedRequest, false);
				if (mappedHandler == null || mappedHandler.getHandler() == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// Apply preHandle methods of registered interceptors.
				HandlerInterceptor[] interceptors = mappedHandler.getInterceptors();
				if (interceptors != null) {
					for (int i = 0; i < interceptors.length; i++) {
						HandlerInterceptor interceptor = interceptors[i];
						if (!interceptor.preHandle(processedRequest, response, mappedHandler.getHandler())) {
							triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, null);
							return;
						}
						interceptorIndex = i;
					}
				}

				// Actually invoke the handler.
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				// Do we need view name translation?
				if (mv != null && !mv.hasView()) {
					mv.setViewName(getDefaultViewName(request));
				}

				// Apply postHandle methods of registered interceptors.
				if (interceptors != null) {
					for (int i = interceptors.length - 1; i >= 0; i--) {
						HandlerInterceptor interceptor = interceptors[i];
						interceptor.postHandle(processedRequest, response, mappedHandler.getHandler(), mv);
					}
				}
			}
			catch (ModelAndViewDefiningException ex) {
				logger.debug("ModelAndViewDefiningException encountered", ex);
				mv = ex.getModelAndView();
			}
			catch (Exception ex) {
				Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
				mv = processHandlerException(processedRequest, response, handler, ex);
				errorView = (mv != null);
			}

			// Did the handler return a view to render?
			if (mv != null && !mv.wasCleared()) {
				render(mv, processedRequest, response);
				if (errorView) {
					WebUtils.clearErrorRequestAttributes(request);
				}
			}
			else {
				if (logger.isDebugEnabled()) {
					logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
							"': assuming HandlerAdapter completed request handling");
				}
			}

			// Trigger after-completion for successful outcome.
			triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, null);
		}

		catch (Exception ex) {
			// Trigger after-completion for thrown exception.
			triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, ex);
			throw ex;
		}
		catch (Error err) {
			ServletException ex = new NestedServletException("Handler processing failed", err);
			// Trigger after-completion for thrown exception.
			triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, ex);
			throw ex;
		}

		finally {
			// Clean up any resources used by a multipart request.
			if (processedRequest != request) {
				cleanupMultipart(processedRequest);
			}
		}
	}

   通读上面的代码首先遇到第一个问题:

processedRequest = checkMultipart(request);

   这就需要来看看checkMultipart方法了。

/**
	 * Convert the request into a multipart request, and make multipart resolver available.
	 * <p>If no multipart resolver is set, simply use the existing request.
	 * @param request current HTTP request
	 * @return the processed request (multipart wrapper if necessary)
	 * @see MultipartResolver#resolveMultipart
	 */
	protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
		if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
			if (request instanceof MultipartHttpServletRequest) {
				logger.debug("Request is already a MultipartHttpServletRequest - if not in a forward, " +
						"this typically results from an additional MultipartFilter in web.xml");
			}
			else {
				return this.multipartResolver.resolveMultipart(request);
			}
		}
		// If not returned before: return original request.
		return request;
	}

 从方法的字面意思首先可以看出这个方法用来检查request是否是multipartrequest,众所周知MultipartResolver在Spring中用来处理文件上传,可参见http://zachary-guo.iteye.com/blog/1294443 这篇博文。

首先进行request是否是MultipartHttpServletRequest的判断

if (this.multipartResolver != null && this.multipartResolver.isMultipart(request))
//multipartResolver与viewResolver一样都有init方法进行初始化。
Request is already a MultipartHttpServletRequest - if not in a forward, " +
						"this typically results from an additional MultipartFilter in web.xml

 告诉我们request在web.xml中配置的MultipartFilter拦截过,已经被转换为MultipartHttpServletRequest,如果没有转换,则在else语句中进行转换。

 当判断request并非MultipartHttpServletRequest实例时,直接返回。

 代码继续往下读,我们可以看到第二个问题了:

// Determine handler for the current request.
				mappedHandler = getHandler(processedRequest, false);

  了解过HandlerMapping接口的同学都知道

public interface HandlerMapping {

HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;

}

 就是用来查找处理request所对应的HandlerExecutionChain,对HandlerExecutionChain的理解,以我的知识,只能给出大概的意思,就是Adapter和一组interceptor,Adapter用来干嘛呢,造Controller吧,暂时这么认为。下面就来看Dispatcher里的handler方法。

/**
	 * Return the HandlerExecutionChain for this request. Try all handler mappings in order.
	 * @param request current HTTP request
	 * @param cache whether to cache the HandlerExecutionChain in a request attribute
	 * @return the HandlerExceutionChain, or <code>null</code> if no handler could be found
	 */
	protected HandlerExecutionChain getHandler(HttpServletRequest request, boolean cache) throws Exception {
		HandlerExecutionChain handler = (HandlerExecutionChain) request.getAttribute(HANDLER_EXECUTION_CHAIN_ATTRIBUTE);
		if (handler != null) {
			if (!cache) {
				request.removeAttribute(HANDLER_EXECUTION_CHAIN_ATTRIBUTE);
			}
			return handler;
		}

		for (HandlerMapping hm : this.handlerMappings) {
			if (logger.isTraceEnabled()) {
				logger.trace(
						"Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
			}
			handler = hm.getHandler(request);
			if (handler != null) {
				if (cache) {
					request.setAttribute(HANDLER_EXECUTION_CHAIN_ATTRIBUTE, handler);
				}
				return handler;
			}
		}
		return null;
	}

  第二个参数cache可以看出就是设置缓存,为request保存它的HandlerExecutionChain,而整个getHandler方法其实就是遍历ApplicationContext中用到的HandlerMapping,查找匹配到request的handler。

  接着往下走,看到了如果遍历都没有查找出handler,那么就得进入下面的方法了:

noHandlerFound(processedRequest, response);

  它的主体:

/**
	 * No handler found -> set appropriate HTTP response status.
	 * @param request current HTTP request
	 * @param response current HTTP response
	 * @throws Exception if preparing the response failed
	 */
	protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
		if (pageNotFoundLogger.isWarnEnabled()) {
			String requestUri = new UrlPathHelper().getRequestUri(request);
			pageNotFoundLogger.warn("No mapping found for HTTP request with URI [" + requestUri +
					"] in DispatcherServlet with name '" + getServletName() + "'");
		}
		response.sendError(HttpServletResponse.SC_NOT_FOUND);
	}

  看一眼就明白就是报错用的。

  继续向下:

 

// Apply preHandle methods of registered interceptors.
				HandlerInterceptor[] interceptors = mappedHandler.getInterceptors();

  先搁下,因为它涉及的方法还没有研究和理解。以后再看,做任何事都要有主次之分,我们现在是了解流程,知道它就是预处理,那就等后面再看懂这个方法。

  我们再来看它获取Adapter的过程:

 

/**
	 * Return the HandlerAdapter for this handler object.
	 * @param handler the handler object to find an adapter for
	 * @throws ServletException if no HandlerAdapter can be found for the handler. This is a fatal error.
	 */
	protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
		for (HandlerAdapter ha : this.handlerAdapters) {
			if (logger.isTraceEnabled()) {
				logger.trace("Testing handler adapter [" + ha + "]");
			}
			if (ha.supports(handler)) {
				return ha;
			}
		}
		throw new ServletException("No adapter for handler [" + handler +
				"]: Does your handler implement a supported interface like Controller?");
	}

  又是一个遍历的过程,为了找寻支持handler的Adapter,handler我们暂且以Controller为例来说明,当看到这里的代码时,即使你在前面有所疑问,那也拨开云雾看到晴天了。我们到目前为之所做的工作就是为request,例如"/login"找到能够处理它的Controller,而这个Controller怎么来Adapter制造出来的。

 

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

  这里从字面,我们能想象和理解得到,这已经经过了Controller处理的request,返回了ModelandView对象,

 

// Do we need view name translation?
				if (mv != null && !mv.hasView()) {//注意这里是没有视图的时候
					mv.setViewName(getDefaultViewName(request));
				}

  这段代码又是干什么呢?我也晕,因为看了一遍它的调用和初始化之后,不是太明白,不急慢慢来分析。

 

/**
	 * Translate the supplied request into a default view name.
	 * @param request current HTTP servlet request
	 * @return the view name (or <code>null</code> if no default found)
	 * @throws Exception if view name translation failed
	 */
	protected String getDefaultViewName(HttpServletRequest request) throws Exception {
		return this.viewNameTranslator.getViewName(request);
	}

 调用的是this.viewNameTranslator的方法,OK,真麻烦的过程,那咱就去看

viewNameTranslator
	/** RequestToViewNameTranslator used by this servlet */
	private RequestToViewNameTranslator viewNameTranslator;

 还是不懂它是干嘛用的,从字面意思看起来像是对请求到视图名这一过程的分析转换,唉,那就去看它的初始化。

 

/**
	 * Initialize the RequestToViewNameTranslator used by this servlet instance.
	 * <p>If no implementation is configured then we default to DefaultRequestToViewNameTranslator.
	 */
	private void initRequestToViewNameTranslator(ApplicationContext context) {
		try {
			this.viewNameTranslator =
					context.getBean(REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME, RequestToViewNameTranslator.class);
			if (logger.isDebugEnabled()) {
				logger.debug("Using RequestToViewNameTranslator [" + this.viewNameTranslator + "]");
			}
		}
		catch (NoSuchBeanDefinitionException ex) {
			// We need to use the default.
			this.viewNameTranslator = getDefaultStrategy(context, RequestToViewNameTranslator.class);
			if (logger.isDebugEnabled()) {
				logger.debug("Unable to locate RequestToViewNameTranslator with name '" +
						REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME + "': using default [" + this.viewNameTranslator +
						"]");
			}
		}
	}

  初始化的过程就是一个简单的读取ApplicationContext判断是否有注册

RequestToViewNameTranslator 

  如果我猜的没错的话,它其实是匹配request处理后一个自动给予的视图名

this.viewNameTranslator.getViewName(request);

  这个方法就是具体的实现了,但这不是我们本文讨论的内容,本文就是带着走DispatcherServlet的流程。

 好的,继续往下看,累了吧,那喝一杯 ,咱歇会儿

// Did the handler return a view to render?
			if (mv != null && !mv.wasCleared()) {
				render(mv, processedRequest, response);
				if (errorView) {
					WebUtils.clearErrorRequestAttributes(request);
				}
			}
			else {
				if (logger.isDebugEnabled()) {
					logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
							"': assuming HandlerAdapter completed request handling");
				}
			}

  看来到render了,那就转去看render吧

	/**
	 * Render the given ModelAndView.
	 * <p>This is the last stage in handling a request. It may involve resolving the view by name.
	 * @param mv the ModelAndView to render
	 * @param request current HTTP servlet request
	 * @param response current HTTP servlet response
	 * @throws Exception if there's a problem rendering the view
	 */
	protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
		// Determine locale for request and apply it to the response.
		Locale locale = this.localeResolver.resolveLocale(request);
		response.setLocale(locale);

		View view;
		if (mv.isReference()) {//这是ModelandView的方法用于判断它的属性Object view是否是一个字符串
			// We need to resolve the view name.
			view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
			if (view == null) {
				throw new ServletException(
						"Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" +
								getServletName() + "'");
			}
		}
		else {
			// No need to lookup: the ModelAndView object contains the actual View object.
			view = mv.getView();
			if (view == null) {
				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.isDebugEnabled()) {
			logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'");
		}
		view.render(mv.getModelInternal(), request, response);
	}

 

	public boolean isReference() {
		return (this.view instanceof String);
	}
 

首先是解析viewName,这里大家再熟悉不过了,肯定是ViewResolver的事情了

protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale,
			HttpServletRequest request) throws Exception {

		for (ViewResolver viewResolver : this.viewResolvers) {
			View view = viewResolver.resolveViewName(viewName, locale);
			if (view != null) {
				return view;
			}
		}
		return null;
	}

我们用初始化的viewResolvers根据我们的排序顺序,按优先级去查找它们对应前缀后缀的View

如果不是字符串呢,那Object view就已经是一个View对象了,则不需要ViewResolver来解析了。

到此,我们整个流程应该算是马马虎虎的看懂了,完成了,大家也看到了,在其中我们跳过了HandlerInceptor的预处理和后处理方法, View的render方法,HandlerAdapter的handle方法。

以后再了解这些类吧,了解他们的时候就能更好的看清Spring mvc的组织结构,这种看代码的方法其实挺舒服的,等于是我用了过程式的方法。

 

 

 

 

 

 

0
2
分享到:
评论

相关推荐

    Spring MVC源码深度剖析开源架构源码2021.pdf

    Spring MVC是Java EE开发中广泛使用的一个Web框架,它是Spring框架的一部分,主要负责Model-View-Controller(MVC)架构模式的实现。本知识点将围绕Spring MVC的源码深度剖析展开,详细介绍Spring MVC的源码结构、...

    开发Spring MVC应用程序补充—程序源码下载.rar_spring_spring mvc_spring mvc 源码_sp

    标题中的"开发Spring MVC应用程序补充—程序源码下载.rar_spring_spring mvc_spring mvc 源码_sp"表明这是一个关于Spring MVC框架的开发教程,其中包含了源代码供学习者参考。Spring MVC是Spring框架的一个核心组件...

    Mastering Spring MVC 4(2015.09)源码

    总的来说,"Mastering Spring MVC 4(2015.09)源码"提供了深入学习Spring MVC的机会,你可以通过阅读和分析源码来了解如何配置DispatcherServlet、怎样编写控制器、如何进行数据绑定与验证,以及如何利用拦截器等特性...

    Spring5MVC源码.docx

    【Spring5MVC源码分析】 Spring MVC 是一个基于Java的、用于构建Web应用程序的、高度可插拔的MVC框架,它是Spring Framework的重要组成部分。Spring MVC的核心目标是简化前端控制器的开发,使得开发者可以专注于...

    Spring源码学习十:DispatcherServlet请求分发源码分析1

    总的来说,Spring MVC的DispatcherServlet通过一系列步骤,从初始化时的bean扫描到请求到达时的分发处理,确保了请求能够准确地传递到正确的Controller方法。这一过程涉及到bean的扫描、请求映射的建立、拦截器的...

    spring mvc框架源码

    Spring MVC 是一个基于Java的轻量级Web应用框架,它为构建Web应用程序提供模型-视图-控制器(MVC)架构。Spring MVC的核心是解耦应用程序的各个组件,使其能够独立工作并易于测试。在本源码分析中,我们将探讨Spring...

    《精通Spring MVC 4》源码

    总的来说,《精通Spring MVC 4》源码是一个深度学习Spring MVC的绝佳资源。通过仔细阅读和分析这些源代码,开发者不仅可以提升对Spring MVC框架的理解,还能学习到设计模式、性能优化和最佳实践,从而在实际项目中更...

    spring mvc mybatis 整合源码,带数据库脚本,带详细注释

    - 学习Spring MVC和MyBatis的整合,可以参考官方文档、教程和开源项目的源码。 - 通过调试源码,理解其实现原理,有助于提升实战技能。 总结,这个压缩包提供了一个完整的Spring MVC和MyBatis整合的示例,包含了...

    Spring MVC jar包

    Spring MVC 是一个基于Java的轻量级Web应用框架,它为开发者提供了模型-视图-控制器(MVC)架构,使开发人员能够更好地组织和分离应用程序的业务逻辑、数据处理和用户界面。Spring MVC是Spring框架的一个核心组件,...

    Spring.MVC-A.Tutorial-Spring.MVC学习指南 高清可复制版PDF

    在Spring MVC中,每个请求都会经过DispatcherServlet,这是一个前端控制器,它会根据请求的URL和配置的映射规则将请求分发到合适的处理器。处理器可以是自定义的Controller类,Controller通过方法注解(如@...

    Spring源码学习九:DispatcherServlet初始化源码分析1

    Spring源码学习九:DispatcherServlet初始化源码分析1 DispatcherServlet是SpringMVC的核心分发器,它实现了请求分发,是处理请求的入口,本篇将深入源码分析它的初始化过程。 首先,从DispatcherServlet的名称上...

    最全最经典spring-mvc教程

    Spring MVC 是一款强大的Java Web开发框架,用于构建高效、可维护和模块化的Web应用程序。它作为Spring框架的一部分,提供了一种优雅的方式来处理HTTP请求和响应,使得开发者可以专注于业务逻辑而不是底层实现。在这...

    Spring MVC学习指南

    ### Spring MVC 学习指南详解 #### 一、Spring MVC 概述 Spring MVC 是 Spring 框架的一部分,主要用于构建基于 Model-View-Controller (MVC) 设计模式的 Web 应用程序。它提供了丰富的功能来简化 Web 开发过程,...

    Spring MVC 基础实例源码01

    Spring MVC 是一个基于Java的轻量级Web应用框架,它是Spring框架的重要组成部分,主要用于构建Web应用程序的后端控制器。这个"Spring MVC 基础实例源码01"的资源很可能是为了帮助初学者理解Spring MVC的核心概念和...

    Spring Mvc实例

    Spring MVC 是一个基于Java的轻量级Web应用框架,它是Spring框架的重要组成部分,主要用于构建Web应用程序的后端控制器。在本实例中,我们将探讨如何利用Spring MVC的注解来简化前后台交互,以及如何实现简单视图...

    Spring MVC 4.2.3

    Spring MVC是Spring框架的一个核心模块,专为构建Web应用程序而设计。它提供了模型-视图-控制器(MVC)架构,使开发者能够有效地分离业务逻辑、数据处理和用户界面。在"Spring MVC 4.2.3"版本中,我们看到了一系列的...

    spring mvc源码实例

    Spring MVC 是一个基于Java的轻量级Web应用框架,它为构建RESTful应用程序提供了强大的支持。这个源码实例应该是为了帮助开发者深入理解Spring MVC的工作原理和实现机制。下面,我们将详细探讨Spring MVC的关键知识...

    看透spring mvc源代码分析与实践扫描版带目录+源码

    通过深入学习Spring MVC的源码,开发者不仅可以更好地理解其内部机制,还能优化自己的代码,提升应用程序的性能和可维护性。这本扫描版书籍的源码分析部分,无疑会帮助读者在实践中应用这些知识,解决实际问题。

    springmvc项目源码

    通过深入学习这个 Spring MVC 项目源码,你可以掌握 MVC 设计模式的运用,了解 Spring 框架的组件协同工作方式,以及在实际项目中的最佳实践。这将有助于提升你的 Java Web 开发技能,更好地理解和使用 Spring MVC。

Global site tag (gtag.js) - Google Analytics