spring-mvc第一期:細說Spring MVC的配置(完全基於Java註解)

目錄

1.概述Spring MVC

1.1.DispatcherServlet

1.1.1.配置

1.1.2.源碼分析

1.2.HandlerMapping & HandlerAdapter & HandlerExecutionChain

1.3.ViewResolver

1.3.1.配置

1.3.2.源碼分析

1.4.Filter & Interceptors

1.4.1.配置

1.4.2.源碼分析

附錄:

pom.xml文件


下一期:讓Controller沒有祕密

1.概述Spring MVC

最近在研究Spring Framework Web MVC官方文檔裏的一些內容,在這做幾期筆記,記錄一下。

先不管那些細枝末節的配置,咋們先吧學習環境搭建一下,我用到的配置如下:

  • IDEA
  • Java
  • maven
  • spring-mybatis
  • spring-web
  • Thymleaf ,FreeMaker,JSP(作爲viewResolvers模塊的配置做多個說明)

代碼倉庫地址:https://gitee.com/qiu-qian/demo-world.git(spring-mvc模塊)

在官方文檔的一開始,這樣寫道:

Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning.

Spring MVC, as many other web frameworks, is designed around the front controller pattern where a central Servlet,the DispatcherServlet

可以看出 spring web框架是的底層是Servlet,並且有一個被稱爲前端控制器的Servlet,DispatcherServlet

然後我們從一張圖說起:

上圖可以說是一個請求從到達Servlet容器到響應到瀏覽器的大概過程,當然其中忽略了許多細節,下文將逐一分析

1.1.DispatcherServlet

1.1.1.配置

我們先看一張圖:

 可以看出這裏兩套Spring的上下文,ServletWebApplicationContext 管理着一些web層的bean而RootWebApplicationContext 則管理着一些基礎的bean,如數據訪問層,學過java EE的你一定對這些不陌生,和以往不同,5.2.6版本的官方推薦配置方式如下:

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    /**
     * 方法返回的帶有@Configuration註解的類將會用來配置ContextLoaderListener,
     * 創建的應用上下文中的bean。
     * 這些bean通常是驅動應用後端的中間層和數據層組件
     *
     * @return 結果
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[]{RootConfig.class};
    }

    /**
     * 方法返回的帶有@Configuration註解的類將會用來定
     * 義DispatcherServlet應用上下文中的bean
     *
     * @return 結果
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[]{MvcConfig.class};
    }

    /**
     * 它會將一個或多個路徑映射到DispatcherServlet上。在本例中,它映射的是“/”,這表示
     * 它會是應用的默認Servlet。它會處理進入應用的所有請求
     *
     * @return 結果
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

 AbstractAnnotationConfigDispatcherServletInitialize 這個類名很長,但功能也很全,與Servlet有關的配置基本都在這裏進行(DispatcherServelt也是一種Servlet),包括指定這兩個上下文配置的位置和DispatcherServlet的映射,接下來我們來分別配置這兩個上下文:

  • RootConfig (這裏我們先暫時不使用mybatis來操作數據庫)專注於web層,所以只用配置一個掃描路徑即可)
@Configuration
@ComponentScan(basePackages = {"com.swing.spring.mvc"})
public class RootConfig {
   
}
  • MvcConfig(這個類就是spirngMvc配置的中心啦,例如靜態資源路徑,試圖解析器,攔截器等)這裏先配置一個基本的單元
/**
 * @author swing
 */
@Configuration
@EnableWebMvc
@ComponentScan("com.swing.spring.mvc")
public class MvcConfig implements WebMvcConfigurer, ApplicationContextAware {
    private ApplicationContext applicationContext;

