接口设计中的统一语言原则
在接口设计时,确保系统之间的对话语言统一非常重要,尤其是在接口的命名、参数列表、包装结构体、版本策略、幂等性实现及同步异步处理方式等方面。
关键点
-
接口响应的明确性:
- 接口应明确表示处理结果,以避免混乱。例如,响应体中
success
、code
、info
和message
的含义需清晰定义。 - 例子中,收单服务的接口由于没有明确的文档和逻辑,导致开发和调用者之间产生理解偏差。
- 接口应明确表示处理结果,以避免混乱。例如,响应体中
-
对外隐藏内部实现:
- 应避免将内部服务的状态码和错误信息直接暴露给客户端。可以通过设计更清晰的响应结构体来实现,例如去掉外层的
info
字段。
- 应避免将内部服务的状态码和错误信息直接暴露给客户端。可以通过设计更清晰的响应结构体来实现,例如去掉外层的
-
接口结构的设计逻辑:
- 设计逻辑应清楚地定义每个字段的含义,并规范客户端如何处理这些响应。
- 例如,只有在
success
为true
时才解析data
字段。
-
版本控制策略:
- 版本策略应在一开始就确定,确保统一性,可以通过 URL Path、QueryString 或 HTTP 头等方式实现。
- 不同接口实现时,应遵循相同的版本控制规则,以避免混淆。
-
同步与异步处理:
- 接口应明确其处理方式。如果内部实现为异步,但接口命名为同步,可能会导致用户误解和不必要的等待。
-
最佳实践与优化:
- 使用自定义异常和响应包装来简化代码,提高可读性和可维护性。
- 通过框架自动处理响应体包装和错误处理,简化业务逻辑。
小技巧
- 可以通过自定义注解和框架扩展实现统一的 API 版本控制,简化开发过程。
- 确保设计文档与接口实现保持一致,方便后续的维护与扩展。
接口处理方式要明确同步还是异步
在设计 API 接口时,明确处理方式(同步或异步)是至关重要的。这不仅影响到接口的响应时间,还会影响用户体验和系统架构。
下面我们通过文件上传服务的实例来说明这一点。
问题示例
在一个文件上传服务中,有一个上传接口执行了两个步骤:上传原图和压缩上传缩略图。如果每一步都耗时 5 秒,那么整个接口的响应时间将达到至少 10 秒。
为了提升响应速度,将上传操作改为异步处理,但这种处理方式存在明显问题:
private ExecutorService threadPool = Executors.newFixedThreadPool(2);
public UploadResponse upload(UploadRequest request) {
//上传原始文件任务提交到线程池处理
Future<String> uploadFile = threadPool.submit(() -> uploadFile(request.getFile()));
//上传缩略图任务提交到线程池处理
Future<String> uploadThumbnailFile = threadPool.submit(() -> uploadThumbnailFile(request.getFile()));
// 等待上传原始文件任务完成,最多等待1秒
try {
response.setDownloadUrl(uploadFile.get(1, TimeUnit.SECONDS));
} catch (Exception e) {
e.printStackTrace();
}
// 等待上传缩略图任务完成,最多等待1秒
try {
response.setThumbnailDownloadUrl(uploadThumbnailFile.get(1, TimeUnit.SECONDS));
} catch (Exception e) {
e.printStackTrace();
}
return response;
}
上传接口的请求和响应比较简单,传入二进制文件,传出原文件和缩略图下载地址
@Data
public class UploadRequest {
private byte[] file;
}
@Data
public class UploadResponse {
private String downloadUrl;
private String thumbnailDownloadUrl;
}
模拟上传耗时,随机休眠
private String uploadFile(byte[] data) {
try {
TimeUnit.MILLISECONDS.sleep(500 + ThreadLocalRandom.current().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
return "http://www.demo.com/download/" + UUID.randomUUID().toString();
}
private String uploadThumbnailFile(byte[] data) {
try {
TimeUnit.MILLISECONDS.sleep(1500 + ThreadLocalRandom.current().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
return "http://www.demo.com/download/" + UUID.randomUUID().toString();
}
问题分析
尽管接口看似通过异步处理来提高响应速度,但由于设置了较短的超时时间,导致接口行为变得不可预测:
- 不完整响应:一旦发生超时,接口可能无法返回完整的数据(例如,缺少原文件或缩略图的下载地址)。
- 行为不一致:接口的表现可能因为执行的环境和网络状况而异,给用户带来困扰。
更合理的设计
更合理的方式是,让上传接口要么是彻底的同步处理,要么是彻底的异步处理 。
-
所谓同步处理,接口一定是同步上传原文件和缩略图的,调用方可以自己选择调用超时,如果来得及可以一直等到上传完成,如果等不及可以结束等待,下一次再重试;
-
所谓异步处理,接口是两段式的,上传接口本身只是返回一个任务 ID,然后异步做上传操作,上传接口响应很快,客户端需要之后再拿着任务 ID 调用任务查询接口查询上传的文件 URL。
同步处理
在同步处理模式下,接口会等待所有操作完成后再返回响应:
/**
* 同步上传文件和缩略图
*
* 本方法接收一个同步上传请求对象,其中包含要上传的文件
* 它将文件和缩略图上传到服务器,并返回一个包含下载URL和缩略图下载URL的响应对象
*
* @param request 包含要上传文件的同步上传请求对象
* @return 包含文件下载URL和缩略图下载URL的响应对象
*/
public SyncUploadResponse syncUpload(SyncUploadRequest request) {
// 创建一个同步上传响应对象
SyncUploadResponse response = new SyncUploadResponse();
// 上传文件并设置下载URL
response.setDownloadUrl(uploadFile(request.getFile()));
// 上传缩略图并设置下载URL
response.setThumbnailDownloadUrl(uploadThumbnailFile(request.getFile()));
// 返回包含下载URL和缩略图下载URL的响应对象
return response;
}
这样,调用方可以自行选择超时处理,等待操作完成或决定终止请求。
@Data
public class SyncUploadRequest {
private byte[] file;
}
@Data
public class SyncUploadResponse {
private String downloadUrl;
private String thumbnailDownloadUrl;
}
异步处理
在异步处理模式下,接口只需返回一个任务 ID,上传操作在后台进行,客户端可稍后查询结果:
/**
* 异步上传文件服务方法
* 该方法接收一个异步上传请求对象,处理文件上传任务,并返回一个包含任务ID的响应对象
* 主要通过多线程方式执行文件上传和缩略图上传,同时生成对应的下载URL
*
* @param request 异步上传请求对象,包含需要上传的文件信息
* @return AsyncUploadResponse 异步上传响应对象,包含上传任务的唯一标识
*/
public AsyncUploadResponse asyncUpload(AsyncUploadRequest request) {
// 创建异步上传响应对象
AsyncUploadResponse response = new AsyncUploadResponse();
// 生成上传任务ID
String taskId = "upload" + atomicInteger.incrementAndGet();
// 设置响应对象的上传任务ID
response.setTaskId(taskId);
// 执行文件上传任务
threadPool.execute(() -> {
// 上传文件并获取下载URL
String url = uploadFile(request.getFile());
// 使用下载URL更新或创建同步查询上传任务响应对象,并设置下载URL
downloadUrl.computeIfAbsent(taskId, id -> new SyncQueryUploadTaskResponse(id)).setDownloadUrl(url);
});
// 执行缩略图上传任务
threadPool.execute(() -> {
// 上传缩略图并获取下载URL
String url = uploadThumbnailFile(request.getFile());
// 使用下载URL更新或创建同步查询上传任务响应对象,并设置缩略图下载URL
downloadUrl.computeIfAbsent(taskId, id -> new SyncQueryUploadTaskResponse(id)).setThumbnailDownloadUrl(url);
});
// 返回异步上传响应对象
return response;
}
@Data
public class AsyncUploadRequest {
private byte[] file;
}
@Data
public class AsyncUploadResponse {
private String taskId;
}
查询接口
为查询上传结果设计一个同步接口,使用任务 ID 获取上传状态和结果:
/**
* 同步查询上传任务信息
*
* 该方法用于同步查询一个上传任务的相关信息,包括文件的下载URL和缩略图的下载URL
* 它首先创建一个SyncQueryUploadTaskResponse对象,然后根据请求的taskId设置对应的下载URL和缩略图下载URL
*
* @param request 包含taskId的请求对象,用于标识和查询特定的上传任务
* @return 返回一个SyncQueryUploadTaskResponse对象,包含查询到的上传任务信息
*/
public SyncQueryUploadTaskResponse syncQueryUploadTask(SyncQueryUploadTaskRequest request) {
// 创建响应对象,初始化时传入请求的taskId
SyncQueryUploadTaskResponse response = new SyncQueryUploadTaskResponse(request.getTaskId());
// 设置文件的下载URL,如果request的taskId在downloadUrl映射中不存在,则使用默认的response对象获取URL
response.setDownloadUrl(downloadUrl.getOrDefault(request.getTaskId(), response).getDownloadUrl());
// 设置缩略图的下载URL,逻辑同上
response.setThumbnailDownloadUrl(downloadUrl.getOrDefault(request.getTaskId(), response).getThumbnailDownloadUrl());
// 返回填充好信息的响应对象
return response;
}
@Data
@RequiredArgsConstructor
public class SyncQueryUploadTaskRequest {
private final String taskId;
}
@Data
@RequiredArgsConstructor
public class SyncQueryUploadTaskResponse {
private final String taskId;
private String downloadUrl;
private String thumbnailDownloadUrl;
}
Controller
/**
* 控制器类,用于处理与文件上传相关的API请求
*/
@Slf4j
@RequestMapping("apiasyncsyncmode")
@RestController
public class APIAsyncSyncModeController {
/**
* 文件服务接口,用于执行文件上传和查询任务
*/
@Autowired
private FileService fileService;
/**
* 处理错误的上传请求
* 此方法演示了一个不推荐的上传方式,可能因为安全性、性能或其他原因
*
* @return 返回上传响应,包含上传结果
*/
@GetMapping("wrong")
public UploadResponse upload() {
UploadRequest request = new UploadRequest();
return fileService.upload(request);
}
/**
* 同步上传文件
* 此方法用于需要立即得到上传结果的场景
*
* @return 返回同步上传响应,包含上传结果
*/
@GetMapping("syncUpload")
public SyncUploadResponse syncUpload() {
SyncUploadRequest request = new SyncUploadRequest();
return fileService.syncUpload(request);
}
/**
* 异步上传文件
* 此方法适用于不需要立即响应的上传场景,可以提高应用的响应性能
*
* @return 返回异步上传响应,包含上传任务的信息
*/
@GetMapping("asyncUpload")
public AsyncUploadResponse asyncUpload() {
AsyncUploadRequest request = new AsyncUploadRequest();
return fileService.asyncUpload(request);
}
/**
* 同步查询上传任务状态
* 此方法用于查询特定上传任务的当前状态,适用于需要跟踪上传进度的场景
*
* @param taskId 上传任务的唯一标识符
* @return 返回同步查询响应,包含上传任务的当前状态
*/
@GetMapping("syncQuery")
public SyncQueryUploadTaskResponse syncQuery(@RequestParam("taskId") String taskId) {
SyncQueryUploadTaskRequest request = new SyncQueryUploadTaskRequest(taskId);
return fileService.syncQueryUploadTask(request);
}
}
总结
使用方可以根据业务性质选择合适的方法:如果是后端批处理使用,那么可以使用同步上传,多等待一些时间问题不大;
如果是面向用户的接口,那么接口响应时间不宜过长,可以调用异步上传接口,然后定时轮询上传结果,拿到结果再显示
经过改造的 FileService
提供了更明确的 API 接口设计:
- 同步上传接口:
syncUpload
,用户可以等待上传完成。 - 异步上传接口:
asyncUpload
,快速返回任务 ID,客户端可轮询查询上传结果。
这样的设计使得接口行为清晰、可预测,便于用户根据需求选择合适的处理方式。
本站资源均来自互联网,仅供研究学习,禁止违法使用和商用,产生法律纠纷本站概不负责!如果侵犯了您的权益请与我们联系!
转载请注明出处: 免费源码网-免费的源码资源网站 » Java避坑案例 - 接口设计_明确接口的处理方式:同步 vs 异步
发表评论 取消回复