可方便扩展的JIRA Rest Web API的封装调用
2015-09-25 13:38
471 查看
JIRA是一个缺陷跟踪管理系统,被广泛应用于缺陷跟踪、客户服务、需求收集、流程审批、任务跟踪、项目跟踪和敏捷管理等工作领域,当我们需要把第三方业务系统集成进来时,可以调用他的API。
JIRA本身的API非常强大,但它是一个底层的API体系,并不是一个易用的接口,如果要开发和拓展,所以需要我们二次包装。
jira官方为解决这个问题,推出了方便强大的javaclientlibrary(目前只有java客户端库,没有.Net类库)
jira的RestAPI最新文档官网.
https://docs.atlassian.com/jira/REST/latest/
https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials
如果是编写java桌面或web应用,jira提供了更方便的方式(Client类库),JIRARESTJavaClientisaJavalibrary(usablefromanyJVMlanguage)whichallowstoeasilytalktoanyJIRA4.2+instanceusingnew(andstillevolving)RESTAPI.
https://ecosystem.atlassian.net/wiki/display/JRJC/Home
如果使用Client类库,可以方便应用各种现成的jira实体类(如项目、问题、备注、自定义字段......),不需要再重复造轮子,大幅提升效率。
首先,必须要了解JIRAapi的接口结构,其中<resource-name>可以理解成api的方法,比如project,就是项目信息,user就是用户信息,issue就是问题信息....
1
JIRA'sRESTAPIisprovidedbyapluginthatisanchoredundertheURIpathcomponent
还先要搞清楚jiraapi的认证体系,摘自官网:
thefirststepinusingtheJIRARESTAPIistoauthenticateauseraccountwithyourJIRAsite.ForthepurposesofthistutorialwewilluseHTTPBASICAuthentication,butanyauthenticationthatworksagainstJIRAwillworkagainsttheRESTAPI.Thisincludes:
OAuth
HTTPCookies
TrustedApplications
os_username/os_passwordqueryparameters
为方便使用,我们采用BasicAuth
Buildastringoftheformusername:password
Base64encodethestring
Supplyan"Authorization"headerwithcontent"Basic"followedbytheencodedstring.Forexample,thestring"fred:fred"encodesto"ZnJlZDpmcmVk"inbase64,soyouwouldmaketherequestasfollows.
搞清楚了BasicAuth,用C#进行代码实现,写了一个用于ASP.NETMVC的包装类,方便重复调用。
publicclassJiraApi
{
privatestringm_Username;
privatestringm_Password;
publicJiraApi(stringusername,stringpassword)
{
m_Username=username;
m_Password=password;
}
///<summary>
///处理post请求,执行新建、编辑、删除等操作
///</summary>
///<paramname="sData">json输入字符</param>
///<paramname="uri">api的具体地址,一般是baseurl+业务处理资源关键字</param>
///<returns>Jira返回的WebResponse输出</returns>
publicstringDoPost(stringsData,stringuri)
{
Uriaddress=newUri(uri);
HttpWebRequestrequest;
//HttpWebResponseresponse1=null;
StreamReadersr;
stringreturnXML=string.Empty;
if(address==null){thrownewArgumentNullException("address");}
try
{
request=WebRequest.Create(address)asHttpWebRequest;
request.Method="POST";
request.ContentType="application/json";
stringbase64Credentials=GetEncodedCredentials();
request.Headers.Add("Authorization","Basic"+base64Credentials);
//request.Credentials=newNetworkCredential(sUsername,sPassword);
if(sData!=null)
{
byte[]byteData=UTF8Encoding.UTF8.GetBytes(sData);
request.ContentLength=byteData.Length;
using(StreampostStream=request.GetRequestStream())
{
postStream.Write(byteData,0,byteData.Length);
}
using(HttpWebResponseresponse1=request.GetResponse()asHttpWebResponse)
{
StreamReaderreader=newStreamReader(response1.GetResponseStream());
stringstr=reader.ReadToEnd();
returnstr;
}
}
return"error";
}
catch(WebExceptionwex)
{
if(wex.Response!=null)
{
using(HttpWebResponseerrorResponse=(HttpWebResponse)wex.Response)
{
try
{
stringsError=string.Format("Theserverreturned'{0}'withthestatuscode{1}({2:d}).",
errorResponse.StatusDescription,errorResponse.StatusCode,
errorResponse.StatusCode);
sr=newStreamReader(errorResponse.GetResponseStream(),Encoding.UTF8);
returnXML=sr.ReadToEnd();
returnreturnXML;
}
finally
{
if(errorResponse!=null)errorResponse.Close();
}
}
}
else
{
//thrownewException(wex.Message);
returnwex.Message;
}
}
}
///<summary>
///处理get请求,执行查询操作
///</summary>
///<paramname="resource">输入的业务处理资源关键字,必填项</param>
///<paramname="argument">参数,用于获取具体查询操作,非必填项</param>
///<paramname="data">暂时没用处,非必填项</param>
///<paramname="method">默认为GET,非必填项</param>
///<returns></returns>
publicstringDoQuery(
stringresource,
stringargument=null,
stringdata=null,
stringmethod="GET")
{
stringurl=string.Format("{0}{1}/",Config.BaseURL,resource.ToString());
if(argument!=null)
{
url=string.Format("{0}{1}/",url,argument);
}
HttpWebRequestrequest=WebRequest.Create(url)asHttpWebRequest;
request.ContentType="application/json";
request.Method=method;
if(data!=null)
{
using(StreamWriterwriter=newStreamWriter(request.GetRequestStream()))
{
writer.Write(data);
}
}
stringbase64Credentials=GetEncodedCredentials();
request.Headers.Add("Authorization","Basic"+base64Credentials);
HttpWebResponseresponse=request.GetResponse()asHttpWebResponse;
stringresult=string.Empty;
using(StreamReaderreader=newStreamReader(response.GetResponseStream()))
{
result=reader.ReadToEnd();
}
returnresult;
}
privatestringGetEncodedCredentials()
{
stringmergedCredentials=string.Format("{0}:{1}",m_Username,m_Password);
byte[]byteCredentials=UTF8Encoding.UTF8.GetBytes(mergedCredentials);
returnConvert.ToBase64String(byteCredentials);
}
}
///查询jira的项目情况
///</summary>
///<paramname="sender"></param>
///<paramname="e"></param>
protectedvoidPage_Load(objectsender,EventArgse)
{
JiraApimanager=newJiraApi(Config.User,Config.Password);
Response.Write(manager.DoQuery("project"));
}
{
if(!IsPostBack)
{
JiraApiapi=newJiraApi(Config.User,Config.Password);
//对问题添加备注
SubmitComment(api,Request.QueryString["issue"],Request.QueryString["comment"]);
}
}
///<summary>
///添加问题的备注
///</summary>
///<paramname="jm">JiraApi实例</param>
///<paramname="issue">问题的关键字,比如SomeIssue-18</param>
///<paramname="comment">备注的内容</param>
privatevoidSubmitComment(JiraApiapi,stringissue,stringcomment)
{
stringsData=string.Format("{\"body\":\"{0}\"}",comment);
stringuri=Config.BaseURL+string.Format("issue/{0}/comment",issue);
Response.Write(api.DoPost(sData,uri));
}
JIRA本身的API非常强大,但它是一个底层的API体系,并不是一个易用的接口,如果要开发和拓展,所以需要我们二次包装。
jira官方为解决这个问题,推出了方便强大的javaclientlibrary(目前只有java客户端库,没有.Net类库)
jira的RestAPI最新文档官网.
JIRA6.4.12RESTAPIdocumentation
JIRARESTAPITutorials:
如果是编写java桌面或web应用,jira提供了更方便的方式(Client类库),JIRARESTJavaClientisaJavalibrary(usablefromanyJVMlanguage)whichallowstoeasilytalktoanyJIRA4.2+instanceusingnew(andstillevolving)RESTAPI.
JIRAJavaClientlibrary
如果使用Client类库,可以方便应用各种现成的jira实体类(如项目、问题、备注、自定义字段......),不需要再重复造轮子,大幅提升效率。
首先,必须要了解JIRAapi的接口结构,其中<resource-name>可以理解成api的方法,比如project,就是项目信息,user就是用户信息,issue就是问题信息....
1
http://hostname/rest/<api-name>/<api-version>/<resource-name>
JIRA'sRESTAPIisprovidedbyapluginthatisanchoredundertheURIpathcomponent
/rest/.Hence,ifyourJIRAsiteisrunningat:
还先要搞清楚jiraapi的认证体系,摘自官网:
thefirststepinusingtheJIRARESTAPIistoauthenticateauseraccountwithyourJIRAsite.ForthepurposesofthistutorialwewilluseHTTPBASICAuthentication,butanyauthenticationthatworksagainstJIRAwillworkagainsttheRESTAPI.Thisincludes:
OAuth
HTTPCookies
TrustedApplications
os_username/os_passwordqueryparameters
为方便使用,我们采用BasicAuth
BasicAuthheaders
Ifyouneedtoyoumayconstructandsendbasicauthheadersyourself.Todothisyouneedtoperformthefollowingsteps:Buildastringoftheformusername:password
Base64encodethestring
Supplyan"Authorization"headerwithcontent"Basic"followedbytheencodedstring.Forexample,thestring"fred:fred"encodesto"ZnJlZDpmcmVk"inbase64,soyouwouldmaketherequestasfollows.
一个curl的例子,注意红色字符串是对“username:password”的Base64编码 curl-D--XGET-H"Authorization:BasicZnJlZDpmcmVk"-H"Content-Type:application/json""http://kelpie9:8081/rest/api/2/issue/QA-31"
搞清楚了BasicAuth,用C#进行代码实现,写了一个用于ASP.NETMVC的包装类,方便重复调用。
1、C#实现的API包装类
publicclassJiraApi
{
privatestringm_Username;
privatestringm_Password;
publicJiraApi(stringusername,stringpassword)
{
m_Username=username;
m_Password=password;
}
///<summary>
///处理post请求,执行新建、编辑、删除等操作
///</summary>
///<paramname="sData">json输入字符</param>
///<paramname="uri">api的具体地址,一般是baseurl+业务处理资源关键字</param>
///<returns>Jira返回的WebResponse输出</returns>
publicstringDoPost(stringsData,stringuri)
{
Uriaddress=newUri(uri);
HttpWebRequestrequest;
//HttpWebResponseresponse1=null;
StreamReadersr;
stringreturnXML=string.Empty;
if(address==null){thrownewArgumentNullException("address");}
try
{
request=WebRequest.Create(address)asHttpWebRequest;
request.Method="POST";
request.ContentType="application/json";
stringbase64Credentials=GetEncodedCredentials();
request.Headers.Add("Authorization","Basic"+base64Credentials);
//request.Credentials=newNetworkCredential(sUsername,sPassword);
if(sData!=null)
{
byte[]byteData=UTF8Encoding.UTF8.GetBytes(sData);
request.ContentLength=byteData.Length;
using(StreampostStream=request.GetRequestStream())
{
postStream.Write(byteData,0,byteData.Length);
}
using(HttpWebResponseresponse1=request.GetResponse()asHttpWebResponse)
{
StreamReaderreader=newStreamReader(response1.GetResponseStream());
stringstr=reader.ReadToEnd();
returnstr;
}
}
return"error";
}
catch(WebExceptionwex)
{
if(wex.Response!=null)
{
using(HttpWebResponseerrorResponse=(HttpWebResponse)wex.Response)
{
try
{
stringsError=string.Format("Theserverreturned'{0}'withthestatuscode{1}({2:d}).",
errorResponse.StatusDescription,errorResponse.StatusCode,
errorResponse.StatusCode);
sr=newStreamReader(errorResponse.GetResponseStream(),Encoding.UTF8);
returnXML=sr.ReadToEnd();
returnreturnXML;
}
finally
{
if(errorResponse!=null)errorResponse.Close();
}
}
}
else
{
//thrownewException(wex.Message);
returnwex.Message;
}
}
}
///<summary>
///处理get请求,执行查询操作
///</summary>
///<paramname="resource">输入的业务处理资源关键字,必填项</param>
///<paramname="argument">参数,用于获取具体查询操作,非必填项</param>
///<paramname="data">暂时没用处,非必填项</param>
///<paramname="method">默认为GET,非必填项</param>
///<returns></returns>
publicstringDoQuery(
stringresource,
stringargument=null,
stringdata=null,
stringmethod="GET")
{
stringurl=string.Format("{0}{1}/",Config.BaseURL,resource.ToString());
if(argument!=null)
{
url=string.Format("{0}{1}/",url,argument);
}
HttpWebRequestrequest=WebRequest.Create(url)asHttpWebRequest;
request.ContentType="application/json";
request.Method=method;
if(data!=null)
{
using(StreamWriterwriter=newStreamWriter(request.GetRequestStream()))
{
writer.Write(data);
}
}
stringbase64Credentials=GetEncodedCredentials();
request.Headers.Add("Authorization","Basic"+base64Credentials);
HttpWebResponseresponse=request.GetResponse()asHttpWebResponse;
stringresult=string.Empty;
using(StreamReaderreader=newStreamReader(response.GetResponseStream()))
{
result=reader.ReadToEnd();
}
returnresult;
}
privatestringGetEncodedCredentials()
{
stringmergedCredentials=string.Format("{0}:{1}",m_Username,m_Password);
byte[]byteCredentials=UTF8Encoding.UTF8.GetBytes(mergedCredentials);
returnConvert.ToBase64String(byteCredentials);
}
}
调用方法:查询Get
///<summary>///查询jira的项目情况
///</summary>
///<paramname="sender"></param>
///<paramname="e"></param>
protectedvoidPage_Load(objectsender,EventArgse)
{
JiraApimanager=newJiraApi(Config.User,Config.Password);
Response.Write(manager.DoQuery("project"));
}
调用方法:提交Post
protectedvoidPage_Load(objectsender,EventArgse){
if(!IsPostBack)
{
JiraApiapi=newJiraApi(Config.User,Config.Password);
//对问题添加备注
SubmitComment(api,Request.QueryString["issue"],Request.QueryString["comment"]);
}
}
///<summary>
///添加问题的备注
///</summary>
///<paramname="jm">JiraApi实例</param>
///<paramname="issue">问题的关键字,比如SomeIssue-18</param>
///<paramname="comment">备注的内容</param>
privatevoidSubmitComment(JiraApiapi,stringissue,stringcomment)
{
stringsData=string.Format("{\"body\":\"{0}\"}",comment);
stringuri=Config.BaseURL+string.Format("issue/{0}/comment",issue);
Response.Write(api.DoPost(sData,uri));
}
2、JAVAClient类库实现的APIDEMO
packagejiraTEST;
importjava.net.URI;
importjava.net.URISyntaxException;
importjava.util.ArrayList;
importjava.util.HashMap;
importjava.util.Iterator;
importjava.util.List;
importjava.util.Map;
importjava.util.concurrent.ExecutionException;
importorg.apache.commons.codec.binary.Base32;
importorg.joda.time.DateTime;
importcom.atlassian.jira.rest.client.IssueRestClient;
importcom.atlassian.jira.rest.client.JiraRestClient;
importcom.atlassian.jira.rest.client.SearchRestClient;
importcom.atlassian.jira.rest.client.domain.BasicIssue;
importcom.atlassian.jira.rest.client.domain.BasicProject;
importcom.atlassian.jira.rest.client.domain.BasicUser;
importcom.atlassian.jira.rest.client.domain.Comment;
importcom.atlassian.jira.rest.client.domain.Field;
importcom.atlassian.jira.rest.client.domain.Issue;
importcom.atlassian.jira.rest.client.domain.Project;
importcom.atlassian.jira.rest.client.domain.SearchResult;
importcom.atlassian.jira.rest.client.domain.Transition;
importcom.atlassian.jira.rest.client.domain.input.ComplexIssueInputFieldValue;
importcom.atlassian.jira.rest.client.domain.input.FieldInput;
importcom.atlassian.jira.rest.client.domain.input.IssueInput;
importcom.atlassian.jira.rest.client.domain.input.IssueInputBuilder;
importcom.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory;
importcom.atlassian.util.concurrent.Promise;
importcom.google.common.collect.Lists;
publicclassCvteJiraDemo{
publicstaticStringBaseURL="http://jira-test:8080/";
publicstaticStringUser="admin";
publicstaticStringPassword="admin";
privatestaticURIjiraServerUri=URI
.create("http://jira-test/rest/api/2/");
privatestaticbooleanquiet=false;
privatestaticfinallongBUG_TYPE_ID=1L;//JIRAmagicvalue
privatestaticfinallongTASK_TYPE_ID=3L;//JIRAmagicvalue
privatestaticfinalDateTimeDUE_DATE=newDateTime();
privatestaticfinalStringPRIORITY="Trivial";
privatestaticfinalStringDESCRIPTION="description";
publicstaticvoidmain(String[]args)throwsInterruptedException,
ExecutionException{
finalAsynchronousJiraRestClientFactoryfactory=newAsynchronousJiraRestClientFactory();
URIjiraServerUri;
try{
jiraServerUri=newURI(BaseURL);
finalJiraRestClientrestClient=(JiraRestClient)factory
.createWithBasicHttpAuthentication(jiraServerUri,User,
Password);
getAllProjects(restClient);
getProject(restClient,"DEMO");
getIssue(restClient,"FEEDBACK-14");
getIssueFields(restClient,"FEEDBACK-27");
addIssue(restClient,"FEEDBACK","AAAAB");
addIssueComplex(restClient,"FEEDBACK",DUE_DATE.toString());
}catch(URISyntaxExceptione){
//TODOAuto-generatedcatchblock
e.printStackTrace();
}finally{
}
}
privatestaticvoidprintln(Objecto){
if(!quiet){
System.out.println(o);
}
}
privatestaticvoidparseArgs(String[]argsArray)throwsURISyntaxException{
finalList<String>args=Lists.newArrayList(argsArray);
if(args.contains("-q")){
quiet=true;
args.remove(args.indexOf("-q"));
}
if(!args.isEmpty()){
jiraServerUri=newURI(args.get(0));
}
}
privatestaticTransitiongetTransitionByName(
Iterable<Transition>transitions,StringtransitionName){
for(Transitiontransition:transitions){
if(transition.getName().equals(transitionName)){
returntransition;
}
}
returnnull;
}
//得到所有项目信息
privatestaticvoidgetAllProjects(finalJiraRestClientrestClient)
throwsInterruptedException,ExecutionException{
try{
Promise<Iterable<BasicProject>>list=restClient
.getProjectClient().getAllProjects();
Iterable<BasicProject>a=list.get();
Iterator<BasicProject>it=a.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
}finally{
}
}
//得到单一项目信息
privatestaticvoidgetProject(finalJiraRestClientrestClient,
StringporjectKEY)throwsInterruptedException,ExecutionException{
try{
Projectproject=restClient.getProjectClient()
.getProject(porjectKEY).get();
System.out.println(project);
}finally{
}
}
//得到单一问题信息
privatestaticvoidgetIssue(finalJiraRestClientrestClient,
StringissueKEY)throwsInterruptedException,ExecutionException{
try{
Promise<Issue>list=restClient.getIssueClient()
.getIssue(issueKEY);
Issueissue=list.get();
System.out.println(issue);
}finally{
}
}
//创建问题
publicstaticBasicIssuecreateIssue(finalJiraRestClientjiraRestClient,
IssueInputnewIssue){
BasicIssuebasicIssue=jiraRestClient.getIssueClient()
.createIssue(newIssue).claim();
returnbasicIssue;
}
//添加备注到问题
publicstaticvoidaddCommentToIssue(finalJiraRestClientjiraRestClient,Issueissue,Stringcomment){
IssueRestClientissueClient=jiraRestClient.getIssueClient();
issueClient.addComment(issue.getCommentsUri(),Comment.valueOf(comment)).claim();
}
//删除问题,目前找不到对应API
publicstaticvoiddeleteIssue(finalJiraRestClientjiraRestClient,Issueissue){
IssueRestClientissueClient=jiraRestClient.getIssueClient();
//issueClient.deleteIssue(issue.getKey(),false).claim();
}
//通过标题获取问题
publicstaticIterablefindIssuesByLabel(finalJiraRestClientjiraRestClient,Stringlabel){
SearchRestClientsearchClient=jiraRestClient.getSearchClient();
Stringjql="labels%3D"+label;
com.atlassian.jira.rest.client.domain.SearchResultresults=((SearchRestClient)jiraRestClient).searchJql(jql).claim();
returnresults.getIssues();
}
//通过KEY获取问题
publicstaticIssuefindIssueByIssueKey(finalJiraRestClientjiraRestClient,StringissueKey){
SearchRestClientsearchClient=jiraRestClient.getSearchClient();
Stringjql="issuekey=\""+issueKey+"\"";
SearchResultresults=searchClient.searchJql(jql).claim();
return(Issue)results.getIssues().iterator().next();
}
//创建问题:仅有简单问题名称
privatestaticvoidaddIssue(finalJiraRestClientrestClient,
StringporjectKEY,StringissueName)throwsInterruptedException,
ExecutionException{
try{
IssueInputBuilderbuilder=newIssueInputBuilder(porjectKEY,
TASK_TYPE_ID,issueName);
builder.setDescription("issuedescription");
finalIssueInputinput=builder.build();
try{
//createissue
finalIssueRestClientclient=restClient.getIssueClient();
finalBasicIssueissue=client.createIssue(input).claim();
finalIssueactual=client.getIssue(issue.getKey()).claim();
System.out.println(actual);
}finally{
if(restClient!=null){
//restClient.close();
}
}
}finally{
}
}
//创建问题:包含自定义字段
privatestaticvoidaddIssueComplex(finalJiraRestClientrestClient,
StringporjectKEY,StringissueName)throwsInterruptedException,
ExecutionException{
try{
IssueInputBuilderbuilder=newIssueInputBuilder(porjectKEY,
TASK_TYPE_ID,issueName);
builder.setDescription("issuedescription");
//builder.setFieldValue("priority",ComplexIssueInputFieldValue.with("name",PRIORITY));
//单行文本
builder.setFieldValue("customfield_10042","单行文本测试");
//单选字段
builder.setFieldValue("customfield_10043",ComplexIssueInputFieldValue.with("value","一般"));
//数值自定义字段
builder.setFieldValue("customfield_10044",100.08);
//用户选择自定义字段
builder.setFieldValue("customfield_10045",ComplexIssueInputFieldValue.with("name","admin"));
//用户选择自定义字段(多选)
Map<String,Object>user1=newHashMap<String,Object>();
user1.put("name","admin");
Map<String,Object>user2=newHashMap<String,Object>();
user2.put("name","wangxn");
ArrayListpeoples=newArrayList();
peoples.add(user1);
peoples.add(user2);
builder.setFieldValue("customfield_10047",peoples);
//设定父问题
Map<String,Object>parent=newHashMap<String,Object>();
parent.put("key","FEEDBACK-25");
FieldInputparentField=newFieldInput("parent",newComplexIssueInputFieldValue(parent));
builder.setFieldInput(parentField);
finalIssueInputinput=builder.build();
try{
finalIssueRestClientclient=restClient.getIssueClient();
finalBasicIssueissue=client.createIssue(input).claim();
finalIssueactual=client.getIssue(issue.getKey()).claim();
System.out.println(actual);
}finally{
if(restClient!=null){
//restClient.close();
}
}
}finally{
}
}
//获取问题的所有字段
privatestaticvoidgetIssueFields(finalJiraRestClientrestClient,
StringissueKEY)throwsInterruptedException,ExecutionException{
try{
Promise<Issue>list=restClient.getIssueClient()
.getIssue(issueKEY);
Issueissue=list.get();
Iterable<Field>fields=issue.getFields();
Iterator<Field>it=fields.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
}finally{
}
}
}
相关文章推荐
- OC基础-OC三大基本数据结构-NSString
- eclipse运行项目时怎么设置虚拟机内存大小
- 自动生成Makefile的全过程详解2
- ASP.NET 将数据生成PDF (二)
- 「两」创建一个带 ssh 镜座服务(修订版)--采用 Dockerfile 创
- 杭电4500小Q系列故事——屌丝的逆袭
- VS2010 常用快捷键
- buildConfigField "boolean", "LOG_DEBUG", "true"
- 终端查询数据库sqlite(创建你自己,或者是coredata创建)那里的东西
- asp.net生成PDF文件 (1)
- 黑马程序员————java实现快速排序
- JS获得当前浏览器的地址参数信息
- android的事件处理
- 右键菜单没有新建文本文件了,怎么办?
- java PriorityBlockingQueue 基于优先级队列,的读出操作可以阻止.
- php中包含js产生的中文乱码问题
- 视频分辨率
- 牛逼博客链接集合
- github使用
- vs2013常用2