# 路由&请求参数绑定

# RESTful

RESTful是一个资源定位及资源操作的风格。使用POST、DELETE、PUT、GET,使用不同方法对资源进行操作,分别对应 添加、 删除、修改、查询

需求:RESTful方式实现商品信息查询,返回json数据

从URL上获取参数:根据id查询商品,使用RESTful风格开发的接口地址是:http://127.0.0.1/item/1

  • 注解@RequestMapping("item/{id}")声明请求的URL,{xxx}为占位符,请求的URL是“item /1

  • 使用@PathVariable() Integer id获取URL上的数据

    @RequestMapping("item/{id}")
    public @ResponseBody Item queryItemById(@PathVariable Integer id) {
        Item item = this.itemService.queryItemById(id);
        return item;
    }
    
    • 如果@RequestMapping中表示为"item/{id}",id和形参名称一致,@PathVariable不用指定名称。如果不一致,例如"item/{ItemId}"则需要指定名称@PathVariable("itemId")
  • 注意

    • @PathVariable是获取url上数据的。@RequestParam获取请求参数的(包括post表单提交)
    • 如果加上@ResponseBody注解,就不会走视图解析器,不会返回页面,返回如json数据。如果不加,就走视图解析器,返回页面
  • 注意:
    • 表单只支持GET、POST请求,若要发送其他请求(一般也不会使用表单发送其他请求),表单本身设置为POST请求,并需要input中属性name="_method" value="PUT"
    • 后端中需要配置过滤器org.springframework.web.filter.HiddenHttpMethodFilter
    • Ajax 支持其他类型的请求

# 路由

# @Controller

# @RestController 🔥

@Controller@ResponseBody的组合

使用 @ResponseBody 会自动设置如下代码:

  • 设置 ContentType、MIME Type。如使用 UTF-8 解决乱码

    response.setContentType("application/json;charset=utf-8");// 返回对象为 Object 时
    response.setContentType("text/plain;charset=utf-8");// 返回对象为 String 时
    
  • 序列化、调用 response 的输出方法

    response.getWriter().write("hello 测试2");// 若输出对象,则省掉手动序列号操作
    

# @RequestMapping

封装了 Servlet 的 url-partten

作用:用于建立请求 URL处理请求方法之间的对应关系所有请求方法都会经过此路由

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
    String name() default "";

    @AliasFor("path")
    String[] value() default {};
    @AliasFor("value")
    String[] path() default {};

    RequestMethod[] method() default {};
    String[] params() default {};
    String[] headers() default {};
    String[] consumes() default {};
    String[] produces() default {};
}

出现位置:

  • 上:请求URL的第一级访问目录,模块化管理。此处不写的话,就相当于应用的根目录。写的话需要以/开头,如/user

  • 方法上:请求 URL 的第二级访问目录。 如/add。但是RESTful风格的可以使用@**Mapping替代

    当使用分级配置时,前端页面的请求路径中,要么写绝对路径即带/和项目名,要么写相对路径不带/(表示相对应用的根目录),否则404

属性:

  • value:用于指定请求的 URL数组。它和 path 属性的作用是一样的。大小写敏感

  • method:用于指定请求的方式,值为RequestMethod枚举类数组

    可以有多个请求方式映射一个方法,但是下面的简写注解不可以在同一个方法中使用多个,否则只有最先写的起作用

  • params:用于指定限制请求参数的条件。它支持简单的表达式。要求请求参数的key和value必须和配置的一模一样。如:

    • params = {"accountName"},表示请求参数必须有 accountName
    • params = {"moeny!100"},表示请求参数中 money 不能是 100
  • headers:用于限定发送的请求中必须包含某请求头

# @GetMapping

用在方法上,替代方法的@RequestMapping

# @PostMapping

用在方法上,替代方法的@RequestMapping

# @PutMapping

用在方法上,替代方法的@RequestMapping

# @DeleteMapping

用在方法上,替代方法的@RequestMapping

# @PatchMapping

用在方法上,替代方法的@RequestMapping。部分更新

# Url 参数

# 经典— @RequestParam

