后端提供服务,通常返回的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