SpringBoot 的 web 场景,其实就是 Spring MVC,使用方式和 Spring MVC 的用法是一模一样的。但是被整合到 SpringBoot 中,有些新的用法还是需要去学习学习的。

路径访问

在学习了 SpringBoot 中的静态资源访问。其中,默认就给了我们 4 个设置静态资源的目录,他们分别是 staticpublicresourcesMETA-INF/resources。不过,在使用 Spring Initializer 来构建我们的 Spring Boot 项目时,默认就有一个 static 目录用户放置静态资源文件夹。

什么时静态资源,所谓的静态资源就是图片、视频、音频、css 文件、js 文件等等。

当我们在这 4 个文件中放置四张不同的图片。访问这些图片看看能不能访问得到。

在访问路径的时候,以下 4 访问路径的方式是错误的:

http://localhost:8080/static/FIL767.png

http://localhost:8080/public/FIL767.png

http://localhost:8080/resources/FIL767.png

http://localhost:8080/META-INF/resources/FIL767.png

正确的访问方式:http://localhost:8080/FIL767.png

为什么不要访问前缀就可访问?这个问题,后面会有提到。

访问图片 FIL767.png

访问图片 FIL770.png

访问图片 FIL2893.png

访问图片 FIL2893.png

这四张图片都可以正常访问的到。

路径访问的先后问题

如果我在 Controller 中写一个和第一张图片(FIL767.png)一模一样的路径,那么它又该怎么处理呢?是直接报错,还是直接访问静态资源文件或者是 Controller 中的路径?

@RestController
public class TestController {

    @RequestMapping("/FIL767.png")
    public String hello() {
        return "这不是图片,你被骗了!";
    }

}

访问之后,效果如下。发现访问的不是图片,而是一个页面。

由此得知,当 web 模块中出现两个同一个目录的时候,它会先去寻找 Controller 中的有没有这个路径。如果有这个路径,web 模块收到并且返回给用户的浏览器端,如果 Controller 中没有这个路径,那么便会去静态资源管理器中寻找静态资源中有没有这个路径。如果有,将静态资源中的内容返回到浏览器界面。如果静态资源找不到,那么就返回一个 404 错误页面。提示资源不存在。

修改静态资源访问路径

接下来,我们来探讨。为什么这 4 种访问方式是有问题的?

http://localhost:8080/static/FIL767.png

http://localhost:8080/public/FIL767.png

http://localhost:8080/resources/FIL767.png

http://localhost:8080/META-INF/resources/FIL767.png

其实在 SpringBoot 中,有一个配置参数,叫做 spring.mvc.static-path-pattern ,它是用于设置静态资源访问路径的访问前缀。默认是没有访问前缀,即访问静态资源文件时,访问的方式是直接从根目录下访问的,上面的访问例子就是这样的。

