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