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>

猜你喜欢

转载自blog.csdn.net/qq_42013035/article/details/106496975