Skip to content

Commit 9cb5b89

Browse files
committed
引入前缀树,加速url解析,正确处理异常
1 parent e4d9700 commit 9cb5b89

File tree

8 files changed

+161
-101
lines changed

8 files changed

+161
-101
lines changed

README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
## 一言·古诗词 API
22

3-
<div style="text-align:center">
43
<img src="https://api.gushi.ci/all.svg">
5-
</div>
6-
74

85
### 简介
96

@@ -89,9 +86,8 @@ JSON调用可以获取来源、作者、分类等信息,可以供你自定义
8986
xhr.open('get', 'https://api.gushi.ci/all.txt');
9087
xhr.onreadystatechange = function () {
9188
if (xhr.readyState === 4) {
92-
var data = JSON.parse(xhr.responseText);
9389
var gushici = document.getElementById('gushici');
94-
gushici.innerText = data.content;
90+
gushici.innerText = xhr.responseText;
9591
}
9692
};
9793
xhr.send();
@@ -112,10 +108,16 @@ TXT调用和JSON调用基本一致,可以节省一些流量。或者,你甚
112108
4. ConvertUtil 负责转码
113109
5. Service 没有使用 Service Proxy,因此无需额外生成代码。
114110

115-
### 待改进
111+
### 更新历史
112+
113+
* 2018.08.06 1.1:
114+
1. 引入前缀树,使分类检索效率由 O(n) (n为所有分类数) 变为 O(L) (L为分类级数)。
115+
缺点是空间复杂度由 O(n) 变为 O(nL),代码复杂度增加60行
116+
2. 优化了正则匹配获取地址参数的逻辑
117+
3. 正确加入了全局的错误处理(包括 Router 和 EventBus)
118+
* 2018.08.05 1.0:初始版本,支持 4 种格式返回,支持按分类搜索
119+
116120

117-
1. 错误处理
118-
2. 优化部分可能会阻塞的代码
119121

120122
### 关于项目
121123

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>ma.luan</groupId>
88
<artifactId>yiyan</artifactId>
9-
<version>1.0</version>
9+
<version>1.1</version>
1010

1111
<properties>
1212
<java.version>1.8</java.version>

src/main/java/ma/luan/yiyan/MainVerticle.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ public void start() {
2929
.setPort(config().getJsonObject("redis").getInteger("port",6379))
3030
.setSelect(config().getJsonObject("redis").getInteger("select",0));
3131

32+
// 配置 RuntimeError 错误记录
33+
vertx.exceptionHandler(error -> log.error(error));
34+
3235
// 顺序部署 Verticle
3336
Future.<Void>succeededFuture()
3437
.compose(v -> Future.<String>future(s -> vertx.deployVerticle(new ApiVerticle(),new DeploymentOptions().setConfig(config()), s)))

src/main/java/ma/luan/yiyan/api/ApiVerticle.java

Lines changed: 42 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import io.vertx.core.AbstractVerticle;
44
import io.vertx.core.Future;
5+
import io.vertx.core.eventbus.ReplyException;
6+
import io.vertx.core.eventbus.ReplyFailure;
57
import io.vertx.core.http.HttpServerResponse;
68
import io.vertx.core.json.JsonArray;
79
import io.vertx.core.json.JsonObject;
@@ -14,24 +16,23 @@
1416
import org.apache.logging.log4j.Logger;
1517

1618
import java.util.Arrays;
17-
import java.util.regex.Matcher;
18-
import java.util.regex.Pattern;
19+
import java.util.stream.Collectors;
1920

2021
public class ApiVerticle extends AbstractVerticle {
21-
private Logger log = LogManager.getLogger(this.getClass());
22+
//private Logger log = LogManager.getLogger(this.getClass());
2223

2324
@Override
2425
public void start(Future<Void> startFuture) {
2526
Router router = Router.router(vertx);
2627
router.route().handler(BodyHandler.create());
2728
router.get("/*").handler(this::log); // 全局日志处理,会执行 next() 到下一个
2829
router.get("/").handler(this::handleRoot); // 首页
29-
router.get("/favicon.ico").handler(c -> returnError(c, new Exception("404"))); // 针对浏览器返回404
30+
router.get("/favicon.ico").handler(c -> c.fail(404)); // 针对浏览器返回404
3031
router.get("/log").handler(this::showLog); // 显示日志
31-
router.get("/*").handler(this::handleGushici); // 核心API调用
32-
router.route().last().handler(c -> { // 其他返回 404 (应该不会走到这里)
33-
returnError(c, new Exception("404"));
34-
});
32+
router.routeWithRegex("/([a-z0-9/]*)\\.?(txt|json|png|svg|)")
33+
.handler(this::handleGushici); // 核心API调用
34+
router.route().last().handler(c -> c.fail(404)) // 其他返回404
35+
.failureHandler(this::returnError); // 对上面所有的错误进行处理
3536
vertx
3637
.createHttpServer()
3738
.requestHandler(router::accept)
@@ -58,25 +59,25 @@ private void handleRoot(RoutingContext routingContext) {
5859
result.put("list", res.result().body());
5960
returnJsonWithCache(routingContext, result);
6061
} else {
61-
returnError(routingContext, res.cause());
62+
routingContext.fail(res.cause());
6263
}
6364
});
6465
}
6566

