Vert.x,Web - Restful API

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

案例概述

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

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

sql 复制代码
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数据库,所以需要在项目中引入相关依赖:

xml 复制代码
<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();
	}
	@Override
	public void stop() throws Exception {
		if (null != client) { //停止时候释放连接。
			client.close();
		}
	}
	
	@Override
	public 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 , 整型, 非必选, 请求数据的分页页码, 默认值: 1
    limit, 整型, 非必选, 请求数据的分页大小, 默认值: 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);
	});
}

测试接口:

失败调用:

至此,后端部分已经编写完成,下一文章将实现前端调用和展示部分。

相关推荐
聪明的笨猪猪13 小时前
Java Redis “Sentinel(哨兵)与集群”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
222you13 小时前
Mybatis(1)
java·tomcat·mybatis
渣哥13 小时前
三级缓存揭秘:Spring 如何优雅地处理循环依赖问题
javascript·后端·面试
靠近彗星13 小时前
1.5操作系统引导
java·linux·服务器·操作系统
xuejianxinokok13 小时前
Postgres 18 的新功能
后端·postgresql
渣哥13 小时前
为什么几乎所有 Java 项目都离不开 IoC?Spring 控制反转的优势惊人!
javascript·后端·面试
用户38568033499613 小时前
appium从入门到精通php,移动端自动化测试Appium 从入门到项目实战Python版
后端
瑶山13 小时前
社区版Idea怎么创建Spring Boot项目?Selected Java version 17 is not supported. 问题解决
java·spring boot·intellij-idea·创建项目
学习编程的Kitty13 小时前
JavaEE初阶——多线程(1)初识线程与创建线程
java·开发语言·java-ee
天天摸鱼的java工程师14 小时前
SpringCloud + Nacos + Feign + Resilience4j:微服务间调用的熔断降级与重试策略
后端