REST风格

REST风格

初识REST

Restful就是一个资源定位及资源操作的风格。不是标准也不是协议,只是一种风格。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。

比如说,我们以前在进行开发的时候,通常一个请求是:http://localhost:8888/user/getUser?id=1

在这个请求中,getUser是方法名,一个动词,我们要根据这个动词去进行相应的操作,换句话说,这个为名id的用户,它的资料存在这个一个getUser方法中。从现实中来看,这是不科学的,一个用户怎么会存在一个动词当中,一个用户应该使用: http://localhost:8888/user/1 来表达才比较对,user代表了这个群体是一个用户,1表示了这个用户的id。

基于这种思想,就诞生了一个名为:REST的风格。这种规范自2000年以来,开始流传于世间。

构建REST风格网站

引入依赖

在构建过程中,除了前面必要的依赖,如mysql、mybatis之外,还需要引入一个人依赖:

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

有了这个依赖,才能实现对页面的处理,首先看一个例子:

页面之间的跳转

我们在controller中先写入这一段代码:

1
2
3
4
5

@GetMapping("/index")
public String index(){
return "index";
}

这一段代码表示,接收一个index请求,并且会返回一个为名index的字符串。看起来是这样的,但实际上,它在引入了thymeleaf依赖之后,会自动在String后面拼串,返回的是一个名为 index.html的页面。

同时,也需要在 resource的templates包中,放入index的页面,它就能够识别,且自动跳转。

同时,你还要学习关于计算机网络和HTTP相关的知识,至少要知道一下几点:

POST:一般是用于提交数据的

GET:一般是用于获取数据的

PUT:一般是用于更新一条数据的

DELETE:一般是用于删除数据的

PATCH:一般是用于更新一批数据的

实体类

为了使得出入的数据都能被完全的转移至数据库,通常需要以下几个实体类:

用于传入数据库的user:

1
2
3
4
5
6
7
8
9
@Data
@Alias("user")
public class User {
private Long id;
private String userName;
private SexEnum sex = null;
private String note;

}

前端传进来的user:

1
2
3
4
5
6
7
8
@Data
public class UserVo {
private Long id;
private String userName;
private int sexCode;
private String sexName;
private String note;
}

在controller层中,需要添加的方法:

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
34
35
36
37
38
39
40
41
42
// 转换Vo变为PO
private User changeToPo(UserVo userVo) {
User user = new User();
user.setId(userVo.getId());
user.setUserName(userVo.getUserName());
user.setSex(SexEnum.getSexEnum(userVo.getSexCode()));
user.setNote(userVo.getNote());
return user;
}

// 转换PO变为VO
private UserVo changeToVo(User user) {
UserVo userVo = new UserVo();
userVo.setId(user.getId());
userVo.setUserName(user.getUserName());
userVo.setSexCode(user.getSex().getCode());
userVo.setSexName(user.getSex().getName());
userVo.setNote(user.getNote());
return userVo;
}

// 将PO列表转换为VO列表
private List<UserVo> changeToVoes(List<User> poList) {
List<UserVo> voList = new ArrayList<>();
for (User user : poList) {
UserVo userVo = changeToVo(user);
voList.add(userVo);
}
return voList;
}

// 结果VO
@Data
public class ResultVo {
public ResultVo(Boolean success, String message) {
this.success = success;
this.message = message;
}

private Boolean success = null;
private String message = null;
}

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server:
port: 8888

mybatis:
mapper-locations: classpath:/mapper/*Mapper.xml
type-aliases-package: com.example.rest.pojo
type-handlers-package: com.example.rest.typeHandler

spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/springtest?useSSL=false&serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8
driver-class-name: com.mysql.cj.jdbc.Driver
tomcat:
max-idle: 10
max-wait: 10000
max-active: 50
initial-size: 5

MyBatis的xml配置

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
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.rest.dao.UserDao">


<select id="getUser" parameterType="long" resultType="user">
select * from t_user
where id = #{id}
</select>

<insert id="insertUser" useGeneratedKeys="true" keyProperty="id"
parameterType="user">
insert into t_user(user_name, note)
values(#{userName}, #{note})
</insert>

<update id="updateUser">
update t_user
<set>
<if test="userName != null">user_name =#{userName},</if>
<if test="note != null">note =#{note}</if>
</set>
where id = #{id}
</update>

<select id="findUsers" resultType="user">
select id, user_name as userName, note from t_user
<where>
<if test="userName != null">
and user_name = #{userName}
</if>
<if test="note != null">
and note = #{note}
</if>
</where>
</select>

<delete id="deleteUser" parameterType="long">
delete from t_user where id = #{id}
</delete>
</mapper>

为了不多赘述,以下省略mybatis、mysql、typeHandler和enum的配置。

controller层

添加json数据至数据库

先贴controller层的设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
public class UserController {

// 用户服务接口
@Autowired
private UserService userService = null;

@GetMapping("/index")
public String index(){
return "index";
}

@PostMapping("/user")
public User insertUser(@RequestBody UserVo userVo){
User user=this.changeToPo(userVo);
return userService.insertUser(user);
}
}

这里使用的是PostMaping,表示我们所接受到的是Post请求,这里在UserVO使用@RequestBody,目的是表示这里接受的必须是json型数据,也之后json型数据才会被绑定到UserVo当中。

然后在使用转换,在传入数据库当中,这里打开PostMan开始测试:

传入的是json型数据,可以发现,这里在URL并没有使用任何的动词,而是直接传入json数据到后端。

看看数据库:

这显然成功了。

获得数据库信息

我们继续在controller中添加数据:

1
2
3
4
5
6
7
// 获取用户
@GetMapping(value = "/user/{id}")
@ResponseBody
public UserVo getUser(@PathVariable("id") Long id) {
User user = userService.getUser(id);
return changeToVo(user);
}

这里我们看到,使用的是@PathVariable,这是什么意思呢?这表示我们的id字段将会从url中获取,于是id变获得了,{id},可能你会提问,我使用:?id=1,不是也可以吗?当然可以,但是当你传入多个参数的时候,就会变的很麻烦,别着急,往下看。

在看看是否从数据库获取成功:

输入: http://localhost:8888/user/1

我们发现,这次没使用所谓的getUser,也获得了数据。但是你看,这里userName居然是空的?

我回去看看数据库,原来数据库设置用户名是user_name,而我们设计的类是userName,这不一样,但是没关系,我们可以在yml配置文件中的mybatis配置中加一句:configuration: map-underscore-to-camel-case: true

这样它就会开启驼峰命名法,会自动将大写转化为 _ 加 小写 ,这样在映射数据库中,就成功把userName转化为user_name了。

查询符合要求的用户

在查询符合要求的用户时,为了节省查询量,肯定需要传入多个关键词,比如:

1
2
3
4
5
6
7
8
9
10
@GetMapping("/users/{userName}/{note}/{start}/{limit}")
@ResponseBody
public List<UserVo> findUsers(
@PathVariable("userName") String userName,
@PathVariable("note") String note,
@PathVariable("start") int start,
@PathVariable("limit") int limit) {
List<User> userList = userService.findUsers(userName, note, start, limit);
return this.changeToVoes(userList);
}

可以看到的是,我们传入了多个关键词就只用/分割开就行了,不需要传入id=1&username=“xxx”,这么麻烦了,这就是 @PathVariable的作用。

修改用户数据
1
2
3
4
5
6
7
8
9

@PutMapping("/user/{id}")
@ResponseBody
public User updateUser(@PathVariable("id") Long id, @RequestBody UserVo userVo) {
User user = this.changeToPo(userVo);
user.setId(id);
userService.updateUser(user);
return user;
}

可以看到,这里将两个注解结合到了一起,使用在这个url中,修改所需要的修改的用户:

看,这样就好了,即在url中选择了用户id,又在json中传达了需要的数据,并且又避免了动词的存在,一举三得。再看看数据库:

其他测试

其他数据测试也是几乎一样的,这里仅提供代码:

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
   @PatchMapping("/user/{id}/{userName}")
@ResponseBody
public ResultVo changeUserName(@PathVariable("id") Long id,
@PathVariable("userName") String userName) {
int result = userService.updateUserName(id, userName);
ResultVo resultVo = new ResultVo(result>0,result > 0 ? "更新成功" : "更新用户【" + id + "】失败。");
return resultVo;
}

@DeleteMapping("/user/{id}")
@ResponseBody
public ResultVo deleteUser(@PathVariable("id") Long id) {
int result = userService.deleteUser(id);
ResultVo resultVo = new ResultVo(result>0,
result > 0 ? "更新成功" : "更新用户【" + id + "】失败。");
return resultVo;
}

@PatchMapping("/user/name")
@ResponseBody
public ResultVo changeUserName2(Long id, String userName) {
int result = userService.updateUserName(id, userName);
ResultVo resultVo = new ResultVo(result>0,
result > 0 ? "更新成功" : "更新用户名【" + id + "】失败。");
return resultVo;
}

代码补充

性别的枚举:

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
34
35
36
37
38
39
40
public enum SexEnum {/**/


