SpringMVC 笔记

SpringMVC

仅供参考!!!

笔记作者:谢禹宏

邮箱:190464906@qq.com

Hello World

  • Maven

    <dependencies>
      <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.2.4.RELEASE</version>
      </dependency>
      <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.10</version>
      </dependency>
      <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>servlet-api</artifactId>
        <version>2.5</version>
      </dependency>
      <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>jsp-api</artifactId>
        <version>2.2</version>
      </dependency>
      <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>1.2</version>
      </dependency>
    </dependencies>
    
  • web.xml

    <!DOCTYPE web-app PUBLIC
     "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
     "http://java.sun.com/dtd/web-app_2_3.dtd" >
    
    <web-app>
      <!-- 配置DispatcherServlet -->
      <servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 配置DispatcherServlet的一个初始化参数: 配置SpringMVC配置文件的位置和名称 -->
        <!--
          实际上也可以不通过contextConfigLocation来配置SpringMVC配置文件的位置和名称, 而使用默认的。
          默认的配置文件为: /WEB-INF/<servlet-name>-servlet.xml
    	  本配置的默认文件为: DispatcherServlet-servlet.xml
        -->
        <init-param>
          <param-name>contextConfigLocation</param-name>
          <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <!-- 配置DispatcherServlet的启动级别 -->
        <load-on-startup>1</load-on-startup>
      </servlet>
      <servlet-mapping>
        <servlet-name>DispatcherServlet</servlet-name>
        <!--
            在SpringMVC中:
            /: 只匹配所有的请求, 不会匹配jsp页面
            /*: 匹配所有的请求, 包括jsp页面
        -->
        <url-pattern>/</url-pattern>
      </servlet-mapping>
        
      <!-- 配置SpringMVC自带的过滤器, 作用: 处理请求时的乱码 -->
      <filter>
        <filter-name>encoding</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <!-- 告诉SpringMVC如何进行转码 -->
        <init-param>
          <param-name>encoding</param-name>
          <param-value>UTF-8</param-value>
        </init-param>
      </filter>
      <filter-mapping>
        <filter-name>encoding</filter-name>
        <!-- /*拦截所有请求 -->
        <url-pattern>/*</url-pattern>
      </filter-mapping>
        
    </web-app>
    
  • springmvc.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    
        <!-- 配置包扫描 -->
        <context:component-scan base-package="org.hong.controller"></context:component-scan>
    
        <!-- 配置视图解析器: 如何把controller方法返回值解析为实际的物理视图 -->
        <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <property name="prefix" value="/WEB-INF/views/"/>
            <property name="suffix" value=".jsp"/>
        </bean>
    </beans>
    
  • controller

    package org.hong.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    // 使用@Controller注解将HelloWorld类注册为Controller组件并放到Spring容器中
    @Controller
    public class HelloWorld {
    
        /*
         * 1.使用RequestMapping来映射请求地址
         * 2.返回值会通过视图解析器解析为实际的物理视图, 对于InternalResourceViewResolver视图解析器, 会做如下的解析:
         *  通过 (prefix + returnVal + suffix) 这样的方式得到实际的物理视图, 然后做转发操作 (/WEB-INFO/views/success.jsp)
         */
        @RequestMapping("hello")
        public String hello(){
            System.out.println("Hello World!");
            return "success";
        }
    
    }
    
  • index.jsp

    <html>
    <body>
    <h2>Hello World!</h2>
    </body>
        <a href="hello">Hello World</a>
    </html>
    
  • success.sjp

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
        <title>Title</title>
    </head>
    <body>
        <h4>Success Page</h4>
    </body>
    </html>
    

@RequestMapping映射请求

修饰类和方法

  • SpringMVC 使用 @RequestMapping 注解为控制器指定可以处理哪些URL请求
  • 在控制器的类定义及方法定义处都可标注
    • 类定义处:提供初步的请求映射信息。相对于 WEB 应用的根目录
    • 方法处:提供进一步的细分映射信息。相对于类定义处的URL。若类定义处未标注 @RequestMapping,则方法处标记的URL相对于WEB应用的根目录
  • DispatcherServlet截获请求后,就通过控制器上 @RequestMapping 提供的映射信息确定请求所对应的处理方法
package org.hong.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("springmvc")
public class SpringMVC_RequestMapping {

    private static final String SUCCESS = "success";

    // 请求地址应该为 springmvc/testRequestMapping
    @RequestMapping("/testRequestMapping")
    public String testRequestMapping(){
        System.out.println("testRequestMapping");
        return SUCCESS;
    }

}
<a href="springmvc/testRequestMapping">testRequestMapping</a>

映射请求

  • @RequestMapping 除了可以使用请求URL映射请求外,还可以使用请求方法、请求参数及请求头映射请求
  • @RequestMapping 的 value、method、params 及 heads 分别表示请求URL、请求方法、请求参数及请求头的映射条件,他们之间是与的关系,联合使用多个条件可以使请求映射更加精确化
  • params 和 headers 支持简单的表达式
    • param1:表示请求请求必须包含名为 param1 的请求参数
    • !param1:表示请求不能包含名为 param1 的请求参数
    • param1 != value1:表示请求包含名为 param1 的请求参数,但其值不能为 value1
    • {”param1 = value1“, “param2”}:请求必须包含 parma1 和 param2 的两个请求参数,且 param1 参数的值必须为 value1
请求方式

使用 @RequestMapping 的 method 属性可指定请求的方式

// 常用: 使用method属性指定请求方式, 使用RequestMethod枚举表示
@RequestMapping(value = "testMethod", method = RequestMethod.POST)
public String testMethod(){
    System.out.println("testMethod");
    return SUCCESS;
}

但是每次都去指定请求方式比较麻烦,此时可以使用 @PostMapping、@GetMapping …

// 可以直接使用 @XXXMapping 注解(XXX为请求的方式)直接指明请求方式
// 这些注解的使用与 @RequestMapping 完全一致
@PostMapping("testMethod")
public String testMethod(){
    System.out.println("testMethod");
    return SUCCESS;
}
<!-- GET方式:× -->
<a href="springmvc/testMethod">testMethod</a><br/>
<!-- POST方式:√ -->
<form action="springmvc/testMethod" method="POST">
  <input type="submit"/>
</form>
请求参数及请求头

必须包含 username 和 age 属性,且 age 属性不能为 18 ;请求头的 Accept-Language 属性必须为 zh-CN,zh;q=0.9 才能访问

@RequestMapping(value = "testParamAndHeaders", params = {"username", "age!=18"}, headers = {"Accept-Language: zh-CN,zh;q=0.9"})
public String testParamAndHeaders(){
    System.out.println("testParamAndHeaders");
    return SUCCESS;
}
<a href="springmvc/testParamAndHeaders?username=123456&age=19">testMethod</a>

此时我们指定请求头的 Accept-Language 属性必须为 en-US,zh;q=0.9 ,此时我们将不能再访问了,中国默认 zh-CN

@RequestMapping(value = "testParamAndHeaders", params = {"username", "age!=18"}, headers = {"Accept-Language: en-US,zh;q=0.9"})
public String testParamAndHeaders(){
    System.out.println("testParamAndHeaders");
    return SUCCESS;
}

使用通配符映射URL

  • Ant 风格资源地址支持 3 种通配符
    • ? :匹配文件名中的一个字符
    • *:匹配文件名中的任意字符
    • **:**匹配多层路径
  • @RequestMapping还支持Ant风格的URL:
    • /user/*/createUser:匹配 /user/aaa/createUser/user/bbb/createUser等URL
    • /user/**/createUser:匹配 /user/createUser/user/aaa/bbb/createUser等URL
    • /user/createUser??:匹配 /user/createUseraa/user/createUserbb等URL

@PathVariable 映射 URL 绑定的占位符

  • 带占位符的 URL 是 Spring3.0新增的功能,该功能在 SpringMVC 向 REST 目标挺进发展过程中具有历程碑的意义
  • 通过 @PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的形参列表中:URL 中的 {xxx}占位符可以通过 @PathVariable(“xxx”) 绑定到操作方法的形参列表中。
@RequestMapping(value = "/testPathVariable/{id}")
public String testPathVariable(@PathVariable("id") Integer id){
    System.out.println("testPathVariable:" + id);
    return SUCCESS;
}
<a href="springmvc/testPathVariable/1">testPathVariable</a>

REST风格

HTTP 协议里面,有四个表示操作方法的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:

  1. GET 用来获取资源 --> 查询
  2. POST 用来新建资源 --> 添加
  3. PUT 用来更新资源 --> 修改
  4. DELETE 用来删除资源 --> 删除

示例 ( order : 订单 ):

  • /order/1 HTTP GET :得到 id = 1 的 order
  • /order/1 HTTP PUT :更新 id = 1 的 order
  • /order/1 HTTP DELETE :删除 id = 1 的 order
  • /order HTTP POST : 新增 order

如何发送 PUT 请求和 DELETE 请求

  1. 在 web.xml 中配置 HiddenHttpMethodFilter 过滤器
  2. 发送 POST 请求
  3. 在发送 POST 请求时携带一个 name="_method" 的隐藏域,值为 DELETE 或 PUT

在 SpringMVC 的目标方法中得到 id 值

  • 使用 @PathVariable 注解

HiddenHttpMethodFilter

浏览器form表单只支持 GET 与 POST 请求,而 DELETE、PUT 等 method 并不支持,Spring3.0 添加了一个过滤器,可以将这些请求转换为标准的 http 方法,使得支持 GET、POST、PUT 与 DELETE 请求。

注意点:在 Tomcat 8.0 以上版本中不支持 jsp的 get/post/head 之外的请求,可以设置错误页面参数 isErrorPage=“true”

  • web.xml

    <!-- 配置 org.springframework.web.filter.HiddenHttpMethodFilter: 可以把 POST 请求转为 DELETE 或 PUT 请求 -->
    <filter>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    
  • index.jsp

    <%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true"%>
    <html>
      <head>
        <title>$Title$</title>
      </head>
      <body>
    	<a href="springmvc/testRest/1">Test Rest GET</a>
        <form action="springmvc/testRest" method="post">
          <input type="submit" value="Test Rest POST">
        </form>
        <form action="springmvc/testRest/1" method="post">
          <input type="hidden" name="_method" value="DELETE"/>
          <input type="submit" value="Test Rest DELETE">
        </form>
        <form action="springmvc/testRest/1" method="post">
          <input type="hidden" name="_method" value="PUT"/>
          <input type="submit" value="Test Rest PUT">
        </form>
      </body>
    </html>
    
  • Controller

    @RequestMapping(value = "/testRest/{id}", method = RequestMethod.GET)
    public String testRestPost(@PathVariable("id") Integer id){
        System.out.println("testRest Get: " + id);
        return SUCCESS;
    }
    
    @RequestMapping(value = "/testRest", method = RequestMethod.POST)
    public String testRestGet(){
        System.out.println("testRest Post");
        return SUCCESS;
    }
    
    @RequestMapping(value = "/testRest/{id}", method = RequestMethod.PUT)
    public String testRestPut(@PathVariable("id") Integer id){
        System.out.println("testRest Put: " + id);
        return SUCCESS;
    }
    
    @RequestMapping(value = "/testRest/{id}", method = RequestMethod.DELETE)
    public String testRestDelete(@PathVariable("id") Integer id){
        System.out.println("testRest Delete: " + id);
        return SUCCESS;
    }
    

映射请求参数

@RequestParam

使用@RequestParam 来映射请求参数。

  • value:请求参数的参数名,必须与请求参数名一致;如果目标方法参数名与请求参数名一致可以不写
  • required:设置该参数是否必须赋值。默认为 true,表示请求中一定要有相应的参数,否则将报错。
  • defaultValue:请求参数的默认值

使用示例

  • Controller

    @RequestMapping(value = "/testRequestParam")
    public String testRequestParam(@RequestParam("username")String username, @RequestParam(value = "age", required = false) Integer age){
        System.out.println("testRequestParam: " + "username=" + username + ", age=" + age);
        return SUCCESS;
    }
    
  • index.jsp

    <a href="springmvc/testRequestParam?username=hong&age=18">Test RequestParam</a>
    

@RequestBody

使用 @RequestBody 来映射 post 请求中的 json 数据,可以自动封装成一个对象。

使用示例

@PostMapping(value = "/testRequestBody")
public String testRequestParam(@RequestBody User user){
    System.out.println(user);
    return SUCCESS;
}

注意:在使用文件上传时不能使用这个注解,文件上传会将数据转换成二进制流,而不是 json 数据,因此无法获取,还会报错。

@RequestHeader

使用 @RequestHeader 映射请求头信息 (了解),属性同 @RequestParam

使用示例

  • Controller

    @RequestMapping("/testRequestHeader")
    public String testRequestHeader(@RequestHeader("Accept-Language") String language){
        System.out.println("testRquestHeader: Accept-Language=" + language);
        return SUCCESS;
    }
    
  • index.jsp

    <a href="springmvc/testRequestHeader">Test RequestHeader</a>
    

@CookieValue

使用 @CookieVallue 映射一个 Cookie 值 (了解)。属性同 @RequestParam

使用示例

  • Controller

    @RequestMapping("/testCookieValue")
    public String testCookieValue(@CookieValue("JSESSIONID") String sessionId){
        System.out.println("CookieValue: JSESSIONID=" + sessionId);
        return SUCCESS;
    }
    
  • index.jsp

    <a href="springmvc/testCookieValue">Test CookieValue</a>
    

