1 Star 0 Fork 686

飞鱼/easyopen

forked from tanghc/easyopen 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
index.html 85.25 KB
一键复制 编辑 原始数据 按行查看 历史
tanghc 提交于 2018-06-27 16:58 . update
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107
<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;}
.container{margin: 0px !important;}
#readme .markdown-body, #readme .plain{border:0px !important;}
</style>
</head>
<body>
<div>
<div style="width:30%;">
<ul id="tree" class="ztree" style="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>
文档预览:
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 class="anchor" id="easyopen介绍_1" href="#easyopen介绍_1"></a>easyopen介绍</h1>
<p>一个简单易用的接口开放平台,平台封装了常用的参数校验、结果返回等功能,开发者只需实现业务代码即可。</p>
<p>easyopen的功能类似于<a href="http://open.taobao.com/docs/api.htm?spm=a219a.7629065.0.0.6cQDnQ&amp;apiId=4">淘宝开放平台</a>,它的所有接口只提供一个url,通过参数来区分不同业务。这样做的好处是接口url管理方便了,平台管理者只需维护好接口参数即可。由于参数的数量是可知的,这样可以在很大程度上进行封装。封装完后平台开发者只需要写业务代码,其它功能可以通过配置来完成。</p>
<p>得益于Java的注解功能以及Spring容器对bean的管理,我们的开放接口平台就这样产生了。</p>
<h1><a class="anchor" id="结构图_1" href="#结构图_1"></a>结构图</h1>
<p><img alt="easyopen结构图" src="https://gitee.com/uploads/images/2018/0117/095712_1f70de95_332975.png" 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 class="anchor" id="功能特点_1" href="#功能特点_1"></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 class="anchor" id="快速开始_1" href="#快速开始_1"></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... -&gt; 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 class="anchor" id="编写业务类_1" href="#编写业务类_1"></a>编写业务类</h2>
<ul class="task-list">
<li>新建一个java类名为HelloworldApi,并加上@ApiService注解</li>
</ul>
<p>加上@ApiService注解后这个类就具有了提供接口的能力。</p>
<div class="white"><div class="highlight"><pre><span class="nd">@ApiService</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">HelloWorldApi</span> <span class="o">{</span>
<span class="o">}</span>
</pre></div></div>
<ul class="task-list">
<li>在类中添加一个方法</li>
</ul>
<div class="white"><div class="highlight"><pre>@Api(name = "hello")
public String helloworld() {
return "hello world";
}
</pre></div></div>
<p>这个方法很简单,就返回了一个字符串,方法被@Api标记,表示对应的接口,name是接口名。</p>
<p>到此,一个业务方法就写完了,接下来是编写sdk并测试。</p>
<h2><a class="anchor" id="编写sdk并测试_1" href="#编写sdk并测试_1"></a>编写SDK并测试</h2>
<p>此过程在easyopen-sdk中进行。</p>
<ul class="task-list">
<li>新建Response响应类</li>
</ul>
<div class="white"><div class="highlight"><pre>public class HelloResp extends BaseResp&lt;String&gt; {
}
</pre></div></div>
<ul class="task-list">
<li>新建Request请求类</li>
</ul>
<div class="white"><div class="highlight"><pre>public class HelloReq extends BaseNoParamReq&lt;HelloResp&gt; {
public HelloReq(String name) {
super(name);
}
}
</pre></div></div>
<p>BaseResp的泛型参数指定返回体类型,这里指定String</p>
<ul class="task-list">
<li>编写单元测试</li>
</ul>
<div class="white"><div class="highlight"><pre>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());
}
}
}
</pre></div></div>
<p>这样,一个完整的接口就写完了。</p>
<h1><a class="anchor" id="自定义服务器项目_1" href="#自定义服务器项目_1"></a>自定义服务器项目</h1>
<p>easyopen-server是已经搭建好的服务器项目,可拿来立即使用。为了加深对easyopen的了解,我们自己来搭建一个,步骤非常简单,这里就使用springmvc框架搭建,步骤如下:</p>
<h3><a class="anchor" id="创建项目_1" href="#创建项目_1"></a>创建项目</h3>
<ul class="task-list">
<li>新建工程</li>
</ul>
<p>eclipse新建一个springmvc工程,工程名为myopen,建好后的工程结构如下:</p>
<p><img alt="1" src="https://gitee.com/uploads/images/2018/0118/115037_af31ccae_332975.png" title="1.png" /></p>
<ul class="task-list">
<li>添加依赖</li>
</ul>
<p>打开pom.xml添加easyopen依赖</p>
<div class="white"><div class="highlight"><pre>&lt;dependency&gt;
&lt;groupId&gt;net.oschina.durcframework&lt;/groupId&gt;
&lt;artifactId&gt;easyopen&lt;/artifactId&gt;
&lt;version&gt;1.8.4&lt;/version&gt;
&lt;/dependency&gt;
</pre></div></div>
<ul class="task-list">
<li>添加api入口</li>
</ul>
<p>新建一个IndexController并继承ApiController</p>
<div class="white"><div class="highlight"><pre><span class="nd">@Controller</span>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/api"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">IndexController</span> <span class="kd">extends</span> <span class="n">ApiController</span> <span class="o">{</span>
<span class="o">}</span>
</pre></div></div>
<p>其中头部@RequestMapping("/api")注解用来定义接口的URL,如果项目带contextPath则url为:<a target="_blank" href="http://localhost:8080/myopen/api,如果没有contextPath则为:http://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>
<div class="white"><div class="highlight"><pre>@Controller
@RequestMapping("/api")
public class IndexController extends ApiController {
@Override
protected void initApiConfig(ApiConfig apiConfig) {
Map&lt;String, String&gt; appSecretStore = new HashMap&lt;String, String&gt;();
appSecretStore.put("test", "123456");
/*
* 添加秘钥配置,map中存放秘钥信息,key对应appKey,value对应secret
* @param appSecretStore
*/
apiConfig.addAppSecret(appSecretStore);
}
}
</pre></div></div>
<p>到这里easyopen已经搭建完成了,接着就可以编写业务代码和SDK了。</p>
<h1><a class="anchor" id="错误处理_1" href="#错误处理_1"></a>错误处理</h1>
<p>easyopen对错误处理已经封装好了,最简单的做法是向上throw即可,在最顶层的Controller会做统一处理。例如:</p>
<div class="white"><div class="highlight"><pre>if(StringUtils.isEmpty(param.getGoods_name())) {
throw new ApiException("goods_name不能为null");
}
</pre></div></div>
<p>或者</p>
<div class="white"><div class="highlight"><pre>if(StringUtils.isEmpty(param.getGoods_name())) {
throw new RuntimeException("goods_name不能为null");
}
</pre></div></div>
<p>为了保证编码风格的一致性,推荐统一使用ApiException</p>
<h2><a class="anchor" id="i18n国际化_1" href="#i18n国际化_1"></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 alt="输入图片说明" src="https://gitee.com/uploads/images/2018/0118/142511_feacd145_332975.png" 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中配置错误信息
<code>
# 商品名字不能为空
isv.goods_error_100=\u5546\u54C1\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
</code></li>
</ul>
<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>
<div class="white"><div class="highlight"><pre>@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&lt;String, String&gt; appSecretStore = new HashMap&lt;String, String&gt;();
appSecretStore.put("test", "123456");
apiConfig.addAppSecret(appSecretStore);
}
}
</pre></div></div>
<p>添加一句apiConfig.getIsvModules().add("i18n/isv/goods_error");就行</p>
<ul class="task-list">
<li>新建一个interface用来定义错误</li>
</ul>
<div class="white"><div class="highlight"><pre>// 按模块来定义异常消息,团队开发可以分开进行
public interface GoodsErrors {
String isvModule = "isv.goods_error_"; // 前缀
// 100为前缀后面的错误码
// 这句话即可找到isv.goods_error_100错误
ErrorMeta NO_GOODS_NAME = new ErrorMeta(isvModule, "100", "商品名称不能为空.");
}
</pre></div></div>
<p>接下来就可以使用了</p>
<div class="white"><div class="highlight"><pre>if (StringUtils.isEmpty(param.getGoods_name())) {
throw GoodsErrors.NO_GOODS_NAME.getException();
}
</pre></div></div><h3><a class="anchor" id="国际化消息传参_1" href="#国际化消息传参_1"></a>国际化消息传参</h3>
<p>即代码中变量传入到properties文件中去,做法是采用{0},{1}占位符。0代表第一个参数,1表示第二个参数。</p>
<div class="white"><div class="highlight"><pre># 商品名称太短,不能小于{0}个字
isv.goods_error_101=\u5546\u54C1\u540D\u79F0\u592A\u77ED\uFF0C\u4E0D\u80FD\u5C0F\u4E8E{0}\u4E2A\u5B57
</pre></div></div><div class="white"><div class="highlight"><pre>if(param.getGoods_name().length() &lt; 3) {
throw GoodsErrors.SHORT_GOODS_NAME.getException(3); // 这里的“3”会填充到{0}中
}
</pre></div></div>
<p>直接放进getException(Object... params)方法参数中,因为是可变参数,可随意放。</p>
<h1><a class="anchor" id="业务参数校验_1" href="#业务参数校验_1"></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>
<div class="white"><div class="highlight"><pre>@Api(name = "goods.add")
public void addGoods(GoodsParam param) {
...
}
</pre></div></div>
<p>在GoodsParam中添加JSR-303注解:</p>
<div class="white"><div class="highlight"><pre>public class GoodsParam {
@NotEmpty(message = "商品名称不能为空")
private String goods_name;
// 省略get,set
}
</pre></div></div>
<p>如果不传商品名称则返回</p>
<div class="white"><div class="highlight"><pre>{"code":"100","msg":"商品名称不能为空"}
</pre></div></div><h2><a class="anchor" id="参数校验国际化_1" href="#参数校验国际化_1"></a>参数校验国际化</h2>
<p>国际化的配置方式如下:</p>
<div class="white"><div class="highlight"><pre>@NotEmpty(message = "{goods.name.notNull}")
private String goods_name;
</pre></div></div>
<p>国际化资源文件goods_error_en.properties中添加:</p>
<div class="white"><div class="highlight"><pre>goods.name.notNull=The goods_name can not be null
</pre></div></div>
<p>goods_error_zh_CN.properties中添加:</p>
<div class="white"><div class="highlight"><pre>goods.name.notNull=\u5546\u54C1\u540D\u79F0\u4E0D\u80FD\u4E3Anull
</pre></div></div><h2><a class="anchor" id="参数校验国际化传参_1" href="#参数校验国际化传参_1"></a>参数校验国际化传参</h2>
<p>下面校验商品名称的长度,要求大于等于3且小于等于20。数字3和20要填充到国际化资源中去。</p>
<div class="white"><div class="highlight"><pre>// 传参的格式:{xxx}=value1,value2...
@Length(min = 3, max = 20, message = "{goods.name.length}=3,20")
private String goods_name;
</pre></div></div>
<p>goods_error_en.properties:</p>
<div class="white"><div class="highlight"><pre>goods.name.length=The goods_name length must &gt;= {0} and &lt;= {1}
</pre></div></div>
<p>goods_error_zh_CN.properties中添加:</p>
<div class="white"><div class="highlight"><pre>goods.name.length=\u5546\u54C1\u540D\u79F0\u957F\u5EA6\u5FC5\u987B\u5927\u4E8E\u7B49\u4E8E{0}\u4E14\u5C0F\u4E8E\u7B49\u4E8E{1}
</pre></div></div>
<p>这样value1,value2会分别填充到{0},{1}中</p>
<h1><a class="anchor" id="自定义结果返回_1" href="#自定义结果返回_1"></a>自定义结果返回</h1><h2><a class="anchor" id="改变json输出策略_1" href="#改变json输出策略_1"></a>改变json输出策略</h2>
<p>默认使用阿里的fastjson进行json输出</p>
<div class="white"><div class="highlight"><pre>JSON.toJSONString(obj)
</pre></div></div>
<p>如果要更换输出策略,操作方式如下:</p>
<div class="white"><div class="highlight"><pre>@Override
protected void initApiConfig(ApiConfig apiConfig) {
...
// 自定义json格式输出,将null字符串变成""
apiConfig.setJsonResultSerializer(new JsonResultSerializer(){
@Override
public String serialize(Object obj) {
return JSON.toJSONString(obj, SerializerFeature.WriteMapNullValue,SerializerFeature.WriteNullStringAsEmpty);
}
});
...
}
</pre></div></div>
<p>上面示例是将null字符串变成"",如果是给安卓app提供接口,这个会很管用。</p>
<h2><a class="anchor" id="改变json格式_1" href="#改变json格式_1"></a>改变json格式</h2>
<p>easyopen默认的返回类是ApiResult,解析成json格式为:</p>
<div class="white"><div class="highlight"><pre>{
"code": "0",
"msg": "",
"data": {...}
}
</pre></div></div>
<p>我们也可以自定义返回结果,比如我们要返回这样的json:</p>
<div class="white"><div class="highlight"><pre>{
"errCode":"0",
"errMsg":"",
"body":{...}
}
</pre></div></div>
<ul class="task-list">
<li>首先,新建结果类,实现com.gitee.easyopen.Result接口:</li>
</ul>
<div class="white"><div class="highlight"><pre>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;
}
}
</pre></div></div>
<p>MyResult类可定义自己想要的字段名字,实现Result接口对应的方法即可</p>
<ul class="task-list">
<li>然后,新建一个结果生成器,实现ResultCreator接口:</li>
</ul>
<div class="white"><div class="highlight"><pre>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;
}
}
</pre></div></div>
<p>ResultCreator接口定义两个方法,createResult是返回正确内容的方法,createErrorResult是返回错误时候的方法。
分别实现它们,用我们刚才新建的MyResult类。</p>
<ul class="task-list">
<li>最后,配置结果生成器,在initApiConfig方法中配置:</li>
</ul>
<div class="white"><div class="highlight"><pre>@Controller
@RequestMapping("/project/api")
public class IndexController extends ApiController {
@Override
protected void initApiConfig(ApiConfig apiConfig) {
// 配置结果生成器
apiConfig.setResultCreator(new MyResultCreator());
省略其它代码...
}
}
</pre></div></div>
<p>调用apiConfig.setResultCreator(new MyResultCreator());进行配置</p>
<h1><a class="anchor" id="自定义序列化_1" href="#自定义序列化_1"></a>自定义序列化</h1>
<p>easyopen序列化使用fastjson处理json,xstream处理xml。现在我们来自定义实现一个json处理:</p>
<ul class="task-list">
<li>新建一个类JsonFormatter,实现ResultSerializer接口</li>
</ul>
<div class="white"><div class="highlight"><pre>public class JsonFormatter implements ResultSerializer {
@Override
public String serialize(Object obj) {
Gson gson = new Gson();
return gson.toJson(obj);
}
}
</pre></div></div>
<p>这里使用了Gson。</p>
<ul class="task-list">
<li>在apiConfig中配置</li>
</ul>
<div class="white"><div class="highlight"><pre>@Controller
@RequestMapping("/project/api")
public class IndexController extends ApiController {
@Override
protected void initApiConfig(ApiConfig apiConfig) {
// 自定义json序列化
apiConfig.setJsonResultSerializer(new JsonFormatter());
省略其它代码...
}
}
</pre></div></div><h1><a class="anchor" id="定义接口版本号_1" href="#定义接口版本号_1"></a>定义接口版本号</h1>
<p>设置@Api注解的version属性,不指定默认为""。<a href="/api" class="gfm gfm-team_member ">@pytho_njay </a>(name = "goods.get" , version = "2.0")</p>
<h1><a class="anchor" id="在业务类中获取request对象_1" href="#在业务类中获取request对象_1"></a>在业务类中获取Request对象</h1><div class="white"><div class="highlight"><pre>HttpServletRequest request = ApiContext.getRequest();
</pre></div></div><h1><a class="anchor" id="接口交互详解_1" href="#接口交互详解_1"></a>接口交互详解</h1><h2><a class="anchor" id="请求参数_1" href="#请求参数_1"></a>请求参数</h2>
<p>easyopen定义了7个固定的参数,用json接收</p>
<div class="white"><div class="highlight"><pre>{
"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"
}
</pre></div></div>
<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 class="anchor" id="请求方式_1" href="#请求方式_1"></a>请求方式</h2>
<p>请求数据放在body体中,采用json格式。这里给出一个POST工具类:</p>
<div class="white"><div class="highlight"><pre>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();
}
}
}
</pre></div></div>
<ul class="task-list">
<li>请求操作代码:</li>
</ul>
<div class="white"><div class="highlight"><pre>@Test
public void testGet() throws Exception {
Map&lt;String, String&gt; param = new HashMap&lt;String, String&gt;();
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());
}
</pre></div></div><h2><a class="anchor" id="签名算法_1" href="#签名算法_1"></a>签名算法</h2>
<p>签名算法描述如下:</p>
<ol class="task-list">
<li>将请求参数按参数名升序排序;</li>
<li>按请求参数名及参数值相互连接组成一个字符串:<code>&lt;paramName1&gt;&lt;paramValue1&gt;&lt;paramName2&gt;&lt;paramValue2&gt;...</code></li>
<li>将应用密钥分别添加到以上请求参数串的头部和尾部:<code>&lt;secret&gt;&lt;请求参数字符串&gt;&lt;secret&gt;</code></li>
<li>对该字符串进行MD5运算,得到一个二进制数组;</li>
<li>将该二进制数组转换为十六进制的字符串(全部大写),该字符串即是这些请求参数对应的签名;</li>
<li>该签名值使用<code>sign</code>参数一起和其它请求参数一起发送给服务开放平台。</li>
</ol>
<p>伪代码:</p>
<div class="white"><div class="highlight"><pre>Map&lt;String,Object&gt; paramsMap = new ...; // 参数
Set&lt;String&gt; keySet = paramsMap.keySet();
List&lt;String&gt; paramNames = new ArrayList&lt;String&gt;(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.&amp; 5.
String sign = md5(source);
// 6.
paramsMap.put("sign",sign);
</pre></div></div><h2><a class="anchor" id="服务端验证_1" href="#服务端验证_1"></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 class="anchor" id="忽略验证_1" href="#忽略验证_1"></a>忽略验证</h1>
<p>如果某个接口不需要进行验证工作,可以在@Api注解上设置属性ignoreValidate=true(默认false)。这样调用接口时,不会进行验证操作。</p>
<p>同样的,在@ApiService注解里也有一个对应的ignoreValidate属性,设置为true的话,Service类下面所有的接口都忽略验证。</p>
<h2><a class="anchor" id="忽略所有接口验证_1" href="#忽略所有接口验证_1"></a>忽略所有接口验证</h2>
<p>设置ApiConfig.setIgnoreValidate(true),所有接口的签名认证操作都将忽略(业务参数校验除外)。</p>
<h1><a class="anchor" id="生成文档页面_1" href="#生成文档页面_1"></a>生成文档页面</h1>
<p>easyopen提供一个简单的api文档查看页面,类似于swagger,基于注解功能来生成文档页面。生成的文档页面可以查看参数、结果说明,也可以进行模拟请求。对于前后端分离的项目来说会很有帮助。文档界面如下图所示:</p>
<p><img alt="输入图片说明" src="https://gitee.com/uploads/images/2018/0203/145842_55f2794e_332975.png" title="3.png" /></p>
<p>左边的树形菜单对应文档名称,点击树可前往查看对应的接口说明。点击请求按钮可以发起请求进行测试。可修改业务参数中的值进行测试。</p>
<p>下面来讲解文档注解的使用方法。</p>
<p>文档页面默认是关闭的,需要在ApiConfig中设置</p>
<div class="white"><div class="highlight"><pre>apiConfig.setShowDoc(true);// 为true开启文档页面。
</pre></div></div>
<p>接下来对获取商品接口设置文档信息:</p>
<div class="white"><div class="highlight"><pre>@Api(name = "goods.get")
public Goods getGoods(GoodsParam param) {
...
return goods;
}
</pre></div></div><h2><a class="anchor" id="设置文档注解_1" href="#设置文档注解_1"></a>设置文档注解</h2>
<p>在接口方法上添加一个@ApiDocMethod注解。</p>
<div class="white"><div class="highlight"><pre>@Api(name = "goods.get")
@ApiDocMethod(description="获取商品") // 文档注解,description为接口描述
public Goods getGoods(GoodsParam param) {
...
return goods;
}
</pre></div></div><h2><a class="anchor" id="设置参数注解_1" href="#设置参数注解_1"></a>设置参数注解</h2>
<p>进入GoodsParam类,使用@ApiDocField注解</p>
<div class="white"><div class="highlight"><pre>public class GoodsParam {
@ApiDocField(description = "商品名称", required = true, example = "iphoneX")
private String goods_name;
// 省略 get set
}
</pre></div></div>
<p>@ApiDocField注解用来定义字段信息,@ApiDocField注解定义如下</p>
<div class="white"><div class="highlight"><pre>/**
* 字段描述
*/
String description() default "";
/**
* 字段名
*/
String name() default "";
/**
* 数据类型,int string float
* @return
*/
DataType dataType() default DataType.STRING;
/**
* 是否必填
*/
boolean required() default false;
</pre></div></div><h2><a class="anchor" id="设置返回结果注解_1" href="#设置返回结果注解_1"></a>设置返回结果注解</h2>
<p>同样,在Goods类中设置@ApiDocField</p>
<div class="white"><div class="highlight"><pre>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
}
</pre></div></div>
<p>到此已经设置完毕了,可以访问url进行预览</p>
<h2><a class="anchor" id="文档页面url_1" href="#文档页面url_1"></a>文档页面URL</h2>
<p>文档页面的url格式为:apiUrl + "/doc"。如:apiUrl为<a target="_blank" href="http://localhost:8080/api/v1,那么文档页面就是:http://localhost:8080/api/v1/doc">http://localhost:8080/api/v1,那么文档页面就是:http://localhost:8080/api/v1/doc</a></p>
<h2><a class="anchor" id="list返回_1" href="#list返回_1"></a>List返回</h2>
<p>如果接口方法返回一个List,设置方式如下</p>
<div class="white"><div class="highlight"><pre> @Api(name = "goods.list", version = "2.0")
@ApiDocMethod(description="获取商品列表"
,results={@ApiDocField(description="商品列表",name="list", elementClass=Goods.class)}
)
public List&lt;Goods&gt; listGoods(GoodsParam param) {
}
</pre></div></div>
<p>elementClass对应List中的元素类型</p>
<h2><a class="anchor" id="设置文档密码-v1-8-5-_1" href="#设置文档密码-v1-8-5-_1"></a>设置文档密码(v1.8.5)</h2><div class="white"><div class="highlight"><pre>apiConfig.setDocPassword("doc123"); // 设置文档页面密码
</pre></div></div>
<p>设置完成后,需要输入密码才能访问文档页面,可配合线上联调,排错。</p>
<h2><a class="anchor" id="修改文档页返回字段名-v1-8-5-_1" href="#修改文档页返回字段名-v1-8-5-_1"></a>修改文档页返回字段名(v1.8.5)</h2>
<p>文档页面默认返回字段名为code,msg,data。可在IndexController中重写<code>processDocVelocityContext()</code>定义</p>
<div class="white"><div class="highlight"><pre>@Override
protected void processDocVelocityContext(VelocityContext context) {
context.put("code_name", "errCode");
context.put("code_description", "状态值,\"0\"表示成功,其它都是失败");
context.put("msg_name", "errMsg");
context.put("msg_description", "错误信息,出错时显示");
context.put("data_name", "respData");
context.put("data_description", "返回的数据,没有则返回{}");
}
</pre></div></div><h2><a class="anchor" id="第三方类返回_1" href="#第三方类返回_1"></a>第三方类返回</h2>
<p>如果有个一个PageInfo类,是第三方jar中的,没办法对其修改,那要如何对它里面的属性编写对应文档呢。</p>
<p>PageInfo类内容如下:</p>
<div class="white"><div class="highlight"><pre>// 假设这是jar中的类,没法修改。但是要对其进行文档生成
public class PageInfo&lt;T&gt; {
private int pageIndex;
private int pageSize;
private long total;
// 省略 get set
}
</pre></div></div>
<p>我们可以显式的声明字段信息:</p>
<div class="white"><div class="highlight"><pre> @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&lt;Goods&gt; pageinfo(GoodsParam param) {
}
</pre></div></div><h2><a class="anchor" id="文档模型复用_1" href="#文档模型复用_1"></a>文档模型复用</h2>
<p>如果多个接口都返回PageInfo,需要复制黏贴大量的注解,改一个地方需要改多个接口,无法达到复用效果。我们可以新建一个GoodsPageVo继承PageInfo,然后把文档注解写在类的头部,这样可以达到复用效果。</p>
<div class="white"><div class="highlight"><pre>@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&lt;Goods&gt; {
}
</pre></div></div><div class="white"><div class="highlight"><pre> @Api(name = "goods.pageinfo", version = "2.0")
@ApiDocMethod(description="获取商品列表",resultClass=GoodsPageVo.class)
public PageInfo&lt;Goods&gt; pageinfo2(GoodsParam param) {
}
</pre></div></div>
<p>使用resultClass=GoodsPageVo.class指定返回结果类型即可。</p>
<h2><a class="anchor" id="更改文档显示位置-v1-9-1-_1" href="#更改文档显示位置-v1-9-1-_1"></a>更改文档显示位置(v1.9.1)</h2>
<p>可以使用<code>order</code>属性来指定文档显示位置,值越小越靠前</p>
<div class="white"><div class="highlight"><pre>@ApiDoc(value = "文档demo,参考DocDemoApi.java", order = 1)
@ApiDocMethod(description = "第一个", order = 1 // 指定了order,优先按这个值排序
)
若不指定order,则按接口名排序
</pre></div></div><h1><a class="anchor" id="使用oauth2_1" href="#使用oauth2_1"></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>
<div class="white"><div class="highlight"><pre>@Service
public class Oauth2ManagerImpl implements Oauth2Manager {
...
}
public class User implements OpenUser {
...
}
</pre></div></div>
<p>因为对于accessToken的管理每个开发人员所用的方式都不一样,所以需要自己来实现。</p>
<ul class="task-list">
<li>Oauth2Manager接口定义如下:</li>
</ul>
<div class="white"><div class="highlight"><pre>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;
}
</pre></div></div><h2><a class="anchor" id="accesstoken获取流程_1" href="#accesstoken获取流程_1"></a>accessToken获取流程</h2>
<ul class="task-list">
<li>第一步获取code</li>
</ul>
<div class="white"><div class="highlight"><pre>1、首先通过如http://localhost:8080/api/authorize?client_id=test&amp;response_type=code&amp;redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Foauth2callback访问授权页面;
2、该控制器首先检查clientId是否正确;如果错误将返回相应的错误信息;
3、然后判断用户是否登录了,如果没有登录首先到登录页面登录;
4、登录成功后生成相应的code即授权码,然后重定向到客户端地址,如http://localhost:8080/oauth2callback?code=6d250650831fea227749f49a5b49ccad;在重定向到的地址中会带上code参数(授权码),接着客户端可以根据授权码去换取accessToken。
</pre></div></div>
<ul class="task-list">
<li>第二步通过code换取accessToken</li>
</ul>
<div class="white"><div class="highlight"><pre>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、code的正确性,如果错误会返回相应的错误;
3、如果验证通过会生成并返回相应的访问令牌accessToken。
{
"access_token": "01e111c0d3c8e415fea038d5c64432ef",
"refresh_token": "d1165b75d386b3ef1bd0423b4e3bfef9",
"token_type": "Bearer",
"expires_in": 7200,
"username": "admin"
}
</pre></div></div>
<p>以上两个步骤需要在客户端上实现。示例项目easyopen-server上有个例子可以参考,启动服务,然后访问<a target="_blank" href="http://localhost:8080/go_oauth2">http://localhost:8080/go_oauth2</a></p>
<p>获取accessToken用户:</p>
<div class="white"><div class="highlight"><pre>// 拿到accessToken用户
OpenUser user = ApiContext.getAccessTokenUser();
</pre></div></div><h2><a class="anchor" id="使用refreshtoken刷新accesstoken_1" href="#使用refreshtoken刷新accesstoken_1"></a>使用refreshToken刷新accessToken</h2>
<p>accessToken有过期时间,为了防止过期可以通过refreshToken来换取新的accessToken,方便后续接口调用。</p>
<div class="white"><div class="highlight"><pre>1. 首先通过如http://localhost:8080/api/accessToken,POST提交如下数据访问:
refresh_token:你的refreshToken
client_id:test
client_secret:123456
grant_type:refresh_token
2. 服务器会验证client_id、client_secret、refresh_token的正确性,如果错误会返回相应的错误;
3. 如果验证通过会生成并返回新的访问令牌accessToken和新的refreshToken
返回结果:
{
"access_token": "01e111c0d3c8e415fea038d5c64432ef",
"refresh_token": "d1165b75d386b3ef1bd0423b4e3bfef9",
"token_type": "Bearer",
"expires_in": 7200,
"username": "admin"
}
</pre></div></div>
<p>成功换取新的accessToken和refreshToken后,老的accessToken和refreshToken不能使用。</p>
<h1><a class="anchor" id="使用jwt_1" href="#使用jwt_1"></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,用户每次交互需要带一个sessionid过来。如果不使用分布式session,一旦服务器重启session会丢失,这样会影响用户体验,甚至影响业务逻辑。如果把用户信息存在客户端就没这个问题了。</p>
<p>easyopen创建JWT方式如下:</p>
<div class="white"><div class="highlight"><pre>Map&lt;String, String&gt; data = new HashMap&lt;&gt;();
data.put("id", user.getId().toString());
data.put("username", user.getUsername());
String jwt = ApiContext.createJwt(data);
</pre></div></div>
<p>这段代码用在用户登录的时候,然后把jwt返回给客户端,让客户端保存,如H5可以存在localStorage中。</p>
<p>客户端传递jwt方式:</p>
<div class="white"><div class="highlight"><pre>method.setRequestHeader("Authorization", "Bearer " + jwt);
</pre></div></div>
<p>即在header头部添加一个Authorization,内容是"Bearer " + jwt</p>
<p>客户端请求过来后,服务端获取jwt中的数据:</p>
<div class="white"><div class="highlight"><pre>// 获取jwt数据
Map&lt;String, Claim&gt; jwtData = ApiContext.getJwtData();
</pre></div></div><h1><a class="anchor" id="拦截器_1" href="#拦截器_1"></a>拦截器</h1>
<p>easyopen在1.3.1版本开始支持拦截器。</p>
<p>easyopen拦截器实现原理跟springmvc拦截器类似,拦截器作用在api方法上,即有@Api注解的方法。</p>
<p>拦截器定义如下:</p>
<div class="white"><div class="highlight"><pre>/**
* 拦截器,原理同springmvc拦截器
* @author tanghc
*
*/
public interface ApiInterceptor {
/**
* 预处理回调方法,在方法调用前执行
* @param request
* @param response
* @param serviceObj service类
* @param argu 方法参数
* @return
* @throws Exception
*/
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object serviceObj, Object argu)
throws Exception;
/**
* 接口方法执行完后调用此方法。
* @param request
* @param response
* @param serviceObj service类
* @param argu 参数
* @param result 方法返回结果
* @throws Exception
*/
void postHandle(HttpServletRequest request, HttpServletResponse response, Object serviceObj, Object argu,
Object result) throws Exception;
/**
* 结果包装完成后执行
* @param request
* @param response
* @param serviceObj service类
* @param argu 参数
* @param result 最终结果,被包装过
* @param e
* @throws Exception
*/
void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object serviceObj, Object argu,
Object result, Exception e) throws Exception;
/**
* 匹配拦截器
* @param apiMeta 接口信息
* @return
*/
boolean match(ApiMeta apiMeta);
}
</pre></div></div>
<p>同样也提供一个适配器ApiInterceptorAdapter</p>
<ul class="task-list">
<li>拦截器执行流程:</li>
</ul>
<p>跟springmvc拦截器执行流程一样</p>
<ol class="task-list">
<li>preHandle 如果返回false,则不调用api方法,接着逆序调用afterCompletion,需要通过response自定义返回</li>
<li>如果preHandle 返回true,继续进行下一个preHandle</li>
<li>preHandle执行完毕后,逆序执行postHandle</li>
<li>最后逆序调用afterCompletion</li>
</ol>
<ul class="task-list">
<li>正常流程:</li>
</ul>
<div class="white"><div class="highlight"><pre>ApiInterceptor1.preHandle
ApiInterceptor2.preHandle
apiMethod.invoke() // api方法调用
ApiInterceptor2.postHandle
ApiInterceptor1.postHandle
ApiInterceptor2.afterCompletion
ApiInterceptor1.afterCompletion
</pre></div></div><h2><a class="anchor" id="配置拦截器_1" href="#配置拦截器_1"></a>配置拦截器</h2>
<p>新建一个日志处理拦截器,继承ApiInterceptorAdapter,重写父类中的方法</p>
<div class="white"><div class="highlight"><pre>public class LogInterceptor extends ApiInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object serviceObj, Object argu)
throws Exception {
System.out.println("======preHandle======");
System.out.println("IP:" + RequestUtil.getClientIP(request));
System.out.println("接口类:" + serviceObj.getClass().getName());
if(argu != null) {
System.out.println("参数类:" + argu.getClass().getName());
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object serviceObj, Object argu,
Object result) throws Exception {
System.out.println("======postHandle======");
System.out.println("接口类:" + serviceObj.getClass().getName());
if(argu != null) {
System.out.println("参数类:" + argu.getClass().getName());
}
System.out.println("结果:" + JSON.toJSONString(result));
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object serviceObj,
Object argu, Object result, Exception e) throws Exception {
System.out.println("======afterCompletion======");
System.out.println("接口类:" + serviceObj.getClass().getName());
System.out.println("参数类:" + argu.getClass().getName());
System.out.println("最终结果:" + JSON.toJSONString(result));
System.out.println("e:" + e);
}
}
</pre></div></div>
<p>在apiConfgi中添加拦截器:</p>
<div class="white"><div class="highlight"><pre>@Override
protected void initApiConfig(ApiConfig apiConfig) {
...
// 配置拦截器
apiConfig.setInterceptors(
new ApiInterceptor[] { new LogInterceptor()});
...
}
</pre></div></div><h2><a class="anchor" id="拦截范围_1" href="#拦截范围_1"></a>拦截范围</h2>
<p>默认拦截所有接口,如果要拦截指定接口,可重写boolean match()方法:</p>
<div class="white"><div class="highlight"><pre>// 只拦截goods.get接口
@Override
public boolean match(ApiMeta apiMeta) {
return apiMeta.getName().equals("goods.get");
}
</pre></div></div><h1><a class="anchor" id="数据加密传输_1" href="#数据加密传输_1"></a>数据加密传输</h1>
<p>默认我们的数据传输都是不经过加密的,要加密传输的话得用上HTTPS协议。easyopen在1.4.0版本开始提供了数据加密传输,不需要HTTPS协议。</p>
<p>easyopen基于公私钥+AES加密传输,交互流程如下:</p>
<div class="white"><div class="highlight"><pre>0. 事先把公钥放在客户端,私钥放在服务端
1. 客户端生成一个随机码randomKey
2. 将randomKey通过公钥RSA加密str = rsa_encode(randomKey,publicKey)
3. 将str发送到服务端
4. 服务端通过私钥解开str,得到randomKey:randomKey = rsa_decode(str, privateKey)
5. 服务端使用randomKey通过AES对称加密一个值(假设值为"0")返回给客户端,resp = aes_encode("0", randomKey)
6. 客户端用自己的randomKey去aes解密resp,如果得到的是"0",则整个握手交互完成,后续都用这个randomKey进行aes加解密传输
注:
黑客是可以拿到公钥的,但是黑客无法知道客户端生成的随机码randomKey,一旦str发生改变则握手失败.
整个过程的重点就是将randomKey安全的送到服务器,后期都用randomKey进行对称加密传输,对称加密黑客无法破解.
此流程参照HTTPS,只不过服务器无法将公钥正确的送到客户端(浏览器),因此浏览器的HTTPS需要CA机构介入.
</pre></div></div><h2><a class="anchor" id="公私钥配置_1" href="#公私钥配置_1"></a>公私钥配置</h2>
<ul class="task-list">
<li>生成公私钥</li>
</ul>
<div class="white"><div class="highlight"><pre>public class PubPriKeyTest extends TestCase {
/**
* 生成公私钥
* @throws Exception
*/
@Test
public void testCreate() throws Exception {
KeyStore store = RSAUtil.createKeys();
System.out.println("公钥:");
System.out.println(store.getPublicKey());
System.out.println("私钥:");
System.out.println(store.getPrivateKey());
}
}
</pre></div></div>
<ul class="task-list">
<li>在客户端新建一个pub.key文件,放入公钥字符串,pub.key放在客户端,启动的时候加载。</li>
<li>在服务端新建一个pri.key文件,放入私钥字符串,pri.key放在resources目录下,服务启动会自动读取。</li>
</ul>
<p>easyopen-sdk的resources下已经存放了一个pub.key,实例化一个EncryptClient对象就能使用数据加密传输。感兴趣的同学可以查看源码,了解整个交互流程。</p>
<div class="white"><div class="highlight"><pre>// 数据加密传输不需要secret
OpenClient client = new EncryptClient(url, app_key);
</pre></div></div><h1><a class="anchor" id="可自定义默认版本号_1" href="#可自定义默认版本号_1"></a>可自定义默认版本号</h1>
<p>接口默认的版本号是空字符串"",如果要修改,可进行如下配置:</p>
<div class="white"><div class="highlight"><pre>// 修改默认版本号,所有的接口的默认版本变为1.0
apiConifg.setDefaultVersion("1.0");
</pre></div></div><h1><a class="anchor" id="自定义session管理_1" href="#自定义session管理_1"></a>自定义session管理</h1>
<p>easyopen1.4.0开始支持。</p>
<ul class="task-list">
<li>创建session</li>
</ul>
<p>登陆成功后创建session,并返回sessionId</p>
<div class="white"><div class="highlight"><pre>// 自定义session
@PostMapping("managedSessionLogin")
public String managedSessionLogin(HttpServletRequest request) {
// 假设登陆成功,创建一个sessionId返回给客户端
SessionManager sessionManager = ApiContext.getApiConfig().getSessionManager();
HttpSession session = sessionManager.getSession(null);
session.setAttribute("username", "tom");
return session.getId();
}
</pre></div></div>
<ul class="task-list">
<li>使用session</li>
</ul>
<p>客户端需要传递access_token参数,值为sessionId</p>
<p>请求参数:</p>
<div class="white"><div class="highlight"><pre>{
"access_token": "4191FB1D8356495D98ECCF91C615A530",
"app_key": "test",
"data": "{}",
"sign": "F7AB6BC059DFCA93CA2328C9BAF236BA",
"sign_method": "md5",
"name": "manager.session.get",
"format": "json",
"version": "",
"timestamp": "2018-03-13 13:48:45"
}
</pre></div></div>
<p>服务端通过HttpSession session = ApiContext.getManagedSession();获取session</p>
<div class="white"><div class="highlight"><pre>@Api(name = "manager.session.get")
public Object managersetsession() {
HttpSession session = ApiContext.getManagedSession();
System.out.println(session.getId());
Object user = session.getAttribute("username");
return user;
}
</pre></div></div><h2><a class="anchor" id="使用redis管理session_1" href="#使用redis管理session_1"></a>使用redis管理session</h2>
<p>easyopen默认使用谷歌的guava缓存进行session管理,但是在集群的情况下会有问题,因此easyopen还提供了一个Redis版本。配置如下:</p>
<ul class="task-list">
<li>pom添加redis依赖</li>
</ul>
<div class="white"><div class="highlight"><pre>&lt;dependency&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-starter-data-redis&lt;/artifactId&gt;
&lt;/dependency&gt;
</pre></div></div>
<ul class="task-list">
<li>添加redis参数</li>
</ul>
<div class="white"><div class="highlight"><pre>
#################redis基础配置#################
spring.redis.database=0
spring.redis.host=10.1.11.48
spring.redis.password=0987654321rfvujmtgbyhn
spring.redis.port=6379
# 连接超时时间 单位 ms(毫秒)
spring.redis.timeout=3000
#################redis线程池设置#################
# 连接池中的最大空闲连接,默认值也是8。
spring.redis.pool.max-idle=500
#连接池中的最小空闲连接,默认值也是0。
spring.redis.pool.min-idle=50
# 如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
spring.redis.pool.max-active=2000
# 等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException
spring.redis.pool.max-wait=1000
</pre></div></div>
<ul class="task-list">
<li>设置apiConfig</li>
</ul>
<div class="white"><div class="highlight"><pre>@Controller
@RequestMapping(value = "/api")
public class IndexController extends ApiController {
@Autowired
private RedisTemplate redisTemplate; // 1 声明redis模板
@Override
protected void initApiConfig(ApiConfig apiConfig) {
// 配置秘钥键值对
Map&lt;String, String&gt; appSecretStore = new HashMap&lt;String, String&gt;();
appSecretStore.put("test", "123456");
// 2 配置sessionManager
RedisSessionManager sessionManager = new RedisSessionManager(redisTemplate);
apiConfig.setSessionManager(sessionManager);
apiConfig.addAppSecret(appSecretStore);
}
}
</pre></div></div><h3><a class="anchor" id="修改redis的key前缀_1" href="#修改redis的key前缀_1"></a>修改redis的key前缀</h3>
<p>默认存入redis的key前缀为<code>session:</code>,如果要自定义前缀可调用:</p>
<div class="white"><div class="highlight"><pre>sessionManager.setKeyPrefix("session-key:");
</pre></div></div><h1><a class="anchor" id="app-key和secret存放在数据库或redis中_1" href="#app-key和secret存放在数据库或redis中_1"></a>app_key和secret存放在数据库或redis中</h1>
<p>这里以redis为例</p>
<p>新建一个RedisAppSecretManager类实现AppSecretManager接口</p>
<div class="white"><div class="highlight"><pre>/**
* 使用方式:
*
* &lt;pre&gt;
@Autowired
private AppSecretManager appSecretManager;
@Override
protected void initApiConfig(ApiConfig apiConfig) {
...
apiConfig.setAppSecretManager(appSecretManager);
...
}
* &lt;/pre&gt;
*
* @author tanghc
*
*/
@Component
public class RedisAppSecretManager implements AppSecretManager {
public static String APP_KEY_PREFIX = "easyopen_app_key:";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void addAppSecret(Map&lt;String, String&gt; appSecretStore) {
stringRedisTemplate.opsForHash().putAll(APP_KEY_PREFIX, appSecretStore);
}
@Override
public String getSecret(String appKey) {
return (String)stringRedisTemplate.opsForHash().get(APP_KEY_PREFIX, appKey);
}
@Override
public boolean isValidAppKey(String appKey) {
if (appKey == null) {
return false;
}
return getSecret(appKey) != null;
}
}
</pre></div></div>
<p>存放app_key和secret采用hash set的方式,这样在redis中查看会比较方便,一目了然.</p>
<p>然后在IndexController中:</p>
<div class="white"><div class="highlight"><pre>@Autowired
private AppSecretManager appSecretManager;
@Override
protected void initApiConfig(ApiConfig apiConfig) {
...
apiConfig.setAppSecretManager(appSecretManager);
...
}
</pre></div></div><h1><a class="anchor" id="使用webflux_1" href="#使用webflux_1"></a>使用WebFlux</h1>
<p>这里基于springboot2 + WebFlux,相关教程见:<a href="https://www.ibm.com/developerworks/cn/java/spring5-webflux-reactive/index.html">springboot-webflux</a></p>
<p>需要easyopen1.7.0及以上版本</p>
<ul class="task-list">
<li>在pom.xml中添加WebFlux依赖</li>
</ul>
<div class="white"><div class="highlight"><pre>&lt;dependency&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-starter-webflux&lt;/artifactId&gt;
&lt;/dependency&gt;
</pre></div></div>
<ul class="task-list">
<li>在IndexController中添加:</li>
</ul>
<div class="white"><div class="highlight"><pre>
@Controller
@RequestMapping("/api")
public class IndexController extends ApiController {
...
// http://localhost:8080/api/mono
@RequestMapping("mono")
@ResponseBody
public Mono&lt;Object&gt; mono(HttpServletRequest request, HttpServletResponse response) {
return Mono.justOrEmpty(this.invoke(request, response));
}
...
}
</pre></div></div>
<p>api的url由之前的<a target="_blank" href="http://localhost:8080/api变为http://localhost:8080/api/mono">http://localhost:8080/api变为http://localhost:8080/api/mono</a></p>
<p>其它地方不变</p>
<h1><a class="anchor" id="开启app对接模式-v1-7-5-_1" href="#开启app对接模式-v1-7-5-_1"></a>开启app对接模式(v1.7.5)</h1>
<p>如果平台直接跟Android或IOS对接,可开启这个功能。因为手机上的时间有可能跟服务端的时间不一致(用户的手机情况不可控)。</p>
<p>失去了时间校验,一个请求有可能被反复调用,服务端需要防止重复提交,有必要的话上HTTPS。</p>
<p>开启方式:</p>
<div class="white"><div class="highlight"><pre>apiConfig.openAppMode();
</pre></div></div>
<p>开启app对接模式,开启后不进行timeout校验。</p>
<h1><a class="anchor" id="防止表单重复提交-v1-7-7-_1" href="#防止表单重复提交-v1-7-7-_1"></a>防止表单重复提交(v1.7.7)</h1>
<p>使用redis分布式锁解决表单重复提交问题。</p>
<div class="white"><div class="highlight"><pre>核心思想:
try {
锁(用户id + 接口名) {
执行业务代码
}
} finally {
释放锁
}
在锁的内部执行业务代码时,其它线程进来都将拒之门外。
</pre></div></div>
<p>新增拦截器继承BaseLockInterceptor </p>
<div class="white"><div class="highlight"><pre>/**
* 使用分布式锁防止表单重复提交
*
* @author tanghc
*/
public class LockInterceptor extends BaseLockInterceptor {
private StringRedisTemplate redisTemplate;
public LockInterceptor() {
redisTemplate = ApiContext.getApplicationContext().getBean(StringRedisTemplate.class);
}
@SuppressWarnings("rawtypes")
@Override
protected RedisTemplate getRedisTemplate() {
return redisTemplate;
}
@Override
protected String getUserId() {
Map&lt;String, Claim&gt; jwtData = ApiContext.getJwtData();
String id = jwtData.get("id").asString(); // 用户id
return id;
}
@Override
public boolean match(ApiMeta apiMeta) {
return "userlock.test".equals(apiMeta.getName()); // 只针对这个接口
}
}
</pre></div></div>
<p>实现上面三个方法即可,match方法返回true表示执行这个拦截器,可针对特定的接口做操作。</p>
<p>然后配置拦截器:</p>
<div class="white"><div class="highlight"><pre>apiConfig.setInterceptors(new ApiInterceptor[] {new LockInterceptor()});
</pre></div></div><h1><a class="anchor" id="监控页面-v1-8-1-_1" href="#监控页面-v1-8-1-_1"></a>监控页面(v1.8.1)</h1>
<p>启动服务端,浏览器访问:<a target="_blank" href="http://localhost:8080/api/monitor">http://localhost:8080/api/monitor</a></p>
<p>输入密码:monitor123</p>
<h2><a class="anchor" id="修改默认密码_1" href="#修改默认密码_1"></a>修改默认密码</h2><div class="white"><div class="highlight"><pre>apiConfig.setMonitorPassword(newPassword);
</pre></div></div>
<ul class="task-list">
<li>监控内容放在Map对象中,存放接口对应的累积信息,并不会记录每次请求的信息,因此无需担心内存使用量增多。</li>
</ul>
<h1><a class="anchor" id="文件上传-v1-8-7-_1" href="#文件上传-v1-8-7-_1"></a>文件上传(v1.8.7)</h1>
<ul class="task-list">
<li>SDK</li>
</ul>
<div class="white"><div class="highlight"><pre>/**
* 上传文件,读取本地文件
*
* @throws IOException
*/
@Test
public void testUpload() throws IOException {
GoodsParam param = new GoodsParam();
param.setGoods_name("iphone6");
GoodsReq req = new GoodsReq("file.upload", param);
String path = this.getClass().getResource("").getPath();
List&lt;UploadFile&gt; files = new ArrayList&lt;&gt;();
// 这里的headImg,idcardImg要跟服务端参数名对应
files.add(new UploadFile("headImg", new File(path + "1.txt")));
files.add(new UploadFile("idcardImg", new File(path + "2.txt")));
GoodsResp result = client.requestFile(req, files);
System.out.println("--------------------");
if (result.isSuccess()) {
System.out.println(result.getData());
} else {
System.out.println("errorMsg:" + result.getMsg());
}
System.out.println("--------------------");
}
</pre></div></div>
<ul class="task-list">
<li>服务端处理</li>
</ul>
<div class="white"><div class="highlight"><pre>@Api(name = "file.upload")
@ApiDocMethod(description = "文件上传")
Object upload(UploadParam param) throws IllegalStateException, IOException {
// 获取上传文件
MultipartFile headImgFile = param.getHeadImg();
MultipartFile idcardImgFile = param.getIdcardImg();
StringBuilder sb = new StringBuilder();
sb.append("表单名:").append(headImgFile.getName()).append(",")
.append("文件大小:").append(headImgFile.getSize()).append(";");
sb.append("表单名:").append(idcardImgFile.getName()).append(",")
.append("文件大小:").append(idcardImgFile.getSize()).append(";");
// headImgFile.getInputStream(); // 返回文件流
// headImgFile.getBytes(); // 返回文件数据流
headImgFile.transferTo(new File("D:/new_" + headImgFile.getOriginalFilename()));
idcardImgFile.transferTo(new File("D:/new_" + idcardImgFile.getOriginalFilename()));
return new ApiResult(sb.toString());
}
</pre></div></div>
<ul class="task-list">
<li>UploadParam.java</li>
</ul>
<div class="white"><div class="highlight"><pre>public class UploadParam {
@ApiDocField(description = "商品名称", required = true, example = "iphoneX")
@NotEmpty(message = "商品名称不能为空")
@Length(min = 3, max = 20, message = "{goods.name.length}=3,20")
private String goods_name;
// 这里定义上传的文件,属性名称对应客户端上传的name
@ApiDocField(description = "头像图片", required = true, dataType = DataType.FILE)
@NotNull(message = "请上传头像图片")
private MultipartFile headImg;
@ApiDocField(description = "身份证图片", required = true, dataType = DataType.FILE)
@NotNull(message = "请上传身份证图片")
private MultipartFile idcardImg;
//getter,setter
}
</pre></div></div>
<p><code>headImg</code>,<code>idcardImg</code>就是上传的表单名,客户端需要于此对应。</p>
<h2><a class="anchor" id="上传内存文件_1" href="#上传内存文件_1"></a>上传内存文件</h2>
<p>有些文件不是从本地读取的,而是从其它地方下载到内存中,比如从阿里云下载文件到内存中,不落地。</p>
<div class="white"><div class="highlight"><pre>List&lt;UploadFile&gt; files = new ArrayList&lt;&gt;();
files.add(new UploadFile("headImg","headImg.txt", this.getClass().getResourceAsStream("1.txt")));
files.add(new UploadFile("idcardImg", "idcardImg.txt", this.getClass().getResourceAsStream("2.txt")));
GoodsResp result = client.requestFile(req, files);
</pre></div></div>
<p>或者</p>
<div class="white"><div class="highlight"><pre>List&lt;UploadFile&gt; files = new ArrayList&lt;&gt;();
files.add(new UploadFile("headImg","headImg.txt", byte[]));
files.add(new UploadFile("idcardImg", "idcardImg.txt", byte[]));
GoodsResp result = client.requestFile(req, files);
</pre></div></div>
<p>主要通过UploadFile类的构造方法来区分</p>
<div class="white"><div class="highlight"><pre>/**
* @param name 表单名称,不能重复
* @param file 文件
* @throws IOException
*/
public UploadFile(String name, File file)
/**
* @param name 表单名称,不能重复
* @param fileName 文件名
* @param input 文件流
* @throws IOException
*/
public UploadFile(String name, String fileName, InputStream input)
/**
* @param name 表单名称,不能重复
* @param fileName 文件名
* @param fileData 文件数据
*/
public UploadFile(String name, String fileName, byte[] fileData)
</pre></div></div><h1><a class="anchor" id="限流功能-v1-9-1-_1" href="#限流功能-v1-9-1-_1"></a>限流功能(v1.9.1)</h1>
<p>针对每个接口做限流功能,限流方式有两种:</p>
<ul class="task-list">
<li>限流策略:每秒处理固定数量的请求,超出请求返回错误信息。可用在秒杀、抢购业务</li>
<li>令牌桶策略:每秒放置固定数量的令牌数,不足的令牌数做等待处理,直到拿到令牌为止。平滑输出,可减轻服务器压力。</li>
</ul>
<p>两种策略可在后台页面切换</p>
<h2><a class="anchor" id="开启限流功能_1" href="#开启限流功能_1"></a>开启限流功能</h2>
<p>以springboot为例</p>
<ul class="task-list">
<li>application.properties配置redis信息</li>
<li>IndexController中配置:</li>
</ul>
<div class="white"><div class="highlight"><pre>@Autowired
private RedisTemplate redisTemplate;
@Override
protected void initApiConfig(ApiConfig apiConfig) {
...
// 配置拦截器
apiConfig.setInterceptors(
new ApiInterceptor[] {
new LimitInterceptor() // 限流拦截器,放在首位
...
});
/*
****************设置限流管理************************* */
apiConfig.setLimitManager(new ApiLimitManager(redisTemplate));
// [可选],设置配置页面访问密码
//apiConfig.setLimitPassword(limitPassword);
// [可选],设置【策略】,默认为限流策略
//apiConfig.setDefaultLimitType(defaultLimitType);
// [可选],设置【每秒可处理请求数】,默认50。策略为限流策略时,该值生效
//apiConfig.setDefaultLimitCount(defaultLimitCount);
// [可选],设置【令牌桶容量】,默认50。策略为令牌桶策略时,该值生效
//apiConfig.setDefaultTokenBucketCount(defaultTokenBucketCount);
}
</pre></div></div>
<ul class="task-list">
<li>访问:<a target="_blank" href="http://localhost:8080/api/limit">http://localhost:8080/api/limit</a> 对接口进行限流设置,默认密码limit123</li>
</ul>
<h1><a class="anchor" id="文档页头部显示项目描述-v1-9-1-_1" href="#文档页头部显示项目描述-v1-9-1-_1"></a>文档页头部显示项目描述(v1.9.1)</h1>
<p>IndexController重写getDocRemark()方法</p>
<div class="white"><div class="highlight"><pre>@Override
protected String getDocRemark() {
return "文档描述,支持html标签&lt;br&gt;";
}
</pre></div></div><h1><a class="anchor" id="使用get方式请求-v1-9-1-_1" href="#使用get方式请求-v1-9-1-_1"></a>使用GET方式请求(v1.9.1)</h1>
<p>直接在url跟参数</p>
<p>如接口URL为:<a target="_blank" href="http://localhost:8080/api">http://localhost:8080/api</a></p>
<p>则完整地址为:</p>
<div class="white"><div class="highlight"><pre>http://localhost:8080/api?name=doc.result.5&amp;version=&amp;app_key=test&amp;data=%257B%2522goods_name%2522%253A%2522iphoneX%2522%257D&amp;timestamp=2018-06-22%2009%3A38%3A32&amp;format=json&amp;sign=552BB4285F59C4CC230164E8E3BF4348
</pre></div></div>
<ul class="task-list">
<li>注:参数值需要urlencode一下,如果用到上传功能,还是需要post方式</li>
</ul>
<hr>
<h1><a class="anchor" id="客户端请求代码_1" href="#客户端请求代码_1"></a>客户端请求代码</h1><h2><a class="anchor" id="java_1" href="#java_1"></a>Java</h2><div class="white"><div class="highlight"><pre>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&lt;String, String&gt; jsonMap = new HashMap&lt;String, String&gt;();
jsonMap.put("goodsName", "iphoneX");
String json = JSON.toJSONString(jsonMap);
json = URLEncoder.encode(json, "utf-8");
// 系统参数
Map&lt;String, Object&gt; param = new HashMap&lt;String, Object&gt;();
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&lt;String, ?&gt; paramsMap, String secret) throws IOException {
Set&lt;String&gt; keySet = paramsMap.keySet();
List&lt;String&gt; paramNames = new ArrayList&lt;String&gt;(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 &lt; bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] &amp; 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());
}
}
</pre></div></div><h2><a class="anchor" id="javascript_1" href="#javascript_1"></a>JavaScript</h2><div class="white"><div class="highlight"><pre>/**
//需要发布到服务器上运行,并且server端需要处理跨域
//在IndexController.java上加@CrossOrigin(origins={"*"})
sdk.config({
url : 'http://localhost:8080/api'
,app_key : 'test'
,secret : '123456'
,jwt : 'xxx'
});
sdk.post({
name : 'goods.get' // 接口名
// ,version:'1.0'
// ,access_token:''
,data : {'goods_name':'iphone'} // 请求参数
,callback:function(resp) { // 成功回调
console.log(resp)
}
});
sdk.post({
name : 'goods.get' // 接口名
,data : {'goods_name':'iphone'} // 请求参数
,callback:function(resp) { // 成功回调
console.log(resp)
}
});
*/
;(function(){
var config = {
url : 'http://127.0.0.1:8080/api'
,app_key : 'test'
,secret : '123456'
,default_version : ''
,api_name : "name"
,version_name : "version"
,app_key_name : "app_key"
,data_name : "data"
,timestamp_name : "timestamp"
,sign_name : "sign"
,format_name : "format"
,access_token_name : "access_token"
,jwt : ''
}
var DEFAULT_FORMAT = 'json';
function copy(source, target) {
if (target &amp;&amp; source &amp;&amp; typeof source == 'object') {
for (var p in source) {
target[p] = source[p];
}
}
return target;
}
function add0(m){return m&lt;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&lt;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();
}
var ajax = {
/**
* 提交请求
* @param options
* { url:'',params:{},success:function(){},error:function(){} }
*/
request:function(url,params,headers,callback,error) {
error = error || function(e){alert('数据请求失败')};
var xhr = this.createXhrObject();
var paramStr = JSON.stringify(params);
xhr.onreadystatechange = function() {
var jsonData = '';
if (xhr.readyState == 4){
var status = xhr.status;
if ((status &gt;= 200 &amp;&amp; status &lt; 300) || status == 304){
jsonData = JSON.parse(xhr.responseText);
callback(jsonData, paramStr);
} else {
jsonData = JSON.parse('{"message":"后台请求错误(status:' + status + ')"}');
console.log(xhr.responseText)
error(jsonData, paramStr);
}
}
};
xhr.open('POST', url, true);
xhr.setRequestHeader("Content-Type","application/json");
xhr.setRequestHeader("X-Requested-With","XMLHttpRequest");
if(headers) {
for (var key in headers) {
xhr.setRequestHeader(key,headers[key]);
}
}
xhr.send(paramStr);
}
/**
* 创建XHR对象
* @private
*/
,createXhrObject:function() {
var methods = [
function(){ return new XMLHttpRequest();}
,function(){ return new ActiveXObject('Msxml2.XMLHTTP');}
,function(){ return new ActiveXObject('Microsoft.XMLHTTP');}
];
for(var i=0,len=methods.length; i&lt;len; i++) {
try {
methods[i]();
} catch (e) {
continue;
}
this.createXhrObject = methods[i];
return methods[i]();
}
throw new Error("创建XHR对象失败");
}
}
var sdk = {
config:function(cfg) {
copy(cfg,config);
}
/**
* post请求
* @param opts.name 接口名
* @param opts.version 版本号
* @param opts.data 请求数据,json对象
* @param opts.access_token
* @param opts.callback 响应回调
* @param jwt jwt
*/
,post:function(opts, jwt) {
var name = opts.name,
version = opts.version || config.default_version,
data = opts.data || {},
callback = opts.callback,
accessToken = opts.access_token;
var headers = {};
var postData = {};
data = data || {};
postData[config.api_name] = name;
postData[config.version_name] = version;
postData[config.app_key_name] = config.app_key;
postData[config.data_name] = encodeURIComponent(JSON.stringify(data));
postData[config.timestamp_name] = formatDate(new Date());
postData[config.format_name] = DEFAULT_FORMAT;
if(accessToken) {
postData[config.access_token_name] = accessToken;
}
postData[config.sign_name] = buildSign(postData,config.secret);
var _jwt = config.jwt || jwt;
if(_jwt) {
headers['Authorization'] = 'Bearer ' + _jwt;
}
ajax.request(config.url,postData,headers, callback);
}
}
window.sdk = sdk;
})();
</pre></div></div><h1><a class="anchor" id="常见问题_1" href="#常见问题_1"></a>常见问题</h1><h2><a class="anchor" id="在业务方法中如何获取request对象-_1" href="#在业务方法中如何获取request对象-_1"></a>在业务方法中如何获取request对象?</h2>
<p>ApiContext.getRequest(),该方法可在子线程中使用。</p>
<h2><a class="anchor" id="sdk加密传输json解析错误_1" href="#sdk加密传输json解析错误_1"></a>SDK加密传输json解析错误</h2>
<p>如果使用了shiro等权限框架,注意配置url,IndexController里面有多个url,除了api,还有api/doc,api/ssl等,这些url都需要允许访问
可以使用api*通配符来允许访问。</p>
<h2><a class="anchor" id="如何获取当前调用者的appkey-_1" href="#如何获取当前调用者的appkey-_1"></a>如何获取当前调用者的appKey?</h2>
<p>ApiContext.getApiParam().fatchAppKey()</p>
<h2><a class="anchor" id="直接返回方法中的结果_1" href="#直接返回方法中的结果_1"></a>直接返回方法中的结果</h2><div class="white"><div class="highlight"><pre>@Api(name = "xxx"
, wrapResult = false // 对结果不进行包装,直接将ApiResult转成json形式返回
)
public ApiResult fun(GoodsParam param) {
ApiResult apiResult = new ApiResult();
apiResult.setCode(200);
apiResult.setMsg("xxx");
return apiResult;
}
</pre></div></div><h2><a class="anchor" id="拦截器中使用spring注入_1" href="#拦截器中使用spring注入_1"></a>拦截器中使用spring注入</h2>
<p>// 第一步</p>
<div class="white"><div class="highlight"><pre>@Component // 加这个注解
public class MyInterceptor extends ApiInterceptorAdapter {
...
}
</pre></div></div>
<p>// 第二步</p>
<div class="white"><div class="highlight"><pre>@Controller
@RequestMapping("/api")
public class IndexController extends ApiController {
@Autowired
private MyInterceptor myInterceptor;
@Override
protected void initApiConfig(ApiConfig apiConfig) {
...
apiConfig.setInterceptors(new ApiInterceptor[]{myInterceptor});
...
}
}
</pre></div></div><h1><a class="anchor" id="系统错误码_1" href="#系统错误码_1"></a>系统错误码</h1>
<p>具体参见:<code>com.gitee.easyopen.message.Errors.java</code></p>
<ul class="task-list">
<li>-9=系统错误</li>
<li>1=调用不存在的服务请求</li>
<li>2=服务请求参数非法</li>
<li>3=服务请求缺少应用键参数</li>
<li>4=服务请求的应用键参数无效</li>
<li>5=服务请求需要签名,缺少签名参数</li>
<li>6=服务请求的签名无效</li>
<li>7=服务请求超时</li>
<li>8=服务请求业务逻辑出错</li>
<li>9=服务不可用</li>
<li>10=服务请求时间格式有误</li>
<li>11=服务请求序列化格式错误</li>
<li>12=服务请求出错, contentType 不支持</li>
<li>13=JSON 格式错误: </li>
<li>14=accessToken错误</li>
<li>15=accessToken已过期</li>
<li>16=未设置accessToken</li>
<li>17=操作token错误</li>
<li>18=token错误</li>
<li>19=算法不支持</li>
<li>20=ssl交互错误</li>
<li>21=jwt已过期</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ***********************************************************内容分割线****************************************************************** -->
<h1>md5.js</h1>
<a href="static/md5.js" target="_blank">md5.js</a>
</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>
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
Java
1
https://gitee.com/yuzhongxin/easyopen.git
git@gitee.com:yuzhongxin/easyopen.git
yuzhongxin
easyopen
easyopen
master

搜索帮助