SpringMVC实现原理与SpringBootMVC实现原理

SpringMVC实现原理与SpringBootMVC实现原理

一、MVC

MVC是模型(Model)、视图(View)、控制器(Controller)的简写,是一种软件设计规范。是将业务逻辑、数据、显示分离的方法来组织代码。

MVC主要作用是降低了视图与业务逻辑间的双向耦合。

MVC不是一种设计模式,MVC是一种架构模式。当然不同的MVC存在差异。

  • Model(模型):数据模型,提供要展示的数据,因此包含数据和行为,可以认为是领域模型或JavaBean组件(包含数据和行为),不过现在一般都分离开来:Value Object(数据Dao) 和 服务层(行为Service)。也就是模型提供了模型数据查询和模型数据的状态更新等功能,包括数据和业务。

  • View(视图):负责进行模型的展示,一般就是我们见到的用户界面,客户想看到的东西。

  • Controller(控制器):接收用户请求,委托给模型进行处理(状态改变),处理完毕后把返回的模型数据返回给视图,由视图负责展示。也就是说控制器做了个调度员的工作。

二、SpringMVC实现原理

官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc

Spring MVC是Spring Framework的一部分,是基于Java实现MVC的轻量级Web框架。Spring的web框架围绕DispatcherServlet设计。DispatcherServlet的作用是将请求分发到不同的处理器。从Spring 2.5开始,使用Java 5或者以上版本的用户可以采用基于注解的controller声明方式。

Spring MVC框架像许多其他MVC框架一样, 以请求为驱动 , 围绕一个中心Servlet分派请求及提供其他功能,DispatcherServlet是一个实际的Servlet (它继承自HttpServlet 基类)。

SpringMVC底层实现流程

底层实现流程

1.DispatcherServlet表示前置控制器,是整个SpringMVC的控制中心。用户发出请求,DispatcherServlet接收请求并拦截请求。

我们假设请求的url为 : http://localhost:8080/SpringMVC/hello

如上url拆分成三部分:

  • http://localhost:8080 服务器域名
  • SpringMVC 部署在服务器上的web站点
  • hello 表示控制器

通过分析,如上url表示为:请求位于服务器localhost:8080上的SpringMVC站点的hello控制器。

2.HandlerMapping为处理器映射。DispatcherServlet调用HandlerMapping,HandlerMapping根据请求url查找Handler。

3.HandlerExecution表示具体的Handler,其主要作用是根据url查找控制器,如上url被查找控制器为:hello。

4.HandlerExecution将解析后的信息传递给DispatcherServlet,如解析控制器映射等。

5.HandlerAdapter表示处理器适配器,其按照特定的规则去执行Handler。

6.Handler让具体的Controller执行。

7.Controller将具体的执行信息返回给HandlerAdapter,如ModelAndView。

8.HandlerAdapter将视图逻辑名或模型传递给DispatcherServlet。

9.DispatcherServlet调用视图解析器(ViewResolver)来解析HandlerAdapter传递的逻辑视图名。

10视图解析器将解析的逻辑视图名传给DispatcherServlet。

11.DispatcherServlet根据视图解析器解析的视图结果,调用具体的视图。

源码简单解析MVC

在spring-webmvc的jar包下找到DispatcherServlet类,这是SpringMVC整个Web框架的核心和控制中心;


DispatcherServlet类

1.DispatcherServlet生成对象时,先对属性进行初始化的设置

public static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE = DispatcherServlet.class.getName() + ".CONTEXT";
public static final String LOCALE_RESOLVER_ATTRIBUTE = DispatcherServlet.class.getName() + ".LOCALE_RESOLVER";
public static final String THEME_RESOLVER_ATTRIBUTE = DispatcherServlet.class.getName() + ".THEME_RESOLVER";
public static final String THEME_SOURCE_ATTRIBUTE = DispatcherServlet.class.getName() + ".THEME_SOURCE";

request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, this.getThemeSource());

2.DispatcherServlet接收到用户发来的请求,根据请求的URL调用处理器映射器来得到对应的处理器

@Nullable
    protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        if (this.handlerMappings != null) {
            Iterator var2 = this.handlerMappings.iterator();
            while(var2.hasNext()) {
                HandlerMapping mapping = (HandlerMapping)var2.next();
                HandlerExecutionChain handler = mapping.getHandler(request);//请求对应的处理器
                if (handler != null) {
                    return handler;
                }
            }
        }

        return null;
    }

3.处理器解析请求以后,DispatcherServlet根据解析到的信息调用处理器适配器来得到控制器

protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
        if (this.handlerAdapters != null) {
            Iterator var2 = this.handlerAdapters.iterator();

            while(var2.hasNext()) {
                HandlerAdapter adapter = (HandlerAdapter)var2.next();
                if (adapter.supports(handler)) {
                    return adapter;
                }
            }
        }
        throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
    }

4.找到控制器后,由控制器Controller执行真正的处理请求的行为(根据请求调用service层对象访问数据库、得到数据、实现需求等)
并具体的执行信息(ModelAndView)返回给HandlerAdapter,HandlerAdapter将视图逻辑名或模型传递给DispatcherServlet。

5.DispatcherServlet调用视图解析器(ViewResolver)来解析HandlerAdapter传递的逻辑视图名。

 @Nullable
    protected View resolveViewName(String viewName, @Nullable Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception {
        if (this.viewResolvers != null) {
            Iterator var5 = this.viewResolvers.iterator();

            while(var5.hasNext()) {
                ViewResolver viewResolver = (ViewResolver)var5.next();
                View view = viewResolver.resolveViewName(viewName, locale);
                if (view != null) {
                    return view;
                }
            }
        }
        return null;
    }

6.DispatcherServlet根据视图解析器解析的视图结果,调用具体的视图,返回到用户。

view.render(mv.getModelInternal(), request, response);

三、SpringBootMVC实现原理

与其他Spring开发功能相似,SpringBoot为MVC的框架也实现了自动默认配置,这些自动配置由WebMvcAutoConfiguration类来实现。
在这里主要对SpringBootMVC实现视图解析器、静态资源处理、格式转换器自动配置原理进行简单记录。

视图解析器

SpringBoot中的视图解析功能由ContentNegotiatingViewResolver类来实现,由该类的实例对象来根据方法的返回值取得视图对象,再进行渲染。WebMvcAutoConfiguration类中有得到视图解析器viewResolver对象的方法

@Bean
@ConditionalOnBean({ViewResolver.class})
@ConditionalOnMissingBean(name = {"viewResolver"},value = {ContentNegotiatingViewResolver.class})
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
    ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
    resolver.setContentNegotiationManager((ContentNegotiationManager)beanFactory.getBean(ContentNegotiationManager.class));
        // ContentNegotiatingViewResolver使用所有其他视图解析器来定位视图,具有较高的优先级
    resolver.setOrder(-2147483648);
    return resolver;
}

接下来可以探究ContentNegotiatingViewResolver类中解析视图(获取视图)操作的源码,类中定义了一个方法如下

@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
    RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
    List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest());
    if (requestedMediaTypes != null) {//获取候选的视图对象
        List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes);
        View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs);//选择一个最合适的试图对象bestView然后返回这个对象
        if (bestView != null) {
            return bestView;
        }
    }

如何获取候选的对象,点进getCandidateViews的源码

private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes) throws Exception {
    List<View> candidateViews = new ArrayList();
    if (this.viewResolvers != null) {
        Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
        Iterator var5 = this.viewResolvers.iterator();

        while(var5.hasNext()) {
            ViewResolver viewResolver = (ViewResolver)var5.next();
            View view = viewResolver.resolveViewName(viewName, locale);
            if (view != null) {
                candidateViews.add(view);
            }

            Iterator var8 = requestedMediaTypes.iterator();

            while(var8.hasNext()) {
                MediaType requestedMediaType = (MediaType)var8.next();
                List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
                Iterator var11 = extensions.iterator();

                while(var11.hasNext()) {
                    String extension = (String)var11.next();
                    String viewNameWithExtension = viewName + '.' + extension;
                    view = viewResolver.resolveViewName(viewNameWithExtension, locale);
                    if (view != null) {
                        candidateViews.add(view);
                    }
                }
            }
        }
    }

可得,该方法getCandidateViews是将所有的视图都放到List中,然后把所有的视图解析器拿来进行while循环逐个解析

如何获得最合适的视图,点进getBestView方法的源码

private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
        Iterator var4 = candidateViews.iterator();

        while(var4.hasNext()) {
            View candidateView = (View)var4.next();
            if (candidateView instanceof SmartView) {
                SmartView smartView = (SmartView)candidateView;
                if (smartView.isRedirectView()) {
                    return candidateView;
                }
            }
        }

        var4 = requestedMediaTypes.iterator();

        while(var4.hasNext()) {
            MediaType mediaType = (MediaType)var4.next();
            Iterator var10 = candidateViews.iterator();

            while(var10.hasNext()) {
                View candidateView = (View)var10.next();
                if (StringUtils.hasText(candidateView.getContentType())) {
                    MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
                    if (mediaType.isCompatibleWith(candidateContentType)) {
                        if (this.logger.isDebugEnabled()) {
                            this.logger.debug("Selected '" + mediaType + "' given " + requestedMediaTypes);
                        }

                        attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, 0);
                        return candidateView;
                    }
                }
            }
        }

        return null;
    }
}

