本博客介绍如何进行文件的分块上传。本文侧重介绍客户端,服务器端请参考博客《Java 文件分块上传服务器端源代码》。建议读者朋友在阅读本文代码前先了解一下 MIME 协议。

所谓分块上传并非把大文件进行物理分块,然后挨个上传,而是依次读取大文件的一部分文件流进行上传。分块,倒不如说分流比较切实。本文通过一个项目中的示例,说明使用 Apache 的 HttpComponents/HttpClient 对大文件进行分块上传的过程。示例使用的版本是

HttpComponents Client 4.2.1。

本文仅以一小 demo 功能性地解释 HttpComponents/HttpClient 分块上传,没有考虑 I/O 关闭、多线程等资源因素,读者可以根据自己的项目酌情处理。

本文核心思想及流程:以 100 MB 大小为例,大于 100 MB 的进行分块上传,否则整块上传。对于大于 100 MB 的文件,又以 100 MB 为单位进行分割,保证每次以不大于 100 MB 的大小进行上传。比如 304 MB 的一个文件会分为 100 MB、100

MB、100 MB、4 MB 等四块依次上传。第一次读取 0 字节开始的 100 MB 个字节,上传;第二次读取第 100 MB 字节开始的 100 MB 个字节,上传;第三次读取第 200 MB 字节开始的 100 MB 个字节,上传;第四次读取最后剩下的 4 MB 个字节进行上传。

自定义的 ContentBody 源码如下,其中定义了流的读取和输出:

package com.defonds.rtupload.common.util.block;

import java.io.File;

import java.io.IOException;

import java.io.OutputStream;

import java.io.RandomAccessFile;

import org.apache.http.entity.mime.content.AbstractContentBody;

import com.defonds.rtupload.GlobalConstant;

public class BlockStreamBody extends AbstractContentBody {

//给MultipartEntity看的2个参数

private long blockSize=0;//本次分块上传的大小

private String fileName=null;//上传文件名

//writeTo需要的3个参数

private int blockNumber=0,blockIndex=0;//blockNumber分块数;blockIndex当前第几块

private File targetFile=null;//要上传的文件

private BlockStreamBody(String mimeType) {

super(mimeType);

// TODO Auto-generated constructor stub

}

/**

* 自定义的ContentBody构造子

* @param blockNumber分块数

* @param blockIndex当前第几块

* @param targetFile要上传的文件

*/

public BlockStreamBody(int blockNumber, int blockIndex, File targetFile) {

this("application/octet-stream");

this.blockNumber=blockNumber;//blockNumber初始化

this.blockIndex=blockIndex;//blockIndex初始化

this.targetFile=targetFile;//targetFile初始化

this.fileName=targetFile.getName();//fileName初始化

//blockSize初始化

if (blockIndex

this.blockSize=GlobalConstant.CLOUD_API_LOGON_SIZE;

} else {//最后一块

this.blockSize=targetFile.length() - GlobalConstant.CLOUD_API_LOGON_SIZE * (blockNumber - 1);

}

}

@Override

public void writeTo(OutputStream out) throws IOException {

byte b[] = new byte[1024];//暂存容器

RandomAccessFile raf=newRandomAccessFile(targetFile, "r");//负责读取数据

if (blockIndex== 1) {//第一块

int n=0;

long readLength=0;//记录已读字节数

while (readLength <= blockSize - 1024) {//大部分字节在这里读取

n=raf.read(b, 0, 1024);

readLength += 1024;

out.write(b, 0, n);

}

if (readLength <= blockSize) {//余下的不足 1024 个字节在这里读取

n=raf.read(b, 0, (int)(blockSize - readLength));

out.write(b, 0, n);

}

} else if (blockIndex

raf.seek(GlobalConstant.CLOUD_API_LOGON_SIZE * (blockIndex - 1));//跳过前[块数*固定大小 ]个字节

int n=0;

long readLength=0;//记录已读字节数

while (readLength <= blockSize - 1024) {//大部分字节在这里读取

n=raf.read(b, 0, 1024);

readLength += 1024;

out.write(b, 0, n);

}

if (readLength <= blockSize) {//余下的不足 1024 个字节在这里读取

n=raf.read(b, 0, (int)(blockSize - readLength));

out.write(b, 0, n);

}

} else {//最后一块

raf.seek(GlobalConstant.CLOUD_API_LOGON_SIZE * (blockIndex - 1));//跳过前[块数*固定大小 ]个字节

int n=0;

while ((n=raf.read(b, 0, 1024)) != -1) {

out.write(b, 0, n);

}

}

//TODO 最后不要忘掉关闭out/raf

}

@Override

public String getCharset() {

// TODO Auto-generated method stub

return null;

}

@Override

public String getTransferEncoding() {

// TODO Auto-generated method stub

return "binary";

}

@Override

public String getFilename() {

// TODO Auto-generated method stub

return fileName;

}

@Override

public long getContentLength() {

// TODO Auto-generated method stub

return blockSize;

}

}