    /**
     * 註冊靜態資源的路徑
     *
     * @param registry 註冊
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //處理程序.資源路徑(webapp下)
        registry.addResourceHandler("/images/**").addResourceLocations("/images/");
        registry.addResourceHandler("/css/**").addResourceLocations("/css/");
        registry.addResourceHandler("/js/**").addResourceLocations("/js/");
    }


    /**
     * 啓用轉發到默認 Servlet的功能。
     */
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    /**
     * 獲取web上下文
     *
     * @param applicationContext 容器
     * @throws BeansException 異常
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

}

 這裏的 @EnableWebMvc 註解表示開啓SpringMVC 與XML配置中的<mvc:annotation-driven> 作用相同

回過頭來,我們再來總結一下AbstractAnnotationConfigDispatcherServletInitialize

1.1.2.源碼分析

從上圖不難看出,這個DispatcherServelt有點東西噢,現在先來研究一下它

先使用我已經完成的例子來調試一下,讀者可以跟着一起來,我們來看一看一個請求在到達Controller之前被DispatcherServlet如何處理過,如下:

簡單分析一下上面的幾個方法(從下往上依次由前到後)

  • FrameworkServlet.service(),由上面的繼承關係可得此方法中調用的 super.serviec(...)是來自HttpServlet,也正是在這裏,SpringMVC 通過 ServletAPI 接過來處理請求的接力棒

  • FrameworkServelt.doGet(),處理Get請求
  • FrameworkServlet.processRequest(),處理請求

  • DispatcherServlet.doService()

 這裏需要講解一下,在這個方法裏,爲request請求增加了四個屬性:依次爲(web應用上下文,語言環境解析器,主題解析器,主題源)爲啥要在請求上綁定這些屬性捏?官方文檔這樣解釋:

  •  DispatcherServlet.doDispatch() 準備工作完成啦,開始對請求做調度了(這個方法,可以說是web處理的中心了,下文的分析會經常回到此方法,建議多設置幾個調試斷點)

此方法註釋寫道,開始實際地調用(請求)處理器了,可見,前面說到的那些處理,可跟請求的內容無關,即不管你是來請求啥的,我先給你包裝一下再說,接下來我們就要來步入這第二個重要類了(也開始了第一張圖上的第二步驟)

 

1.2.HandlerMapping & HandlerAdapter & HandlerExecutionChain

你是否曾想過,在controller中使用 @RequestMapping("/user")  時,是誰來解析這個註解的呢? 沒錯這就是HandlerMapper 的實現類RequestMappingHandlerMapping 的功勞,我們可以來簡單討論一下這個類,它在web程序初始化時,作Bean在 WebMvcConfigurationSupport.requestMappingHandlerMapping()方法中注入,在WebApplicationContext 中的找到這個bean 並定位到它的 MappingRegistry屬性,這裏就是存放url 和 handler 映射關係的地方,判斷一個請求改由哪個handler去執行當然也得靠它啦,如下:

HandlerMapping 還有一個常用的實現類SimpleUrlHandlerMapping  這個類維護一個  Map<String, Object> urlMap ,給出調試時的值參考

 你應該很容易明白這是啥了吧 ,而這兩個實現類,在DispatcherServlet 初始化時候就被放在 List<HandlerMapping> handlerMappings 中維護,方便使用。

簡單的解了HandlerMapping,我們繼續回到請求流程來,各種映射關係有了,可一個請求又是如何通過這些映射關係跳轉到對應的handler然後執行呢?這裏就得靠 HandlerAdapter 類了,執行器適配器,顧名思義,就是作爲 request 和 handler 之間的中介,官方文檔解釋的很不錯:

Help the DispatcherServlet to invoke a handler mapped to a request, regardless of how the handler is actually invoked. For example, invoking an annotated controller requires resolving annotations. The main purpose of a HandlerAdapter is to shield the DispatcherServlet from such details

在上文的 DispatcherServlet.doDispatch() 方法中,先看這樣一代碼片段:

HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
ModelAndView mv = null;

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

分析一下上面的代碼:首先通過getHandler(Rquest)方法,從請求中獲取handler(對應圖一的第二步,只需要簡單的遍歷handlerMappings 即可找到對應的handler),並將它放入HandlerExecutionChain,然後執行HandlerAdapter.handle(...)方,便返回了ModelAdnView(即圖一中的第四步結果)再深層次的實現原理不再分析,也沒有實質性的意義,

這裏還需要注意一個類:HandlerExecutionChain 由下文的官方說明可確定這條鏈由兩部分組成:handler+interceptors

Handler execution chain, consisting of handler object and any handler interceptors.Returned by HandlerMapping's {@link HandlerMapping#getHandler} method.

HandlerAdapter.handle(...)中的handler便是通過HandlerExecutionChain.getHandler()獲得。

值得說一下的是,這裏的handler在定義時聲明爲Objcet類型,可見它有多個表現的類,比如常見的有如下兩個:

  • HandlerMethod,當我們去請求Controller中的Handler方法時,handler的實現類即爲此類
  • ResourceHttpRequestHandler 當我們去訪問靜態資源的時候(例如image,js,css等,上文WebConfig中的addResourceHandlers配置)其實也需要handler(即此類)處理,此時mv返回的結果就是null了,並直接將靜態文件予以呈現,

1.3.ViewResolver

1.3.1.配置

上文我們已經獲得了Model和ViewName,這也是MVC中的MV,Model是處理後的數據(通常由DTO組成,然後在視圖層將這些數據渲染呈現出來),ViewName是我們的視圖名,但只有視圖名還不夠,程序還是無法定位到我們具體的視圖位置,所有我們這裏需要一個視圖解析器(viewResolver),不同模板引擎的視圖後綴和解析策略都不盡相同,因此viewResolver的配置也不太相同,(當然,如果你使用的是前後端開發模式,例如當下比較流行的SpringMVC+Vue,那麼便不需要ViewResolver,handler將直接放回JSON格式的數據給Node.js渲染)

好在SpringMVC內置了許多主流的模板引擎支持,常用如下:

  • JSP and JSTL
  • FreeMarker
  • Groovy Markup
  • Script Views

本文我們來做三個例子的,分別使用內置的JSP和FreeMarker,還有第三方的 Thymeleaf,其他的大家都舉一反三啦:(注:以下配置皆在MvcConfig中)

  • JSP
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
    registry.jsp("/WEB-INF/templates/jsp", ".jsp");
}
  • FreeMarker
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freeMarker();
    }

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer config = new FreeMarkerConfigurer();
        config.setTemplateLoaderPath("/WEB-INF/templates/freemarker");
        return config;
    }
  • thymeleaf (由於它不是Spring內置,使用配置會略多,我參考thymeleaf官方文檔配置如下:(此處若碰到小bug,可參考我的另外一篇博文鏈接 )
/**
     * 視圖解析器
     *
     * @return 視圖解析器
     */
    @Bean
    public ThymeleafViewResolver viewResolver() {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
        viewResolver.setTemplateEngine(templateEngine());
        //解決中文亂碼
        viewResolver.setCharacterEncoding("UTF-8");
        //解析器的加載順序(數字越大,越後執行)
        viewResolver.setOrder(1);
        return viewResolver;
    }

