SpringBoot对接外部接口,一步一步性能调实战篇
需求分析:
本平台对接某某平台的接口,保证接口的稳定性和安全性实战:
首先我们初始化一个Demo,SpringBoot初始化教程略,初始化后的效果如下:
1。引入依赖
这里我们使用commonshttpclient3!https:mvnrepository。comartifactcommonshttpclientcommonshttpclientdependencygroupIdcommonshttpclientgroupIdcommonshttpclientartifactIdversion3。1versiondependency!使用goole的json转化工具dependencygroupIdcom。google。code。gsongroupIdgsonartifactIdversion2。8。6versiondependencydependencygroupIdorg。projectlombokgroupIdlombokartifactIddependency2。编写工具类(HttpClientUtils):
Get请求:
根据需求,这里我们需要两个参数,一个是token,一个是url参数url地址,权限验证采用的是BearerTokenpublicstaticStringsendGet(StringurlParam,Stringtoken){1。创建httpClient实例对象HttpClienthttpClientnewHttpClient();设置httpClient连接主机服务器超时时间:15000毫秒httpClient。getHttpConnectionManager()。getParams()。setConnectionTimeout(15000);2。创建GetMothod实例对象GetMethodgetMethodnewGetMethod(urlParam);3。设置post请求超时时间、请求头getMethod。getParams()。setParameter(HttpMethodParams。SOTIMEOUT,60000);getMethod。addRequestHeader(ContentType,applicationjson);if(!StringUtils。isEmpty(token)){HeaderheadernewHeader(Authorization,Bearertoken);getMethod。addRequestHeader(header);}try{4。执行getMethod,调用http接口httpClient。executeMethod(getMethod);5。读取内容〔流的形式读取〕InputStreamisgetMethod。getResponseBodyAsStream();BufferedReaderbrnewBufferedReader(newInputStreamReader(is,StandardCharsets。UTF8));采用线程安全的StringBufferStringBufferresnewStringBuffer();Stringstr;while((strbr。readLine())!null){res。append(str);}returnres。toString();}catch(IOExceptione){e。printStackTrace();}finally{6。释放连接getMethod。releaseConnection();}returnnull;}
Post请求:
根据需求,这里我们需要三个参数,一个是token,一个是url参数url地址,还有一个是请求体,权限验证采用的是BearerTokenpublicstaticStringsendPost(StringurlParam,MapString,ObjectjsonMap,Stringtoken){1。创建httpClient实例对象HttpClienthttpClientnewHttpClient();设置httpClient连接主机服务器超时时间:15000毫秒httpClient。getHttpConnectionManager()。getParams()。setConnectionTimeout(15000);2。创建PostMethod实例对象PostMethodpostMethodnewPostMethod(urlParam);设置post请求超时时间、请求头postMethod。getParams()。setParameter(HttpMethodParams。SOTIMEOUT,60000);postMethod。addRequestHeader(ContentType,applicationjson;charsetutf8);if(!StringUtils。isEmpty(token)){HeaderheadernewHeader(Authorization,Bearertoken);postMethod。addRequestHeader(header);}3。设置请求体GsongsonnewGson();StringjsonStrgson。toJson(jsonMap);postMethod。setRequestBody(jsonStr);try{4。执行postMethod,调用http接口httpClient。executeMethod(postMethod);5。读取内容〔流的形式读取〕InputStreamispostMethod。getResponseBodyAsStream();BufferedReaderbrnewBufferedReader(newInputStreamReader(is,StandardCharsets。UTF8));采用线程安全的StringBufferStringBufferresnewStringBuffer();Stringstr;while((strbr。readLine())!null){res。append(str);}returnres。toString();}catch(IOExceptione){e。printStackTrace();}finally{7。释放连接postMethod。releaseConnection();}returnnull;}
Main方法测试:1。调用获取token接口StringbaseUrlhttp:;StringurlbaseUrltoken;MapString,ObjectjsonMapnewHashMap();jsonMap。put(username,);jsonMap。put(password,);StringressendPost(url,jsonMap,null);log。info(获得的请求结果:{},res);
获得的请求结果:{message:success,status:1,data:{token:eyJh}}
我们请求得到JSON字符串后,使用GSON来解析JSON,提取有用的信息,如token2。解析JSON,得到tokenGsongsonnewGson();克服泛型类型擦除问题具体查阅https:zditect。commainadvancedjavagsonjsontomap。htmlTypemapTypenewTypeTokenHashMapString,Object(){}。getType();HashMapString,ObjectresMapgson。fromJson(res,mapType);log。info(请求结果解析:{},resMap);LinkedTreeMapdata(LinkedTreeMap)resMap。get(data);Stringtoken(String)data。get(token);log。info(token为:{},token);
获取得到token后我们开始使用,模拟一次Get请求:urlbaseUrl;log。info(获得的请求结果:{},sendGet(url,token));获得的请求结果:{message:success,status:1,data:{}
完整工具类,方便大家拿来直接使用:
HttpClientUtils。javapackagecom。example。demo;importcom。google。gson。Gson;importcom。google。gson。internal。LinkedTreeMap;importcom。google。gson。reflect。TypeToken;importlombok。extern。slf4j。Slf4j;importorg。apache。commons。httpclient。Header;importorg。apache。commons。httpclient。HttpClient;importorg。apache。commons。httpclient。methods。GetMethod;importorg。apache。commons。httpclient。methods。PostMethod;importorg。apache。commons。httpclient。params。HttpMethodParams;importorg。springframework。util。StringUtils;importjava。io。BufferedReader;importjava。io。IOException;importjava。io。InputStream;importjava。io。InputStreamReader;importjava。lang。reflect。Type;importjava。nio。charset。StandardCharsets;importjava。util。HashMap;importjava。util。Map;authorxhDate2022914Slf4jpublicclassHttpClientUtils{publicstaticStringsendPost(StringurlParam,MapString,ObjectjsonMap,Stringtoken){1。创建httpClient实例对象HttpClienthttpClientnewHttpClient();设置httpClient连接主机服务器超时时间:15000毫秒httpClient。getHttpConnectionManager()。getParams()。setConnectionTimeout(15000);2。创建PostMethod实例对象PostMethodpostMethodnewPostMethod(urlParam);设置post请求超时时间、请求头postMethod。getParams()。setParameter(HttpMethodParams。SOTIMEOUT,60000);postMethod。addRequestHeader(ContentType,applicationjson;charsetutf8);if(!StringUtils。isEmpty(token)){HeaderheadernewHeader(Authorization,Bearertoken);postMethod。addRequestHeader(header);}3。设置请求体GsongsonnewGson();StringjsonStrgson。toJson(jsonMap);postMethod。setRequestBody(jsonStr);try{4。执行postMethod,调用http接口httpClient。executeMethod(postMethod);5。读取内容〔流的形式读取〕InputStreamispostMethod。getResponseBodyAsStream();BufferedReaderbrnewBufferedReader(newInputStreamReader(is,StandardCharsets。UTF8));采用线程安全的StringBufferStringBufferresnewStringBuffer();Stringstr;while((strbr。readLine())!null){res。append(str);}returnres。toString();}catch(IOExceptione){e。printStackTrace();}finally{7。释放连接postMethod。releaseConnection();}returnnull;}publicstaticStringsendGet(StringurlParam,Stringtoken){1。创建httpClient实例对象HttpClienthttpClientnewHttpClient();设置httpClient连接主机服务器超时时间:15000毫秒httpClient。getHttpConnectionManager()。getParams()。setConnectionTimeout(15000);2。创建GetMothod实例对象GetMethodgetMethodnewGetMethod(urlParam);3。设置post请求超时时间、请求头getMethod。getParams()。setParameter(HttpMethodParams。SOTIMEOUT,60000);getMethod。addRequestHeader(ContentType,applicationjson);if(!StringUtils。isEmpty(token)){HeaderheadernewHeader(Authorization,Bearertoken);getMethod。addRequestHeader(header);}try{4。执行getMethod,调用http接口httpClient。executeMethod(getMethod);5。读取内容〔流的形式读取〕InputStreamisgetMethod。getResponseBodyAsStream();BufferedReaderbrnewBufferedReader(newInputStreamReader(is,StandardCharsets。UTF8));采用线程安全的StringBufferStringBufferresnewStringBuffer();Stringstr;while((strbr。readLine())!null){res。append(str);}returnres。toString();}catch(IOExceptione){e。printStackTrace();}finally{6。释放连接getMethod。releaseConnection();}returnnull;}publicstaticvoidmain(String〔〕args){1。调用获取token接口StringbaseUrlhttp:;StringurlbaseUrl;MapString,ObjectjsonMapnewHashMap();jsonMap。put(username,);jsonMap。put(password,);StringressendPost(url,jsonMap,null);log。info(获得的请求结果:{},res);2。解析JSON,得到tokenGsongsonnewGson();克服泛型类型擦除问题具体查阅https:zditect。commainadvancedjavagsonjsontomap。htmlTypemapTypenewTypeTokenHashMapString,Object(){}。getType();HashMapString,ObjectresMapgson。fromJson(res,mapType);log。info(请求结果解析:{},resMap);LinkedTreeMapdata(LinkedTreeMap)resMap。get(data);Stringtoken(String)data。get(token);log。info(token为:{},token);3。模拟Get请求TODO需要使用URL编码urlbaseUrl;log。info(获得的请求结果:{},sendGet(url,token));}}
为前端提供接口并测试:
首先我们统一返回风格:Result。javapackagecom。example。demo;importlombok。Data;importjava。io。Serializable;authorxhDate2022914DatapublicclassResultTimplementsSerializable{privatestaticfinallongserialVersionUID1L;编码:0表示成功,其他值表示失败privateintcode0;消息内容privateStringmsgsuccess;响应数据privateTdata;publicResultTok(Tdata){this。setData(data);returnthis;}publicResultTerror(Stringmsg){this。code500;this。msgmsg;returnthis;}}
新建ApiController:
首先我们将公共变量做一个提取:publicstaticStringTOKEN;publicstaticfinalStringBASEURLhttp:;publicstaticfinalStringUSERNAME;publicstaticfinalStringPASSWORD;静态代码块static{1。调用获取token接口StringurlBASEURL;MapString,ObjectjsonMapnewHashMap();jsonMap。put(username,USERNAME);jsonMap。put(password,PASSWORD);StringressendPost(url,jsonMap,null);2。解析JSON,得到tokenGsongsonnewGson();TypemapTypenewTypeTokenHashMapString,Object(){}。getType();HashMapString,ObjectresMapgson。fromJson(res,mapType);LinkedTreeMapdata(LinkedTreeMap)resMap。get(data);TOKEN(String)data。get(token);log。info(token获取成功:{},TOKEN);}
模拟Get请求:Get请求请求地址:http:localhost:8080identitygetDetailget?handlexxxreturnGetMapping(getDetailget)publicResultStringgetDataGet(RequestParamStringhandle){log。info(开始发起Get请求,token为:{},TOKEN);Assert。notNull(handle);StringurlBASEURLxxxhandle;try{StringressendGet(url,TOKEN);returnnewResultString()。ok(res);}catch(Exceptione){e。printStackTrace();returnnewResultString()。error(请求失败!);}}
模拟Post请求:模拟POST请求请求地址:http:localhost:8080identitygetDetailpostPostMapping(getDetailpost)publicResultStringgetDataPost(RequestBodyHashMapString,ObjectrequestBody){StringurlBASEURL;try{StringressendPost(url,requestBody,TOKEN);;returnnewResultString()。ok(res);}catch(Exceptione){e。printStackTrace();returnnewResultString()。error(请求失败!);}}
整体代码:packagecom。example。demo;importcom。google。gson。Gson;importcom。google。gson。internal。LinkedTreeMap;importcom。google。gson。reflect。TypeToken;importlombok。extern。slf4j。Slf4j;importorg。springframework。util。Assert;importorg。springframework。web。bind。annotation。;importjava。lang。reflect。Type;importjava。util。HashMap;importjava。util。Map;importstaticcom。example。demo。HttpClientUtils。sendGet;importstaticcom。example。demo。HttpClientUtils。sendPost;authorxhDate2022914RestControllerRequestMapping(identity)Slf4jpublicclassApiController{publicstaticStringTOKEN;publicstaticfinalStringBASEURLhttp:;publicstaticfinalStringUSERNAME;publicstaticfinalStringPASSWORD;静态代码块static{1。调用获取token接口StringurlBASEURLidentitytoken;MapString,ObjectjsonMapnewHashMap();jsonMap。put(username,USERNAME);jsonMap。put(password,PASSWORD);StringressendPost(url,jsonMap,null);2。解析JSON,得到tokenGsongsonnewGson();TypemapTypenewTypeTokenHashMapString,Object(){}。getType();HashMapString,ObjectresMapgson。fromJson(res,mapType);LinkedTreeMapdata(LinkedTreeMap)resMap。get(data);TOKEN(String)data。get(token);log。info(token获取成功:{},TOKEN);}Get请求请求地址:http:localhost:8080identitygetDetailget?handlereturnGetMapping(getDetailget)publicResultStringgetDataGet(RequestParamStringhandle){log。info(开始发起Get请求,token为:{},TOKEN);Assert。notNull(handle);StringurlBASEURLhandle;try{StringressendGet(url,TOKEN);returnnewResultString()。ok(res);}catch(Exceptione){e。printStackTrace();returnnewResultString()。error(请求失败!);}}模拟POST请求请求地址:http:localhost:8080identitygetDetailpostPostMapping(getDetailpost)publicResultStringgetDataPost(RequestBodyHashMapString,ObjectrequestBody){StringurlBASEURL;try{StringressendPost(url,requestBody,TOKEN);;returnnewResultString()。ok(res);}catch(Exceptione){e。printStackTrace();returnnewResultString()。error(请求失败!);}}}优化:
模拟场景:在尽可能的不破坏源代码的情况下,不喜勿喷优化一:属性通过配置文件读取
新建application。yml文件api:baseUrl:http:username:password:
新建配置文件读取类:
ApiConfig。javapackagecom。example。demo;importlombok。Data;importorg。springframework。boot。context。properties。ConfigurationProperties;importorg。springframework。stereotype。Component;authorxhDate2022914ComponentConfigurationProperties(prefixapi)DatapublicclassApiConfig{API地址privateStringbaseUrl;代理用户名privateStringusername;代理密码privateStringpassword;}
ApiController进行微调:AutowiredApiConfigapiConfig;publicstaticStringTOKEN;publicstaticStringBASEURL;publicstaticStringUSERNAME;publicstaticStringPASSWORD;PostConstructprivatevoidgetBaseInfo(){BASEURLapiConfig。getBaseUrl();USERNAMEapiConfig。getUsername();PASSWORDapiConfig。getPassword();}privateStringgetToken(){if(!StringUtils。isEmpty(TOKEN)){returnTOKEN;}1。调用获取token接口StringurlBASEURLtoken;MapString,ObjectjsonMapnewHashMap();jsonMap。put(username,USERNAME);jsonMap。put(password,PASSWORD);StringressendPost(url,jsonMap,null);2。解析JSON,得到tokenGsongsonnewGson();TypemapTypenewTypeTokenHashMapString,Object(){}。getType();HashMapString,ObjectresMapgson。fromJson(res,mapType);LinkedTreeMapdata(LinkedTreeMap)resMap。get(data);TOKEN(String)data。get(token);log。info(token获取成功:{},TOKEN);returnTOKEN;}
由于Token会存在过期时间,所以我们这里引用Redis引入依赖:!整合redisdependencygroupIdorg。springframework。bootgroupIdspringbootstarterdataredisartifactIddependency在application。yml添加redis配置:spring:redis配置redis:地址host:xxxxx端口,默认为xxxport:xxxx数据库索引(db0,db1,db2。。。不同业务可以放在不同数据库中)database:0密码password:xxxx注入RedisTemplate,并优化AutowiredRedisTemplateString,StringredisTemplate;privateStringgetToken(){ValueOperationsString,StringoperationsredisTemplate。opsForValue();0。查询Redisif(!StringUtils。isEmpty(operations。get(token))){returnoperations。get(token);}1。调用获取token接口StringurlBASEURLtoken;MapString,ObjectjsonMapnewHashMap();jsonMap。put(username,USERNAME);jsonMap。put(password,PASSWORD);StringressendPost(url,jsonMap,null);2。解析JSON,得到tokenGsongsonnewGson();TypemapTypenewTypeTokenHashMapString,Object(){}。getType();HashMapString,ObjectresMapgson。fromJson(res,mapType);LinkedTreeMapdata(LinkedTreeMap)resMap。get(data);Stringtoken(String)data。get(token);设置TOKEN6小时过期operations。set(token,token,6,TimeUnit。HOURS);log。info(token获取成功:{},token);returntoken;}进一步优化
场景:如果有大量请求同时访问一个正好过期的缓存数据,可能会出现缓存击穿,所以我们的解决方案是添加分布式锁加入依赖:!整合redis开始!原生redissondependencygroupIdorg。redissongroupIdredissonartifactIdversion3。11。0versiondependency!操作redisTemplatedependencygroupIdorg。springframework。bootgroupIdspringbootstarterdataredisartifactIddependency!整合redis结束创建RedissionConfig。java配置RedissionClientpackagecom。example。demo;importorg。redisson。Redisson;importorg。redisson。api。RedissonClient;importorg。redisson。config。Config;importorg。springframework。context。annotation。Bean;importorg。springframework。stereotype。Component;authorxhDate2022914ComponentpublicclassRedissionConfig{所有对redisson的使用都是通过RedissonClient对象Bean(destroyMethodshutdown)publicRedissonClientredisson(){创建配置ConfigconfignewConfig();可以用rediss:来启用SSL连接,useSingleServer表示单例模式config。useSingleServer()。setAddress(redis:xxxx:xxxx)。setDatabase(0)。setPassword(xxxx);根据config创建出RedissonClient实例returnRedisson。create(config);}}注入并编写读锁、写锁:AutowiredRedisTemplateString,StringredisTemplate;AutowiredRedissonClientredisson;privateStringgetToken(){0。查询RedisStringtokenreadToken();if(!StringUtils。isEmpty(token)){returntoken;}1。调用获取token接口StringurlBASEURLxxxtoken;MapString,ObjectjsonMapnewHashMap();jsonMap。put(username,USERNAME);jsonMap。put(password,PASSWORD);StringressendPost(url,jsonMap,null);2。解析JSON,得到tokenGsongsonnewGson();TypemapTypenewTypeTokenHashMapString,Object(){}。getType();HashMapString,ObjectresMapgson。fromJson(res,mapType);LinkedTreeMapdata(LinkedTreeMap)resMap。get(data);token(String)data。get(token);设置TOKENAssert。isTrue(setToken(token));log。info(token获取成功:{},token);returntoken;}privateStringreadToken(){RReadWriteLocklockredisson。getReadWriteLock(tokenlock);RLockrLocklock。readLock();Stringtoken;try{加读锁rLock。lock();tokenredisTemplate。opsForValue()。get(token);}catch(Exceptione){e。printStackTrace();}finally{rLock。unlock();}returntoken;}privatebooleansetToken(Stringtoken){RReadWriteLocklockredisson。getReadWriteLock(tokenlock);RLockrLocklock。writeLock();try{改数据加写锁,读数据加读锁rLock。lock();redisTemplate。opsForValue()。set(token,token,6,TimeUnit。HOURS);}catch(Exceptione){e。printStackTrace();}finally{rLock。unlock();}returntrue;}再次优化:
互联网系统经常会遇到高并发大流量的请求,在突发情况下(如秒杀、抢购),瞬间大流量会直接把系统打垮,为了防止出现这种情况最常见的解决方案之一就是限流,当请求达到一定的并发数或速率,就进行等待、排队、降级、拒绝服务等。
基于Guava工具类【令牌桶算法】,借助自定义注解AOP实现接口限流
令牌桶算法的原理也比较简单:系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。单机模式模拟:添加依赖:!Guava:限流工具类RateLimiterdependencygroupIdcom。google。guavagroupIdguavaartifactIdversion30。1jreversiondependency!加入AOP依赖dependencygroupIdorg。springframework。bootgroupIdspringbootstarteraopartifactIddependency自定义限流注解:
Limit。javapackagecom。example。demo;importjava。lang。annotation。;importjava。util。concurrent。TimeUnit;authorxhDate2022915Retention(RetentionPolicy。RUNTIME)Target({ElementType。METHOD})DocumentedpublicinterfaceLimit{资源的key,唯一作用:不同的接口,不同的流量控制Stringkey()default;最多的访问限制次数doublepermitsPerSecond();获取令牌最大等待时间longtimeout();获取令牌最大等待时间,单位(例:分钟秒毫秒)默认:毫秒TimeUnittimeunit()defaultTimeUnit。MILLISECONDS;得不到令牌的提示语Stringmsg()default系统繁忙,请稍后再试。;}使用AOP切面拦截限流注解packagecom。example。demo;importcom。google。common。collect。Maps;importcom。google。common。util。concurrent。RateLimiter;importlombok。extern。slf4j。Slf4j;importorg。aspectj。lang。ProceedingJoinPoint;importorg。aspectj。lang。annotation。Around;importorg。aspectj。lang。annotation。Aspect;importorg。aspectj。lang。reflect。MethodSignature;importorg。springframework。stereotype。Component;importorg。springframework。web。bind。annotation。ResponseBody;importorg。springframework。web。context。request。RequestContextHolder;importorg。springframework。web。context。request。ServletRequestAttributes;importjavax。servlet。http。HttpServletResponse;importjava。io。IOException;importjava。io。PrintWriter;importjava。lang。reflect。Method;importjava。util。HashMap;importjava。util。Map;authorxhDate2022915Slf4jAspectComponentpublicclassLimitAop{不同的接口,不同的流量控制map的key为Limiter。keyprivatefinalMapString,RateLimiterlimitMapMaps。newConcurrentMap();Around(annotation(com。example。demo。Limit))publicObjectaround(ProceedingJoinPointjoinPoint)throwsThrowable{MethodSignaturesignature(MethodSignature)joinPoint。getSignature();Methodmethodsignature。getMethod();拿limit的注解Limitlimitmethod。getAnnotation(Limit。class);if(limit!null){key作用:不同的接口,不同的流量控制Stringkeylimit。key();RateLimiterrateLimiternull;验证缓存是否有命中keyif(!limitMap。containsKey(key)){创建令牌桶rateLimiterRateLimiter。create(limit。permitsPerSecond());limitMap。put(key,rateLimiter);log。info(新建了令牌桶{},容量{},key,limit。permitsPerSecond());}rateLimiterlimitMap。get(key);拿令牌booleanacquirerateLimiter。tryAcquire(limit。timeout(),limit。timeunit());拿不到命令,直接返回异常提示if(!acquire){log。debug(令牌桶{},获取令牌失败,key);this。responseFail(limit。msg());returnnull;}}returnjoinPoint。proceed();}直接向前端抛出异常parammsg提示信息privatevoidresponseFail(Stringmsg){HttpServletResponseresp((ServletRequestAttributes)RequestContextHolder。getRequestAttributes())。getResponse();resp。setCharacterEncoding(utf8);resp。setContentType(applicationjson;charsetutf8);PrintWriterwriternull;try{writerresp。getWriter();}catch(IOExceptione){e。printStackTrace();}writer。write(newResultString()。error(msg)。toString());}}给需要限流的接口加上注解Get请求returnGetMapping(getDetailget)Limit(keylimit1,permitsPerSecond1,timeout1000,timeunitTimeUnit。MILLISECONDS,msg当前排队人数较多,请稍后再试!)publicResultStringgetDataGet(RequestParamStringhandle){log。info(开始发起Get请求,token为:{},getToken());Assert。notNull(handle);StringurlBASEURLhandle;try{StringressendGet(url,getToken());returnnewResultString()。ok(res);}catch(Exceptione){e。printStackTrace();returnnewResultString()。error(请求失败!);}}
多次请求时:
来源:blog。csdn。netm051517236articledetails126855104