SpringMVC (2)

SpringMVC:Ajax处理、文件传输、拦截器与异常处理

处理Ajax

Ajax

  • Ajax:实现异步请求,不重新加载整个页面的情况下,与服务器交换数据并更新部分网页内容(利用axios.min.js)

  • 基本的参数:参见请求配置 | Axios 中文文档

    • url:请求路径(绝对路径,需要加上tomcat的上下文路径)
    • method:请求方式
    • params:请求参数——以name=value & name=value的方式传输,存储位置为url中,可以直接通过request.getParameter()获取
    • data:请求参数——以json格式发送,存储的位置为请求报文的请求体中(注意,get没有请求体,因此使用该参数时,请求方式通常为post)
  • 常用的函数:

    1
    2
    3
    4
    5
    6
    7
    8
    axios.request(config)
    axios.get(url[, config])
    axios.delete(url[, config])
    axios.head(url[, config])
    axios.options(url[, config])
    axios.post(url[, data[, config]])
    axios.put(url[, data[, config]])
    axios.patch(url[, data[, config]])
    • 注意,post方法默认使用data参数来传递请求参数
    • 如果要使用params的方式传输,则可以直接将参数拼接到url中,例如传递参数id=1时,url为/targetAddress?id=1(不推荐)
  • 例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <head>
    <meta charset="UTF-8">
    <title>首页</title>
    <script type="text/javascript" th:src="@{/static/js/vue.js}"></script>
    <script type="text/javascript" th:src="@{/static/js/axios.min.js}"></script>
    </head>
    <body>
    <div id="app">
    <h1>index.html</h1>

    <input type="button" value="测试SpringMVC处理ajax" @click="testAjax()"><br>
    </div>
    <script type="text/javascript">
    var vue = new Vue({
    el:"#app",
    methods:{
    testAjax(){
    axios.post(
    "/Demo4_war_exploded/test/ajax?id=1001", <!--param参数传递请求参数id-->
    {username:"admin",password:"123456"} <!--data参数来传递请求参数-->
    ).then(response=>{ <!--response封装了服务器返回的结果,封装到了data属性中-->
    console.log(response.data);
    });
    }
    }
    });
    </script>
    </body>
    • 此时,相应的控制器方法:

      1
      2
      3
      4
      5
      6
      @RequestMapping("/test/ajax")
      public void testAjax(Integer id, @RequestBody String requestBody, HttpServletResponse response) throws IOException {
      System.out.println("requestBody:"+requestBody); // 请求体中的请求数据
      System.out.println("id:"+id); // param中的请求参数
      response.getWriter().write("hello,axios"); // console.log(response.data)打印结果为"hello,axios"
      }
      • 不返回字符串,因为只是局部更新网页,不需要新的模板

      • 输出:

        1
        2
        requestBody:{"username":"admin","password":"123456"}
        id:1001

RequestBody注解

  • 数据从前端传向后端

  • 方式1:如上例,请求体的内容被转换为string

  • 方式2:转换为实体对象

    • 添加依赖

      1
      2
      3
      4
      5
      <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.12.1</version>
      </dependency>
    • SpringMVC配置文件开启注解驱动

    • 在处理请求的控制器方法的形参位置,直接设置json格式的请求参数要转换的java类型的形参,使用@RequestBody注解标识即可

      1
      2
      3
      4
      5
      @RequestMapping("/test/RequestBody/jsonPojo")
      public void testRequestBody(@RequestBody User user, HttpServletResponse response) throws IOException {
      System.out.println(user);
      response.getWriter().write("hello,RequestBody");
      }
  • 方式3:转化为map

    1
    2
    3
    4
    5
    @RequestMapping("/test/RequestBody/jsonMap")
    public void testRequestBody(@RequestBody Map<String, Object> map, HttpServletResponse response) throws IOException {
    System.out.println(map);
    response.getWriter().write("hello,RequestBody");
    }