用于映射请求的参数名处理器形参名, 当二者一致时(区分大小写)可省略该注解!

注意:

  • 🔥参数类型推荐使用包装数据类型,因为基础数据类型不可为null;
  • 🔥Boolean 布尔类型的参数,请求的参数值可以为 true 或 false,1 或 0,接收后自动映射为布尔类型

属性:

  • valuename:请求参数中的名称。若参数名称和形参一致,可以不用指定 value 或 name
  • required:请求参数中是否必须提供此参数。默认值:true。表示必须提供,如果不提供将报错
  • defaultValue:请求参数默认值

Postman 请求如下(直接在 Params 中写 Query Params即可):

GET /v1/test/url2?name=conanan&flag=1 HTTP/1.1
Host: http://127.0.0.1:80
Content-Type: application/x-www-form-urlencoded

和 Content-Type: application/x-www-form-urlencoded 没有关系,默认就是该类型,只需注意?后的键值对即可

Java 后台可以如下方式接收(只需切换下 url 即可)

@GetMapping("/url1")
public String url1(String name, Boolean flag){
    return String.format("url1: %s, %s", name, flag);
}

// 可省略 @RequestParam
@GetMapping("/url2")
public String url2(@RequestParam String name, Boolean flag){
    return String.format("url2: %s, %s", name, flag);
}

# RESTful — @PathVariable

用于绑定 url 中的占位符。url 支持占位符是 spring3.0 之后加入的。是 springmvc 支持 RESTful 风格 URL 的一个重要标志。

注意:

  • 🔥参数类型推荐使用包装数据类型,因为基础数据类型不可为null;但这里一般不会出现/rest//1情况
  • 🔥Boolean 布尔类型的参数,请求的参数值可以为 true 或 false,1 或 0,接收后自动映射为布尔类型

属性:

  • valuename:用于指定 url 中占位符名称。若占位符名称和形参一致,可以不用指定value。
  • required:是否必须提供占位符。

Postman 请求如下(直接在 Params 中写 Query Params即可):

GET /v1/test/rest/conanan/1 HTTP/1.1
Host: http://127.0.0.1:80
Content-Type: application/x-www-form-urlencoded

和 Content-Type: application/x-www-form-urlencoded 没有关系,默认就是该类型

Java 后台可以如下方式接收(只需切换下 url 即可)

@GetMapping("/rest/{name}/{flag}")
public String rest1(@PathVariable String name,
                    @PathVariable Boolean flag){
    return String.format("url2: %s, %s", name, flag);
}

# Body 参数

注意

GET 请求没有 Body 参数

# GET 表单—同 @RequestParam

表单参数绑定:

  • 表单提交的数据都是k=v格式的 username=haha&password=123key就是表单中name的值。GET 请求将数据放入 Url 的?
  • SpringMVC 的参数绑定过程是把表单提交的请求参数,作为控制器中方法的参数进行绑定的
  • 其他和经典@RequestParam一致!!!

注意

需要注意的是,Postman 中 Body 选择 x-www-form-urlencoded,且方法为 GET 的不是真正的 GET 表单提交,它不会把键值对放入?后!!!所以还是推荐使用@RequestParam时采用 Query Params 即可

# POST 表单—直接 POJO 接收

表单参数绑定:

  • 表单提交的数据都是k=v格式的 username=haha&password=123key就是表单中name的值。POST 请求将数据放入请求体

  • SpringMVC 的参数绑定过程是把表单提交的请求参数,作为控制器中方法的参数进行绑定的

  • 要求表单中参数名称和 POJO 类的属性名称保持一致。并且控制器方法的参数类型是 POJO 类型

  • 给 List 集合中的元素赋值,使用下标

  • 给 Map 集合中的元素赋值,使用键值对

Postman 请求如下(在 Body 中 x-www-form-urlencoded 写参数即可):

POST /v1/test/body1 HTTP/1.1
Host: http://127.0.0.1:80
Content-Type: application/x-www-form-urlencoded