在自定义的 HttpComponents/HttpClient 工具类 HttpClient4Util 里进行分块上传的封装:

publicstaticString restPost(String serverURL, File targetFile,Map mediaInfoMap){

String content ="";

try{

DefaultHttpClient httpClient = newDefaultHttpClient();

HttpPost post = newHttpPost(serverURL +"?");

httpClient.getParams().setParameter("http.socket.timeout",60*60*1000);

MultipartEntity mpEntity = newMultipartEntity();

List keys = newArrayList(mediaInfoMap.keySet());

Collections.sort(keys, String.CASE_INSENSITIVE_ORDER);

for(Iterator iterator = keys.iterator(); iterator.hasNext();) {

String key = iterator.next();

if(StringUtils.isNotBlank(mediaInfoMap.get(key))) {

mpEntity.addPart(key, newStringBody(mediaInfoMap.get(key)));

}

}

if(targetFile!=null&&targetFile.exists()){

ContentBody contentBody = newFileBody(targetFile);

mpEntity.addPart("file", contentBody);

}

post.setEntity(mpEntity);

HttpResponse response = httpClient.execute(post);

content = EntityUtils.toString(response.getEntity());

httpClient.getConnectionManager().shutdown();

} catch(Exception e) {

e.printStackTrace();

}

System.out.println("=====RequestUrl==========================\n"

+getRequestUrlStrRest(serverURL, mediaInfoMap).replaceAll("&fmt=json",""));

System.out.println("=====content==========================\n"+content);

returncontent.trim();

}

其中 "file" 是分块上传服务器对分块文件参数定义的名字。细心的读者会发现,整块文件上传直接使用 Apache 官方的 InputStreamBody,而分块才使用自定义的 BlockStreamBody。

最后调用 HttpClient4Util 进行上传:

publicstaticMap uploadToDrive(

Map params, String domain) {

File targetFile = newFile(params.get("filePath"));

longtargetFileSize = targetFile.length();

intmBlockNumber =0;

if(targetFileSize

mBlockNumber = 1;

} else{

mBlockNumber = (int) (targetFileSize / GlobalConstant.CLOUD_API_LOGON_SIZE);

longsomeExtra = targetFileSize

% GlobalConstant.CLOUD_API_LOGON_SIZE;

if(someExtra >0) {

mBlockNumber++;

}

}

params.put("blockNumber", Integer.toString(mBlockNumber));

if(domain !=null) {

LOG.debug("Drive---domain="+ domain);

LOG.debug("drive---url="+"http://"+ domain +"/sync"

+ GlobalConstant.CLOUD_API_PRE_UPLOAD_PATH);

} else{

LOG.debug("Drive---domain=null");

}

String responseBodyStr = HttpClient4Util.getRest("http://"+ domain

+ "/sync"+ GlobalConstant.CLOUD_API_PRE_UPLOAD_PATH, params);

ObjectMapper mapper = newObjectMapper();

DrivePreInfo result;

try{

result = mapper.readValue(responseBodyStr, ArcDrivePreInfo.class);

} catch(IOException e) {

LOG.error("Drive.preUploadToArcDrive error.", e);

thrownewRtuploadException(GlobalConstant.ERROR_CODE_13001);// TODO

}

// JSONObject jsonObject = JSONObject.fromObject(responseBodyStr);

if(Integer.valueOf(result.getRc()) ==0) {

intuuid = result.getUuid();

String upsServerUrl = result.getUploadServerUrl().replace("https",

"http");

if(uuid != -1) {

upsServerUrl = upsServerUrl

+ GlobalConstant.CLOUD_API_UPLOAD_PATH;

params.put("uuid", String.valueOf(uuid));

for(inti =1; i <= mBlockNumber; i++) {

params.put("blockIndex",""+ i);

HttpClient4Util.restPostBlock(upsServerUrl, targetFile,

params);//

}

}

} else{

thrownewRtuploadException(GlobalConstant.ERROR_CODE_13001);// TODO

}

returnnull;

}

其中 params 这个 Map 里封装的是服务器分块上传所需要的一些参数,而上传块数也在这里进行确定。

本文中的示例经本人测试能够上传大文件成功,诸如 *.mp4 的文件上传成功没有出现任何问题。如果读者朋友测试时遇到问题无法上传成功,请在博客后跟帖留言,大家共同交流下。本文示例肯定还存在很多不足之处,如果读者朋友发现还请留言指出,笔者先行谢过了。

本博客将介绍如何进行文件的分块上传。如果读者还想了解文件的“分块”下载相关内容可以去参考博客《Java

服务器端支持断点续传的源代码【支持快车、迅雷】》。

本文侧重介绍服务器端,客户端端请参考本篇博客的姊妹篇《Java

文件分块上传客户端源代码》,关于分块上传的思想及其流程,已在该博客中进行了详细说明,这里不再赘述。

直接上代码。接收客户端 HTTP 分块上传请求的 Spring MVC 控制器源代码如下:

@Controller

publicclassUploadControllerextendsBaseController {

privatestaticfinalLog log = LogFactory.getLog(UploadController.class);

privateUploadService uploadService;

privateAuthService authService;

/**

* 大文件分成小文件块上传,一次传递一块,最后一块上传成功后,将合并所有已经上传的块,保存到File Server

* 上相应的位置,并返回已经成功上传的文件的详细属性. 当最后一块上传完毕,返回上传成功的信息。此时用getFileList查询该文件,

* 该文件的uploadStatus为2。client请自行处理该状态下文件如何显示。(for UPS Server)

*

*/

@RequestMapping("/core/v1/file/upload")

@ResponseBody

publicObject upload(HttpServletResponse response,

@RequestParam(value ="client_id", required =false) String appkey,

@RequestParam(value ="sig", required =false) String appsig,

@RequestParam(value ="token", required =false) String token,

@RequestParam(value ="uuid", required =false) String uuid,

@RequestParam(value ="block", required =false) String blockIndex,

@RequestParam(value ="file", required =false) MultipartFile multipartFile,

@RequestParamMap parameters) {

checkEmpty(appkey, BaseException.ERROR_CODE_16002);

checkEmpty(token, BaseException.ERROR_CODE_16007);

checkEmpty(uuid, BaseException.ERROR_CODE_20016);

checkEmpty(blockIndex, BaseException.ERROR_CODE_20006);

checkEmpty(appsig, BaseException.ERROR_CODE_10010);

if(multipartFile ==null) {

thrownewBaseException(BaseException.ERROR_CODE_20020);// 上传文件不存在

}

Long uuidL = parseLong(uuid, BaseException.ERROR_CODE_20016);

Integer blockIndexI = parseInt(blockIndex, BaseException.ERROR_CODE_20006);

Map appMap = getAuthService().validateSigature(parameters);

AccessToken accessToken = CasUtil.checkAccessToken(token, appMap);

Long uid = accessToken.getUid();

String bucketUrl = accessToken.getBucketUrl();

// 从上传目录拷贝文件到工作目录

String fileAbsulutePath = null;

try{

fileAbsulutePath = this.copyFile(multipartFile.getInputStream(), multipartFile.getOriginalFilename());

} catch(IOException ioe) {

log.error(ioe.getMessage(), ioe);

thrownewBaseException(BaseException.ERROR_CODE_20020);// 上传文件不存在

}

File uploadedFile = newFile(Global.UPLOAD_TEMP_DIR + fileAbsulutePath);

checkEmptyFile(uploadedFile);// file 非空验证

Object rs = uploadService.upload(uuidL, blockIndexI, uid, uploadedFile, bucketUrl);

setHttpStatusOk(response);

returnrs;

}

// TODO 查看下这里是否有问题

// 上传文件非空验证

privatevoidcheckEmptyFile(File file) {

if(file ==null|| file.getAbsolutePath() ==null) {

thrownewBaseException(BaseException.ERROR_CODE_20020);// 上传文件不存在

}

}

/**

* 写文件到本地文件夹

*

* @throws IOException

*             返回生成的文件名

*/

privateString copyFile(InputStream inputStream, String fileName) {

OutputStream outputStream = null;

String tempFileName = null;

intpointPosition = fileName.lastIndexOf(".");

if(pointPosition <0) {// myvedio

tempFileName = UUID.randomUUID().toString();// 94d1d2e0-9aad-4dd8-a0f6-494b0099ff26

} else{// myvedio.flv

tempFileName = UUID.randomUUID() + fileName.substring(pointPosition);// 94d1d2e0-9aad-4dd8-a0f6-494b0099ff26.flv

}

try{

outputStream = newFileOutputStream(Global.UPLOAD_TEMP_DIR + tempFileName);

intreadBytes =0;

byte[] buffer =newbyte[10000];

while((readBytes = inputStream.read(buffer,0,10000)) != -1) {

outputStream.write(buffer, 0, readBytes);

}

returntempFileName;

} catch(IOException ioe) {

// log.error(ioe.getMessage(), ioe);

thrownewBaseException(BaseException.ERROR_CODE_20020);// 上传文件不存在

} finally{

if(outputStream !=null) {

try{

outputStream.close();

} catch(IOException e) {

}

}

if(inputStream !=null) {

try{

inputStream.close();

} catch(IOException e) {

}

}

}

}

/**

* 测试此服务是否可用

*

* @param response

* @return

* @author zwq7978

*/

@RequestMapping("/core/v1/file/testServer")

@ResponseBody

publicObject testServer(HttpServletResponse response) {

setHttpStatusOk(response);

returnGlobal.SUCCESS_RESPONSE;

}

publicUploadService getUploadService() {

returnuploadService;

}

publicvoidsetUploadService(UploadService uploadService) {

this.uploadService = uploadService;

}

publicvoidsetAuthService(AuthService authService) {

this.authService = authService;

}

publicAuthService getAuthService() {

returnauthService;

}

}

比如要上传的文件是 test450k.mp4。对照《Java

文件分块上传客户端源代码》中分块上传服务器对分块文件参数定义的名字"file",upload 方法里使用的是 MultipartFile 接收该对象。对于每次的 HTTP 请求,使用 copyFile 方法将文件流输出到服务器本地的一个临时文件夹里,比如作者的是 D:/defonds/syncPath/uploadTemp,该文件下会有

50127019-b63b-4a54-8f53-14efd1e58ada.mp4 临时文件生成用于保存上传文件流。

分块依次上传。当所有块都上传完毕之后,将这些临时文件都转移到服务器指定目录中,比如作者的这个目录是 D:/defonds/syncPath/file,在该文件夹下会有/1/temp_dir_5_1 目录生成,而 uploadTemp 的临时文件则被挨个转移到这个文件夹下,生成形如 5.part0001 的文件。以下是文件转移的源代码:

/**

* 把所有块从临时文件目录移到指定本地目录或S2/S3

*

* @param preUpload

*/

privatevoidmoveBlockFiles(BlockPreuploadFileInfo preUpload) {

@SuppressWarnings("unchecked")

String[] s3BlockUrl=newString[preUpload.getBlockNumber()];

String[] localBlockUrl=newString[preUpload.getBlockNumber()];//本地的块文件路径    以便以后删除

List blocks = (List) getBaseDao().queryForList(

"upload.getBlockUploadFileByUuid", preUpload.getUuid());

String tempDirName = SyncUtil.getTempDirName(preUpload.getUuid(), preUpload.getUid());

String parentPath = Global.UPLOAD_ABSOLUTE_PAHT_ + Global.PATH_SEPARATIVE_SIGN

+ String.valueOf(preUpload.getUid());

String dirPath = parentPath + Global.PATH_SEPARATIVE_SIGN + tempDirName;

newFile(dirPath).mkdirs();//创建存放块文件的文件夹 (本地)

intj=0;

for(BlockUploadInfo info : blocks) {

try{

String strBlockIndex = createStrBlockIndex(info.getBlockIndex());

String suffixPath = preUpload.getUuid() + ".part"+ strBlockIndex;

String tempFilePath = info.getTempFile();

File tempFile = newFile(tempFilePath);

File tmpFile = newFile(dirPath + suffixPath);

if(tmpFile.exists()) {

FileUtils.deleteQuietly(tmpFile);

}

FileUtils.moveFile(tempFile, tmpFile);

localBlockUrl[j]=dirPath + suffixPath;

j++;

info.setStatus(Global.MOVED_TO_NEWDIR);

getBaseDao().update("upload.updateBlockUpload", info);

if(log.isInfoEnabled())

log.info(preUpload.getUuid() + " "+ info.getBuId() +" moveBlockFiles");

} catch(IOException e) {

log.error(e.getMessage(), e);

thrownewBaseException("file not found");

}

}

preUpload.setLocalBlockUrl(localBlockUrl);

preUpload.setDirPath(dirPath);

preUpload.setStatus(Global.MOVED_TO_NEWDIR);

getBaseDao().update("upload.updatePreUploadInfo", preUpload);

}

