SpringBoot构建RESTful风格应用
Spring Boot 构建 RESTful 风格应用
前后端不分离:
以前没有移动互联网时,我们做的大部分应用都是前后端不分的,比如jsp,或者thymeleaf等后端分离模板,在这种架构的应用中,数据基本上都是在后端渲染好返回给前端展示的,也就是后端需要控制前端的展示,前端与后端的耦合度很高。
这种应用模式比较适合纯网页应用,但是当后端对接App时,App可能并不需要后端返回一个HTML网页,而仅仅是数据本身,所以后端原本返回网页的接口不再适用于前端App应用,为了对接App后端还需再开发一套接口。这样前后端不分离就有局限性了。
前后端分离:
在前后端分离的应用模式中,后端仅返回前端所需的数据,不再渲染HTML页面,不再控制前端的效果。至于前端用户看到什么效果,从后端请求的数据如何加载到前端中,都由前端自己决定,网页有网页的处理方式,App有App的处理方式,但无论哪种前端,所需的数据基本相同,后端仅需开发一套逻辑对外提供数据即可。
在前后端分离的应用模式中 ,前端与后端的耦合度相对较低。
我们通常将后端开发的每个视图都称为一个接口,或者API,前端通过访问接口来对数据进行增删改查。
前面我们讲解了前后端不分离不分离模式,现在来讲解一下前后端分离模式怎么实现.
API:全称是 Application Programming Interface,应用程序编程接口。API是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。简单的说API 是一套协议,规定了我们与外界的沟通方式:如何发送请求和接收响应。
比如我们平时在QQ上可以看到天气信息,而这些天气信息不是腾讯公司用卫星监测到的,而是去调用气象局的天气信息的。但腾讯公司不需要也不能访问气象局的数据库和源码,而是通过调用气象局提供的一个公共函数来实现,我们只需要知道这个函数需要传递什么参数,以及返回什么样的数据就行,函数的内部结构我们并不需要知道。这个函数就是API。
为了在团队内部形成共识、防止个人习惯差异引起的混乱,我们需要找到一种大家都觉得很好的接口实现规范,
而且这种规范能够让后端写的接口,用途一目了然,减少双方之间的合作成本。所以出现了接口服务架构,目前市面上大部分公司开发人员使用的接口服务架构主要有:restful、rpc。
REST:那Rest是什么呢,它是一种架构风格,就像气象局建立API时要遵守的一种规则,可以是Rest也可以是其它规则。这种规则是为web应用服务的,也就是用URL定位资源,用HTTP动词(GET,POST,DELETE,PUT)描述操作,用HTTP Status Code返回结果状态的这种client和server的交互方式。实现看Url就知道要什么,看http 方法就知道干什么,看http status code就知道结果如何。
RESTFul:RESTful是一种定义Web API接口的设计风格,尤其适用于前后端分离的应用模式中。RESTFul就是为了实现REST这种交互方式而制定的一套约束条件和规则,符合这些约束条件和原则的应用程序或设计就是RESTful。也就是REST本身不实用,实用的是如何设计 RESTful API(REST风格的网络接口)。这种风格的理念认为后端开发任务就是提供数据的,对外提供的是数据资源的访问接口,所以在定义接口时,客户端访问的URL路径就表示这种要操作的数据资源。
那么RESTFul有哪些设计规范呢?
API与用户的通信协议,使用HTTPs协议或者HTTP协议,统一确定用一种。
应该尽量将API部署在专用域名之下,如https://xxx.xxx.com;
如果多个项目创建API,把项目名称带上 如[https://项目名.XXX.com
应该将API的版本号放入URL。
http://www.example.com/app/1.0/foo
http://www.example.com/app/1.1/foo
http://www.example.com/app/2.0/foo
另一种做法是,将版本号放在HTTP头信息中,但不如放入URL方便和直观。Github就采用了这种做法。
因为不同的版本,可以理解成同一种资源的不同表现形式,所以应该采用同一个URL。版本号可以在HTTP请求头信息的Accept字段中进行区分
Accept: vnd.example-com.foo+json; version=1.0
Accept: vnd.example-com.foo+json; version=1.1
路径又称"终点"(endpoint),表示API的具体网址,每个网址代表一种资源(resource)
(1) 资源作为网址,只能有名词,不能有动词,而且所用的名词往往与数据库的表名对应。
举例来说,以下是不好的例子:
/selectGoods
/listOrders
/retreiveClientByOrder?orderId=1
对于一个简洁结构,你应该始终用名词。 此外,利用的HTTP方法可以分离网址中的资源名称的操作。
GET /goods :将返回所有商品清单
POST /goods :将商品新建到集合
GET /goods/4 :将获取商品 4
PATCH(或)PUT /goods/4 :将更新商品 4
(2) API中的名词应该使用复数。无论子资源或者所有资源。
举例来说,获取产品的API可以这样定义
获取单个产品:http://127.0.0.1:8080/AppName/goods/1
获取所有产品: http://127.0.0.1:8080/AppName/goods
对于资源的具体操作类型,由HTTP动词表示。
常用的HTTP动词有下面四个(括号里是对应的SQL命令)。
- GET(SELECT):从服务器取出资源(一项或多项)。
- POST(CREATE):在服务器新建一个资源。
- PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
- DELETE(DELETE):从服务器删除资源。
还有三个不常用的HTTP动词。
- PATCH(UPDATE):在服务器更新(更新)资源(客户端提供改变的属性)。
- HEAD:获取资源的元数据。
- OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。
下面是一些例子。
GET /goods:列出所有商品清单
POST /goods:新建一个商品(上传文件)
GET /goods/ID:获取某个指定商品的信息
PUT /goods/ID:更新某个指定商品的信息(提供该商品的全部信息)
PATCH /goods/ID:更新某个指定商品的信息(提供该商品的部分信息)
DELETE /goods/ID:删除某个商品
GET /goods/ID/attributes:列出某个指定商品的所有属性信息
DELETE /goods/ID/attributes/ID:删除某个指定商品的指定属性
如果记录数量很多,服务器不可能都将它们返回给用户,API会提供参数,过滤返回结果,用于补充规范一些通用字段,常见的参数有:
1. ?limit=20:指定返回记录的数量为20;
2. ?offset=8:指定返回记录的开始位置为8;
3. ?page=1&per_page=50:指定第1页,以及每页的记录数为50;
4. ?sortby=name&order=asc:指定返回结果按照name属性进行升序排序;
5. ?attr_id=2:指定筛选条件。
服务器会向用户返回状态码和提示信息,以下是常用的一些状态码,可以根据实际业务添加对应的状态码,需和http状态码对应:
1. 200 OK - [GET]:服务器成功返回用户请求的数据;
2. 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功;
3. 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务);
4. 204 NO CONTENT - [DELETE]:用户删除数据成功;
5. 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作;
6. 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误);
7. 403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的;
8. 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作;
9. 406 Not Acceptable - [GET]:用户请求的格式不可得;
10. 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的;
11. 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误;
12. 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
如果状态码是4xx,服务器就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。
比如Google 的出错信息:
{
"error": {
"errors": [
{
"domain": "global",
"reason": "insufficientFilePermissions",
"message": "The user does not have sufficient permissions for file {fileId}."
}
],
"code": 403,
"message": "The user does not have sufficient permissions for file {fileId}."
}
}
针对不同操作,服务器向用户返回的结果应该符合以下规范。
GET /collection:返回资源对象的列表(数组)
GET /collection/ID:返回单个资源对象(json)
POST /collection:返回新生成的资源对象(json)
PUT /collection/ID:返回完整的资源对象(json)
DELETE /collection/ID:返回一个空文档(空字符串)
比如:
code:200,
msg:查询成功
data:[{},{},{}]
RESTful API最好做到Hypermedia(即返回结果中提供链接,连向其他API方法),使得用户不查文档,也知道下一步应该做什么。
比如,Github的API就是这种设计,访问api.github.com会得到一个所有可用API的网址列表。
{
"current_user_url": "https://api.github.com/user",
"authorizations_url": "https://api.github.com/authorizations",
// ...
}
从上面可以看到,如果想获取当前用户的信息,应该去访问api.github.com/user,然后就得到了下面结果。
{
"message": "Requires authentication",
"documentation_url": "https://developer.github.com/v3"
}
上面代码表示,服务器给出了提示信息,以及文档的网址。
服务器返回的数据格式,应该尽量使用JSON,避免使用XML。
注意:上面的规范是一些约定的,并不是强制,可以不遵守,也可以只遵守几条
后端:
@ResponseBody
@GetMapping("/users/{id}")
public User getUserById(@PathVariable("id") Integer id){
return userService.getUserById(id);
}
前端:
localhost:8080/users/1
后端:
@ResponseBody
@PostMapping("/users")
public int addUser(User user){
return userService.addUser(user);
}
前端:
<form action="http://localhost:8080/users" method="post">
<input type="text" name="uname" />
<input type="text" name="age" />
<input type="submit" value="提交"/>
</form>
后端:
@ResponseBody
@PutMapping("/users")
public Users updateUser(User user){
return userService.updateUser(user);
}
前端:
<form action="http://localhost:8080/users" method="post">
<input type="hidden" name="id" />
<input type="text" name="uname" />
<input type="text" name="age" />
<input type="hidden" name="_method" value="PUT"/> <!-- 在表单中添加 _method 提交put 请求-->
<input type="submit" value="提交"/>
</form>
后端:
@DeleteMapping("/users/{id}")
@ResponseBody
public String deleteUser(@PathVariable("id") Integer id){
return userService.deleteUserById(id);
}
前端:
<form action="http://localhost:8083/user/1" method="post">
<input type="hidden" name="_method" value="DELETE"/>
<input type="submit" value="提交"/>
</form>
异步请求,对于PUT和DELETE请求,使用post方法提交,在发送的数据中加上_method=PUT/DELETE
什么是统一响应体呢?在前后端分离架构下,后端主要是一个RESTful API
的数据接口。接口中有时返回数据,有时又没有,还有的会出错,也就是结果不一致。只用http状态码表达不够。
那么可以通过修改响应返回的JSON
数据,让其带上一些固有的字段,例如以下这样的:
{
"code": 600,
"msg": "success",
"data": {
"id": 1,
"uname": "daimenglaoshi"
"qq":2398779723
}
}
1.创建一个响应结果类
package com.test.restful.util;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseResult<T> {
public int code; //返回状态码
private String msg; //返回描述信息
private T data; //返回内容体
}
2.创建一个生成响应结果类的类
package com.test.restful.util;
public class Response {
private final static String SUCCESS = "success";
private final static String FAIL = "fail";
//不同的响应情景
public static <T> ResponseResult<T> makeOKRsp() {
return new ResponseResult<T>(200,SUCCESS,null);
}
public static <T> ResponseResult<T> makeOKRsp(String message) {
return new ResponseResult<T>(200,message,null);
}
public static <T> ResponseResult<T> makeOKRsp(T data) {
return new ResponseResult<T>(200,SUCCESS,data);
}
public static <T> ResponseResult<T> makeErrRsp(String message) {
return new ResponseResult<T>(500,message,null);
}
public static <T> ResponseResult<T> makeErrRsp() {
return new ResponseResult<T>(500,FAIL,null);
}
public static <T> ResponseResult<T> makeRsp(int code, String msg, T data) {
return new ResponseResult<T>(code,msg,data);
}
}
3.控制层调用
@GetMapping("/users")
public ResponseResult<List<User>> findUsers()
{
List<User> userList= userService.findUsers();
return Response.makeOKRsp(userList);
}
4.Postman测试
5.封装状态码
我们也可以将状态码封装到枚举类中
比如:
package com.test.restful.util;
public enum CodeStatus {
// 成功
SUCCESS(200),
// 错误的请求
FAIL(400),
// 访问被拒绝,比如未认证(签名错误)
UNAUTHORIZED(401),
// 接口不存在
NOT_FOUND(404),
// 服务器内部错误
INTERNAL_SERVER_ERROR(500);
//自定义 状态码
NOT_ALLOWRD_REG(1001);
public int code;
CodeStatus(int code) {
this.code = code;
}
}
那么我们在Response类中使用状态码时可以替换成CodeStatus来访问,视频中会有详细讲解。
在使用统一响应结果的时候,还会遇到一种情况,就是程序的报错是由于运行时出异常导致的,有些异常是我们可以提前预知在业务中抛出,有些则是无法提前预知的。不管能否预知,我们都需要对异常处理,如果我们可以定义一个统一的全局异常处理,在Controller
捕获所有异常,并且做适当处理,也做成统一响应体返回就好了。
- 自定义一个异常类(如:
NotAllowedRegException
),捕获针对项目或业务的某个异常; - 使用
@ExceptionHandler
注解处理自定义异常和通用异常,并指明异常的处理类型; - 使用
@ControllerAdvice
接收所有的控制层方法抛出的异常
创建NotAllowedRegException类
package com.test.restful.exception;
import lombok.Data;
@Data
public class NotAllowedRegException extends Exception {
private int code; //这里的状态码 在 StatusCode枚举类设置好
private String message="异常:该用户不允许注册";
public NotAllowedRegException(String msg) {
super(msg);
}
public NotAllowedRegException() {
}
}
创建统一异常处理类GlobalExceptionHandler:
package com.test.restful.controller;
import com.test.restful.exception.NotAllowedRegException;
import com.test.restful.util.Response;
import com.test.restful.util.ResponseResult;
import com.test.restful.util.StatusCode;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/*
@RestControllerAdvice=@RestController+ @ControllerAdvice @ControllerAdvice字面上意思是“控制器通知”,作用是接收所有的控制层方法抛出的异常
如果你只想对一部分控制器添加通知,比如某个包下的控制器,可以写@RestControllerAdvice("包名")
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
//@ExceptionHandler 注解用来指明异常的处理类型,即如果这里指定为 NullpointerException,则数组越界异常就不会进到这个方法中来。
@ExceptionHandler(Exception.class)
public ResponseResult handlerException(Exception e) {
// 自定义异常
if (e instanceof NotAllowedRegException) {
return Response.createFailResp(StatusCode.NOT_ALLOWRD_REG.code,((NotAllowedRegException) e).getMessage());
}else {
// 其他异常,当我们定义了多个异常时,这里可以增加判断和记录
return Response.createFailResp(StatusCode.SERVER_ERROR.code,e.getMessage());
}
}
}
创建控制层方法测试:
package com.test.restful.controller;
import com.test.restful.exception.NotAllowedRegException;
import com.test.restful.pojo.User;
import com.test.restful.util.Response;
import com.test.restful.util.ResponseResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class User3Controller {
@PostMapping("/users")
public ResponseResult addUser(User user) throws NotAllowedRegException {
if(user.getUname().equals("daimenglaoshi"))
throw new NotAllowedRegException();
return Response.createOkResp();
}
}
测试结果: