# REST接口代码规范

北京致远互联软件股份有限公司 2017年03月

# 适用范围

本文适用致远V5产品线所有新增的REST接口,包括二次开发接口,已开放的除外。

本文主要用于有新增Rest接口诉求开发,本文涵盖了开发Rest过程中的全部规则。

本文技术适用于有Rest基础的开发,如果你还不知道什么叫Rest,请通过互联网视频学习,完成基础后再进行本文的阅读。

# REST开发规范

# 1. 原则

  • 所有的REST接口均基于JAX-RS规范进行开发, 应用代码里禁止出现对jersey的调用
  • 只允许使用GET和POST,严禁使用PUT和DELETE(某些安全软件将DELETE动作视为危险行为)
  • 已确定/已发布的接口禁止修改 请求方法/URL/参数名/返回值结构/内容 ,禁止删除 接口/参数/返回值中的属性

如有上述变更诉求,请新启用一个接口

允许新增参数,但该参数不能为必填,如必填需提供缺省值兼容旧的调用

# 2. 协议约定

  • 根据CRUD来设计接口,R(Retrieve)使用GET,CUD(create,update,delete)使用POST

  • 使用GET方法时, URL里不能包含动词

  • 使用POST方法时,使用create/add表示新增,update表示修改,remove表示删除(勿使用delete关键字,极易被安全软件拦截)

  • 仅需要传递id的CUD操作,使用body传递id

  • 正常情况下所有R(获取数据)操作都使用GET方法 ,查询内容参数使用QueryParams(仅允许在查询请求参数非常多的情况下,如参数数量大于5个,才可以使用post)

  • 缺省接受JSON格式的参数,输出JSON。

@Consumes("application/json")
@Produces("application/json")
  • 使用Jackson进行JSON处理,缺省将Date输出为长整型值。 URL带参数option.n_a_s=1时将数值输出为字符串(Number as String),以避免在Javascript中解析长整型的精度问题。

  • 对象和方法命名语义需与内部保持一致,不允许自行创造。比如不要把Member叫做Person。

# 3. 代码规范

  • 代码使用com.seeyon.ctp.rest.resources包名 ,放置在各自模块的工程下自行管理。例如 com.seeyon.ctp.rest.resources.MemberResource
  • 所有资源均需继承BaseResource
  • 资源必须以名词命名,并且同时提供单数和复数两种形式的资源,路径首字母小写
资源名称 路径 ClassName 备注
Member member MemberResource 单个人员
Members members MembersResource 人员列表
Affair affair AffairResource 单条事项
Affairs affairs AffairsResource 事项列表
  • 方法名称不允许改变,不允许重载
  • 使用POST进行remove操作
  • 文档必须完备,所有必填内容必须注明到javadoc中
  • 对外接口需要增加标注@RestInterfaceAnnotation

# 4. URL命名

根据RESTful的定义,我们知道,REST定义的URL描述的是资源

虽然我们没有使用PUT、DELETE,但是URL即资源的描述这个属性不会改变

需要特别注意:URL并不是动作的描述,因尽量避免使用增删改以外的动词。

  • URL传递unicode字符一定要encode
  • URL建议按照对象/功能/参数命名,例
  news/like
  news/replay/remove/{id} 或 news/replay/{id}/remove
  news/{id}/replays
  • get方法不需要再在URL使用get开始,如一个bbs模块的方法getConent,应命名为bbs/content/{id},而不是bbs/getContent/{id}
  • @Path注解中的值,不能以/开始
  • URL遵从Java代码规范, 不允许使用缩写或拼音 ,规避以下JavaScript关键字
delete、in、enum、let、function、typeof、debugger、console、prototype

# 4.1. URL例

正确

GET tasks
GET bbs/{id}/replys
POST affair/remove
   BODY affairId:{id}
POST task/update     特殊:为兼容老代码允许使用task/update/{id}
   BODY taskId:{id},title:{title},...

错误

GET plan/getPlans          应为 GET plans
POST meeting/getOrderDate  应为 GET meeting/orderDate/{roomId}
   BODY roomId:{id}

禁止使用缩写

POST uc/modifypwd 错误命名