ResponseBody注解

  • 数据从后端传向前端

  • 标识一个控制器方法,将该方法的返回值直接作为响应报文的响应体,响应到浏览器(没有该注解,则为页面跳转)

  • 通常需要将java对象转换为json字符串,再响应回浏览器,因此需要导入jackson依赖。此时能够直接返回一个java对象——这里的java对象可以为map、list、实体类

  • 需要jackson依赖

  • 例如:

    1
    2
    3
    4
    5
    6
    7
    8
    @RequestMapping("/test/ResponseBody/json")
    @ResponseBody
    public List<User> testResponseBodyJson(){
    User user1 = new User("admin1", "123456", 20, "男");
    User user2 = new User("admin2", "123456", 20, "男");
    User user3 = new User("admin3", "123456", 20, "男");
    return Arrays.asList(user1, user2, user3);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
      <div id="app">
    ...
    <input type="button" value="ResponseBody" @click="testResponseBody()"><br>
    </div>
    <script type="text/javascript">
    var vue = new Vue({
    el:"#app",
    methods:{
    ...
    testResponseBody(){
    axios.post("/Demo4_war_exploded/test/ResponseBody/json").then(response=>{
    console.log(response.data);
    });
    }
    }
    });
    </script>
  • 小结:@ResponseBody和@RequestBody封装了响应数据和请求数据。注意区分之前的请求数据处理方法和响应数据处理方法

    • @ResponseBody和ModelAndView:
      • @ResponseBody将方法的返回值,以特定的格式写入到response的body区域,再将数据返回给客户端
      • 当方法上面没有写ResponseBody,底层会将方法的返回值封装为ModelAndView对象——该对象需要设置返回视图,此时会导致页面刷新
    • @RequestParam、@RequestBody、@PathVariable:
      • @RequestParam一般用于get请求,参数是一个个的参数。此时的请求url一般形式如http://localhost:8089/test/getDataById?id=1
      • @RequestBody一般用于post请求,参数是一个对象或者集合,并且请求一般为json类型的请求体
      • @PathVariable一般用于get请求,参数是一个个的参数。能够体现RestFul风格。此时的请求url一般为http://localhost:8089/test/getDataById/1
      • 参考@RequestParam、@RequestBody、@PathVariable区别和案例分析
  • @RestController注解是springMVC提供的一个复合注解,标识在控制器的类上,就相当于为类添加了 @Controller注解,并且为其中的每个方法添加了@ResponseBody注解

文件上传与下载

文件下载

  • ResponseEntity为控制器方法的返回类型,此时返回值就是响应到浏览器的响应报文——如果响应报文为文件,自然就是文件下载了

  • 处理器方法:具体文件名可以通过请求参数确定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @RequestMapping("/test/down")
    public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException {
    //获取ServletContext对象
    ServletContext servletContext = session.getServletContext();
    //获取服务器中文件的真实路径——war包下的路径,在项目中的路径为src/main/webapp/static/img
    String realPath = servletContext.getRealPath("/static/image");
    realPath = realPath + File.separator + "1.png";
    //创建输入流
    InputStream is = new FileInputStream(realPath);
    //创建字节数组,is.available()获取输入流所对应文件的字节数
    byte[] bytes = new byte[is.available()];
    is.read(bytes); //将流读到字节数组中
    //创建HttpHeaders对象设置响应头信息
    MultiValueMap<String, String> headers = new HttpHeaders();
    //设置要下载方式以及下载文件的名字
    headers.add("Content-Disposition", "attachment;filename=1.png");
    //设置响应状态码
    HttpStatus statusCode = HttpStatus.OK;
    //创建ResponseEntity对象
    ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes, headers, statusCode);

    is.close(); //关闭输入流
    return responseEntity;
    }
    • session参数:用于获得上下文,以得到某个资源的真实路径
    • 请求头本质是键值对
      • Content-Disposition:设置下载文件的方式
        • attachment;filename=1.png:以附件的方式下载,下载文件的默认名字为1.png

