เราสามารถพัฒนาอินเทอร์เฟซ REST อย่างรวดเร็วผ่าน Spring Boot และเราอาจต้องเรียกอินเทอร์เฟซ REST ภายในและภายนอกผ่าน Spring Boot เพื่อให้ตรรกะทางธุรกิจเสร็จสมบูรณ์ในระหว่างการใช้งานอินเทอร์เฟซ
ใน Spring Boot มีสองวิธีทั่วไปในการเรียก REST API ซึ่งใช้ในการใช้การโทรผ่านบริการผ่าน RestTemplate ในตัวหรือพัฒนาเครื่องมือไคลเอนต์ HTTP ของคุณเอง
RestTemplate มีฟังก์ชั่นพื้นฐานที่ทรงพลังมาก แต่ในสถานการณ์พิเศษบางอย่างเราอาจคุ้นเคยกับการใช้คลาสเครื่องมือห่อหุ้มของเราเองเช่นการอัปโหลดไฟล์ไปยังระบบไฟล์แบบกระจายการประมวลผลคำขอ HTTPS พร้อมใบรับรอง ฯลฯ
บทความนี้ใช้ RestTemplate เป็นตัวอย่างในการบันทึกปัญหาและการแก้ปัญหาที่พบในระหว่างการใช้งาน RestTemplate เพื่อเรียกอินเทอร์เฟซ
1. บทนำสู่ RestTemplate
1. RestTemplate คืออะไร
httpClient ที่เราห่อหุ้มตัวเองมักจะมีรหัสเทมเพลตบางอย่างเช่นการสร้างการเชื่อมต่อการสร้างส่วนหัวคำขอและหน่วยคำขอจากนั้นแยกวิเคราะห์ข้อมูลการตอบกลับตามการตอบสนองและปิดการเชื่อมต่อในที่สุด
RestTemplate เป็นการเข้ารหัส HTTPClient อีกครั้งในฤดูใบไม้ผลิซึ่งทำให้กระบวนการเริ่มต้นการร้องขอ HTTP และการตอบสนองการประมวลผลง่ายขึ้นด้วยระดับนามธรรมที่สูงขึ้นลดรหัสแม่แบบผู้บริโภคและสร้างรหัสที่ซ้ำซ้อนน้อยลง
ที่จริงแล้วถ้าคุณคิดเกี่ยวกับคลาส XXXTEMPLATE จำนวนมากภายใต้ Spring Boot พวกเขายังมีวิธีการเทมเพลตที่หลากหลาย แต่ระดับนามธรรมนั้นสูงกว่าและรายละเอียดเพิ่มเติมจะถูกซ่อนไว้
โดยวิธีการที่สปริงคลาวด์มีการเรียกร้องการเรียกร้องให้บริการซึ่งดำเนินการตาม Netflix Feign รวมสปริงคลาวด์ริบบิ้นและสปริงคลาวด์ฮิสทริกและใช้วิธีการกำหนดไคลเอนต์บริการเว็บบริการที่ประกาศ
ในสาระสำคัญการแสร้งก็คือการห่อหุ้มอีกครั้งบนพื้นฐานของ RestTemplate ซึ่งช่วยให้เรากำหนดและใช้คำจำกัดความของอินเทอร์เฟซบริการที่ขึ้นอยู่กับ
2. วิธีการทั่วไปสำหรับ RestTemplate
มีวิธีการร้องขอมากมายสำหรับบริการ REST ทั่วไปเช่น Get, Post, Put, Delete, Head, Aptions ฯลฯ RestTemplate ใช้วิธีที่พบบ่อยที่สุดและสิ่งที่ใช้กันมากที่สุดคือ Get และ Post คุณสามารถอ้างถึงซอร์สโค้ดเมื่อเรียก API นี่คือคำจำกัดความของวิธีการบางอย่าง (รับโพสต์ลบ):
วิธีการ
สาธารณะ <t> t getForObject (url string, class <t> การตอบสนอง, วัตถุ ... urivariables) สาธารณะ <t> responseEntity <t> getForentity (url สตริง, คลาส <t> responsetype, วัตถุ ... urivariables) สาธารณะ <t> t postforobject postforentity (url string, @Nullable Object Request, คลาส <t> Responsetype, Object ... urivariables) โมฆะสาธารณะลบ (url สตริง, วัตถุ ... urivariables) โมฆะสาธารณะลบ (URL URL)
ในเวลาเดียวกันคุณควรให้ความสนใจกับวิธีการแลกเปลี่ยนและดำเนินการอีกสองวิธี
การแลกเปลี่ยนที่เปิดเผยโดย RestTemplate นั้นแตกต่างจากอินเทอร์เฟซอื่น ๆ :
(1) อนุญาตให้ผู้โทรระบุวิธีการร้องขอ HTTP (รับ, โพสต์, ลบ ฯลฯ )
(2) ข้อมูลร่างกายและส่วนหัวสามารถเพิ่มลงในคำขอและเนื้อหาของมันอธิบายโดยพารามิเตอร์ 'httpentity <?> requestentity'
(3) การแลกเปลี่ยนสนับสนุน 'ประเภทที่มีพารามิเตอร์' (เช่นคลาสทั่วไป) เป็นประเภทการส่งคืนและคุณสมบัตินี้อธิบายโดย 'parameterizedTypeReference <t> responseType'
RestTemplate ทั้งหมดได้รับโพสต์และวิธีการอื่น ๆ และการเรียกขั้นสุดท้ายของวิธีการดำเนินการคือวิธีการดำเนินการ การใช้งานภายในของวิธีการกระตุ้นคือการแปลง URI แบบสตริงเป็น java.net.uri จากนั้นวิธีการ doexecute จะถูกเรียก การใช้วิธี doexecute มีดังนี้:
doexecute
/*** ดำเนินการวิธีการที่กำหนดใน URI ที่ให้ไว้ * <p> {@link clienthttprequest} ถูกประมวลผลโดยใช้ {@link requestcallback}; * การตอบกลับด้วย {@link responsextractor} * @param url URL ที่ขยายออกอย่างสมบูรณ์เพื่อเชื่อมต่อกับ * @param วิธีการวิธี HTTP เพื่อดำเนินการ (รับโพสต์ ฯลฯ ) * @param requestcallback วัตถุที่เตรียมคำขอ (สามารถเป็น {@code null}) * @param responsextractor วัตถุที่แยกออกจากการตอบกลับ {@link responsextractor} */ @nullable ป้องกัน <t> t doexecute (uri url, @nullable httpmethod วิธี, @nullable requestCallback RequestCallback, @Nullable ResponsExtractor <t> ResponsExtractor) assert.notnull (วิธีการ "'วิธี' จะต้องไม่เป็นโมฆะ"); clienthttpresponse การตอบสนอง = null; ลอง {clienthttpRequest Request = createRequest (URL, Method); if (requestCallback! = null) {requestCallback.DoWithRequest (คำขอ); } response = request.execute (); HandlerEsponse (URL, วิธีการตอบสนอง); if (responseextractor! = null) {return responsExtractor.extractData (การตอบกลับ); } else {return null; }} catch (iOexception ex) {string resource = url.toString (); string query = url.getRawQuery (); Resource = (Query! = NULL? Resource.SubString (0, Resource.indexof ('?')): ทรัพยากร); โยน ResourceAccessException ใหม่ ("I/O ข้อผิดพลาดบน" + method.name () + "คำขอ/" " + ทรัพยากร +"/":" + ex.getMessage (), ex); } ในที่สุด {ถ้า (ตอบกลับ! = null) {response.close (); -วิธีการ DoExecute ห่อหุ้มวิธีการเทมเพลตเช่นการสร้างการเชื่อมต่อคำขอการประมวลผลและคำตอบการเชื่อมต่อการเชื่อมต่อ ฯลฯ
เมื่อคนส่วนใหญ่เห็นสิ่งนี้พวกเขาอาจคิดว่าการห่อหุ้มส่วนที่เหลือเป็นแบบนี้ใช่ไหม?
3. โทรง่าย ๆ
โทรโพสต์เป็นตัวอย่าง:
GoodsServiceClient
แพ็คเกจ com.power.demo.restclient; นำเข้า com.power.demo.common.appconst; นำเข้า com.power.demo.restclient.clientrequest.clientgetgoodsbygoodsidrequest; นำเข้า com.power.demo.rientclient.clientrientrientrientrient org.springframework.beans.factory.annotation.autowired; นำเข้า org.springframework.beans.factory.annotation.value; นำเข้า org.springframework.stereotype.Component; นำเข้า org.springframework.web. **/ @ComponentPublic Class GoodsServiceClient {// URL อินเทอร์เฟซที่เรียกโดยผู้บริโภคบริการมีดังนี้: http: // localhost: 9090 @value ("$ {Spring.power.serviceurl}") สตริงส่วนตัว _serviceurl; @AutoWired RestTemplate RestTemplate; Public ClientGetGoodsByGoodSidResponse getGoodsByGoodSID (ClientGetGoodSbyGoodSidRequest) {String SVCURL = getGoodSSVCURL () + "/getInfobyId"; ClientGetGoodSbyGoodSidResponse การตอบสนอง = NULL; ลอง {response = restTemplate.postForObject (svcurl, คำขอ, clientgetGoodsByGoodSidResponse.class); } catch (exception e) {e.printstacktrace (); การตอบสนอง = ใหม่ ClientGetGoodSbyGoodSidResponse (); Response.setCode (AppConst.Fail); Response.SetMessage (e.toString ()); } ตอบกลับการตอบกลับ; } สตริงส่วนตัว getGoodsSvCurl () {string url = ""; if (_serviceUrl == null) {_ServiceUrl = ""; } if (_serviceUrl.Length () == 0) {return url; } if (_serviceurl.substring (_serviceurl.length () - 1, _serviceurl.length ()) == "/") {url = string.format ("%sapi/v1/สินค้า", _serviceurl); } else {url = string.format ("%s/api/v1/สินค้า", _serviceurl); } return url; -ในการสาธิตวิธีการ resttemplate.postforobject เรียกโดยตรงและเอนทิตี deserialization จะถูกแปลงเป็น resttemplate เหล่านี้การห่อหุ้มภายใน
2. สรุปปัญหา
1. ไม่พบ httpmessageconverter ที่เหมาะสมสำหรับข้อยกเว้นประเภทคำขอ
ปัญหานี้มักจะเกิดขึ้นเมื่อวัตถุที่เข้ามาถูกส่งผ่านใน postforobject สำหรับการโทร
วิเคราะห์ซอร์สโค้ด RestTemplate ในวิธีการ Dowithrequest ของคลาส HttpentityRequestCallback หาก MessageConverters (ฟิลด์นี้จะยังคงกล่าวถึงในภายหลัง) จะไม่ตรงตามตรรกะที่ส่งคืน (นั่นคือไม่มีการจับคู่ HTTPMessageConverter) ข้อยกเว้นข้างต้น
httpentityrequestcallback.dowithrequest
@Override @suppresswarnings ("ไม่ได้ตรวจสอบ") โมฆะสาธารณะ dowithrequest (clienthttprequest httprequest) โยน ioexception {super.dowithrequest (httprequest); Object RequestBody = this.requestEntity.getBody (); if (requestbody == null) {httpheaders httpheaders = httprequest.getheaders (); httpheaders requestheaders = this.requestentity.getheaders (); if (! requestheaders.isempty ()) {สำหรับ (map.entry <string, list <String>> รายการ: requestheaders.entryset ()) {httpheaders.put (entry.getKey (), LinkedList ใหม่ <> (entry.getValue ())); }} if (httpheaders.getContentLength () <0) {httpheaders.setContentLength (0L); }} else {class <?> requestbodyclass = requestbody.getClass (); พิมพ์ requestbodyType = (this.requestEntity อินสแตนซ์ของการร้องขอความต้องการ? ((ขอให้ <?>) this.requestEntity) .getType (): requestbodyclass); httpheaders httpheaders = httprequest.getheaders (); httpheaders requestheaders = this.requestentity.getheaders (); mediaType requestContentType = requestheaders.getContentType (); สำหรับ (httpmessageConverter <?> messageConverter: getMessageConverters ()) {ถ้า (messageConverter อินสแตนซ์ของ generichttpmessageconverter) {generichttpmessageconverter if (genericConverter.canWrite (requestbodyType, requestbodyclass, requestcontentType)) {ถ้า (! requestheaders.isempty ()) {สำหรับ (map.entry <string, รายการ <String >> รายการ: requestheaders.entryset ()) {httpheaders.put.put.put. }} if (logger.isdebugenabled ()) {ถ้า (requestcontentType! = null) {logger.debug ("เขียน [" + คำขอ + "] เป็น /" + requestcontenttype + " /" โดยใช้ [" + messageConverter +"] ") MessageConverter + "]"); requestheaders.entryset ()) {httpheaders.put (entry.getKey (), linkedList ใหม่ <> (entry.getValue ())); [" + MessageConverter +"] "); } else {logger.debug ("เขียน [" + requestbody + "]"); }} ((httpMessageConverter <Ojrop>) MessageConverter) .write (RequestBody, RequestContentType, httpRequest); กลับ; }} string message = "ไม่สามารถเขียนคำขอ: ไม่มี httpmessageConverter ที่เหมาะสมสำหรับประเภทคำขอ [" + requestbodyclass.getName () + "]"; if (requestContentType! = null) {message + = "และประเภทเนื้อหา [" + requestContentType + "]"; } โยน RestClientException ใหม่ (ข้อความ); - ทางออกที่ง่ายที่สุดคือการห่อส่วนหัวคำขอ HTTP และทำให้วัตถุคำขอเป็นอนุกรมเป็นสตริงเพื่อส่งพารามิเตอร์ รหัสตัวอย่างอ้างอิงมีดังนี้:
PostforObject
/ * * การโทรโพสต์การเรียก * */ สตริงคงที่สาธารณะ postforObject (restTemplate restTemplate, url สตริง, พารามิเตอร์วัตถุ) {httpheaders ส่วนหัว = httpheaders ใหม่ (); mediaType type = mediaType.parsemediatype ("แอปพลิเคชัน/json; charset = utf-8"); Headers.setContentType (ประเภท); headers.add ("ยอมรับ", mediaType.application_json.toString ()); String json = serializeutil.serialize (params); httpentity <string> formentity = new httpentity <string> (JSON, ส่วนหัว); String result = restTemplate.postForObject (url, formentity, string.class); ผลการกลับมา; -หากเราต้องการส่งคืนวัตถุโดยตรงเราสามารถ deserialize สตริงที่ส่งคืนได้โดยตรง:
PostforObject
/ * * การโทรโพสต์คำขอ * */ สาธารณะคงที่ <t> t postforobject (restTemplate restTemplate, url สตริง, พารามิเตอร์วัตถุ, คลาส <t> clazz) {t response = null; String respstr = postforObject (restTemplate, url, params); การตอบสนอง = serializeutil.deserialize (respstr, clazz); การตอบกลับกลับ; -ในหมู่พวกเขามีเครื่องมือมากมายสำหรับการทำให้เป็นอนุกรมและ deserialization เช่น Fastjson, Jackson และ GSON
2. ไม่พบ httpmessageconverter ที่เหมาะสมสำหรับข้อยกเว้นประเภทการตอบสนอง
เช่นเดียวกับข้อยกเว้นที่เกิดขึ้นเมื่อเริ่มการร้องขอจะมีปัญหาเมื่อจัดการการตอบกลับ
มีคนถามคำถามเดียวกันเกี่ยวกับ Stackoverflow สาเหตุที่แท้จริงคือตัวแปลงข้อความ HTTP HTTPMESSAGECONVERTER ขาดประเภท MIME กล่าวคือเมื่อ HTTP ส่งผลลัพธ์ผลลัพธ์ไปยังไคลเอนต์ไคลเอนต์จะต้องเริ่มต้นแอปพลิเคชันที่เหมาะสมเพื่อประมวลผลเอกสารเอาต์พุต สามารถทำได้ผ่านหลายประเภท MIME (Multifunctional Internet Mail Augmentation Protocol)
สำหรับการตอบกลับฝั่งเซิร์ฟเวอร์ HTTPMESSAGECONVERTER จำนวนมากรองรับประเภทสื่อที่แตกต่างกัน (mimetypes) โดยค่าเริ่มต้น การสนับสนุนเริ่มต้นสำหรับ stringhttpmessageConverter คือ mediaType.text_plain การสนับสนุนเริ่มต้นสำหรับ sourcehttpmessageConverter คือ mediaType.text_xml การสนับสนุนเริ่มต้นสำหรับ formhttpmessageConverter คือ mediaType.Application_Form_urlencoded ในบริการที่เหลือสิ่งที่เราใช้มากที่สุดคือ MappingJackson2httpmessageConverter นี่เป็นตัวแปลงทั่วไปที่ค่อนข้างทั่วไป (สืบทอดมาจากอินเตอร์เฟส GenerichttpMessageConverter) จากการวิเคราะห์ mimetype ที่รองรับโดยค่าเริ่มต้นคือ mediaType.application_json:
MappingJackson2httpmessageConverter
/*** สร้าง {@link mapingjackson2httpmessageConverter} ใหม่ด้วย {@link objectmapper} * คุณสามารถใช้ {@link Jackson2ObjectMapperBuilder} เพื่อสร้างได้อย่างง่ายดาย *@See Jackson2ObjectMapperBuilder#json () */ สาธารณะ mapingjackson2httpmessageConverter (ObjectMapper ObjectMapper) {super (ObjectMapper, MediaType.Application_JSON, MediaType ใหม่ ("แอปพลิเคชัน", " *+Json")); -อย่างไรก็ตามการตอบสนองเริ่มต้นของอินเทอร์เฟซแอปพลิเคชันบางตัว mimetype ไม่ใช่แอปพลิเคชัน/JSON ตัวอย่างเช่นหากเราเรียกอินเทอร์เฟซการพยากรณ์อากาศภายนอกหากเราใช้การกำหนดค่าเริ่มต้นของ RestTemplate มันไม่มีปัญหาในการส่งคืนการตอบกลับสตริงโดยตรง:
string url = "http://wthrcdn.etouch.cn/weather_mini?city=shanghai"; ผลการศึกษา = resttemplate.getForobject (url, string.class); clientweatherresultvo vo = serializeutil.deserialize
อย่างไรก็ตามหากเราต้องการส่งคืนวัตถุเอนทิตีโดยตรง:
String url = "http://wthrcdn.etouch.cn/weather_mini?city=shanghai";clientweatherresultvo weatherresultvo = resttemplate.getForObject (url, clientweatherresultvo.class);
จากนั้นรายงานข้อยกเว้นโดยตรง:
ไม่สามารถแยกการตอบสนอง: ไม่พบ httpmessageconverter ที่เหมาะสมสำหรับประเภทการตอบสนอง [คลาส]
และประเภทเนื้อหา [Application/Octet-Stream]
หลายคนพบปัญหานี้ ส่วนใหญ่ของพวกเขาสับสนเมื่อพวกเขาพบมันครั้งแรก อินเทอร์เฟซจำนวนมากถูกส่งกลับในรูปแบบข้อความ JSON, XML หรือแบบธรรมดา แอปพลิเคชัน/ออคเตตสตรีมคืออะไร?
ตรวจสอบซอร์สโค้ด RestTemplate และติดตามมันไปตลอดทางคุณจะพบว่าวิธีการ ExtractData ของคลาส HTTPMESSAGECONVERTEREXTRACTOR ของคลาสมีการตอบสนองการแยกวิเคราะห์และตรรกะ deserialization หากไม่ประสบความสำเร็จข้อมูลข้อยกเว้นที่ถูกโยนจะเหมือนกับด้านบน:
httpmessageconverterterextractor.extractData
@Override @suppresswarnings ({"ไม่ได้ตรวจสอบ", "Rawtypes", "ทรัพยากร"}) สาธารณะ t extractData (clienthttpresponse การตอบสนอง) พ่น IOException {messageBodyClientHttPresponSewrapper responsewrapper = ใหม่ข้อความ if (! ResponseWrapper.hasmessagebody () || responsewrapper.hasempmessagebody ()) {return null; } mediaType contentType = getContentType (Responsewrapper); ลอง {สำหรับ (httpmessageConverter <?> messageConverter: this.messageConverters) {ถ้า (MessageConverter อินสแตนซ์ของ Generichttpmessageconverter) {generichttpmessageConverterterter <? if (genericMessageConverter.canread (this.responsetype, null, contentType)) {ถ้า (logger.isdebugenabled ()) {logger.debug ("อ่าน [" + this.responsetype + "] เป็น /" " } return (t) GenericMessageConverter.read (this.responsetype, null, responsewrapper); }} if (this.responseclass! = null) {ถ้า (messageConverter.canread (this.responseclass, contentType)) {ถ้า (logger.isdebugenabled ()) {logger.debug ("" MessageConverter + "]"); } return (t) MessageConverter.read ((คลาส) this.responseclass, Responsewrapper); }}}} catch (iOexception | httpmessagenotreadableException ex) {โยน restClientException ใหม่ ("ข้อผิดพลาดในขณะที่แยกการตอบสนองสำหรับประเภท [" + this.responsetype + "] และประเภทเนื้อหา [" + contentType + "]", ex); } โยน RestClientException ใหม่ ("ไม่สามารถแยกการตอบสนอง: ไม่พบ httpmessageConverter ที่เหมาะสมที่พบ" + "สำหรับประเภทการตอบสนอง [" + this.responsetype + "] และประเภทเนื้อหา [" + contentType + "]"); - รหัสตัวอย่างของโซลูชันบน stackoverflow เป็นที่ยอมรับ แต่ไม่ถูกต้อง ควรเพิ่ม mimetypes ทั่วไป ฉันจะโพสต์รหัสที่ฉันคิดว่าถูกต้อง:
Resttemplateconfig
แพ็คเกจ com.power.demo.restclient.config; นำเข้า com.fasterxml.jackson.databind.objectmapper; นำเข้า com.google.common.collect.lists; นำเข้า org.springframework.beans.factory.annotation.autowired; org.springframework.boot.web.client.resttemplatebuilder นำเข้า org.springframework.context.annotation.bean; นำเข้า org.springframework.http.mediatype; นำเข้า org.springframework.httt org.springframework.http.converter.cbor.mappingjackson2cborhttpmessageconverter; นำเข้า org.springframework.http.converter.feed.atomfeedhttpmessageConverter; org.springframework.http.converter.feed.rsschannelhttpmessageConverter; นำเข้า org.springframework.http.converter.json.gsonhttpmessageConverter; org.springframework.http.converter.json.jsonbhttpmessageConverter; นำเข้า org.springframework.http.converter.json.mappingjackson2httpmessageConverter; org.springframework.http.converter.smile.mappingjackson2smilehttpmessageconverter; นำเข้า org.springframework.http.converter.support.allencompassingformhttpmessageConverter; org.springframework.http.converter.xml.jaxb2rootelementhttpmessageConverter; นำเข้า org.springframework.http.converter.xml.mappingjackson2xmlhttpmessageconverter org.springframework.http.converter.xml.sourcehttpmessageConverter; นำเข้า org.springframework.stereotype.component; นำเข้า org.springframework.util.classutils; java.util.list; @componentpublic คลาส resttemplateconfig {บูลีนสุดท้ายคงที่ romepresent = classutils.ispresent ("com.rometools.rome.feed.wirefeed", resttemplate. บูลีนสุดท้ายคงสุดท้าย jaxb2present = classutils.ispresent ("javax.xml.bind.binder", resttemplate.class.getclassloader ()); Boolean สุดท้ายคงที่ boolean Jackson2Present = classutils.ispresent ("com.fasterxml.jackson.databind.objectmapper", resttemplate.class.getclassloader ()) && classutils.ispresent ("com.fasterxml.jackson.core.core BOOLEAN สุดท้ายแบบคงที่ BOOLEAN JACKSON2XMLPRESENT = classUtils.ispresent ("com.fasterxml.jackson.dataformat.xml.xmlmapper", resttemplate.class.getclassloader ()); BOOLEAN BOOLEAN ครั้งสุดท้ายส่วนตัว Jackson2SmilePresent = classUtils.ispresent ("com.fasterxml.jackson.dataformat.smile.smilefactory", resttemplate.class.getclassloader ()); BOOLEAN สุดท้ายคงที่ BOOLEAN JACKSON2CBORPRESENT = classUtils.ispresent ("com.fasterxml.jackson.dataformat.cbor.cborfactory", resttemplate.class.getclassloader ()); BOOLEAN BOOLEAN ครั้งสุดท้ายส่วนตัว GOSPRESENT = classUtils.ispresent ("com.google.gson.gson", resttemplate.class.getclassloader ()); Boolean สุดท้ายคงที่ jsonBpresent = classutils.ispresent ("javax.json.bind.jsonb", resttemplate.class.getclassloader ()); // หมายเหตุเมื่อเริ่มต้นว่าตั้งแต่เราฉีด RestTemplate ลงในบริการเราจำเป็นต้องยกตัวอย่างอินสแตนซ์ของคลาสนี้เมื่อเริ่มต้น @Autowired Private RestTemplateBuilder Builder; @autowired ObjectMapper ObjectMapper; // ใช้ RestTemplateBuilder เพื่อยกตัวอย่างวัตถุ RestTemplate ฤดูใบไม้ผลิได้ฉีดอินสแตนซ์ RestTemplateBuilder โดยค่าเริ่มต้น @Bean RestTemplate RestTemplate () {RESTTEMPLATE RESTTEMPLATE = BUILDER.BUILD (); รายการ <httpmessageConverter <? >> messageConverters = lists.newarraylist (); MappingJackson2httpmessageConverter Converter = ใหม่ mapingjackson2httpmessageConverter (); Converter.SetObjectMapper (ObjectMapper); // ข้อยกเว้นจะปรากฏขึ้นโดยไม่ต้องเพิ่ม // ไม่สามารถแยกการตอบสนอง: ไม่มี httpmessageConverter ที่เหมาะสมที่พบสำหรับประเภทการตอบสนอง [คลาส] mediaType [] mediaTypes = ใหม่ mediaType [] {mediaType.application_json, mediaType.application_octet_stream mediaType.text_plain, mediaType.text_xml, mediaType.application_stream_json, mediaType.application_atom_xml, mediaType.application_form_urlencoded, mediaType.application_pdf,}; Converter.setsupportedmediatypes (array.aslist (mediaTypes)); //messageconverters.add(Converter); if (jackson2present) {messageConverters.add (ตัวแปลง); } อื่นถ้า (gsonpresent) {messageConverters.add (ใหม่ gsonhttpmessageConverter ()); } อื่นถ้า (jsonBpresent) {messageConverters.add (ใหม่ JSONBHTTPMESSAGECONVERTER ()); } messageConverters.add (ใหม่ formhttpmessageConverter ()); MessageConverters.add (ใหม่ StringhttpmessageConverter ()); MessageConverters.add (ใหม่ StringhttpmessageConverter ()); MessageConverters.add (ทรัพยากรใหม่ httpmessageConverter (false)); MessageConverters.add (ใหม่ SourcehttpmessageConverter ()); MessageConverters.add (ใหม่ AllencompassingFormhttpMessageConverter ()); if (romepresent) {messageConverters.add (ใหม่ AtomFeedhttpMessageConverter ()); MessageConverters.add (ใหม่ RSSCHANNELHTTPMESSAGECONVERTER ()); } if (jackson2xmlpresent) {messageConverters.add (ใหม่ mapingjackson2xmlhttpmessageConverter ()); } อื่นถ้า (jaxb2present) {messageConverters.add (ใหม่ jaxb2rootelementhttpmessageConverter ()); } if (jackson2smilepresent) {messageConverters.add (ใหม่ mapingjackson2smilehttpmessageConverter ()); } if (jackson2cborpresent) {messageConverters.add (ใหม่ mapingjackson2cborhttpmessageConverter ()); } RestTemplate.SetMessAgEconverters (MessageConverters); ส่งคืน RestTemplate; -หลังจากเห็นรหัสข้างต้นและเปรียบเทียบการใช้งานภายในของ RestTemplate คุณจะรู้ว่าฉันได้อ้างถึงซอร์สโค้ดของ RestTemplate คนที่หมกมุ่นอยู่กับความสะอาดอาจบอกได้ว่ารหัสชิ้นนี้เป็นคำฟางเล็กน้อย พวงข้างต้นของตัวแปรสุดท้ายคงที่และ MessageConverters เติมวิธีการข้อมูลจะแสดงให้เห็นถึงการใช้งาน RestTemplate หากมีการแก้ไข RestTemplate มันจะได้รับการแก้ไขที่นี่ซึ่งไม่เป็นมิตรมากและไม่ได้ดู OO เลย
หลังการวิเคราะห์แล้ว RestTemplateBuilder.build () สร้างวัตถุ RestTemplate เพียงปรับเปลี่ยน mediaType ที่รองรับด้วย MappingJackson2httpmessageConverter ภายใน แม้ว่าฟิลด์ MessageConverters ของ RestTemplate เป็นส่วนตัวสุดท้าย แต่เรายังสามารถแก้ไขได้ผ่านการสะท้อนกลับ รหัสที่ได้รับการปรับปรุงมีดังนี้:
Resttemplateconfig
แพ็คเกจ com.power.demo.restclient.config; นำเข้า com.fasterxml.jackson.databind.objectmapper; นำเข้า com.google.common.collect.lists; นำเข้า org.springframework.beans.factory.annotation.autowired; org.springframework.boot.web.client.resttemplatebuilder นำเข้า org.springframework.context.annotation.bean; นำเข้า org.springframework.http.mediatype; org.springframework.http.converter.json.mappingjackson2httpmessageconverter; นำเข้า org.springframework.stereotype.component; นำเข้า org.springframework.web.client.resttemplate; java.util.list; นำเข้า java.util.optional; นำเข้า java.util.stream.collectors; @componentpublic class resttemplateconfig {// note เมื่อเริ่มต้นเพราะเราฉีด Resttemplate ลงในบริการ @autowired ObjectMapper ObjectMapper; // ใช้ RestTemplateBuilder เพื่อยกตัวอย่างวัตถุ RestTemplate สปริงได้ฉีดอินสแตนซ์ RestTemplate โดยค่าเริ่มต้น @Bean RestTemplate RestTemplate () {restTemplate restTemplate = builder.build (); รายการ <httpmessageConverter <? >> messageConverters = lists.newarraylist (); MappingJackson2httpmessageConverter Converter = ใหม่ mapingjackson2httpmessageConverter (); Converter.SetObjectMapper (ObjectMapper); // ข้อยกเว้นอาจเกิดขึ้นได้หากไม่มีการเพิ่ม // ไม่สามารถแยกการตอบสนอง: ไม่มี httpmessageConverter ที่เหมาะสมที่พบสำหรับประเภทการตอบสนอง [คลาส] mediaType [] mediaTypes = ใหม่ mediaType [] {mediaType.application_json, mediaType.application_octet_stream, mediatype.text_html mediaType.application_stream_json, mediaType.application_atom_xml, mediaType.application_form_urlencoded, mediaType.application_json_utf8, mediaType.application_pdf,}; Converter.setsupportedmediatypes (array.aslist (mediaTypes)); ลอง {// set messageConverters ผ่านฟิลด์ฟิลด์สะท้อน = restTemplate.getClass (). getDeclaredField ("MessageConverters"); field.setAccessible (จริง); รายการ <httpmessageConverter <? >> orgconverterlist = (รายการ <httpmessageConverter <? >>) ฟิลด์. get (resttemplate); ตัวเลือก <httpmessageConverter <? >> opconverter = orgconverterlist.stream () .filter (x -> x.getClass (). getName (). equalsignorecase (mapingjackson2httpmessageConverter.class. if (opconverter.ispresent () == false) {return restTemplate; } MessageConverters.add (Converter); // เพิ่ม MappingJackson2httpmessageConverter // เพิ่มรายการ httpmessageconverter ที่เหลืออยู่เดิม <httpmessageConverter <? >> leftConverters = orgConverterList.stream x.getClass (). getName (). equalsignorecase (mapingjackson2httpmessageConverter.class .getName ()) == false) .collect (collectors.tolist ()); MessageConverters.addall (leftConverters); System.out.println (string.format ("【 httpmessageconverter 】ปริมาณดั้งเดิม: %s, ปริมาณที่สร้างขึ้นใหม่: %s", orgconverterlist.size (), messageConverters.size ()); } catch (exception e) {e.printstacktrace (); } RestTemplate.SetMessAgEconverters (MessageConverters); ส่งคืน RestTemplate; -นอกเหนือจากฟิลด์ MessageConverters ดูเหมือนว่าเราไม่สนใจเกี่ยวกับแพ็คเกจการพึ่งพาภายนอกและกระบวนการก่อสร้างภายในของ RestTemplate อีกต่อไป ตามที่คาดไว้มันสะอาดและกระชับและดูแลรักษาได้ง่าย
3. ปัญหารหัสขยะ
นี่เป็นคำถามที่คลาสสิกมาก การแก้ปัญหานั้นง่ายมากค้นหา httpmessageConverter และดู charset ที่รองรับเริ่มต้น AbstractJackson2httpmessageConverter เป็นคลาสพื้นฐานสำหรับ httpmessageConverter จำนวนมากและการเข้ารหัสเริ่มต้นคือ UTF-8:
Abstractjackson2httpmessageconverter
Public Abstract Class AbstractJackson2httpmessageConverter ขยาย AbstractGenerichttpMessageConverter <Ojrop> {สาธารณะคงที่ charset final charset default_Charset = StandardCharSets.UTF_8;}StringhttpmessageConverter ค่อนข้างพิเศษ บางคนรายงานว่าปัญหาที่อ่านไม่ออกเกิดจากการเข้ารหัส ISO-8859-1 ที่สนับสนุนโดยค่าเริ่มต้น:
stringhttpmessageconverter
/*** การใช้งาน {@link httpmessageConverter} ที่สามารถอ่านและเขียนสตริงได้ * * <p> โดยค่าเริ่มต้นตัวแปลงนี้รองรับสื่อทุกประเภท ({@code}), * และเขียนด้วย {@code content-type} ของ {@code text/plain} สิ่งนี้สามารถแทนที่ * โดยการตั้งค่า {@link #SetSupportedmediatypes supportedediatypes} * * @author arjen poutsma * @author Juergen Hoeller * @Since 3.0 */คลาสสาธารณะ StringhttpmessageConverter ขยาย AbstracthtpMessageConverter <String> {สาธารณะ /*** ตัวสร้างเริ่มต้นที่ใช้ {@code "ISO-8859-1"} เป็น charset เริ่มต้น * @See #StringhttpMessageConverter (charset) */ public stringhttpmessageConverter () {this (default_charset); -หากรหัสที่อ่านไม่ออกระหว่างการใช้งานเราสามารถตั้งค่าการเข้ารหัสที่รองรับโดย httpmessageConverter ผ่านวิธีการที่ใช้กันทั่วไป ได้แก่ UTF-8, GBK ฯลฯ
4. ข้อยกเว้น Deserialization
นี่เป็นอีกปัญหาหนึ่งที่พบได้ง่ายในระหว่างกระบวนการพัฒนา เนื่องจากมีเฟรมเวิร์กและเครื่องมือโอเพ่นซอร์สจำนวนมากใน Java และเวอร์ชันมีการเปลี่ยนแปลงบ่อยครั้งผิดพลาดที่ไม่คาดคิดมักเกิดขึ้น
ใช้เวลา Joda เป็นตัวอย่าง Joda Time เป็นกรอบเวลาและวันที่ของ Java ที่ได้รับความนิยม แต่ถ้าอินเทอร์เฟซของคุณเปิดเผยประเภทของเวลา Joda เช่น Datetime ดังนั้นผู้โทรอินเตอร์เฟส (ระบบ isomorphic และ heterogeneous) อาจพบปัญหาการทำให้เป็นอนุกรม
org.springframework.http.converter.httpmessageConversionexception: ประเภทคำจำกัดความข้อผิดพลาด: [ประเภทง่าย ๆ , คลาส org.joda.time.chronology]; ข้อยกเว้นที่ซ้อนกันคือ com.fasterxml.jackson.databind.exc.invaliddefinitionexception: ไม่สามารถสร้างอินสแตนซ์ของ `org.joda.time.chronology` (ไม่มีผู้สร้างเช่นโครงสร้างเริ่มต้นที่มีอยู่): ประเภทนามธรรม
ที่ [แหล่งที่มา: (pushbackinputstream);
ฉันพบสิ่งนี้ในโรงงานก่อนหน้าและต่อมาเพื่อความสะดวกในการโทรฉันเปลี่ยนกลับเป็นประเภทวันที่ที่เปิดเผย Java โดยตรง
แน่นอนว่ามีมากกว่าโซลูชันนี้ คุณสามารถใช้แจ็คสันเพื่อสนับสนุนการทำให้เป็นอนุกรมและ deserialization ของคลาสที่กำหนดเอง ในระบบที่มีข้อกำหนดความแม่นยำไม่สูงมากใช้งาน Simple DateTime Custom Serialization:
Datetimeserializer
แพ็คเกจ com.power.demo.util; นำเข้า com.fasterxml.jackson.core.jsongenerator; นำเข้า com.fasterxml.jackson.core.jsonprocessingException; นำเข้า com.fasterxml.jackson.databind.jonserializer; org.joda.time.dateTime; นำเข้า org.joda.time.format.datetimeformat; นำเข้า org.joda.time.format.datetimeformatter; นำเข้า java.io.ioexception;/*** โดยค่าเริ่มต้นแจ็คสัน * <p>* เมื่อ serializing jodatime, dateTime สามารถถูกทำให้เป็นอนุกรมซึ่งง่ายต่อการอ่าน **/คลาสสาธารณะ datetimeserializer ขยาย jSonserializer <Tatetime> {ส่วนตัว datetimeformatter วันที่ = datetimeformat.forpattern ("yyy-mm-dd hh: mm); @Override เป็นโมฆะสาธารณะ serialize (ค่าวันที่ dateTime, jSongenerator jgen, serializerprovider ผู้ให้บริการ) พ่น Ioexception, jsonprocessingException {jgen.writestring (value.toString (DateFormatter)); - และ Detetime Deserialization:
DateTimedeserializer
แพ็คเกจ com.power.demo.util; นำเข้า com.fasterxml.jackson.core.jsonparser; นำเข้า com.fasterxml.jackson.core.jsonprocessingException; นำเข้า com.fasterxml.jackson.databind.deserialization; com.fasterxml.jackson.databind.jsonnode; นำเข้า org.joda.time.dateTime; นำเข้า org.joda.time.format.dateTimeFormat; นำเข้า org.joda.time.format.dateTimeFormatter; DateTimedEserIalizer ขยาย JSondEserializer <TateTime> {ส่วนตัว dateTimeFormatter DateFormatter = datetimeFormat.forpattern ("yyyy-mm-dd hh: mm: ss"); @Override Public DateTime Deserialize (JsonParser JP, บริบท DeserializationContext) พ่น IOException, JsonProcessingException {JsonNode Node = jp.getCodec (). ReadTree (JP); สตริง s = node.astext (); dateTime parse = dateTime.parse (s, dateFormatter); กลับแยกวิเคราะห์; -ในที่สุดคุณสามารถสรุปปัญหาการโทรทั่วไปในคลาส RestTemplateconfig และคุณสามารถอ้างถึงได้ดังนี้:
Resttemplateconfig
package com.power.demo.restclient.config;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.module.SimpleModule;import com.google.common.collect.Lists;import com.power.demo.util.DateTimeSerializer;import com.power.demo.util.DatetimeDeserializer;import org.joda.time.DateTime;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.web.client.RestTemplateBuilder;import org.springframework.context.annotation.Bean;import org.springframework.http.MediaType;import org.springframework.http.converter.HttpMessageConverter;import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;import org.springframework.stereotype.Component;import org.springframework.web.client.RestTemplate;import java.lang.reflect.Field;import java.util.Arrays;import java.util.List;import java.util.Optional;import java.util.stream.Collectors;@Componentpublic class RestTemplateConfig { // Be careful when starting that because we inject RestTemplate into the service, we need to instantiate an instance of this class when starting @Autowired private RestTemplateBuilder builder; @autowired ObjectMapper ObjectMapper; // Use RestTemplateBuilder to instantiate the RestTemplate object. Spring has injected the RestTemplateBuilder instance by default @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = builder.build(); //Register model, used to implement jackson joda time serialization and deserialization SimpleModule module = new SimpleModule(); module.addSerializer(DateTime.class, new DateTimeSerializer()); module.addDeserializer(DateTime.class, new DatetimeDeserializer()); objectMapper.registerModule(module); List<HttpMessageConverter<?>> messageConverters = Lists.newArrayList(); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setObjectMapper(objectMapper); //Exception will appear without adding//Could not extract response: no suitable HttpMessageConverter found for response type [class ] MediaType[] mediaTypes = new MediaType[]{ MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM, MediaType.TEXT_HTML, MediaType.TEXT_PLAIN, MediaType.TEXT_XML, MediaType.APPLICATION_STREAM_JSON, MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON_UTF8, MediaType.APPLICATION_PDF, }; converter.setSupportedMediaTypes(Arrays.asList(mediaTypes)); try { //Set MessageConverters Field field = restTemplate.getClass().getDeclaredField("messageConverters"); field.setAccessible(true); List<HttpMessageConverter<?>> orgConverterList = (List<HttpMessageConverter<?>>) field.get(restTemplate); Optional<HttpMessageConverter<?>> opConverter = orgConverterList.stream() .filter(x -> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.class .getName())) .findFirst(); if (opConverter.isPresent() == false) { return restTemplate; } messageConverters.add(converter);//Add MappingJackson2HttpMessageConverter //Add the original remaining HttpMessageConverter List<HttpMessageConverter<?>> leftConverters = orgConverterList.stream() .filter(x -> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.class .getName()) == false) .collect(Collectors.toList()); messageConverters.addAll(leftConverters); System.out.println(String.format("【HttpMessageConverter】Original quantity: %s, reconstructed quantity: %s" , orgConverterList.size(), messageConverters.size())); } catch (exception e) {e.printstacktrace (); } restTemplate.setMessageConverters(messageConverters); return restTemplate; -目前良好地解决了RestTemplate常用调用问题,而且不需要你写RestTemplate帮助工具类了。
上面列举的这些常见问题,其实.NET下面也有,有兴趣大家可以搜索一下微软的HttpClient常见使用问题,用过的人都深有体会。更不用提RestSharp 这个开源类库,几年前用的过程中发现了非常多的Bug,到现在还有一个反序列化数组的问题困扰着我们,我只好自己造个简单轮子特殊处理,给我最深刻的经验就是,很多看上去简单的功能,真的碰到了依然会花掉不少的时间去排查和解决,甚至要翻看源码。所以,我们写代码要认识到,越是通用的工具,越需要考虑到特例,可能你需要花80%以上的精力去处理20%的特殊情况,这估计也是满足常见的二八定律吧。
参考:
https://stackoverflow.com/questions/21854369/no-suitable-httpmessageconverter-found-for-response-type
https://stackoverflow.com/questions/40726145/rest-templatecould-not-extract-response-no-suitable-httpmessageconverter-found
https://stackoverflow.com/questions/10579122/resttemplate-no-suitable-httpmessageconverter
http://forum.spring.io/forum/spring-projects/android/126794-no-suitable-httpmessageconverter-found
The above is all the content of this article. ฉันหวังว่ามันจะเป็นประโยชน์ต่อการเรียนรู้ของทุกคนและฉันหวังว่าทุกคนจะสนับสนุน wulin.com มากขึ้น