文件下载做中转 ResponseEntity

前景了解

项目需求,后端需要统计某个服务器上文件的下载次数。面对这种需求,我想在后端开发中很常见了吧~
此文的大概思路就是通过给文件下载增加一个 中转链接。这个中转链接就是项目的请求,接收到请求后,可以做任何处理,最终只要把数据再以流的方式返回就好啦!

此文项目

此文提及的需求是,前端需要下载服务器上的app应用,统计各个app的下载量。

DB里的表结构可参考以下:

CREATE TABLE `app_store_app` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'app的id',
  `app_name` varchar(20) DEFAULT NULL COMMENT '应用名',
  `total_down` int(11) DEFAULT NULL COMMENT '总下载量',
  `target_url` varchar(200) DEFAULT NULL COMMENT '跳转链接(方便做中转)',
  `down_url` varchar(200) DEFAULT NULL COMMENT '资源下载地址 或者 存储与服务器的路径',
  `app_icon` varchar(100) DEFAULT NULL COMMENT '应用图标',
  `app_version` varchar(10) DEFAULT NULL,
  `app_code` int(5) NOT NULL,
  `sub_content` varchar(100) DEFAULT NULL COMMENT '应用简介',
  `content` text COMMENT '应用介绍主体',
  `is_delete` int(1) DEFAULT '0' COMMENT '是否删除(0:否 1:是)',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='应用商店里的应用表';

INSERT INTO `app_store_app` VALUES ('1', '迷你世界', '4', '/api/redirect/downloadApp?appId=1', 'https://jenkinsapk.oss-cn-hangzhou.aliyuncs.com/jkapk/apk/20191112/doudizhu.apk', '/appicon/xxx.png', '1.0', '1', '随时可以联机,超好玩', '随时可以联机,超好玩放到接口极光里的机房记得', '0', '2019-11-13 13:39:49', '2019-11-19 16:48:16');
INSERT INTO `app_store_app` VALUES ('2', '迷你世界2', '2', '/api/redirect/downloadApp?appId=2', '/android/201191113/app-anzhi-release.apk', '/appicon/xxx.png', '1.0', '1', '随时可以联机,超好玩', '随时可以联机,超好玩放到接口极光里的机房记得', '0', '2019-11-13 13:39:49', '2019-11-14 16:17:58');

了解ResponseEntity

ResponseEntity是对http响应的一个封装的类,可定义响应Header、响应Body、响应状态。
更多资料 可自行百度,网上一大推
其源码也是比较简单的,底下是自己的分析思路,不喜勿喷。哈哈

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.http;

import java.net.URI;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.function.Function;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;

public class ResponseEntity<T> extends HttpEntity<T> {
    //内部私有变量
    private final Object status;

    //构造函数
    public ResponseEntity(HttpStatus status) {
        this((Object)null, (MultiValueMap)null, (HttpStatus)status);
    }

    //构造函数
    public ResponseEntity(@Nullable T body, HttpStatus status) {
        this(body, (MultiValueMap)null, (HttpStatus)status);
    }

    //构造函数
    public ResponseEntity(MultiValueMap<String, String> headers, HttpStatus status) {
        this((Object)null, headers, (HttpStatus)status);
    }


    //构造函数-- 外部可以用的-- status 不可为空
    public ResponseEntity(@Nullable T body, @Nullable MultiValueMap<String, String> headers, HttpStatus status) {
        super(body, headers);
        Assert.notNull(status, "HttpStatus must not be null");
        this.status = status;
    }

    //构造函数-- 内部可以用的-- status 不可为空--status 类型为object
    private ResponseEntity(@Nullable T body, @Nullable MultiValueMap<String, String> headers, Object status) {
        super(body, headers);
        Assert.notNull(status, "HttpStatus must not be null");
        this.status = status;
    }

    //获取http的状态 ,
    // 如果构造对象的时候,status应该是一个可以强转成Integer的数值。有点不懂这里为什么this.status 不直接定义为 HttpStatus ,public
    //类型的构造函数都是要求HttpStatus类型的
    public HttpStatus getStatusCode() {
        return this.status instanceof HttpStatus ? (HttpStatus)this.status : HttpStatus.valueOf((Integer)this.status);
    }

    //获取http的状态code
    public int getStatusCodeValue() {
        return this.status instanceof HttpStatus ? ((HttpStatus)this.status).value() : (Integer)this.status;
    }

