Skip to content

Commit

Permalink
Merge branch 'master' of github.com:Ikki-Dai/blogs
Browse files Browse the repository at this point in the history
  • Loading branch information
Ikki-Dai committed Feb 11, 2022
2 parents b5197fe + de6385f commit 4312228
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 5 deletions.
41 changes: 36 additions & 5 deletions _posts/2019-02-23-Restful设计.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ Restful[^Rest] 是由 Roy Thomas Fielding[^Thomas] 在2000 年的博士论文中

### 一切皆资源

- 系统通常有一个用户登录,我们设计为 `/login`, 但是设计为 `POST /sessions ` 就略显别扭,虽然这个在逻辑上是说的通的
- 系统通常有一个用户登录,我们设计为 `/login`, ~~但是设计为 `POST /sessions ` 就略显别扭,虽然这个在逻辑上是说的通的~~
- `POST /session` 笔者见过这样的实践
- 播放音乐的接口,理解为创建一个播放状态,可以变更为 `POST /musics/{id}?status=play` , 修改某个音乐的状态为播放

### 方法动词
Expand All @@ -41,15 +42,32 @@ Restful[^Rest] 是由 Roy Thomas Fielding[^Thomas] 在2000 年的博士论文中
| PUT | 更新 | 修改, 比如 PUT /cars/231, 修改标号为231 的车辆信息 |
| DELETE | 删除 | 比如 DELETE /cars/231, 删除编号231 的车辆信息 |

- 多条件查询时, 使用GET方法只能在URL 后面缀上长串的 Key=Value ,显得啰嗦且难于记忆,这时,我们可以创建一个 `POST /cars/queries` 的URL ,表示创建一个查询; 如果设计成异步,可以在该接口返回202 状态码,并在body 里响应一个地址如: `https://domain.com/cars/queries/31421`,

- 使用一级URL 更有助于记忆和理解,但这不是绝对的
##### GET 和 POST 区别
- GET只接受ASCII 字符且长度受限, POST 不受限
- GET 设计时考虑参数可读性,参数的数量
- GET 请求更加容易被浏览器刷新,要考虑幂等性
- GET 请求容易被缓存

##### 实战建议
- 路径参数 (spring mvc 中 使用 `@PathVariable` 注解的参数)
- 建议处理可枚举的参数, 因为对监控不太友好, 按路径可能会统计出很多指标;
- 但是比如在一个多租户的系统中, 用来传递租户id,恰好有自然被 监控系统发现并自动分组
- 关于路径参数中使用id 以符合 restful 风格的问题:在没有副作用的情况下使用,(可能作者当年也没有想到,互联网的数据是一个指数级的增长吧,作者当初也许是设想在一个 MIS 系统中,很自然的传递和查看一些数据吧)。

- queryString (spring mvc 中 使用 `@RequestParameter` 注解的参数)
- 参考意见是: 如果一个地址很方便的被 使用者 copy/paste 进行传递,那么那建议 使用 `GET + QueryString` 的组合。比如,在监控系统中, 把查询条件在 queryString 中 传递,方便使用中,传递信息,打开浏览器 接收者便能直接查看到问题所在,而不是按照 对方所说的设置查询参数。 比如 kibana 和 grafana 的 url 通常都是这样很长的。
- 要注意GET URI 字符受限问题,比如某个资源的翻页查询,通常只有有限的5~6 个参数,不妨使用 `GET + QueryString` 组合。
- 如果查询条件过于复杂,服务器不能提供响应,不妨改为POST + 202 的异步组合, facebook 的 广告数据接口有类似的设计

### 使用正确的状态码

- 比如404, 表示接口找不到呢还是资源接口不存在? 我的理解是,restful接口的实现者,就算没有很好的实践 `HATEOAS` ,但是至少会提供接口文档,所以不会存在调用了一个不存在的接口的情况
- 比如404, 表示接口找不到呢还是资源接口不存在?
- 如果是被解读为资源不存在,那就是一个业务问题,对用户来说是能理解的,是合情合理的
- 如果是被解读接口不存在,这就是一个技术问题,是应该消灭在开发阶段的, 而不应该暴露在生产环境的。
- 综上, restful 原本就是 表示资源的,属于业务范畴,用404 表示资源不存在也是合理的。如果404 被解读成了接口找不到,那就是开发的问题。按照时间线来说,现有接口,后提供资源服务。

- 状态码会不会不够用?绝大部分情况下,一个接口总是返回一个甚至2个期望的结果,但是出现异常的情况却有很多,所以与其期望明确定义好每一种异常情况,不如做好异常业务状态的提示信息的用户体验
- 状态码会不会不够用?绝大部分情况下,一个REST API 总是返回一个甚至2个期望的结果,但是出现异常的情况却有很多,所以与其期望明确定义好每一种异常情况,不如做好异常业务状态的提示信息的用户体验

