查看: 1404|回复: 0

[Java代码] SpringMVC返回图片的几种方式(小结)

发表于 2018-2-25 08:00:00

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

I. 返回二进制图片

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

  1. @RequestMapping(value = {"/img/render"}, method = {RequestMethod.GET, RequestMethod.POST, RequestMethod.OPTIONS})
  2. @CrossOrigin(origins = "*")
  3. @ResponseBody
  4. public String execute(HttpServletRequest httpServletRequest,
  5. HttpServletResponse httpServletResponse) {
  6. // img为图片的二进制流
  7. byte[] img = xxx;
  8. httpServletResponse.setContentType("image/png");
  9. OutputStream os = httpServletResponse.getOutputStream();
  10. os.write(img);
  11. os.flush();
  12. os.close();
  13. return "success";
  14. }
复制代码

注意事项

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

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

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

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

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

1. bean定义

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

  1. @Data
  2. public class BaseRequest {
  3. private static final long serialVersionUID = 1146303518394712013L;
  4. /**
  5. * 输出图片方式:
  6. *
  7. * url : http地址 (默认方式)
  8. * base64 : base64编码
  9. * stream : 直接返回图片
  10. *
  11. */
  12. private String outType;
  13. /**
  14. * 返回图片的类型
  15. * jpg | png | webp | gif
  16. */
  17. private String mediaType;
  18. public ReturnTypeEnum returnType() {
  19. return ReturnTypeEnum.getEnum(outType);
  20. }
  21. public MediaTypeEnum mediaType() {
  22. return MediaTypeEnum.getEnum(mediaType);
  23. }
  24. }
复制代码

为了简化判断,定义了两个注解,一个ReturnTypeEnum, 一个 MediaTypeEnum, 当然必要性不是特别大,下面是两者的定义

  1. public enum ReturnTypeEnum {
  2. URL("url"),
  3. STREAM("stream"),
  4. BASE64("base");
  5. private String type;
  6. ReturnTypeEnum(String type) {
  7. this.type = type;
  8. }
  9. private static Map<String, ReturnTypeEnum> map;
  10. static {
  11. map = new HashMap<>(3);
  12. for(ReturnTypeEnum e: ReturnTypeEnum.values()) {
  13. map.put(e.type, e);
  14. }
  15. }
  16. public static ReturnTypeEnum getEnum(String type) {
  17. if (type == null) {
  18. return URL;
  19. }
  20. ReturnTypeEnum e = map.get(type.toLowerCase());
  21. return e == null ? URL : e;
  22. }
  23. }
复制代码
  1. @Data
  2. public enum MediaTypeEnum {
  3. ImageJpg("jpg", "image/jpeg", "FFD8FF"),
  4. ImageGif("gif", "image/gif", "47494638"),
  5. ImagePng("png", "image/png", "89504E47"),
  6. ImageWebp("webp", "image/webp", "52494646"),
  7. private final String ext;
  8. private final String mime;
  9. private final String magic;
  10. MediaTypeEnum(String ext, String mime, String magic) {
  11. this.ext = ext;
  12. this.mime = mime;
  13. this.magic = magic;
  14. }
  15. private static Map<String, MediaTypeEnum> map;
  16. static {
  17. map = new HashMap<>(4);
  18. for (MediaTypeEnum e: values()) {
  19. map.put(e.getExt(), e);
  20. }
  21. }
  22. public static MediaTypeEnum getEnum(String type) {
  23. if (type == null) {
  24. return ImageJpg;
  25. }
  26. MediaTypeEnum e = map.get(type.toLowerCase());
  27. return e == null ? ImageJpg : e;
  28. }
  29. }
复制代码

上面是请求参数封装的bean,返回当然也有一个对应的bean

  1. @Data
  2. public class BaseResponse {
  3. /**
  4. * 返回图片的相对路径
  5. */
  6. private String path;
  7. /**
  8. * 返回图片的https格式
  9. */
  10. private String url;
  11. /**
  12. * base64格式的图片
  13. */
  14. private String base;
  15. }
复制代码

说明:

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

2. 返回的封装方式

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

  1. protected void buildResponse(BaseRequest request,
  2. BaseResponse response,
  3. byte[] bytes) throws SelfError {
  4. switch (request.returnType()) {
  5. case URL:
  6. upload(bytes, response);
  7. break;
  8. case BASE64:
  9. base64(bytes, response);
  10. break;
  11. case STREAM:
  12. stream(bytes, request);
  13. }
  14. }
  15. private void upload(byte[] bytes, BaseResponse response) throws SelfError {
  16. try {
  17. // 上传到图片服务器,根据各自的实际情况进行替换
  18. String path = UploadUtil.upload(bytes);
  19. if (StringUtils.isBlank(path)) { // 上传失败
  20. throw new InternalError(null);
  21. }
  22. response.setPath(path);
  23. response.setUrl(CdnUtil.img(path));
  24. } catch (IOException e) { // cdn异常
  25. log.error("upload to cdn error! e:{}", e);
  26. throw new CDNUploadError(e.getMessage());
  27. }
  28. }
  29. // 返回base64
  30. private void base64(byte[] bytes, BaseResponse response) {
  31. String base = Base64.getEncoder().encodeToString(bytes);
  32. response.setBase(base);
  33. }
  34. // 返回二进制图片
  35. private void stream(byte[] bytes, BaseRequest request) throws SelfError {
  36. try {
  37. MediaTypeEnum mediaType = request.mediaType();
  38. HttpServletResponse servletResponse = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
  39. servletResponse.setContentType(mediaType.getMime());
  40. OutputStream os = servletResponse.getOutputStream();
  41. os.write(bytes);
  42. os.flush();
  43. os.close();
  44. } catch (Exception e) {
  45. log.error("general return stream img error! req: {}, e:{}", request, e);
  46. if (StringUtils.isNotBlank(e.getMessage())) {
  47. throw new InternalError(e.getMessage());
  48. } else {
  49. throw new InternalError(null);
  50. }
  51. }
  52. }
复制代码

说明:

请无视上面的几个自定义异常方式,需要使用时,完全可以干掉这些自定义异常即可;这里简单说一下,为什么会在实际项目中使用这种自定义异常的方式,主要是有以下几个优点

配合全局异常捕获(ControllerAdvie),使用起来非常方便简单

所有的异常集中处理,方便信息统计和报警

如,在统一的地方进行异常计数,然后超过某个阀值之后,报警给负责人,这样就不需要在每个出现异常case的地方来主动埋点了

避免错误状态码的层层传递

- 这个主要针对web服务,一般是在返回的json串中,会包含对应的错误状态码,错误信息
- 而异常case是可能出现在任何地方的,为了保持这个异常信息,要么将这些数据层层传递到controller;要么就是存在ThreadLocal中;显然这两种方式都没有抛异常的使用方便

有优点当然就有缺点了:

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

  1. @Override
  2. public synchronized Throwable fillInStackTrace() {
  3. return this;
  4. }
复制代码

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

III. 项目相关

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

QuickMedia: https://github.com/liuyueyi/quick-media :

BaseAction: com.hust.hui.quickmedia.web.wxapi.WxBaseAction#buildReturn

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



回复

使用道具 举报