- 浏览: 236059 次
- 性别:
- 来自: 上海
文章分类
最新评论
-
shuhucy:
必须赞啊,源码理解的很深,解决一个困扰两天的问题
Spring AOP源码分析(八)SpringAOP要注意的地方 -
sealinesu:
精彩
Spring事务源码分析(一)Spring事务入门 -
whlt20090509:
"WEB-INF/view目录下有一个简单的hell ...
SpringMVC源码总结(一)HandlerMapping和HandlerAdapter入门 -
hai0378:
兄台 算我一个,最近在研究dubbo motan 及 zk ...
ZooKeeper源码研究寻求小伙伴 -
zpkbtzmbw:
看懂了,原理
SpringMVC源码总结(五)Tomcat的URIEncoding、useBodyEncodingForURI和CharacterEncodingFilter
SpringMVC源码总结(十二)ViewResolver介绍
- 博客分类:
- SpringMVC源码分析
首先我们先看看ModelAndView中重要的View接口。
View接口:
再看下ViewResolver接口:
它是对给定的viewName找到对应的View对象,然后使用该view对象的render方法将本身的内容写到response中。
然后就看下,当我们的处理函数返回一个viewName时,SpringMVC是如何渲染的。
继续看下processDispatchResult是如何来渲染的
这里可以看到整体的处理流程。首先判断view是不是一个视图的名称,若是需要找到这个视图名称对应的View对象,然后便是调用view对象的render方法,渲染到response中。
由于我们的处理函数经常仅仅是返回一个view名称,所以我们重点要看看它是如何根据视图名称来找到对应的View对象的,即resolveViewName方法内容。其实上文已经说明了View接口和ViewResolver 接口,ViewResolver 接口就是根据view名称来找到对应的View对象的,所以看下面就会很清晰明白
这里就是对DispatcherServlet的private List<ViewResolver> viewResolvers属性进行遍历找到一个能够获取View对象的ViewResolver,并返回这个view对象。
至此整个流程便走通了,接下来就是要看看有哪些ViewResolver以及它们的注册来源是什么?
常用的ViewResolver有:FreeMarkerViewResolver、InternalResourceViewResolver、VelocityViewResolver等。
接下来就是如何来注册这些ViewResolver:
还是在DispatcherServlet的初始化策略中,调用了initViewResolvers,如下:
这和HandleMapping和HandlerAdapter的初始化过程基本类似。this.detectAllViewResolvers是DispatcherServlet的一个boolean属性,可以在web.xml文件中修改这个值,默认是true。
当detectAllViewResolvers为true,意味着就会获取从xml文件中解析出来的ViewResolver。如果为false,则直接去找bean name为"viewResolver"并且是ViewResolver类型的作为DispatcherServlet的ViewResolver。
当上述两种情况都没有找到,则会启用默认的ViewResolver,在this.viewResolvers = getDefaultStrategies(context, ViewResolver.class)中,这个过程已经多次说过,可以见本系列第一篇HandleMapping的来源。它就是依据DispatcherServlet.properties文件中所配置的ViewResolver,如下:
也就是默认采用的是InternalResourceViewResolver。
再说说在xml文件中配置ViewResolver的情况,如下:
这里是以FreeMarkerViewResolver为例来说明,它的配置内容还是需要有待继续研究。这里只是粗略的说下它的继承情况。
FreeMarkerViewResolver继承AbstractTemplateViewResolver继承UrlBasedViewResolver继承AbstractCachingViewResolver。
首先是抽象类AbstractCachingViewResolver:它加入了缓存功能,它有几个重要的属性。
属性一:cacheLimit 最大的缓存数量,默认为1024。
属性二:viewAccessCache 是ConcurrentHashMap类型的,适合高并发。
属性三:viewCreationCache是LinkedHashMap类型的
我们再来看下,由view名称来解析到view视图对象的具体过程:
对于Object cacheKey = getCacheKey(viewName, locale);默认为viewName + "_" + locale;
但是可以被子类覆盖,子类UrlBasedViewResolver覆盖了它,变成只有viewName。
先从viewAccessCache中看能否找到已缓存的view视图,若能找到则返回。若未找到则加上同步锁synchronized (this.viewCreationCache),进入这个方法之后,最关键的是仍需要进行一次判断view = this.viewCreationCache.get(cacheKey),看看是否已经创建过了,并不是viewAccessCache和viewCreationCache他们所缓存的内容不一样而是如果没有这个判断,则会有多线程问题。
如线程1和线程2同时要解析相同的view名称,他们都来到同步锁synchronized (this.viewCreationCache)之前,线程2先拿到锁,线程1等待,线程2创建好view视图后,加入viewCreationCache和viewAccessCache,并释放锁。此时线程1获得锁,进入同步锁synchronized (this.viewCreationCache)内部,若不进行判断,则线程1又会去创建一次view视图。所以view = this.viewCreationCache.get(cacheKey)并判断view是否为null这一步骤是十分有用的。
创建View视图的任务就交给了子类来实现。resolveViewName这个方法基本上就分析完了,应该还会想到,它的那个cacheLimit限制好像还没发挥出作用。
继续回看
viewCreationCache 的类型是LinkedHashMap,但是它复写了protected boolean removeEldestEntry(Map.Entry<Object, View> eldest)方法,当该方法返回true时,LinkedHashMap则会删除最老的key。在这里我们可以看到,当viewCreationCache 的所存的View数量达到cacheLimit时,就会删除最老的那个key和value,同时也会使viewAccessCache删除这个key和value。
viewAccessCache主要是用来高并发的访问,viewCreationCache 则是用来统计最老的key。他们所存储的view都是一样的。
View接口:
String getContentType(); /** * Render the view given the specified model. * <p>The first step will be preparing the request: In the JSP case, * this would mean setting model objects as request attributes. * The second step will be the actual rendering of the view, * for example including the JSP via a RequestDispatcher. * @param model Map with name Strings as keys and corresponding model * objects as values (Map can also be {@code null} in case of empty model) * @param request current HTTP request * @param response HTTP response we are building * @throws Exception if rendering failed */ //上面说的很清楚,对于jsp来说,第一步就是将model作为request的attributes;第二步才开始渲染view void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
再看下ViewResolver接口:
View resolveViewName(String viewName, Locale locale) throws Exception;
它是对给定的viewName找到对应的View对象,然后使用该view对象的render方法将本身的内容写到response中。
然后就看下,当我们的处理函数返回一个viewName时,SpringMVC是如何渲染的。
try { // Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); } finally { if (asyncManager.isConcurrentHandlingStarted()) { return; } } applyDefaultViewName(request, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } //这里是我们的关注重点,就是进行视图渲染的过程 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Error err) { triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err); }
继续看下processDispatchResult是如何来渲染的
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception { boolean errorView = false; if (exception != null) { if (exception instanceof ModelAndViewDefiningException) { logger.debug("ModelAndViewDefiningException encountered", exception); mv = ((ModelAndViewDefiningException) exception).getModelAndView(); } else { Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); mv = processHandlerException(request, response, handler, exception); errorView = (mv != null); } } // Did the handler return a view to render? //这里是我们关注的重点 if (mv != null && !mv.wasCleared()) { render(mv, request, 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"); } } if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { // Concurrent handling started during a forward return; } if (mappedHandler != null) { mappedHandler.triggerAfterCompletion(request, response, null); } }
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()) { // 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() + "'"); } try { view.render(mv.getModelInternal(), request, response); } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'", ex); } throw ex; } }
这里可以看到整体的处理流程。首先判断view是不是一个视图的名称,若是需要找到这个视图名称对应的View对象,然后便是调用view对象的render方法,渲染到response中。
由于我们的处理函数经常仅仅是返回一个view名称,所以我们重点要看看它是如何根据视图名称来找到对应的View对象的,即resolveViewName方法内容。其实上文已经说明了View接口和ViewResolver 接口,ViewResolver 接口就是根据view名称来找到对应的View对象的,所以看下面就会很清晰明白
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; }
这里就是对DispatcherServlet的private List<ViewResolver> viewResolvers属性进行遍历找到一个能够获取View对象的ViewResolver,并返回这个view对象。
至此整个流程便走通了,接下来就是要看看有哪些ViewResolver以及它们的注册来源是什么?
常用的ViewResolver有:FreeMarkerViewResolver、InternalResourceViewResolver、VelocityViewResolver等。
接下来就是如何来注册这些ViewResolver:
protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); initLocaleResolver(context); initThemeResolver(context); initHandlerMappings(context); initHandlerAdapters(context); initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); //我们关注的重点 initViewResolvers(context); initFlashMapManager(context); }
还是在DispatcherServlet的初始化策略中,调用了initViewResolvers,如下:
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"); } } }
这和HandleMapping和HandlerAdapter的初始化过程基本类似。this.detectAllViewResolvers是DispatcherServlet的一个boolean属性,可以在web.xml文件中修改这个值,默认是true。
/** Detect all ViewResolvers or just expect "viewResolver" bean? */ private boolean detectAllViewResolvers = true;
当detectAllViewResolvers为true,意味着就会获取从xml文件中解析出来的ViewResolver。如果为false,则直接去找bean name为"viewResolver"并且是ViewResolver类型的作为DispatcherServlet的ViewResolver。
当上述两种情况都没有找到,则会启用默认的ViewResolver,在this.viewResolvers = getDefaultStrategies(context, ViewResolver.class)中,这个过程已经多次说过,可以见本系列第一篇HandleMapping的来源。它就是依据DispatcherServlet.properties文件中所配置的ViewResolver,如下:
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
也就是默认采用的是InternalResourceViewResolver。
再说说在xml文件中配置ViewResolver的情况,如下:
<bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer"> <property name="templateLoaderPath" value="/WEB-INF/views" /> <property name="defaultEncoding" value="utf-8" /> <property name="freemarkerSettings"> <props> <prop key="locale">zh_CN</prop> </props> </property> </bean> <bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver"> <property name="suffix" value=".html" /> <property name="contentType" value="text/html;charset=utf-8" /> <property name="requestContextAttribute" value="request" /> <property name="exposeRequestAttributes" value="true" /> <property name="exposeSessionAttributes" value="true" /> </bean>
这里是以FreeMarkerViewResolver为例来说明,它的配置内容还是需要有待继续研究。这里只是粗略的说下它的继承情况。
FreeMarkerViewResolver继承AbstractTemplateViewResolver继承UrlBasedViewResolver继承AbstractCachingViewResolver。
首先是抽象类AbstractCachingViewResolver:它加入了缓存功能,它有几个重要的属性。
/** Default maximum number of entries for the view cache: 1024 */ public static final int DEFAULT_CACHE_LIMIT = 1024; /** The maximum number of entries in the cache */ private volatile int cacheLimit = DEFAULT_CACHE_LIMIT; /** Fast access cache for Views, returning already cached instances without a global lock */ private final Map<Object, View> viewAccessCache = new ConcurrentHashMap<Object, View>(DEFAULT_CACHE_LIMIT); /** Map from view key to View instance, synchronized for View creation */ @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; } } };
属性一:cacheLimit 最大的缓存数量,默认为1024。
属性二:viewAccessCache 是ConcurrentHashMap类型的,适合高并发。
属性三:viewCreationCache是LinkedHashMap类型的
我们再来看下,由view名称来解析到view视图对象的具体过程:
public View resolveViewName(String viewName, Locale locale) throws Exception { //这里进行了是否进行缓存的判断,即cacheLimit是否大于0 if (!isCache()) { //不进行缓存,始终每次都创建 return createView(viewName, locale); } else { //viewAccessCache viewCreationCache两者的key Object cacheKey = getCacheKey(viewName, locale); View view = this.viewAccessCache.get(cacheKey); if (view == null) { synchronized (this.viewCreationCache) { view = this.viewCreationCache.get(cacheKey); if (view == null) { // Ask the subclass to create the View object. view = createView(viewName, locale); if (view == null && this.cacheUnresolved) { view = UNRESOLVED_VIEW; } if (view != null) { this.viewAccessCache.put(cacheKey, view); this.viewCreationCache.put(cacheKey, view); if (logger.isTraceEnabled()) { logger.trace("Cached view [" + cacheKey + "]"); } } } } } return (view != UNRESOLVED_VIEW ? view : null); } }
对于Object cacheKey = getCacheKey(viewName, locale);默认为viewName + "_" + locale;
但是可以被子类覆盖,子类UrlBasedViewResolver覆盖了它,变成只有viewName。
先从viewAccessCache中看能否找到已缓存的view视图,若能找到则返回。若未找到则加上同步锁synchronized (this.viewCreationCache),进入这个方法之后,最关键的是仍需要进行一次判断view = this.viewCreationCache.get(cacheKey),看看是否已经创建过了,并不是viewAccessCache和viewCreationCache他们所缓存的内容不一样而是如果没有这个判断,则会有多线程问题。
如线程1和线程2同时要解析相同的view名称,他们都来到同步锁synchronized (this.viewCreationCache)之前,线程2先拿到锁,线程1等待,线程2创建好view视图后,加入viewCreationCache和viewAccessCache,并释放锁。此时线程1获得锁,进入同步锁synchronized (this.viewCreationCache)内部,若不进行判断,则线程1又会去创建一次view视图。所以view = this.viewCreationCache.get(cacheKey)并判断view是否为null这一步骤是十分有用的。
创建View视图的任务就交给了子类来实现。resolveViewName这个方法基本上就分析完了,应该还会想到,它的那个cacheLimit限制好像还没发挥出作用。
继续回看
private final Map<Object, View> viewAccessCache = new ConcurrentHashMap<Object, View>(DEFAULT_CACHE_LIMIT); 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; } } };
viewCreationCache 的类型是LinkedHashMap,但是它复写了protected boolean removeEldestEntry(Map.Entry<Object, View> eldest)方法,当该方法返回true时,LinkedHashMap则会删除最老的key。在这里我们可以看到,当viewCreationCache 的所存的View数量达到cacheLimit时,就会删除最老的那个key和value,同时也会使viewAccessCache删除这个key和value。
viewAccessCache主要是用来高并发的访问,viewCreationCache 则是用来统计最老的key。他们所存储的view都是一样的。
发表评论
-
SpringMVC源码总结(十一)mvc:interceptors拦截器介绍
2014-09-08 20:21 5764本文章针对mvc:interceptors标签进行介绍,它的注 ... -
SpringMVC源码总结(十)自定义HandlerMethodArgumentResolver
2014-09-04 07:45 7362上一篇文章介绍了HandlerMethodArgumentRe ... -
SpringMVC源码总结(九)HandlerMethodArgumentResolver介绍
2014-09-02 06:24 12386本文章主要介绍HandlerMethodArgumentRes ... -
SpringMVC源码总结(八)类型转换PropertyEditor的背后
2014-08-30 17:13 4831PropertyEditor是Spring最初 ... -
SpringMVC源码总结(七)mvc:annotation-driven中的HttpMessageConverter
2014-08-27 22:32 6564这一篇文章主要介绍下HttpMessageConverter整 ... -
SpringMVC源码总结(六)mvc:annotation-driven中的HandlerMethodReturnValueHandler
2014-08-26 06:21 6490经过了两篇的乱码说明,要重新回到mvc:annotation- ... -
SpringMVC源码总结(五)Tomcat的URIEncoding、useBodyEncodingForURI和CharacterEncodingFilter
2014-08-22 06:32 7314继续上一章节的乱码问题。上一篇文章仅仅说了设置Tomcat的U ... -
SpringMVC源码总结(四)由StringHttpMessageConverter引出的客户端服务器端之间的乱码过程分析
2014-08-20 22:49 3625继续上一篇文章遗留的乱码问题,引出从客户端数据到服务器端的乱码 ... -
SpringMVC源码总结(三)mvc:annotation-driven和mvc:message-converters简单介绍
2014-08-19 06:58 10119上一篇文章讲述了最简单的mvc:annotation-driv ... -
SpringMVC源码总结(二)mvc:annotation-driven以及@Controller和@RequestMapping的那些事
2014-08-16 22:47 8616上一篇文章让我们了解HandlerMapping和Handle ... -
SpringMVC源码总结(一)HandlerMapping和HandlerAdapter入门
2014-08-16 22:42 14674刚接触SpringMVC,对它的xml文件配置一直比较模模糊糊 ...
相关推荐
- `springmvc_hello` 可能是简单的 Hello World 示例,展示了 Spring MVC 的基本使用。 - `spring_user` 可能是一个用户管理模块,涉及到 Hibernate 的实体类、持久化操作以及 Spring MVC 的 Controller。 5. **...
ModelAndView 包含了视图名和模型数据,接着视图解析器(ViewResolver)将视图名转化为实际的视图,如 JSP、Thymeleaf 等。 Spring MVC 还涉及到模型数据绑定、数据验证和异常处理。例如,`@ModelAttribute` 注解...
在本压缩包 "springmvc源码测试代码" 中,我们可能找到了用于理解和学习 Spring MVC 源码的测试代码。这里我们将深入探讨 Spring MVC 的核心概念、工作流程以及如何通过测试代码来理解其实现。 首先,Spring MVC 的...
《看透SpringMVC源代码分析与实践》这...总的来说,深入学习SpringMVC源码能够提升我们的技术水平,使我们在面对复杂业务场景时更有信心。《看透SpringMVC源代码分析与实践》这本书无疑是实现这一目标的重要参考资料。
一个典型的SpringMVC流程包括:DispatcherServlet接收请求,通过HandlerMapping找到对应的Controller,Controller处理业务逻辑后返回ModelAndView,ViewResolver解析视图,最后将结果展示给用户。在源码中,可以研究...
在源码中,你可以看到这些核心组件的实现细节,比如DispatcherServlet如何解析请求、HandlerMapping如何查找映射、ViewResolver如何解析视图。阅读源码有助于理解Spring MVC的工作机制,提高问题排查能力,也能启发...
本压缩包包含SpringMVC的源码,对于学习和理解SpringMVC的工作原理及其内部机制非常有价值。 1. **DispatcherServlet**:SpringMVC的核心组件,它作为前端控制器,负责接收HTTP请求,并根据请求信息(如URL、HTTP...
通过尚硅谷的SpringMVC源码分析,开发者可以更深入地了解SpringMVC的内部机制,如DispatcherServlet、HandlerAdapter、ModelAndView的实现细节,以及AOP、IOC容器在SpringMVC中的应用。源码学习有助于提升对框架原理...
在这个项目源码中,我们可以深入理解 Spring MVC 的工作原理以及如何在实际应用中使用它。 1. **Spring MVC 架构**: Spring MVC 包含了 DispatcherServlet、HandlerMapping、HandlerAdapter、ModelAndView、...
通过对DispatcherServlet、HandlerMapping、Controller、ViewResolver等关键组件的源码阅读,我们可以深入理解其内部机制,这对于优化性能、解决异常或扩展功能都十分有帮助。同时,了解Spring MVC的源码也有助于...
SpringMVC的核心组件包括DispatcherServlet、HandlerMapping、HandlerAdapter、ViewResolver和ModelAndView。 1. **DispatcherServlet**:这是所有请求的入口点,它负责接收HTTP请求并分发到合适的处理器。...
在"springmvc 练习源码带注释.zip"中,我们可以预期找到一个包含Spring MVC和MyBatis整合的示例项目。这个练习源码可能涵盖了以下关键知识点: 1. **Spring MVC 框架**: - **DispatcherServlet**:Spring MVC的...
### SpringMVC源码分析概览 #### 一、JavaWeb与Servlet基础知识 在深入了解SpringMVC源码之前,我们首先需要对JavaWeb的基础概念进行简单的回顾。JavaWeb的核心是Servlet,这是一种运行在服务器端的应用程序接口...
本视频课程“SpringMvc深入理解源码分析”旨在帮助开发者深入理解Spring MVC的工作原理和核心机制,从而更好地利用它来构建高效、可维护的Web应用。 在Spring MVC中,主要涉及以下几个核心概念: 1. **...
**SpringMVC 3.1 实例源码详解** SpringMVC是Spring框架的一个核心模块,专注于处理Web应用的请求和响应。在这个基于SpringMVC 3.1的实例中,我们将深入探讨其主要功能、架构和配置。Spring 3.1引入了一些重要的...
通过学习和理解Servlet和SpringMVC的源码,开发者可以更好地优化性能,解决潜在问题,以及根据需求定制自己的Web框架。SpringMVC的灵活性和丰富的功能使其成为现代Java Web开发的首选,而Servlet作为基础,提供了对...
SpringMVC是Spring框架的一部分,专门用于构建Web应用程序的模型-视图-控制器(MVC)架构。在本文中,我们将深入探讨SpringMVC的核心概念、工作原理以及如何在Eclipse环境中进行设置和运行。 首先,SpringMVC设计...