6667
private void handleGushici(RoutingContext routingContext) {
6768
// 这里有两层回调,因为第二层回调需要用到第一层回调的数据。
68-
parseURI(routingContext.normalisedPath()) // 获取到 URI 上面的参数
69+
parseURI(routingContext) // 获取到 URI 上面的参数
6970
.setHandler(params -> {
7071
if (params.succeeded()) {
7172
vertx.eventBus().<String>send(Key.GET_GUSHICI_FROM_REDIS, params.result(), res -> { // 从 Redis 拿数据
7273
if (res.succeeded()) {
7374
returnGushici(routingContext, res.result().body(), params.result());
7475
} else {
75-
returnError(routingContext, res.cause());
76+
routingContext.fail(res.cause());
7677
}
7778
});
7879
} else {
79-
returnError(routingContext, params.cause());
80+
routingContext.fail(params.cause());
8081
}
8182
});
8283
}
@@ -86,7 +87,7 @@ private void showLog(RoutingContext routingContext) {
8687
if (res.succeeded()) {
8788
returnJson(routingContext, res.result().body());
8889
} else {
89-
returnError(routingContext, res.cause());
90+
routingContext.fail(res.cause());
9091
}
9192
});
9293
}
@@ -104,15 +105,19 @@ private void returnJsonWithCache(RoutingContext routingContext, JsonObject jsonO
104105
.end(jsonObject.encodePrettily());
105106
}
106107

107-
private void returnError(RoutingContext routingContext, Throwable cause) {
108+
private void returnError(RoutingContext routingContext) {
108109
JsonObject result = new JsonObject();
109-
result.put("error", cause.getMessage());
110-
int statusCode = cause.getMessage().startsWith("404") ? 404 : 500;
111-
if (statusCode == 500) {
112-
log.error(cause);
110+
int errorCode = routingContext.statusCode() > 0 ? routingContext.statusCode() : 500;
111+
// 不懂 Vert.x 为什么 EventBus 和 Web 是两套异常系统
112+
if (routingContext.failure() instanceof ReplyException) {
113+
errorCode = ((ReplyException) routingContext.failure()).failureCode();
114+
}
115+
result.put("error-code", errorCode);
116+
if (routingContext.failure() != null) {
117+
result.put("reason", routingContext.failure().getMessage());
113118
}
114119
setCommonHeader(routingContext.response()
115-
.setStatusCode(statusCode)
120+
.setStatusCode(errorCode)
116121
.putHeader("content-type", "application/json; charset=utf-8"))
117122
.end(result.encodePrettily());
118123
}
@@ -146,15 +151,14 @@ private void returnGushici(RoutingContext routingContext, String obj, JsonObject
146151
.putHeader("Content-Length", res.result().length() + "")
147152
.write(res.result()).end();
148153
} else {
149-
returnError(routingContext, res.cause());
154+
routingContext.fail(res.cause());
150155
}
151156
});
152157
break;
153158
}
154159
default:
155-
returnError(routingContext, new Exception("参数错误"));
160+
routingContext.fail(new ReplyException(ReplyFailure.RECIPIENT_FAILURE, 400, "参数错误"));
156161
}
157-
158162
}
159163