    //比较是否响应头、响应body、响应code是否相等
    public boolean equals(@Nullable Object other) {
        if (this == other) {
            return true;
        } else if (!super.equals(other)) {
            //super.equals 只是比较了header 和 body
            return false;
        } else {
            //ResponseEntity 的equal 还要追加一个比较 status
            ResponseEntity<?> otherEntity = (ResponseEntity)other;
            return ObjectUtils.nullSafeEquals(this.status, otherEntity.status);
        }
    }

    public int hashCode() {
        return super.hashCode() * 29 + ObjectUtils.nullSafeHashCode(this.status);
    }

    public String toString() {
        StringBuilder builder = new StringBuilder("<");
        builder.append(this.status.toString());
        if (this.status instanceof HttpStatus) {
            builder.append(' ');
            builder.append(((HttpStatus)this.status).getReasonPhrase());
        }

        builder.append(',');
        T body = this.getBody();
        HttpHeaders headers = this.getHeaders();
        if (body != null) {
            builder.append(body);
            builder.append(',');
        }

        builder.append(headers);
        builder.append('>');
        return builder.toString();
    }

    //设置状态码,返回一个builder,可进行其它的设置
    public static ResponseEntity.BodyBuilder status(HttpStatus status) {
        Assert.notNull(status, "HttpStatus must not be null");
        return new ResponseEntity.DefaultBuilder(status);
    }

    public static ResponseEntity.BodyBuilder status(int status) {
        return new ResponseEntity.DefaultBuilder(status);
    }

    public static <T> ResponseEntity<T> of(Optional<T> body) {
        Assert.notNull(body, "Body must not be null");
        return (ResponseEntity)body.map(ResponseEntity::ok).orElse(notFound().build());
    }

    //(静态方法)构造一个响应code为200类型的ResponseEntity构造器
    public static ResponseEntity.BodyBuilder ok() {
        return status(HttpStatus.OK);
    }

    //(静态方法)构造一个响应body 为入参body 的 ,状态码为200的ResponseEntity实体
    public static <T> ResponseEntity<T> ok(T body) {
        ResponseEntity.BodyBuilder builder = ok();
        return builder.body(body);
    }

    //(静态方法)
    public static ResponseEntity.BodyBuilder created(URI location) {
        ResponseEntity.BodyBuilder builder = status(HttpStatus.CREATED);
        return (ResponseEntity.BodyBuilder)builder.location(location);
    }
    //(静态方法)构造一个响应code为202类型的ResponseEntity构造器
    public static ResponseEntity.BodyBuilder accepted() {
        return status(HttpStatus.ACCEPTED);
    }
    //(静态方法)构造一个响应code为204类型的ResponseEntity构造器
    public static ResponseEntity.HeadersBuilder<?> noContent() {
        return status(HttpStatus.NO_CONTENT);
    }
    //(静态方法)构造一个响应code为400类型的ResponseEntity构造器
    public static ResponseEntity.BodyBuilder badRequest() {
        return status(HttpStatus.BAD_REQUEST);
    }
    //(静态方法)构造一个响应code为404类型的ResponseEntity构造器
    public static ResponseEntity.HeadersBuilder<?> notFound() {
        return status(HttpStatus.NOT_FOUND);
    }
    //(静态方法)构造一个响应code为422类型的ResponseEntity构造器
    public static ResponseEntity.BodyBuilder unprocessableEntity() {
        return status(HttpStatus.UNPROCESSABLE_ENTITY);
    }

    //body构造器的实现类
    //实现方法,可定义http响应的一些信息(状态code,状态头)
    //方便new ResponseEntity
    private static class DefaultBuilder implements ResponseEntity.BodyBuilder {
        private final Object statusCode;
        private final HttpHeaders headers = new HttpHeaders();

        public DefaultBuilder(Object statusCode) {
            this.statusCode = statusCode;
        }

        public ResponseEntity.BodyBuilder header(String headerName, String... headerValues) {
            String[] var3 = headerValues;
            int var4 = headerValues.length;

            for(int var5 = 0; var5 < var4; ++var5) {
                String headerValue = var3[var5];
                this.headers.add(headerName, headerValue);
            }

            return this;
        }

        public ResponseEntity.BodyBuilder headers(@Nullable HttpHeaders headers) {
            if (headers != null) {
                this.headers.putAll(headers);
            }

            return this;
        }

        public ResponseEntity.BodyBuilder allow(HttpMethod... allowedMethods) {
            this.headers.setAllow(new LinkedHashSet(Arrays.asList(allowedMethods)));
            return this;
        }

        public ResponseEntity.BodyBuilder contentLength(long contentLength) {
            this.headers.setContentLength(contentLength);
            return this;
        }