privateString createStrBlockIndex(intblockIndex) {

String strBlockIndex;

if(blockIndex <10) {

strBlockIndex = "000"+ blockIndex;

} elseif(10<= blockIndex && blockIndex <100) {

strBlockIndex = "00"+ blockIndex;

} elseif(100<= blockIndex && blockIndex <1000) {

strBlockIndex = "0"+ blockIndex;

} else{

strBlockIndex = ""+ blockIndex;

}

returnstrBlockIndex;

}

最后是文件的组装源代码:

/**

* 组装文件

*

*/

privatevoidassembleFileWithBlock(BlockPreuploadFileInfo preUpload) {

String dirPath = preUpload.getDirPath();

// 开始在指定目录组装文件

String uploadedUrl = null;

String[] separatedFiles;

String[][] separatedFilesAndSize;

intfileNum =0;

File file = newFile(dirPath);

separatedFiles = file.list();

separatedFilesAndSize = newString[separatedFiles.length][2];

Arrays.sort(separatedFiles);

fileNum = separatedFiles.length;

for(inti =0; i

separatedFilesAndSize[i][0] = separatedFiles[i];

String fileName = dirPath + separatedFiles[i];

File tmpFile = newFile(fileName);

longfileSize = tmpFile.length();

separatedFilesAndSize[i][1] = String.valueOf(fileSize);

}

RandomAccessFile fileReader = null;

RandomAccessFile fileWrite = null;

longalreadyWrite =0;

intlen =0;

byte[] buf =newbyte[1024];

try{

uploadedUrl = Global.UPLOAD_ABSOLUTE_PAHT_ + Global.PATH_SEPARATIVE_SIGN + preUpload.getUid() + Global.PATH_SEPARATIVE_SIGN + preUpload.getUuid();

fileWrite = newRandomAccessFile(uploadedUrl,"rw");

for(inti =0; i

fileWrite.seek(alreadyWrite);

// 读取

fileReader = newRandomAccessFile((dirPath + separatedFilesAndSize[i][0]),"r");

// 写入

while((len = fileReader.read(buf)) != -1) {

fileWrite.write(buf, 0, len);

}

fileReader.close();

alreadyWrite += Long.parseLong(separatedFilesAndSize[i][1]);

}

fileWrite.close();

preUpload.setStatus(Global.ASSEMBLED);

preUpload.setServerPath(uploadedUrl);

getBaseDao().update("upload.updatePreUploadInfo", preUpload);

if(Global.BLOCK_UPLOAD_TO!=Global.BLOCK_UPLOAD_TO_LOCAL)

{

//组装完毕没有问题  删除掉S2/S3上的block

String[] path=preUpload.getS3BlockUrl();

for(String string : path) {

try{

if(Global.BLOCK_UPLOAD_TO==Global.BLOCK_UPLOAD_TO_S2)

{

S2Util.deleteFile(preUpload.getBucketUrl(), string);

}else

{

S3Util.deleteFile(preUpload.getBucketUrl(), string);

}

} catch(Exception e) {

log.error(e.getMessage(), e);

}

}

}

if(log.isInfoEnabled())

log.info(preUpload.getUuid() + " assembleFileWithBlock");

} catch(IOException e) {

log.error(e.getMessage(), e);

try{

if(fileReader !=null) {

fileReader.close();

}

if(fileWrite !=null) {

fileWrite.close();

}

} catch(IOException ex) {

log.error(e.getMessage(), e);

}

}

}

BlockPreuploadFileInfo 是我们自定义的业务文件处理 bean。

OK,分块上传的服务器、客户端源代码及其工作流程至此已全部介绍完毕,以上源代码全部是经过项目实践过的,大部分现在仍运行于一些项目之中。有兴趣的朋友可以自己动手,将以上代码自行改造,看看能否运行成功。如果遇到问题可以在本博客下跟帖留言,大家一起讨论讨论。

