初识Spring MVC

SpringMVC 起步

SpringMVC基于模型-视图-通知器(Model-View-Controller,MVC)模式实现的,它能够帮助我们构建像Spring框架那样灵活和松耦合的Web应用程序。

SpringMVC 运行流程:

idea一直再Updating indexes_ide

流程说明:
  1. 用户发送请求到前端控制器(DispatchServlet),该控制器会过滤出哪些请求可以访问servlet、哪些不能访问。就是URL-Pattern的作用,并且会加载SpringMVC.xml配置文件。
  2. 前端控制器会找到处理器映射器(HandlerMapping),通过HandlerMapping完成URL到Controller映射的组件,简单来说,就是将在SpringMVC.xml中配置的或者注解的URL与对应的处理类找到并进行存储,用Map这样的方式来存储。
  3. HandlerMapping有了映射关系,并且找到了URL对应的处理器,HandlerMapping就会将其处理器(Handler)返回,在放回前,会加上很多拦截器。
  4. DispatchServlet拿到Handler之后,找到HandlerAdapter(处理器适配器),通过它来访问处理器,并执行处理器。
  5. 执行处理器。
  6. 处理器会返回一个ModelAndView对象到HandlerAdapter。
  7. 通过HandlerAdapter将ModelAndView对象返回到前端控制器DispatchServlet。
  8. 前端控制器请求视图解析器(ViewResolver)去进行视图解析,根据逻辑视图名解析成真真的视图(JSP),其实就是将ModelAndView对象中存放视图的名称进行查找,找到对应的页面形成视图对象。
  9. 返回视图对象到前端控制器。
  10. 视图渲染,就是将ModelAndView对象中的数据放到Request域中,用来让页面加载数据的。
  11. 通过第8步,通过名称找到了对应的页面,通过第10步,request域中有了所需要的数据,那么就可以进行视图渲染了。最后将其返回即可。
搭建SpringMVC
java配置方式:

配置类:

public class SpittrWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer  {
    @Override
    //配置Servlet的映射
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
    @Override
    //返回的配置类用来配置ContextLoaderListener创建的spring上下文的bean主要加载后端的中间层和数据层 即原来的web.xml
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{RootConfig.class};
    }
    @Override
    //返回的配置类用来配置DispatcherServlet创建的应用上下文的bean主要加载web层 即原来的spring-mvc.xml
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }
}

AbstractAnnotationConfigDispatcherServletInitializer会自动的配置DispatcherServlet和Spring应用上下文(ContextLoaderListener)

Servlet 3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果能发现的话,就会用它来配置Servlet容器。

Spring提供了这个接口的实现,名为SpringServletContainerInitializer,这个类反过来又会查找实WebApplicationInitializer的类并将配置的任务交给它们来完成。

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {}

Spring 3.2引入了一个WebApplicationInitializer基础实现,也就是AbstractAnnotationConfigDispatcherServletInitializer因为我们的SpittrWebAppInitializer扩展了AbstractAnnotationConfigDispatcherServletInitializer(同时也就实现了WebApplicationInitializer)因此当部署到Servlet 3.0容器中的时候,容器会自动发现它,并用它来配置Servlet上下文。

SpittrWebAppInitializer重写了三个方法:

@Override
    //配置Servlet的映射
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
    @Override
    //返回的配置类用来配置ContextLoaderListener创建的spring上下文的bean主要加载后端的中间层和数据层 即原来的web.xml
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{RootConfig.class};
    }
    @Override
    //返回的配置类用来配置DispatcherServlet创建的应用上下文的bean主要加载web层 即原来的spring-mvc.xml
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }

注意:java配置方式只能部署在Servlet 3.0的服务器中才能正常工作,如Tomcat 7或更高版本。

WebApplicationInitializer源码文档:

The code-based approach with {@code WebApplicationInitializer}
 * Here is the equivalent {@code DispatcherServlet} registration logic,
 * {@code WebApplicationInitializer}-style:
 译:
    通过使用WebApplicationInitializer的基于代码的方式,其与xml是一个相同的注册逻辑。WebApplicationInitializer代码方式:
 
 * public class MyWebAppInitializer implements WebApplicationInitializer {
 *    @Override
 *    public void onStartup(ServletContext container) {
 *      XmlWebApplicationContext appContext = new XmlWebApplicationContext();
 *      appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
 *
 *      ServletRegistration.Dynamic dispatcher =
 *        container.addServlet("dispatcher", new    
                        DispatcherServlet(appContext));
 *      dispatcher.setLoadOnStartup(1);
 *      dispatcher.addMapping("/");
 *    }
 * }
 *
 * As an alternative to the above, you can also extend from {@link
org.springframework.web.servlet.support.AbstractDispatcherServletInitializer}.

译:你也可以通过集成自AbstractDispatcherServletInitializer来实现,以上二者任选其一


 * As you can see, thanks to Servlet 3.0's new {@link ServletContext#addServlet} method we're actually registering an instance of the {@code DispatcherServlet}, and this means that the {@code DispatcherServlet} can now be treated like any other object receiving constructor injection of its application context in this case.
 
