在这里插入图片描述


接口设计中的统一语言原则

在接口设计时,确保系统之间的对话语言统一非常重要,尤其是在接口的命名、参数列表、包装结构体、版本策略、幂等性实现及同步异步处理方式等方面。

关键点

  1. 接口响应的明确性

    • 接口应明确表示处理结果,以避免混乱。例如,响应体中 successcodeinfomessage 的含义需清晰定义。
    • 例子中,收单服务的接口由于没有明确的文档和逻辑,导致开发和调用者之间产生理解偏差。
  2. 对外隐藏内部实现

    • 应避免将内部服务的状态码和错误信息直接暴露给客户端。可以通过设计更清晰的响应结构体来实现,例如去掉外层的 info 字段。
  3. 接口结构的设计逻辑

    • 设计逻辑应清楚地定义每个字段的含义,并规范客户端如何处理这些响应。
    • 例如,只有在 successtrue 时才解析 data 字段。
  4. 版本控制策略

    • 版本策略应在一开始就确定,确保统一性,可以通过 URL Path、QueryString 或 HTTP 头等方式实现。
    • 不同接口实现时,应遵循相同的版本控制规则,以避免混淆。
  5. 同步与异步处理

    • 接口应明确其处理方式。如果内部实现为异步,但接口命名为同步,可能会导致用户误解和不必要的等待。
  6. 最佳实践与优化

    • 使用自定义异常和响应包装来简化代码,提高可读性和可维护性。
    • 通过框架自动处理响应体包装和错误处理,简化业务逻辑。

小技巧

  • 可以通过自定义注解和框架扩展实现统一的 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,客户端可轮询查询上传结果。

这样的设计使得接口行为清晰、可预测,便于用户根据需求选择合适的处理方式。


在这里插入图片描述

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部