我们将 spring.mvc.static-path-pattern 的值为 /res/** ,将其配置成以下方式。

spring.mvc.static-path-pattern=/res/**

其中 /* 的意思是访问当前目录下的所有静态资源文件,不包含子文件夹。/** 除了访问当前静态资源文件夹下的所有文件,还会访问子文件夹,孙文件夹下所有的静态资源文件。

修改完成之后,此时访问的时候路径就是这样子的 http://localhost:8080/res/demo.png 。比如我们访问 FIL767.png,即访问路径为 http://localhost:8080/res/FIL767.png

QQ截图20210704154622.png

看,修改文件夹之后就可以使用 res 前缀去访问静态资源文件。

这样做的目的

我觉的这样做的目的是为了将静态资源和其它的请求进行区分开来,还有就是 Spring MVC 有一个拦截器嘛,在配置拦截器的时候,不去拦截静态资源的访问路径即可。

修改静态资源文件夹

在最先,我们提到 SpringBoot 集成的 Spring MVC 是默认有 4 个静态资源文件夹。同时,在源码中我们也能够发现。

public class ResourceProperties {
    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
    // 其他部分被省略
}

要修改静态资源文件夹,那么需要添加这个配置项 spring.resources.static-locations ,也就是用它来配置静态资源文件夹项。

危险的骚操作

这一项可以配置多个,比如我们配置成为当前类路径下,看看能否将 application.properties 配置文件给下载下来。

spring.resources.static-locations=classpath:/

提示要下载,看来是配置生效了。

QQ截图20210704160527.png

打开之后可以查看到 springboot 的配置文件

QQ截图20210704160637.png

配置成为类路径的根目录这是一个非常危险的配置操作,因为这样会非常容易将配置文件和编译后的 class 文件暴露到 web 环境中,这样有些坏人(我不是坏人)将 class 文件反编译成 java 文件。最后将源代码造成泄漏导致得不偿失。

非常不建议配置成 classpath:/当然,除非你很勇。

稳妥的配置方式

比如我们将配置文件配置到当前类路径下的 xiaohehe 文件夹下,那么配置的方式如下。

spring.resources.static-locations=classpath:/xiaohehe/

此时配置完成,其它的静态资源文件夹就无效了。因为它只认 xiaohehe 这个文件夹作为静态资源,我们现在可以测试测试。

将这两张图片放置到 xiaohehe 这个文件夹下。

访问 FIL770.png

访问 FIL2893.png

如果我们访问 FIL2895.png 那么肯定找不到,因为它只找 xiaohehe 这个静态资源文件夹。

多个静态文件夹的目的

我想这是因为多人协作的时候吧,这样各放各的文件夹,不会过分的繁琐吧。

favicon 图标

favicon 是每个浏览器的中的选项卡(标签)都会显示一张小图标。这个小图标就是 favicon 。在 SpringBoot 中,只要将 favicon.ico 放置到静态资源目录下就会自动显示了。

不过我在测试的过程中,由于静态资源的路径是有访问前缀的,所以导致放入了 favicon.ico 还是出不来。将 spring.mvc.static-path-pattern 这个静态资源配置项给注释掉即可。还有一种情况就是 favicon.ico 死活都显示不出来,我想说的是,没必要太纠结这个,因为这个不是很重要。

访问一下试试看。左上角有个小图标已经出现了。

其它未使用过的注解

以前我在使用 Spring MVC 开发 web 程序,我用的最频繁的 mvc 注解有这么 5 个。

  • @RequestMapping

  • @ResponseBody

  • @RequestParam

    • 获取请求参数
    • 同样也可以通过 Map 获取的全部的参数
  • @PathVariable

    • 在路径变量中,它也是一个键值对,所以我们可以将其保存到 Map 中,但是 Map 的键值对泛型都必须为 String 类型的。

    • 示例:

      @GetMapping("/name/{name}/age/{age}")
      public Map<String, String> getCar(@PathVariable Map<String, String> map) {
          System.out.println("name:" + map.get("name") + ",age=" + map.get("age"));
          return map;
      }
    • 运行结果:name:xhaohehe,age=18

    • 如果使用实体类来映射也可以吗?不能,会有一个警告,并且会 500 错误:

      Resolved [org.springframework.web.bind.MissingPathVariableException: Missing URI template variable 'entity' for method parameter of type Entity]

  • @RestController(这个注解在学习 SpringBoot 的时才开始用的)

一看就眼熟的那种

除了这些,今天还有了解到一下注解。并了解其中的含义,测试了它们的用法。

  • @RequestHeader

    • 获取请求头的信息

    • 使用方法,比如我们想获取浏览器标识和主机地址

      @GetMapping("/getHeaderTest")
      public String getHeaders(@RequestHeader("User-Agent") String userAgent,
                               @RequestHeader("Host") String host) {
          System.out.println("user-agent:\n" + userAgent + "\nHost:" + host);
          return "ok";
      }
    • 运行结果

      user-agent:
      Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36 SE 2.X MetaSr 1.0
      Host:localhost:8087
    • 同样的,如果要获取全部的请求头。也可以使用 Map 来接收全部的请求头。

  • @RequestAttribute

    • 获取 request 域中的的值

    • 使用方法

      @Controller
      public class RequestAttrController {
      
          @GetMapping("/goto")
          public String goTo(HttpServletRequest request) {
              request.setAttribute("name", "旺财");
              request.setAttribute("age", "12");
              return "forward:/success";
          }
      
          @GetMapping("/success")
          @ResponseBody
          public String success(@RequestAttribute("name") String name,
                                @RequestAttribute("age") String age) {
              System.out.println("name:" + name + ",age=" + age);
              return "ok";
          }
      
      }
    • 运行结果:name:旺财,age=12

    • 它不能向其它的一样使用 Map 来接收全部的 request 请求域中的全部参数

  • @RequestBody

    • 获取请求体,只有请求体存在于 POST 请求中,GET 请求中没有请求体。

    • 使用方法

      @PostMapping("/testBody")
          public String getCookieTest(@RequestBody String content) {
              System.out.println("content:" + content);
              return "ok";
          }
    • 运行结果

      content:name=xiaohehe&age=18&gender=%E7%94%B7

  • @CookieValue

    • 用于获取浏览器中的 Cookie

    • 使用方法

      @GetMapping("/getCookieTest")
      public String getCookieTest(@CookieValue("name") String name,
                                  @CookieValue("age") String age) {
          System.out.println("name:" + name + ",age:" + age);
          return "ok";
      }
    • 运行结果

      name:xiaoqiang,age:40

MatrixVariable 注解和 UrlPathHelper

MatrixVariable 也叫矩阵变量,矩阵变量的语法格式如下。

http://localhost/user1;name=xiaohehe;age=20/user2;name=bestguo;age=18

看起来有一点懵逼的感觉,但是这个也不是很难,我们可以通过“变量代换”的方式来帮助我们来理解这个路径。假设 user1;name=xiaohehe;age=20 为 a,user2;name=bestguo;age=18 为 b,所以上面的路径就变成了这副模样。

http://localhost/a/b

看,其实它的真实面目还是一个路径。然而,有没有发现。我觉得其实它的本质还是 PathVariable (路径变量),矩阵变量是路径变量的一个变种。

那么,矩阵变量就是 a 和 b 里面的 user1 和 user2 了(有点强行解释的感觉),因为 user1 和 user2 后面其实并没有等于一个值。即 user1;name=xiaohehe;age=20user2;name=bestguo;age=18

获取矩阵变量中的内容

注意,由于 SpringBoot 默认是将矩阵变量给禁用掉了,所以需要将其开起来。配置的方式需要通过配置 Bean 的方式来自定义配置。不管它,先复制它再说。

@Configuration(proxyBeanMethods = false)
public class MyConfig {
    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void configurePathMatch(PathMatchConfigurer configurer1) {
                UrlPathHelper helper = new UrlPathHelper();
                // 禁用分号取消
                helper.setRemoveSemicolonContent(false);
                configurer1.setUrlPathHelper(helper);
            }
        };
    }
}

如果我们要获取以下路径中的内容要怎么获取呢?

http://localhost:8087/matrixVariableTest/user1;name=xiaohehe;age=20/user2;name=bestguo;age=18

我们试试这种看看

@GetMapping("/matrixVariableTest/{user1}/{user2}")
@ResponseBody
public String matrixVariableTest(@MatrixVariable("name") String name,
                                 @MatrixVariable("age") String age) {
    System.out.println(name + "---------" + age);
    return "ok";
}

运行测试了一下,发现有一个警告。页面显示的是 400 错误。

Resolved [org.springframework.web.bind.ServletRequestBindingException: Found more than one match for URI path parameter 'name' for parameter type [java.lang.String]. Use 'pathVar' attribute to disambiguate.]

这个错误的原因是因为 user1 这个矩阵变量中有 name 和 age ,user2 这个矩阵变量中也有 name 和 age ,由于出现了重复的,而 MatrixVariable 去识别的时候发现有两个相同的,所以就有问题了。

不过在这个警告中有一个建议,那就是在这个注解中加入一条 pathVar 属性,用来区分它们,所以我们可以将上面的代码改写成如下的方式。这次我们从 user1 这个矩阵变量中拿取 name 和 age 。

@GetMapping("/matrixVariableTest/{user1}/{user2}")
@ResponseBody
public String matrixVariableTest(@MatrixVariable(value = "name", pathVar = "user1") String name,
                                 @MatrixVariable(value = "age", pathVar = "user1") String age) {
    System.out.println(name + "---------" + age);
    return "ok";
}

运行结果有了,页面返回了一个 ok 给我们

xiaohehe---------20

那我想两个都拿取该怎么办?那就都拿呗

@GetMapping("/matrixVariableTest/{user1}/{user2}")
@ResponseBody
public String matrixVariableTest(@MatrixVariable(value = "name", pathVar = "user1") String name,
                                 @MatrixVariable(value = "age", pathVar = "user1") String age,
                                 @MatrixVariable(value = "name", pathVar = "user2") String name2,
                                 @MatrixVariable(value = "age", pathVar = "user2") String age2) {
    System.out.println(name + "---------" + age);
    System.out.println(name2 + "---------" + age2);
    return "ok";
}

运行结果如下

xiaohehe---------20
bestguo---------18

万能的 Map

矩阵变量中的各个属性和值都可以将其保存至 Map 集合中,但是它的泛型必须为 String, String 类型的。

如果是这种的方式话,不添加 pathVar ,那么它得到的默认就是第一个 user1 里面的内容。

@GetMapping("/matrixVariableTest/{user1}/{user2}")
@ResponseBody
public String matrixVariableTest(@MatrixVariable Map<String, String> user) {
    // 打印map
    System.out.println(user);
    return "ok";
}
// 运行结果:{name=xiaohehe, age=20}

所以,要获取 user2 中的话,就还需要添加一个 pathVar 参数才能获取到。

@GetMapping("/matrixVariableTest/{user1}/{user2}")
@ResponseBody
public String matrixVariableTest(@MatrixVariable(pathVar = "user1") Map<String, String> user1,
                                 @MatrixVariable(pathVar = "user2") Map<String, String> user2) {
    // 打印map
    System.out.println(user1);
    System.out.println(user2);
    return "ok";
}
// 运行结果如下 
// {name=xiaohehe, age=20}
// {name=bestguo, age=18}

UrlPathHelper 是啥

这个类是用于解析路径中的信息,比如发送 get 请求中所带的 “?” 后面的参数,以及路径变量和矩阵变量这种写法,全都是由它来解析的。

矩阵变量默认为禁用的原因,是因为 UrlPathHelper 在解析路径的时候,如果遇到 “;” 那么将 “;” 后面的内容全部给忽略掉了。