代码拉取完成,页面将自动刷新
同步操作将从 LengLeng/easyopen 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>easyopen开发文档</title>
<link rel="stylesheet" href="./static/highlight/styles/vs.css">
<script src="./static/highlight/highlight.pack.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<link rel="stylesheet" type="text/css" href="./static/github2-rightpart.css" media="all">
<link rel="stylesheet" type="text/css" href="./static/github1-contents.css">
<link rel="stylesheet" href="./static/zTreeStyle.css" type="text/css">
<style>
.ztree li a.curSelectedNode {
padding-top: 0px;
background-color: #FFE6B0;
color: black;
height: 16px;
border: 1px #FFB951 solid;
opacity: 0.8;
}
.ztree{
overflow: auto;
height:100%;
min-height: 200px;
top: 0px;
}
.task-list{list-style-type: disc !important;}
</style>
</head>
<body style="">
<div>
<div style="width:30%;">
<ul id="tree" class="ztree" style="width: 260px; overflow: auto; position: fixed; z-index: 2147483647; border: 0px none; left: 0px; bottom: 0px;">
<!-- 目录内容在网页另存为之后将插入到此处 -->
</ul>
</div>
<div id="readme" style="width:70%;margin-left:25%;">
<article class="markdown-body">
<!-- ***********************************************************内容分割线****************************************************************** -->
<!-- 请把你的html正文部分粘贴到此处,在浏览器中打开之后将会自动生成目录。如果想要将目录保留并嵌入到此文档中,只需在浏览器中“另存为->网页,全部”即可 -->
<div class="ui container">
<div id="project-title">
<div class="title-wrap">
<div class="left">
<i class="icon eye"></i>
文档预览:
durcframework/easyopen
</div>
<div class="right">
Export by Gitee
</div>
</div>
</div>
<div class="ui container" id="wiki-preview-container">
<div id="wiki-preview">
<div class="ui segment">
<div id="page-detail" class="markdown-body">
<div class='title'>easyopen开发文档</div><div class='content'><h1>
<a id="easyopen介绍" class="anchor" href="#easyopen%E4%BB%8B%E7%BB%8D"></a>easyopen介绍</h1>
<p>一个简单易用的接口开放平台,平台封装了常用的参数校验、结果返回等功能,开发者只需实现业务代码即可。</p>
<p>easyopen的功能类似于<a href="http://open.taobao.com/docs/api.htm?spm=a219a.7629065.0.0.6cQDnQ&apiId=4">淘宝开放平台</a>,它的所有接口只提供一个url,通过参数来区分不同业务。这样做的好处是接口url管理方便了,平台管理者只需维护好接口参数即可。由于参数的数量是可知的,这样可以在很大程度上进行封装。封装完后平台开发者只需要写业务代码,其它功能可以通过配置来完成。</p>
<p>得益于Java的注解功能以及Spring容器对bean的管理,我们的开放接口平台就这样产生了。</p>
<h1>
<a id="结构图" class="anchor" href="#%E7%BB%93%E6%9E%84%E5%9B%BE"></a>结构图</h1>
<p><img src="https://gitee.com/uploads/images/2018/0117/095712_1f70de95_332975.png" alt="easyopen结构图" title="easyopen_arc.png"></p>
<ul class="task-list">
<li>服务器启动完毕时,从Spring容器中找到被@ApiService标记的业务类</li>
<li>循环业务类,找到被@Api标记的方法,并保存对应的参数,method,对象信息。</li>
<li>客户端请求过来时,根据name-version可定位到具体的业务类中的某个方法,然后invoke方法。</li>
<li>包装结果返回。</li>
</ul>
<h1>
<a id="功能特点" class="anchor" href="#%E5%8A%9F%E8%83%BD%E7%89%B9%E7%82%B9"></a>功能特点</h1>
<ul class="task-list">
<li>开箱即用,写完业务代码直接启动服务即可使用,无需其它配置。</li>
<li>参数自动校验,支持国际化参数校验(JSR-303)。</li>
<li>校验功能和结果返回功能实现各自独立,方便自定义实现或扩展。</li>
<li>采用注解来定义接口,维护简单方便。</li>
<li>支持i18n国际化消息返回。</li>
<li>自动生成文档页面,类似swagger。</li>
<li>采用数字签名进行参数验证,签名算法见:easyopen\签名算法.txt。</li>
<li>采用appKey-secret形式接入平台,即需要给接入方提供一个appKey和secret。</li>
</ul>
<h1>
<a id="快速开始" class="anchor" href="#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B"></a>快速开始</h1>
<p>eclipse下(idea原理一样)</p>
<ul class="task-list">
<li>下载或clone项目<a href="https://gitee.com/durcframework/easyopen.git">https://gitee.com/durcframework/easyopen.git</a> <a href="https://gitee.com/durcframework/easyopen/repository/archive/master.zip">下载zip</a>
</li>
<li>eclipse右键import... -> Exsiting Maven Projects。选择easyopen目录</li>
<li>导入到eclipse后会有三个工程,等待相关jar包下载。</li>
<li>全部jar下载完毕后,启动easyopen-server项目,由于是spring-boot项目,直接运行EasyopenSpringbootApplication.java即可</li>
<li>在easyopen-sdk中找到SdkTest.java测试用例,运行单元测试。</li>
</ul>
<h2>
<a id="编写业务类" class="anchor" href="#%E7%BC%96%E5%86%99%E4%B8%9A%E5%8A%A1%E7%B1%BB"></a>编写业务类</h2>
<ul class="task-list">
<li>新建一个java类名为HelloworldApi,并加上@ApiService注解</li>
</ul>
<p>加上@ApiService注解后这个类就具有了提供接口的能力。</p>
<pre lang="java" class="java"><code>@ApiService
public class HelloWorldApi {
}
</code></pre>
<ul class="task-list">
<li>在类中添加一个方法</li>
</ul>
<pre><code>@Api(name = "hello")
public String helloworld() {
return "hello world";
}
</code></pre>
<p>这个方法很简单,就返回了一个字符串,方法被@Api标记,表示对应的接口,name是接口名。</p>
<p>到此,一个业务方法就写完了,接下来是编写sdk并测试。</p>
<h2>
<a id="编写sdk并测试" class="anchor" href="#%E7%BC%96%E5%86%99sdk%E5%B9%B6%E6%B5%8B%E8%AF%95"></a>编写SDK并测试</h2>
<p>此过程在easyopen-sdk中进行。</p>
<ul class="task-list">
<li>新建Response响应类</li>
</ul>
<pre><code>public class HelloResp extends BaseResp<String> {
}
</code></pre>
<ul class="task-list">
<li>新建Request请求类</li>
</ul>
<pre><code>public class HelloReq extends BaseNoParamReq {
public HelloReq(String name) {
super(name);
}
@Override
public Class<?> buildRespClass() {
return HelloResp.class;
}
}
</code></pre>
<p>BaseResp的泛型参数指定返回体类型,这里指定String</p>
<ul class="task-list">
<li>编写单元测试</li>
</ul>
<pre><code>public class HelloTest extends TestCase {
String url = "http://localhost:8080/api";
String appKey = "test";
String secret = "123456";
// 创建一个实例即可
OpenClient client = new OpenClient(url, appKey, secret);
@Test
public void testGet() throws Exception {
HelloReq req = new HelloReq("hello"); // hello对应@Api中的name属性,即接口名称
HelloResp result = client.request(req); // 发送请求
if (result.isSuccess()) {
String resp = result.getBody();
System.out.println(resp); // 返回hello world
} else {
throw new RuntimeException(result.getMsg());
}
}
}
</code></pre>
<p>这样,一个完整的接口就写完了。</p>
<h1>
<a id="自定义服务器项目" class="anchor" href="#%E8%87%AA%E5%AE%9A%E4%B9%89%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%A1%B9%E7%9B%AE"></a>自定义服务器项目</h1>
<p>easyopen-server是已经搭建好的服务器项目,可拿来立即使用。为了加深对easyopen的了解,我们自己来搭建一个,步骤非常简单,这里就使用springmvc框架搭建,步骤如下:</p>
<h3>
<a id="创建项目" class="anchor" href="#%E5%88%9B%E5%BB%BA%E9%A1%B9%E7%9B%AE"></a>创建项目</h3>
<ul class="task-list">
<li>新建工程</li>
</ul>
<p>eclipse新建一个springmvc工程,工程名为myopen,建好后的工程结构如下:</p>
<p><img src="https://gitee.com/uploads/images/2018/0118/115037_af31ccae_332975.png" alt="1" title="1.png"></p>
<ul class="task-list">
<li>添加依赖</li>
</ul>
<p>打开pom.xml添加easyopen依赖</p>
<pre><code><dependency>
<groupId>net.oschina.durcframework</groupId>
<artifactId>easyopen</artifactId>
<version>1.2.0</version>
</dependency>
</code></pre>
<ul class="task-list">
<li>添加api入口</li>
</ul>
<p>新建一个IndexController并继承ApiController</p>
<pre lang="java" class="java"><code>@Controller
@RequestMapping("/api")
public class IndexController extends ApiController {
}
</code></pre>
<p>其中头部@RequestMapping("/api")注解用来定义接口的URL,如果项目带contextPath则url为:<a href="http://localhost:8080/myopen/api%EF%BC%8C%E5%A6%82%E6%9E%9C%E6%B2%A1%E6%9C%89contextPath%E5%88%99%E4%B8%BA%EF%BC%9Ahttp://localhost:8080/api">http://localhost:8080/myopen/api,如果没有contextPath则为:http://localhost:8080/api</a></p>
<ul class="task-list">
<li>配置秘钥</li>
</ul>
<p>因为接口要提供给客户端,需要为客户端分配一个appKey和secret。配置的地方也在IndexController内,直接重写initApiConfig(ApiConfig apiConfig)方法。完整的代码如下</p>
<pre><code>@Controller
@RequestMapping("/api")
public class IndexController extends ApiController {
@Override
protected void initApiConfig(ApiConfig apiConfig) {
Map<String, String> appSecretStore = new HashMap<String, String>();
appSecretStore.put("test", "123456");
/*
* 添加秘钥配置,map中存放秘钥信息,key对应appKey,value对应secret
* @param appSecretStore
*/
apiConfig.addAppSecret(appSecretStore);
}
}
</code></pre>
<p>到这里easyopen已经搭建完成了,接着就可以编写业务代码和SDK了。</p>
<h1>
<a id="错误处理" class="anchor" href="#%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86"></a>错误处理</h1>
<p>easyopen对错误处理已经封装好了,最简单的做法是向上throw即可,在最顶层的Controller会做统一处理。例如:</p>
<pre><code>if(StringUtils.isEmpty(param.getGoods_name())) {
throw new ApiException("goods_name不能为null");
}
</code></pre>
<p>或者</p>
<pre><code>if(StringUtils.isEmpty(param.getGoods_name())) {
throw new RuntimeException("goods_name不能为null");
}
</code></pre>
<p>为了保证编码风格的一致性,推荐统一使用ApiException</p>
<h2>
<a id="i18n国际化" class="anchor" href="#i18n%E5%9B%BD%E9%99%85%E5%8C%96"></a>i18n国际化</h2>
<p>easyopen支持国际化消息。通过Request对象中的getLocale()来决定具体返回那种语言,客户端通过设置Accept-Language头部来决定返回哪种语言,中文是zh,英文是en。</p>
<p>easyopen通过模块化来管理国际化消息,这样做的好处结构清晰,维护方便。下面就来讲解如何设置国际化消息。</p>
<p>假设我们要对商品模块进行设置,步骤如下:</p>
<ul class="task-list">
<li>在项目的resources下新建如下目录/i18n/isv</li>
<li>在isv目录下新建goods_error_zh_CN.properties属性文件</li>
</ul>
<p><img src="https://gitee.com/uploads/images/2018/0118/142511_feacd145_332975.png" alt="输入图片说明" title="2.png"></p>
<p>属性文件的文件名有规律, <strong>i18n/isv/goods_error</strong> 表示模块路径, <strong>_zh_CN.properties</strong> 表示中文错误消息。如果要使用英文错误,则新建一个goods_error_en.properties即可。</p>
<ul class="task-list">
<li>在goods_error_zh_CN.properties中配置错误信息</li>
</ul>
<pre><code># 商品名字不能为空
isv.goods_error_100=\u5546\u54C1\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
</code></pre>
<p>isv.goods_error_为固定前缀,100为错误码,这两个值后续会用到。</p>
<p>接下来是把属性文件加载到国际化容器当中。</p>
<ul class="task-list">
<li>添加国际化配置</li>
</ul>
<p>easyopen的所以配置操作都在ApiConfig类里面,配置工作可以在ApiController.initApiConfig(ApiConfig apiConfig)方法中进行,因此只要重写initApiConfig(ApiConfig apiConfig)方法就行了。</p>
<pre><code>@Controller
@RequestMapping(value = "/api/v1")
public class IndexController extends ApiController {
@Override
protected void initApiConfig(ApiConfig apiConfig) {
// 配置国际化消息
apiConfig.getIsvModules().add("i18n/isv/goods_error");// 模块路径
// 配置appKey秘钥
Map<String, String> appSecretStore = new HashMap<String, String>();
appSecretStore.put("test", "123456");
apiConfig.addAppSecret(appSecretStore);
}
}
</code></pre>
<p>添加一句apiConfig.getIsvModules().add("i18n/isv/goods_error");就行</p>
<ul class="task-list">
<li>新建一个interface用来定义错误</li>
</ul>
<pre><code>// 按模块来定义异常消息,团队开发可以分开进行
public interface GoodsErrors {
String isvModule = "isv.goods_error_"; // 前缀
// 100为前缀后面的错误码
// 这句话即可找到isv.goods_error_100错误
ErrorMeta NO_GOODS_NAME = new ErrorMeta(isvModule, "100", "商品名称不能为空.");
}
</code></pre>
<p>接下来就可以使用了</p>
<pre><code>if (StringUtils.isEmpty(param.getGoods_name())) {
throw GoodsErrors.NO_GOODS_NAME.getException();
}
</code></pre>
<h3>
<a id="国际化消息传参" class="anchor" href="#%E5%9B%BD%E9%99%85%E5%8C%96%E6%B6%88%E6%81%AF%E4%BC%A0%E5%8F%82"></a>国际化消息传参</h3>
<p>即代码中变量传入到properties文件中去,做法是采用{0},{1}占位符。0代表第一个参数,1表示第二个参数。</p>
<pre><code># 商品名称太短,不能小于{0}个字
isv.goods_error_101=\u5546\u54C1\u540D\u79F0\u592A\u77ED\uFF0C\u4E0D\u80FD\u5C0F\u4E8E{0}\u4E2A\u5B57
</code></pre>
<pre><code>if(param.getGoods_name().length() < 3) {
throw GoodsErrors.SHORT_GOODS_NAME.getException(3); // 这里的“3”会填充到{0}中
}
</code></pre>
<p>直接放进getException(Object... params)方法参数中,因为是可变参数,可随意放。</p>
<h1>
<a id="自定义结果返回" class="anchor" href="#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%93%E6%9E%9C%E8%BF%94%E5%9B%9E"></a>自定义结果返回</h1>
<p>easyopen默认的返回类是ApiResult,解析成json格式为:</p>
<pre><code>{
"code": "0",
"msg": "",
"data": {...}
}
</code></pre>
<p>我们也可以自定义返回结果,比如我们要返回这样的json:</p>
<pre><code>{
"errCode":"0",
"errMsg":"",
"body":{...}
}
</code></pre>
<ul class="task-list">
<li>首先,新建结果类,实现com.gitee.easyopen.Result接口:</li>
</ul>
<pre><code>import com.gitee.easyopen.Result;
public class MyResult implements Result {
private static final long serialVersionUID = -6618981510574135069L;
private String errCode;
private String errMsg;
private String body;
@Override
public void setCode(Object code) {
this.setErrCode(String.valueOf(code));
}
@Override
public void setMsg(String msg) {
this.setErrMsg(msg);
}
@Override
public void setData(Object data) {
this.setBody(String.valueOf(data));
}
public String getErrCode() {
return errCode;
}
public void setErrCode(String errCode) {
this.errCode = errCode;
}
public String getErrMsg() {
return errMsg;
}
public void setErrMsg(String errMsg) {
this.errMsg = errMsg;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
}
</code></pre>
<p>MyResult类可定义自己想要的字段名字,实现Result接口对应的方法即可</p>
<ul class="task-list">
<li>然后,新建一个结果生成器,实现ResultCreator接口:</li>
</ul>
<pre><code>import com.gitee.easyopen.Result;
import com.gitee.easyopen.ResultCreator;
public class MyResultCreator implements ResultCreator {
@Override
public Result createResult(Object returnObj) {
MyResult ret = new MyResult();
ret.setCode(0);
ret.setData(returnObj);
return ret;
}
@Override
public Result createErrorResult(Object code, String errorMsg, Object data) {
MyResult ret = new MyResult();
ret.setCode(code);
ret.setMsg(errorMsg);
ret.setData(data);
return ret;
}
}
</code></pre>
<p>ResultCreator接口定义两个方法,createResult是返回正确内容的方法,createErrorResult是返回错误时候的方法。
分别实现它们,用我们刚才新建的MyResult类。</p>
<ul class="task-list">
<li>最后,配置结果生成器,在initApiConfig方法中配置:</li>
</ul>
<pre><code>@Controller
@RequestMapping("/project/api")
public class IndexController extends ApiController {
@Override
protected void initApiConfig(ApiConfig apiConfig) {
// 配置结果生成器
apiConfig.setResultCreator(new MyResultCreator());
省略其它代码...
}
}
</code></pre>
<p>调用apiConfig.setResultCreator(new MyResultCreator());进行配置</p>
<h1>
<a id="自定义序列化" class="anchor" href="#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%BA%8F%E5%88%97%E5%8C%96"></a>自定义序列化</h1>
<p>easyopen序列化使用fastjson处理json,xstream处理xml。现在我们来自定义实现一个json处理:</p>
<ul class="task-list">
<li>新建一个类JsonFormatter,实现ResultSerializer接口</li>
</ul>
<pre><code>public class JsonFormatter implements ResultSerializer {
@Override
public String serialize(Object obj) {
Gson gson = new Gson();
return gson.toJson(obj);
}
}
</code></pre>
<p>这里使用了Gson。</p>
<ul class="task-list">
<li>在apiConfig中配置</li>
</ul>
<pre><code>@Controller
@RequestMapping("/project/api")
public class IndexController extends ApiController {
@Override
protected void initApiConfig(ApiConfig apiConfig) {
// 自定义json序列化
apiConfig.setJsonResultSerializer(new JsonFormatter());
省略其它代码...
}
}
</code></pre>
<h1>
<a id="定义接口版本号" class="anchor" href="#%E5%AE%9A%E4%B9%89%E6%8E%A5%E5%8F%A3%E7%89%88%E6%9C%AC%E5%8F%B7"></a>定义接口版本号</h1>
<p>设置@Api注解的version属性,不指定默认为""。@Api(name = "goods.get" , version = "2.0")</p>
<h1>
<a id="在业务类中获取request对象" class="anchor" href="#%E5%9C%A8%E4%B8%9A%E5%8A%A1%E7%B1%BB%E4%B8%AD%E8%8E%B7%E5%8F%96request%E5%AF%B9%E8%B1%A1"></a>在业务类中获取Request对象</h1>
<pre><code>HttpServletRequest request = ApiContext.getRequest();
</code></pre>
<h1>
<a id="业务参数校验" class="anchor" href="#%E4%B8%9A%E5%8A%A1%E5%8F%82%E6%95%B0%E6%A0%A1%E9%AA%8C"></a>业务参数校验</h1>
<p>业务参数校验采用JSR-303方式,关于JSR-303介绍可以参考这篇博文:<a href="https://www.ibm.com/developerworks/cn/java/j-lo-jsr303/">JSR 303 - Bean Validation 介绍及最佳实践</a></p>
<p>在参数中使用注解即可,框架会自动进行验证。如下面一个添加商品接口,它的参数是GoodsParam</p>
<pre><code>@Api(name = "goods.add")
public void addGoods(GoodsParam param) {
...
}
</code></pre>
<p>在GoodsParam中添加JSR-303注解:</p>
<pre><code>public class GoodsParam {
@NotEmpty(message = "商品名称不能为空")
private String goods_name;
// 省略get,set
}
</code></pre>
<p>如果不传商品名称则返回</p>
<pre><code>{"code":"100","msg":"商品名称不能为空"}
</code></pre>
<h2>
<a id="参数校验国际化" class="anchor" href="#%E5%8F%82%E6%95%B0%E6%A0%A1%E9%AA%8C%E5%9B%BD%E9%99%85%E5%8C%96"></a>参数校验国际化</h2>
<p>国际化的配置方式如下:</p>
<pre><code>@NotEmpty(message = "{goods.name.notNull}")
private String goods_name;
</code></pre>
<p>国际化资源文件goods_error_en.properties中添加:</p>
<pre><code>goods.name.notNull=The goods_name can not be null
</code></pre>
<p>goods_error_zh_CN.properties中添加:</p>
<pre><code>goods.name.notNull=\u5546\u54C1\u540D\u79F0\u4E0D\u80FD\u4E3Anull
</code></pre>
<h2>
<a id="参数校验国际化传参" class="anchor" href="#%E5%8F%82%E6%95%B0%E6%A0%A1%E9%AA%8C%E5%9B%BD%E9%99%85%E5%8C%96%E4%BC%A0%E5%8F%82"></a>参数校验国际化传参</h2>
<p>下面校验商品名称的长度,要求大于等于3且小于等于20。数字3和20要填充到国际化资源中去。</p>
<pre><code>// 传参的格式:{xxx}=value1,value2...
@Length(min = 3, max = 20, message = "{goods.name.length}=3,20")
private String goods_name;
</code></pre>
<p>goods_error_en.properties:</p>
<pre><code>goods.name.length=The goods_name length must >= {0} and <= {1}
</code></pre>
<p>goods_error_zh_CN.properties中添加:</p>
<pre><code>goods.name.length=\u5546\u54C1\u540D\u79F0\u957F\u5EA6\u5FC5\u987B\u5927\u4E8E\u7B49\u4E8E{0}\u4E14\u5C0F\u4E8E\u7B49\u4E8E{1}
</code></pre>
<p>这样value1,value2会分别填充到{0},{1}中</p>
<h1>
<a id="接口交互详解" class="anchor" href="#%E6%8E%A5%E5%8F%A3%E4%BA%A4%E4%BA%92%E8%AF%A6%E8%A7%A3"></a>接口交互详解</h1>
<h2>
<a id="请求参数" class="anchor" href="#%E8%AF%B7%E6%B1%82%E5%8F%82%E6%95%B0"></a>请求参数</h2>
<p>easyopen定义了7个固定的参数,用json接收</p>
<pre><code>{
"name":"goods.get",
"version":"2.0",
"app_key":"test",
"data":"%7B%22goods_name%22%3A%22iphone6%22%7D",
"format":"json",
"timestamp":"2018-01-16 17:02:02",
"sign":"4CB446DF67DB3637500D4715298CE4A3"
}
</code></pre>
<ul class="task-list">
<li>name:接口名称</li>
<li>version:接口版本号</li>
<li>app_key:分配给客户端的app_key</li>
<li>data:业务参数,json格式并且urlencode</li>
<li>format:返回格式,json,xml两种</li>
<li>timestamp:时间戳,yyyy-MM-dd HH:mm:ss</li>
<li>sign:签名串</li>
</ul>
<p>其中sign需要使用双方约定的签名算法来生成。</p>
<h2>
<a id="请求方式" class="anchor" href="#%E8%AF%B7%E6%B1%82%E6%96%B9%E5%BC%8F"></a>请求方式</h2>
<p>请求数据放在body体中,采用json格式。这里给出一个POST工具类:</p>
<pre><code>public class PostUtil {
private static final String UTF8 = "UTF-8";
private static final String CONTENT_TYPE_JSON = "application/json";
private static final String ACCEPT_LANGUAGE = "Accept-Language";
/**
* POST请求
* @param url
* @param params
* @param lang 语言zh,en
* @return
* @throws Exception
*/
public static String post(String url, JSONObject params, String lang) throws Exception {
String encode = UTF8;
// 使用 POST 方式提交数据
PostMethod method = new PostMethod(url);
try {
String requestBody = params.toJSONString();
// 请求数据放在body体中,采用json格式
method.setRequestEntity(new StringRequestEntity(requestBody, CONTENT_TYPE_JSON, encode));
// 设置请求语言
method.setRequestHeader(ACCEPT_LANGUAGE, lang);
HttpClient client = new HttpClient();
int state = client.executeMethod(method); // 返回的状态
if (state != HttpStatus.SC_OK) {
throw new Exception("HttpStatus is " + state);
}
String response = method.getResponseBodyAsString();
return response; // response就是最后得到的结果
} catch (Exception e) {
throw e;
} finally {
method.releaseConnection();
}
}
}
</code></pre>
<ul class="task-list">
<li>请求操作代码:</li>
</ul>
<pre><code>@Test
public void testGet() throws Exception {
Map<String, String> param = new HashMap<String, String>();
Goods goods = new Goods();
String data = JSON.toJSONString(goods);
data = URLEncoder.encode(data, "UTF-8");
param.put("name", "hello");
param.put("app_key", appId);
param.put("data", data);
param.put("timestamp", getTime());
param.put("version", "");
param.put("format", "json");
String sign = ApiUtil.buildSign(param, secret);
param.put("sign", sign);
System.out.println("请求内容:" + JSON.toJSONString(param));
String resp = PostUtil.post(url, param,"zh");
System.out.println(resp);
}
public String getTime() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
</code></pre>
<h2>
<a id="签名算法" class="anchor" href="#%E7%AD%BE%E5%90%8D%E7%AE%97%E6%B3%95"></a>签名算法</h2>
<p>签名算法描述如下:</p>
<ol class="task-list">
<li>将请求参数按参数名升序排序;</li>
<li>按请求参数名及参数值相互连接组成一个字符串:<paramName1><paramValue1><paramName2><paramValue2>...;</li>
<li>将应用密钥分别添加到以上请求参数串的头部和尾部:<secret><请求参数字符串><secret>;</li>
<li>对该字符串进行MD5运算,得到一个二进制数组;</li>
<li>将该二进制数组转换为十六进制的字符串(全部大写),该字符串即是这些请求参数对应的签名;</li>
<li>该签名值使用sign参数一起和其它请求参数一起发送给服务开放平台。</li>
</ol>
<p>伪代码:</p>
<pre><code>Map<String,Object> paramsMap = new ...; // 参数
Set<String> keySet = paramsMap.keySet();
List<String> paramNames = new ArrayList<String>(keySet);
// 1.
Collections.sort(paramNames);
StringBuilder paramNameValue = new StringBuilder();
// 2.
for (String paramName : paramNames) {
paramNameValue.append(paramName).append(paramsMap.get(paramName));
}
// 3.
String source = secret + paramNameValue.toString() + secret;
// 4.& 5.
String sign = md5(source);
// 6.
paramsMap.put("sign",sign);
</code></pre>
<h2>
<a id="服务端验证" class="anchor" href="#%E6%9C%8D%E5%8A%A1%E7%AB%AF%E9%AA%8C%E8%AF%81"></a>服务端验证</h2>
<p>服务端拿到请求数据后会sign字段进行验证,验证步骤如下:</p>
<ol class="task-list">
<li>根据客户端传过来的app_key拿到服务端保存的secret</li>
<li>拿到secret后通过签名算法生成服务端的serverSign</li>
<li>比较客户端sign和serverSign是否相等,如果相等则证明客户端传来的数据是合法数据,否则不通过返回错误信息。</li>
<li>处理业务,返回结果</li>
</ol>
<h1>
<a id="忽略验证" class="anchor" href="#%E5%BF%BD%E7%95%A5%E9%AA%8C%E8%AF%81"></a>忽略验证</h1>
<p>如果某个接口不需要进行验证工作,可以在@Api注解上设置属性ignoreValidate=true(默认false)。这样调用接口时,不会进行验证操作。</p>
<p>同样的,在@ApiService注解里也有一个对应的ignoreValidate属性,设置为true的话,Service类下面所有的接口都忽略验证。</p>
<h2>
<a id="忽略所有接口验证" class="anchor" href="#%E5%BF%BD%E7%95%A5%E6%89%80%E6%9C%89%E6%8E%A5%E5%8F%A3%E9%AA%8C%E8%AF%81"></a>忽略所有接口验证</h2>
<p>设置ApiConfig.setIgnoreValidate(true),所有接口的签名认证操作都将忽略(业务参数校验除外)。</p>
<h1>
<a id="生成文档页面" class="anchor" href="#%E7%94%9F%E6%88%90%E6%96%87%E6%A1%A3%E9%A1%B5%E9%9D%A2"></a>生成文档页面</h1>
<p>easyopen提供一个简单的api文档查看页面,类似于swagger,基于注解功能来生成文档页面。生成的文档页面可以查看参数、结果说明,也可以进行模拟请求。对于前后端分离的项目来说会很有帮助。文档界面如下图所示:</p>
<p><img src="https://gitee.com/uploads/images/2018/0203/145842_55f2794e_332975.png" alt="输入图片说明" title="3.png"></p>
<p>左边的树形菜单对应文档名称,点击树可前往查看对应的接口说明。点击请求按钮可以发起请求进行测试。可修改业务参数中的值进行测试。</p>
<p>下面来讲解文档注解的使用方法。</p>
<p>文档页面默认是关闭的,需要在ApiConfig中设置</p>
<pre><code>apiConfig.setShowDoc(true);// 为true开启文档页面。
</code></pre>
<p>接下来对获取商品接口设置文档信息:</p>
<pre><code>@Api(name = "goods.get")
public Goods getGoods(GoodsParam param) {
...
return goods;
}
</code></pre>
<h2>
<a id="设置文档注解" class="anchor" href="#%E8%AE%BE%E7%BD%AE%E6%96%87%E6%A1%A3%E6%B3%A8%E8%A7%A3"></a>设置文档注解</h2>
<p>在接口方法上添加一个@ApiDocMethod注解。</p>
<pre><code>@Api(name = "goods.get")
@ApiDocMethod(description="获取商品") // 文档注解,description为接口描述
public Goods getGoods(GoodsParam param) {
...
return goods;
}
</code></pre>
<h2>
<a id="设置参数注解" class="anchor" href="#%E8%AE%BE%E7%BD%AE%E5%8F%82%E6%95%B0%E6%B3%A8%E8%A7%A3"></a>设置参数注解</h2>
<p>进入GoodsParam类,使用@ApiDocField注解</p>
<pre><code>public class GoodsParam {
@ApiDocField(description = "商品名称", required = true, example = "iphoneX")
private String goods_name;
// 省略 get set
}
</code></pre>
<p>@ApiDocField注解用来定义字段信息,@ApiDocField注解定义如下</p>
<pre><code>/**
* 字段描述
*/
String description() default "";
/**
* 字段名
*/
String name() default "";
/**
* 数据类型,int string float
* @return
*/
DataType dataType() default DataType.STRING;
/**
* 是否必填
*/
boolean required() default false;
</code></pre>
<h2>
<a id="设置返回结果注解" class="anchor" href="#%E8%AE%BE%E7%BD%AE%E8%BF%94%E5%9B%9E%E7%BB%93%E6%9E%9C%E6%B3%A8%E8%A7%A3"></a>设置返回结果注解</h2>
<p>同样,在Goods类中设置@ApiDocField</p>
<pre><code>public class Goods {
@ApiDocField(description = "id")
private Long id;
@ApiDocField(description = "商品名称")
private String goods_name;
@ApiDocField(description = "价格", dataType = DataType.FLOAT)
private BigDecimal price;
// 省略 get set
}
</code></pre>
<p>到此已经设置完毕了,可以访问url进行预览</p>
<h2>
<a id="文档页面url" class="anchor" href="#%E6%96%87%E6%A1%A3%E9%A1%B5%E9%9D%A2url"></a>文档页面URL</h2>
<p>文档页面的url格式为:apiUrl + "/doc"。如:apiUrl为<a href="http://localhost:8080/api/v1%EF%BC%8C%E9%82%A3%E4%B9%88%E6%96%87%E6%A1%A3%E9%A1%B5%E9%9D%A2%E5%B0%B1%E6%98%AF%EF%BC%9Ahttp://localhost:8080/api/v1/doc">http://localhost:8080/api/v1,那么文档页面就是:http://localhost:8080/api/v1/doc</a></p>
<h2>
<a id="list返回" class="anchor" href="#list%E8%BF%94%E5%9B%9E"></a>List返回</h2>
<p>如果接口方法返回一个List,设置方式如下</p>
<pre><code> @Api(name = "goods.list", version = "2.0")
@ApiDocMethod(description="获取商品列表"
,results={@ApiDocField(description="商品列表",name="list", elementClass=Goods.class)}
)
public List<Goods> listGoods(GoodsParam param) {
}
</code></pre>
<p>elementClass对应List中的元素类型</p>
<h2>
<a id="第三方类返回" class="anchor" href="#%E7%AC%AC%E4%B8%89%E6%96%B9%E7%B1%BB%E8%BF%94%E5%9B%9E"></a>第三方类返回</h2>
<p>如果有个一个PageInfo类,是第三方jar中的,没办法对其修改,那要如何对它里面的属性编写对应文档呢。</p>
<p>PageInfo类内容如下:</p>
<pre><code>// 假设这是jar中的类,没法修改。但是要对其进行文档生成
public class PageInfo<T> {
private int pageIndex;
private int pageSize;
private long total;
// 省略 get set
}
</code></pre>
<p>我们可以显式的声明字段信息:</p>
<pre><code> @Api(name = "goods.pageinfo", version = "1.0")
@ApiDocMethod(description="获取商品列表"
,results={@ApiDocField(name="pageIndex",description="第几页",dataType=DataType.INT,example="1"),
@ApiDocField(name="pageSize",description="每页几条数据",dataType=DataType.INT,example="10"),
@ApiDocField(name="total",description="每页几条数据",dataType=DataType.LONG,example="100"),
@ApiDocField(name="rows",description="数据",dataType=DataType.ARRAY,elementClass=Goods.class),}
)
public PageInfo<Goods> pageinfo(GoodsParam param) {
}
</code></pre>
<h2>
<a id="文档模型复用" class="anchor" href="#%E6%96%87%E6%A1%A3%E6%A8%A1%E5%9E%8B%E5%A4%8D%E7%94%A8"></a>文档模型复用</h2>
<p>如果多个接口都返回PageInfo,需要复制黏贴大量的注解,改一个地方需要改多个接口,无法达到复用效果。我们可以新建一个GoodsPageVo继承PageInfo,然后把文档注解写在类的头部,这样可以达到复用效果。</p>
<pre><code>@ApiDocBean(fields = {
@ApiDocField(name="pageIndex",description="第几页",dataType=DataType.INT,example="1"),
@ApiDocField(name="pageSize",description="每页几条数据",dataType=DataType.INT,example="10"),
@ApiDocField(name="total",description="每页几条数据",dataType=DataType.LONG,example="100"),
@ApiDocField(name="rows",description="商品列表",dataType=DataType.ARRAY,elementClass=Goods.class),
})
public class GoodsPageVo extends PageInfo<Goods> {
}
</code></pre>
<pre><code> @Api(name = "goods.pageinfo", version = "2.0")
@ApiDocMethod(description="获取商品列表",resultClass=GoodsPageVo.class)
public PageInfo<Goods> pageinfo2(GoodsParam param) {
}
</code></pre>
<p>使用resultClass=GoodsPageVo.class指定返回结果类型即可。</p>
<h1>
<a id="使用oauth2" class="anchor" href="#%E4%BD%BF%E7%94%A8oauth2"></a>使用oauth2</h1>
<p>如果第三方应用和本开放平台对接时需要获取用户隐私数据(如商品、订单),为为了安全与隐私,第三方应用需要取得用户的授权,即获取访问用户数据的授权令牌 AccessToken 。这种情况下,第三方应用需要引导用户完成帐号“登录授权”的流程。</p>
<p>easyopen从1.2.0版本开始支持oauth2认证。接入方式很简单:</p>
<ol class="task-list">
<li>新建一个Oauth2ManagerImpl类,实现Oauth2Manager接口</li>
<li>用户类实现OpenUser接口。</li>
</ol>
<pre><code>@Service
public class Oauth2ManagerImpl implements Oauth2Manager {
...
}
public class User implements OpenUser {
...
}
</code></pre>
<p>因为对于accessToken的管理每个开发人员所用的方式都不一样,所以需要自己来实现。</p>
<ul class="task-list">
<li>Oauth2Manager接口定义如下:</li>
</ul>
<pre><code>public interface Oauth2Manager {
/**
* 添加 auth code
*
* @param authCode
* code值
* @param authUser
* 用户
*/
void addAuthCode(String authCode, OpenUser authUser);
/**
* 添加 access token
*
* @param accessToken
* token值
* @param authUser
* 用户
* @param expiresIn 时长,秒
*/
void addAccessToken(String accessToken, OpenUser authUser, long expiresIn);
/**
* 验证auth code是否有效
*
* @param authCode
* @return 无效返回false
*/
boolean checkAuthCode(String authCode);
/**
* 根据auth code获取用户
*
* @param authCode
* @return 返回用户
*/
OpenUser getUserByAuthCode(String authCode);
/**
* 根据access token获取用户名
*
* @param accessToken
* token值
* @return 返回用户
*/
OpenUser getUserByAccessToken(String accessToken);
/**
* 获取auth code / access token 过期时间
*
* @return
*/
long getExpireIn(ApiConfig apiConfig);
/**
* 用户登录,需判断是否已经登录
* @param request
* @return 返回用户对象
*/
OpenUser login(HttpServletRequest request) throws LoginErrorException;
}
</code></pre>
<h2>
<a id="accesstoken获取流程" class="anchor" href="#accesstoken%E8%8E%B7%E5%8F%96%E6%B5%81%E7%A8%8B"></a>accessToken获取流程:</h2>
<ul class="task-list">
<li>第一步获取code</li>
</ul>
<pre><code>1、首先通过如http://localhost:8080/api/authorize?client_id=test&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Foauth2callback访问授权页面;
2、该控制器首先检查clientId是否正确;如果错误将返回相应的错误信息;
3、然后判断用户是否登录了,如果没有登录首先到登录页面登录;
4、登录成功后生成相应的code即授权码,然后重定向到客户端地址,如http://localhost:8080/oauth2callback?code=6d250650831fea227749f49a5b49ccad;在重定向到的地址中会带上code参数(授权码),接着客户端可以根据授权码去换取accessToken。
</code></pre>
<ul class="task-list">
<li>第二步通过code换取accessToken</li>
</ul>
<pre><code>1、首先通过如http://localhost:8080/api/accessToken,POST提交如下数据访问:
code:6d250650831fea227749f49a5b49ccad
client_id:test
client_secret:123456
grant_type:authorization_code
redirect_uri:http://localhost:8080/api/authorize
2、该控制器会验证client_id、client_secret、auth code的正确性,如果错误会返回相应的错误;
3、如果验证通过会生成并返回相应的访问令牌accessToken。
</code></pre>
<p>以上两个步骤需要在客户端上实现。示例项目easyopen-server上有个例子可以参考,启动服务,然后访问<a href="http://localhost:8080/go_oauth2">http://localhost:8080/go_oauth2</a></p>
<p>获取accessToken用户:</p>
<pre><code>// 拿到accessToken用户
OpenUser user = ApiContext.getAccessTokenUser();
</code></pre>
<h1>
<a id="使用jwt" class="anchor" href="#%E4%BD%BF%E7%94%A8jwt"></a>使用JWT</h1>
<p>JWT的介绍参考这里:<a href="https://www.jianshu.com/p/576dbf44b2ae">什么是 JWT -- JSON WEB TOKEN</a>。</p>
<p>之前我们的web应用使用session来维持用户与服务器之间的关系,其原理是使用一段cookie字符与服务器中的一个Map来对应,Map<cookie,UserInfo>,用户每次交互需要带一个sessionid过来。如果不使用分布式session,一旦服务器重启session会丢失,这样会影响用户体验,甚至影响业务逻辑。如果把用户信息存在客户端就没这个问题了。</p>
<p>easyopen创建JWT方式如下:</p>
<pre><code>Map<String, String> data = new HashMap<>();
data.put("id", user.getId().toString());
data.put("username", user.getUsername());
String jwt = ApiContext.createJwt(data);
</code></pre>
<p>这段代码用在用户登录的时候,然后把jwt返回给客户端,让客户端保存,如H5可以存在localStorage中。</p>
<p>客户端传递jwt方式:</p>
<pre><code>method.setRequestHeader("Authorization", "Bearer " + jwt);
</code></pre>
<p>即在header头部添加一个Authorization,内容是"Bearer " + jwt</p>
<p>客户端请求过来后,服务端获取jwt中的数据:</p>
<pre><code>// 获取jwt数据
Map<String, Claim> jwtData = ApiContext.getJwtData();
</code></pre>
<h1>
<a id="客户端请求代码" class="anchor" href="#%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%AF%B7%E6%B1%82%E4%BB%A3%E7%A0%81"></a>客户端请求代码</h1>
<h2>
<a id="java" class="anchor" href="#java"></a>Java</h2>
<pre><code>import java.io.IOException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.Test;
import com.alibaba.fastjson.JSON;
import junit.framework.TestCase;
public class PostTest extends TestCase {
@Test
public void testPost() throws IOException {
String appKey = "test";
String secret = "123456";
// 业务参数
Map<String, String> jsonMap = new HashMap<String, String>();
jsonMap.put("goodsName", "iphoneX");
String json = JSON.toJSONString(jsonMap);
json = URLEncoder.encode(json, "utf-8");
// 系统参数
Map<String, Object> param = new HashMap<String, Object>();
param.put("name", "goods.get");
param.put("app_key", appKey);
param.put("data", json);
param.put("timestamp", getTime());
param.put("version", "");
String sign = buildSign(param, secret);
param.put("sign", sign);
System.out.println("=====请求数据=====");
System.out.println(JSON.toJSON(param));
}
/**
* 构建签名
*
* @param paramsMap
* 参数
* @param secret
* 密钥
* @return
* @throws IOException
*/
public static String buildSign(Map<String, ?> paramsMap, String secret) throws IOException {
Set<String> keySet = paramsMap.keySet();
List<String> paramNames = new ArrayList<String>(keySet);
Collections.sort(paramNames);
StringBuilder paramNameValue = new StringBuilder();
for (String paramName : paramNames) {
paramNameValue.append(paramName).append(paramsMap.get(paramName));
}
String source = secret + paramNameValue.toString() + secret;
return md5(source);
}
/**
* 生成md5,全部大写
*
* @param message
* @return
*/
public static String md5(String message) {
try {
// 1 创建一个提供信息摘要算法的对象,初始化为md5算法对象
MessageDigest md = MessageDigest.getInstance("MD5");
// 2 将消息变成byte数组
byte[] input = message.getBytes();
// 3 计算后获得字节数组,这就是那128位了
byte[] buff = md.digest(input);
// 4 把数组每一字节(一个字节占八位)换成16进制连成md5字符串
return byte2hex(buff);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 二进制转十六进制字符串
*
* @param bytes
* @return
*/
private static String byte2hex(byte[] bytes) {
StringBuilder sign = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] & 0xFF);
if (hex.length() == 1) {
sign.append("0");
}
sign.append(hex.toUpperCase());
}
return sign.toString();
}
public String getTime() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
}
</code></pre>
<h2>
<a id="javascript" class="anchor" href="#javascript"></a>JavaScript</h2>
<pre><code>
var url = 'http://192.168.11.12:8080/api';
var app_key = 'test';
var secret = '123456';
/**
请求示例:
var name = 'goods.get';
var version = '';
var jsonData = {};
// form表单
var formArray = _parent.find('form').serializeArray();
jQuery.each(formArray, function(i, field){
jsonData[field.name] = field.value;
});
ApiUtil.post(name,jsonData,version,function(resp,postDataStr){
console.log(resp);
});
*/
var ApiUtil = (function(){
var API_NAME = "name";
var VERSION_NAME = "version";
var APP_KEY_NAME = "app_key";
var DATA_NAME = "data";
var TIMESTAMP_NAME = "timestamp";
var SIGN_NAME = "sign";
var FORMAT_NAME = "format";
var DATA_TYPE = 'json';
function add0(m){return m<10?'0'+m:m }
function formatDate(time)
{
var y = time.getFullYear();
var m = time.getMonth()+1;
var d = time.getDate();
var h = time.getHours();
var mm = time.getMinutes();
var s = time.getSeconds();
return y+'-'+add0(m)+'-'+add0(d)+' '+add0(h)+':'+add0(mm)+':'+add0(s);
}
/** 构建签名 */
function buildSign(postData,secret) {
var paramNames = [];
for(var key in postData) {
paramNames.push(key);
}
paramNames.sort();
var paramNameValue = [];
for(var i=0,len=paramNames.length;i<len;i++) {
var paramName = paramNames[i];
paramNameValue.push(paramName);
paramNameValue.push(postData[paramName]);
}
var source = secret + paramNameValue.join('') + secret;
// MD5算法参见http://pajhome.org.uk/crypt/md5/
return hex_md5(source).toUpperCase();
}
return {
post:function(name,data,version,callback) {
var postData = {};
postData[API_NAME] = name;
postData[VERSION_NAME] = version;
postData[APP_KEY_NAME] = app_key;
postData[DATA_NAME] = encodeURIComponent(JSON.stringify(data));
postData[TIMESTAMP_NAME] = formatDate(new Date());
postData[FORMAT_NAME] = DATA_TYPE;
postData[SIGN_NAME] = buildSign(postData,secret);
var postDataStr = JSON.stringify(postData);
jQuery.ajax({
url:url,
type:'post',
dataType:DATA_TYPE,
contentType: "application/json;charset=utf-8",
data:postDataStr,
success:function(data){
callback(data,postDataStr);
}
});
}
};
})();
</code></pre></div>
</div>
</div>
</div>
</div>
</div>
<!-- ***********************************************************内容分割线****************************************************************** -->
</article>
</div>
</div>
<script src="./static/jquery-1.10.2.min.js"></script>
<script src="./static/jquery.ztree.all-3.5.min.js"></script>
<script src="./static/jquery.ztree_toc.min.js"></script>
<script type="text/javascript">
var title = document.title;
$(document).ready(function(){
$('#tree').ztree_toc({
_header_nodes: [{ id:1, pId:0, name:title,open:false}], // 第一个节点
ztreeSetting: {
view: {
dblClickExpand: false,
showLine: true,
showIcon: false,
selectedMulti: false
},
data: {
simpleData: {
enable: true,
idKey : "id",
pIdKey: "pId"
// rootPId: "0"
}
},
callback: {
beforeClick: function(treeId, treeNode) {
$('a').removeClass('curSelectedNode');
if(treeNode.id == 1){
$('body').scrollTop(0);
}
if($.fn.ztree_toc.defaults.is_highlight_selected_line == true) {
$('#' + treeNode.id).css('color' ,'red').fadeOut("slow" ,function() {
$(this).show().css('color','black');
});
}
}
}
},
is_auto_number:true, // 菜单是否显示编号,如果markdown标题没有数字标号可以设为true
documment_selector:'.markdown-body',
is_expand_all: true // 菜单全部展开
});
// 代码高亮
$('.highlight').each(function(i, block) {
hljs.highlightBlock(block);
});
});
</script>
</body>
</html>
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。