映射 POJO 对象

SpringMVC 会按请求参数名和 POJO 属性名进行自动匹配,自动为该对象填充属性值。支持级联属性。如:dept.dId、dept.dName

使用示例

  • POJO

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
    
        private String username;
        private String password;
        private String email;
        private Integer age;
    
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Address {
    
        private String province;
        private String city;
    
    }
    
  • Controller

    @RequestMapping("/testPojo")
    public String testPojo(User user){
        System.out.println("testPojo: User=" + user);
        return SUCCESS;
    }
    
  • Html

    <form action="springmvc/testPojo" method="post">
      username: <input type="text" name="username"/><br/>
      password: <input type="password" name="password"/><br/>
      email: <input type="text" name="email"/><br/>
      age: <input type="text" name="age"/><br/>
      city: <input type="text" name="address.city"/><br/>
      province: <input type="text" name="address.province"/><br/>
      <input type="submit"/>
    </form>
    

使用 Servlet API 作为入参

MVC 的 Handler 方法可以接收的 Servlet API 类型的参数

  • HttpServletRequest
  • HttpServletResponse
  • HttpSession
  • java.security.Principal
  • Locale
  • InputStream
  • OutputStream
  • Read
  • Writer

使用示例

  • Controller

    @RequestMapping("testServletAPI")
    public String testServletAPI(HttpServletRequest request, HttpServletResponse response){
        System.out.println("testServletAPI: " + request + "," + response);
        return SUCCESS;
    }
    
  • Html

    <a href="springmvc/testServletAPI">Test Servelt API</a>
    

处理模型数据

SpringMVC 提供了一下几种途径输出模型数据:

  • ModelAndView:处理方法返回值类型为 ModelAndView时,方法体即可通过该对象添加模型数据。
  • Map 及 Model:入参为 org.springframework.ui.Model、org.springframework.ui.ModelMap 或 java.util.Map,处理方法会在返回时将 Map 中的数据自动添加到模型中。
  • @SessionAttributes:将模型中的某个属性暂存到 HttpSession 中,以便多个请求之间可以共享这个属性。
  • @ModelAttribute:方法入参标注该注解后,入参的对象就会放到数据模型中。

ModelAndView

  • 控制器处理方法的返回值如果为 ModelAndView,则其既包含视图信息,也包含模型数据信息。

  • 添加模型数据:

    ModelAndView addObject(String attributeName, Object attributeValue)
    ModelAndView addAllObject(Map<String, ?> modelMap)
    
  • 设置视图:

    void setView(View view)
    void setViewName(String viewName)
    

使用示例

  • Controller

    @RequestMapping("testModelAndView")
    public ModelAndView testModelAndView(){
        String viewName = SUCCESS;
        // 构造器, 创建ModelAndView对象
        ModelAndView modelAndView = new ModelAndView(viewName);
        // 添加模型数据到 ModelAndView 中
        modelAndView.addObject("time", new Date());
        return modelAndView;
    }
    
  • Html

    <!-- index.js -->
    <a href="springmvc/testModelAndView">Test ModelAndView</a>
    
    <!-- success.jsp -->
    <p>${requestScope.time}</p>
    

Map

  • SpringMVC 在内部使用了有一个 org.springframework.ui.Model 接口存储模型数据
  • 具体步骤:
    • SpringMVC 在调用方法前会创建一个隐含的模型对象作为模型数据的存储容器。
    • 如果方法的入参为 Map 或 Model 类型,SpringMVC 会将隐含模型的引用传递给这些入参。在方法体内,开发者可以通过这个入参对象访问到模型中的所有数据,也可以向模型中添加新的属性数据

使用示例

  • Controller

    @RequestMapping("testMap")
    public String testMap(Map<String, Object> map){
        map.put("names", Arrays.asList("Tom", "Jerry"));
        return SUCCESS;
    }
    
  • Html

    <!-- index.js -->
    <a href="springmvc/testMap">Test Map</a>
    
    <!-- success.jsp -->
    <p>${requestScope.names}</p>
    

目标方法可以添加 Map 类型(实际上也可以是 Model 或 ModelMap 类型)的参数因为 SpringMVC 实际传递给目标方法是 BindingAwareModelMap 类型的参数,Map、Model、ModelMap都是 BindingAwareModelMap 的接口或父类。

Model 及 ModelMap

  • ModelMap是Model接口的实现类

  • 添加模型数据:

    // Model
    Model addAttribute(String attributeName, Object attributeValue);
    Model addAllAttributes(Map<String, ?> map);
    
    // ModelMap
    ModelMap addAttribute(String attributeName, Object attributeValue)
    ModelMap addAllAttributes(Map<String, ?> attributes)
    

使用示例

  • Controller

    @RequestMapping("testModel")
    public String testModel(Model model){
        // 添加模型数据到 Model 中
        model.addAttribute("name", "hong");
        return SUCCESS;
    }
    
    @RequestMapping("testModelMap")
    public String testModelMap(ModelMap modelMap){
        modelMap.addAttribute("framework", "SpringMVC");
        return SUCCESS;
    }
    
  • Html

    <!-- index.jsp -->
    <a href="springmvc/testModel">Test Model</a>
    <br/>
    
    <a href="springmvc/testModelMap">Test ModelMap</a>
    <br/>
    
    <!-- success.jsp -->
    <p>${requestScope.name}</p>
    <p>${requestScope.framework}</p>
    

@SessionAttributes

  • 若希望在多个请求之间公用某个模型属性数据,则可以在控制器上标注一个 @SessionAttributes,SpringMVC 将会把模型中对应的属性暂存到 HttpSession 中。

  • @SessionAttributes 除了可以通过属性名指定需要放到会话中的属性外,还可以通过模型属性的对象类型指定哪些模型属性需要放到会话中。

    @SessionAttributes(types=User.class) // 会将隐含模型中所有类型为 User.class 的属性添加到会话中。
    @SessionAttributes(value={"user1", "user2"}) // 会将隐含模型中名为 user1 的属性添加到会话中。
    @SessionAttributes(types={User.class, Dept.class}) // 会将隐含模型中 User 或 Dept 类型的属性添加到会话中
    @SessionAttributes(value={"user1", "user2"}, types={dept.class}) // 会将隐含模型中名为 user1、user2 或 dept 类型的属性添加到会话中
    

使用示例:

  • Controller

    @Controller
    @SessionAttributes(value = {"username"}, types={Address.class})
    @RequestMapping("springmvc")
    public class SpringMVC_Model {
        @RequestMapping("testSessionAttributes")
        public String testSessionAttributes(Model model){
            model.addAttribute("username", "admin");
            model.addAttribute("address", new Address("湖南", "长沙"));
            System.out.println("testSessionAttributes");
            return SUCCESS;
        }
    }
    
  • Html

    <!-- index.jsp -->
    <a href="springmvc/testSessionAttributes">Test SessionAttributes</a>
    
    <!-- success.jsp -->
    <p>session-username: ${sessionScope.username}</p>
    <p>session-address: ${sessionScope.address}</p>
    
  • 结果

    session-username: admin
    session-address: Address(province=湖南, city=长沙)
    