POST uc/password 正确命名

原则上一段URL只有一个单词,如一段URL里需要多个单词则需要按单词分割路径,原则上勿用驼峰命名

POST coll/transRepalValid     错误命名,原则上勿用驼峰命名
POST coll/transStepBackValid  错误命名,原则上勿用驼峰命名

POST collaboration/valid/repeal    正确命名
POST collaboration/valid/rollback  正确命名

# 5. 代码注释规范

# 5.1. 原则

  1. 使用标准格式的JAVADOC
  2. 必须包含接口说明,参数说明,返回值说明。格式必须易读,请编写时注意调整注释的格式
  3. JAVADOC应如实反映接口的功能。如有必要,可以在接口说明中增加场景
  4. JAVADOC中,参数需要列明其名称、是否必填、允许值范围以及必要的说明,如参数间有关联,需要备注
  5. 如参数为枚举/对象,必须将其使用的属性、值完全说明
  6. 返回值应注明返回数据中的数据结构以及要有必要的参数说明
  7. 应使用@since标明接口的启用版本
  8. 应使用@date标明接口编写的时间

# 5.2. 注释示例

/**
 * 保存秀吧信息
 * URL show/showbar
 * @since 6.0sp1
 * @date 2016-12-01
 * @param params 秀吧的参数
 * 
<pre>

 *    类型    名称            必填    备注
 *    Long    showbarId        Y    主题Id
 *    Long    coverPicture        N    封面Id
 *    String    showbarName        Y    主题名称
 *    String    summary            N    主题简介
 *    String    address            Y    主题发布地址
 *    String    startDate        Y    主题开始时间
 *    String    endDate            Y    主题结束时间
 *    String    showbarAuthScope    Y    授权范围类型
 *				{
 *				All                全部
 *            	All_extend_externalStaff    全部(外部人员除外)
 *            	Part                部分授权
 *            	All_group            全集团
 *				}
 *    String    showbarAuth        Y    授权范围字符串
</pre>

 * @return 返回对象com.seeyon.apps.show.po.ShowbarInfo
 * 
<pre>

 *     成功 {success:true,data:showBarData}
 *                 showBarData    来自于对象com.seeyon.apps.show.po.ShowbarInfo
 *                         {
 *                         "accountId"    所属单位ID,
 *                         "commentNum"    评论总数,
 *                         "coverPicture"    封面图片ID(对应show_imgae表),
 *                         "createFrom"    创建自:PC、M1,
 *                         "createTime"    创建时间,
 *                         "createUserId"    秀吧创建人ID,
 *                         "extraMap"    额外信息,
 *                         "id"    秀吧id,
 *                         "imgNum"    照片总数,
 *                         "likeNum"    点赞次数,
 *                         "new"    是否为新建,
 *                         "orderNum"    序号,
 *                         "settopTime"    置顶时间(未置顶则为空),
 *                         "showbarName"    秀吧名称,
 *                         "status"    状态标识:0删除,1正常,2系统预制秀,
 *                         "viewNum"    浏览次数
 *                        }
 *    失败 {success:false,msg:errorMessage}
 *
</pre>

 * @throws BusinessException    出错信息
 */
@POST
@Path("showbar")
@Consumes({MediaType.APPLICATION_XML,MediaType.APPLICATION_JSON})
public Response saveShowbarInfo(Map<String,Object> params)throws BusinessException

# 6. Response返回值定义

# 6.1. 原则

平台统一定义格式,各应用只需要传入数据及消息信息

成功的请求

{
  code: 0,
  data: 返回数据,
  message: 'messges if exists'
}

失败的请求

{
  code: 1,
  message: 'error messges'
}

message定义

如果是要提示给最终用户的message,必须进行国际化

成功的请求

大家只需要关心data内的内容,如果有需要传递的消息,请设置msg参数(非必填,如传空,平台将不返回该字段)

失败的请求

message必须填写,且内容恰当

# 6.2. 返回值示例

必须return Response,而不是具体的POJO对象;不允许自己进行JSON的toString,直接调用success或者fail方法由框架进行JSON转换。

