SpringMVC返回图片的几种方式

SpringMVC返回图片的几种方式

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

注意事项

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

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

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

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

那么我们提供的一个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;
    }
}

undefined

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

说明:

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

  1. 配合全局异常捕获(ControllerAdvie),使用起来非常方便简单
  2. 所有的异常集中处理,方便信息统计和报警
如,在统一的地方进行异常计数,然后超过某个阀值之后,报警给负责人,这样就不需要在每个出现异常case的地方来主动埋点了
  1. 避免错误状态码的层层传递
- 这个主要针对web服务,一般是在返回的json串中,会包含对应的错误状态码,错误信息
- 而异常case是可能出现在任何地方的,为了保持这个异常信息,要么将这些数据层层传递到controller;要么就是存在ThreadLocal中;显然这两种方式都没有抛异常的使用方便

有优点当然就有缺点了:

  1. 异常方式,额外的性能开销,所以在自定义异常中,我都覆盖了下面这个方法,不要完整的堆栈
@Override
public synchronized Throwable fillInStackTrace() {
    return this;
}
  1. 编码习惯问题,有些人可能就非常不喜欢这种使用方式

III. 项目相关

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

IV. 其他

声明

尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见解不全,如有问题,欢迎批评指正

扫描关注,java分享

QrCode

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏刘君君

Validator 使用总结

20760
来自专栏博岩Java大讲堂

Java日志体系(commons-logging)Java日志系统学习

73950
来自专栏情情说

深入浅出MyBatis:MyBatis解析和运行原理

上一篇介绍了反射和动态代理基础,主要是为本篇文章做个铺垫,反射使配置和灵活性大大提高,可以给很多配置设置参数,动态代理可以在运行时创建代理对象,做一些特殊的处理...

40870
来自专栏日常分享

DAO设计模式的理解

它可以实现业务逻辑与数据库访问相分离。相对来说,数据库是比较稳定的,其中DAO组件依赖于数据库系统,提供数据库访问的接口。

20220
来自专栏SDNLAB

Open vSwitch系列之openflow版本兼容

众所周知Open vSwitch支持的openflow版本从1.0到1.5版本(当前Open vSwitch版本是2.3.2)通过阅读代码,处理openflow...

585130
来自专栏技术碎碎念

Jsp语法、指令及动作元素

一、JSP的语法 1、JSP的模板元素:(先写HTML)    就是JSP中的那些HTML标记    作用:页面布局和美化 2、JSP的Java脚本表达式:  ...

45360
来自专栏Java架构师学习

带你深入了解Java线程中的那些事

引言 说到Thread大家都很熟悉,我们平常写并发代码的时候都会接触到,那么我们来看看下面这段代码是如何初始化以及执行的呢? public class Thre...

35080
来自专栏码匠的流水账

spring security运行时配置ignore url

以前用shiro的比较多,不过spring boot倒是挺推崇自家的spring security的,有默认的starter,于是也就拿来用了。

12610
来自专栏zhisheng

JAVA虚拟机关闭钩子(Shutdown Hook)

当你认真的去看一个组件的源码的时候,你会经常看见这种关闭钩子的函数,如果你不了解的话,谷歌一下,你就会发现如下文章就是搜索引擎出来的第一篇,不愧是出自我们优秀的...

33930
来自专栏猿天地

用aop加redis实现通用接口缓存

系统在高并发场景下,最有用的三个方法是缓存,限流,降级。 缓存就是其中之一,目前缓存基本上是用redis或者memcached。 redis和memcached...

36970

扫码关注云+社区

领取腾讯云代金券