    /**
     * 模板引擎
     *
     * @return 模板引擎
     */
    @Bean
    public SpringTemplateEngine templateEngine() {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver());
        templateEngine.setEnableSpringELCompiler(true);
        return templateEngine;
    }

    /**
     * 模板解析器
     *
     * @return 模板解析器
     */
    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setApplicationContext(this.applicationContext);
        templateResolver.setPrefix("/WEB-INF/templates/thymeleaf/");
        templateResolver.setSuffix(".html");
        templateResolver.setTemplateMode(TemplateMode.HTML);
        templateResolver.setCacheable(true);
        return templateResolver;
    }

當然,如果你有特別的需求,SpringMVC其實也是可以支持同時配置多個視圖解析器,像下面這樣(注意要使用order屬性指定解析順序):

/**
     * 視圖解析器
     *
     * @return 視圖解析器
     */
    @Bean
    public ThymeleafViewResolver viewResolver() {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
        viewResolver.setTemplateEngine(templateEngine());
        //解決中文亂碼
        viewResolver.setCharacterEncoding("UTF-8");
        //解析器的加載順序(數字越大,越後執行)
        viewResolver.setOrder(1);
        return viewResolver;
    }

    @Bean
    public ViewResolver viewResolverJsp() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/templates/jsp/");
        resolver.setSuffix(".jsp");
        resolver.setOrder(2);
        return resolver;
    }

這裏再插播一個小插曲,如果有這樣的需求:例如我只想請求一個頁面,例如一成不變的404 500頁面,很明顯我們不需要走handler,直接呈現這個頁面就可以了,那麼我們可以使用如下配置:

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/404").setViewName("404");
    }