name=conanan&
sex=1&
address.provinceName=shannxi&
address.cityName=xian&
petList[0].nikeName=nikeName1&
petList[0].sex=1&
petList[1].nikeName=nikeName2&
petList[1].sex=0&
petMap['one'].nikeName=nikeName3&
petMap['one'].sex=1&
petMap['two'].nikeName=nikeName4&
petMap['two'].sex=0

若参数重复,会出现其他问题,如最后一个 petMap['one'].sex=0,不会替换!

Content-Type: application/x-www-form-urlencoded,看其中 Body 请求体为键值对数据

Java 后台可以如下方式接收

@Data
public class Student {
    private String name;
    private Boolean sex;
    private Address address;
    private List<Pet> petList;
    private Map<String, Pet> petMap;
}
@Data
public class Address {
    private String provinceName;
    private String cityName;
}
@Data
public class Pet {
    private String nikeName;
    private Boolean sex;
}
@PostMapping("/body1")
public String body1(Student student){
    return String.format("body1: %s, %s, %s, %s, %s, %s",
                         student.getName(),
                         student.getSex(),
                         student.getAddress().getProvinceName(),
                         student.getAddress().getCityName(),
                         student.getPetList(),
                         student.getPetMap());

若有多个对象,则还是根据属性名称来接收参数!!!

@PostMapping("/body1")
public String body1(Student student, Teacher teacher){
    return "";
}

若 Student 和 Teacher 有同名属性,则绑定后该同名属性的值相同!

# JSON—@RequestBody

作用:用于获取请求体内容(GET也有),常用于JSON数据封装。不使用则得到是key=value&key=value结构的数据。

属性:

  • required是否必须有请求体,默认为 true,即使是 GET 请求也必须添加请求体!
<form action="springmvc/useRequestBody" method="post">
    用户名称:<input type="text" name="username" ><br/>  
    用户密码:<input type="password" name="password" ><br/>  
    用户年龄:<input type="text" name="age" ><br/> 
 <input type="submit" value=" 保存 "> </form>
@RequestMapping("/useRequestBody") 
public String useRequestBody(@RequestBody(required=false) String body){  
    System.out.println(body);  
    return "success"; 
}

# 其他接收参数方式

# @DateTimeFormat

直接在JavaBean属性上添加注释即可(在get或set方法上添加,命名规范的话字段上添加也行。由于一般自动生成,所以都行)

@DateTimeFormat(pattern="yyyy-MM-dd HH:mm")
private Date creationTime;

# 默认类型转换器

如 SpringMVC 会将a,b,c转为String[] 类型,但是不能专为List<String>,这就是它默认的一个类型转换器

# 自定义类型转换器

除了类型转换器,SpringMVC还提供了注解@DateTimeFormate来转换日期格式。查看5常用注解这一章。

SpringMVC还可以实现一些数据类型自动转换。内置转换器全都在org.springframework.core.convert.support包下。如String转Integer等等

如遇特殊类型转换要求,比如日期数据有很多种格式,SpringMVC没办法把带-字符串转换成日期类型,需要我们自己编写自定义类型转换器。步骤如下:

  1. 定义一个类,实现 Spring提供的 Converter 接口,该接口有两个泛型。

    //Converter<S, T>:  S:source,需要转换的源的类型;T:target,需要转换的目标类型
    @Componet
    public class StringToDateConverter implements Converter<String, Date> {
        @Override
        public Date convert(String source) {
            DateFormat format = null;
            try {
                if (StringUtils.isEmpty(source)) {
                    throw new NullPointerException("请输入要转换的日期");
                }
                format = new SimpleDateFormat("yyyy-MM-dd");
                Date date = format.parse(source);
                return date;
            } catch (Exception e) {
                throw new RuntimeException("输入日期有误");
            }
        }
    }
    
  2. 在 spring配置文件中配置类型转换器。JavaConfig暂时不会怎么配置

    spring 配置类型转换器的机制是,将自定义的转换器注册到类型转换服务中去。

    <!-- 配置类型转换器工厂 --> 
    <bean id="converterService"   class="org.springframework.context.support.ConversionServiceFactoryBean"> 
    <!--FormattingConversionServiceFactoryBean可以让SpringMVC支持和@DateTimeFormat等Spring内部自定义的转换器,建议-->
     <!-- 给工厂注入一个新的类型转换器 -->      
        <property name="converters"> 
          <array> 
           <!-- 配置自定义类型转换器 -->       
              <ref bean="stringToDateConverter"/>
          </array>      
        </property> 
    </bean>
    <!-- 配置spring开启注解mvc的支持,替代处理器映射器和处理器适配器配置,并配置类型转换器 -->
    <mvc:annotation-driven conversion-service="conversionService" />
    

# Map 接收参数

在 Controller 层不知道前端都会传递哪些参数给到后端,为了扩展方便(但是,不能传到 Service 层还是 Map 吧!只能在 Controller 接收时方便点)。许多框架(如 CXF 不允许接收 Map 参数),毕竟不能把 Java 写成 JS 是吧。

  • SpringMVC处理请求用Map类型接收参数时,如果参数无注解,则会传入BindingAwareModelMap类型,等价于Model、ModelMap参数。所以无法接受到传入的参数
  • 参数添加 @RequestParam 注解时,会将参数包装称 LinkedHashMap 对象,参数的key为Map的key,参数值为Map的key,支持Get、Post方法(应该支持Put、Delete,没有测,俩方法与Post类似)。可以封装 PageBean 参数
  • 添加 @RequestBody 注解时,接收 Json 类型数据,也会包装成 LinkedHashMap 对象,该注解不支持Get请求,Get请求没有请求体不能传Json。

# Servlet Api

控制器(处理器)形参中添加如下类型的参数,处理适配器会默认识别并进行赋值

  • HttpServletRequest:通过request对象获取请求信息
  • HttpServletResponse:通过response处理响应信息
  • HttpSession:通过session对象得到session中存放的对象

# @RequestHeader

一般不怎么用

  • 作用:用于获取请求消息头。
  • 属性:value提供消息头名称。required是否必须有此消息头

# @CookieValue

一般不怎么用

  • 作用:用于把指定 cookie 名称的值传入控制器方法参数。
  • 属性:value指定 cookie 的名称。required是否必须有此 cookie

# @SessionAttribute

  • 作用:用于多次执行控制器方法间的参数共享。

  • 属性:

    • value:用于指定存入的属性名称
    • type:用于指定存入的数据类型。
    @Controller("sessionAttributeController") 
    @RequestMapping("/springmvc") 
    @SessionAttributes(value= {"username","password","age"},types= {String.class,Integer.class})//存入到session域 
    public class SessionAttributeController { 
        @RequestMapping("/testPut")    
        public String testPut(Model model){           
            model.addAttribute("username", "泰斯特");           
            model.addAttribute("password","123456");           
            model.addAttribute("age", 31);   
            //跳转之前将数据保存到 username、password 和 age 中,因为注解@SessionAttribute 中有这几个参数 
            return "success"; 
        }
        
        @RequestMapping("/testGet")       
        public String testGet(ModelMap model){           
            System.out.println(model.get("username")+";"+model.get("password")+";"+model.get("age"));           
            return "success";       
        } 
        
        @RequestMapping("/testClean")        
        public String complete(SessionStatus sessionStatus){         
            sessionStatus.setComplete();            
            return "success";        
        }
    }
    

# @ModelAttribute

该注解是 SpringMVC4.3 版本以后新加入的。它可以用于修饰方法和参数。

  • 出现在方法上,表示当前方法会在控制器的方法执行之前先执行。它可以修饰没有返回值和有具体返回值的方法
  • 出现在参数上,获取指定的数据给参数赋值
  • 属性:value用于获取数据的 key。key 可以是 POJO 的属性名称,也可以是 map 结构的 key。

  • 应用场景:当表单提交数据不是完整的实体类数据时,保证没有提交数据的字段使用数据库对象原来的数据

    我们在编辑一个用户时,用户有一个创建信息字段,该字段的值是不允许被修改的。在提交表单数据是肯定没有此字段的内容,一旦更新会把该字段内容置为 null,此时就可以使用此注解解决问题。但是这种问题有其他办法解决。

  • 基于POJO 属性的基本使用

    <a href="springmvc/testModelAttribute?username=test">测试 modelattribute</a> 
    
    @ModelAttribute  
    public void showModel(User user) {   
        System.out.println("执行了 showModel 方法"+user.getUsername());  
    } 
    @RequestMapping("/testModelAttribute") 
    public String testModelAttribute(User user) {   
        System.out.println("执行了控制器的方法"+user.getUsername());   
        return "success";  
    }
    //执行了 showModel 方法
    //执行了控制器的方法
    
  • 基于 Map 的应用场景示例 1:ModelAttribute 修饰方法带返回值

    <!--需求:  修改用户信息,要求用户的密码不能修改 -->
    <form action="springmvc/updateUser" method="post"> 
        用户名称:<input type="text" name="username" ><br/>  
        用户年龄:<input type="text" name="age" ><br/>  
        <input type="submit" value=" 保存 "> 
    </form> 
    
    // 模拟修改用户方法 
    @RequestMapping("/updateUser") 
    public String testModelAttribute(User user) {  
        System.out.println("控制器中处理请求的方法:修改用户:"+user);  
        return "success"; 
    }
    @ModelAttribute public User showModel(String username) {  
        //模拟去数据库查询  
        User abc = findUserByName(username); 
        System.out.println("执行了 showModel 方法"+abc);  
        return abc; 
    } 
    // 模拟去数据库查询 
    private User findUserByName(String username) {  
        User user = new User();  
        user.setUsername(username);
        user.setAge(19);  
        user.setPassword("123456");  
        return user; 
    }
    //输出会给未提交的age字段赋值19,其他的使用提交的数据
    
  • 基于 Map 的应用场景示例 2:ModelAttribute 修饰方法不带返回值

    @RequestMapping("/updateUser") 
    public String testModelAttribute(@ModelAttribute("abc")User user) {  
        System.out.println("控制器中处理请求的方法:修改用户:"+user);  
        return "success"; 
    }
    @ModelAttribute 
    public void showModel(String username,Map<String,User> map) { 
     	//模拟去数据库查询  
        User user = findUserByName(username); 
        System.out.println("执行了 showModel 方法"+user);  
        map.put("abc",user); 
    } 
    // 模拟去数据库查询 
    private User findUserByName(String username) {  
        User user = new User();  
        user.setUsername(username);
        user.setAge(19);  
        user.setPassword("123456");  
        return user; 
    }
    //输出会给未提交的age字段赋值19,其他的使用提交的数据
    

# Multipart 文件

# 文件上传的回顾

  • form表单的enctype取值必须是multipart/form-data(默认值是application/x-www-form-urlencoded)。enctype代表表单请求正文的类型
  • method 属性取值必须是 Post
  • 提供一个文件选择域<input type="file" />
<form action="user/fileupload" method="post" enctype="multipart/form-data">       
    选择文件:<input type="file" name="upload"/><br/>        
    <input type="submit" value="上传文件"/>    
</form>
@RequestMapping(value="/fileupload")    
public String fileupload(HttpServletRequest request) throws Exception {        
    // 先获取到要上传的文件目录        
    String path = request.getSession().getServletContext().getRealPath("/uploads");        
    // 创建File对象,一会向该路径下上传文件        
    File file = new File(path);        
    // 判断路径是否存在,如果不存在,创建该路径        
    if(!file.exists()) {            
        file.mkdirs();        
    }        
    // 创建磁盘文件项工厂        
    DiskFileItemFactory factory = new DiskFileItemFactory();        
    ServletFileUpload fileUpload = new ServletFileUpload(factory);        
    // 解析request对象        
    List<FileItem> list = fileUpload.parseRequest(request);        
    // 遍历        
    for (FileItem fileItem : list) {            
        // 判断文件项是普通字段,还是上传的文件            
        if(fileItem.isFormField()) {                            

        }else {                
            // 上传文件项
            // 获取到上传文件的名称                
            String filename = fileItem.getName();               
            // 上传文件                
            fileItem.write(new File(file, filename));                
            // 删除临时文件                
            fileItem.delete();            
        }        
    }                
    return "success";    
}

# SpringMVC传统方式的文件上传

pom.xml中添加依赖(可不加)

<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

传统方式的文件上传,指的是我们上传的文件和访问的应用存在于同一台服务器上。 并且上传完成之后,浏览器可能跳转。

@RequestMapping(value="/fileupload2")    
public String fileupload2(HttpServletRequest request,MultipartFile upload) throws Exception {        
    System.out.println("SpringMVC方式的文件上传...");        
    // 先获取到要上传的文件目录        
    String path = request.getSession().getServletContext().getRealPath("/uploads");        
    // 创建File对象,一会向该路径下上传文件        
    File file = new File(path);        
    // 判断路径是否存在,如果不存在,创建该路径        
    if(!file.exists()) {            
        file.mkdirs();        
    }        
    // 获取到上传文件的名称        
    String filename = upload.getOriginalFilename();        
    String uuid = UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();        
    // 把文件的名称唯一化        
    filename = uuid+"_"+filename;        
    // 上传文件        
    upload.transferTo(new File(file,filename));        
    return "success";    
}
  • SpringBoot配置

  • JavaConfig配置MultipartResolver接口的实现类

    • CommonsMultipartResolver:使用Jakarta Commons FileUpload解析multipart请求

    • StandardServletMultipartResolver:依赖于Servlet3.0对multipart请求支持(始于Spring3.1

      选择这个,它使用Servlet所提供的功能支持,不依赖其他项目。它没有构造器参数和属性

      @Bean
      public MultipartResolver multipartResolver() throws IOException {
          return new StandardServletMultipartResolver();
      }
      

      如果配置DispatcherServlet的Servlet初始化类继承了AbstractAnnotationConfigDispatcherServletInitializer或AbstractDispatcherServletInitializer的话,通过重载customize Registration()方法(它会得到Dynamic参数)来配置multipart的具体细节

      //class Config extends AbstractAnnotationConfigDispatcherServletInitializer
      @Override
      protected void customizeRegistration(Dynamic registration) {
          registration.setMultipartConfig(new MultipartConfigElement("/tmp/file/uploads",2097152,4194304,0));
          //location,maxFileSize,maxRequestSize,fileSizeThreshold(为0则上传文件写到磁盘)
      }
      
  • spring-config.xml配置文件解析器

    <!-- 配置文件上传解析器,id是固定的!!!--> 
    <bean id="multipartResolver"  class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> 
     <!-- 设置上传文件的最大尺寸为 5MB -->  
        <property name="maxUploadSize">   
            <value>5242880</value>  
        </property> 
    </bean>
    

# SpringMVC跨服务器方式的文件上传

在实际开发中,我们会有很多处理不同功能的服务器(不是服务器集群),目的是让服务器各司其职,从而提高我们项目的运行效率。例如:

  • 应用服务器:负责部署我们的应用
  • 文件服务器:负责存储用户上传文件的服务器
  • 数据库服务器:运行我们的数据库
  • ……

步骤:

  1. 搭建图片服务器

    1. 根据文档配置tomcat9的服务器,现在是2个服务器
    2. 导入资料中day02_springmvc5_02image项目,作为图片服务器使用
  2. 实现SpringMVC跨服务器方式文件上传

    1. 导入依赖的jar包的坐标(sun公司提供的,下面导包时注意)

      <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>
      
    2. 控制器

      @RequestMapping(value="/fileupload3")
      public String fileupload3(MultipartFile upload) throws Exception {        
          System.out.println("SpringMVC跨服务器方式的文件上传...");                
          // 定义图片服务器的请求路径        
          String path = "http://localhost:9090/day02_springmvc5_02image/uploads/";//创建好该文件夹              
          // 获取到上传文件的名称        
          String filename = upload.getOriginalFilename();        
          String uuid = UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();        
          // 把文件的名称唯一化        
          filename = uuid+"_"+filename;        
          // 向图片服务器上传文件                
          // 创建客户端对象        
          Client client = Client.create();        
          // 连接图片服务器        
          WebResource webResource = client.resource(path+filename);        
          // 上传文件        
          webResource.put(upload.getBytes());        
          return "success";    
      }
      
    3. 配置文件解析器,同上