        public ResponseEntity.BodyBuilder contentType(MediaType contentType) {
            this.headers.setContentType(contentType);
            return this;
        }

        public ResponseEntity.BodyBuilder eTag(String etag) {
            if (!etag.startsWith("\"") && !etag.startsWith("W/\"")) {
                etag = "\"" + etag;
            }

            if (!etag.endsWith("\"")) {
                etag = etag + "\"";
            }

            this.headers.setETag(etag);
            return this;
        }

        public ResponseEntity.BodyBuilder lastModified(long date) {
            this.headers.setLastModified(date);
            return this;
        }

        //给响应头新增一个重定向地址,结合http的状态码301 和 302 ,跳转到对应的地址
        public ResponseEntity.BodyBuilder location(URI location) {
            this.headers.setLocation(location);
            return this;
        }

        public ResponseEntity.BodyBuilder cacheControl(CacheControl cacheControl) {
            String ccValue = cacheControl.getHeaderValue();
            if (ccValue != null) {
                this.headers.setCacheControl(cacheControl.getHeaderValue());
            }

            return this;
        }

        public ResponseEntity.BodyBuilder varyBy(String... requestHeaders) {
            this.headers.setVary(Arrays.asList(requestHeaders));
            return this;
        }

        //创建一个body 为空的ResponseEntity
        public <T> ResponseEntity<T> build() {
            return this.body((Object)null);
        }

        public <T> ResponseEntity<T> body(@Nullable T body) {
            //new 一个 ResponseEntity
            return new ResponseEntity(body, this.headers, this.statusCode);
        }
    }

    //构造响应body的构造器接口
    //继承了响应header的构造器接口
    public interface BodyBuilder extends ResponseEntity.HeadersBuilder<ResponseEntity.BodyBuilder> {
        ResponseEntity.BodyBuilder contentLength(long var1);

        ResponseEntity.BodyBuilder contentType(MediaType var1);

        <T> ResponseEntity<T> body(@Nullable T var1);
    }

    //构造响应header的构造器接口
    public interface HeadersBuilder<B extends ResponseEntity.HeadersBuilder<B>> {
        B header(String var1, String... var2);

        B headers(@Nullable HttpHeaders var1);

        B allow(HttpMethod... var1);

        B eTag(String var1);

        B lastModified(long var1);

        B location(URI var1);

        B cacheControl(CacheControl var1);

        B varyBy(String... var1);

        <T> ResponseEntity<T> build();
    }
}

这里我把app 改成了图片,道理是一毛一样的。
(1)预览图片

    //展示图片
    @RequestMapping(value = "/showPicEntity", method = RequestMethod.GET)
    public ResponseEntity showPicEntity() {
        String file = "D:\\output.png";
        FileSystemResource res = new FileSystemResource(file);
        ResponseEntity<byte[]> entity = null;
        try {
            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.setCacheControl(CacheControl.noCache());
            httpHeaders.setPragma("no-cache");
            httpHeaders.setExpires(0L);
            httpHeaders.setContentType(MediaType.IMAGE_PNG);
            entity = new ResponseEntity<byte[]>(FileUtils.toByteArray(res.getInputStream()), httpHeaders, HttpStatus.OK);
        } catch (IOException e) {
            LOGGER.error("读取文件流异常:{}", e);
            entity = new ResponseEntity(HttpStatus.NOT_FOUND);
        }
        return entity;
    }

在这里插入图片描述
(2)下载图片
注意: 返回的时候不用再追加@ResponseBody 了。

 //下载图片
    @RequestMapping(value = "/downPicEntity", method = RequestMethod.GET)
    public ResponseEntity downPicEntity() {
        String file = "D:\\output.png";
        FileSystemResource res = new FileSystemResource(file);
        ResponseEntity<byte[]> entity = null;
        try {
            //
            entity = ResponseEntity
                    .ok()
                    //下载文件要以二进制流的媒体类型
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    //attachement - 附件 ,指定下载文件名
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode("test.png", "UTF-8"))
                    .body(FileUtils.toByteArray(res.getInputStream()));
        } catch (IOException e) {
            LOGGER.error("读取文件流异常:{}", e);
            entity = new ResponseEntity(HttpStatus.NOT_FOUND);
        }
        return entity;
    }

实战

经过上面下载图片的代码,所以 app下载做中转,采用同样的方法,在target_url 的请求里 先做各种额外操作(统计啊),然后再把down_url 对应的资源转成 ResponseEntity 返回就好啦~
在这里插入图片描述

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值