1. SpringMVC 基础

  • SpringMVC 在 在 三层架构的位置

  • SpringMVC 的优势

    • 清晰的角色划分:
      • 前端控制器(DispatcherServlet)
      • 请求到处理器映射(HandlerMapping)
      • 处理器适配器(HandlerAdapter)
      • 视图解析器(ViewResolver)
      • 处理器或页面控制器(Controller)
      • 验证器( Validator)
      • 命令对象(Command 请求参数绑定到的对象就叫命令对象)
      • 表单对象(Form Object 提供给表单展示和提交到的对象就叫表单对象)。
    • 分工明确,而且扩展点相当灵活,可以很容易扩展,虽然几乎不需要。
    • 由于命令对象就是一个 POJO,无需继承框架特定 API,可以使用命令对象直接作为业务对象。
    • 和 Spring 其他框架无缝集成,是其它 Web 框架所不具备的。
    • 可适配,通过 HandlerAdapter 可以支持任意的类作为处理器。
    • 可定制性,HandlerMapping、ViewResolver 等能够非常简单的定制。
    • 功能强大的数据验证、格式化、绑定机制。
    • 利用 Spring 提供的 Mock 对象能够非常简单的进行 Web 层单元测试。
    • 本地化、主题的解析的支持,使我们更容易进行国际化和主题的切换。
    • 强大的 JSP 标签库,使 JSP 编写更容易。
  • SpringMVC 和 和 Struts2

    • 共同点:
      • 它们都是表现层框架,都是基于 MVC 模型编写的。
      • 它们的底层都离不开原始 ServletAPI。
      • 它们处理请求的机制都是一个核心控制器。
    • 区别:
      • Spring MVC 的入口是 Servlet, 而 Struts2 是 Filter
      • Spring MVC 是基于方法设计的,而 Struts2 是基于类,Struts2 每次执行都会创建一个动作类。所以 Spring MVC 会稍微比 Struts2 快些。
      • Spring MVC 使用更加简洁,同时还支持 JSR303, 处理 AJAX 的请求更方便 (JSR303 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们 JavaBean 的属性上面,就可以在需要校验的时候进行校验了)
      • Struts2 的 OGNL 表达式使页面的开发效率相比 Spring MVC 更高些,但执行效率并没有比 JSTL 提升,尤其是 struts2 的表单标签,远没有 HTML 执行效率高。
  • 入门程序:

    index 页面:

    <body>
    <h3>入门程序</h3>
    <a href="hello">入门程序</a>
    </body>
    

    web.xml

    <web-app>
      <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
          <param-name>contextConfigLocation</param-name>
          <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
      </servlet>
      <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
      </servlet-mapping>
    </web-app>
    
    • DispatcherServlet 为前端控制器,所有请求通向前端控制器
    • init-param 为初始化参数,用于加载 Spring 配置文件
    • load-on-startup 代表程序启动就加载,创建 Servlet 对象
    • 路径使用 / 而不能使用 /*/ 会匹配到 /springmvc 这样的路径型 url,而不会匹配到像 .jsp 这样的后缀型的 url。

    springmvc.xml

    <!-- 开启注解扫描 -->
      <context:component-scan base-package="pers.ylq"/>
    
      <!-- 视图解析器对象 -->
      <bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
          <property name="prefix" value="/WEB-INF/pages/"/>
          <property name="suffix" value=".jsp"/>
      </bean>
    
      <!-- 开启SpringMVC框架注解的支持 -->
      <mvc:annotation-driven conversion-service="conversionService"/>
    
    • InternalResourceViewResolver 为视图解析器
    • prefix 为前缀,suffix 为后缀
    • 经视图解析器跳转至请求页面

    HelloController.java

    @Controller
      public class HelloController {
          @RequestMapping(path = "/hello")
          public String sayHello(){
              System.out.println("Hello SpringMVC");
              return "success";
          }
      }
    
    • @Controller 用 IoC 创建对象
    • RequestMapping 声明请求路径
    • 当 dispatcherServlet 配置路径为 *.do 的时候,后台可以写 /hello.do 或者 /hello
    • 返回 success.jsp
  • 入门程序简单分析

  • 详细分析

  • 前端控制器:DispatcherServlet
    用户请求到达前端控制器,它就相当于 mvc 模式中的 c,dispatcherServlet 是整个流程控制的中心,由它调用其它组件处理用户的请求,dispatcherServlet 的存在降低了组件之间的耦合性。

  • 处理器映射器:HandlerMapping
    HandlerMapping 负责根据用户请求找到 Handler 即处理器,SpringMVC 提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。

  • 处理器:Handler
    它就是我们开发中要编写的具体业务控制器。由 DispatcherServlet 把用户请求转发到 Handler。由 Handler 对具体的用户请求进行处理。

  • 处理器适配器:HandlAdapter
    通过 HandlerAdapter 对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。

  • 视图解析器:View Resolver View Resolver
    负责将处理结果生成 View 视图,View Resolver 首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成 View 视图对象,最后对 View 进行渲染将处理结果通过页面展示给用户。

  • 处理器映射器、处理器适配器、视图解析器称为 SpringMVC 的三大组件。使 用 <mvc:annotation-driven> 自动加载 RequestMappingHandlerMapping (处理映射器) 和 RequestMappingHandlerAdapter(处理适配器),可 用 在 SpringMVC.xml 配 置 文 件 中 使 用 <mvc:annotation-driven> 替代注解处理器和适配器的配置。

  • RequestMapping 注解:用于建立请求 URL 和处理请求方法之间的对应关系。

    • 位置:
      • 类上:一级访问目录
      • 方法上:二级访问目录
    • 属性:
      • value:用于指定请求的 URL。它和 path 属性的作用是一样的。
      • method:用于指定请求的方式。
      • params:用于指定限制请求参数的条件。
      • headers:用于指定限制请求消息头的条件。

2. 请求参数的绑定

  • 可以封装的类型

    • 基本类型和 String 类型
    • 实体类,以及关联的实体类
    • 数组和集合类型
  • 绑定的方法:通过把表单提交请求参数,作为控制器中方法参数进行绑定。

  • 基本类型和 String 类型的绑定:参数名称必须和控制器中方法的形参名称保持一致。(严格区分大小写)

  • 实体类类型的绑定:参数名和方法的形参中的实体类的属性名一致即可。

  • 实体类中有实体类:比如方法的形参为 Account,有一个属性为 User user,User 中有个成员变量 name,则需要前台参数的 name 为 user.name。

  • 参数的乱码问题:

    • 配置 Spring 官方提供的过滤器 CharacterEncodingFilter 传递参数即可。
    • 可以解决 post 请求体乱码问题,不能解决 get 乱码问题,因为 get 的请求参数在请求行中(tomcat8 已经解决)。
    • <filter>
          <filter-name>characterEncodingFilter</filter-name>
          <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
          <init-param>
              <param-name>encoding</param-name>
              <param-value>UTF-8</param-value>
          </init-param>
      </filter>
      <filter-mapping>
          <filter-name>characterEncodingFilter</filter-name>
          <url-pattern>/*</url-pattern>
      </filter-mapping>
      
  • 集合的封装:

    public class Account implements Serializable{
        private List<User> list;
        private Map<String,User> map;
    }
    
    @RequestMapping("/saveAccount")
    public String saveAccount(Account account){
        return "success";
    }
    
    <body>
    <input name="list[0].username" />
    <input name="list[1].username" />
    <input name="map['key'].username" />
    </body>
    
  • 自定义类型转换器:表单提交的任何数据类型全部都是字符串类型,Spring 框架内部会默认进行数据类型转换,来帮我们封装数据。但有时候可能因为格式问题封装失败,比如 String 转 Date,这时,我们需要自定义类型转换器。

    1. 自定义类型转换类,实现 Converter 接口。

    2. 注册自定义类型转换器,在 springmvc.xml 配置文件中编写配置

      <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
          <property name="converters">
              <set>
                  <bean class="自定义类的全类名"/>
              </set>
          </property>
      </bean>
      <!-- 注册类型转换器 -->
      <mvc:annotation-driven conversion-service="conversionService"/>
      
      • 注册类型转换器可以添加自定义的类型转换器,原有的不会失效。
  • 在控制器中获取原生 ServletAPI 对象(request、response 等)

    • 只需要在控制器的方法参数定义 HttpServletRequest 和 HttpServletResponse 类型的形参。

3. 常用注解

  • @RequestParam

    • 作用:把请求中的指定名称的参数传递给控制器中的形参赋值
    • 位置:参数上
    • 属性:
      • value:请求参数中的名称
      • required:请求参数中是否必须提供此参数,默认值是 true,必须提供
  • @RequestBody

    • 作用:用于获取请求体的内容(注意:get 方法不可以)
    • 位置:参数上
    • 属性 required:是否必须有请求体,默认值是 true
    • 备注:可以用来获取前台异步传输的 JSON 数据
  • @RestController

    • 作用:直接返回 json 数据,是 @Crotroller 和 @ResponseBody 的组合
    • 位置:类上
    • 备注:适用于前后端分离的项目,不再返回视图
  • @PathVariable

    • 作用:拥有绑定 url 中的占位符的。例如:url 中有/delete/就是占位符
    • 属性 value:指定 url 中的占位符名称
    • RESTFul 风格的 URL:请求路径一样,可以根据不同的请求方式去执行后台的不同方法
    • @RequestMapping(path="/hello/{id}")
          public String sayHello(@PathVariable("id") String id) {
          System.out.println(id);
          return "success";
      }
      
  • @RequestHeader

    • 作用:获取指定请求头的值
    • 位置:参数上
    • 属性 value:请求头的名称
  • @CookieValue

    • 作用:用于获取指定 cookie 的名称的值
    • 属性 value:cookie 的名称
  • @ModelAttribute

    • 作用:
      • 方法上:表示当前方法会在控制器方法执行前先执行。
      • 参数上:获取指定的数据给参数赋值。
    • 修饰的方法有返回值
      @ModelAttribute
      public User showUser(String name) {
          User user = new User();
          return user;
      }
      
      @RequestMapping("/updateUser")
      public String updateUser(User user) {
          //可以获取到showUser方法返回的User
          System.out.println(user);
          return "success";
      }
      
    • 修饰的方法没有返回值
      @ModelAttribute
      public void showUser(String name,Map<String, User> map) {
          User user = new User();
          map.put("abc", user);
      }
      
      @RequestMapping("/updateUser")
      public String updateUser(@ModelAttribute(value="abc") User user) {
          System.out.println(user);
          return "success";
      }
      
  • @SessionAttributes

    • 作用:用于多次执行控制器方法间的参数共享
    • 属性 value:指定存入属性的名称
    • @Controller
      @RequestMapping(path="/user")
      @SessionAttributes(value= {"username","password","age"}) // 把数据存入到session域对象中
      public class HelloController {
          //向session中存入值
          @RequestMapping(path="/save")
          public String save(Model model) {
              model.addAttribute("username", "root");
              model.addAttribute("password", "123");
              model.addAttribute("age", 20);
              return "success";
          }
      
          //从session中获取值
          @RequestMapping(path="/find")
          public String find(ModelMap modelMap) {
              String username = (String) modelMap.get("username");
              String password = (String) modelMap.get("password");
              Integer age = (Integer) modelMap.get("age");
              System.out.println(username + " : "+password +" : "+age);
              return "success";
          }
      
          //清除值
          @RequestMapping(path="/delete")
          public String delete(SessionStatus status) {
              status.setComplete();
              return "success";
          }
      }
      
    • 向 Model 中存值,会把值存入 request 域中,通过注解 @SessionAttributes,会将值同时存入 session 域。

4. 响应数据和结果视图

  • 转发可以访问 WEB-INF 目录,重定向不行。

  • 当方法的返回值是字符串的时候,字符串即为逻辑视图的名称,根据视图解析器解析为物理视图的地址,例如返回 "success"

    <!-- 视图解析器对象 -->
    <bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/pages/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
    

    经视图解析器,转发至 /WEB-INF/pages/success.jsp

  • 当返回值为 void,程序会将请求路径作为返回值,经视图解析器寻找对应路径。比如请求路径为 /user/delete,则会经视图解析器,寻找 WEB-INF/pages/user/delete.jsp,可以由参数获取 request 或 response 自己手动转发或重定向。

  • 返回值是 ModelAndView 对象,该对象是 Spring 提供的一个对象,可以用来存储值和要跳转的视图。当返回值为 String,参数为 Model 的时候,底层调用的也是此对象。

    @RequestMapping("/testModelAndView")
    public ModelAndView testModelAndView(){
        // 创建ModelAndView对象
        ModelAndView mv = new ModelAndView();
        User user = new User();
        // 把user对象存储到mv对象中,也会把user对象存入到request对象
        mv.addObject("user",user);
        // 跳转到哪个页面
        mv.setViewName("success");
        return mv;
    }
    
  • SpringMVC 框架提供的转发和重定向

    • forward 关键字进行转发,redirect 关键字进行重定向
    • 重定向不能访问 WEB-INF 目录,使用 SpringMVC 重定向的时候,路径不需要再加项目名,底层已经帮我们加上了。
    • public String testForwardOrRedirect(){
          return "forward:/WEB-INF/pages/success.jsp";
          return "redirect:/index.jsp";
      }
      
  • SpringMVC 前端控制器也会拦截静态资源,通过 mvc:resources 标签配置不过滤

    <mvc:resources location="/css/" mapping="/css/**"/>
    <mvc:resources location="/images/" mapping="/images/**"/>
    <mvc:resources location="/js/" mapping="/js/**"/>
    
    • location 表示 webapp 目录下的包下的所有文件
    • mapping 表示以 /xxx 开头的所有请求路径
  • 接收与响应 JSON 数据:

    @RequestMapping("/testJson")
    public @ResponseBody Address testJson(@RequestBody Address address) {
        System.out.println(address);
        address.setAddressName("上海");
        return address;
    }
    
    • @RequestBody 注解会将前台传进来的 JSON 自动封装为对应对象。
    • @ResponseBody 会将返回的对象解析为 JSON 然后响应给前台。
    • @ResponseBody 可以作用于类和方法上面。
    • 使用前提:pom 坐标中加入 jackson 2.7.0 以上版本
      <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.9.0</version>
      </dependency>
      <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>2.9.0</version>
      </dependency>
      <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>2.9.0</version>
      </dependency>
      

5. 文件上传

  • 传统文件上传
    • jar 包

      <dependency>
          <groupId>commons-fileupload</groupId>
          <artifactId>commons-fileupload</artifactId>
          <version>1.3.1</version>
      </dependency>
      <dependency>
          <groupId>commons-io</groupId>
          <artifactId>commons-io</artifactId>
          <version>2.4</version>
      </dependency>
      
    • 前台代码

      <form action="user/fileupload1" method="post" enctype="multipart/form-data">
          选择文件:<input type="file" name="upload"><br>
          <input type="submit" value="上传">
      </form>
      
      • enctype:是表单请求正文的类型,取值必须是 multipart/form-data 才能上传文件,表示把表单分为多个部分。
      • 必须为 post,get 的 url 有长度限制。
    • 后台代码:

      @RequestMapping("/fileupload1")
      public String fileuoload1(HttpServletRequest request) throws Exception {
          // 使用fileupload组件完成文件上传
          // 上传的位置
          String path = request.getSession().getServletContext().getRealPath("/uploads/");
          // 判断,该路径是否存在
          File file = new File(path);
          if(!file.exists()){
              file.mkdirs();
          }
          // 解析request对象,获取上传文件项
          DiskFileItemFactory factory = new DiskFileItemFactory();
          ServletFileUpload upload = new ServletFileUpload(factory);
          // 解析request
          List<FileItem> items = upload.parseRequest(request);
          // 遍历
          for(FileItem item:items){
              // 进行判断,当前item对象是否是上传文件项
              if(item.isFormField()){
                  // 说明普通表单项
              }else{
                  // 说明上传文件项
                  // 获取上传文件的名称
                  String filename = item.getName();
                  // 把文件的名称设置唯一值,uuid
                  String uuid = UUID.randomUUID().toString().replace("-", "");
                  filename = uuid+"_"+filename;
                  // 完成文件上传
                  item.write(new File(path,filename));
                  // 删除临时文件,大于10k会产生临时文件
                  item.delete();
              }
          }
          return "success";
      }
      
  • SpringMVC 文件上传原理
    • 我们只需要配置文件解析器,SpringMVC 自动帮我们解析,我们只需要参数获取文件就行。
    • 解析器的 id 必须为 multipartResolver
    • 使用 MultipartFile 获取文件。
    • 参数名必须和前台 name 属性名一致。
  • 代码实现
    • 配置文件解析器

      <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
          <property name="maxInMemorySize" value="10485760"/>
      </bean>
      
      • maxInMemorySize 为配置上传文件最大大小,单位字节
    • 后台实现

      @Controller
      @RequestMapping("/user")
      public class UserController {
          @RequestMapping("/fileupload")
          public String fileupload(HttpServletRequest request, MultipartFile upload) throws Exception {
              String path = request.getSession().getServletContext().getRealPath("/uploads/");
              File file = new File(path);
              if (!file.exists()) file.mkdirs();
              String filename = upload.getOriginalFilename();
              String uuid = UUID.randomUUID().toString();
              filename = uuid + "_" + filename;
              //将文件写入指定目录
              upload.transferTo(new File(path, filename));
              return "success";
          }
      }
      
  • 跨服务器上传
    • jar 包
      <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-core</artifactId>
          <version>1.18.1</version>
      </dependency>
      <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-client</artifactId>
          <version>1.18.1</version>
      </dependency>
      
    • 后台实现
      @RequestMapping("/fileupload3")
      public String fileuoload3(MultipartFile upload) throws Exception {
          // 定义上传文件服务器路径
          String path = "http://localhost:9090/uploads/";
          // 获取上传文件的名称
          String filename = upload.getOriginalFilename();
          // 把文件的名称设置唯一值,uuid
          String uuid = UUID.randomUUID().toString().replace("-", "");
          filename = uuid+"_"+filename;
          // 创建客户端的对象
          Client client = Client.create();
          // 和图片服务器进行连接
          WebResource webResource = client.resource(path + filename);
          // 上传文件
          webResource.put(upload.getBytes());
          return "success";
      }
      

6. SpringMVC 异常处理

  • 自定义异常类
    public class SysException extends Exception{
        // 存储提示信息的
        private String message;
        public SysException(String message) {
            this.message = message;
        }
    }
    
  • 自定义异常处理器:实现 HandlerExceptionResolver 接口
    public class SysExceptionResolver implements HandlerExceptionResolver{
        public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
            // 获取到异常对象
            SysException e = null;
            if(ex instanceof SysException){
                e = (SysException)ex;
            }else{
                e = new SysException("系统正在维护....");
            }
            // 创建ModelAndView对象
            ModelAndView mv = new ModelAndView();
            mv.addObject("errorMsg",e.getMessage());
            mv.setViewName("error");
            return mv;
        }
    }
    
  • 配置异常处理器
    <bean id="sysExceptionResolver" class="cn.itcast.exception.SysExceptionResolver"/>
    

7. SpringMVC 拦截器

  • SpringMVC 框架中的拦截器用于对处理器进行预处理和后处理的技术。
  • 拦截器和过滤器的功能比较类似,有区别
    • 过滤器是 Servlet 规范的一部分,任何框架都可以使用过滤器技术。
    • 拦截器是 SpringMVC 框架独有的。
    • 过滤器配置了 /*,可以拦截任何资源。
    • 拦截器只会对控制器中的方法进行拦截。
  • 想要自定义拦截器,需要实现 HandlerInterceptor 接口。
  • 拦截器的执行流程
  • 自定义拦截器
    public class MyInterceptor1 implements HandlerInterceptor{
    /**
    * return true放行
    * return false拦截
    * 可以使用转发或者重定向直接跳转到指定的页面。
    */
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            return true;
        }
    }
    
  • springmvc.xml 中配置拦截器
    <!--配置拦截器-->
    <mvc:interceptors>
        <mvc:interceptor>
            <!--要拦截的具体的方法-->
            <mvc:mapping path="/user/*"/>
            <!--不要拦截的方法
            <mvc:exclude-mapping path=""/>
            -->
            <!--配置拦截器对象-->
            <bean class="cn.itcast.controller.cn.itcast.interceptor.MyInterceptor1" />
        </mvc:interceptor>
    
        <!--配置第二个拦截器-->
        <mvc:interceptor>
            <mvc:mapping path="/**"/>
            <bean class="cn.itcast.controller.cn.itcast.interceptor.MyInterceptor2" />
        </mvc:interceptor>
    </mvc:interceptors>
    
  • HandlerInterceptor 接口中的方法
    • preHandle 方法是 controller 方法执行前拦截的方法,可以进行跳转页面。
    • postHandle 方法是 controller 方法执行后执行的方法,在 JSP 视图执行前拦截,可以进行跳转页面。
    • afterCompletion 方法是在 JSP 执行后执行,不能进行页面跳转。