译:如你所见,正是由于servlet3.0 的新方法(ServletContext#addServlet)出现,我们可以通过它注册一个DispatchServlet的实例了。这也意味着DispatchServlet可以像其他对象那
样,通过构造注入的方式接受一个Application Context

This style is both simpler and more concise. There is no concern for dealing with init-params, etc, just normal JavaBean-style properties and constructor arguments. You are free to create and work with your Spring application contexts as necessary before injecting them into the {@code DispatcherServlet}.

译:这种方式既简单又简洁。我们不需要如何去处理初始化参数,等,仅仅需要处理像普通JavaBean那样的属性和构造方法参数。在你必须将他们注入到DispatchServlet中之前,你可以很自由的构建和处理你的spring应用程序。

 Most major Spring Web components have been updated to support this style of registration.  You'll find that {@code DispatcherServlet}, {@code FrameworkServlet},{@code ContextLoaderListener} and {@code DelegatingFilterProxy} all now support constructor arguments. Even if a component (e.g. non-Spring, other third party) has not been specifically updated for use within {@code WebApplicationInitializers}, they still
 may be used in any case. The Servlet 3.0 {@code ServletContext} API allows for setting init-params, context-params, etc programmatically.
 
 译:大部分Spring web 组件都进行了更新,以至于能够支持这个注册方式,你可以发现像 DispatcherServlet、FrameworkServlet、ContextLoaderListener和DelegatingFilterProxy ,现在全部都支持构造参数注入。甚至是一些非spring,其他第三方组织的组件在WebApplicationInitializers中使用的没有支持的也会更新,以至于一直可以使用。Servlet3.0 API 也可以通过编程的方式 去设置 初始化参数、容器参数等
 

A 100% code-based approach to configuration In the example above, 
{@code WEB-INF/web.xml} was successfully replaced with code in
 * the form of a {@code WebApplicationInitializer}, but the actual
 * {@code dispatcher-config.xml} Spring configuration remained XML-based.
 * {@code WebApplicationInitializer} is a perfect fit for use with Spring's code-based
 * {@code @Configuration} classes. 
  See @{@link org.springframework.context.annotation.Configuration Configuration} Javadoc for complete details, but the following example demonstrates refactoring to use Spring's {@link org.springframework.web.context.support.AnnotationConfigWebApplicationContext AnnotationConfigWebApplicationContext} in lieu of {@code XmlWebApplicationContext}, and user-defined {@code @Configuration} classes {@code AppConfig} and {@code DispatcherConfig} instead of Spring XML files.
 
 译 :上面是一个完全由代码的配置方式的案例,web.xml文件已经被一个WebApplicationInitializer 成功取代。当然,实际上 Spring的配置任然保持着基于xml方式,WebApplicationInitializer 是一个更加适合 Spring代码的 配置类。可以查看Configuration的文档,了解完整的详情。但是下面这个例子是使用Spring的 AnnotationConfigWebApplicationContext 来代替XmlWebApplicationContext进行重构的。并且用户通过@Configuration,AppConfig,DispatcherConfig来代替xml配置文件。
 

 
 This example also goes a bit beyond those above to demonstrate typical configuration of the 'root' application context and registration of the {@code ContextLoaderListener}:
译:这个例子比上面的例子更进一步,使用root application context 来重构的典型配置,并且注册ContextLoaderListener
 
 * public class MyWebAppInitializer implements WebApplicationInitializer {
 *    @Override
 *    public void onStartup(ServletContext container) {
 *      // Create the 'root' Spring application context
        创建root Spring的应用上下文
 *      AnnotationConfigWebApplicationContext rootContext =
 *        new AnnotationConfigWebApplicationContext();
 *      rootContext.register(AppConfig.class);
        
 *      // Manage the lifecycle of the root application context
        管理root 应用上下文的生命周期
 *      container.addListener(new ContextLoaderListener(rootContext));

 *      // Create the dispatcher servlet's Spring application context
        // 创建了Servlet的前端控制器的Spring应用上下文
 *      AnnotationConfigWebApplicationContext dispatcherContext =
 *        new AnnotationConfigWebApplicationContext();
 *      dispatcherContext.register(DispatcherConfig.class);

 *      // Register and map the dispatcher servlet
        //注册并且映射前端控制器
 *      ServletRegistration.Dynamic dispatcher =
 *        container.addServlet("dispatcher", new 
                    DispatcherServlet(dispatcherContext));
 *      dispatcher.setLoadOnStartup(1);
 *      dispatcher.addMapping("/");
 *    }
 * }
 
 
 
 *
 * As an alternative to the above, you can also extend from {@link
 org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer}.
 你也可以通过集成自AbstractAnnotationConfigDispatcherServletInitializer,以上二者任选其一。
 
Remember that {@code WebApplicationInitializer} implementations are  
  detected automatically -- so you are free to package them within your application as you see fit.
  
记住,WebApplicationInitializer 的实现类 都是自动识别的。所以,你可以自由的将他们打包到你认为合适的应用程序中。
  

Ordering {@code WebApplicationInitializer} execution
 {@code WebApplicationInitializer} implementations may optionally be annotated at the
 * class level with Spring's @{@link org.springframework.core.annotation.Order Order}
 * annotation or may implement Spring's {@link org.springframework.core.Ordered Ordered}
 * interface. If so, the initializers will be ordered prior to invocation. This provides
 * a mechanism for users to ensure the order in which servlet container initialization
 * occurs. Use of this feature is expected to be rare, as typical applications will likely
 * centralize all container initialization within a single {@code WebApplicationInitializer}.
 
Caveats :说明、警告

web.xml versioning 
web.xml版本

{@code WEB-INF/web.xml} and {@code WebApplicationInitializer} use are not mutually exclusive; for example, web.xml can register one servlet, and a {@code WebApplicationInitializer} can register another. An initializer can even modify registrations performed in {@code web.xml} through methods such as {@link ServletContext#getServletRegistration(String)}. 

译:web.xml与WebApplicationInitializer不会相互排斥,例如,web.xml可注册一个Servlet,而WebApplicationInitializer可以注册另一个,后者甚至可以修改在xml中注册的。


However, if {@code WEB-INF/web.xml} is present in the application, its {@code version} attribute must be set to "3.0" or greater, otherwise {@code ServletContainerInitializer} bootstrapping will be ignored by the servlet container

译:然而,如果web.xml存在于应用中,那么它的版本必须在3.0或以上,否则ServletContainerInitializer 自己化启动将会被Servlet容器忽略。
 

 * <h3>Mapping to '/' under Tomcat</h3>、
 在tomcat下映射/
 * <p>Apache Tomcat maps its internal {@code DefaultServlet} to "/", and on Tomcat versions
<= 7.0.14, this servlet mapping cannot be overridden programmatically 
 7.0.15 fixes this issue. Overriding the "/" servlet mapping has also been tested successfully under GlassFish 3.1
译:在tomcat7.0.14及以下,tomcat使用其自身的DefaultServlet去映射/,这个Servlet映射将不能被编程方式重写在7.0.15版本。在GlassFish 3.1下, 成功测试出可以重写/这个Servlet映射器。
 */

public interface WebApplicationInitializer {

    /**
     * Configure the given {@link ServletContext} with any servlets, filters, listeners context-params and attributes necessary for initializing this web application. 
     为你这个web容器配置任何必要的 filter,listener,servlet,及其参数和属性
     
     See examples {@linkplain WebApplicationInitializer above}.
     * @param servletContext the {@code ServletContext} to initialize
     * @throws ServletException if any call against the given {@code ServletContext}
     * throws a {@code ServletException}
     */
    void onStartup(ServletContext servletContext) throws ServletException;

}

WebConfig:

@Configuration
@EnableWebMvc
//@EnableWebMvc 相当于之前在springmvc.xml文件中配置的<mvc:annotation-driven>
@ComponentScan("chendongdong.spring.test.bean.Test5.web") //启动组件扫描
public class WebConfig implements WebMvcConfigurer{
    /*配置JSP视图解析器*/
    @Bean
    public ViewResolver viewResolver(){
        InternalResourceViewResolver resolver =
                new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");//前缀
        resolver.setSuffix(".jsp");//后缀
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }

    /*配置静态资源的处理*/
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

RootConfig:

@Configuration
//扫描基础包,使用默认的过滤器。即全部都扫描。排除标有Controller注解的。
@ComponentScan(basePackages = "chendongdong.spring.test.bean.Test5", useDefaultFilters = true,excludeFilters = {
        @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = EnableWebMvc.class)
})
public class RootConfig {
}

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>chendongdong.spring.test</groupId>
  <artifactId>SpringTest</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>
  <name>SpringTest</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.1.8.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>5.1.8.RELEASE</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aop</artifactId>
      <version>5.1.8.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>5.1.8.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.1.8.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>5.1.8.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context-support</artifactId>
      <version>5.1.8.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>3.0-alpha-1</version>
    </dependency>
  </dependencies>
</project>

SpittrController

@Controller
public class SpittrController {

    @RequestMapping("/PrintSpttr")
    @ResponseBody
    public String PrintSpttr(){
        System.out.println("==== PrintSpttr ====");
        return "==== PrintSpttr ====";
    }
}

测试:

idea一直再Updating indexes_ide_02

xml配置方式:
接受请求参数
处理查询参数:

Controller:

@Controller
@RequestMapping({"/SpittrController","/Spittr","/"})
public class SpittrController {

    @RequestMapping("/PrintSpttr")
    @ResponseBody
    public String PrintSpttr(@RequestParam(value = "print",defaultValue = "陈菲菲") String print){
        System.out.println(print);
        return print;
    }
}

测试:

idea一直再Updating indexes_ide_03

解决方案:

org.springframework.http.converter.StringHttpMessageConverter. 打开源码可以发现:

默认编码已经被设定为了 Charset.forName(“ISO-8859-1”);

public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {
        // 省略 ....
        public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1");
        private final List<Charset> availableCharsets;
        private boolean writeAcceptCharset = true;
        
        public StringHttpMessageConverter(Charset defaultCharset) {
            super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL);
            this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values());
        }
        // 省略 ....
    }