1.3.2.源碼分析

視圖解析的和渲染的過程是在 doDispatch() 中的這處:

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

 

 

1.4.Filter & Interceptors

爲啥將這個放到最後來講捏?原因是Interceptor中的幾個方法的執行時間是穿插在上述的記過過程中的。

1.4.1.配置

簡單配置一個 filter 和 Interceptors, 如下:

/**
 * @author swing
 * 繼承一個Spring已經給我們提供的過濾器,加點表示信息
 */
@Slf4j
public class MyCharacterEncodingFilter extends CharacterEncodingFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.info("我是一個過濾,我來確保你沒有亂碼!");
        super.doFilterInternal(request, response, filterChain);
    }
}
/**
 * @author swing
 * 這裏爲了實驗,來定義一個無聊的攔截器
 */
@Slf4j
public class BoringInterceptors implements HandlerInterceptor {
    /**
     * 在實際的handler執行之前處理
     *
     * @return 這個返回值決定一整條攔截鏈是否可以執行下去,如果返回false,則此攔截器後的攔截器都不再執行
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        StringBuffer url = request.getRequestURL();
        log.info("我攔截到了:" + url);
        return true;
    }

    /**
     * 在handler執行後處理
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("我是postHandler");
    }

    /**
     * 在請求結束後處理
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("我是afterCompletion");
    }
}

 Filter是原生Servelt中的東西,所以自然要在上文中的WebAppInitializer中配置,如下:

/**
     * 給servlet容器加個過濾器
     *
     * @return 過濾器數組
     */
    @Override
    protected Filter[] getServletFilters() {
        return new Filter[]{
                new MyCharacterEncodingFilter()
        };
    }

而Interceptor是SpringMVC中的,故得在WebConfig中配置

 /**
     * 添加攔截器
     *
     * @param registry 攔截器註冊中心
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new BoringInterceptors()).addPathPatterns("/**").excludePathPatterns("/post/**").order(1);
    }

1.4.2.源碼分析

這兩個小夥伴大家並不陌生,那麼他們的運行時機又是怎樣的呢,接下來我們調試簡單分析一下:

上文提到,spring mvc其實是從servlet 容器中拿到的請求,而我們又知道,過濾器是在請求被servlet處理前執行,所以Filter的執行,當然是在請求還沒到達spring前就執行了,再來看看Interceptor.preHandle方法,在我們之前分析的HandlerAdapter.handle方法執行前就被執行,具體來說是這裏:

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

這個方法相信簡單分析一下便能理解原理,遍歷被存在HandlerExecutionChain中的 HandlerInterceptor[] interceptors 在這裏被依次執行preHandle方法,至於Interceptor的PostHandle 和 afterCompletion 的執行時機,在doDispatch()中的這個位置,原理同上,不再贅述:

mappedHandler.applyPostHandle(processedRequest, response, mv);

if (mappedHandler != null) {
  mappedHandler.triggerAfterCompletion(request, response, null);
}

好啦!這一節就講這些了,建議回過頭再去對照的圖一和 doDispatch 這個方法消化一下吧,最好clone一下代碼對照運行下

祝您好運!!!

歡迎點贊評論指出毛病,謝謝!

 

 

附錄:

pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.swing</groupId>
    <artifactId>spring-mvc</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!--日誌 BEGIN-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        <!--日誌 END-->

        <!--工具 BEGIN-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>
        <!--工具 END-->

        <!--模板引擎 BEGIN-->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
            <version>3.0.11.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.30</version>
        </dependency>
        <!--模板引擎 END-->

        <!--Spring BEGIN-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.1.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>
        <!--Spring DEN-->

        <!--持久層 BEGIN-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.6.RELEASE</version>
            <exclusions>
                <exclusion>
                    <artifactId>spring-core</artifactId>
                    <groupId>org.springframework</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>spring-beans</artifactId>
                    <groupId>org.springframework</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.21</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
        </dependency>
        <!--持久層 END-->
    </dependencies>


    <build>
        <finalName>spring-mvc</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <encoding>utf-8</encoding>
                    <target>1.8</target>
                    <source>1.8</source>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章