Restful API利器——RestPack项目教程

目录

  • 项目背景
  • RestPack 简介
  • 引入 RestPack 依赖
  • 启用 RestPack
  • @RestPackController 注解
  • RestPack 异常处理
  • 自定义数据格式
  • 日志输出

项目背景

在互联网、移动互联网、车联网、物联网繁荣的今天,
各种客户端设备层出不穷,为了能用同一套服务端程序处理各种客户端的访问,
HTTP Restful API
变得流行起来。

但是客户端与服务端交互时,往往会有一些通用的需求,比如:

  • 服务端返回的报文,有一套统一的标准,这样有利于开发和维护。
  • 服务端在处理一个 API 请求时,如果出异常了,
    总是希望在请求的返回结果中给出一个明确的错误码,
    客户端可以根据错误码作进一步的处理。
  • 为了方便排查问题,总是希望对于每个请求,服务端会返回一个 requestId,
    后台可以将这个请求产生的日志与这个 requestId 相关联。
    这样一旦前后端联调时发现了问题,前端工程师只要给出 requestId ,
    后台工程师就可以拿着这个 requestId 快速找出相关日志,方便分析排查问题。
    ......

为了满足这些非功能性需求,笔者总结了之前很多项目的开发经验,
归纳出一套统一的数据返回格式,如下(分成功和失败两种情况):

成功响应内容:

{
  "requestId" : "d56c24d006aa4d5e9b8903b3256bf3e3",
  "serverTime" : 1502592752449,
  "spendTime" : 5,
  "resultCode" : "success",
  "data" : {
    "key1": "value1",
    "key2": "value2"
  }
}
  • requestId : 服务端生成的请求唯一ID号,
    当这个请求有问题时,可以拿着这个 ID 号,
    在海量日志快速查询到此请求的日志信息,以方便排查问题。
  • serverTime : 服务器时间,
    很多场景下需要使用当前时间值,但客户端本地的时间有可能不准,
    因为这里返回服务器端时间供客户端使用。
  • spendTime : 本次请求在服务器端处理所消耗的时间,
    这里显示出来以方便诊断慢请求相关问题。
  • resultCode : 结果码,
    "success" 表示成功,其它表示一个错误的错误码,
    错误码的值及具体含意由项目中客户端与服务端约定。
  • data : 实际的业务数据,内容由每个 API 的业务逻辑决定。

错误响应内容:

{
  "requestId" : "d7ab68ac513e4549896aa33f0cda3518",
  "serverTime" : 1502594589673,
  "spendTime" : 8,
  "resultCode" : "name.duplicate",
  "message" : "昵称重复: terran4j,请换个昵称!",
  "props" : {
    "name": "terran4j"
  }
}

与成功响应类似,都有 requestId、serverTime、spendTime 等字段。
不同的是 resultCode 是一个自定义的错误码,并且多了message 、props 两个字段:

  • message : 错误信息描述,
    是一段易于人理解的字符串信息,方便开发人员知晓错误原因。
  • props : 错误上下文相关属性,
    本项可选,有的错误码可能需要前端在程序中作进一步处理,
    所以后台可以在 props 中提供一些 key - value 的属性值,
    方便程序读取(而不是让前端程序从 message 中解析文本内容获取这些值)。

RestPack 简介

若要让项目中每个 API 的实现都遵循这套统一的数据规范,
无疑要在每个API方法中编写一些重复性的代码。
因此笔者根据实际项目经验总结,开发了一套名为 RestPack 的工具包,
可以帮助 Restful API 的开发者将API 的返回结果自动包装成统一格式的报文。

RestPack 一词中, Rest 代表 Http Restful API 的意思,
而 Pack 是 "包装、包裹" 的意思,合起来的意思就是在原本的 Http Restful API 基础上,
将返回数据再包裹一层,以符合之前所讲的数据规范。

本文主要目标是介绍 RestPack 的用法。

引入 RestPack 依赖

然后,您就可以在您的项目的 pom.xml 文件中,引用 restpack 的 jar 包了,如下所示:

        <dependency>
            <groupId>terran4j</groupId>
            <artifactId>terran4j-commons-restpack</artifactId>
            <version>${restpack.version}</version>
        </dependency>

