Spring Boot 可以使用 Spring MVC 处理 HTTP 请求。

配置文件

Spring 的 @Value 值注入方式不再赘述,参考Spring 环境抽象 。这里注重介绍的是 Spring boot 的多环境配置。

都知道通过 Spring 的 @Profile("development/production") 可以选择根据 spring.profiles.active/default 配置来是否决定加载该 bean而在 Spring boot 中可以通过这个配置来加载差异化的后补配置,后补配置文件名格式 application-{profile}.properties 其中 {profile} 对应着配置项的名称,常用的如:

  • application-dev.properties:开发环境
  • application-test.properties:测试环境
  • application-prod.properties:生产环境

例如需要启用 application-dev.properties 里的配置,在 appliction.properties 文件里可以这样设置:

1
spring.profiles.active=dev

注意的是附加的配置项如有和 appliction.properties 里的冲突,会覆盖。

Handler Methods

Spring boot 的 Web 这一部分可以使用 Spring MVC 或者 WebFlux,以下着重讲解 Spring MVC 在 Spring 下的使用。

首先构建一个 POJO model User 用于后续的使用。

1
2
3
4
5
6
public class User {
    private int id;
    private String name;
    private String address;
    // 省略 setter/getter
}

Request Mapping Handler Methods

通过 Spring MVC Request Mapping 的方式可以将请求的 URL 和处理该请求的 handler 进行匹配,通常会通过注解的方式来实现。

@RequestMapping 用于创建 URL 和 handler 的映射,可以同时用于 class 和 method 上,这时候实际的 method handler 的 URL 就需要加上 class 上的 RequestMapping value 作为 前缀

@RequestMapping 的默认 value 为 URL 可以,处理固定的字符串还可以使用占位符;method 值为 HTTP 方法,如实现 user 的 RESTful 接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@RestController
@RequestMapping(value="/users")
public class MyRestController {

    @RequestMapping(value="/{user}", method=RequestMethod.GET)
    public User getUser(@PathVariable Long user) {
    // ...
    }

    @RequestMapping(value="/{user}/customers", method=RequestMethod.GET)
    List<Customer> getUserCustomers(@PathVariable Long user) {
    // ...
    }

    @RequestMapping(value="/{user}", method=RequestMethod.DELETE)
    public User deleteUser(@PathVariable Long user) {
    // ...
    }
}
  • RequestMapping 的 value 默认为 / method 默认所有方法,支持 ** 多级、* 单级、及 ? 单字符的 Ant 风格通配符。

  • 除了 RequestMapping method 指定方法,还可以使用 @GetMapping @PostMapping @PutMapping @DeleteMapping @PatchMapping 等快捷方式。

  • 除了 path 参数,还可以使用 consumes 指定请求中的 Media Types 来进一步的细分如 @PostMapping(path = "/pets", consumes = "application/json"),这里对应的是 HTTP 协议 request 中的 Accept 头。

  • 范围更广的,可以进一步使用 headers 进一步根据请求头来细分 Mapping,如 GetMapping(path = "/pets", headers = "myHeader=myValue")

  • RequestMapping 还可以通过 params 参数进行细分(其实 method 也是个细分的项)如:

    1
    2
    3
    4
    5
    
    @RequestMapping(value="/login", params="flag")                  // 请求中必须含有有名为 flag 的键
    @RequestMapping(value="/login", params="!flag")                 // 请求中不能含有要有名为 flag 的键
    @RequestMapping(value="/login", params="flag=hello")            // 请求中名为 flag 的键的值必须为 hello
    @RequestMapping(value="/login", params="flag!=hello")           // 请求中名为 flag 的键的值不能为 hello
    @RequestMapping(value="/login", params={"flag1","flag2=hello"}) // 请求中必须含有 flag1 的键同时名为 flag2 键的值必须是 hello

可以对一个方法指定多个 url 匹配方式,这个情况下需要考虑值没有注入的的情况,如果需要考虑值的合法性需要:

1
2
3
4
@RequestMapping(value = {"/", "/{id}"})
public String postUser(@Nullable @PathVariable Integer id) {
    return "SUCCESS";
}

上述的 @Nullable 也可以转换为 @PathVariable(required = false)

获取 Request 中的数据

所有获取 request 中的数据方式大体可以参考 Method Arguments 表格。

URI Path 中的数据

  • Mapping 值里可以使用路径占位符,使用 @PathVariable 会根据名字来着注入到方法的对应参数里,可以包含多个路径占位符,方法参数里也是可以获取到类注解里声明的路径占位符的。

  • Mapping 里还可以使用 ${} 的方式使用配置值

  • Mapping 值里还可以使用下面的通配符,即 ANT 风格

    • ? 一个字符
    • * 零个字符或者一段路径(指的是 / / 之间的)
    • ** 零个字符或者多个路径段
  • Mapping 还可以使用 {varName:regx} 的方式使用正则来匹配值

  • PathVariable 可以加入 name(即 value) 参数这样方法参数名就不用和占位符一致

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {

    @GetMapping("/pets/{petId}")
    public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
        // ...
    }
}
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable("petId") Long myPetId) {
    // ...
}
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String ext) {
    // ...
}

键值数据

@RequestParam 可以通过注解函数参数来获取 Get 方法的 QueryString 键值对,该注解也可解析 Request Body 中的 form-datax-www-form-urlencoded 的 Content-type 键值数据,Http中数据键名默认是和注解的参数形成对应,也可以通过 @RequestParam("paramName") 指定名称。

@RequestParam 能转换的数据有限(一些简单的类型),复杂数据参见 数据转换和验证章节。另外值得注意的是有个特殊用法,如果注解的是 Map<String, String> 或者 MultiValueMap<String, String> 类型的参数,且不指定 name 参数的话,所有的键值参数将会被放入 Map,如:

1
2
3
4
5
6
7
8
9
@RequestMapping("/")
public String test(@RequestParam Map<String, String> m) {
    Iterator iter = m.entrySet().iterator();
    while (iter.hasNext()) {
        Map.Entry entry = (Map.Entry) iter.next();
        System.out.println(entry);
    }
    return "OK";
}

特别的对应 multipart/form-data 形式的含有文件的请求可使用 MultipartFile 来处理:

1
2
3
4
5
6
7
8
@PostMapping("/form")
public String handleFormUpload(@RequestParam("name") String name,
        @RequestParam("file") MultipartFile file) {
    if (!file.isEmpty()) {
        byte[] bytes = file.getBytes();
        // store the bytes somewhere
    }
}

如果在请求中含有多个同名的键,可以使用 (@RequestParam List<String> list) 的形式来获取而不丢失。如果想将请求中的数据递解析为数组可以利用这种形式的,不过值之间需要使用 , 进行分割

@RequestParam 有默认参数值选项,如 @RequestParam(name="name", required=false, defaultValue="World")

请求头信息

  • @RequestHeader, 通过这个注解可以获取到 request 里的请求头的键值对数据,其 value 指定请求头名。和 RequestParam 类似,可以 Map 形式获取所以信息。

  • @CookieValue, 特别的,这个注解可以获取请求头中的 cookie 值中的键值对,默认参数类型只能是 String。

1
2
3
4
5
6
7
@GetMapping("/demo")
public void handle(
        @RequestHeader("Accept-Encoding") String encoding,
        @RequestHeader("Keep-Alive") long keepAlive,
        @CookieValue("JSESSIONID") String cookie) {
    //...
}

包装数据

有时会为了方便处理表单,或者传输的键值过多,需要使用值对象来装载,这时候就可以用 @ModelAttribute 来实现。

可以从,URL、QueryString、Request Body 中方便的获取每一部分的键值,最后组装在一起,如下的代码:

1
2
3
4
@RequestMapping("/user/{id}")
public String test(@ModelAttribute User user) {
    // ...
}

对应 curl 命令可以有如下的请求示例

1
2
3
4
$ curl -X POST \
  'http://localhost:8080/user/1/?address=Beijing' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'name=Tom'

