Vert.x,Web - Restful API

embedded/2024/10/18 0:22:08/

将通过Vert.x Web编写一个前后分离的Web应用,做为Vert.x Web学习小结。本文为后端部分,后端实现业务逻辑,并通过RESTfull接口给前端(Web页面)调用。

案例概述

假设我们要设计一个人力资源(HR)系统,要实现对员工信息的增删改查。我们将前端和后端设计成两个Verticle,这样可以实现灵活的部署,可以将前端和后端部署在一个JVM上,也可以部署到不同的JVM或者不同的服务器上。

员工信息存放在MySQL数据库中,所以需要先创建对应数据库和表:

create database hr;
use hr;create table emp (empno int not null auto_increment,ename varchar(24),job varchar(16),constraint emp_pk primary key(empno)
);insert into emp values(7369, 'SMITH', 'CLERK');
insert into emp values(7499, 'ALLEN', 'SALESMAN');
insert into emp values(7521, 'WARD', 'SALESMAN');
insert into emp values(7566, 'JONES', 'MANAGER');
insert into emp values(7654, 'MARTIN', 'SALESMAN');
insert into emp values(7698, 'BLAKE', 'MANAGER');
insert into emp values(7782, 'CLARK', 'MANAGER');
insert into emp values(7788, 'SCOTT', 'ANALYST');
insert into emp values(7839, 'KING', 'PRESIDENT');
insert into emp values(7844, 'TURNER', 'SALESMAN');
insert into emp values(7876, 'ADAMS', 'CLERK');
insert into emp values(7900, 'JAMES', 'CLERK');
insert into emp values(7902, 'FORD', 'ANALYST');
insert into emp values(7934, 'MILLER', 'CLERK');

后端Restfull实现

后端设计如下的Restful AIP:

请求方法    请求路径                        功能说明      
---------- ----------------------------- ------------- 
GET        /api/v1/hr/employees          获取员工列表
POST       /api/v1/hr/employees          创建新员工
GET        /api/v1/hr/employees/{empNo}  获取员工信息
DELETE     /api/v1/hr/employees/{empNo}  删除一个员工
PUT        /api/v1/hr/employees/{empNo}  修改员工信息 

因为需要使用HTTP并访问MySQL数据库,所以需要在项目中引入相关依赖:

<dependency><groupId>io.vertx</groupId><artifactId>vertx-core</artifactId><version>4.5.10</version>
</dependency>
<dependency><groupId>io.vertx</groupId><artifactId>vertx-web</artifactId><version>4.5.10</version>
</dependency>
<dependency><groupId>io.vertx</groupId><artifactId>vertx-mysql-client</artifactId><version>4.5.10</version>
</dependency>

数据库连接

创建SQL客户端用于访问数据库,程序架构大致如下:

java">public class HrWebService extends AbstractVerticle {public HrWebService() {MySQLConnectOptions connectOptions = new MySQLConnectOptions().setHost("127.0.0.1").setPort(3306).setUser("root").setPassword("Passw0rd").setDatabase("hr").setConnectTimeout(2000).addProperty("autoReconnect", "true").addProperty("useSSL","false").addProperty("rewriteBatchedStatements", "true");PoolOptions poolOptions = new PoolOptions().setMaxSize(5);client = MySQLBuilder.client().using(vertx).with(poolOptions).connectingTo(connectOptions).build();}@Overridepublic void stop() throws Exception {if (null != client) { //停止时候释放连接。client.close();}}@Overridepublic void start() throws Exception {HttpServer server = vertx.createHttpServer();Router router = Router.router(vertx);// 在这里添加路由...server.requestHandler(router).listen(8081);}
}

接口基础部分

因为POST/PUT方法需要使用请求体传递数据,所以需要允许请求体,为避免受到攻击,设置请求体的最大大小为100KB。