注意点:

  • @SessionAttributes 注解的 values 和 types 属性的关系是或者 (or) 的关系
  • @SessionAttributes 注解只能放在类上面
  • @SessionAttributes 注解会作用该类的所有方法

@SessionAttribute

@SessionAttribute 放在入参中,用于取出 HttpSession 中的数据,如果不写 @SessionAttribute 注解则会先从 HttpServeltRequest中查找,如果没有再从 HttpSession中取数据。

使用示例:

  • Controller

    @RequestMapping("testSessionAttribute")
    public String testSessionAttribute(Model model, @SessionAttribute("username") String username){
        System.out.println(username);
        return SUCCESS;
    }
    
  • Html

    <a href="springmvc/testSessionAttribute">Test SessionAttribute</a>
    

@ModelAttribute

绑定请求参数到实体对象(表单的命令对象)

将 SpringMVC 自动封装的请求参数添加到请求域中,默认 key 为参数名,也可以指定 key。

@RequestMapping("testModelAttribute")
// user入参标注了@ModelAttribute注解后, 本不该出现在model中的user被SpringMVC放在了model中。
public String testModelAttribute(Model model, @ModelAttribute("wuhu") User user){
    System.out.println(user);
    System.out.println(model.getAttribute("wuhu"));
    return SUCCESS;
}
注解一个非请求处理方法

@ModelAttribute 标注的方法将在每次调用该控制器类的请求处理方法前被调用。这种特性可以用来控制登录权限,当然控制登录权限的方法有很多,例如拦截器、过滤器等。还可以给 POJO 对象进行进一步赋值

  • Controller

    // 被 @ModelAttribute 修饰的方法会在所有目标方法前执行
    // 登录判读, hasUser的返回值会被存放在Model中, 目标方法可以获取到; 根据这个值可以判读是否登录
    @ModelAttribute("hasUser")
    public boolean hasUser(@SessionAttribute(value = "user") User user, required = false){
        if(user != null){
            return true;
        }
        return false;
    }
    
    @RequestMapping("testModelAttribute")
    public String testModelAttribute(@RequestAttribute("hasUser") boolean hasUser){
        return SUCCESS;
    }
    
  • Html

    <form action="springmvc/testModelAttribute" method="post">
      <input type="hidden" name="id" value="1"/><br/>
      <input type="text" name="username" value="Tom"/><br/>
      <input type="password" name="password" value="123456"/><br/>
      <input type="submit" value="登录"/>
    </form>
    

请求转发 AND 重定向

  • 一般情况下,控制器方法返回字符串类型的值会被当成逻辑视图名处理
  • 如果返回的字符串中带了 forward: 或 redirect: 前缀时,SpringMVC 会对他们进行特殊处理:将 forward: 和 redirect: 当成指示符,其后的的字符串作为 URL 来处理,因此不会拼接视图解析器的前缀和后缀

forward转发

Controller 方法在提供了 String 类型的返回值之后,默认就是请求转发。
它相当于request.getRequestDispatcher("url").forward(request,response)
使用请求转发,既可以转发到 jsp,也可以转发到其他的控制器方法。

  • Controller

    @RequestMapping("/testForward")
    public String testForward(){
        System.out.println("testForward方法执行了");
        return "forward:/WEB-INF/pages/success.jsp";
    }
    
  • jsp

    <a href="user/testForward">testForward</a><br>
    

注意:如果用了forward:,则路径必须写成实际视图 url,不能写逻辑视图。

redirect重定向

controller 方法提供了一个 String 类型返回值之后,它需要在返回值里使用redirect:
它相当于response.sendRedirect(url)如果是重定向到 jsp 页面,则 jsp 页面不能写在 WEB-INF 目录中,否则无法找到。

  • Controller

    @RequestMapping("/testRedirect")
    public String testRedirect(){
        System.out.println("testRedirect方法执行了");
        return "redirect:/redirect.jsp";
    }
    
  • jsp

    <a href="user/testRedirect">testRedirect</a><br>
    

注意:如果用了Redirect:,则路径必须写成实际视图 url,不能写逻辑视图。

RESTful SpringMVC CRUD