对应没有获取到的值,会使用其类型的空值,其值的获取范围:

  • @SessionAttributes
  • 同一个控制器中的 @ModelAttribute 方法。
  • URI 模板变量和类型转换器中获取的。
  • 使用默认构造器初始化的。

@ModelAttribute 除了修饰参数外,可以直接注解处理方法,用于包装数据,。

获取 Request Body 中的数据

获取 Request Body 中的数据的一大需求就是获取其中的 json 数据。使用 @RequestBody 注解方法参数可以直接完成反序列化:

1
2
3
4
@RequestMapping("/json")
public User jsonTest(@RequestBody User user) {
    return "Hello" + user.getName();
}

可以使用下面的命令测试

1
2
3
4
5
6
7
8
$ curl -X POST \
  'http://localhost:8080/json/' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Tom",
    "id": 1,
    "address": "Beijing"
  }'

body 中的 json 数据允许与 VO 中的不对应,该注解不能解析 application/x-www-form-urlencoded 类型的 body,这一点没有 django rest framework 里统一解析方便。

获取文件

对于 body 中的文件可以通过注入的 ServletRequest 然后进行比较存储,也可以使用 Multipartfile 或者 Part 类型的参数,通过 Spring 注入进来(适用于 form-data):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import javax.servlet.http.Part;
// ...
@RequestMapping("/upload")
public String uploadTest(Part file) {
    String fileName = file.getSubmittedFileName();
    System.out.println(fileName);
    try {
        file.write("/your/path/to/file" + fileName);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return "SUCCESS";
}

获取其他数据

如果想要在处理方法里使用到进一步的数据,如获取请求方法、获取原生 ServletRequest、HttpSession 等可以很方便的根据类型来注入,所以可以注入到方法的参数可以参考文档 Method Arguments

1
2
3
4
@RequestMapping('/')
public User getUser(HttpMethod method)) {
    // ...
}

其实 @RequestParam 对参数不用注解也是可以同名请求参数注入到方法的参数里的,而且是默认运行为空的

@SessionAttribute 注解控制器的参数的话,可用获取到预先存在于 HttpSession 中被 setAttribute 的属性。

@RequestAttribute 注解控制器的参数的话,可以被用于访问由过滤器或拦截器创建的、预先存在的请求属性,如在自定义的 filter 里面使用 request.setAttribute 设置的属性。

数据转换和验证

@RequestParam, @RequestHeader, @PathVariable, @MatrixVariable, @CookieValue 等注解都是基于 http 协议的 string 进行类型转换到注入类型的,像 int, long, Date 目标参数类型是可以简单的直接转换的,如果需要稍复杂的数据就需要 WebDataBinder 或者注册 Formatters 来完成了

Formatters

日期和数值注解可以方便的使用 @DateTimeFormat @NumberFormat 注解来解析,如下

1
2
3
4
5
@RequestMapping("/format")
public String formatTest(@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date date,
                         @NumberFormat(pattern = "#,###.##") Double num) {
    // ...
}

上面的日期也可以使用 @DateTimeFormat(pattern = "yyyy-MM-dd") 形式,注意是上面的没有使用 @RequestParam 也是可以非显示的注入的。

WebDataBinder?

与 Formatters 转换简单值不同,使用 WebDataBinder 可以进行复杂的复合数据的转换:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import org.springframework.core.convert.converter.Converter;

@Component
public class StringToUserConverter implements Converter<String, User>{
    @Nullable
    @Override
    public User convert(String s) {
        User user = new User();
        String []strArr = s.split("-");
        if (strArr.length < 3) return null;
        user.setId(Integer.valueOf(strArr[0]));
        user.setName(strArr[1]);
        user.setAddress(strArr[2]);
        return user;
    }
}

对应的处理方法:

1
2
3
4
5
@RequestMapping("/converter")
public String converterTest(User user) {
    System.out.println(user.getName());
    return "SUCCESS";
}