整个 pom.xml 内容类似于:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>terran4j</groupId>
    <artifactId>terran4j-demo-restpack</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>terran4j-demo-restpack</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.9.RELEASE</version>
    </parent>

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

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>terran4j</groupId>
            <artifactId>terran4j-commons-restpack</artifactId>
            <version>${restpack.version}</version>
        </dependency>
    </dependencies>

</project>

如果是 gradle,请在 build.gradle 中添加依赖,如下所示:

compile "com.github.terran4j:terran4j-commons-restpack:${restpack.version}"

${restpack.version} 最新稳定版,请参考 这里

启用 RestPack

为了在应用程序中启用 RestPack,需要在 SpringBootApplication 类上加@EnableRestPack 注解,
整个 main 程序代码,如下所示:

package com.terran4j.demo.restpack;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import com.terran4j.commons.restpack.EnableRestPack;

@EnableRestPack
@SpringBootApplication
public class RestPackDemoApp {

    public static void main(String[] args) {
        SpringApplication.run(RestPackDemoApp.class, args);
    }

}

加上 @EnableRestPack 才能启用 RestPack 的功能,否则本文下面所讲的效果都不会起作用。

@RestPackController 注解

以前实现 HTTP Restful API,就是用 Spring Boot MVC 编写一个 Controller 类,
并在类上加上 @RestController 注解
(对于这一点不清楚的读者,请先阅读笔者之前写过的
Spring Boot快速入门
一书,其中《 Spring Boot MVC
这章详细描述了这一点)。

要在原有的 Controller 类上启用 RestPack 功能,
仅仅是将类上的注解由 @RestController 改成 @RestPackController 就可以了,
代码如下所示:

package com.terran4j.demo.restpack;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import com.terran4j.commons.restpack.HttpResultPackController;
import com.terran4j.commons.util.error.BusinessException;

@RestPackController
@RequestMapping("/demo/restpack")
public class RestPackDemoController {
    
    private static final Logger log = LoggerFactory.getLogger(RestPackDemoController.class);

    @RequestMapping(value = "/echo", method = RequestMethod.GET)
    public String echo(@RequestParam(value = "msg") String msg) throws BusinessException {
        log.info("echo, msg = {}", msg);
        return msg;
    }
    
}

编写好这个类后,我们启动 main 程序,然后浏览器输入URL:

http://localhost:8080/demo/restpack/echo?msg=abc

浏览器中显示结果为:

{
  "requestId" : "2141d927f1de453ba3edd83306ecdf3e",
  "serverTime" : 1502597485688,
  "spendTime" : 21,
  "resultCode" : "success",
  "data" : "abc"
}

如果我们去掉 @EnableRestPack (或将 @RestPackController 还原成 @RestController),
再访问的结果仅为:

abc

说明 RestPack 可以将原本的返回数据,自动包装成我们定义的数据规范格式了。

对于无返回值的方法, RestPack 同样有效果,
比如我们在上面的 RestPackDemoController 类中添加如下方法:

@RestPackController
@RequestMapping("/demo/restpack")
public class RestPackDemoController {
    
    @RequestMapping(value = "/void", method = RequestMethod.GET)
    public void doVoid(@RequestParam(value = "msg") String msg) throws BusinessException {
        log.info("doVoid, msg = {}", msg);
    }
}

重启程序后在浏览器输入URL:

http://localhost:8080/demo/restpack/void?msg=abc

显示的结果如下:

{
  "requestId" : "2df4aa14dfab46e196ebf7e79b2b35d6",
  "serverTime" : 1502627058784,
  "spendTime" : 35,
  "resultCode" : "success"
}

由于方法没有返回值,所以"data"字段也不出现了,但其它字段都有了。

如果返回值是自定义的复杂对象,RestPack 同样能转化成 json 格式放在 "data" 字段中,
比如我们再添加如下代码:


@RestPackController
@RequestMapping("/demo/restpack")
public class RestPackDemoController {
    
    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public HelloBean hello(@RequestParam(value = "name") String name) throws BusinessException {
        log.info("hello, name = {}", name);
        HelloBean bean = new HelloBean();
        bean.setName(name);
        bean.setMessage("Hello, " + name + "!");
        bean.setTime(new Date());
        return bean;
    }
}

类 HelloBean 的定义如下:

package com.terran4j.demo.restpack;

import java.util.Date;

public class HelloBean {
    
    private String name;
    
    private String message;
    
    private Date time;

    // 省略 getter /setter 方法。
    
}

重启程序后在浏览器输入URL:

http://localhost:8080/demo/restpack/hello?name=neo

显示的结果如下:

{
  "requestId" : "ab5c43c3415042b682b290e17fad1358",
  "serverTime" : 1502957833154,
  "spendTime" : 30,
  "resultCode" : "success",
  "data" : {
    "name" : "neo",
    "message" : "Hello, neo!",
    "time" : "2017-08-17 16:17:13"
  }
}

发现 "data" 中的字段与 HelloBean 的属性是对应的。

RestPack 异常处理

当服务端抛出异常时,RestPack 会将异常包装成错误报文返回。

从客户端的角度来看,异常分两种:

  • 一种是业务异常,
    如: 注册时用户名已存在、用户输入错误,等。这种情况下,
    客户端需要明确的异常原因及关键字段数据,
    以便于客户端程序知晓如何在界面上给予用户提示。
  • 另一种是系统异常,
    如: 数据库无法访问、程序BUG,等。
    这种异常需要客户端模糊处理(尽量避免暴露系统本身的问题),
    比如弹出一个“对不起,系统开小差了”,
    或“系统维护中,请稍后重试”之类的提示。

RestPack 提供了一个叫 BusinessException 的异常类来代表业务异常,
如果方法抛出的异常类是 BusinessException 类或其子类,
RestPack 就按业务异常处理,如果不是就按系统异常处理。
为了查看运行效果,我们添加一个新的方法:

@RestPackController
@RequestMapping("/demo/restpack")
public class RestPackDemoController {
    
    @RequestMapping(value = "/regist", method = RequestMethod.GET)
    public void regist(@RequestParam(value = "name") String name) throws BusinessException {
        log.info("regist, name = {}", name);
        if (name.length() < 3) {
            String suggestName = name + "123";
            throw new BusinessException("name.invalid")
                    .setMessage("您输入的名称太短了,建议为:${suggestName}")
                    .put("suggestName", suggestName);
        }
        log.info("regist done, name = {}", name);
    }
}

在 BusinessException 类中,构造方法中的参数(如上面的 "name.invalid" ) 就是错误码,
put(String, Object) 方法用于设置一些异常上下文属性,会出现在返回报文的 props 字段中,
setMessage(String) 方法用于设置异常信息,可以用 ${} 来引用 put 方法出现的字段。

重启程序,在浏览器中访问URL:

http://localhost:8080/demo/restpack/regist?name=ne

结果如下:

{
  "requestId" : "22e5651199f645628fdf724e9f0826a3",
  "serverTime" : 1502627761012,
  "spendTime" : 1,
  "resultCode" : "name.invalid",
  "message" : "您输入的名称太短了,建议为:ne123",
  "props" : {
    "suggestName" : "ne123"
  }
}

自定义数据格式

或许你的项目需要自定义返回报文的数据格式,而不是使用 RestPack 默认的这一套数据格式,
可以有两种做法:

第一,对这些通用字段进行重命名,在 application.yml 配置文件中定义如下:

terran4j:
  restpack:
    renaming:
      requestId: requestCode
      serverTime: currentTime
      spendTime: spend
      resultCode: status
      data: result
      message: msg
      props: data
      success: OK

terran4j.restpack.renaming 下的配置项是对相应的字符进行重命名,
如: requestId 被改成 requestCode,serverTime 被改成 serverTime......
其中最后一个 success: OK 表示对请求成功时的返回码重命名为"OK"(默认为"success")。

重命名后数据的组织结构没有变,只是字段名改了,如下所示:

{
  "requestCode" : "22e5651199f645628fdf724e9f0826a3",
  "currentTime" : 1502627761012,
  "spend" : 1,
  "status" : "OK",
  "result" : {
    "name" : "neo",
    "message" : "Hello, neo!",
    "time" : "2017-08-17 16:17:13"
  }
}

如果你希望连数据结构也自定义,则需要使用第二种方式了。

第二种方式:编写一个服务实现 HttpResultConverter 接口,并注册到 Spring 容器中,
如下代码所示:


package com.terran4j.demo.restpack;

import com.terran4j.commons.restpack.HttpResult;
import com.terran4j.commons.restpack.HttpResultConverter;
import org.springframework.stereotype.Service;

@Service
public class DemoHttpResultConverter implements HttpResultConverter {

    @Override
    public Object convert(HttpResult httpResult) {
        // 这里可以将 HttpResult 对象转成你需要的格式
        // RestPack 框架会将本方法的返回对象转成 JSON 串返回给请求方。
        return httpResult;
    }
}

注意,整个应用程序不可以注册多个 HttpResultConverter 接口的实现对象,
否则 RestPack 不知道使用哪个就会在启动时报错。

日志输出

RestPack 在开始处理请求时,会生成唯一的 requestId,
这个 requestId 不但会在返回报文中出现,还会一开始就放到日志的MDC中,
对于 log4j 或 logback (它们都支持 MDC),
你可以在配置将 requestId 信息输出到日志中,这样每条日志就用 requestId 相关联了。

比如在项目中,将 logback.xml 配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true" scanPeriod="1000">

    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%date %level requestId=%X{requestId} -- %-40logger{35}[%line]: %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="file" class="ch.qos.logback.core.FileAppender">
        <file>./restpack.log</file>
        <encoder>
            <pattern>%date %level requestId=%X{requestId} -- %-40logger{35}[%line]: %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="stdout" />
        <appender-ref ref="file" />
    </root>

</configuration>

重点是日志输出格式,也就是<pattern>中加上requestId=%X{requestId}

%date %level requestId=%X{requestId} -- %-40logger{35}[%line]: %msg%n

%X{} 是使用 MDC 中的字段,有关 logback / log4j 中 MDC 的用法,不清楚的读者请自行百度搜索。

logback.xml 配置好后,再重启服务,在浏览器中输入URL:

http://localhost:8080/demo/restpack/echo?msg=abc

结果控制台输出如下:

2017-08-17 16:34:08,570 INFO requestId=ca2a12a0031f493db97856a3300b917a -- c.t.commons.restpack.RestPackAspect     [120]: request '/demo/restpack/echo' begin, params:
{
  "msg" : "abc"
}
2017-08-17 16:34:08,571 INFO requestId=ca2a12a0031f493db97856a3300b917a -- c.t.d.r.RestPackDemoController          [29]: echo, msg = abc
2017-08-17 16:34:08,572 INFO requestId=ca2a12a0031f493db97856a3300b917a -- c.t.commons.restpack.RestPackAdvice     [63]: request '/demo/restpack/echo' end, response:
{
  "requestId" : "ca2a12a0031f493db97856a3300b917a",
  "serverTime" : 1502958848570,
  "spendTime" : 2,
  "resultCode" : "success",
  "data" : "abc"
}

可以看到日志中有requestId=ca2a12a0031f493db97856a3300b917a 这段内容。

这样的好处是排查日志方便,比如在 linux 环境中,对日志文件执行类似如下命令:

grep -n "requestId=ca2a12a0031f493db97856a3300b917a" xxx.log

(xxx.log 是程序产生的日志文件的名称),
就可以在大量日志内容中快速过滤出这条请求的日志了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,591评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,729评论 6 342
  • iOS网络架构讨论梳理整理中。。。 其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单...
    yhtang阅读 5,160评论 1 23
  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,653评论 0 15
  • 卡塔尼亚位于西西里岛的东海岸,欧洲著名的埃特纳火山的山脚。这座富饶繁华的城市在历史上屡遭埃特纳火山的浩劫,又总是顽...
    渌青阅读 498评论 0 0