精华内容
下载资源
问答
  • 请求接口及返回值规范[HTTP&RPC] 0. 禁止规则&设计原则 任何接口不允许使用编程语言相关的扩展名,可以使用与返回值类型相关的扩展名 ...接口应该尽量的少, 后端接口不应该因前端的简单格式调整或者查询...

    API设计规范

    1. 原则

        原则是规范的基本设计思路, 在规范中无法找到相应的设计细节时,则应该按照设计的初衷,思路或者原则来判断应该如何进行设计

    • 接口应该尽量的少, 后端接口不应该因前端的简单格式调整或者查询字段的多少有多调整,请注意一个后端接口不应该仅仅服务于一个前端需求,而应该是服务于一堆类似的各种可能的前端资源请求需求
      理由:因前端需求变更通常大于后端变更,接口设计时应考虑多种设计需求,后端接口尽量不受前端需求变更影响
    • 出于前端或后端性能考虑而增加冗余接口是允许的
    • 规范为避归这种问题而设计, 很理想, 各个项目可根据自身实际情况和综合考虑成本与可行性进行适当的裁减或部分采纳甚至修改,但建议尽量多的遵从

    2. API命名

    2.1 API命名风格的选择

        关于接口风格的设计一直争论不休, 请看这里 https://www.zhihu.com/question/28570307

        根据接口设计目标的不同, 可以主要分成以下两类:

    • 给项目内部前端开发调内部接口
      设计目标: 部分接口对性能要求高, 接口逻辑场景复杂
      建议设计: 注意区分资源型接口与非资源型接口, 建议优先采用Restful设计, 但注意不要被Restful束缚, 性能是最重要的
      设计理由: Restful有太多的优点, 可以促进对系统设计的深入分析, 可以方便的生成文档, 更加具体的规范和约束, 有利于接口的一致性, 方遍形成外部接口, 但因它的设计并未优先考虑性能, 所以需要有所取舍和折中,不要为了Rest而Rest
    • 给第三方调用的外部开放接口
    • 服务间调用的内部接口

    2.2 路径名

    • 路径名称不允许使用/结尾(需注意检查/结尾的路径是否是有效的)
      理由:搜索引擎优化角度考虑,一个同样的页面不应该有两个地址,会被搜索引擎认为是作弊,搜索引擎对无/的地址支持更加友好

    • 蛇底式命名风格(snake_case)
      关于分割 分隔符"-“与”_"的选择:

      1. -分隔符从大小写的键盘录入的角度属于小写(因不需要按Shift键),与URL中保持小写原则相一致(url的特定部分不区分大小写,且小写字母更容易录入)【缺点】
        -其实更容易识别 _会与下划线格式混淆(尤其在doc中书写地址的时候)【缺点】
      2. -用于连接和_用于连接语义上有区别,_通常用于连接含义相关的部分,而-用于连接相互较为独立的部分,这样看起来_语义上更接近 【优点】
        2.1 Google SEO 遵循这个语义 把-当空格处理,认为是三个关键词 会把_直接去掉,认为整体是一个单词
        2.2 大部分文本编辑器也遵循这个语义,双击选择时 _连接会认为是同一个单词同时选择,而-分隔的两个单词不会同时选中
        2.3 鉴于get传参与post传参的一致性,post body中的json也建议采用同样的命名规则。便于程序处理【可考究】
      3. 目前蛇底式与驼峰式较为常见,而烤串式不常见【优点】

      snake_case: 微信开放平台,腾讯开放平台,开源中国 百度,阿里云 gitee.com
      camelCase: 京东,百度,爱奇艺
      小写无连字符:百度百科

    2.2.1 资源类接口(resource) 【RestFul风格】

        资源名(resources_name)既为接口名称,名词复数形式(批量获取时语义更加),尽量一个单词(简洁) ;

        RestFul风格, 基于资源的设计,HTTP Method作为动词,表示要对资源执行的动作

    • 单资源怎删改查
    【GET】          /users/1           # 查看某个用户信息
    【POST】         /users             # 新建用户信息【非幂等, 多次执行可能会新建多个用户】
    【PUT】          /users/1           # 更新用户信息【幂等, 同一请求多次执行结果一定相同】, 局部替换或追加资源的字段, PUT也可以用于新增,但需是由客户端能够确定唯一主键的场景
    【PATCH】        /users/1           # 对用户信息执行某一个动作【非幂等, 同一请求多次执行可能获得不同结果】, 例如登录错误计数+1 多次执行资源产生的结果不同
    【DELETE】       /users/1           # 删除用户信息
    

        设计理由: PUT用于局部更新并不符合HTTP1.1和RFC5789规范,但依然这样设计,理由如下

    • GITLAB这样去实现, 虽然它不符合规范, Kuberenetes API则遵循了标准

    • 如果PUT只能用于全部替换, 那这个动词似乎没什么意义,而只是PATCH的一个特例

    • 简单批处理请求 (batch)

        同时处理多个资源, 但对每个资源的处理是一致的,比如更新同样的资源

    【GET】          /users             # 查询用户信息列表
    【PUT】          /users/1,2,3       # 更新用户信息【幂等, 同一请求多次执行结果一定相同】, 局部替换资源的字段,
    【PATCH】        /users/1,2,3       # 对用户信息执行某一个动作【非幂等, 同一请求多次执行可能获得不同结果】, 例如登录错误计数+1 多次执行资源产生的结果不同
    【DELETE】       /users/1,2,3       # 删除用户信息
    
    • 复杂批处理请求/混合请求(mixed)

        使用RFC6902 JSON Patch规范完成批量处理请求 https://tools.ietf.org/html/rfc6902

        原理:同时处理多个资源,但对每个资源的处理是不一致的,更新不一样的字段,甚至处理的动作也是不一致的
    建议将动作封装到body中并组合成一个新的body发送给后端
    PATCH JSON Patch规范进行批量操作:

    • 聚合请求/页面级请求

        用于首页聚合显示,展板展厅类页面
    待研究… 目前建议后端封装专用接口,前端一次请求

    2.2.2 非资源类接口(non-resource)【JSON-RPC风格】

    禁止使用其它HTTP Method
    POST  数据提交(意图修改服务器端的数据)
    GET   数据获取  
    

        接口名使用动宾结构 通常接口对应一个动作或状态而不是操作一个资源, (例如:健康检查, 日志查看,附件上传)
    可考虑名称细节继续规范化

    2.2.3 资源间逻辑关系

        两个资源之间可能存在关系,例如班级的学生,老师的学生,用户的角色
        目前在资源从属关系上没有太多的实践经验,建议参考gitlab资源接口设计,gitlab从存在较为复杂的逻辑关系

    通常建议:

    • 具有明确从属关系资源且不具备全局特点的接口,可以设计为类似于resource/subsource的形式
    • 具有全局特点的资源优先实现顶层接口, 优先设计全局接口resouces1, 可选设计为resource2/subsource1形式,用于应对多对多关系,后者只返回具有从属关系的部分数据
    • 建议设计为子资源而不是资源的一个属性(可以是数组类型的属性),应考虑子资源是否有一定的复杂性(具备多个字段的对象),应考虑子资源是否需要被独立检索

    2.3 扩展名

    • 接口扩展名不允许带有框架相关信息
      例如: .action .do
      理由: 安全角度考虑,将保留后台实现方案,黑客可根据框架日志漏洞进行针对性攻击
    • 接口扩展名可以使用返回值格式作为扩展名或无扩展名实现, 建议无扩展名是默认返回格式为json
      例如: .json .xml
      理由: 前端语言对json最为友好,毕竟JSON是JavaScript Object Notation

    2.4 传参规范

    2.4.1 参数位置

    1. 用于排序,搜索,过滤等请求条件参数应放在请求字符串列表,不允许放在body中(尤其是post请求)
      理由:便于用户视觉识别或快速的手动在地址栏修改
    2. 用户排序,搜索,过滤等请求条件参数应在浏览器地址栏中显示(尤其适用react-route技术实现的应注意)
      理由:便于用户对连接进行分享
    3. 涉及用户敏感隐私的字段不允许出现在浏览器地址栏中,应放在请求body中
      例如:密码,Token
      理由:防止被用户身旁的人意外观察而被记忆

    2.4.2 参数名称

    • 分页及排序参数

      参数规范名参数作用位置默认值备注
      page当前页码地址栏1
      per_page每页条目数地址栏20这个值必须有,用以计算需要返回的总条目数
      limit单次请求总条目数限制地址栏100防止意外查询过量数据, 后台也应做最大值限制
      sort升序或降序地址栏desc降序通常常用,更关心最近操作的条目
      order_by排序列地址栏id建议不要乱序或随机, 更关心最近插入的条目

    理由:
        sort与order_by的参考gitlab api设计 order_by后面加上by可以避免与sort之间发生歧义 明确表示是排序字段或者排序列

    • 条目筛选参数(行筛选, 返回满足条件的行)

      行筛选的筛选参数名应与资源数据返回中的参数名一致,故建议采用蛇底式(snake_case)命名规范

    • 内容筛选参数(列筛选或关系筛选)

      内容筛选性参数均为布尔型,注意遵循2.3.3中对布尔值,未传值和传空值两种特殊情况的处理规范

      参数规范名参数作用位置备注
      simple只返回简要信息地址栏参考5.1.1 两分组列筛选设计规范
      [groupname]附加分组列信息地址栏参考5.1.2 多分组列筛选设计规范

    2.4.3 参数值类型

    参数类型取值规范备注
    布尔型真: true或1 假: false或0未传值,按假值处理,传空值,按真值处理
    数组型(GET传参)1,2,3可用于多条目修改删除, post传参时按相关content-type格式执行
    时间日期型(优先)11位 GMT/UTC时间戳不带有时区信息,防止在多语言操作系统中造成服务器与客户端理解不一致,防止前端或后端的设计依赖特定的时间格式, 缺点不能飙到1970年前的时间
    时间日期型(备选)ISO8601规范需要表达1970年以前的时间时考虑
    枚举型枚举值或枚举名是不太判断得出哪个优先,可实践后获得结论
    待补充…

    3. 请求规范

    • GET请求不允许携带Body
      理由: 不符合HTTP规范,有些代理服务器会直接将GET请求的Body丢弃(这些代理服务器或中间服务器不一定在我们能控制的范围内)

    • 请求接口是应准确携带content-type头部, 用于表征请求的body中的数据类型,后端需要具此来解析对应body中的数据
      建议: 请求json格式的接口。建议携带content-type=application/json

      其它常用content-type值参考下表

      Content-Type:application/json                      (推荐,JsonBody提交, 尤其是参数复杂时, 可清晰的表达嵌套数据,可能是趋势,案例:京东,csdn,airbnb,bilibili)
      |-如果是base64个是的图片, 建议图片编码为字符串之后以json格式返回给前端
      Content-Type:application/x-www-form-urlencoded     (推荐,普通表单提交, 目前这种方式仍然居多)
      Content-Type:multipart/form-data                   (文件上传时, 用于带有文件上传的表单提交)
      Content-Type:application/x-protobuf                 Google ProtoBuf格式, 在前端使用protobuf格式目前仍存在争议, 但仍然可见使用案例(例如: 知乎前端数据分析接口)
      Content-Type:text/plain                             普通文本
      Content-Type:text/html; charset=utf-8               HTML文件
      Content-Type:text/css                               CSS文件
      Content-Type:application/javascript; charset=utf-8  JS文件
      Content-Type:image/png                              PNG图片
      Content-Type:image/gif                              GIF图片
      Content-Type:image/x-icon                           ico图片
      Content-Type:image/webp                             WEBP图片
      content-type:image/svg+xml                          SVG图片
      
      

    4. 响应规范

    • 账户授权等敏感数据应存储于Cookie当中, 并设置HTTP-only属性,用以阻止js脚本读取权限,禁止存储于LocalStorage等其它存储技术
      理由: 目前只有cookie存储具备安全防御机制,也就是Http-only, 防御可能的跨站请求攻击(风险极大)

    • 响应后content-type必须与响应body的数据类型一致,前端会据此对响应body进行类型转换
      例如: json的body如果返回text/plain类型 那么前端接受时将识别body为字符串而不是json对象
      常用content-type值参考3.

      最合适的Ajax内容编码类型 https://segmentfault.com/a/1190000006871099

    4.1 数据分页的响应规范

        建议采用 Link Header机制返回分页元信息, 以此不去破坏返回Body中存储的是资源内容的restful设计思想
    https://git.d.com/help/api/README.md#pagination-link-header

    4.2 错误码

    4.2.1 返回码设计

    • 设计目标
      为了便于分析统计软件可以快速的统计异常
      为了快速确定谁要对错误进行处理
      为了快速定位产生异常的模块甚至代码位置
      尽量不破坏HTTP返回码的语义
      前端程序分类处理的便利

        错误码为7位数据

    • 0表示正常
      理由: 因正常只有一种,而错误有很多种, 0为正常便于程序中用if判断

    • 第1位表示错误来源
      2 客户端错误(用户自行解决,或管理员协助通过系统配置解决)
      4 客户端错误(需要研发介入,修复需要前端修改代码)
      5 服务端错误(临时异常,不需要研发介入,修复不需要修改代码)
      6 服务端错误(软件bug)

        第2-3位表示服务代码(便于统计)
        第4-7位表示具体错误(前端引导提示和后端查询)

    4.2.2 全局结构

    {
      "meta":{
        "code":"10000",   
        "msg": "ok";
      }
      "data": {} or [], # 内部格式由具体业务决定
    }
    

    4.2.3 异常结构

    4.2.3.1 服务端异常结构(仅开发环境)
    • 多行文本类异常信息(仅开发环境)
          有些异常的反馈信息文本有多行,多行文本直接以字符串格式包装进json的话,前端的显示非常的丑陋(出现大量的\r\n)符号,因此建议将多行文本封装为字符串列表,以优化浏览器端显示

      ...
      "data": {
        "type":"text",
        "ip": "暴异常的结点IP",
        "exception": [
          "line1",
          "line2",
          "line3"
        ];
      }
      ...
      
    • 异常栈类型异常结构(仅开发环境)

        异常栈列表是有序的, 所以后端实现时应注意保序

    ...
    "data": {
      "type":"exceptions",
      "ip": "暴异常的结点IP",
      "exception": {
        "exception_name1": "exception_message1",
        "exception_name2": "exception_message2",
        "exception_nam32": "exception_message3",
      }
    }
    ...
    
    • 栈追踪(stacktrace)类型异常结构(仅开发环境)

      ...
      "data": {
        "type":"stacktrace",
        "ip": "暴异常的结点IP",
        "exception": 待定...
      }
      ...
      
    • 其它对象类型异常结构(仅开发环境)

      ...
      "data": {
        "type":"error",
        "ip": "暴异常的结点IP",
        "exception": {
           自定义Object
        }
      }
      ...
      
    4.2.3.2 参数错误异常结构(全部环境)

        错误码: 2000301

        参数错误信息反馈在data域, data是一个数组,表示一组参数的错误,field字段值应该与参数名称一致,用于帮助前端实现时定位错误的输入框,message可供前端参考提示,但前端可以选择提示此信息也可以考虑以更人性化的方式给出合适的提示

    ...
    "data": [{
      "field":"username",
      "message": "用户名不满足安全要求";
    },{
      "field":"mobile",
      "message": "手机号格式不正确";
    }]
    ...
    

    5. 其它API功能设计

    5.1 内容筛选(列筛选或关系筛选)

    5.1.1 两分组列筛选

        接口返回的列字段中,可以分成两组,一组是简要信息,另外一组是扩展信息,建议使用布尔型参数simple进行列筛选

    • simple=true时 返回简要信息
    • simple=false时 返回完整信息(简要信息+扩展信息)
    • simple未传参时 返回完整信息(例如,符合语义,未传参未设置false)

        场景示例: 获取用户列表接口, simple=true时只返回用户信息,simple=false时,返回用户信息及用户所属的资源信息(用户的项目,用户的…)

    5.1.2 多分组列筛选

        接口返回的列字段中,会被分为多个分组,为每个列分组其一个有含义的名称,当传参groupname=true或传递空值时 ,则返回,未传参或传递false时,不返回属于此分组的列

    例如:
    GET users?role=&project=
    则返回用户基本信息,用户所属用户组信息,用户拥有项目信息

    5.1.3 复杂字段筛选

        复杂字段筛选,可能会要求控制粒度到每个字段级别,建议使用GraphQL技术实现

    A 返回码与HTTP状态值表

    0表示正常【可选实现, 不确定这么做有什么实质性的好处】
    0(200) 通用正常, 如果不知道用什么, 就是用这个
    0(201) 创建成功, 可用于Rest接口的POST新增对象 
    0(202) 可用于异步请求, 长任务, 利于短信, 邮件发送, 提示浏览器可以不必保持连接
    0(204) 服务器成功响应,但是没有返回数据回, 通常可用于Rest接口的Delete功能
    
    错误码为7位数据
    第1位表示错误来源(放置于第一位, 便于程序判断和统计分类)  
     |-2 客户端错误(用户自行解决,或管理员协助通过系统配置解决)
     |-4 客户端错误(需要研发介入,修复需要前端修改代码)
     |-5 服务端错误(临时异常,不需要研发介入,修复不需要修改底阿妈)
     |-6 服务端错误(软件bug)
    第2-3位表示服务代码  
     |-00 系统错误(一般为后端bug或故障导致,或多个服务通用的错误)
       |-400xxxx         客户端错误(返回结构中给出错误细节)  
         |-4000xxx(4xx)    HTTP客户端标准错误映射(一般未请求格式错误,需研发介入修正)
           |-4000400(400)    其它未细分的请求格式不正确(需研发介入修正)
           |-4000404(404)    请求资源【接口】不存在(通常需要研发或运维介入修正, 注意与数据不存在区分)
           |-4000405(405)    不支持的http-method(method错误一定是研发疏忽,需要研发介入修正)
           |-4000415(415)    不支持的Content-Type(需要介入修正)
           |-4000601(400)    Json语法不正确(需前端研发接入修正)
       |-200xxxx         用户可自行修正的系统公共错误(考虑前端处理便利, 根据所需处理进行细分,可能存在不同逻辑用返回值区分,相同逻辑不同提示用错误细节区分)
         |-20001xx(401)    认证类问题(表达用户提供的身份信息的异常)
           |-2000100(401)    其它位置的认证类问题
           |-2000101(401)    需要认证类接口但未提供任何认证信息, 但不确定登录后是否有接口权限或数据权限,前端应根据情况适当进行登录引导  
           |-2000102(401)    认证信息已失效(例如Token)
           |-2000103(401)    认证信息不可用(例如Token)
         |-20002xx(200)    鉴权类问题(表达用户提供合理身份信息后对接口和数据的所有权限异常)
           |-2000201(200)    请求的【数据】无权限访问,但接口有权限,无法通过登录来解决问题,用户只能放弃访问,或联系管理员授权
           |-2000202(200)    请求的【接口】无权限访问,通常无权限的接口不应引导调用,但也可能引导用户申请权限  
         |-20003xx         请求数据的格式类问题
           |-2000301(200)    其它非细分的请求格式或参数不正确
           |-2000304(200)    请求的【数据】不存在,注意与接口不存在相区分,接口不存在需要研发介入,数据不存在用户可自行修正
       |-500xxxx   服务端错误(返回结构中开发环境给出细节,生产环境之给响应值,不给细节)
         |-50001xx(503)  数据库相关网络异常
           |-5000100(503)  数据库连接异常(配置错误,网络异常等)
           |-5000101(503)  数据库连接异常(数据库被正常关闭)
           |-5000102(503)  数据库连接异常(数据库异常退出)
         |-5000200(503)  负载均衡结点(无可用结点,结点配置错误或网络连接异常)
         |-50005xx(503)  服务端HTTP网络异常(临时网络故障[可能自动修复], 配置错误[不可自动修复])
           |-5000502(503)  HTTP协议网络连接异常 对应HTTP502
           |-5000503(503)  HTTP协议网络连接异常 对应HTTP503 与之上的区别是tcp可以是否可以连通
           |-5000504(503)  HTTP协议网络连接异常 对应HTTP504 (连接超时)
       |-600xxxx(500)   初步识别的软件bug(需程序员介入处理)
         |-6000000(500)   服务端未知错误(软件中未识别和处理的软件bug)
           |-6000001(500)   空指针异常
         |-60001xx(500)     数据库相关错误(未判断出来的)
           |-6000101(500)     SQL语法错误
           |-6000102(500)     数据库返回值不符合预期(SelectOne时返回多条记录)
    
     |-01 用户权限系统(非通用部分)
     |-02 文件管理/上传下载系统
     |-xx 其它服务代码(一般为用户参数输入错误,业务系统建议使用30以上代码)
    第4-7位表示具体错误 
     |-xx 具体的错误代码(由具体服务定义)
    

    B 参考文献

    错误码设计 https://blog.csdn.net/yzzst/article/details/54799971
    聊聊RESTful https://howardwchen.com/2017/09/18/talk-about-restful-popular-api-design-1/
    Kubernetes API规约 https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#patch-operations
    【主要参考】Restful风格的接口设计 https://juejin.im/entry/59b8d34c6fb9a00a4455dd04
    腾讯开放平台 http://open.qq.com/
    URI设计原则 https://stackoverflow.com/questions/1619152/how-to-create-rest-urls-without-verbs/1619677#1619677
    Gitlab API V3 https://link.zhihu.com/?target=https%3A//developer.github.com/v3/
    返回值的处理 https://www.v2ex.com/t/340607?p=2
    RESTful API定义及使用规范 https://zhuanlan.zhihu.com/p/31298060
    HTTP状态值https://zhuanlan.zhihu.com/p/31298060
    http://wiki.open.qq.com/wiki/v3/user/get_info open.weibo.com
    http://wiki.open.qq.com/wiki/website/%E5%BE%AE%E5%8D%9A%E7%A7%81%E6%9C%89%E8%BF%94%E5%9B%9E%E7%A0%81%E8%AF%B4%E6%98%8E
    https://docs.open.alipay.com/common/105806
    http://open.weibo.com/wiki/Error_code
    https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419318634&token=&lang=zh_CN
    http://lbs.amap.com/api/webservice/info/

    展开全文
  • 文章目录一、前言二、环境说明...虽然说后端接口的编写并没有统一规范要求,而且如何构建这几个部分每个公司要求都不同,没有什么“一定是最好的”标准,但其中最重要的关键点就是看是否规范。 二、环境说明  因为讲解


    一、前言

     一个后端接口大致分为四个部分组成:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。虽然说后端接口的编写并没有统一规范要求,而且如何构建这几个部分每个公司要求都不同,没有什么“一定是最好的”标准,但其中最重要的关键点就是看是否规范。

    二、环境说明

     因为讲解的重点是后端接口,所以需要导入一个spring-boot-starter-web包,而lombok作用是简化类,前端显示则使用了knife4j,具体使用在Spring Boot整合knife4j实现Api文档已写明。另外从springboot-2.3开始,校验包被独立成了一个starter组件,所以需要引入如下依赖:

    <dependency>
    			<!--新版框架没有自动引入需要手动引入-->
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-validation</artifactId>
            </dependency>
    
            <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>knife4j-spring-boot-starter</artifactId>
                <!--在引用时请在maven中央仓库搜索最新版本号-->
                <version>2.0.2</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
    

    三、参数校验

     一个接口一般对参数(请求数据)都会进行安全校验,参数校验的重要性自然不必多说,那么如何对参数进行校验就有讲究了。一般来说有三种常见的校验方式,我们使用了最简洁的第三种方法

    • 业务层校验
    • Validator + BindResult校验
    • Validator + 自动抛出异常

     业务层校验无需多说,即手动在java的Service层进行数据校验判断。不过这样太繁琐了,光校验代码就会有很多。
     而使用Validator+ BindingResult已经是非常方便实用的参数校验方式了,在实际开发中也有很多项目就是这么做的,不过这样还是不太方便,因为你每写一个接口都要添加一个BindingResult参数,然后再提取错误信息返回给前端(简单看一下)。

    @PostMapping("/addUser")
    public String addUser(@RequestBody @Validated User user, BindingResult bindingResult) {
        // 如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里
        List<ObjectError> allErrors = bindingResult.getAllErrors();
        if(!allErrors.isEmpty()){
            return allErrors.stream()
                .map(o->o.getDefaultMessage())
                .collect(Collectors.toList()).toString();
        }
        // 返回默认的错误信息
        // return allErrors.get(0).getDefaultMessage();
        return validationService.addUser(user);
    }
    

    Validator + 自动抛出异常(使用)

     内置参数校验如下:

    注解校验功能
    @AssertFalse必须是false
    @AssertTrue必须是true
    @DecimalMax小于等于给定的值
    @DecimalMin大于等于给定的值
    @Digits可设定最大整数位数和最大小数位数
    @Email校验是否符合Email格式
    @Future必须是将来的时间
    @FutureOrPresent当前或将来时间
    @Max最大值
    @Min最小值
    @Negative负数(不包括0)
    @NegativeOrZero负数或0
    @NotBlank不为null并且包含至少一个非空白字符
    @NotEmpty不为null并且不为空
    @NotNull不为null
    @Null为null
    @Past必须是过去的时间
    @PastOrPresent必须是过去的时间,包含现在
    @PositiveOrZero正数或0
    @Size校验容器的元素个数

     首先Validator可以非常方便的制定校验规则,并自动帮你完成校验。首先在入参里需要校验的字段加上注解,每个注解对应不同的校验规则,并可制定校验失败后的信息:

    @Data
    public class User {
        @NotNull(message = "用户id不能为空")
        private Long id;
    
        @NotNull(message = "用户账号不能为空")
        @Size(min = 6, max = 11, message = "账号长度必须是6-11个字符")
        private String account;
    
        @NotNull(message = "用户密码不能为空")
        @Size(min = 6, max = 11, message = "密码长度必须是6-16个字符")
        private String password;
    
        @NotNull(message = "用户邮箱不能为空")
        @Email(message = "邮箱格式不正确")
        private String email;
    }
    

     校验规则和错误提示信息配置完毕后,接下来只需要在接口仅需要在校验的参数上加上@Valid注解(去掉BindingResult后会自动引发异常,异常发生了自然而然就不会执行业务逻辑):

    @RestController
    @RequestMapping("user")
    public class ValidationController {
    
        @Autowired
        private ValidationService validationService;
    
        @PostMapping("/addUser")
        public String addUser(@RequestBody @Validated User user) {
    
            return validationService.addUser(user);
        }
    }
    
    

     现在我们进行测试,打开knife4j文档地址,当输入的请求数据为空时,Validator会将所有的报错信息全部进行返回,所以需要与全局异常处理一起使用。

    // 使用form data方式调用接口,校验异常抛出 BindException
    // 使用 json 请求体调用接口,校验异常抛出 MethodArgumentNotValidException
    // 单个参数校验异常抛出ConstraintViolationException
    // 处理 json 请求体调用接口校验失败抛出的异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultVO<String> MethodArgumentNotValidException(MethodArgumentNotValidException e) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        List<String> collect = fieldErrors.stream()
            .map(DefaultMessageSourceResolvable::getDefaultMessage)
            .collect(Collectors.toList());
        return new ResultVO(ResultCode.VALIDATE_FAILED, collect);
    }
    // 使用form data方式调用接口,校验异常抛出 BindException
    @ExceptionHandler(BindException.class)
    public ResultVO<String> BindException(BindException e) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        List<String> collect = fieldErrors.stream()
            .map(DefaultMessageSourceResolvable::getDefaultMessage)
            .collect(Collectors.toList());
        return new ResultVO(ResultCode.VALIDATE_FAILED, collect);
    }
    

    请添加图片描述

    分组校验和递归校验

     分组校验有三个步骤:

    1. 定义一个分组类(或接口)
    2. 在校验注解上添加groups属性指定分组
    3. Controller方法的@Validated注解添加分组类
    public interface Update extends Default{
    }
    
    @Data
    public class User {
        @NotNull(message = "用户id不能为空",groups = Update.class)
        private Long id;
    	......
    }
    
    @PostMapping("update")
    public String update(@Validated({Update.class}) User user) {
        return "success";
    }
    

     如果Update不继承Default@Validated({Update.class})就只会校验属于Update.class分组的参数字段;如果继承了,会校验了其他默认属于Default.class分组的字段。

     对于递归校验(比如类中类),只要在相应属性类上增加@Valid注解即可实现(对于集合同样适用)

    自定义校验

    Spring Validation允许用户自定义校验,实现很简单,分两步:

    1. 自定义校验注解
    2. 编写校验者类
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @Constraint(validatedBy = {HaveNoBlankValidator.class})// 标明由哪个类执行校验逻辑
    public @interface HaveNoBlank {
    
        // 校验出错时默认返回的消息
        String message() default "字符串中不能含有空格";
        Class<?>[] groups() default { };
        Class<? extends Payload>[] payload() default { };
        /**
         * 同一个元素上指定多个该注解时使用
         */
        @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
        @Retention(RUNTIME)
        @Documented
        public @interface List {
            NotBlank[] value();
        }
    }
    
    public class HaveNoBlankValidator implements ConstraintValidator<HaveNoBlank, String> {
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            // null 不做检验
            if (value == null) {
                return true;
            }
            // 校验失败
            return !value.contains(" ");
            // 校验成功
        }
    }
    

    四、全局异常处理

     参数校验失败会自动引发异常,我们当然不可能再去手动捕捉异常进行处理。但我们又不想手动捕捉这个异常,又要对这个异常进行处理,那正好使用SpringBoot全局异常处理来达到一劳永逸的效果!

    基本使用

    首先,我们需要新建一个类,在这个类上加上@ControllerAdvice@RestControllerAdvice注解,这个类就配置成全局处理类了。(这个根据你的Controller层用的是@Controller还是@RestController来决定) 然后在类中新建方法,在方法上加上@ExceptionHandler注解并指定你想处理的异常类型,接着在方法内编写对该异常的操作逻辑,就完成了对该异常的全局处理!我们现在就来演示一下对参数校验失败抛出的MethodArgumentNotValidException全局处理:

    package com.csdn.demo1.global;
    
    import org.springframework.validation.ObjectError;
    import org.springframework.web.bind.MethodArgumentNotValidException;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    @RestControllerAdvice
    @ResponseBody
    public class ExceptionControllerAdvice {
    
        @ExceptionHandler(MethodArgumentNotValidException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
            // 从异常对象中拿到ObjectError对象
            ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
            // 然后提取错误提示信息进行返回
            return objectError.getDefaultMessage();
        }
    }
    

     我们再次进行测试,这次返回的就是我们制定的错误提示信息!我们通过全局异常处理优雅的实现了我们想要的功能!以后我们再想写接口参数校验,就只需要在入参的成员变量上加上Validator校验规则注解,然后在参数上加上@Valid注解即可完成校验,校验失败会自动返回错误提示信息,无需任何其他代码!
    在这里插入图片描述

    自定义异常

     在很多情况下,我们需要手动抛出异常,比如在业务层当有些条件并不符合业务逻辑,而使用自定义异常有诸多优点:

    • 自定义异常可以携带更多的信息,不像这样只能携带一个字符串。
    • 项目开发中经常是很多人负责不同的模块,使用自定义异常可以统一了对外异常展示的方式。
    • 自定义异常语义更加清晰明了,一看就知道是项目中手动抛出的异常。
       我们现在就来开始写一个自定义异常:
    package com.csdn.demo1.global;
    
    import lombok.Getter;
    
    @Getter //只要getter方法,无需setter
    public class APIException extends RuntimeException {
        private int code;
        private String msg;
    
        public APIException() {
            this(1001, "接口错误");
        }
    
        public APIException(String msg) {
            this(1001, msg);
        }
    
        public APIException(int code, String msg) {
            super(msg);
            this.code = code;
            this.msg = msg;
        }
    }
    

     然后在刚才的全局异常类中加入如下:

    	//自定义的全局异常
        @ExceptionHandler(APIException.class)
        public String APIExceptionHandler(APIException e) {
            return e.getMsg();
        }
    

     这样就对异常的处理就比较规范了,当然还可以添加对Exception的处理,这样无论发生什么异常我们都能屏蔽掉然后响应数据给前端,不过建议最后项目上线时这样做,能够屏蔽掉错误信息暴露给前端,在开发中为了方便调试还是不要这样做。另外,当我们抛出自定义异常的时候全局异常处理只响应了异常中的错误信息msg给前端,并没有将错误代码code返回。这还需要配合数据统一响应。

    五、数据统一响应

     统一数据响应是我们自己自定义一个响应体类,无论后台是运行正常还是发生异常,响应给前端的数据格式是不变的!这里我包括了响应信息代码code和响应信息说明msg,首先可以设置一个枚举规范响应体中的响应码和响应信息。

    @Getter
    public enum ResultCode {
        SUCCESS(1000, "操作成功"),
        FAILED(1001, "响应失败"),
        VALIDATE_FAILED(1002, "参数校验失败"),
        ERROR(5000, "未知错误");
        private int code;
        private String msg;
        ResultCode(int code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    }
    

     自定义响应体

    package com.csdn.demo1.global;
    
    import lombok.Getter;
    
    @Getter
    public class ResultVO<T> {
        /**
         * 状态码,比如1000代表响应成功
         */
        private int code;
        /**
         * 响应信息,用来说明响应情况
         */
        private String msg;
        /**
         * 响应的具体数据
         */
        private T data;
        
        public ResultVO(T data) {
            this(ResultCode.SUCCESS, data);
        }
    
        public ResultVO(ResultCode resultCode, T data) {
            this.code = resultCode.getCode();
            this.msg = resultCode.getMsg();
            this.data = data;
        }
    }
    

     最后需要修改全局异常处理类的返回类型

    @RestControllerAdvice
    public class ExceptionControllerAdvice {
    
        @ExceptionHandler(APIException.class)
        public ResultVO<String> APIExceptionHandler(APIException e) {
            // 注意哦,这里传递的响应码枚举
            return new ResultVO<>(ResultCode.FAILED, e.getMsg());
        }
    
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
            ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
            // 注意哦,这里传递的响应码枚举
            return new ResultVO<>(ResultCode.VALIDATE_FAILED, objectError.getDefaultMessage());
        }
    }
    

     最后在controller层进行接口信息数据的返回

    @GetMapping("/getUser")
        public ResultVO<User> getUser() {
            User user = new User();
            user.setId(1L);
            user.setAccount("12345678");
            user.setPassword("12345678");
            user.setEmail("123@qq.com");
    
            return new ResultVO<>(user);
        }
    

     经过测试,这样响应码和响应信息只能是枚举规定的那几个,就真正做到了响应数据格式、响应码和响应信息规范化、统一化!
    在这里插入图片描述

    六、全局处理响应数据(可选择)

     接口返回统一响应体 + 异常也返回统一响应体,其实这样已经很好了,但还是有可以优化的地方。要知道一个项目下来定义的接口搞个几百个太正常不过了,要是每一个接口返回数据时都要用响应体来包装一下好像有点麻烦,有没有办法省去这个包装过程呢?当然是有的,还是要用到全局处理。但是为了扩展性,就是允许绕过数据统一响应(这样就可以提供多方使用),我们可以自定义注解,利用注解来选择是否进行全局响应包装。
     首先创建自定义注解,作用相当于全局处理类开关:

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD}) // 表明该注解只能放在方法上
    public @interface NotResponseBody {
    }
    

     其次创建一个类并加上注解使其成为全局处理类。然后继承ResponseBodyAdvice接口重写其中的方法,即可对我们的controller进行增强操作,具体看代码和注释:

    package com.csdn.demo1.global;
    
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.core.MethodParameter;
    import org.springframework.http.MediaType;
    import org.springframework.http.converter.HttpMessageConverter;
    import org.springframework.http.server.ServerHttpRequest;
    import org.springframework.http.server.ServerHttpResponse;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
    
    @RestControllerAdvice(basePackages = {"com.scdn.demo1.controller"}) // 注意哦,这里要加上需要扫描的包
    public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
        @Override
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
           // 如果接口返回的类型本身就是ResultVO那就没有必要进行额外的操作,返回false
            // 如果方法上加了我们的自定义注解也没有必要进行额外的操作
            return !(returnType.getParameterType().equals(ResultVO.class) || returnType.hasMethodAnnotation(NotResponseBody.class));
        }
    
        @Override
        public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {
            // String类型不能直接包装,所以要进行些特别的处理
            if (returnType.getGenericParameterType().equals(String.class)) {
                ObjectMapper objectMapper = new ObjectMapper();
                try {
                    // 将数据包装在ResultVO里后,再转换为json字符串响应给前端
                    return objectMapper.writeValueAsString(new ResultVO<>(data));
                } catch (JsonProcessingException e) {
                    throw new APIException("返回String类型错误");
                }
            }
            // 将原本的数据包装在ResultVO里
            return new ResultVO<>(data);
        }
    }
    

     重写的这两个方法是用来在controller将数据进行返回前进行增强操作,supports方法要返回为true才会执行beforeBodyWrite方法,所以如果有些情况不需要进行增强操作可以在supports方法里进行判断。对返回数据进行真正的操作还是在beforeBodyWrite方法中,我们可以直接在该方法里包装数据,这样就不需要每个接口都进行数据包装了,省去了很多麻烦。此时controller只需这样写就行了:

    @GetMapping("/getUser")
    //@NotResponseBody  //是否绕过数据统一响应开关
    public User getUser() {
        User user = new User();
        user.setId(1L);
        user.setAccount("12345678");
        user.setPassword("12345678");
        user.setEmail("123@qq.com");
        // 注意哦,这里是直接返回的User类型,并没有用ResultVO进行包装
        return user;
    }
    

    七、总结

    自此整个后端接口基本体系就构建完毕了

    • 通过Validator + 自动抛出异常来完成了方便的参数校验
    • 通过全局异常处理 + 自定义异常完成了异常操作的规范
    • 通过数据统一响应完成了响应数据的规范
    • 多个方面组装非常优雅的完成了后端接口的协调,让开发人员有更多的经历注重业务逻辑代码,轻松构建后端接口

    参考文章

    展开全文
  • SpringBoot写后端接口,看这一篇就够了!

    万次阅读 多人点赞 2020-09-15 14:22:52
    一个后端接口大致分为四个部分组成:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。如何构建这几个部分每个公司要求都不同,没有什么“一定是最好的”标准,但一个...
    摘要:本文演示如何构建起一个优秀的后端接口体系,体系构建好了自然就有了规范,同时再构建新的后端接口也会十分轻松。

    一个后端接口大致分为四个部分组成:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。如何构建这几个部分每个公司要求都不同,没有什么“一定是最好的”标准,但一个优秀的后端接口和一个糟糕的后端接口对比起来差异还是蛮大的,其中最重要的关键点就是看是否规范!

    本文就一步一步演示如何构建起一个优秀的后端接口体系,体系构建好了自然就有了规范,同时再构建新的后端接口也会十分轻松。

    所需依赖包

    这里用的是SpringBoot配置项目,本文讲解的重点是后端接口,所以只需要导入一个spring-boot-starter-web包就可以了:

    <!--web依赖包,web应用必备-->
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    本文还用了swagger来生成API文档,lombok来简化类,不过这两者不是必须的,可用可不用。

    参数校验

    一个接口一般对参数(请求数据)都会进行安全校验,参数校验的重要性自然不必多说,那么如何对参数进行校验就有讲究了。

    业务层校验

    首先我们来看一下最常见的做法,就是在业务层进行参数校验:

    public String addUser(User user) {
         if (user == null || user.getId() == null || user.getAccount() == null || user.getPassword() == null || user.getEmail() == null) {
             return "对象或者对象字段不能为空";
         }
         if (StringUtils.isEmpty(user.getAccount()) || StringUtils.isEmpty(user.getPassword()) || StringUtils.isEmpty(user.getEmail())) {
             return "不能输入空字符串";
         }
         if (user.getAccount().length() < 6 || user.getAccount().length() > 11) {
             return "账号长度必须是6-11个字符";
         }
         if (user.getPassword().length() < 6 || user.getPassword().length() > 16) {
             return "密码长度必须是6-16个字符";
         }
         if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", user.getEmail())) {
             return "邮箱格式不正确";
         }
         // 参数校验完毕后这里就写上业务逻辑
         return "success";
     }

    这样做当然是没有什么错的,而且格式排版整齐也一目了然,不过这样太繁琐了,这还没有进行业务操作呢光是一个参数校验就已经这么多行代码,实在不够优雅。

    我们来改进一下,使用Spring Validator和Hibernate Validator这两套Validator来进行方便的参数校验!这两套Validator依赖包已经包含在前面所说的web依赖包里了,所以可以直接使用。

    Validator + BindResult进行校验

    Validator可以非常方便的制定校验规则,并自动帮你完成校验。首先在入参里需要校验的字段加上注解,每个注解对应不同的校验规则,并可制定校验失败后的信息:

    @Data
    public class User {
        @NotNull(message = "用户id不能为空")
        private Long id;
    
        @NotNull(message = "用户账号不能为空")
        @Size(min = 6, max = 11, message = "账号长度必须是6-11个字符")
        private String account;
    
        @NotNull(message = "用户密码不能为空")
        @Size(min = 6, max = 11, message = "密码长度必须是6-16个字符")
        private String password;
    
        @NotNull(message = "用户邮箱不能为空")
        @Email(message = "邮箱格式不正确")
        private String email;
    }
    校验规则和错误提示信息配置完毕后,接下来只需要在接口需要校验的参数上加上@Valid注解,并添加BindResult参数即可方便完成验证:
    @RestController
    @RequestMapping("user")
    public class UserController {
        @Autowired
        private UserService userService;
    
        @PostMapping("/addUser")
        public String addUser(@RequestBody @Valid User user, BindingResult bindingResult) {
            // 如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里
            for (ObjectError error : bindingResult.getAllErrors()) {
                return error.getDefaultMessage();
            }
            return userService.addUser(user);
        }
    }

    这样当请求数据传递到接口的时候Validator就自动完成校验了,校验的结果就会封装到BindingResult中去,如果有错误信息我们就直接返回给前端,业务逻辑代码也根本没有执行下去。

    此时,业务层里的校验代码就已经不需要了:

    public String addUser(User user) {
         // 直接编写业务逻辑
         return "success";
     }

    现在可以看一下参数校验效果。我们故意给这个接口传递一个不符合校验规则的参数,先传递一个错误数据给接口,故意将password这个字段不满足校验条件:

    {
        "account": "12345678",
        "email": "123@qq.com",
        "id": 0,
        "password": "123"
    }

    再来看一下接口的响应数据:

    这样是不是方便很多?不难看出使用Validator校验有如下几个好处:

    (1)简化代码,之前业务层那么一大段校验代码都被省略掉了。

    (2)使用方便,那么多校验规则可以轻而易举的实现,比如邮箱格式验证,之前自己手写正则表达式要写那么一长串,还容易出错,用Validator直接一个注解搞定。(还有更多校验规则注解,可以自行去了解哦)

    (3)减少耦合度,使用Validator能够让业务层只关注业务逻辑,从基本的参数校验逻辑中脱离出来。

    使用Validator+ BindingResult已经是非常方便实用的参数校验方式了,在实际开发中也有很多项目就是这么做的,不过这样还是不太方便,因为你每写一个接口都要添加一个BindingResult参数,然后再提取错误信息返回给前端。

    这样有点麻烦,并且重复代码很多(尽管可以将这个重复代码封装成方法)。我们能否去掉BindingResult这一步呢?当然是可以的!

    Validator + 自动抛出异常

    我们完全可以将BindingResult这一步给去掉:

    @PostMapping("/addUser")
    public String addUser(@RequestBody @Valid User user) {
        return userService.addUser(user);
    }

    去掉之后会发生什么事情呢?直接来试验一下,还是按照之前一样故意传递一个不符合校验规则的参数给接口。此时我们观察控制台可以发现接口已经引发MethodArgumentNotValidException异常了:

    其实这样就已经达到我们想要的效果了,参数校验不通过自然就不执行接下来的业务逻辑,去掉BindingResult后会自动引发异常,异常发生了自然而然就不会执行业务逻辑。也就是说,我们完全没必要添加相关BindingResult相关操作嘛。

    不过事情还没有完,异常是引发了,可我们并没有编写返回错误信息的代码呀,那参数校验失败了会响应什么数据给前端呢?

    我们来看一下刚才异常发生后接口响应的数据:

    没错,是直接将整个错误对象相关信息都响应给前端了!这样就很难受,不过解决这个问题也很简单,就是我们接下来要讲的全局异常处理!

    全局异常处理

    参数校验失败会自动引发异常,我们当然不可能再去手动捕捉异常进行处理,不然还不如用之前BindingResult方式呢。又不想手动捕捉这个异常,又要对这个异常进行处理,那正好使用SpringBoot全局异常处理来达到一劳永逸的效果!

    基本使用

    首先,我们需要新建一个类,在这个类上加上@ControllerAdvice或@RestControllerAdvice注解,这个类就配置成全局处理类了。(这个根据你的Controller层用的是@Controller还是@RestController来决定)

    然后在类中新建方法,在方法上加上@ExceptionHandler注解并指定你想处理的异常类型,接着在方法内编写对该异常的操作逻辑,就完成了对该异常的全局处理!

    我们现在就来演示一下对参数校验失败抛出的MethodArgumentNotValidException全局处理:

    @RestControllerAdvice
    public class ExceptionControllerAdvice {
    
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
            // 从异常对象中拿到ObjectError对象
            ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
            // 然后提取错误提示信息进行返回
            return objectError.getDefaultMessage();
        }
    
    }

    我们再来看下这次校验失败后的响应数据:

    没错,这次返回的就是我们制定的错误提示信息!我们通过全局异常处理优雅的实现了我们想要的功能!以后我们再想写接口参数校验,就只需要在入参的成员变量上加上Validator校验规则注解,然后在参数上加上@Valid注解即可完成校验,校验失败会自动返回错误提示信息,无需任何其他代码!更多的校验思路:SpringBoot实现通用的接口参数校验

    自定义异常

    全局处理当然不会只能处理一种异常,用途也不仅仅是对一个参数校验方式进行优化。在实际开发中,如何对异常处理其实是一个很麻烦的事情。传统处理异常一般有以下烦恼:

    • 是捕获异常(try…catch)还是抛出异常(throws)
    • 是在controller层做处理还是在service层处理又或是在dao层做处理
    • 处理异常的方式是啥也不做,还是返回特定数据,如果返回又返回什么数据
    • 不是所有异常我们都能预先进行捕捉,如果发生了没有捕捉到的异常该怎么办?

    以上这些问题都可以用全局异常处理来解决,全局异常处理也叫统一异常处理,全局和统一处理代表什么?代表规范!规范有了,很多问题就会迎刃而解!

    全局异常处理的基本使用方式大家都已经知道了,我们接下来更进一步的规范项目中的异常处理方式:自定义异常。

    在很多情况下,我们需要手动抛出异常,比如在业务层当有些条件并不符合业务逻辑,我这时候就可以手动抛出异常从而触发事务回滚。那手动抛出异常最简单的方式就是throw new RuntimeException("异常信息")了,不过使用自定义会更好一些:

    • 自定义异常可以携带更多的信息,不像这样只能携带一个字符串。
    • 项目开发中经常是很多人负责不同的模块,使用自定义异常可以统一了对外异常展示的方式。
    • 自定义异常语义更加清晰明了,一看就知道是项目中手动抛出的异常。

    我们现在就来开始写一个自定义异常:

    @Getter //只要getter方法,无需setter
    public class APIException extends RuntimeException {
        private int code;
        private String msg;
    
        public APIException() {
            this(1001, "接口错误");
        }
    
        public APIException(String msg) {
            this(1001, msg);
        }
    
        public APIException(int code, String msg) {
            super(msg);
            this.code = code;
            this.msg = msg;
        }
    }

    在刚才的全局异常处理类中记得添加对我们自定义异常的处理:

    @ExceptionHandler(APIException.class)
    public String APIExceptionHandler(APIException e) {
        return e.getMsg();
    }

    这样就对异常的处理就比较规范了,当然还可以添加对Exception的处理,这样无论发生什么异常我们都能屏蔽掉然后响应数据给前端,不过建议最后项目上线时这样做,能够屏蔽掉错误信息暴露给前端,在开发中为了方便调试还是不要这样做。

    现在全局异常处理和自定义异常已经弄好了,不知道大家有没有发现一个问题,就是当我们抛出自定义异常的时候全局异常处理只响应了异常中的错误信息msg给前端,并没有将错误代码code返回。这就要引申出我们接下来要讲的东西了:数据统一响应

    数据统一响应

    现在我们规范好了参数校验方式和异常处理方式,然而还没有规范响应数据!比如我要获取一个分页信息数据,获取成功了呢自然就返回的数据列表,获取失败了后台就会响应异常信息,即一个字符串,就是说前端开发者压根就不知道后端响应过来的数据会是啥样的!所以,统一响应数据是前后端规范中必须要做的!

    自定义统一响应体

    统一数据响应第一步肯定要做的就是我们自己自定义一个响应体类,无论后台是运行正常还是发生异常,响应给前端的数据格式是不变的!那么如何定义响应体呢?关于异常的设计:如何更优雅的设计异常

    可以参考我们自定义异常类,也来一个响应信息代码code和响应信息说明msg:

    @Getter
    public class ResultVO<T> {
        /**
         * 状态码,比如1000代表响应成功
         */
        private int code;
        /**
         * 响应信息,用来说明响应情况
         */
        private String msg;
        /**
         * 响应的具体数据
         */
        private T data;
    
        public ResultVO(T data) {
            this(1000, "success", data);
        }
    
        public ResultVO(int code, String msg, T data) {
            this.code = code;
            this.msg = msg;
            this.data = data;
        }
    }

    然后我们修改一下全局异常处理那的返回值:

    @ExceptionHandler(APIException.class)
    public ResultVO<String> APIExceptionHandler(APIException e) {
        // 注意哦,这里返回类型是自定义响应体
        return new ResultVO<>(e.getCode(), "响应失败", e.getMsg());
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        // 注意哦,这里返回类型是自定义响应体
        return new ResultVO<>(1001, "参数校验失败", objectError.getDefaultMessage());
    }

    我们再来看一下此时如果发生异常了会响应什么数据给前端:

    OK,这个异常信息响应就非常好了,状态码和响应说明还有错误提示数据都返给了前端,并且是所有异常都会返回相同的格式!异常这里搞定了,别忘了我们到接口那也要修改返回类型,我们新增一个接口好来看看效果:

    @GetMapping("/getUser")
    public ResultVO<User> getUser() {
        User user = new User();
        user.setId(1L);
        user.setAccount("12345678");
        user.setPassword("12345678");
        user.setEmail("123@qq.com");
    
        return new ResultVO<>(user);
    }

    看一下如果响应正确返回的是什么效果:

    这样无论是正确响应还是发生异常,响应数据的格式都是统一的,十分规范!

    数据格式是规范了,不过响应码code和响应信息msg还没有规范呀!大家发现没有,无论是正确响应,还是异常响应,响应码和响应信息是想怎么设置就怎么设置,要是10个开发人员对同一个类型的响应写10个不同的响应码,那这个统一响应体的格式规范就毫无意义!所以,必须要将响应码和响应信息给规范起来。

    响应码枚举

    要规范响应体中的响应码和响应信息用枚举简直再恰当不过了,我们现在就来创建一个响应码枚举类:

    @Getter
    public enum ResultCode {
    
        SUCCESS(1000, "操作成功"),
    
        FAILED(1001, "响应失败"),
    
        VALIDATE_FAILED(1002, "参数校验失败"),
    
        ERROR(5000, "未知错误");
    
        private int code;
        private String msg;
    
        ResultCode(int code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    
    }

    然后修改响应体的构造方法,让其只准接受响应码枚举来设置响应码和响应信息:

    public ResultVO(T data) {
        this(ResultCode.SUCCESS, data);
    }
    
    public ResultVO(ResultCode resultCode, T data) {
        this.code = resultCode.getCode();
        this.msg = resultCode.getMsg();
        this.data = data;
    }

    然后同时修改全局异常处理的响应码设置方式:

    @ExceptionHandler(APIException.class)
    public ResultVO<String> APIExceptionHandler(APIException e) {
        // 注意哦,这里传递的响应码枚举
        return new ResultVO<>(ResultCode.FAILED, e.getMsg());
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        // 注意哦,这里传递的响应码枚举
        return new ResultVO<>(ResultCode.VALIDATE_FAILED, objectError.getDefaultMessage());
    }

    这样响应码和响应信息只能是枚举规定的那几个,就真正做到了响应数据格式、响应码和响应信息规范化、统一化!这些可以参考:Java项目构建基础:统一结果,统一异常,统一日志

    全局处理响应数据

    接口返回统一响应体 + 异常也返回统一响应体,其实这样已经很好了,但还是有可以优化的地方。要知道一个项目下来定义的接口搞个几百个太正常不过了,要是每一个接口返回数据时都要用响应体来包装一下好像有点麻烦,有没有办法省去这个包装过程呢?当然是有滴,还是要用到全局处理。

    首先,先创建一个类加上注解使其成为全局处理类。然后继承ResponseBodyAdvice接口重写其中的方法,即可对我们的controller进行增强操作,具体看代码和注释:

    @RestControllerAdvice(basePackages = {"com.rudecrab.demo.controller"}) // 注意哦,这里要加上需要扫描的包
    public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
        @Override
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
            // 如果接口返回的类型本身就是ResultVO那就没有必要进行额外的操作,返回false
            return !returnType.getGenericParameterType().equals(ResultVO.class);
        }
    
        @Override
        public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {
            // String类型不能直接包装,所以要进行些特别的处理
            if (returnType.getGenericParameterType().equals(String.class)) {
                ObjectMapper objectMapper = new ObjectMapper();
                try {
                    // 将数据包装在ResultVO里后,再转换为json字符串响应给前端
                    return objectMapper.writeValueAsString(new ResultVO<>(data));
                } catch (JsonProcessingException e) {
                    throw new APIException("返回String类型错误");
                }
            }
            // 将原本的数据包装在ResultVO里
            return new ResultVO<>(data);
        }
    }

    重写的这两个方法是用来在controller将数据进行返回前进行增强操作,supports方法要返回为true才会执行beforeBodyWrite方法,所以如果有些情况不需要进行增强操作可以在supports方法里进行判断。对返回数据进行真正的操作还是在beforeBodyWrite方法中,我们可以直接在该方法里包装数据,这样就不需要每个接口都进行数据包装了,省去了很多麻烦。

    我们可以现在去掉接口的数据包装来看下效果:

    @GetMapping("/getUser")
    public User getUser() {
        User user = new User();
        user.setId(1L);
        user.setAccount("12345678");
        user.setPassword("12345678");
        user.setEmail("123@qq.com");
        // 注意哦,这里是直接返回的User类型,并没有用ResultVO进行包装
        return user;
    }

    然后我们来看下响应数据:

    成功对数据进行了包装!

    注意:beforeBodyWrite方法里包装数据无法对String类型的数据直接进行强转,所以要进行特殊处理,这里不讲过多的细节,有兴趣可以自行深入了解。

    总结

    自此整个后端接口基本体系就构建完毕了

    • 通过Validator + 自动抛出异常来完成了方便的参数校验
    • 通过全局异常处理 + 自定义异常完成了异常操作的规范
    • 通过数据统一响应完成了响应数据的规范
    • 多个方面组装非常优雅的完成了后端接口的协调,让开发人员有更多的经历注重业务逻辑代码,轻松构建后端接口

    本文作者RudeCrab ,原文转载请链接RudeCrab授权

    点击关注,第一时间了解华为云新鲜技术~

    展开全文
  • 如何编写后端接口

    万次阅读 2019-12-05 15:47:37
    如何编写后端接口 一、接口的设计与规划 好的程序,一定有一套合理的后端接口。在设计后端接口的时候,要考虑清楚各个方面的需求,最好能有一个操作流程作为指导。 二、使用eclipse创建一个能够连接数据库的...

    如何编写后端接口

    一、接口的设计与规划

    好的程序,一定有一套合理的后端接口。在设计后端接口的时候,要考虑清楚各个方面的需求,最好能有一个操作流程作为指导。

    二、使用eclipse创建一个能够连接数据库的spring boot项目

    插件安装
    Help
    Eclipse Marketplace...
    Spring Tool Suite STS for Eclipse
    安装
    完成
    file
    new
    spring starter project
    填写名称
    选则依赖
    MySQL Driver
    Spring Data JPA
    Spring Web
    完成

    打开eclipse,若没有插件,则安装。file->new->spring starter project->填写名称等基本信息->选则依赖(MySQL Driver,Spring Data Jpa,Spring Web)->完成
    等程序运行完成(看右下角),就创建好了!
    若是pom.xml文件第一行报错:Unknown,则将Spring Boot Version改小,可以改成2.1.4.RELEASE。

    三、编写数据库连接,日志输出

    将resource文件夹下的application.properties改成application.yml,并加入logback-spring.xml文件。
    application.yml

    server:
      port: 8080
    spring:
      http:
        encoding:
          charset: utf8
          enabled: true
          force: true
      jmx:
        enabled: false
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/zuoshi?serverTimezone=GMT%2B8&characterEncoding=UTF-8
        username: 
        password: 
      jpa:
        database: MYSQL
        properties:
          hibernate:
            dialect: org.hibernate.dialect.MySQL5InnoDBDialect 
            hbm2ddl: 
              auto: update
        hibernate:
          ddl-auto: update
        show-sql: true
    ##配置日志
    logging:
        # 日志配置文件
        config: classpath:logback-spring.xml
        # 日志path路径配置
        path: ..\poem\logger
    

    logback-spring.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
        <!-- 配置日志按天生成文件 S -->
        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <Prudent>true</Prudent>
            <rollingPolicy  class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <FileNamePattern>${LOG_PATH}/info-%d{yyyy-MM-dd}.log </FileNamePattern>
                <MaxHistory>60</MaxHistory>
            </rollingPolicy>
            <layout class="ch.qos.logback.classic.PatternLayout">
                <Pattern> %d{yyyy-MM-dd HH:mm:ss} -%msg%n </Pattern>
            </layout>
        </appender>
        <!-- 配置日志按天生成文件 E -->
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>%date %-5level [%thread] %logger{100}[%line] - %msg%n</pattern>
            </encoder>
        </appender>
        <logger name="com.gome" level="INFO"/>
        <logger name="org.apache" level="WARN"/>
        <logger name="com.alibaba" level="INFO"/>
        <logger name="com.alibaba.dubbo" level="INFO"/>
        <logger name="org.springframework.web" level="INFO"/>
        <logger name="org.springframework.test" level="INFO"/>
        <logger name="org.springframework.boot" level="INFO"/>
        <logger name="org.springframework" level="INFO"/>
        <logger name="jdbc" level="WARN"/>
        <logger name="jdbc.sqltiming" level="DEBUG"/>
        <logger name="org.mybatis" level="WARN"/>
        <logger name="org.spring.jdbc" level="WARN"/>
        <!--
         其中appender的配置表示打印到控制台(稍后详细讲解appender );
         <root level="INFO">将root的打印级别设置为“INFO”,指定了名字为“STDOUT”的appender。
        -->
        <root level="INFO">
            <appender-ref ref="FILE"/>
            <appender-ref ref="STDOUT"/>
        </root>
    </configuration>

    四、编写entity(关联数据库的类)

    每个entity对应数据库里的一张表,注意在类头上添加注解:@Entity,@Table(name=“entity”),每个属性对应一个字段,主键上添加注解:@Id,@GeneratedValue(strategy = GenerationType.IDENTITY),文本类型属性添加注解:@Column(name = “user_pwd”, nullable = false, length = 20),boolean类型属性,添加注解:@Column(name = “is_delete”, nullable = false),时间类型属性添加注解:@Column(name = “create_time”,nullable = false)@Temporal(TemporalType.TIMESTAMP)。

    五、编写repository(数据库查询接口)

    repository是一个很好用的,查询数据库的接口。注意在类头上写上注解:@Repository,@Transactional。只要按照命名规范去写,这个接口就不需要实现,可以直接调用。有三个函数特别管用(注意该类的继承关系):

    @Repository
    @Transactional
    public interface ArticleRepository extends JpaRepository<Article, Integer>{
     Optional<Article> findById(Integer id);//按照主键查找
     void deleteById(Integer id);//按照主键删除
     @SuppressWarnings("unchecked")
     Article saveAndFlush(Article article);//保存并更新
    }

    六、编写service(服务层的接口)

    服务层直接和数据层交互,可以把一些元操作变成接口,以便控制层调用。

    七、编写serviceimpl(服务层接口的实现)

    注意类头上的注解:@Service,既然是接口的实现,那么,需要implements接口,然后将接口里的函数引入。声明一个repository的实例,并且加上注解:@Autowired,加上如下的代码:

    import org.hibernate.Session;
    import org.hibernate.SessionFactory;
    import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
    import org.hibernate.cfg.Configuration;
    import org.hibernate.query.Query;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    public Session getSession() {
      Configuration cfg = new Configuration();
      cfg.configure("/hibernate.cfg.xml");
      StandardServiceRegistryBuilder builder = new StandardServiceRegistryBuilder();
      builder.applySettings(cfg.getProperties());
      SessionFactory factory = cfg.buildSessionFactory(builder.build());
      return factory.openSession();
     }

    接下来使用hql语法进行数据库的操作:

    Session session = getSession();
    String hql = "select count(*) from Article where titleId = :index";
    Query query = session.createQuery(hql).setParameter("index", bookId);
    Integer count = (Integer) query.uniqueResult();
       
    String hqltwo = "update Article set chapterId = chapterId + 1 where chapterId >= :first and chapterId < last and titleId = :bookId";
    session.createQuery(hqltwo).setParameter("first", chapterId).setParameter("last", count).setParameter("bookId", bookId);

    八、编写controller(暴露给用户使用的接口,实际是一个类)

    类头上写上注解和接口路径:@RestController,@RequestMapping("/rest/article"),每个方法上方,写上接口路径注解和返回JSON格式的注解:@RequestMapping("/throughArticle"),
    @ResponseBody,注意创建一个serviceimpl的实例,加上@Autowired注解;

    九、关于异常捕获

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    @ControllerAdvice
    @ResponseBody
    public class myException {
     //1登录失败2读取失败3创建失败4修改失败5加载失败6加入失败7删除失败8返回失败9请重新登录
     private String NullPointerExceptionStr="空指针异常";
        private String ArrayIndexOutOfBoundsStr="数组越界异常";
        private String ClassCastExceptionStr="类型转换异常";
        private int ERROR_CODE = 400;
        
        static Logger logger = LoggerFactory.getLogger(myException.class);
        //空指针异常
        @ExceptionHandler(NullPointerException.class)
        public ReturnData nullPointerExceptionHandler(NullPointerException ex) {
            return resultFormat(ERROR_CODE, new Exception(NullPointerExceptionStr));
        }
        //类型转换异常
        @ExceptionHandler(ClassCastException.class)
        public ReturnData classCastExceptionHandler(ClassCastException ex) {
            return resultFormat(ERROR_CODE,  new Exception(ClassCastExceptionStr));
        }
        //数组越界异常
        @ExceptionHandler(ArrayIndexOutOfBoundsException.class)
        public ReturnData ArrayIndexOutOfBoundsException(ArrayIndexOutOfBoundsException ex) {
            return resultFormat(ERROR_CODE, new Exception(ArrayIndexOutOfBoundsStr));
        }
        //其他错误
        @ExceptionHandler({Exception.class})
        public ReturnData exception(Exception ex) {
         ex.printStackTrace();
         logger.info(ex.getMessage());
            return resultFormat(ERROR_CODE, new Exception(ResponseCode.SYSTEMBUSY.getMsg()));
        }
        //自定义错误
        public static ReturnData toException(Integer code, String msg) {
         logger.info(msg);
         return ReturnData.build(code, msg);
        }
        private <T extends Throwable> ReturnData resultFormat(Integer code, T ex) {
            ex.printStackTrace();
            return ReturnData.build(code, ex.getMessage());
        }
    }

    其中,ReturnData 是自定义的数据存储输出类。

    十、关于跨域的问题

    新增加config路径,写过滤器:

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    
    public class MyInterceptor extends HandlerInterceptorAdapter{
        static Logger logger = LoggerFactory.getLogger(MyInterceptor.class);
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object arg2) throws Exception {         
      response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));        
      response.setHeader("Access-Control-Allow-Methods", "*");        
      response.setHeader("Access-Control-Allow-Credentials", "true");
      response.setHeader("Access-Control-Allow-Headers", "x-requested-with,Authorization,Content-Type");
      response.setHeader("Access-Control-Max-Age", "3600");
      System.err.println("------------------>:已完成跨域处理");  
      logger.info("已完成跨域处理!");
      return true;    
      }
    }
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class MyWebConfigurer implements WebMvcConfigurer{
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(new MyInterceptor())
      .addPathPatterns("/**");
     }
     @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**");
        }
    }

    在controller类头上加上跨域注解:@CrossOrigin。

    十一、数据的传递

    自定义的数据传递类,需要包含三个部分:
    一、执行结果状态;
    二、执行结果状态描述;
    三、执行结果(Object类型);

    十二、服务器上tomcat的设置

    1、如何使用域名访问网站?
    (1)config->server.xml更改:把8080端口改成80;
    (2)修改

    <Engine name="Catalina" defaultHost="localhost">
    

    <Engine name="Catalina" defaultHost="www.test.com">
    

    (3)找到host标签,修改为:

    <Host name="www.test.com"  appBase="webapps"
                unpackWARs="true" autoDeploy="true">
        <Context path="" docBase="xxxx"/><!--xxxx就是在webapps下面xxxx.war的名字-->
        <Alias>test.com</Alias><!--如果没带www域名也配置好映射了就加上这个-->
    

    (4)重启tomcat服务;
    2、一个tomcat部署多个项目
    (1)tomcat路径下,新建一个文件夹,以区别于webapps文件夹,用于存放第二个项目;
    (2)server.xml里,加入一个新的Service(可以拷贝后修改参数);
    (3)注意:两个项目不能使用同一个端口,项目存放的文件夹需要修改;

    十三、利用tomcat部署程序到服务器上

    spring boot项目打包成war包:
    (1)pom.xml里边配置:

    <packaging>war</packaging>;
    

    (2)pom.xml里边,撤销嵌入的tomcat:在web的dependency下边:

    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
    

    (3)添加servlet依赖:

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
    

    (4)添加SpringBootApplication启动类:

    /**
     * 修改启动类,继承 SpringBootServletInitializer 并重写 configure 方法
     */
    public class SpringBootStartApplication extends SpringBootServletInitializer {
        @Override
        protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
            // 注意这里要指向原先用main方法执行的Application启动类
            return builder.sources(Application.class);
        }
    }

    (5)右键工程:Run as->Maven build…->Goals里边填上:clean package->确定;
    (6)在target文件夹下能找到打包好的war包;

    十四、运行程序

    去掉war包的后缀(工程名.war),放到部署好的文件夹下边(一般为webapps),启动tomcat就好了。

    十五、几个eclipse的使用技巧

    1、自动提示的设置

    Window
    Preferences
    Java
    Editor
    Content Assist
    勾上Enable auto activation
    第二个加上26个字母
    完成

    26个字母不用区分大小写。
    2、自动注释的设置

    Window
    Preferences
    Java
    Code Style
    Code Templates
    完成

    通过Alt+Shift+J自动添加注释。
    3、@GeneratedValue注解
    strategy属性提供四种值:
    AUTO主键由程序控制, 是默认选项 ,不设置就是这个;
    IDENTITY 主键由数据库生成, 采用数据库自增长, Oracle不支持这种方式;
    SEQUENCE 通过数据库的序列产生主键, MYSQL 不支持;
    Table 提供特定的数据库产生主键, 该方式更有利于数据库的移植。
    4、打开数据库的几种方式
    ddl-auto:create:每次运行该程序,没有表格会新建表格,表内有数据会清空
    ddl-auto:create-drop:每次程序结束的时候会清空表
    ddl-auto:update:每次运行程序,没有表格会新建表格,表内有数据不会清空,只会更新
    ddl-auto:validate:运行程序会校验数据与数据库的字段类型是否相同,不同会报错
    5、@Controller与@RestController
    Controller可以返回网页,RestController返回的是数据。
    6、网页接口与后端接口的区别
    网页接口需要使用thymleaf模板,引入如下依赖:

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

    后端接口则直接使用POSTMAN访问测试。
    7、@ResponseBody
    让结果类按照JSON字符串的形式返回。

    十六、eclipse常见问题

    1、系统找不到menifest.mf:找到xmlns中间的空格,先回车,再复原,就好了。
    2、eclipse自动生成get和set:右键->source->Generate Getters and Setters…
    3、org.springframework包报错:
    pom.xml里边加入:

      <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-test</artifactId>
       <version>4.3.2.RELEASE</version>
      </dependency>
    

    4、Archive for required library: “xxxx” in project ‘*’ cannot be read or is not a valid ZIP file:删除路径下的整个文件夹,然后项目右键->Run as->Maven Install,最后update项目。
    5、spring boot数据库:关键字,如user,limit,delete等,不能作为表名,字段名或者出现在需要自定义的地方;

    展开全文
  • 前言我在上一篇博客中写了如何通过参数校验 + 统一响应码 + 统一异常处理来构建一个优雅后端接口体系:adfasdfasdfasdf链接。我们做到了:通过Validator + 自动抛出...
  • 前言:我在上一篇博客中写了如何通过参数校验 + 统一响应码 + 统一异常处理来构建一个优雅后端接口体系,我们可以做到:参数校验、异常操作规范数据响应规范后端接口的协调,这样看上去好像挺完美的,很多地方...
  • SpringBoot实现后端接口

    2020-06-08 09:38:16
    一个后端接口大致分为四个部分组成:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。如何构建这几个部分每个公司要求都不同,没有什么“一定是最好的”标准,但一个...
  • 后端API接口的错误信息返回规范

    千次阅读 2020-10-21 13:47:56
    在讨论接口返回的时候,后端的同事询问我们前端,错误信息的返回,前端有什么意见? 所以做了一些调研给到后端的同事做参考。 错误信息返回 在使用API时无可避免地会因为各种情况而导致接口返回错误的信息。比如指定...
  • 有的做 Java 的小伙伴会调侃自己是接口仔,不过老实说,接口仔也不是那么好当的,今天松哥就来和大家分享一篇前后端分离开发,后端接口设计规范的文章,希望能对各位小伙伴有所启发。另外再悄悄告诉大家,公众号后台...
  • 前言去年的某个时候就想写一篇关于接口的吐槽,当时后端提出了接口方案对于我来说调用起来非常难受,但又说不上为什么,没有论点论据所以也就作罢。最近因为写全栈的缘故,团队内部也遇到了一些关于接口设计的问题,...
  • 从没有规范到有规范,再从有规范到扩展规范
  • 这个问题我相信很多人都会遇到,那就是本来好好的代码,莫名其妙就崩溃了,一番查询发现,后端返回的json格式变了,心里顿时一万只草泥马在奔腾,然后怒气冲冲找到后端,去质问。这个时候博弈就开始了,遇到好说话的...
  • 一个后端接口大致分为四个部分:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、相应数据(response)。优秀的后端接口的关键点就是看是否规范。 1.所需要的依赖 SpringBoot配置项目,需要...
  • 一个后端接口大致分为四个部分组成:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。如何构建这几个部分每个公司要求都不同,没有什么“一定是最好的”标准,但一个...
  • 后端开发接口规范

    千次阅读 2019-05-23 07:33:07
    因此制定本接口规范规范前后端的开发标准。 参考如下接口文档格式: 接口名称:审核列表 接口描述:接口的使用场景 接口URL: {service}/rider/check/r/new/list 请求方式:get|post 请求参数(json对象)...
  • 一个后端接口大致分为四个部分组成:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。 本文主要演示如何构建起一个优秀的后端接口体系,体系构建好了自然就有了规范...
  • 如何设计和编写标准的后端接口

    千次阅读 2020-10-11 13:18:59
    如何设计和编写标准的后端接口
  • 后端编写前端接口规范

    千次阅读 2019-11-30 12:51:14
    | 字段 | 类型 | 描述 | | ----- | ------ | ------ | | eventTotal | string | 活动参与总人次 | | userTotal | string | 用户总数量 | | digitTotal | string | 数字资源总量 | | clickTotal | string | 各网站...
  • java后台接口统一返回数据类型

    千次阅读 2020-01-06 16:59:47
    为了规范后台开发接口的标准,以及便于前台统一处理接口返回数据,定义一个通用的返回类是必要的。 @ApiModel("统一的返回类型") public class ResultBean<T> { private static String SUCCESS_CODE="0";...
  • 一个后端接口大致分为四个部分组成:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。如何构建这几个部分每个公司要求都不同,没有什么“一定是最好的”标准,但一个...
  • Springboot如何优雅地实现后端接口所需要的依赖包参数校验Validator + BindResult进行校验==Validator + 自动抛出异常==自定义异常==数据统一响应==响应码枚举==全局处理响应数据==数据统一响应自定义注解绕过数据...
  • SpringBoot写出优雅的后端接口

    千次阅读 2020-04-09 11:04:46
    SpringBoot写出优雅的后端接口pom.xml业务层校验Validator + BindResult进行校验Validator + 自动抛出异常全局异常处理自定义异常数据统一响应-自定义统一响应体响应码枚举全局处理响应数据 pom.xml <parent>...
  • 作者:海角在眼前链接:https://www.cnblogs.com/lovesong/p/5533149.html今天一位前端开发人员扯起了后端接口的皮,那个兄弟对后端人员提供的接口很...
  • web开发后端数据返回格式

    千次阅读 2018-06-08 20:55:12
    //返回数据 private Object data ; public JsonDate( boolean ret) { this . ret = ret ; } //成功的时候 public static JsonDate success (Object object , String msg){ JsonDate ...
  • 如何设计一个“正确”的后端接口

    千次阅读 2020-04-08 19:55:13
    一个后端接口正常情况下会包含①接口地址url②接口的请求方式(get/post)③请求数据④相应数据 在此记录一下如何构建一个完整的后端接口的过程 无论一个简单还是复杂的接口,无论是对外开放的接口还是http接口,参数...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 24,737
精华内容 9,894
关键字:

后端接口返回数据类型规范