欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

SpringMVC返回图片的几种方式(小结)

程序员文章站 2023-12-05 16:42:16
后端提供服务,通常返回的json串,但是某些场景下可能需要直接返回二进制流,如一个图片编辑接口,希望直接将图片流返回给前端,此时可以怎么处理? i. 返回二进制图片 主...

后端提供服务,通常返回的json串,但是某些场景下可能需要直接返回二进制流,如一个图片编辑接口,希望直接将图片流返回给前端,此时可以怎么处理?

i. 返回二进制图片

主要借助的是 httpservletresponse这个对象,实现case如下

@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";
}

注意事项

  1. 注意contenttype定义了图片类型
  2. 将二进制写入 httpservletresponse#getoutputstream
  3. 写完之后,flush(), close()请务必执行一次

ii. 返回图片的几种方式封装

一般来说,一个后端提供的服务接口,往往是返回json数据的居多,前面提到了直接返回图片的场景,那么常见的返回图片有哪些方式呢?

  1. 返回图片的http地址
  2. 返回base64格式的图片
  3. 直接返回二进制的图片
  4. 其他...(我就见过上面三种,别的还真不知道)

那么我们提供的一个controller,应该如何同时支持上面这三种使用姿势呢?

1. bean定义

因为有几种不同的返回方式,至于该选择哪一个,当然是由前端来指定了,所以,可以定义一个请求参数的bean对象

@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, 当然必要性不是特别大,下面是两者的定义

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;
  }
}

@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

@data
public class baseresponse {

  /**
   * 返回图片的相对路径
   */
  private string path;


  /**
   * 返回图片的https格式
   */
  private string url;


  /**
   * base64格式的图片
   */
  private string base;
}

说明:

实际的项目环境中,请求参数和返回肯定不会像上面这么简单,所以可以通过继承上面的bean或者自己定义对应的格式来实现

2. 返回的封装方式

既然目标明确,封装可算是这个里面最清晰的一个步骤了

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中;显然这两种方式都没有抛异常的使用方便

有优点当然就有缺点了:

异常方式,额外的性能开销,所以在自定义异常中,我都覆盖了下面这个方法,不要完整的堆栈

@override
public synchronized throwable fillinstacktrace() {
  return this;
}

编码习惯问题,有些人可能就非常不喜欢这种使用方式

iii. 项目相关

只说不练好像没什么意思,上面的这个设计,完全体现在了我一直维护的开源项目 quick-media中,当然实际和上面有一些不同,毕竟与业务相关较大,有兴趣的可以参考

quickmedia: :

baseaction: com.hust.hui.quickmedia.web.wxapi.wxbaseaction#buildreturn

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。