摘要
在SpringBoot接口開發(fā)中,存在著本模塊的代碼需要訪問外面模塊接口或外部url鏈接的需求, 比如調用外部的地圖API或者天氣API。那么有哪些方式可以調用外部接口呢?本博文將介紹SpringBoot常見的訪問外部接口方式。幫助大家更好的使用SpringBoot訪問外部接口。
一、需要訪問外部接口背景
調用其它模塊的API,或者其它三方服務,比如調用外部的地圖API或者天氣API等。
二、訪問外部接口的常見方案
2.1 采用原生的Http請求
在代碼中采用原生的http請求,代碼參考如下:
@RequestMapping("/doPostGetJson")
public String doPostGetJson() throws ParseException {
//此處將要發(fā)送的數據轉換為json格式字符串
String jsonText = "{id: 1}";
JSONObject json = (JSONObject) JSONObject.parse(jsonText);
JSONObject sr = this.doPost(json);
System.out.println("返回參數: " + sr);
return sr.toString();
}
public static JSONObject doPost(JSONObject date) {
HttpClient client = HttpClients.createDefault();
// 要調用的接口方法
String url = "http://192.168.1.101:8080/getJson";
HttpPost post = new HttpPost(url);
JSONObject jsonObject = null;
try {
StringEntity s = new StringEntity(date.toString());
s.setContentEncoding("UTF-8");
s.setContentType("application/json");
post.setEntity(s);
post.addHeader("content-type", "text/xml");
HttpResponse res = client.execute(post);
String response1 = EntityUtils.toString(res.getEntity());
System.out.println(response1);
if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
String result = EntityUtils.toString(res.getEntity());// 返回json格式:
jsonObject = JSONObject.parseObject(result);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return jsonObject;
}
2.2? 采用Feign進行消費(PRC通信)
在maven項目中添加依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
<version>1.2.2.RELEASE</version>
</dependency>
編寫接口,放置在service層。這里的decisionEngine.url 是配置在properties中的 是ip地址和端口號 decisionEngine.url=http://10.2.1.148:3333/decision/person 是接口名字。
@FeignClient(url = "${decisionEngine.url}",name="engine")
public interface DecisionEngineService {
@RequestMapping(value="/decision/person",method= RequestMethod.POST)
public JSONObject getEngineMesasge(@RequestParam("uid") String uid,@RequestParam("productCode") String productCode);
}
在Java的啟動類上加上@EnableFeignClients
@EnableFeignClients //參見此處
@EnableDiscoveryClient
@SpringBootApplication
@EnableResourceServer
public class Application implements CommandLineRunner {
private static final Logger LOGGER = LoggerFactory.getLogger(Application.class);
@Autowired
private AppMetricsExporter appMetricsExporter;
@Autowired
private AddMonitorUnitService addMonitorUnitService;
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
}
在代碼中調用接口即可
@Autowired
private DecisionEngineService decisionEngineService ;
// ...
decisionEngineService.getEngineMesasge("uid" , "productCode");
2.3 采用RestTemplate方法
在Spring-Boot開發(fā)中,RestTemplate同樣提供了對外訪問的接口API,這里主要介紹Get和Post方法的使用。Get請求提供了兩種方式的接口getForObject 和 getForEntity,getForEntity提供如下三種方法的實現(xiàn)。
2.3.1 Get請求之——getForEntity(Stringurl,Class responseType,Object…urlVariables)
該方法提供了三個參數,其中url為請求的地址,responseType為請求響應body的包裝類型,urlVariables為url中的參數綁定,該方法的參考調用如下:
// http://USER-SERVICE/user?name={name)
RestTemplate restTemplate=new RestTemplate();
Map<String,String> params=new HashMap<>();
params.put("name","dada"); //
ResponseEntity<String> responseEntity=restTemplate.getForEntity("http://USERSERVICE/user?name={name}",String.class,params);
2.3.2 Get請求之——getForEntity(URI url,Class responseType)
該方法使用URI對象來替代之前的url和urlVariables參數來指定訪問地址和參數綁定。URI是JDK java.net包下的一個類,表示一個統(tǒng)一資源標識符(Uniform Resource Identifier)引用。參考如下:
RestTemplate restTemplate=new RestTemplate();
UriComponents uriComponents=UriComponentsBuilder.fromUriString("http://USER-SERVICE/user?name={name}")
.build()
.expand("dodo")
.encode();
URI uri=uriComponents.toUri();
ResponseEntity<String> responseEntity=restTemplate.getForEntity(uri,String.class).getBody();
2.3.3 Get請求之——getForObject
getForObject方法可以理解為對getForEntity的進一步封裝,它通過HttpMessageConverterExtractor對HTTP的請求響應體body內容進行對象轉換,實現(xiàn)請求直接返回包裝好的對象內容。getForObject方法有如下:
getForObject(String url,Class responseType,Object...urlVariables)
getForObject(String url,Class responseType,Map urlVariables)
getForObject(URI url,Class responseType)
2.3.4 Post 請求
Post請求提供有三種方法,postForEntity、postForObject和postForLocation。其中每種方法都存在三種方法,postForEntity方法使用如下:
RestTemplate restTemplate=new RestTemplate();
User user=newUser("didi",30);
ResponseEntity<String> responseEntity=restTemplate.postForEntity("http://USER-SERVICE/user",user,String.class); //提交的body內容為user對象,請求的返回的body類型為String
String body=responseEntity.getBody();
postForEntity存在如下三種方法的重載
postForEntity(String url,Object request,Class responseType,Object... uriVariables)
postForEntity(String url,Object request,Class responseType,Map uriVariables)
postForEntity(URI url,Object request,Class responseType)
postForEntity中的其它參數和getForEntity的參數大體相同在此不做介紹。
2.4 在接口調用中需要注意什么?
需要注意兩點:
- 需要設置超時時間
- 需要自行處理異常
三、SpringBoot保證接口冪等
3.1 什么是冪等
當我們討論接口的冪等性時一般是在說:以相同的請求調用這個接口一次和調用這個接口多次,對系統(tǒng)產生的影響是相同的。如果一個接口滿足這個特性,那么我們就說這個 接口是一個冪等接口。
- 接口冪等和防止重復提交是一回事嗎?
嚴格來說,并不是。
- 冪等: 更多的是在重復請求已經發(fā)生,或是無法避免的情況下,采取一定的技術手段讓這些重復請求不給系統(tǒng)帶來副作用。
- 防止重復: 提交更多的是不讓用戶發(fā)起多次一樣的請求。比如說用戶在線購物下單時點了提交訂單按鈕,但是由于網絡原因響應很慢,此時用戶比較心急多次點擊了訂單提交按鈕。 這種情況下就可能會造成多次下單。一般防止重復提交的方案有:將訂單按鈕置灰,跳轉到結果頁等。主要還是從客戶端的角度來解決這個問題。
- 哪些情況下客戶端是防止不了重復提交的?
雖然我們可在客戶端做一些防止接口重復提交的事(比如將訂單按鈕置灰,跳轉到結果頁等), 但是如下情況依然客戶端是很難控制接口重復提交到后臺的,這也進一步表明了接口冪等和防止重復提交不是一回事以及后端接口保證接口冪等的必要性所在。
- 接口超時重試:接口可能會因為某些原因而調用失敗,出于容錯性考慮會加上失敗重試的機制。如果接口調用一半,再次調用就會因為臟數據的存在而出現(xiàn)異常。
- 消息重復消費:在使用消息中間件來處理消息隊列,且手動ack確認消息被正常消費時。如果消費者突然斷開連接,那么已經執(zhí)行了一半的消息會重新放回隊列。被其他消費者重新消費時就會導致結果異常,如數據庫重復數據,數據庫數據沖突,資源重復等。
- 請求重發(fā):網絡抖動引發(fā)的nginx重發(fā)請求,造成重復調用;
3.2 什么是接口冪等?
在HTTP/1.1中,對冪等性進行了定義。它描述了一次和多次請求某一個資源對于資源本身應該具有同樣的結果(網絡超時等問題除外),即第一次請求的時候對資源產生了副作用,但是以后的多次請求都不會再對資源產生副作用。
這里的副作用是不會對結果產生破壞或者產生不可預料的結果。也就是說,其任意多次執(zhí)行對資源本身所產生的影響均與一次執(zhí)行的影響相同。
- 對哪些類型的接口需要保證接口冪等?
我們看下標準的restful請求,冪等情況是怎么樣的:
-
SELECT查詢操作
- GET:只是獲取資源,對資源本身沒有任何副作用,天然的冪等性。
- HEAD:本質上和GET一樣,獲取頭信息,主要是探活的作用,具有冪等性。
- OPTIONS:獲取當前URL所支持的方法,因此也是具有冪等性的。
-
DELETE刪除操作
- 刪除的操作,如果從刪除的一次和刪除多次的角度看,數據并不會變化,這個角度看它是冪等的
- 但是如果,從另外一個角度,刪除數據一般是返回受影響的行數,刪除一次和多次刪除返回的受影響行數是不一樣的,所以從這個角度它需要保證冪等。(折中而言DELETE操作通常也會被納入保證接口冪等的要求)
-
ADD/EDIT操作
- PUT:用于更新資源,有副作用,但是它應該滿足冪等性,比如根據id更新數據,調用多次和N次的作用是相同的(根據業(yè)務需求而變)。
- POST:用于添加資源,多次提交很可能產生副作用,比如訂單提交,多次提交很可能產生多筆訂單。
3.3 保證冪等的方式
3.3.1 數據庫層面
典型的數據庫悲觀鎖:for update
select * from t_order where order_id = trade_no for update;
為什么加for update就可以?
- 當線程A執(zhí)行for update,數據會對當前記錄加鎖,其他線程執(zhí)行到此行代碼的時候,會等待線程A釋放鎖之后,才可以獲取鎖,繼續(xù)后續(xù)操作。
- 事物提交時,for update獲取的鎖會自動釋放。
PS:這種方式很少被使用,因為如果業(yè)務處理比較耗時,并發(fā)情況下,后面線程會長期處于等待狀態(tài),占用了很多線程,讓這些線程處于無效等待狀態(tài),我們的web服務中的線程數量一般都是有限的,如果大量線程由于獲取for update鎖處于等待狀態(tài),不利于系統(tǒng)并發(fā)操作。
3.3.2 唯一ID/索引
針對的是插入操作。數據庫唯一主鍵的實現(xiàn)主要是利用數據庫中主鍵唯一約束的特性,一般來說唯一主鍵比較適用于“插入”時的冪等性,其能保證一張表中只能存在一條帶該唯一主鍵的記錄。
使用數據庫唯一主鍵完成冪等性時需要注意的是,該主鍵一般來說并不是使用數據庫中自增主鍵,而是使用分布式 ID 充當主鍵,這樣才能能保證在分布式環(huán)境下 ID 的全局唯一性。
- 去重表
去重表本質上也是一種唯一索引方案。
這種方法適用于在業(yè)務中有唯一標的插入場景中,比如在以上的支付場景中,如果一個訂單只會支付一次,所以訂單ID可以作為唯一標識。這時,我們就可以建一張去重表,并且把唯一標識作為唯一索引,在我們實現(xiàn)時,把創(chuàng)建支付單據和寫入去去重表,放在一個事務中,如果重復創(chuàng)建,數據庫會拋出唯一約束異常,操作就會回滾。
3.3.3 樂觀鎖(基于版本號或者時間戳)
針對更新操作。
- 使用版本號或者時間戳
這種方法適合在更新的場景中,比如我們要更新商品的名字,這時我們就可以在更新的接口中增加一個版本號,來做冪等
boolean updateGoodsName(int id,String newName,int version);
在實現(xiàn)時可以如下
update goods set name=#{newName},version=#{version} where id=#{id} and version<${version}
- 狀態(tài)機
本質上也是樂觀鎖,這種方法適合在有狀態(tài)機流轉的情況下,比如就會訂單的創(chuàng)建和付款,訂單的付款肯定是在之前,這時我們可以通過在設計狀態(tài)字段時,使用int類型,并且通過值類型的大小來做冪等,比如訂單的創(chuàng)建為0,付款成功為100。付款失敗為99
在做狀態(tài)機更新時,我們就這可以這樣控制文章來源:http://www.zghlxwxcb.cn/news/detail-733098.html
update `order` set status=#{status} where id=#{id} and status<#{status}
3.3.4 分布式鎖
分布式鎖實現(xiàn)冪等性的邏輯是,在每次執(zhí)行方法之前判斷,是否可以獲取到分布式鎖,如果可以,則表示為第一次執(zhí)行方法,否則直接舍棄請求即可。需要注意的是分布式鎖的key必須為業(yè)務的唯一標識,通常用redis分布式鎖或者zookeeper來實現(xiàn)分布式鎖。文章來源地址http://www.zghlxwxcb.cn/news/detail-733098.html
博文參考
到了這里,關于SpringBoot——SpringBoot訪問外部接口的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!