找到配置SpringMVC的类 WebMvcConfigurerAdapter

public interface WebMvcConfigurer {
    /**
     * Configure the {@link HttpMessageConverter}s to use for reading or writing
     * to the body of the request or response. If no converters are added, a
     * default list of converters is registered.
     * <p><strong>Note</strong> that adding converters to the list, turns off
     * default converter registration. To simply add a converter without impacting
     * default registration, consider using the method
     * {@link #extendMessageConverters(java.util.List)} instead.
     * @param converters initially an empty list of converters
     */
    void configureMessageConverters(List<HttpMessageConverter<?>> converters);

    /**
     * A hook for extending or modifying the list of converters after it has been
     * configured. This may be useful for example to allow default converters to
     * be registered and then insert a custom converter through this method.
     * @param converters the list of configured converters to extend.
     * @since 4.1.3
     */
    void extendMessageConverters(List<HttpMessageConverter<?>> converters);
}

configureMessageConverters,extendMessageConverters区别:前者覆盖之前默认的编码,后者扩展编码。所以我们只需要覆盖默认的编码就好了。

@Configuration
@EnableWebMvc
//@EnableWebMvc 相当于之前在springmvc.xml文件中配置的<mvc:annotation-driven>
@ComponentScan("chendongdong.spring.test.bean.Test5.web") //启动组件扫描
public class WebConfig implements WebMvcConfigurer{
    /*全局修改输出为UTF-8编码*/
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(Charset.forName("UTF-8"));
        stringHttpMessageConverter.setWriteAcceptCharset(false);
        converters.add(stringHttpMessageConverter);
    }
}