| 状态码 | 说明 |
| ------ | ---------- |
Expand All @@ -59,6 +77,19 @@ Restful[^Rest] 是由 Roy Thomas Fielding[^Thomas] 在2000 年的博士论文中
| 4xx | 错误请求 |
| 5xx | 服务器错误 |

### Richardson Maturity Model

| 级别 | 说明 |
| :---- | :--------- |
| Level 0 | The swarmp of POX|
| Level 1 | Resources |
| Level 2 | HTTP Verbs |
| Level 3 | Hypermedia Controls |

这个不展开细说,可能我们大部分的API 处于 1~2 或 2~3 的一个中间状态, 尝试了就是最好的。

我想说的是,不管restful 风格在实践过程中有多少 争议,既然存在这么一个 非标准的`准则`, 作为开发就应该去布道,思考,并反复实践,抽象我们的业务,使我们的API **尽量**去符合和遵循这样一个东西。

## 总结

- 如果你不熟悉Restful, 还是多在各种业务场景下练习抽象思维
Expand Down
10 changes: 10 additions & 0 deletions _posts/2021-05-12-序列化反序列化常用设置.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,15 @@ mybatis.configuration.default-enum-type-handler=org.apache.ibatis.type.EnumOrdin

- 枚举类型会被序列化为Ordinal 存储

## 关于枚举使用的一点建议

- 虽然框架都提供了 使用 ordinal 反序列化的设置, 实际使用中会出现 几个问题
- 枚举的 ordinal 和 枚举的顺序有关, 会被代码格式等工具破坏
- `java.lang.Enum` 默认不提供 `valueOf(int ordinal)` 方法, 在三方框架不提供适配时, 使用成本高, 需要手动封装
- 在提供给三方使用的SDK 或者jar 包里使用 枚举 更合适, 源代码不会被破坏






91 changes: 91 additions & 0 deletions _posts/2021-10-20-扫码登陆实战方案.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# 扫码登陆实战方案

## 需求

- 用户 使用手机扫描 浏览器登录页的二维码
- 用户手机上显示 用户许可页面,同时浏览器显示扫码成功,处于等待确认状态
- 用户在手机上确认登陆,浏览器显示确认成功并跳转登陆后页面

## 分析

1. 浏览器请求服务器 并生成二维码,显然二维码内含有一些信息能被对应的手机客户端识别
- 二维码内的信息不能被其他软件识别,否则的话,浏览器会被其他人的客户端识别并发起登陆请求
- 二维码被扫描后不能再次被识别,有可能被其他账号扫描并登陆
2. 浏览器二维码被展示出来后,需要实时监听到二维码的消费情况。
- 浏览器轮询显然是个不错的选择,但是我们可以使用**`Server Sent Event` **会是更好的选择
3. 二维码在一定时间内没有被消费,应当过期处理
- 断网从连不能影响消息的消费

## 代码实战

### 浏览器 获取二维码等待响应

```java
@GetMapping("/qr-code/{client_id}")
public ResponseEntity<SseEmitter> QRLogin(@PathVariable("client_id") String clientId) {
String authcode = UUID.randomUUID().toString();
SseEmitter sseEmitter = new SseEmitter(90 * 1000L); //90s 内没有完成 SSE会超时
String id = clientId + '@' + authcode;
BlockingQueue<String> queue = redisson.getBlockingQueue("SSE_QUE:" + id); // 生成通道
localCachedMap.put(id, true);

threadPoolTaskExecutor.submit(() -> {
Map<String, String> body = new HashMap<>();
body.put("clientId", clientId);
body.put("authCode", authcode);
body.put("redirectUrl", "immigrant.com/login/qr");

try {
sseEmitter.send(body, MediaType.APPLICATION_JSON);
while (true) {
String s = queue.take();
log.info(s);
if ("END".equals(s)) {
localCachedMap.put(id, false); //完成后会关闭通道, 二维码泄露也不会发送消息到服务端
sseEmitter.complete();
} else {
sseEmitter.send(s);
}
}

} catch (InterruptedException | IOException e) {
e.printStackTrace();
}


});
// SSE 链接销毁时,要设置通道关闭
sseEmitter.onCompletion(() -> {
queue.clear();
log.info("sse {} completed", id);
});


return ResponseEntity.ok(sseEmitter);
```


### 客户端扫描二维码并发送请求

```java
@PostMapping("/qr-code/{client_id}")
public ResponseEntity<SseEmitter> QRLogin(@PathVariable("client_id") String clientId,@RequestBody ScanRequest request) {
BlockingQueue<String> queue;
if (Boolean.TRUE.equals(localCachedMap.get(clientId))) {
queue = redisson.getBlockingQueue("SSE_QUE:" + clientId);
} else {
return new ResponseEntity<>(HttpStatus.GONE);
}

try {
queue.offer(objectMapper.writeValueAsString(scanRequest));
} catch (JsonProcessingException e) {
e.printStackTrace();
}

return ResponseEntity.noContent().build();
}
```



0 comments on commit 4312228

Please sign in to comment.