使用 SpringMVC 实现 RESTful 风格的 CRUD;本次示例不连接数据库,数据为静态数据。

  • springmvc.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    
        <!-- 配置组件扫描 -->
        <context:component-scan base-package="org.hong"></context:component-scan>
    
        <!-- 配置视图解析器: 如何把controller方法返回值解析为实际的物理视图 -->
        <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <property name="prefix" value="/WEB-INF/views/"/>
            <property name="suffix" value=".jsp"/>
        </bean>
    
    </beans>
    
  • web.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
             version="4.0">
    
        <!-- 实现RESTful的重点Filter -->
        <!-- 配置 org.springframework.web.filter.HiddenHttpMethodFilter: 可以把 POST 请求转为 DELETE 或 PUT 请求 -->
        <filter>
            <filter-name>HiddenHttpMethodFilter</filter-name>
            <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
        </filter>
        <filter-mapping>
            <filter-name>HiddenHttpMethodFilter</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
    
        <!-- 配置DispatcherServlet -->
        <servlet>
            <servlet-name>DispatcherServlet</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
            <!-- 配置DispatcherServlet的一个初始化参数: 配置SpringMVC配置文件的位置和名称 -->
            <!--
              实际上也可以不通过contextConfigLocation来配置SpringMVC配置文件的位置和名称, 而使用默认的。
              默认的配置文件为: /WEB-INF/<servlet-name>-servlet.xml
            -->
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>classpath:springmvc.xml</param-value>
            </init-param>
            <!-- 配置此DispatcherServlet的启动级别 -->
            <load-on-startup>1</load-on-startup>
        </servlet>
    
        <servlet-mapping>
            <servlet-name>DispatcherServlet</servlet-name>
            <!--
                在SpringMVC中:
                /: 只匹配所有的请求, 不会匹配jsp页面
                /*: 匹配所有的请求, 包括jsp页面
            -->
            <url-pattern>/</url-pattern>
        </servlet-mapping>
    
    </web-app>
    
  • POJO

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
    
        private Integer id;
        private String username;
        private String password;
        private Date birth;
    
    }
    
  • Dao

    @Component
    public class UserDao {
    
        private static List<User> list = new ArrayList<User>();
        static{
            try {
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
                list.add(new User(1, "Tom", "123456", sdf.parse("2003-8-5")));
                list.add(new User(2, "Jerry", "123456", sdf.parse("2000-3-5")));
                list.add(new User(3, "Yahu", "123456", sdf.parse("2000-3-5")));
                list.add(new User(4, "Admin", "123456", sdf.parse("2000-3-5")));
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    
    }
    
  • Controller

    @Controller
    public class UserController {
    
        @Autowired
        private UserDao userDao;
    
    }
    

GET ( 查询所有User )

  • Dao

    public List<User> getUserAll(){
        return list;
    }
    
  • Controller

    @GetMapping("/users")
    public String getUserAll(ModelMap map){
        map.addAttribute("userAll", userDao.getUserAll());
        return "forward:index.jsp";
    }
    
  • index.jsp

    <%--
      Created by IntelliJ IDEA.
      User: Administrator
      Date: 2021-02-12
      Time: 16:49
      To change this template use File | Settings | File Templates.
    --%>
    <%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true" %>
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
    <html>
      <head>
        <title>$Title$</title>
      </head>
      <body>
        <form action="" method="POST">
          <input type="hidden" name="_method" value="DELETE"/>
        </form>
    
        <table align="center" width="400px" border="1px" cellspacing="0">
          <caption>
            <h2>用户信息</h2>
            <a href="addUser.jsp">添加用户</a>
          </caption>
          <tr>
            <th>编号</th>
            <th>用户名</th>
            <th>密码</th>
            <th>生日</th>
            <th>操作</th>
          </tr>
          <c:forEach items="${requestScope.userAll}" var="user">
            <tr align="center">
              <td>${user.id}</td>
              <td>${user.username}</td>
              <td>${user.password}</td>
              <td>
                <fmt:formatDate value="${user.birth}" pattern="yyyy-MM-dd" ></fmt:formatDate>
              </td>
              <td>
                <a href="user/${user.id}" class="update">修改</a>
                <!-- a标签无法发送post请求, 因此我们需要借助js -->
                <a href="user/${user.id}" class="delete">删除</a>
              </td>
            </tr>
          </c:forEach>
        </table>
      </body>
    </html>
    

DELETE ( 删除User )

  • Dao

    public void deleteUserById(Integer id) {
        for (int i = 0; i < list.size(); i++) {
            if(list.get(i).getId() == id){
                list.remove(i);
            }
        }
    }
    
  • Controller

    @DeleteMapping("/user/{id}")
    public String deleteUser(@PathVariable("id") Integer id){
        userDao.deleteUserById(id);
        return "redirect:/users";
    }
    
  • index.jsp

    <!-- 引入jquery -->
    <script src="jquery.js"></script>
    <script type="text/javascript">
        $(function () {
        // 测试 jquery 是否引入成功; 结果发现 jquery 并没有生效
    	/*
         * 1.原因: 优雅的 RESTful 风格的 URL 不希望带 .html 或 .do 等后缀, 若将 DispatcherServlet 请求映射配置为 /, 则 SpringMVC 将捕获 WEB 容器的所有请求, 包括静态资源,
         * SpringMVC 会将他们当成一个普通的请求处理, 而这个请求我们并没有进行过映射, 因此将找不到对应的处理器而报错。
         *
         * 2.解决: 可以在 SpringMVC 的配置文件中配置 <mvc:default-servlet-handler/> 的方式解决静态资源的问题
         */
        //alert("hello,jquery.")
        $(".delete").click(function () {
          var href = $(this).attr("href");
          $("form").attr("action", href).submit();
          return false;
        });
      });
    </script>
    
  • springmvc.xml

    <!--
        <mvc:default-servlet-handler/> 将在 SpringMVC 上下文中定义一个 DefaultServletHttpRequestHandler,
        它会对进入 DispatcherServlet 的请求进行筛选, 如果发现时没有经过映射的请求, 就将该请求交给 WEB 应用服务器默认的 Servlet 处理,
        如果不是静态资源的请求, 才由 DispatcherServlet 进行处理。
    
        一般 WEB 应用服务器默认的 Servlet 名称都是 default。若使用的 WEB 服务器的默认 Servlet 名称不是 default, 则需要通过 default-servlet-name 属性显示指定名称
    -->
    <mvc:default-servlet-handler/>
    
    <!-- 
    	当配置了 <mvc:default-servlet-handler/> 后, 访问 Controller 将是 404, 配置如下 <mvc:annotation-driven></mvc:annotation-driven> 可以解决。
    	还将提供已下支持: 
     		支持 ConversionService 示例对表单参数进行类型转换
    		支持使用 @NumberFormat、@DateTimeFormat 注解完成数据类型的格式化
    		支持 @RequestBody 和 @ResponseBody 注解
    -->
    <mvc:annotation-driven></mvc:annotation-driven>
    

POST ( 添加User )

  • Dao

    public void addUser(User user) {
        user.setId(list.size());
        list.add(user);
    }
    
  • Controller

    @PostMapping("/user")
    public String addUser(User user){
        userDao.addUser(user);
        return "redirect:users";
    }
    
  • addUser.jsp

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
        <title>Title</title>
    </head>
    <body>
    <form action="user" method="post">
        用户名: <input type="text" name="username"/><br/>
        密码: <input type="text" name="password"/><br/>
        生日: <input type="text" name="birth"/><br/>
        <input type="submit"/>
    </form>
    </body>
    </html>
    
  • 注意: 实际运行发现 birth 字段并不能正确的赋值,原因是 SpringMVC 在对 String 与 Date 转换失败。 因为 SpringMVC 不知道转换格式。

  • 解决方案:

    • 在需要进行格式化的字段上添加 @DateTimeFormat注解并指定 pattern( 转换格式 ) 属性

    • POJO

      @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class User {
      
          private Integer id;
          private String username;
          private String password;
          @DateTimeFormat(pattern = "yyyy-MM-dd")
          private Date birth;
      
      }
      

PUT ( 修改User )

回显数据

  • Dao

    public User getUserById(Integer id) {
        User user = null;
        for (int i = 0; i < list.size(); i++) {
            if(list.get(i).getId() == id){
                user = list.get(i);
            }
        }
        return user;
    }
    
  • Controller

    @GetMapping("/user/{id}")
    public String getUserById(@PathVariable Integer id, ModelMap map){
        map.addAttribute("user", userDao.getUserById(id));
        return "forward:/editUser.jsp";
    }
    
  • edit.jsp

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
        <title>Title</title>
    </head>
    <body>
    <form action="${pageContext.request.contextPath}/user" method="post">
        <input type="hidden" name="_method" value="PUT"/>
        <input type="hidden" name="id" value="${requestScope.user.id}"/>
        用户名: <input type="text" name="username" value="${requestScope.user.username}"/><br/>
        密码: <input type="text" name="password" value="${requestScope.user.password}"/><br/>
        生日: <input type="text" name="birth" value='<fmt:formatDate value="${requestScope.user.birth}" pattern="yyyy-MM-dd"></fmt:formatDate>'/><br/>
        <input type="submit"/>
    </form>
    </body>
    </html>
    

修改操作

  • Dao

    public void updateUserById(User user) {
        for (int i = 0; i < list.size(); i++) {
            if(list.get(i).getId() == user.getId()){
                list.set(i, user);
            }
        }
    }
    
  • Controller

    @PutMapping("/user")
    public String updateUserById(User user){
        userDao.updateUserById(user);
        return "redirect:/users";
    }
    

数据格式化 & 数据校验

数据格式化 & 数据校验在使用之前都需要配置 <mvc:annotation-driven/> 。都是在 POJO 类中的属性上添加注解。

数据格式化

如果 SpringMVC 接收的参数为 Date 类型且不进行任何的处理,SpringMVC 是无法接收这个数据的,因为 SpringMVC 并不知道应该用何种数据格式进行转换。

  • @DateTimeFormat

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private Integer id;
        private String username;
        private String password;
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        private Date birth;
    }
    
  • @NumberFormat

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private Integer id;
        private String username;
        private String password;
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        private Date birth;
        // 允许前端输入 1000,0000.00 格式的数字并且能正确解析
        @NumberFormat(pattern = "####,####.##")
        private double salary;
    }
    

数据校验

JSR 303
  • JSR 303JavaBean 数据合法校验提供的标注框架,它已经包含在 JavaEE 6.0

  • JSR 303 通过在 Bean 属性上标注类似于 @NotNull@Max 等标注的注解指定校准规则,并通过标注的验证接口对Bean进行验证

    注解功能说明
    @Null被注释的元素必须为 Null
    @NotNull被注释的元素必须不为 Null
    @NotBlank(message =)验证字符串非null,且长度必须大于0
    @Length(min=,max=)被注释的字符串的大小必须在指定的范围内
    @AssertTrue被注释的元素必须为 true
    @AsserFalse被注释的元素必须为 false
    @Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
    @Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
    @DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值,允许接受的范围更大
    @DecimalMax(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值,允许接受的范围更大
    @Size(max, min)被注释的元素的大小必须在指定的范围内 (ps: 字符串的长度、集合的大小)
    @Digits(integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内 ( PS: integer为整数的长度,fraction为小数的长度 )
    @Past被注释的元素必须是一个过去的日期
    @Future被注释的元素必须是一个将来的日期
    @Pattern(value)被注释的元素必须符合指定的正则表达式,不适用于数字类型,不需要 // 例如:^[a-z]$
Hibernate Validator 扩展注解
  • Hibernate ValidatorJSR 303 的一个参考实现,除支持所有标注的校验注解外,他还支持已下的扩展注解

    注解功能说明
    @Email被注释的元素必须是电子邮箱地址
    @Length被注释的字符串的大小必须在指定的范围内
    @NotEmpty被注释的字符串的值必须非空
    @Range被注释的元素必须在合适的范围内
SpringMVC 数据校验
  • Spring 本身并没有提供 JSR 303 的实现,所有必须导入 JSR 303 的实现者的 jar 包
  • Spring 容器中注册 LocalValidatorFactroyBean配置 <mvc:annotation-driven/> 将自动注册
  • POJO类 的属性上添加对应的注解
  • 在目标方法的入参 pojo 前添加 @Valid 注解
Maven
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.1.0.Final</version>
</dependency>
<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.el</artifactId>
    <version>3.0.1-b11</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator-cdi</artifactId>
    <version>6.1.0.Final</version>
</dependency>
<!-- SpringBoot -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
POJO
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;
    private String username;
    private String password;
    @Past // 出生日期一定是一个过去的日期
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birth;
    @Min(1) // 工资应该是大于0的数字
    private double salary;
}
Controller
@PostMapping("/user")
public String addUser(@Valid User user, BindingResult result){
    // 判读是否有校验错误
    if(result.hasErrors()){
        HashMap<String, String> map = new HashMap<>();
        // 获取校验的错误结果
        result.getFieldErrors().forEach(item -> {
            // 获取错误提示
            String message = item.getDefaultMessage();
            // 获取错误属性的属性名
            String field = item.getField();
            map.put(field, message);
        });
    	System.out.println("错误信息:" + map);
    }else{
    	System.out.println(user);
    	userDao.addUser(user);
    }
    return "redirect:users";
}
分组校验

需求:添加操作不能携带ID,修改操作必须携带ID

POJO
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    // message: 提示信息
    // groups: 分组, 传入 Class 对象作为标识
    @Null(message = "新增不能指定ID", groups = {AddGroup.class})
	@NotNull(message = "修改必须指定ID", groups = {UpdateGroup.class})
    private Integer id;
    private String username;
    private String password;
    @Past // 出生日期一定是一个过去的日期
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birth;
    @Min(1) // 工资应该是大于0的数字
    private double salary;
}
Group
AddGroup
public interface AddGroup {
}
UpdateGroup
public interface UpdateGroup {
}
Controller

不再使用 Java 提供的 @Valid 注解,而是使用 Spring 提供的 @Validated 注解,并指定分组;两个注解同时使用只有 @Validated 注解生效

注意查看上面的校验注解中,只给 id 属性添加了分组

默认没有指定分组的校验注解,在分组校验的清空下不会生效。

@PostMapping("/user")
public String addUser(@Validated({AddGroup.class}) User user, BindingResult result){
    // 判读是否有校验错误
    if(result.hasErrors()){
        HashMap<String, String> map = new HashMap<>();
        // 获取校验的错误结果
        result.getFieldErrors().forEach(item -> {
            // 获取错误提示
            String message = item.getDefaultMessage();
            // 获取错误属性的属性名
            String field = item.getField();
            map.put(field, message);
        });
    	System.out.println("错误信息:" + map);
    }else{
    	System.out.println(user);
    	userDao.addUser(user);
    }
    return "redirect:users";
}
Default 接口

默认Jakarta Bean验证组。 除非明确定义了组列表。

场景:不管是添加还是修改,都应该携带 salary 等其他字段字段,但是给这个字段同时添加 AddGroupUpdateGroup 两个分组太臃肿了。