文件上传

  • form表单的请求方式必须为post,属性enctype=”multipart/form-data”(以二进制的方式传输数据)

    1
    2
    3
    4
    <form th:action="@{/test/upload}" method="post" enctype="multipart/form-data">
    头像:<input type="file" name="photo"><br>
    <input type="submit" value="上传">
    </form>
  • 添加依赖

    1
    2
    3
    4
    5
    6
    <!--上传文件-->
    <dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.1</version>
    </dependency>
  • SpringMVC的配置文件中添加配置——通过文件解析器的解析才能将文件转换为MultipartFile对象,配置中的bean id必须为multipartResolver

    1
    2
    3
    4
    5
    6
    7
    8
    <!--文件上传解析器-->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <!--文件默认编码-->
    <property name="defaultEncoding" value="UTF-8"></property>
    <!-- 上传文件大小上限,单位为字节(10485760=10M) -->
    <property name="maxUploadSize" value="10485760"/>
    <property name="maxInMemorySize" value="40960"/>
    </bean>
  • 处理器方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @RequestMapping("/test/upload")
    public String testUp(MultipartFile photo, HttpSession session) throws IOException {
    //获取上传的文件的文件名
    String fileName = photo.getOriginalFilename();
    //获取上传的文件的后缀名
    String hzName = fileName.substring(fileName.lastIndexOf("."));
    //解决重名问题
    String uuid = UUID.randomUUID().toString();
    fileName = uuid + hzName;

    //获取ServletContext对象
    ServletContext servletContext = session.getServletContext();
    //获取当前工程下photo目录的真实路径
    String photoPath = servletContext.getRealPath("/static/photo");
    //创建photoPath所对应的File对象
    File file = new File(photoPath);
    //判断file所对应目录是否存在
    if(!file.exists()){
    file.mkdir();
    }
    String finalPath = photoPath + File.separator + fileName;
    //上传文件
    photo.transferTo(new File(finalPath));
    return "index";
    }
    • 获得上传的文件:上传文件被封装为MultipartFile对象,形参名和表单中的名字相同

