.netcore 3.1高性能微服务架构:webapi规范
1.1 定义
1、基础接口:单一职责原则,每个接口只负责各自的业务,下接db,通用性强。
2、聚合接口:根据调用方需求聚合基础接口数据,业务性强。
1.2 协议
1. 客户端在通过 API 与后端服务通信的过程中, 应该使用 HTTPS(生产环境) 协议
2. 服务端响应的数据格式统一为JSON
1.3域名host
prd环境:https://xxx-xxx-api.example.com/
uat环境:https://xxx-xxx-api-uat.example.com/
test环境:https://xxx-xxx-api-test.example.com/
dev环境:https://xxx-xxx-api-dev.example.com/
将api放到子域名里,这种做法可以保持某些规模化上的灵活性。
1.4路径path
path命名应该是以资源为导向的命名,对资源的操作是由HttpMethod(get、post、put、delete)来决定。所以一般来说url上的单词都应该是名词,一定不要是动词。一般遵循以下约定:
(1)URL 的命名必须全部小写;
(2) URL 必须 是易读的 URL;
(3)一定不可 暴露服务器架构
(4)出现复合词汇使用下划线分隔,例如:animal_types
举几个正面例子:
新增用户:http://localhost/user post方法提交;
修改用户:http://localhost/users put方法提交;
删除文章:http://localhost/articles?author=1&category=2delete方法提交;
查询用户:http://localhost/users get方法提交;
查询文章:http://localhost/articles?author=1&category=2get方法提交;
错误的例子如下:
http://localhost/get_user
https://api.example.com/getUserInfo?userid=1
https://api.example.com/getusers
https://api.example.com/sv/u
https://api.example.com/cgi-bin/users/get_user.php?userid=1
1.5动词
- RESTful的核心思想就是,客户端发出的数据操作指令都是"动词 + 宾语"的结构,动词通常就是四种 HTTP 方法,对应 CRUD 操作:
GET(SELECT):从服务器取出资源(一项或多项)。 POST(CREATE):在服务器新建一个资源。 PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。 PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。 DELETE(DELETE):从服务器删除资源。 |
其中
(1)删除资源 必须 用 DELETE 方法
(2)创建新的资源 必须 使用 POST 方法
(3)更新资源 应该 使用 PUT 方法
(4)获取资源信息 必须 使用 GET 方法
针对每一个路径来说,下面列出所有可行的 HTTP 动词和端点的组合
请求方 | URL | 描述 | |
法 | |||
GET | /zoos | 列出所有的动物园(ID和名称,不要太详细) | |
POST | /zoos | 新增一个新的动物园 | |
GET | /zoos/{zoo} | 获取指定动物园详情 | |
PUT | /zoos/{zoo} | 更新指定动物园(整个对象) | |
PATCH | /zoos/{zoo} | 更新动物园(部分对象) | |
DELETE | /zoos/{zoo} | 删除指定动物园 | |
GET | /zoos/{zoo}/animals | 检索指定动物园下的动物列表(ID和名称,不要太详 | |
细) | |||
GET | /animals | 列出所有动物(ID和名称)。 | |
POST | /animals | 新增新的动物 | |
GET | /animals/{animal} | 获取指定的动物详情 | |
PUT | /animals/{animal} | 更新指定的动物(整个对象) | |
PATCH | /animals/{animal} | 更新指定的动物(部分对象) | |
GET | /animal_types | 获取所有动物类型(ID和名称,不要太详细) | |
GET | /animal_types/{type} | 获取指定的动物类型详情 | |
GET | /employees | 检索整个雇员列表 | |
GET | /employees/{employee} | 检索指定特定的员工 | |
GET | /zoos/{zoo}/employees | 检索在这个动物园工作的雇员的名单(身份证和姓名) | |
POST | /employees | 新增指定新员工 | |
POST | /zoos/{zoo}/employees | 在特定的动物园雇佣一名员工 | |
DELETE | /zoos/{zoo}/employees/{employee} | 从某个动物园解雇一名员工 | |
1.6入参
1、如果记录数量很多,服务器不可能都将它们返回给用户。API 应该 提供参数,过滤返回结果。下面是一些常见的参数。
- ?limit=10:指定返回记录的数量
- ?o?set=10:指定返回记录的开始位置。
- ?page=2&per_page=100:指定第几页,以及每页的记录数。
- ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
- ?animal_type_id=1:指定筛选条件
所有URL参数 必须是全小写,必须使用下划线类型的参数形式。
分页参数 必须 固定为 page 、 per_page
经常使用的、复杂的查询 应该 标签化,降低维护成本,如
GET /trades?status=closed&sort=sortby=name&order=asc # 可为其定制快捷方式 GET /trades/recently_closed
2、入参可分为业务参数和公共参数;公共参数有:
参数 | 名称 | 说明 |
timestamp | 时间戳 | |
clientid | 调用方appid | 统一管理应用,否则不放行 |
token | 令牌 | 幂等情况可用 |
version | 版本号 |
1.7响应
1、出参(返回值):必须的字段有:
字段 | 类型 | 描述 |
code | 数值 | 状态码 |
msg | 字符串 | 信息描述 |
data | 结果集 | 返回结果集 |
2、如果请求处理完全正确,则状态码为0 ;
3、状态码暂定8位数数字,前4位为某一个应用(服务)拟的一个数字,后4位为具体的状态值。状态码分为2种---公共和自定义,公共码以0打头+3位数。
比如:
99990400 --客户端错误,比如请求语法格式错误、无效的请求、无效的签名等。
99991001 -----用户Id不能为空
响应的公共码如下:
编码 | 描述 | 说明 |
001 | 注解使用错误 | |
002 | 微服务不在线,或网络超时 | |
003 | TOKEN解析失败 | |
004 | TOKEN无效或没有对应的用户 | |
400 | 客户端错误,比如请求语法格式错误、 | 服务器 应该 放弃该请求 |
401 | 需要身份认证,比如access_token 无效/过期 | 客户端在收到 401 响应后, |
403 | 没有权限访问该请求 | 服务器收到请求但拒绝提供服务。 |
404 | 用户请求的资源不存在 | 如获取不存在的用户信息 |
410 | 请求的资源不存在,并且未来也不会存在 | 在收到 410 状态码后, |
429 | 请求次数超过允许范围 | |
500 | 未知异常 | 应该 提供完整的错误信息支持,也方便跟踪调试 |
1.8项目结构
1、采用经典DDD领域取到模型:(默认一个解决方案有5个项目)
5个项目分别为:
Web层为最外层接口定义;
Service为具体的应用服务处理;
Infrastructure基础设施层,处理具体的业务逻辑和数据DB的处理;
Domain领域层为模型和仓库接口interface;
Common为通用的一些Helper类;
2、一个解决方案创建5个项目(如上图),并且里包含常用的基础组件:Log4net日志,听云监听;dockerfile,skywalking,全局异常捕捉,接口请求开始和结束的日志记录,swagger,service层的依赖注入,Mapping等。
3、代码全部采用依赖注入写法,尽量少些静态类;
4、HttpClient的写法:使用采用.netcore官方提供的方法,采用工厂类+依赖注入方式:实例代码如下:
1、SartUp类里添加代码-- httpclient初始化: services.AddHttpClient("MsgApi", c => { c.BaseAddress = new Uri(Configuration["OuterApi:MsgApi:url"]); c.Timeout = TimeSpan.FromSeconds(30); }); //2 构造函注入 private IDbContext _dbContext; private IUnitOfWork _unitOfWork; private IordersRepository _ordersRepository; private IordercourseRepository _ordercourseRepository; private ILogger _logger; privatereadonly IConfiguration _config; privatereadonly IHttpClientFactory _clientFactory; public ordersService(IDbContext dbContext, ILogger<ordersService> logger, IConfiguration config, IHttpClientFactory clientFactory) { _dbContext = dbContext; _unitOfWork = new UnitOfWork(_dbContext); _ordersRepository = new ordersRepository(_dbContext); _ordercourseRepository = new ordercourseRepository(_dbContext); _mapper = mapper; _config = config; _logger = logger; _clientFactory = clientFactory; } //3使用 ///<summary> ///判断此时该校区是否可以下单 ///</summary> ///<param name="req"></param> ///<returns></returns> publicasync Task<Result<string>> CheckDept(CheckSchoolDeptReq req) { Result<string> sendRet = new Result<string>(); try { HttpClient client = _clientFactory.CreateClient("ContractApi"); MyHttpClientHelper myHttpClientHelper = new MyHttpClientHelper(); MarketToUPCCheckReq checkreq = new MarketToUPCCheckReq(); sendRet = await myHttpClientHelper.GetData<Result<string>>(client, "MarketToUPCCheck", checkreq); } catch (Exception ex) { sendRet.state = false; sendRet.error_code = ErrorCode.SysExceptionError; sendRet.error_msg = "调用《是否可以下订单接口》报错了。请重试或者联系管理员!"; _logger.LogError(ex, ErrorCode.SysExceptionError +"调用《是否可以下订单》接口报错了:" + ex.Message); } return sendRet; }
1.9日志
1、接口开始前和结束后都已在LogstashFilter里记录,接口里就不需要再次记录;
LogstashFilter里的代码如下:
/// <summary> /// 记录日志用过滤器 /// </summary> public class LogstashFilter : IActionFilter, IResultFilter { private string ActionArguments { get; set; } /// <summary> /// 请求体中的所有值 /// </summary> private string RequestBody { get; set; } private Stopwatch Stopwatch { get; set; } private ILogger _logger; public LogstashFilter(ILogger<LogstashFilter> logger ) { _logger = logger; } /// <summary> /// Action 调用前执行 /// </summary> /// <param name="context"></param> public void OnActionExecuting(ActionExecutingContext context) { long contentLen = context.HttpContext.Request.ContentLength == null ? 0 : context.HttpContext.Request.ContentLength.Value; if (contentLen > 0) { // 读取请求体中所有内容 System.IO.Stream stream = context.HttpContext.Request.Body; if (context.HttpContext.Request.Method == "POST") { stream.Position = 0; } byte[] buffer = new byte[contentLen]; stream.Read(buffer, 0, buffer.Length); RequestBody = System.Text.Encoding.UTF8.GetString(buffer);// 转化为字符串 } ActionArguments = JsonConvert.SerializeObject(context.ActionArguments); Stopwatch = new Stopwatch(); Stopwatch.Start(); string url = context.HttpContext.Request.Host + context.HttpContext.Request.Path + context.HttpContext.Request.QueryString; string method = context.HttpContext.Request.Method; _logger.LogInformation($"地址:{url} \n " + $"方式:{method} \n " + $"请求体:{RequestBody} \n " + $"完整参数:{ActionArguments}\n " ); } /// <summary> /// Action 方法调用后,Result 方法调用前执行 /// </summary> /// <param name="context"></param> public void OnActionExecuted(ActionExecutedContext context) { // do nothing } /// <summary> /// Result 方法调用前(View 呈现前)执行 /// </summary> /// <param name="context"></param> public void OnResultExecuting(ResultExecutingContext context) { // do nothing } /// <summary> /// Result 方法调用后执行 /// </summary> /// <param name="context"></param> public void OnResultExecuted(ResultExecutedContext context) { Stopwatch.Stop(); string url = context.HttpContext.Request.Host + context.HttpContext.Request.Path + context.HttpContext.Request.QueryString; string method = context.HttpContext.Request.Method; string qs = ActionArguments; string res = "在返回结果前发生了异常"; if (context.Result is ObjectResult) { dynamic result = context.Result.GetType().Name == "EmptyResult" ? new { Value = "无返回结果" } : context.Result as dynamic; if (result != null) { res = JsonConvert.SerializeObject(result.Value); } } _logger.LogInformation($"地址:{url} \n " + $"方式:{method} \n " + $"请求体:{RequestBody} \n " + $"参数:{qs}\n " + $"结果:{res}\n " + $"耗时:{Stopwatch.Elapsed.TotalMilliseconds} 毫秒"); } }
2、try Catch日志必须要添加LogError日志,并且要将堆栈信息记录,代码如下:
catch (Exception ex) { _logger.LogError(ex, ErrorCode500 + ex.Message); }