POJO
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User { // 我们不需要显示的制定Default分组, 在不添加分组的情况下, 默认就是Default分组
    // message: 提示信息
    // groups: 分组, 传入 Class 对象作为标识
    @Null(message = "新增不能指定ID", groups = {AddGroup.class})
	@NotNull(message = "修改必须指定ID", groups = {UpdateGroup.class})
    private Integer id;
    private String username;
    private String password;
    @Past // 出生日期一定是一个过去的日期
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birth;
    @Min(1) // 工资应该是大于0的数字
    private double salary;
}
Controller
@PostMapping("/user")
public String addUser(@Validated({AddGroup.class, Default.class}) User user, BindingResult result){
    // 判读是否有校验错误
    if(result.hasErrors()){
        HashMap<String, String> map = new HashMap<>();
        // 获取校验的错误结果
        result.getFieldErrors().forEach(item -> {
            // 获取错误提示
            String message = item.getDefaultMessage();
            // 获取错误属性的属性名
            String field = item.getField();
            map.put(field, message);
        });
    	System.out.println("错误信息:" + map);
    }else{
    	System.out.println(user);
    	userDao.addUser(user);
    }
    return "redirect:users";
}
自定义校验
  1. 编写一个自定义的校验注解
  2. 编写一个自定义的校验器
  3. 关联自定义的校验器和自定义的校验注解
自定义校验注解
package org.hong.common.valid;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class }) // 绑定当前校验注解对应的校验器, 可以绑定多个校验器
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
// message、groups、payload这3个属性是作为校验注解所必须的属性, 这是规范
public @interface ListValue {
    // 绑定一个默认的提示信息, 默认从当前项目的ValidationMessages.properties文件中获取
    // 命名规范: 校验注解全类名+message
    String message() default "{org.hong.common.valid.ListValue.message}"; 

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    int [] value() default {};
}
自定义校验器

一个校验器指定校验一中类型的数据,如果需要一个注解能对多种类型进行校验需要编写多个校验器

package org.hong.common.valid;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;