可得,也就是在候选的view中根据需求条件得到一个最合适的视图返回;
结论:ContentNegotiatingViewResolver这个视图解析器是组合了所有的视图解析器对象来实现视图解析功能;视图解析器的组合逻辑在ContentNegotiatingViewResolver类中的initServletContext方法中定义;

protected void initServletContext(ServletContext servletContext) {
        Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.obtainApplicationContext(), ViewResolver.class).values();
        ViewResolver viewResolver;
        if (this.viewResolvers == null) {
            this.viewResolvers = new ArrayList(matchingBeans.size());
            Iterator var3 = matchingBeans.iterator();

            while(var3.hasNext()) {
                viewResolver = (ViewResolver)var3.next();
                if (this != viewResolver) {
                    this.viewResolvers.add(viewResolver);
                }
            }
        } else {
            for(int i = 0; i < this.viewResolvers.size(); ++i) {
                viewResolver = (ViewResolver)this.viewResolvers.get(i);
                if (!matchingBeans.contains(viewResolver)) {
                    String name = viewResolver.getClass().getName() + i;
                    this.obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(viewResolver, name);
                }
            }
        }

        AnnotationAwareOrderComparator.sort(this.viewResolvers);
        this.cnmFactoryBean.setServletContext(servletContext);
    }

静态资源处理

静态资源:前端写好的固定页面

访问webjars中的静态资源

使用SpringBoot导入静态资源,需要使用webjars导入;当我们想要访问webjars中的静态资源时,WebMvcAutoConfigurationAdapter中的方法addResourceHandlers规定了访问该静态资源的规则:

public void addResourceHandlers(ResourceHandlerRegistry registry) {
            if (!this.resourceProperties.isAddMappings()) {
                logger.debug("Default resource handling disabled");
            } else {
                Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
                CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
                if (!registry.hasMappingForPattern("/webjars/**")) {
                    this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl).setUseLastModified(this.resourceProperties.getCache().isUseLastModified()));
                }

                String staticPathPattern = this.mvcProperties.getStaticPathPattern();
                if (!registry.hasMappingForPattern(staticPathPattern)) {
                    this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl).setUseLastModified(this.resourceProperties.getCache().isUseLastModified()));
                }

            }
        }

源码中指出:当我们访问所有路径为/webjars/**的静态资源时,都需要去classpath:/META-INF/resources/webjars/里找到对应的资源

访问自己编写的静态资源

当我们需要访问自己编写的静态资源时,访问的路径是什么?再看到上面addResourceHandlers方法中有一行代码:

String staticPathPattern = this.mvcProperties.getStaticPathPattern();

这里通过getStaticPathPattern方法设置了静态资源的路径;查看WebMvcProperties类中这个方法的源码;

public String getStaticPathPattern() {
        return this.staticPathPattern;
    }

查看staticPathPattern这个属性在构造器中的初始化:

this.staticPathPattern = "/**";

可得,该类中构造器对staticPathPattern(静态资源路径)的初始化为"/**",访问目录下的任何资源;
而这个属性的定义会找WebProperties这个类中内部类Resources的getStaticLocations()方法;

 public String[] getStaticLocations() {
            return this.staticLocations;
        }

Resources是WebProperties类中的一个静态内部类,其构造器对staticLocations的初始化值为一个常量;

public Resources() {
            this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS;

该常量属性在Resources类中的定义为

private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};

至此,我们得知了静态资源自动配置的默认访问路径;可知:这里声明了访问静态资源时,查找静态资源的路径目录,即上面定义的静态String[]数组里的内容

"classpath:/META-INF/resources/"

"classpath:/resources/"

"classpath:/static/"

"classpath:/public/"

因此,在这四个目录下存放的静态资源可以被识别和访问,我们可以在根目录resources下创建相应的文件夹来存放静态资源;
eg:访问http://localhost:8080/hello.js,就会在以上路径目录中查找相对应的文件

格式转换器

在WebMvcAutoConfiguration类中定义了格式转换的方法

@Bean
        public FormattingConversionService mvcConversionService() {
            Format format = this.mvcProperties.getFormat();
            WebConversionService conversionService = new WebConversionService((new DateTimeFormatters()).dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
            this.addFormatters(conversionService);
            return conversionService;
        }

getXxx()方法就规定了相应的格式;

以上;
需要注意的是,spring中导入的web-mvc的jar包不同版本源码可能会有所不同。SpringBoot版本不同,mvc配置类中的源码也会有所差别。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容