MALE(0, "男"),
FEMALE(1, "女");

private int code;
private String name;

SexEnum(int code, String name) {
this.code = code;
this.name = name;
}

public static SexEnum getSexEnum(int code) {
for (SexEnum sex : SexEnum.values()) {
if (sex.getCode() == code) {
return sex;
}
}
return null;
}

public int getCode() {
return code;
}

public void setCode(int code) {
this.code = code;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

}

其实并不长,之所以看起来长,是因为枚举类型不能使用@Data。

SexTypeHandler:

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
@MappedTypes(SexEnum.class)
@MappedJdbcTypes(JdbcType.INTEGER)
public class SexTypeHandler extends BaseTypeHandler<SexEnum> {

@Override
public SexEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
int code = rs.getInt(columnName);
return SexEnum.getSexEnum(code);
}

@Override
public SexEnum getNullableResult(ResultSet rs, int index) throws SQLException {
int code = rs.getInt(index);
return SexEnum.getSexEnum(code);
}

@Override
public SexEnum getNullableResult(CallableStatement cs, int index) throws SQLException {
int code = cs.getInt(index);
return SexEnum.getSexEnum(code);
}

@Override
public void setNonNullParameter(PreparedStatement ps, int index, SexEnum sex, JdbcType jdbcType) throws SQLException {
ps.setInt(index, sex.getCode());
}

}

学点前端

闭门造车,不是明智之选,看点前端知识:

为了在前端也能够识别且使用thymleaf语法,需要在页面顶部添加:

1
<html lang="en" xmlns:th="http://www.thymeleaf.org">

为了能使用JQuery库,需要添加:

1
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>

JavaScript和JQuery的区别就像,C++和STL类库的区别,int和Integer的区别,面粉和蛋糕的区别。

学一下JS:

在前面的使用post请求时,也可以在页面这么使用:

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
<script type="text/javascript">
function post() {
var params = {
'userName': 'user_name_new',
'sexCode' : 1,
'note' : "note_new"
}
$.post({
url : "./user",
// 此处需要告知传递参数类型为JSON,不能缺少
contentType : "application/json",
// 将JSON转化为字符串传递
data : JSON.stringify(params),
// 成功后的方法
success : function(result) {
if (result == null || result.id == null) {
alert("插入失败");
return;
}
alert("插入成功");
}
});
}
post();
</script>

这样便可以在前端执行JS代码。运行这个页面的时候,会触发post函数,post函数开头便定义了一个值,这个值是json类型,并且会执行$.post方法,它可以这么写是因为JQ的原因,从传达的url中获取要接收的json参数,成功则执行函数,并返回成功的消息框,否则会在页面返回插入失败的消息框。OK。

项目地址:https://github.com/Antarctica000/SpringBoot/tree/master/rest