后端提供服務,通常返回的json串,但是某些場景下可能需要直接返回二進制流,如一個圖片編輯接口,希望直接將圖片流返回給前端,此時可以怎么處理?
I. 返回二進制圖片
主要借助的是 HttpServletResponse這個對象,實現case如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@RequestMapping (value = { "/img/render" }, method = {RequestMethod.GET, RequestMethod.POST, RequestMethod.OPTIONS}) @CrossOrigin (origins = "*" ) @ResponseBody public String execute(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { // img為圖片的二進制流 byte [] img = xxx; httpServletResponse.setContentType( "image/png" ); OutputStream os = httpServletResponse.getOutputStream(); os.write(img); os.flush(); os.close(); return "success" ; } |
注意事項
- 注意ContentType定義了圖片類型
- 將二進制寫入 httpServletResponse#getOutputStream
- 寫完之后,flush(), close()請務必執行一次
II. 返回圖片的幾種方式封裝
一般來說,一個后端提供的服務接口,往往是返回json數據的居多,前面提到了直接返回圖片的場景,那么常見的返回圖片有哪些方式呢?
- 返回圖片的http地址
- 返回base64格式的圖片
- 直接返回二進制的圖片
- 其他...(我就見過上面三種,別的還真不知道)
那么我們提供的一個Controller,應該如何同時支持上面這三種使用姿勢呢?
1. bean定義
因為有幾種不同的返回方式,至于該選擇哪一個,當然是由前端來指定了,所以,可以定義一個請求參數的bean對象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@Data public class BaseRequest { private static final long serialVersionUID = 1146303518394712013L; /** * 輸出圖片方式: * * url : http地址 (默認方式) * base64 : base64編碼 * stream : 直接返回圖片 * */ private String outType; /** * 返回圖片的類型 * jpg | png | webp | gif */ private String mediaType; public ReturnTypeEnum returnType() { return ReturnTypeEnum.getEnum(outType); } public MediaTypeEnum mediaType() { return MediaTypeEnum.getEnum(mediaType); } } |
為了簡化判斷,定義了兩個注解,一個ReturnTypeEnum, 一個 MediaTypeEnum, 當然必要性不是特別大,下面是兩者的定義
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
public enum ReturnTypeEnum { URL( "url" ), STREAM( "stream" ), BASE64( "base" ); private String type; ReturnTypeEnum(String type) { this .type = type; } private static Map<String, ReturnTypeEnum> map; static { map = new HashMap<>( 3 ); for (ReturnTypeEnum e: ReturnTypeEnum.values()) { map.put(e.type, e); } } public static ReturnTypeEnum getEnum(String type) { if (type == null ) { return URL; } ReturnTypeEnum e = map.get(type.toLowerCase()); return e == null ? URL : e; } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
@Data public enum MediaTypeEnum { ImageJpg( "jpg" , "image/jpeg" , "FFD8FF" ), ImageGif( "gif" , "image/gif" , "47494638" ), ImagePng( "png" , "image/png" , "89504E47" ), ImageWebp( "webp" , "image/webp" , "52494646" ), private final String ext; private final String mime; private final String magic; MediaTypeEnum(String ext, String mime, String magic) { this .ext = ext; this .mime = mime; this .magic = magic; } private static Map<String, MediaTypeEnum> map; static { map = new HashMap<>( 4 ); for (MediaTypeEnum e: values()) { map.put(e.getExt(), e); } } public static MediaTypeEnum getEnum(String type) { if (type == null ) { return ImageJpg; } MediaTypeEnum e = map.get(type.toLowerCase()); return e == null ? ImageJpg : e; } } |
上面是請求參數封裝的bean,返回當然也有一個對應的bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@Data public class BaseResponse { /** * 返回圖片的相對路徑 */ private String path; /** * 返回圖片的https格式 */ private String url; /** * base64格式的圖片 */ private String base; } |
說明:
實際的項目環境中,請求參數和返回肯定不會像上面這么簡單,所以可以通過繼承上面的bean或者自己定義對應的格式來實現
2. 返回的封裝方式
既然目標明確,封裝可算是這個里面最清晰的一個步驟了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
protected void buildResponse(BaseRequest request, BaseResponse response, byte [] bytes) throws SelfError { switch (request.returnType()) { case URL: upload(bytes, response); break ; case BASE64: base64(bytes, response); break ; case STREAM: stream(bytes, request); } } private void upload( byte [] bytes, BaseResponse response) throws SelfError { try { // 上傳到圖片服務器,根據各自的實際情況進行替換 String path = UploadUtil.upload(bytes); if (StringUtils.isBlank(path)) { // 上傳失敗 throw new InternalError( null ); } response.setPath(path); response.setUrl(CdnUtil.img(path)); } catch (IOException e) { // cdn異常 log.error( "upload to cdn error! e:{}" , e); throw new CDNUploadError(e.getMessage()); } } // 返回base64 private void base64( byte [] bytes, BaseResponse response) { String base = Base64.getEncoder().encodeToString(bytes); response.setBase(base); } // 返回二進制圖片 private void stream( byte [] bytes, BaseRequest request) throws SelfError { try { MediaTypeEnum mediaType = request.mediaType(); HttpServletResponse servletResponse = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); servletResponse.setContentType(mediaType.getMime()); OutputStream os = servletResponse.getOutputStream(); os.write(bytes); os.flush(); os.close(); } catch (Exception e) { log.error( "general return stream img error! req: {}, e:{}" , request, e); if (StringUtils.isNotBlank(e.getMessage())) { throw new InternalError(e.getMessage()); } else { throw new InternalError( null ); } } } |
說明:
請無視上面的幾個自定義異常方式,需要使用時,完全可以干掉這些自定義異常即可;這里簡單說一下,為什么會在實際項目中使用這種自定義異常的方式,主要是有以下幾個優點
配合全局異常捕獲(ControllerAdvie),使用起來非常方便簡單
所有的異常集中處理,方便信息統計和報警
如,在統一的地方進行異常計數,然后超過某個閥值之后,報警給負責人,這樣就不需要在每個出現異常case的地方來主動埋點了
避免錯誤狀態碼的層層傳遞
- 這個主要針對web服務,一般是在返回的json串中,會包含對應的錯誤狀態碼,錯誤信息
- 而異常case是可能出現在任何地方的,為了保持這個異常信息,要么將這些數據層層傳遞到controller;要么就是存在ThreadLocal中;顯然這兩種方式都沒有拋異常的使用方便
有優點當然就有缺點了:
異常方式,額外的性能開銷,所以在自定義異常中,我都覆蓋了下面這個方法,不要完整的堆棧
1
2
3
4
|
@Override public synchronized Throwable fillInStackTrace() { return this ; } |
編碼習慣問題,有些人可能就非常不喜歡這種使用方式
III. 項目相關
只說不練好像沒什么意思,上面的這個設計,完全體現在了我一直維護的開源項目 Quick-Media中,當然實際和上面有一些不同,畢竟與業務相關較大,有興趣的可以參考
QuickMedia: https://github.com/liuyueyi/quick-media :
BaseAction: com.hust.hui.quickmedia.web.wxapi.WxBaseAction#buildReturn
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:https://my.oschina.net/u/566591/blog/1609293