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"; } 注意事项
II. 返回图片的几种方式封装一般来说,一个后端提供的服务接口,往往是返回json数据的居多,前面提到了直接返回图片的场景,那么常见的返回图片有哪些方式呢?
那么我们提供的一个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串中,会包含对应的错误状态码,错误信息 有优点当然就有缺点了: 异常方式,额外的性能开销,所以在自定义异常中,我都覆盖了下面这个方法,不要完整的堆栈 @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 以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程小技巧。 您可能感兴趣的文章:
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |