Rest服务开发文档

IMS产品开发将使用REST风格进行接口交互.本文档适用于新服开系统开发rest服务端和rest客户端的入门指导.

整体架构介绍

Rest框架:spring mvc + swagger2
服务端项目:exttask-rest-server
客户端项目:smartflow-rest-client

架构特点:

  • 请求和反馈都使用dto对象作为交互介质
  • 异常捕获处理
  • 注解式的请求合法性校验
  • 接口交互日志记录
  • 自动生成请求Headers
  • 在线文档查看测试

服务端接口开发

  1. 开发规范:
  • 包路径约定
    所有的服务端接口都应该写在包com.ccssoft.smartflow.rest内,根据厂商唯一标识的代号小写字母创建下一级包名,每个服务端接口根据接口ID英文小写创建二级包目录用于存放该接口的Dto对象.
  • Dto对象命名约定
    所有请求和返回的对象类名都以Dto结尾,请求一般情况采用RequestDto.java,返回一般情况使用ResponseDto.java.
  • Path路径约定
    发布出来的rest服务路径默认为/rest/api/sg/${厂商代号英文小写}/${接口ID英文小写}.

20190826会议,配置激活系统和业务开通系统单独约定PATH规范:

http://${IP或域名}:${端口}/${服务提供方代号大写}/${HB或者EB}/${资源代号大写}/${接口唯一标识}

HB和EB分别表示“家客”和“集客”

样例PATH:http://127.0.0.1:8080/SG/EB/GPON/REST_MANRES_OPEN_ORDER_CONFIG
  • Action约定
    所有的发布的服务都为Post服务.
  1. 请求合法性校验
    请求需要校验基本合法性,校验有:必填校验,枚举检测校验.
  • 必填校验
    将Dto对象中需要必填的字段注解为@Required
  • 枚举检测校验
    将Dto对象中需要枚举检测校验的字段注解为@EnumValidate(${枚举类})
  1. 服务端代码样例:

    @RestController
    @RequestMapping("/rest/api/sg")
    public class DemoResource {
        @PostMapping("/demo/hi")
        public ResponseDto demo(@RequestBody RequestDto demo) {
            return new ResponseDto("hi");
        }
    }
    
  2. 在线文档查看地址
    http://localhost:9903/swagger-ui.html

  3. 服务端接口日志记录
    服务端记录日志需要,将请求头记录下来,目前头部定义了以下请求头信息:

服开与外部系统的基于http请求的交互(包括ws和restful),请求的http头部需增加如下通用的头部信息

头信息 示例 说明
e-version Hydrogen 业务版本代号,该版本号通常在大的业务需求确定时生成,新业务上线后,由服开系统接收到pboss订单时打上相应版本信息,后续所有接口交互均携带该信息,可以用于蓝绿发布。
e-order-code CMCC-GZ-SGYWKT-20190111-02582377 服开定单号,可以用于统一日志记录,接口间问题故障定位等。
e-session-id 59418517-7520-11e9-9ddd-6057186d4e49 会话标识,用于唯一标识由服开发起的一系列交互过程,服开发起最初服务请求时生成唯一标识,在外系统状态通知或异步请求反馈时必须携带同一标识。
e-area-code 755 分公司区号。
e-token xxxxx 系统间调用认证字符串。
e-caller SG 调用方系统代号,可选值为SG、RESTAR、CAT、PMOS、OMAPP。
e-service-code OPEN_ORDER_CONFIG 服务代号,用于标识不同的服务接口定义,详情见接口需求文档。
e-timestamp Tue, 11 Jun 2019 23:24:18 GMT http-date类型的时间戳,采用last-modified、Expires等相同标准的格式。

日志记录需要通过接口唯一标识映射接口中文名称和接口发送方中文名称,需要在配置文件中加入以下配置示例:

#REST服务端接口信息
smartflow.restserver:
#接口唯一标识
demo:
    #接口中文名称
    name: 示例接口
    #调用方系统中文名称
    caller: 业务开通模拟项目

客户端开发

  1. 开发规范:
  • 包路径约定
    所有的服务端接口都应该写在包com.ccssoft.smartflow.restclient内,根据厂商唯一标识的代号小写字母创建下一级包,每个服务端接口根据接口ID英文小写创建二级包.
  • Dto对象命名约定
    所有请求和返回的对象类名都以Dto结尾,请求一般情况采用RequestDto.java,返回一般情况使用ResponseDto.java.
  1. 抽象代码说明
    客户端根据实际情况抽象出接口com.ccssoft.smartflow.restclient.Client<I, O>和抽象类com.ccssoft.smartflow.restclient.AbstractClient<I, O>以下是代码:

    package com.ccssoft.smartflow.restclient;
    
    import java.util.Map;
    
    /**
    * REST客户端接口
    *
    * @author xiaowj
    */
    public interface Client<I extends SessionRequest, O> {
        /**
        * @param areaCode 本地网编码
        * @param businessKey businessKey
        * @param request 回话请求Dto
        * @return
        */
        O call(String areaCode, String businessKey, I request);
    
        /**
        * 客户端调用接口
        * @param areaCode 本地网编码
        * @param businessKey businessKey
        * @param request 回话请求Dto
        * @param extHeader 自定义headers,内置默认header设置之后无效
        * @return
        */
        O call(String areaCode, String businessKey, I request, Map<String, String> extHeader);
    
        /**
        * 获取接口uri基础路径,路径下必须能取到"url"和"name"
        * @return
        */
        String getUriBase();
    
        /**
        * 获取返回Dto类
        * @return
        */
        Class<O> getResponseClass();
    }
    

抽象类代码:

import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import org.jboss.logging.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

/**
* 客户端抽象类,调用服务端POST接口,自动组装headers
*
* @author xiaowj
* @param <I>
* @param <O>
*/
public abstract class AbstractClient<I extends SessionRequest, O> implements Client<I, O> {
    @Autowired private Environment env;

    @Value("${smartflow.version:test}")
    private String version;

    @Autowired private RestTemplate restTemplate;

    public O call(String areaCode, String businessKey, I request, Map<String, String> extHeader) {
        String interfaceName = env.getProperty(getUriBase() + ".name");
        if (interfaceName == null) {
        throw new RuntimeException("未配置接口名称.");
        }
        String url = env.getProperty(getUriBase() + ".url");
        if (url == null) {
        throw new RuntimeException("未配置接口URL.");
        }
        String serviceCode = env.getProperty(getUriBase() + ".code");
        if (serviceCode == null) {
        serviceCode = url;
        if (url.lastIndexOf("/") != -1) {
            serviceCode = url.substring(url.lastIndexOf("/") + 1, url.length());
        }
        }
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
        headers.setContentType(MediaType.APPLICATION_JSON);
        // 自定义header,若传入了默认header则无效,将会被覆盖掉
        if (extHeader != null && !extHeader.isEmpty()) {
        extHeader.forEach(
            (k, v) -> {
                headers.set(k, v);
            });
        }
        // 业务版本代号
        headers.set("e-version", "H");
        // 分公司区号
        headers.set("e-area-code", areaCode);
        // 服开定单号
        headers.set("e-order-code", businessKey);
        // 调⽤⽅系统代号,可选值为SG、RESTAR、CAT、PMOS、OMAPP
        headers.set("e-caller", "SG");
        // 会话标识
        headers.set("e-session-id", request.sessionId());
        Instant instant = Instant.now();
        String formatted =
            DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneOffset.UTC).format(instant);
        // http-date时间戳
        headers.set("e-timestamp", formatted);
        // 系统间调⽤认证字符串
        headers.set("e-token", UUID.randomUUID().toString());
        // 服务代号,⽤于标识不同的服务接⼝定义,详情⻅接⼝需求⽂档
        headers.set("e-service-code", serviceCode);

        // 记录接口日志,接口名称通过配置拿到
        MDC.put("interface_name", interfaceName);
        // 服务端厂家名称
        String receiver = env.getProperty(getUriBase() + ".receiver");
        MDC.put("receiver", receiver == null ? "其它厂家" : receiver);

        HttpEntity<SessionRequest> entity = new HttpEntity<SessionRequest>(request, headers);
        ResponseEntity<O> response = restTemplate.postForEntity(url, entity, getResponseClass());
        if (response.getStatusCode().is2xxSuccessful()) {
        return response.getBody();
        } else {
        throw new RuntimeException("接口请求失败![detail]:" + response.toString());
        }
    }

    public O call(String areaCode, String businessKey, I request) {
        return this.call(areaCode, businessKey, request, null);
    }
}

代码说明参看注释,当我们开发一个客户端时需要继承AbstractClient类,然后补全入参和出参Dto对象,入参Dto类需要实现com.ccssoft.smartflow.restclient.SessionRequest接口.
样例客户端代码如下:

/**
* 提交业务需求
*
* <p>外部系统通过调用该接口传递产品初始化业务申请要素信息, 综资系统根据申请要素自动生成申请单,并将申请单号反馈给外部系统
*
* @author xiaowj
*/
@Service
public class RestarCreateOrderClient extends AbstractClient<RequestDto, ResponseDto> {
    //配置文件基础路径
    @Override
    public String getUriBase() {
        return "smartflow.restclient.createorder";
    }

    @Override
    public Class<ResponseDto> getResponseClass() {
        return ResponseDto.class;
    }
}

客户端需要注入配置文件中的url信息,格式为smartflow.restclient.${接口ID英文小写}.需要配置接口的urlname,以下为示例配置:

smartflow.restclient: 
    demo: 
        url: http://localhost:9903/rest/api/sg/demo
        name: 示例接口

#Jackson注意事项

  • 若为空或者null的Dto属性不作为json字段,则需要对Dto对象添加@JsonInclude(Include.NON_EMPTY)注解,若Dto对象存在Dto对象属性,则子属性也需要添加注解
  • 若Dto对象的Json属性命名不符合Java命名规范,则需要对该属性的get方法设置@JsonProperty("EMOSSSheetId")注解

注解样例报文:

//设置为空的字段不送节点
@JsonInclude(Include.NON_EMPTY)
public class DemoDto implements SessionRequest {
private String message;

//未设置值的字段
private String empty;

//属性命名不符合Java编码规范,通过set方法注解修改命名
private String NKDFDss = "illegalField";

public String getMessage() {
    return message;
}

public void setMessage(String message) {
    this.message = message;
}

public String getEmpty() {
    return empty;
}

public void setEmpty(String empty) {
    this.empty = empty;
}

@JsonProperty("legal")
public String getNKDFDss() {
    return NKDFDss;
}

public void setNKDFDss(String nKDFDss) {
    NKDFDss = nKDFDss;
}

@Override
public String sessionId() {
    return UUID.randomUUID().toString();
}
}