xml配置方式:

<!-- utf-8编码 -->
    <mvc:annotation-driven>
        <mvc:message-converters register-defaults="true">
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                <constructor-arg value="UTF-8" />
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

配置后再次测试:

idea一直再Updating indexes_spring_04

处理通过路径参数:
@RequestMapping("/Print/{print}")
    @ResponseBody
    public String Print(@PathVariable String print){
        System.out.println(print);
        return print;
    }

测试:

idea一直再Updating indexes_ide_05

处理表单

register.jsp

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%--
  Created by IntelliJ IDEA.
  User: ouYang
  Date: 2019/9/1
  Time: 10:29
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Blog</title>
</head>
<body>

    <h1>欢迎加入Spring的大家庭</h1>
    <form method="post" >
        用户名:<input name="username" type="text" ><br>
        密码:<input name="password" type="password" ><br>
        年龄:<input name="age" type="number" ><br>
        <input type="submit" value="提交"><br>
    </form>
</body>
</html>

一个普通的表单,使用post请求提交,当我们没有指定action属性的时候,点击提交之后,会使用原地址,即请求表单页面的地址,通过post请求提交,本例中是/register. 表单包括 用户名,密码和年龄三个属性。后台进行相应的Bean接受(User)

user:

public class User implements Serializable {
    private String username;
    private String password;
    private Integer age;
    //all/noArgConstructor, getter and setter, toString
}

UserService :

@Service
public class UserService {
    private static List<User> users = new ArrayList<>();
    public void saveUser(User user){
        users.add(user);
    }
}

IndexController :

@Controller
public class IndexController {

    @Autowired
    private UserService userService;

    @GetMapping("/")
    public String home(){
        return "home";
    }

    @GetMapping("/register")
    public String toRegister(){
        return "register";
    }

    /*处理表单数据,并验证*/
    @PostMapping("/register")
    public String register(User user){
        userService.saveUser(user);
        return "redirect:/registerSuccess";
    }

    @GetMapping("/registerSuccess")
    public String registerSuccess(){
        return "registerSuccess";
    }

    @GetMapping("/registerFail")
    public String registerFail(){
        return "registerFail";
    }
}

通过上面代码可以看出,通过get请求类型,请求路径为/register,通过返回逻辑视图名,DispatchServlet将逻辑视图名去查找视图,并渲染返回,就可以进入注册表单页面。显而易见,前端提交的参数会自动填写到User对象中,这就是SpringMVC的强大之处,可以将数据自动与实体进行映射。