系列目录

请求参数 (request params)

上面的 url 并没有自定义参数,接下来我们定义一下请求参数,使得可以通过 http://localhost:8080/api/user/{id}http://localhost:8080/api/user?id={id} 访问到 {id} 的用户。

结论:

1
2
3
4
5
6
7
8

// 以 /api/user/{id} 形式访问
@GetMapping("/api/user/{id}")
public User getUserInfo(@PathVariable("id") int id);

// 以 /api/user?id={id} 形式访问
@GetMapping("/api/user")
public User getUserInfoByParams(@Param("id") int id);

扩充 UserMapper(数据访问层):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/main/java/com/lyh543/springbootdemo/mapper/UserMapper.java

@Repository
public interface UserMapper {
@Select("SELECT * FROM users WHERE email = #{email}")
User getByEmail(@Param("email") String email);

@Select("SELECT * FROM users WHERE id = #{id}")
User getById(@Param("id") int id);

@Insert("INSERT INTO users (email, password, name) VALUES (#{email}, #{password}, #{name})")
@Options(useGeneratedKeys=true, keyProperty = "id") // 注意 insert 的返回值返回仍然是插入行数,而 id 被放自动在了 user.id 里
int insert(User user);
}

扩充 UserService(业务逻辑层):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/main/java/com/lyh543/springbootdemo/service/UserService.java

@Service
public class UserService {
@Autowired
UserMapper userMapper;

public User getByEmail(String email) {
return userMapper.getByEmail(email);
}

public User getUserById(int id) {
return userMapper.getById(id);
}
}

最后是 UserController(视图层):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/main/java/com/lyh543/springbootdemo/web/UserController.java

@RestController
public class UserController {
@Autowired
UserService userService;

// /api/user/1
@GetMapping("/api/user/{id}")
public User getUserInfo(@PathVariable("id") int id) {
return userService.getUserById(id);
}

// /api/user?id=1
@GetMapping("/api/user")
public User getUserInfoByParams(@Param("id") int id) {
return userService.getUserById(id);
}
}

然后运行,httpie 测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ http http://localhost:8080/api/user/1
{
"id": 1,
"email": "lyh543@outlook.com",
"password": "123456",
"name": "lyh543"
}

$ http http://localhost:8080/api/user/2
{
"id": 2,
"email": "test@example.com",
"password": "test",
"name": "test"
}

返回 404 错误

在上面的测试中,如果我们 GET 一个不存在的用户,会返回 200 + 空报文。能不能返回 404 呢?

当然是可以的,我们只需要在 user == null 的时候抛出 ResponseStatusException(HttpStatus.NOT_FOUND) 即可。

mapperservicecontroller 都可以抛异常,应该在哪里抛出呢?应当是 service

  • mapper 只负责单纯地访问数据库;
  • service 主要负责业务的逻辑;
  • controller 只负责将 url 映射到 service、将 service 的返回值转发出去。

于是稍微改一改 service 层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class UserService {
@Autowired
UserMapper userMapper;

public User getByEmail(String email) {
return userMapper.getByEmail(email);
}

public User getUserById(int id) {
User user = userMapper.getById(id);
if (user == null)
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
return user;
}
}

然后编译,命令行运行 http localhost:8000/api/user/3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ http localhost:8080/api/user/3
HTTP/1.1 404
Connection: keep-alive
Content-Type: application/json
Date: Thu, 10 Jun 2021 08:32:21 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
"error": "Not Found",
"message": "404 NOT_FOUND",
"path": "/api/user/3",
"status": 404,
"timestamp": "2021-06-10T08:32:21.775+00:00",
"trace": "org.springframework.web.server.ResponseStatusException: 404 NOT_FOUND ....."
}

对了,如果用浏览器访问,可能会报错。猜测可能是 Spring MVC 的 view 在收到 404 会尝试渲染为 /error,但我们并没有写 /error

浏览器报错
浏览器报错

