# 请求参数 (request params)

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

结论:


// 以 /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(数据访问层):

// 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(业务逻辑层):

// 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(视图层):

// 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 测试:

$ 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 层:

@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

$ 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 自动反序列化,太香了!

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

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

// 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;
    }
}

下面开始请求:


$ 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 方法被打回来了。


$ 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 加上默认构造方法:

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

    // ...
}

再次请求:

$ 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

// 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 命令

$ 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 就可以了。

@PostMapping("/api/user")
public User createUser(@RequestBody User user, HttpServletResponse response) {
    response.setStatus(HttpStatus.CREATED.value());
    return userService.createUser(user);
}
$ 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 内容:

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

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

@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);
    }
}