// ConstraintValidator<A extends Annotation, T>: A-指定注解, T-指定校验数据的类型
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {

    private Set<Integer> set = new HashSet<>();

    /**
     * 初始化方法
     * @param constraintAnnotation 注解的详细信息都在这个对象中
     */
    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] value = constraintAnnotation.value();
        for (int val : value) {
            set.add(val);
        }
    }

    /**
     * 判断是否校验成功
     * @param value 需要校验的值
     * @param context 校验的上下文环境信息
     * @return
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        return set.contains(value);
    }
}
ValidationMessages.properties

编写这个文件的时候不要出现空格和换行,否则可能出现乱码的清空;或者直接写Unicode

org.hong.common.valid.ListValue.message=数据不在指定集合中
POJO
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    // message: 提示信息
    // groups: 分组, 传入 Class 对象作为标识
    @Null(message = "新增不能指定ID", groups = {AddGroup.class})
	@NotNull(message = "修改必须指定ID", groups = {UpdateGroup.class})
    private Integer id;
    private String username;
    private String password;
    @Past // 出生日期一定是一个过去的日期
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birth;
    @Min(1) // 工资应该是大于0的数字
    private double salary;
    @ListValue(values = {0, 1}, groups = {AddGroup.class, UpdateGroup.class})
    private Integer status; // 0: 删除, 1: 未被删除; 要求这个字段只能是0或1
}
Controller
@PostMapping("/user")
public String addUser(@Validated({AddGroup.class}) User user, BindingResult result){
    // 判读是否有校验错误
    if(result.hasErrors()){
        HashMap<String, String> map = new HashMap<>();
        // 获取校验的错误结果
        result.getFieldErrors().forEach(item -> {
            // 获取错误提示
            String message = item.getDefaultMessage();
            // 获取错误属性的属性名
            String field = item.getField();
            map.put(field, message);
        });
    	System.out.println("错误信息:" + map);
    }else{
    	System.out.println(user);
    	userDao.addUser(user);
    }
    return "redirect:users";
}

响应 JSON 数据

  1. 加入 Jackson required包

  2. 编写目标方法,使其返回 JSON 对应的对象或集合

  3. 在方法上添加 @ResponseBody 注解

  4. 如果控制器的所有方法都是返回 JSON 数据可以在类上添加 @RestController 注解,则所有方法都将返回 JSON 数据,简化开发

  • Maven

    <!--Jackson required包-->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
      <version>2.12.1</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.12.1</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-annotations</artifactId>
      <version>2.12.1</version>
    </dependency>
    
  • Controller

    @ResponseBody
    @RequestMapping("/testJson")
    public List<User> testJson(){
        return userDao.getUserAll();
    }
    
  • Jsp

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
      <head>
        <title>$Title$</title>
        <script src="jquery.js"></script>
        <script>
          $(function () {
            $("#testJson").click(function () {
              var url = $(this).attr("href");
              $.post(url, function (data) {
                console.log(data);
              }, "json");
              return false;
            });
          });
        </script>
      </head>
      <body>
        <a href="testJson" id="testJson">Test Json</a>
      </body>
    </html>
    

当返回的数据值是级联对象时,jackson 在工作时可能会报错,在类上添加如下注解解决

// 忽略序列化时一些问题
@JsonIgnoreProperties(value = { "handler" })
// 忽略不存在的字段
@JsonIgnoreProperties(ignoreUnknown = true)

响应出现乱码如解决?

springmvc.xml

<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>

文件上传

准备工作

文件上传是项目开发中最常见的功能之一 ,SpringMVC 可以很好的支持文件上传,但是 SpringMVC 上下文中默认没有装配MultipartResolver,因此默认情况下其不能处理文件上传工作。如果想使用 Spring 的文件上传功能,则需要在上下文中配置MultipartResolver

前端表单要求:为了能上传文件,必须将表单的 method设置为 POST,并将 enctype设置为 multipart/form-data。只有在这样的情况下,浏览器才会把用户选择的文件以二进制数据发送给服务器;

对表单中的 enctype 属性做个详细的说明:

  • application/x-www=form-urlencoded:默认方式,只处理表单域中的 value 属性值,采用这种编码方式的表单会将表单域中的值处理成 URL 编码方式。
  • multipart/form-data:这种编码方式会以二进制流的方式来处理表单数据,这种编码方式会把文件域指定文件的内容也封装到请求参数中,不会对字符编码。
  • text/plain:除了把空格转换为 “+” 号外,其他字符都不做编码处理,这种方式适用直接通过表单发送邮件。
<form action="" enctype="multipart/form-data" method="post">
   <input type="file" name="file"/>
   <input type="submit">
</form>

一旦设置了 enctype为multipart/form-data,浏览器即会采用二进制流的方式来处理表单数据,而对于文件上传的处理则涉及在服务器端解析原始的HTTP响应。在2003年,Apache Software Foundation 发布了开源的 Commons FileUpload 组件,其很快成为 Servlet/JSP 程序员上传文件的最佳选择。

  • Servlet3.0 规范已经提供方法来处理文件上传,但这种上传需要在 Servlet中完成。
  • SpringMVC 则提供了更简单的封装。
  • SpringMVC 为文件上传提供了直接的支持,这种支持是用即插即用的 MultipartResolver 实现的。
  • SpringMVC 使用 Apache Commons FileUpload 技术实现了一个 MultipartResolver 实现类:
  • CommonsMultipartResolver。因此,SpringMVC 的文件上传还需要依赖 Apache Commons FileUpload 的组件。

文件上传

  • Maven

    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.3.3</version>
    </dependency>
    
  • springmvc.xml

    注意!!!这个bena的id必须为:multipartResolver , 否则上传文件会报400的错误!在这里栽过坑,教训!

    <!--文件上传配置-->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
       <!-- 请求的编码格式,必须和jSP的pageEncoding属性一致,以便正确读取表单的内容,默认为ISO-8859-1 -->
       <property name="defaultEncoding" value="utf-8"/>
       <!-- 上传文件大小上限,单位为字节(10485760=10M) -->
       <property name="maxUploadSize" value="10485760"/>
       <property name="maxInMemorySize" value="40960"/>
    </bean>
    
  • index.jsp

    <form action="testMultipartResovler" method="post" enctype="multipart/form-data">
      File: <input type="file" name="file"/>
      <input type="submit"/>
    </form>
    
  • Controller

    @RequestMapping("upload")
    // @RequestParam("file") 将 name=file 控件得到的文件封装成 CommonsMultipartFile 对象
    // 批量上传 CommonsMultipartFile 则为数组即可
    public String  fileUpload2(@RequestPart("file") CommonsMultipartFile file, HttpServletRequest request) throws IOException {
    
        //上传路径保存设置
        String path = request.getSession().getServletContext().getRealPath("/upload");
        File realPath = new File(path);
        if (!realPath.exists()){
            realPath.mkdir();
        }
        //上传文件地址
        System.out.println("上传文件保存地址:"+realPath);
    
        //通过CommonsMultipartFile的方法直接写文件
        String fileName = file.getOriginalFilename();
        file.transferTo(new File(realPath +"/"+ UUID.randomUUID() + fileName.substring(fileName.lastIndexOf('.'))));
    
        return "redirect:file.html";
    }
    

拦截器

自定义拦截器

  • SpringMVC 可以使用拦截器对请求进行拦截处理,用户可以自定义拦截器类实现特定的功能,自定义的拦截器必须实现 HandlerInterceptor 接口SpringMVC 的拦截器是在 DispatcherServlet 调用的。

    • **preHandler():**这个方法在业务处理器处理请求之前被调用,在该方法中对用户请求 request 进行处理。如果程序员决定改拦截器对请求进行拦截处理后还要调用其他的拦截器,或者时业务处理器取进行处理,则返回 true如果程序员决定不需要再调用其他的组件去处理请求,则返回 false。
    • **postHandle():**这个方法在业务处理器处理完请求后,但是 DispatcherServlet 向客户端返回响应前被调用,在该方法中对用户请求 request 进行处理
    • **afterCompletion():**这个方法在 DispatcherServlet 完全处理完请求后被调用,可以在该方法中进行一些资源清理的操作。
  • 创建一个类实现HandlerInterceptor接口

    public class UserFilter implements HandlerInterceptor {}
    
  • 重写HandlerInterceptor接口的默认方法

    // return true; 则继续调用后续的拦截器和目标方法
    // return false; 则不会再调用后续的拦截器和目标方法
    // 通常重写preHandle方法实现拦截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("1.处理前");
        return true;
    }
    
    // 日志
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("2.处理后");
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("3.清理");
    }
    
  • xml配置

    <!-- 拦截器清理 -->
    <mvc:interceptors>
        <mvc:interceptor>
            <!-- **: 包括 /pages/user/ 请求下面的所有请求 -->
            <mvc:mapping path="/springmvc/**"/>
            <bean class="rog.hong.interceptors.FirstInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>
    

执行顺序

  • 假设有 FirstInterceptor 和 SecondInterceptor 过滤器,且它们的配置顺序如下

    <mvc:interceptors>
        <!-- FirstInterceptor: 作用与所有的控制器方法 -->
        <bean class="rog.hong.interceptors.FirstInterceptor"/>
        
        <!-- SecondInterceptor: 指定了作用域 -->
        <mvc:interceptor>
            <mvc:mapping path="/springmvc/**"/>
            <bean class="rog.hong.interceptors.SecondInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>
    
  • 执行顺序

异常处理

制造异常

  • Controller

    @RequestMapping("/testException")
    public String testException() {
        // 手动制造一个异常
        System.out.println(10/0);
        return "success";
    }
    
  • index.jsp

    <a href="springmvc/testException">Test Exception</a>
    
  • 这是一个简单 SpringMVC 示例,在我们点击超链接后浏览器会报一个 500 的运行时异常,这明显不是我们需要的效果,SpringMVC 针对于异常提供了非常简单的操作。

SpringMVC 的异常处理

@ExceptionHandler ( 局部 )

在方法上使用 @ExceptionHandler 注解标记此方法为处理异常的处理方法,这个方法只会处理当前类的异常

  • Controller

    // @ExceptionHandler 注解只有一个 value 属性, 用于指定处理异常类型, value是一个数组
    @ExceptionHandler({ArithmeticException.class})
    public String testExceptionHandler(Exception ex){
        System.out.println("Exception:" + ex);
        return "error";
    }
    
  • error.jsp

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
      <head>
        <title>$Title$</title>
      </head>
      <body>
        <a href="springmvc/testException">Test Exception</a>
      </body>
    </html>
    
  • 再次运行,点击超链接本该出现的 500 异常不再出现,而是跳转到了 error.jsp 页面

@ControllerAdvice ( 全局 )

使用 @ExceptionHandler 注解创建的异常处理方法只能处理当前类的异常,能不能将异常处理方法注册为全局的呢?SpringMVC 提供了 @ControllerAdvice 来实现此功能。效果与上面一样。

@ControllerAdvice
public class ExceptionController {
    @ExceptionHandler({ArithmeticException.class})
    public String testExceptionHandler(Exception ex, Model model){
        System.out.println("Exception-->:" + ex);
        model.addAttribute("exception", ex);
        return "error";
    }
}

小细节

  • @ExceptionHandler 标记的方法不能在入参中传入 Map,但是可以传入 Model、ModelMap 将异常对象传递给前端
  • **优先级:**如果有多个异常处理器,比如:( Exception、ArithmeticException ),SpringMVC 将会进入哪个异常处理器?
    • SpringMVC 会进入更接近本次异常对象的异常处理方法
    • 在异常处理器不报错的情况下只会进入一个异常处理方法,与 Java 的 try-catch 类似
  • 如果同时存在全局异常处理和局部异常处理,且都可以处理 ArithmeticException SpringMVC 将会执行哪个异常处理器?
    • 首先会在出现异常的类中查找异常处理器,找到则调用;
    • 如果没有找到,则到全局异常处理中查找,找到则调用;
    • 如果都没找到,则抛出异常,服务器 500。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值