160164
private HttpServerResponse setCommonHeader(HttpServerResponse response) {
@@ -173,56 +177,27 @@ private void log(RoutingContext routingContext) {
173177

174178
/**
175179
* 根据 uri 获取参数
176-
*
177-
* @param uri 例如:/shenghuo/buyi.png, /all
178-
* @return {format: "png", classes: [shenghuo, buyi]}, {format:"json", classes:[""]}
180+
* @param routingContext example: uri: /shenghuo/buyi.png , /all
181+
* @return {format: "png", categories: [shenghuo, buyi]}, {format:"json", categories:[""]}
179182
*/
180-
private Future<JsonObject> parseURI(String uri) {
183+
private Future<JsonObject> parseURI(RoutingContext routingContext) {
181184
Future<JsonObject> result = Future.future();
182-
if (uri.length() > 100) {
183-
result.fail(new IllegalArgumentException("参数太长了,别玩了"));
184-
return result;
185-
}
186-
187-
JsonObject pathParams = new JsonObject();
188-
189-
String rawClasses;
190-
String rawFormat = "";
191-
192-
if (uri.contains(".")) {
193-
Pattern pattern = Pattern.compile("/(.*)\\.(.*)");
194-
Matcher m = pattern.matcher(uri);
195-
if (!m.find()) {
196-
result.fail(new IllegalArgumentException("非法参数"));
197-
return result;
198-
}
199-
rawClasses = m.group(1);
200-
rawFormat = m.group(2);
201-
} else {
202-
rawClasses = uri.replaceFirst("/", "");
203-
}
204-
205-
206-
String[] classes = rawClasses.split("/");
207-
if (classes.length < 1) {
208-
result.fail(new IllegalArgumentException("非法参数"));
209-
return result;
210-
}
211-
classes[0] = classes[0].replaceFirst("all", "");
212-
pathParams.put("classes", new JsonArray(Arrays.asList(classes)));
213185

214-
// 处理文件后缀
186+
String rawCategory = routingContext.request().getParam("param0");
187+
String rawFormat = routingContext.request().getParam("param1");
188+
// 如果是 "all" 则当没有分类处理
189+
JsonArray categories = new JsonArray(
190+
Arrays.stream(rawCategory.split("/"))
191+
.filter(s -> !s.isEmpty())
192+
.filter(s -> !"all".equals(s))
193+
.collect(Collectors.toList()));
194+
// 默认 json
195+
String format = "".equals(rawFormat) ? "json" : rawFormat;
215196

216-
String format;
217-
if (Arrays.asList("json", "svg", "txt", "png", "").contains(rawFormat)) {
218-
format = "".equals(rawFormat) ? "json" : rawFormat;
219-
} else {
220-
result.fail(new IllegalArgumentException("非法参数"));
221-
return result;
222-
}
197+
JsonObject pathParams = new JsonObject();
198+
pathParams.put("categories", categories);
223199
pathParams.put("format", format);
224-
225200
result.complete(pathParams);
226201
return result;
227202
}
228-
}
203+
}

src/main/java/ma/luan/yiyan/service/DataService.java

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
import io.vertx.core.CompositeFuture;
55
import io.vertx.core.Future;
66
import io.vertx.core.eventbus.Message;
7+
import io.vertx.core.eventbus.ReplyException;
8+
import io.vertx.core.eventbus.ReplyFailure;
79
import io.vertx.core.json.JsonArray;
810
import io.vertx.core.json.JsonObject;
911
import io.vertx.redis.RedisClient;
1012
import io.vertx.redis.RedisOptions;
1113
import ma.luan.yiyan.constants.Key;
14+
import ma.luan.yiyan.util.CategoryTrie;
1215
import org.apache.logging.log4j.LogManager;
1316
import org.apache.logging.log4j.Logger;
1417

@@ -21,7 +24,7 @@ public class DataService extends AbstractVerticle {
2124
private Random random = new Random();
2225
private RedisOptions redisOptions;
2326
private Logger log = LogManager.getLogger(this.getClass());
24-
private static List<String> keysInRedis;
27+
private static CategoryTrie keysInRedis = new CategoryTrie();
2528

2629
public DataService(RedisOptions redisOptions) {
2730
this.redisOptions = redisOptions;
@@ -37,12 +40,12 @@ public void start(Future<Void> startFuture) {
3740
Future<JsonArray> jsonKeys = Future.future(f -> redisClient.keys(Key.JSON, f));
3841
CompositeFuture.all(Arrays.asList(imgKeys, jsonKeys)).setHandler(v -> {
3942
if (v.succeeded()) {
40-
keysInRedis = imgKeys.result().addAll(jsonKeys.result()).stream()
41-
.filter(String.class::isInstance)
42-
.map(String.class::cast)
43-
.collect(Collectors.toList());
43+
imgKeys.result().addAll(jsonKeys.result())
44+
.stream()
45+
.forEach(key -> keysInRedis.insert((String) key));
4446
startFuture.complete();
4547
} else {
48+
log.error(v.cause());
4649
startFuture.fail(v.cause());
4750
}
4851
});
@@ -66,46 +69,51 @@ private void getHelpFromRedis(Message message) {
6669
);
6770
message.reply(newArray);
6871
} else {
69-
message.reply(res.cause());
72+
log.error(res.cause());
73+
message.fail(500, res.cause().getMessage());
7074
}
7175
});
7276
}
7377