拦截器

  • 拦截器用于拦截控制器方法的执行

    • 需要实现接口有HandlerInterceptor
    • 三个方法:
      • preHandle:控制器方法执行之前执行preHandle(),其boolean类型的返回值表示是否拦截或放行,返回true为放行,即调用控制器方法;返回false表示拦截
      • postHandle:控制器方法执行之后执行postHandle(),无返回值
      • afterCompletion:处理完视图和模型数据,渲染视图完毕后执行afterCompletion(),无返回值
  • 范例:

    • 在SpringMVC配置文件添加相关配置(将某一个类设置为拦截器)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      <mvc:interceptors>
      <!--已经通过@Component注解添加了这个bean,因此只需要引用即可-->
      <ref bean="firstInterceptor"/>

      <!--第二种配置方式,精确设置某个拦截器的拦截请求对象——mvc:mapping设置需要拦截的请求,通过
      mvc:exclude-mapping设置需要排除的请求,即不需要拦截的请求-->
      <mvc:interceptor>
      <!--/*表示上下文后的一层的请求,不会拦截/a/b-->
      <!--/**表示拦截所有的请求-->
      <mvc:mapping path="/**"/>
      <mvc:exclude-mapping path="/testRequestEntity"/>
      <ref bean="firstInterceptor"/>
      </mvc:interceptor>

      </mvc:interceptors>
    • 拦截器的实现:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      @Component
      public class FirstInterceptor implements HandlerInterceptor {
      @Override
      public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      System.out.println("FirstInterceptor-->preHandle");
      return true;
      }

      @Override
      public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
      System.out.println("FirstInterceptor-->postHandle");
      }

      @Override
      public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
      System.out.println("FirstInterceptor-->afterCompletion");
      }
      }
  • 多个拦截器的执行顺序:

    • 多个拦截器的配置:

      1
      2
      3
      4
      <mvc:interceptors>
      <ref bean="firstInterceptor"/>
      <ref bean="secondInterceptor"/>
      </mvc:interceptors>
    • 输出:

      1
      2
      3
      4
      5
      6
      FirstInterceptor-->preHandle
      SecondInterceptor-->preHandle
      SecondInterceptor-->postHandle
      FirstInterceptor-->postHandle
      SecondInterceptor-->afterCompletion
      FirstInterceptor-->afterCompletion
    • 执行顺序与在SpringMVC配置文件中配置的顺序有关

    • 若拦截器中某个拦截器preHandle返回了false,该拦截器和它之前的拦截器的preHandle都会执行,所有的拦截器postHandle都不执行,该拦截器之前的返回true的拦截器afterCompletion会执行

异常处理器

  • SpringMVC提供了一个处理控制器方法执行过程中所出现异常的接口:HandlerExceptionResolver

  • HandlerExceptionResolver接口的实现类:

    • DefaultHandlerExceptionResolver(SpringMVC默认使用该实现类处理异常)
    • SimpleMappingExceptionResolve(可自定义)
  • 返回一个ModelAndView,实现页面的跳转

  • 基于配置的异常处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
    <props>
    <!--
    properties的键表示处理器方法执行过程中出现的异常
    properties的值表示若出现指定异常时,跳转到的新视图名称
    -->
    <prop key="java.lang.ArithmeticException">error</prop>
    </props>
    </property>
    <!--exceptionAttribute属性:设置一个属性名,将出现的异常信息在请求域中进行共享-->
    <property name="exceptionAttribute" value="ex"></property>
    </bean>
    • 这里ex为共享在request域中的错误信息

      1
      <p th:text="${ex}" />
  • 基于注解的异常注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package Controller;

    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;

    // 标识一个异常处理组件
    @ControllerAdvice
    public class ExceptionController {

    //@ExceptionHandler用于设置所标识方法处理的异常
    @ExceptionHandler(ArithmeticException.class)
    public String handleArithmeticException(Exception ex, Model model){ //ex表示当前请求处理中出现的异常对象
    model.addAttribute("ex", ex);
    return "error";
    }

    }

SpringMVC执行流程

  • 基本组件:

    • DispatcherServlet:前端控制器,框架提供:统一处理请求和响应,整个流程控制的中心,由它调用其它组件处理用户的请求
    • HandlerMapping:处理器映射器,框架提供;根据请求的url、method等信息查找Handler
    • Handler:处理器;在DispatcherServlet的控制下,Handler对具体的用户请求进行处理
    • HandlerAdapter:处理器适配器,框架提供;通过HandlerAdapter执行控制器方法
    • ViewResolver:视图解析器,框架提供;进行视图解析,得到相应的视图
    • View:视图;将模型数据通过页面展示给用户
  • 执行流程:

    1. 用户向服务器发送请求,请求被SpringMVC 前端控制器 DispatcherServlet捕获
    2. DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI),判断请求URI对应的映射:
      1. 不存在
        1. 再判断是否配置了mvc:default-servlet-handler
        2. 如果没配置,则控制台报映射查找不到,客户端展示404错误
        3. 如果有配置,则访问目标资源(一般为静态资源,如:JS,CSS,HTML),找不到客户端也会展示404错误
      2. 存在则执行下面的流程
        1. 根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain执行链对象的形式返回
        2. DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter
        3. 如果成功获得HandlerAdapter,此时将开始执行拦截器的preHandler(…)方法【正向】
        4. 提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)方法,处理请求。在填充Handler的入参过程中,根据配置,Spring将做一些额外的工作:
          1. HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息
          2. 数据转换:对请求消息进行数据转换。如String转换成Integer、Double等
          3. 数据格式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等
          4. 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中
        5. Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象
        6. 此时将开始执行拦截器的postHandle(…)方法【逆向】
        7. 根据返回的ModelAndView(此时会判断是否存在异常:如果存在异常,则执行HandlerExceptionResolver进行异常处理)选择一个适合的 ViewResolver 进行视图解析,根据ModelAndView,来渲染视图
        8. 渲染视图完毕执行拦截器的afterCompletion(…)方法【逆向】
        9. 将渲染结果返回给客户端