【项目笔记】RESTful风格
REST:Representational State Transfer(表象层状态转变),你一定以为是rest这个单词,其实他是这三个单词的缩写。
什么是RESTful?
RESTful是一种帮助计算机系统通过 Internet 进行通信的架构风格,而不是具体的协议。它代表着分布式服务的架构风格,这使得微服务更容易理解和实现。如果一个架构符合REST原则,就称它为RESTful架构。
URL定位资源,用HTTP动词(GET,POST,DELETE,DETC)描述操作。
看Url就知道要什么
看http method就知道干什么
看http status code就知道结果如何
每一个URI代表一种资源;
客户端和服务器之间,传递这种资源的某种表现层;
客户端通过四个HTTP动词,对服务器端资源进行操作,实现”表现层状态转化”。
GET /rest/api/getDogs –> GET /rest/api/dogs 获取所有小狗狗
GET /rest/api/addDogs –> POST /rest/api/dogs 添加一个小狗狗
GET /rest/api/editDogs/:dog_id –> PUT /rest/api/dogs/:dog_id 修改一个小狗狗
GET /rest/api/deleteDogs/:dog_id –> DELETE /rest/api/dogs/:dog_id 删除一个小狗狗
左边的这种设计,很明显不符合REST风格,上面已经说了,URI 只负责准确无误的暴露资源,而 getDogs/addDogs…已经包含了对资源的操作,这是不对的。相反右边却满足了,它的操作是使用标准的HTTP动词来体现。
RESTful6大原则
REST之父Roy Fielding在论文中阐述REST架构的6大基本原则,它们分别是:
1. C-S架构
数据的存储在Server端,Client端只需使用就行。两端彻底分离的好处使client端代码的可移植性变强,Server端的拓展性变强。两端单独开发,互不干扰。
2. 无状态
http请求本身就是无状态的,基于C-S架构,客户端的每一次请求带有充分的信息能够让服务端识别。请求所需的一些信息都包含在URL的查询参数、header、div,服务端能够根据请求的各种参数,无需保存客户端的状态,将响应正确返回给客户端。无状态的特征大大提高的服务端的健壮性和可拓展性。
当然,这种无状态性的约束也是有缺点的,客户端的每一次请求都必须带上相同重复的信息确定自己的身份和状态,造成传输数据的冗余性,但这种确定对于性能和使用来说,几乎是忽略不计的。
3.统一的接口
REST架构的核心内容,统一的接口对于RESTful服务非常重要。客户端只需要关注实现接口就可以,接口的可读性加强,使用人员方便调用。
REST接口约束定义为:资源识别; 请求动作; 响应信息; 它表示通过uri标出你要操作的资源,通过请求动作(http method)标识要执行的操作,通过返回的状态码来表示这次请求的执行结果。
4.一致的数据格式
服务端返回的数据格式要么是XML,要么是Json(获取数据),或者直接返回状态码,一些知名网站的开放平台的操作数据的api,post、put、patch都是返回的一个状态码 。
如请求一条微博信息,服务端响应信息应该包含这条微博相关的其他URL,客户端可以进一步利用这些URL发起请求获取感兴趣的信息,再如分页可以从第一页的返回数据中获取下一页的URT也是基于这个原理。
5.可缓存
在万维网上,客户端可以缓存页面的响应内容。因此响应都应隐式或显式的定义为可缓存的,若不可缓存则要避免客户端在多次请求后用旧数据或脏数据来响应。管理得当的缓存会部分地或完全地除去客户端和服务端之间的交互,进一步改善性能和延展性。
6.按需编码、可定制代码
服务端可选择临时给客户端下发一些功能代码让客户端来执行,从而定制和扩展客户端的某些功能。比如服务端可以返回一些 Javascript 代码让客户端执行,去实现某些特定的功能。提示:REST架构中的设计准则中,只有按需编码为可选项。如果某个服务违反了其他任意一项准则,严格意思上不能称之为RESTful风格。
REST常见问题
RESTful API
实用的是如何正确地理解 RESTful架构和设计好RESTful API。
SpringBoot中使用
Spring Boot 构建 RESTful 风格应用
1.创建工程
2.导入依赖
3.配置数据库
4.编写实体类
@Entity(name = "t_book")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "book_name")
private String name;
private String author;
//省略 getter/setter
}
5.编写controller
public interface BookRepository extends JpaRepository<Book,Long> {
}
此时,一个标准的RESTful风格的项目就写完了。
根据 id 查询接口
- http://127.0.0.1:8080/books/{id}
分页查询
- http://127.0.0.1:8080/books
查询结果中,除了该有的数据之外,也包含了分页数据:
如果要分页或者排序查询,可以使用 _links 中的链接。http://127.0.0.1:8080/books?page=1&size=3&sort=id,desc
。
查询定制
最广泛的定制,就是查询,因为增删改操作的变化不像查询这么丰富。对于查询的定制,非常容易,只需要提供相关的方法即可。例如根据作者查询书籍:
public interface BookRepository extends JpaRepository {
List findBookByAuthorContaining(@Param("author") String author);
}
注意,方法的定义,参数要有 @Param 注解。
SpringBoot常用web注解
web注解
@GetMapping("users") //等价于 @RequestMapping(value="/users",method=RequestMethod.GET)
@PostMapping("users") //等价于@RequestMapping(value="/users",method=RequestMethod.POST)
@PutMapping("/users/{userId}") //等价于@RequestMapping(value="/users/{userId}",method=RequestMethod.PUT)
@DeleteMapping("/users/{userId}") //等价于@RequestMapping(value="/users/{userId}",method=RequestMethod.DELETE)
@PatchMapping("/profile") //一般实际项目中,我们都是 PUT 不够用了之后才用 PATCH 请求去更新数据。
//----------前后端传值---------------
@PathVariable //用于获取路径参数
@RequestParam //用于获取查询参数。
@ResponseBody //将java对象转为json格式的数据然后返回。
举个简单的例子:
@GetMapping("/klasses/{klassId}/teachers")
public List getKlassRelatedTeachers(
@PathVariable("klassId") Long klassId,
@RequestParam(value = "type", required = false) String type ) {
...
}
如果我们请求的 url 是:/klasses/{123456}/teachers?type=web
那么我们服务获取到的数据就是:klassId=123456,type=web
@PathVariable(“name”) String name //这个注解甚至可以映射
这一切的注解和花哨操作都是基于两个接口
HttpServletRequest
HttpServletResponse
@Controller
public class DemoController {
@RequestMapping("/demo")
@ResponseBody
public String print(HttpServletRequest request){
String name = request.getParameter("name");
System.out.println("Hello," + name);
return name;
}
}
关于 HttpServletRequest 接口是非常非常非常常用的。
方法 | 用途 |
---|---|
getRequestURL() | 返回客户端发出请求时的完整 URL。 |
getRequestURI() | 返回请求行中的参数部分。 |
getQueryString () | 方法返回请求行中的参数部分(参数名 + 值) |
getRemoteHost() | 返回发出请求的客户机的完整主机名。 |
getRemoteAddr() | 返回发出请求的客户机的 IP 地址。 |
getPathInfo() | 返回请求 URL 中的额外路径信息。额外路径信息是请求 URL 中的位于 Servlet 的路径之后和查询参数之前的内容,它以 “/“ 开头。 |
getRemotePort() | 返回客户机所使用的网络端口号。 |
getLocalAddr() | 返回 WEB 服务器的 IP 地址。 |
getLocalName() | 返回 WEB 服务器的主机名。 |
getHeader(string name) | 以 String 的形式返回指定请求头的值。如果该请求不包含指定名称的头,则此方法返回 null。如果有多个具有相同名称的头,则此方法返回请求中的第一个头。头名称是不区分大小写的。可以将此方法与任何请求头一起使用 |
getParameter(String name) | 根据 name 获取请求参数 (常用) |
@RequestBody //用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且Content-Type 为 application/json 格式的数据
@Valid //用于验证接收参数是否符合pojo定义的要求
@RequestParam //将前台传过来的名称进行转换
我用一个简单的例子来给演示一下基本使用!
我们有一个注册的接口:
@PostMapping("/sign-up")
public ResponseEntity signUp(@RequestBody @Valid UserRegisterRequest userRegisterRequest) {
userService.save(userRegisterRequest);
return ResponseEntity.ok().build();
}
第二个例子
@Controller
public class DemoController {
@RequestMapping("/demo")
@ResponseBody
public String print(@RequestParam("nick") String name){
System.out.println("Hello," + name);
return name;
}
}
UserRegisterRequest
对象:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRegisterRequest {
@NotBlank
private String userName;
@NotBlank
private String password;
@FullName
@NotBlank
private String fullName;
}
我们发送 post 请求到这个接口,并且 body 携带 JSON 数据:
{
"userName":"coder","fullName":"shuangkou","password":"123456"}
这样我们的后端就可以直接把 json 格式的数据映射到我们的 UserRegisterRequest
类上。
需要注意的是:一个请求方法只可以有一个@RequestBody
,但是可以有多个@RequestParam
和@PathVariable
。 如果你的方法必须要用两个 @RequestBody
来接受数据的话,大概率是你的数据库设计或者系统设计出问题了!
JSR注解
@NotEmpty //被注释的字符串的不能为 null 也不能为空
@NotBlank //被注释的字符串非 null,并且必须包含一个非空白字符
@Null //被注释的元素必须为 null
@NotNull //被注释的元素必须不为 null
@AssertTrue //被注释的元素必须为 true
@AssertFalse //被注释的元素必须为 false
@Pattern(regex=,flag=) //被注释的元素必须符合指定的正则表达式
@Email //被注释的元素必须是 Email 格式。
@Min(value) //被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) //被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) //被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) //被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max=, min=) //被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) //被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past //被注释的元素必须是一个过去的日期
@Future //被注释的元素必须是一个将来的日期
创建表
@Entity
声明一个类对应一个数据库实体。
@Table
设置表明
@Entity
@Table(name = "role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
}
创建主键
@Id
:声明一个字段为主键。
使用@Id
声明之后,我们还需要定义主键的生成策略。我们可以使用 @GeneratedValue
指定主键生成策略。
1.通过 @GeneratedValue
直接使用 JPA 内置提供的四种主键生成策略来指定主键生成策略。
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
2.通过 @GenericGenerator
声明一个主键策略,然后 @GeneratedValue
使用这个策略
@Id
@GeneratedValue(generator = "IdentityIdGenerator")
@GenericGenerator(name = "IdentityIdGenerator", strategy = "identity")
private Long id;
等价于:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
设置字段类型
@Column
声明字段。
示例:
设置属性 userName 对应的数据库字段名为 user_name,长度为 32,非空
@Column(name = "user_name", nullable = false, length=32)
private String userName;
设置字段类型并且加默认值,这个还是挺常用的。
Column(columnDefinition ="tinyint(1) default 1")
private Boolean enabled;
指定不持久化特定字段
@Transient
:声明不需要与数据库映射的字段,在保存的时候不需要保存进数据库 。
如果我们想让secrect
这个字段不被持久化,可以使用 @Transient
关键字声明。
Entity(name="USER")
public class User {
......
@Transient
private String secrect; // not persistent because of @Transient
}
除了 @Transient
关键字声明, 还可以采用下面几种方法:
static String secrect; // not persistent because of static
final String secrect = “Satish”; // not persistent because of final
transient String secrect; // not persistent because of transient
一般使用注解的方式比较多。
声明大字段
@Lob
:声明某个字段为大字段。
@Lob
private String content;
更详细的声明:
@Lob
//指定 Lob 类型数据的获取策略, FetchType.EAGER 表示非延迟 加载,而 FetchType. LAZY 表示延迟加载 ;
@Basic(fetch = FetchType.EAGER)
//columnDefinition 属性指定数据表对应的 Lob 字段类型
@Column(name = "content", columnDefinition = "LONGTEXT NOT NULL")
private String content;
创建枚举类型的字段
可以使用枚举类型的字段,不过枚举字段要用@Enumerated
注解修饰。
public enum Gender {
MALE("男性"),
FEMALE("女性");
private String value;
Gender(String str){
value=str;
}
}
@Entity
@Table(name = "role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
@Enumerated(EnumType.STRING)
private Gender gender;
省略getter/setter......
}