两种访问方式结果不同的直接原因是浏览器不接受 application/json,Spring MVC 只能返回 text/html;而命令行接收 */*,Spring 就可以返回 application/json 了。

请求体 (request body)

上面的例子中,参数都在 URL 中。使用 POST 方法,就可以在 request body 里装东西了。

nb 的是,Spring Boot 也可以对 request body 自动反序列化,太香了!

1
2
3
@PostMapping("/api/user")
public User createUser(@RequestBody User user) {
}

为了看看 Spring Boot 是如何反序列化、序列化的,我们在 UserController 里写一个函数,把参数 user 直接返回回去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/main/java/com/lyh543/springbootdemo/web/UserController.java

@RestController
public class UserController {
@Autowired
UserService userService;

@GetMapping("/api/user/{id}")
public User getUserInfo(@PathVariable("id") int id) {
return userService.getUserById(id);
}

@PostMapping("/api/user")
public User createUser(@RequestBody User user) {
return user;
}
}

下面开始请求:


1
2
3
4
5
6
7
8
$ http localhost:8080/api/user
HTTP/1.1 405
Allow: POST
Connection: keep-alive
Content-Type: application/json
Date: Thu, 10 Jun 2021 09:27:05 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

httpie 如果没有加 =,就是 GET 方法;如果加了 id=1 或者单单一个的 =,那就是 POST 方法。

这个 url 只给了 PostMapping,所以 GET 方法被打回来了。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ http localhost:8080/api/user id=1
HTTP/1.1 500
Connection: close
Content-Type: application/json
Date: Thu, 10 Jun 2021 09:25:48 GMT
Transfer-Encoding: chunked

{
"error": "Internal Server Error",
"message": "Type definition error: [simple type, class com.lyh543.springbootdemo.entity.User]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.lyh543.springbootdemo.entity.User` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)\n at [Source: (PushbackInputStream); line: 1, column: 2]",
"path": "/api/user",
"status": 500,
"timestamp": "2021-06-10T09:25:48.338+00:00",
"trace": "org.springframework.http.converter.HttpMessageConversionException"
}

在网上查了一下,原来是 User 没有加默认构造方法,所以 Spring 的反序列化器就挂掉了。


User 加上默认构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class User {
private long id;
private String email, password, name;

public User() {}

public User(int id, String email, String password, String name) {
this.id = id;
this.email = email;
this.password = password;
this.name = name;
}

// ...
}

再次请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ http localhost:8080/api/user id=1
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/json
Date: Thu, 10 Jun 2021 09:27:00 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
"email": null,
"id": 1,
"name": null,
"password": null
}

好耶!可以看到 Spring 为我们创建了一个新的 User 对象,并把参数填进去了。

还请读者尝试,当 key 不正确时(如传递 a=1,甚至什么都不传递、只写一个 =),会发生什么。

处理请求体

收到了 User,我们先不考虑验证数据的合法性,直接将收到的数据插入数据库。之前已经写了 UserMapper.insert,我们补一下 UserServiceUserController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/main/java/com/lyh543/springbootdemo/service/UserService.java
@Service
public class UserService {
public User createUser(User user) {
userMapper.insert(user);
return user;
}
}

// src/main/java/com/lyh543/springbootdemo/web/UserController.java
@RestController
public class UserController {
@PostMapping("/api/user")
public User createUser(@RequestBody User user) {
return userService.createUser(user);
}
}

编写好以后重启,运行 http 命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ http localhost:8080/api/user email=test@example.com password=123 name=lyh
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/json
Date: Fri, 11 Jun 2021 03:02:31 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
"email": "test@example.com",
"id": 10,
"name": "lyh",
"password": "123"
}

查一下数据库,发现确实写入数据库了。

返回 201

但是创建用户返回 200 也太不 RESTful 了吧,创建用户应当返回 201 Created。

我们只需要在写 Controller 函数的时候多传递一个 HttpServletResponse response,然后修改这个 Response 就可以了。

1
2
3
4
5
@PostMapping("/api/user")
public User createUser(@RequestBody User user, HttpServletResponse response) {
response.setStatus(HttpStatus.CREATED.value());
return userService.createUser(user);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ http localhost:8080/api/user email=test2@example.com password=123 name=user
HTTP/1.1 201
Connection: keep-alive
Content-Type: application/json
Date: Fri, 11 Jun 2021 03:20:37 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
"email": "test2@example.com",
"id": 12,
"name": "user",
"password": "123"
}

用同样的方法,我们还可以获取 request 内容:

1
2
@PostMapping("/api/user")
public User createUser(@RequestBody User user, HttpServletRequest request, HttpServletResponse response) {}

我们甚至可以将这两个东西 @Autowired 到类上,以后就不用写这两个参数了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class UserController {
@Autowired
UserService userService;
@Autowired
HttpServletRequest request;
@Autowired
HttpServletResponse response;

@PostMapping("/api/user")
public User createUser(@RequestBody User user) {
response.setStatus(HttpStatus.CREATED.value());
return userService.createUser(user);
}
}