java 分块上传_Java 文件分块上传客户端和服务器端源代码相关推荐

  1. java 图片分段上传_java文件分片上传,断点续传

    文件夹数据库处理逻辑 publicclassDbFolder { JSONObject root; publicDbFolder() { this.root =newJSONObject(); thi ...

  2. java文件异步上传_[Java教程]原生javascript实现文件异步上传

    [Java教程]原生javascript实现文件异步上传 0 2017-10-25 19:00:06 效果图: 代码:(demo33.jsp) demo33.jsp名称文件确定 本文网址:http:/ ...

  3. java实现七牛云图片文件的上传

    java实现七牛云图片文件的上传 七牛云:https://portal.qiniu.com/create#resource 首先需要去注册一个账号实现实名认证 之后打开七牛云的 我们需要先创建一个储存 ...

  4. Hadoop环境下用java代码实现hdfs远程文件的上传和下载

    Hadoop环境下用java代码实现hdfs远程文件的上传和下载 文章目录 Hadoop环境下用java代码实现hdfs远程文件的上传和下载 一.新建maven工程 二.文件的上传 三.文件的下载 四 ...

  5. .idea、target等非必要上传的文件被上传到git,如何处理?

    1..idea.target等非必要上传的文件被上传到git,如何处理? 在使用git等版本控制工具进行版本控制的时候,并不是工程所有的文件需要上传到git上进行版本管理,比如有些文件是编译工具自带的 ...

  6. java 文件下载 组件_java文件夹上传下载组件

    核心原理: 该项目核心就是文件分块上传.前后端要高度配合,需要双方约定好一些数据,才能完成大文件分块,我们在项目中要重点解决的以下问题. *如何分片: *如何合成一个文件: *中断了从哪个分片开始. ...

  7. java 调用 swf 文件上传_java文件上传方法

    文件上传方法(一次上传一个文件,多个文件的话,请写循环调用) Upload.uploadFile(theFile, filePath) 说明: theFile:类型是FormFile filePath ...

  8. java http 上传_Java使用HttpURLConnection上传文件

    从普通Web页面上传文件非常easy.仅仅须要在form标签叫上enctype="multipart/form-data"就可以,剩余工作便都交给浏览器去完毕数据收集并发送Http ...

  9. java spring mvc 上传_Java Spring MVC 上传下载文件配置及controller方法详解

    下载: 1.在spring-mvc中配置(用于100M以下的文件下载) 下载文件代码 @RequestMapping("/file/{name.rp}") public Respo ...

最新文章

  1. 基于可见光(LIFI)通信系统 的机动车智能辅助装置(课程设计)
  2. Microbiome:南京农大团队在粘细菌捕食的生态学功能方面取得重要进展
  3. 科大讯飞年报出炉,2018每天赚148万元,53%是政府补助
  4. python3 操作redis
  5. Mnist 0的波函数
  6. 用AutoML找到更小、更快、更好的模型,谷歌开源Model Search
  7. C++之Big Three:拷贝构造、拷贝赋值、析构函数探究
  8. c语言实验操作期末考试怎么把试题保存,c语言上机操作练习题_相关文章专题_写写帮文库...
  9. PAIP.ASP技术手册
  10. discuzcode函数
  11. Python imageio方法示例
  12. 华为电脑管家PcManager多屏协同功能破解
  13. “我们”是量子计算业内人士,“我们”在炒作
  14. android系统内置第三方app
  15. Spring boot项目启动报无法加载主类
  16. ai画面怎么调大小_Adobe Illustrator(Ai)里怎么改变图像大小,快捷键是什么?
  17. EasyGUI-6:文件目录和异常
  18. Feign - Error while extracting response for type [class java.lang.String]
  19. 程序猿进化 - 在拉钩子1024对APE节讲座计划
  20. Nginx✧虚拟主机资源静态化

热门文章

  1. 阿里巴巴开放平台学习
  2. OJ Problem D: 哪一天,哪一秒?
  3. 港大女生闻判拭泪 教师梦恐粉碎
  4. 阿里云oss 上传图片
  5. 【 Threejs 】- Shader 着色器实例渲染教程
  6. 无法启动oracle安装程序,oracle服务无法启动的原因及解决方法
  7. .net sdk 下载
  8. mambo维护技巧集锦(tips)(2006.11.18更新)
  9. Serverless 年终技术盘点 :工业、学术、社区遍地开花,国内厂商迅速卡位
  10. 利用CSS3 transform属性制作漂亮的照片墙特效