java">router.route("/api/v1/hr/*").handler(BodyHandler.create().setBodyLimit(100 * 1024)); 

出于安全性,浏览器通常会限制脚本内发起的跨源HTTP请求。所以需要添加相关的CORS响应标头,来允许跨域访问,否则前端调用会报错。

java">router.route("/api/v1/hr/*").handler(newCorsHandler());
public static CorsHandler newCorsHandler() {/** 设置支持跨域访问/CORS */Set<String> allowedHeaders = new HashSet<>();allowedHeaders.add("x-requested-with");allowedHeaders.add("Access-Control-Allow-Origin");allowedHeaders.add("origin");allowedHeaders.add("Content-Type");allowedHeaders.add("accept");allowedHeaders.add("X-PINGARUNER");Set<HttpMethod> allowedMethods = new HashSet<>();allowedMethods.add(HttpMethod.GET);allowedMethods.add(HttpMethod.POST);allowedMethods.add(HttpMethod.OPTIONS);allowedMethods.add(HttpMethod.DELETE);allowedMethods.add(HttpMethod.PATCH);allowedMethods.add(HttpMethod.PUT);return CorsHandler.create().addOrigin("*") // Access-Control-Allow-Origin.allowedHeaders(allowedHeaders) // Access-Control-Request-Method.allowedMethods(allowedMethods); // Access-Control-Request-Headers
}

最后为(HR)应用设置一个路由错误处理器:

java">router.route("/api/v1/hr/*").failureHandler(this::defaultErrorHandler);public void defaultErrorHandler(RoutingContext routingContext) {Throwable exception = routingContext.failure();int statusCode = routingContext.statusCode(); 服务器记录日志HttpServerRequest request = routingContext.request();String method = request.method().name();String uri = request.absoluteURI();LOGGER.log(Level.SEVERE, method + " " + uri + ", statusCode: " + statusCode, exception); 返回错误信息HttpServerResponse response = routingContext.response();response.setStatusCode(statusCode); // 必须设置, 默认: 200 OK// response.setStatusMessage(exception.getMessage()); // 可覆盖, 默认是statusCode对应的错误信息。// 返回Json格式错误信息: {"error":{"code":500, "message":"Error message here"}}JsonArray errorArray = new JsonArray().add(new JsonObject().put("code", statusCode)).add(new JsonObject().put("message", exception.getMessage()));JsonObject respObj = new JsonObject().put("error", errorArray);response.end(respObj.toString());
}

这里设计上,客户端需要通过HTTP的statusCode来判断请求的释放成功,正常走API的结果解析,错误走这个错误结果解析。也可以在内部出错的时候(status code)500,依然返回200,只是把错误信息和代码放在返回的json中,可以根据自己需要规划。

获取员工列表接口

该接口用于获取员工列表。因为员工数量比较多,需要支持分页。

请求路径: GET /api/v1/hr/employees
请求参数: page , 整型, 非必选, 请求数据的分页页码, 默认值: 1limit, 整型, 非必选, 请求数据的分页大小, 默认值: 5
返回结果:count     , 整型, 总记录数。data      , 数组, 员工信息的数组。数据结构, 对应emp表的行。successful, 布尔类型, 请求是否成功。duration  , 整型, 服务端处理请求的时间(毫秒)。

接口需要总记录数和请求页码的数据,实现上通过2条语句获取,通过Future.all方法将两个异步查询组合在一起,并将结果返回:

java">router.route(HttpMethod.GET, "/api/v1/hr/employees").handler(this::getEmployees);public void getEmployees(RoutingContext routingContext) {long startTime = System.currentTimeMillis();// 获取url请求参数HttpServerRequest request = routingContext.request();String p = request.getParam("page", "1"); // 获取url请求参数page,默认第1页。String l = request.getParam("limit", "5"); // 获取参数limit,默认值5。int page = Integer.parseInt(p);int rowCount = Integer.parseInt(l);int offset = (page - 1) * rowCount; // 计算记录偏移值。HttpServerResponse response = routingContext.response();response.putHeader("content-type", "application/json");JsonObject resultObject = new JsonObject(); // 用于保存结果。String sqlText1 = "select empno, ename, job from emp order by empno desc limit ?, ?";Future<RowSet<Row>> future1 = client.preparedQuery(sqlText1).execute(Tuple.of(offset, rowCount)).onSuccess(rows -> {JsonArray resultArray = new JsonArray(); //保存查询结果集(Array)for (Row row : rows) {JsonObject json = row.toJson();resultArray.add(json);}resultObject.put("data", resultArray); });String sqlText2 = "select count(empno) as cnt from emp";Future<RowSet<Row>> future2 = client.preparedQuery(sqlText2).execute().onSuccess(rows -> {for (Row row : rows) {resultObject.put("count", row.getValue("cnt")); // 总记录数,通常前端计算分页用。}});Future.all(future1, future2).onComplete(ar -> { // 组合两个查询,两个异步都完成时候返回完成。if (ar.succeeded()) {resultObject.put("successful", true); // 设置请求结果为成功。long endTime = System.currentTimeMillis();resultObject.put("duration", endTime - startTime); // 计算执行时间。response.end(resultObject.toString()); // 返回API结果。} else {routingContext.fail(ar.cause());}});
}

通过Postman测试接口:GET http://127.0.0.1:8081/api/v1/hr/employees?page=2&limit=2
在这里插入图片描述
关闭数据库,模拟失败调用,再次执行接口调用:GET http://127.0.0.1:8081/api/v1/hr/employees?page=2&limit=2

root@localhost [hr]> shutdown ;
Query OK, 0 rows affected (0.00 sec)

在这里插入图片描述

创建新员工接口

该接口用创建新员工。

请求路径: POST /api/v1/hr/employees
请求参数: ename, 字符串, 必选, 新员工姓名。job, 字符串, 必选, 新员工职位。
返回结果:empno, 整型, 新员工编号。

代码实现上,获取(Body)请求参数,插入数据库后,API返回员工编号(empno):

java">
router.route(HttpMethod.POST, "/api/v1/hr/employees").handler(this::newEmployee);public void newEmployee(RoutingContext routingContext) {JsonObject empObject = routingContext.body().asJsonObject();String ename = empObject.getString("ename");String job = empObject.getString("job");if (StringUtils.isBlank(ename) || StringUtils.isBlank(job)) { // apache commons-lang3// 有两种方式抛出失败: 调用routingContext.fail方法,并返回处理器方法或者抛出RuntimeException异常。routingContext.fail(new Exception("员工名或者职位不能为空白。"));return ; // 注意, 必须函数返回,否则还会继续调用后续代码。//throw new RuntimeException("员工名或者职位不能为空白。");}String sqlText = "insert into emp (ename, job) values (?, ?)";client.preparedQuery(sqlText).execute(Tuple.of(ename, job)).onSuccess(rows -> {long lastInsertId = rows.property(MySQLClient.LAST_INSERTED_ID);HttpServerResponse response = routingContext.response();response.putHeader("content-type", "application/json");JsonObject responseObject = new JsonObject();responseObject.put("empno", lastInsertId);response.end(responseObject.toString());}).onFailure(exception -> {routingContext.fail(exception);});
}

通过Postman测试接口,正常调用:
在这里插入图片描述
模拟错误参数,job为空字符串。
在这里插入图片描述

删除员工信息接口

该接口用于删除员工信息。

请求路径: DELETE /api/v1/hr/employees/{empNo}
请求参数: empNo, 整型, 必选, 需要删除的员工编号。

需要url的路径参数方式获取员工编号:

java">router.route(HttpMethod.DELETE, "/api/v1/hr/employees/:empNo").handler(this::deleteEmployee);public void deleteEmployee(RoutingContext routingContext) {String en = routingContext.pathParam("empNo");int empNo = 0;try {empNo = Integer.parseInt(en);} catch (NumberFormatException e) {routingContext.fail(new Exception("无效的请求路径, " + e.getMessage(), e));return;}String sqlText = "delete from emp where empno = ?";client.preparedQuery(sqlText).execute(Tuple.of(empNo)).onSuccess(rows -> {HttpServerResponse response = routingContext.response();response.end();});
}

测试接口:
在这里插入图片描述
失败调用。
在这里插入图片描述

修改员工信息接口

该接口用于修改员工信息。

请求路径: PUT /api/v1/hr/employees/{empNo}
请求路径: POST /api/v1/hr/employees
请求参数: empno, 整型,必选,需要修改的员工编号。ename, 字符串, 必选, 新的员工姓名。job, 字符串, 必选, 新的员工职位。返回结果:rows, 整型,已修改的记录数。

实现代码如下:

java">router.route(HttpMethod.PUT, "/api/v1/hr/employees/:empNo").handler(this::updateEmployee);public void updateEmployee(RoutingContext routingContext) {HttpServerResponse response = routingContext.response();response.putHeader("content-type", "application/json");String en = routingContext.pathParam("empNo");int empNo = 0;try {empNo = Integer.parseInt(en);} catch (NumberFormatException e) {throw new RuntimeException("无效的请求路径, " + e.getMessage(), e);}JsonObject empObject = routingContext.body().asJsonObject();String newEname = empObject.getString("ename");String newJob = empObject.getString("job");if (StringUtils.isBlank(newEname) || StringUtils.isBlank(newJob)) {throw new RuntimeException("新的员工名或者职位不能为空。");}String sqlText = "update emp set ename=?, job=? where empno = ?";client.preparedQuery(sqlText).execute(Tuple.of(newEname, newJob, empNo)).onSuccess(rows -> {response.end("{\"rows\": " + rows.rowCount() + "}");}).onFailure(exception -> {routingContext.fail(exception);});
}

测试接口:
在这里插入图片描述
失败调用:
在这里插入图片描述
至此,后端部分已经编写完成,下一文章将实现前端调用和展示部分。


http://www.ppmy.cn/embedded/128307.html

相关文章

C++笔记之静态多态和动态多态

C++笔记之静态多态和动态多态 code review! 在C++中,多态(Polymorphism)是面向对象编程的一个核心概念,允许对象以多种形式存在。多态性主要分为静态多态(Static Polymorphism)和动态多态(Dynamic Polymorphism)。下面将详细解释这两种多态及其在C++中的实现方式、优缺…

前端打印功能(vue +springboot)

后端 后端依赖生成pdf的方法pdf转图片使用(用的打印模版是带参数的 ,参数是aaa)总结 前端页面 效果 后端 依赖 依赖 一个是用模版生成对应的pdf,一个是用来将pdf转成图片需要的 <!--打印的--><dependency><groupId>net.sf.jasperreports</groupId>&l…

opencv的相机标定与姿态解算

首先我们要知道四个重要的坐标系 世界坐标系相机坐标系图像成像坐标系图像像素坐标系 坐标系之间的转换 世界坐标系——相机坐标系 从世界坐标系到相机坐标系&#xff0c;涉及到旋转和平移&#xff08;其实所有的运动也可以用旋转矩阵和平移向量来描述&#xff09;。绕着不…

浮点数二进制制科学计数法理解

Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu 1. 引言 对于浮点数&#xff0c;主要是单精度-float、双精度-double两种类型。 对于浮点类型&#xff0c;我们知道其采用科学计数法&#xff0c;准确来说应该是二进制科学计数法。 为什么准确说是是二进制科学计数法&…

985研一学习日记 - 2024.10.17

一个人内耗&#xff0c;说明他活在过去&#xff1b;一个人焦虑&#xff0c;说明他活在未来。只有当一个人平静时&#xff0c;他才活在现在。 日常 1、起床6:00√ 2、健身1个多小时 今天练了二头和背部&#xff0c;明天练胸和三头 3、LeetCode刷了3题 旋转图像&#xff1a…

022 elasticsearch文档管理(添加、修改、删除、批处理)

文章目录 添加文档修改文档删除文档根据_id取文档使用批处理_bulk PortX&#xff1a; https://portx.online/zh MobaXterm&#xff1a; https://mobaxterm.mobatek.net/ FinalShell&#xff1a; http://www.hostbuf.com/ 添加文档 向索引中添加一行数据 使用json来表示 使用…

汽车免拆诊断案例 | 2022款大众捷达VS5车行驶中挡位偶尔会锁在D3挡

故障现象  一辆2022款大众捷达VS5汽车&#xff0c;搭载EA211发动机和手自一体变速器&#xff0c;累计行驶里程约为4.5万km。该车行驶中挡位偶尔会锁在D3挡&#xff0c;车速最高约50 km/h&#xff0c;且组合仪表上的发动机故障灯和EPC灯异常点亮。 故障诊断  用故障检测仪检…

10.17学习

1.二进制 二进制是一种基数为2的数制&#xff0c;它只使用两个数字&#xff1a;0和1。二进制数在计算机科学和数字电路中非常常见&#xff0c;因为它们可以很容易地被电子设备处理。 ①二进制数的书写&#xff1a; 二进制数通常由一串0和1组成&#xff0c;例如&#xff1a; …