// 错误
public V3xOrgMember getMemberByLoginName(
        @QueryParam("loginName") String loginName) throws Exception {
    return getOrgManager().getMemberByLoginName(decode(loginName));
}

// 正确
public Response getMemberByLoginName(
        @QueryParam("loginName") String loginName) throws BusinessException {
	try{
		// 成功操作,返回对象数据
		return success(getOrgManager().getMemberByLoginName(decode(loginName)));
	}catch(Exception e){
		log.error("根据LoginName获取人员数据异常:"+loginName, e);
		// 失败操作,返回异常信息
		return fail("获取数据异常");
	}
}

# 7. Spring bean引用

因为所有的REST实现都不受Spring管理,所以对于Spring bean不能采取依赖注入方式,请按照下面的方式进行处理

public class MemberResource extends BaseResource {
    private OrgManager orgManager;
    public OrgManager getOrgManager() {
        if (orgManager == null) {
            orgManager = (OrgManager) AppContext.getBean("orgManager");
        }
        return orgManager;
    }

    @GET
    @Path("{id}")
    @Produces(MediaType.APPLICATION_JSON)
    @RestInterfaceAnnotation
    public Response get(@PathParam("id") long id) throws Exception {
        return ok(getOrgManager().getEntityById(getActualClass(), id));
    }
}

# 8. JSON输出过滤和扩展

# 8.1. JSON过滤

不输出实体的某些属性,比如

{
  "orgAccountId" : -7580270040522800906,
  "id" : -4133790465478605204,
  "name" : "自动1",
  "code" : "5",
  "createTime" : 1429064089000,
  "updateTime" : 1429064089000,
  "sortId" : 0,
  "isDeleted" : false,
  "enabled" : true,
  "status" : 1,
  "description" : "",
  "orgLevelId" : 8562916345585944089,
  "orgPostId" : -2379980414295520995,
  "orgDepartmentId" : -5380398112991065519,
  "password" : "123456" ,
  ......
}

不希望输出V3xOrgMember的password属性。

  • 侵入式修改,在需要过滤的域的get方法上加@JsonIgnore注解。
  public class V3xOrgMember
      @JsonIgnore
      public String getPassword() {
      }
  }
  • 非侵入式修改
  // 定义一个空的类,增加注解列出要过滤的class
  @JsonIgnoreProperties(value = { "v3xOrgPrincipal", "password" })
  public class MemberWriteFilter {
  }
  // 初始化时调用注册Mixin
  com.seeyon.ctp.rest.util.MapperFactory.getInstance().addMixInAnnotations(V3xOrgMember.class, MemberWriteFilter.class);

所有的V3xOrgMember转为JSON时将不输出过滤的属性列表

# 8.2. JSON扩展

需要在输出的Bean基础上增加属性时使用,比如输出V3xOrgMember时输出所在单位的名称orgAccountName

com.seeyon.ctp.rest.util.MapperFactory.getInstance().register(V3xOrgMember.class,new  BeanSerializerFactory.Builder() {   
    public Map addFields(Object bean) {
        Map<String,String> data = new HashMap<String,String>();
        V3xOrgMember member = (V3xOrgMember) bean;
        long orgAccountId = member.getOrgAccountId();
        data.put("orgAccountName",orgManager.getAccountById(orgAccountId).getName());
        data.put("orgDepartmentName",balabala);
        data.put("orgPostName",balabala);
        return data;
    }
});

# 9. 分页

统一使用pageSize和pageNo两个QueryParam控制分页

// CTP获取FlipInfo对象
getFlipInfo();
// V3X的迁移代码在方法实现首行进行设置
setPagination();

# 10. 国际化

编写Rest接口时无需特殊处理国际化内容,只是编写代码中如涉及到国际化提示语反馈需要使用CTP的标准国际化组件。

以下内容是:如有国际化远程调用需求用户,需要在调用前传递对应的语言,以确保获取准确的结果数据。

通过http header的Accept-Language控制国际化使用的语言

  Accept-Language:en_US

REST Client中提供方法设置语言

client.setLocale(Locale.SIMPLIFIED_CHINESE);

JSSDK中缺省以浏览器的语言进行国际化,取不到浏览器语言时使用zh_CN 如果需要强制指定,可以在最后一个参数中使用AcceptLanguage指定语言。

