从零开始 Spring Boot 2:处理请求
经过上一篇从零开始Spring Boot 1:快速构建 - 魔芋红茶’s blog (icexmoon.cn)后,我们已经搭建起Spring Boot项目的开发环境,以及一个简单的使用Spring Boot的Web应用。本篇将演示在这个应用基础上,如何实现一个简单的可以进行增删改查(CURD,Create Update Retrieve Delete)的Web应用。
实体类
在创建Controller
之前,我们需要先创建一个实体类(Model),而为了更方便的创建实体类,我们需要引入一个有用的中间件lombok
。
引入这个中间件的方式同样是修改pom.xml
,在其中添加依赖:
...
<dependencies>
...
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
...
这些依赖也叫做启动器(starter),Spring Boot正是通过它来简化框架代码的相关配置。这些启动器内含了各种组件的引入,以及相关的默认设置,它们大致上可以分为官方以及第三方。通过引入这些启动器,我们可以很方便地给Spring Boot项目添加各种功能,并且这些功能是经过官方整合后的,可靠的组件。
我们这个示例项目初始包含两个启动器:
spring-boot-starter-web
,Web应用相关的基础功能,内含一个Nginx。spring-boot-starter-test
,Spring Boot的测试框架。
在从零开始Spring Boot 1:快速构建 - 魔芋红茶’s blog (icexmoon.cn)中介绍热启动时,添加了一个:
spring-boot-devtools
,提供热启动等开发者需要的功能。
spring-boot-xxx
这样方式命名的启动器都是官方提供的组件,我们同样可以引入第三方组件,比如这里的lombok
。
修改Maven配置并添加新的依赖后需要通过Maven重新加载项目,具体的方式有很多种,我习惯于在pom.xml
文件上右键选择Maven
>Reload project
。
此时右下角可能会弹出一个Lombok requires ...
的提示,点击Enable
启用。
现在创建实体类:
package cn.icexmoon.my_first_app.model;
import lombok.Data;
@Data
public class User {
private Long id;
private String name;
private Integer age;
}
这里使用了lombok
的Data
注解,其用途是可以“自动”给实体类附加上getter
和setter
方法,而避免了手动添加的额外工作。
这里的
id
和age
属性可以使用基础类型long
和int
,但是数据库中间件会大量使用泛型(基础类型无法使用泛型),所以最好还是将实体类的属性定义为包装类而非基础类型。
Controller
这里将按照restfull
风格的API创建Controller
,用于处理下面几种URL请求:
POST /user
,添加新用户。GET /user
,获取全部用户信息。PUT /user/{id}
,修改指定用户信息。DELETE /user/{id}
,删除指定用户。GET /user/{id}
,获取指定用户信息。
下面是UserController
的全部代码:
package cn.icexmoon.my_first_app.controller;
import cn.icexmoon.my_first_app.model.Result;
import cn.icexmoon.my_first_app.model.User;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
@RestController
@RequestMapping("/user")
public class UserController {
private Map<Long, User> users = Collections.synchronizedMap(new HashMap<>());
private AtomicLong index = new AtomicLong();
@PostMapping("")
public String addUser(@RequestBody User user) {
Long newId = index.incrementAndGet();
user.setId(newId);
users.put(newId, user);
Result result = new Result();
result.setData(newId);
return result.toString();
}
@DeleteMapping("/{id}")
public Result deleteUser(@PathVariable long id) {
Result result = new Result();
if (!users.containsKey(id)) {
result.setSuccess(false);
result.setMsg("no this person.");
return result;
}
users.remove(id);
result.setMsg("delete success.");
return result;
}
@PutMapping("/{id}")
public Result updateUser(@PathVariable long id,@RequestBody User user) {
Result result = new Result();
if (!users.containsKey(id)) {
result.setSuccess(false);
result.setMsg("no this person.");
return result;
}
user.setId(id);
users.put(id, user);
return result;
}
@GetMapping("/{id}")
public Result getUser(@PathVariable long id) {
Result result = new Result();
User user = users.get(id);
if (user == null) {
result.setSuccess(false);
result.setMsg("no this person.");
return result;
}
result.setData(user);
return result;
}
@GetMapping("")
public String getUsers() {
Result result = new Result();
result.setData(users.values());
return result.toString();
}
}
因为所有的URL请求都以/user
开头,所以这里直接在Controller
类上加入@RequestMapping("/user")
注解以接收所有的/user/xxx
请求(当然也包含全部四种HTTP Method)。
具体的请求由Controller
的相关方法负责接收和处理,这里用GetMapping
、PutMapping
这样的注解来区分不同的HTTP Method。并且使用@PathVariable
注解将“路径参数”绑定到方法参数,用@RequestBody
注解将请求体(JSON格式纯文本)绑定到方法参数,此时框架会自动做“解JSON”处理。
数据的存储这里使用的是“内存中的容器”,其实就是一个HashMap
。所以每次重启应用你都会发现数据被清空,这是显而易见的…
需要注意的是,因为Java的Web服务是用多线程实现的,所以这里作为多个线程共享的Map
必须是一个“线程安全的容器”,因此这里使用了Collections.synchronizedMap
方法。
- 关于容器,可以阅读Java编程笔记8:容器(上) - 魔芋红茶’s blog (icexmoon.cn)。
- 实际上这里也是Java的Web应用和PHP之类的最大差别,因为前者是多线程的,Nginx中的Serverlet是单例,而Serverlet中创建的用于处理特定HTTP请求的Controller也是单例,这意味着多个请求可能会共享一个
Controller
,这也是为什么这里需要使用并发技术来保证Controller
中的属性是线程安全的。关于Controller
是单例这点的更多解释,可以阅读SpringMVC:如何保证Controller的并发安全-Spring专区论坛-技术干货-SpringForAll社区 (spring4all.com)。
这里用自增的整数作为User.id
,并且作为Map
的key使用,因此它也是多个线程共享的。这就需要在线程使用它产生一个新值时进行同步。这里可以使用锁或者syncronize
关键字,但更简洁的方式是使用原子类。这里使用了AtomicLong
类型的index
,并通过index.incrementAndGet
方法产生新的索引,因为原子类的相关操作都具备“原子性”,所以这样做是线程安全的。
最后,为了方便返回统一样式的结果,并JSON化,这里创建了一个辅助的实体类Result
:
package cn.icexmoon.my_first_app.model;
import com.alibaba.fastjson.JSONObject;
import lombok.Data;
@Data
public class Result {
private boolean success = true;
private String msg = "";
private Object data = null;
@Override
public String toString(){
JSONObject jsonObject = new JSONObject();
jsonObject.put("success",success);
jsonObject.put("msg",msg);
jsonObject.put("data",data);
return jsonObject.toString();
}
}
这里引入了一个阿里开发的处理JSON的中间件[fastjson
](alibaba/fastjson: A fast JSON parser/generator for Java. (github.com)),所以需要添加依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
测试
Spring Boot可以使用测试框架编写单元测试代码来进行测试,但我更喜欢使用HTTP客户端测试,这样更直观。
这里使用的测试用HTTP客户端工具是Apipost。
老实说这个乱七八糟的界面是需要习惯好一会的…
接口调用方式也比较简单,具体可以参考我的ApiPost项目。
一个简单的Web应用就完成了,我们在下一篇介绍如何使用数据库,谢谢阅读。
本篇文章的全部代码见learn_spring_boot。