# 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. 原则
- 使用标准格式的JAVADOC
- 必须包含接口说明,参数说明,返回值说明。格式必须易读,请编写时注意调整注释的格式
- JAVADOC应如实反映接口的功能。如有必要,可以在接口说明中增加场景
- JAVADOC中,参数需要列明其名称、是否必填、允许值范围以及必要的说明,如参数间有关联,需要备注
- 如参数为枚举/对象,必须将其使用的属性、值完全说明
- 返回值应注明返回数据中的数据结构以及要有必要的参数说明
- 应使用@since标明接口的启用版本
- 应使用@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方式下,内容格式或某数据的类型错误
快速跳转