相应 id-name-address 的格式可以使用 http://localhost:8080/json/?user=1-Tom-Beijing 访问测试,这里的能够实现的转换的条件是:

  1. 实现 Converter<String, User> 接口,前者是源参数类型,后面是目标参数类型。
  2. 将自定义的 Converter 使用 @Component 加入到 spring 上下文中。
  3. 在 handler 里存在一个参数类型是 User,参数名为 user,同样的在请求中存在键为 user 的 String 值,于是就使用了 StringToUserConverter 进行了转换。

数组形式的参数,也是可以直接调用自定义的 Converter 来完成的(这里的 @RequestParam 好像是必要的),使用 http://localhost:8080/json/?user=1-Tom-Beijing,2-Jack-Wuhan 访问下面的 handler

1
2
3
4
5
6
7
8
@RequestMapping("/converter")
public String converterTest(@RequestParam List<User> user) {
  Iterator<User> iterator = user.iterator();
  while (iterator.hasNext()) {
      System.out.println(iterator.next().getName());
  }
  return "SUCCESS";
}

数据验证

在 VO 或者 POJO 上的属性上加上 JSR-303 注解可以实现字段的验证信息的标注,再在处理方法的注入参数上加上 @Valid 即可实现参数检查,加上参数类型为 Errors 之后,spring 会将错误信息注入。

1
2
3
4
5
public class User {
    @Min(value = 0, message = "must be positive")
    private int id;
    // ...
}
1
2
3
4
5
@RequestMapping("/valid")
public String validTest(@Valid @ModelAttribute User user, Errors errors) {
  System.out.println(errors);
  return "SUCCESS";
}

处理 @Min 注解外,还可参考 javax.validation.constraints 下的注解,还可以使用如 Hibernate 提供的 Range 注解。Errors 也可为 BindingResult,下列示例返回了错误信息,最好处理数据验证错误的方式是在切面里进行统一的处理,参见 Custom Error Message Handling for REST API

1
2
3
4
5
6
@RequestMapping("/valid")
public List<String> validTest(@Valid @ModelAttribute User user, BindingResult bindingResult) {
  return bindingResult.getAllErrors().stream()
          .map(ObjectError::getDefaultMessage)
          .collect(Collectors.toList());
}

也可以自己实现 Validator 接口之后在控制类里使用 @InitBinder 注解一个用于绑定验证器的方法

控制 Response Body

JSON 序列化

通过前面的例子可以知道,在 ResponseBody 或者 RestContoller 注解下的 handler 直接返回 POJO 之后,Jackson JSON 可直接帮你序列化返回(根据 get 方法来获取),如果有不同模式下序列化不同 POJO 字段的需求可以使用 @JsonView 和在 POJO 内定义的 interface 来实现,如:

 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
29
30
31
32
33
@RestController
public class UserController {

    @GetMapping("/user")
    @JsonView(User.WithoutPasswordView.class)
    public User getUser() {
        return new User("eric", "7!jd#h23");
    }
}

public class User {

    public interface WithoutPasswordView {};
    public interface WithPasswordView extends WithoutPasswordView {};

    private String username;
    private String password;

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @JsonView(WithoutPasswordView.class)
    public String getUsername() {
        return this.username;
    }

    @JsonView(WithPasswordView.class)
    public String getPassword() {
        return this.password;
    }
}

上面的视图方法里指定 @JsonView(User.WithoutPasswordView.class) 注解,那么返回的只会解析有 WithoutPasswordView.class 的 getter,不包括没有使用 @JsonView 的。除了直接修饰 handler 也可以下面的方式动态添加到视图 Model 里:

1
2
3
4
5
6
@GetMapping("/user")
public String getUser(Model model) {
    model.addAttribute("user", new User("eric", "7!jd#h23"));
    model.addAttribute(JsonView.class.getName(), User.WithoutPasswordView.class);
    return "userView";
}