$s.Token.getToken('rest','123456','',{
    'AcceptLanguage':'zh_CN'
})

# 11. 当前用户

禁止通过前端传递member的id,比如取某某用户的待办。必须同时支持以下两种方式:

1.从Header和QueryParam中取ticket,通过ticket获取member的Id。

2.如果ticket为空,在Resource层取CurrentUser(禁止在Manager、Api、Dao层取)。 如果取不到CurrentUser,而且也没有传递ticket则抛出异常。

已经支持token绑定用户,绑定用户后任何地方的代码获取当前用户就不会是null了,有两种绑定方式:

1、获取Token时加?loginName=s1参数

POST http://127.0.0.1/seeyon/rest/token/?loginName=s1 HTTP/1.1
Accept: application/json
Host: 127.0.0.1
Content-Type: application/json
Content-Length: 39

{"userName":"rest","password":"123456"}

2、获取Token后单独调用 PUT /rest/token {"token":"xxx", "loginName":"xxx"} 进行绑定

# 12. 内容协商

对于实体返回,建议同时支持JSON和XML,在Resource的Class或方法加入如下声明

@GET
@Path("{userName}/{password}")
@Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})
public Response getToken(@PathParam("userName") String userName,
    @PathParam("password") String password,
    @QueryParam("loginName") String loginName,
    @QueryParam("userAgentFrom") String userAgentFrom) throws Exception {
    return ok(_getToken(userName, password,loginName,userAgentFrom));
}

ok方法会根据请求header的accept自动返回json或xml。

如果需要返回html和纯文本,可以新建一个方法,使用相同的Path

@GET
@Path("{userName}/{password}")
@Produces(MediaType.TEXT_PLAIN)
public Response getTokenString(@PathParam("userName") String userName,
    @PathParam("password") String password,
    @QueryParam("loginName") String loginName,
    @QueryParam("userAgentFrom") String userAgentFrom) throws Exception {
    UserToken token = _getToken(userName, password,loginName,userAgentFrom);
    return ok(token.getId());
}

例如请求:

GET http://127.0.0.1/seeyon/rest/token/rest/123456 HTTP/1.1
Accept: application/json
Host: 127.0.0.1

返回

{
  "bindingUser" : null,
  "id" : "a5ad648c-0a40-49b0-bedd-9fd7025313b5"
}

请求

GET http://127.0.0.1/seeyon/rest/token/rest/123456 HTTP/1.1
Accept: application/xml
Host: 127.0.0.1

返回

<UserToken><bindingUser/><id>55b69a17-b2be-4dfa-80ac-c3a211ec652d</id></UserToken>

请求

GET http://127.0.0.1/seeyon/rest/token/rest/123456 HTTP/1.1
Accept: text/plain
Host: 127.0.0.1

返回

d72640bd-48b0-46cd-87a1-ca9a449da185

# 代码示例

# 常见问题

获取token报错:java.lang.NoSuchMethodError: org.codehaus.stax2.XMLStreamWriter2.writeLong(J)V

此问题在个别客户环境发生,stax2-api.jar和wstx-asl.jar冲突导致。

原因:jackson-dataformat-xml.jar包的ToXmlGenerator类中使用了XMLStreamWriter2接口,但stax2-api.jar和wstx-asl.jar包都定义了XMLStreamWriter2接口。stax2-api.jar包中XMLStreamWriter2接口的实现类Stax2WriterAdapter中有writeLong方法。

解决方法:停止协同服务,将webapps\seeyon\WEB-INF\lib\wstx-asl.jar包剪切走备份,启动协同服务。

获取token返回值有时字符串,有时json格式

参考本文“内容协商”章节,需要在发送请求时在header中固定Accpet参数:

--application/json为json格式
--text/plain为字符串格式

没加token会报:被迫下线的错误

获取token的接口,rest账号密码不正确,会返回401无权限问题

rest用户未授权,会给出明确提示,需要在管理Rest帐号的地方配置权限

报500错误

可能是post方式下,内容格式或某数据的类型错误

编撰人:lichaoj、het、admin