78+
/**
79+
* @param message example: {format: "png", categories: [shenghuo, buyi]}
80+
*/
7481
private void getGushiciFromRedis(Message<JsonObject> message) {
75-
String keyName = message.body().getJsonArray("classes").stream()
76-
.filter(String.class::isInstance)
77-
.map(String.class::cast)
78-
.collect(Collectors.joining(":"));
79-
keyName = ("png".equals(message.body().getString("format")) ? "img" : "json") + ":" + keyName;
80-
81-
checkAndGetKey(keyName)
82+
JsonArray realCategory = new JsonArray()
83+
.add("png".equals(message.body().getString("format")) ? "img" : "json")
84+
.addAll(message.body().getJsonArray("categories"));
85+
checkAndGetKey(realCategory)
8286
.compose(key -> Future.<String>future(s -> redisClient.srandmember(key, s))) // 从 set 随机返回一个对象
8387
.setHandler(res -> {
8488
if (res.succeeded()) {
8589
message.reply(res.result());
8690
} else {
87-
message.fail(404, res.cause().getMessage());
91+
if (res.cause() instanceof ReplyException) {
92+
ReplyException exception = (ReplyException) res.cause();
93+
message.fail(exception.failureCode(), exception.getMessage());
94+
}
95+
message.fail(500, res.cause().getMessage());
8896
}
8997
});
9098
}
9199

92100
/**
93-
* @param keys 用户请求的类别
101+
* @param categories 用户请求的类别 [img, shenghuo ,buyi]
94102
* @return 返回一个随机类别的 key (set)
95103
*/
96-
private Future<String> checkAndGetKey(String keys) {
104+
private Future<String> checkAndGetKey(JsonArray categories) {
97105
Future<String> result = Future.future();
98-
// 这里可以改用多级 Map 减少随机选择范围,不过创建 Map 也要一些开销
99-
List<String> toRandom = keysInRedis.stream()
100-
.filter(key -> key.startsWith(keys))
101-
.collect(Collectors.toList());
106+
List<String> categoryList = categories.stream()
107+
.filter(String.class::isInstance)
108+
.map(String.class::cast)
109+
.collect(Collectors.toList());
110+
List<String> toRandom = keysInRedis.getKeys(categoryList);
102111
if (toRandom.size() >= 1) {
103112
result.complete(toRandom.get(random.nextInt(toRandom.size())));
104113
} else {
105-
result.fail("404, 没有结果,请检查API");
114+
result.fail(new ReplyException(ReplyFailure.RECIPIENT_FAILURE, 404, "没有结果,请检查API"));
106115
}
107116
return result;
108-
109117
}
110118
}
111119

src/main/java/ma/luan/yiyan/service/LogService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ private void getHistoryFromRedis(Message<JsonObject> message) {
6666
message.reply(result);
6767
} else {
6868
log.error(v.cause());
69+
message.fail(500, v.cause().getMessage());
6970
}
7071
});
7172
}

0 commit comments

Comments
 (0)