@JsonView 也是支持数组参数的,所以可以 @JsonView({WithoutPasswordView.class, WithPasswordView.class}) 还有更多的注解用于指定序列化时候的细节控制:

  • @JsonAnyGetter 用于注解 Map 类型的 getter。
  • @JsonGetter 注解 getter 方法指定字段名,而不是根据 getter 名。
  • @JsonProperty 注解属性,显式指定序列化的多种属性,参数如 name,index,access等。
  • @JsonPropertyOrder 注解 POJO 类指定顺序。
  • @JsonRawValue 用于 String 类型 Json 字符串的嵌套序列化。
  • @JsonRootName 外层包裹的对象。
  • @JsonIgnoreProperties @JsonIgnoreType 注解 POJO 类,忽略某些属性和类型。
  • @JsonIgnore 注解需要忽略的属性。

如对密码的序列化的处理,不允许反序列化即对 json 来说只允许写:

1
2
@JsonProperty(access = Access.WRITE_ONLY)
private String password;

详细用法和其他注解参考更多

Thymeleaf

除了返回 JSON 之外,还可返回视图模板的路径,这里以 Thymeleaf 前端模板语言为例。首先添加其依赖:

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

编写控制器

1
2
3
4
5
6
7
8
9
@Controller
@RequestMapping("/account")
public class AccountController {
    @GetMapping("/")
    public String thymeleafTest(@RequestParam(name="name", required=false, defaultValue="World")String name, Model model) {
        model.addAttribute("name", name);
        return "index";
    }
}

这里的控制器就不能使用 RestContoller 或者 ResponseBody ,返回值为字符串就是目标文件的路径,该路径一般为 classpath:/resources/templates/ 下,可以通过配置 spring.thymeleaf.prefix 来指定自定义的路径。model 参数为渲染视图时候的上下文对象,将需要在视图中用到的数据使用 addAttribute 添加,即可在模板里使用。

编写目标文件 resources/templates/index.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1 th:text="${name}">Hello World</h1>
</body>
</html>

这里的 th: 并不是 HTML 里的表格头标签,而是命名域 thymeleaf 的缩写,该命名域由 <html> 标签里的 xmlns:th="http://www.w3.org/1999/xhtml" 指定。th:text="${name}" 就将传递过来的上下文中名为 name 的数据渲染到该标签的 text 中,其他 thymeleaf 使用方法参见 thymeleaf

除此之外还支持 JSP, JSTL, Thymeleaf, FreeMarker, Velocity, Groovy, Mustache,模板语言对比

控制 Response Header

  • Mapping 中的 consumes 可以精细化 Media Type,相应的可以使用 produces 指定返回的 Media type ,如 @GetMapping(path = "/pets/{petId}", produces = "application/json;charset=UTF-8"),即可指定 Response HTTP 协议中的 Content-Type 头。

更好的方式是在 controller 里返回一个 ResponseEntity 类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@GetMapping("/entity")
public ResponseEntity<String> entityTest() {
    return ResponseEntity.ok().header("echo", "hi").body("hi");
}

@GetMapping("/entity/json")
public ResponseEntity<?> entityJsonTest() {
    HttpHeaders headers = new HttpHeaders();
    headers.add("echo", "hi");
    return new ResponseEntity<User>(new User(), headers, HttpStatus.UNAUTHORIZED);
}

这里示范了两种方式,一个是链式调用,一个是自己构造。上面的 ResponseEntity<?> 返回值也可以改为固定的类型。

通常来说,返回的状态码一般分为成功和失败两种,成功相对失败更加的唯一,所以有时候可以使用 @ResponseStatus 来简化 handler 的响应:

1
2
3
4
5
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
    // ...
}

默认的 handler 成功之后是返回 SUCCESS 的,上面的代码修改了其默认返回状态码,而错误时候的返回码可以通过抛出异常来统一的处理。

控制其他

页面跳转

1
return "redirect:files/{path}";

静态资源

Spring Boot 默认提供静态资源目录位置需置于 classpath 下,目录名需符合如下规则:

  • /static
  • /public
  • /resources
  • /META-INF/resources

如果想要自定义如 404 错误的视图页面直接在如 src/main/resources/templates/error/ 路径下创建如 404.html 等页面即可。

参考