• .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=2 delete方法提交;

    查询用户: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动词

    1. 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:指定返回记录的数量
    • ?offset=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} 
     " +
                   $"方式:{method} 
     " +
                   $"请求体:{RequestBody} 
     " +
                   $"完整参数:{ActionArguments}
     " );
    
    
            }
    
            /// <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} 
     " +
                        $"方式:{method} 
     " +
                        $"请求体:{RequestBody} 
     " +
                        $"参数:{qs}
     " +
                        $"结果:{res}
     " +
                        $"耗时:{Stopwatch.Elapsed.TotalMilliseconds} 毫秒");
    
                
            }
        }

    2、try Catch日志必须要添加LogError日志,并且要将堆栈信息记录,代码如下: 

    catch (Exception ex)
                { 
                    _logger.LogError(ex, ErrorCode500 + ex.Message);
                }
  • 相关阅读:
    7.$a = 'abcdef'; 请取出$a的值并打印出第一个字母
    8.PHP可以和sql server/oracle等数据库连接吗?
    6.能够使HTML和PHP分离开使用的模板
    4.用PHP打印出前一天的时间格式是2006-5-10 22:21:21
    5.echo(),print(),print_r()的区别
    3.数据库中的事务是什么?
    spring中配置quartz调用两次及项目日志log4j不能每天生成日志解决方法
    tomcat7性能调优与配置(以windows版为例)
    eclipse中maven下载不了私服上面的第三方包问题
    birt4.6部署到tomcat及启动服务报错解决方法
  • 原文地址:https://www.cnblogs.com/puzi0315/p/12240442.html
Copyright © 2020-2023  润新知