记一次代码重构

本贴最后更新于 3017 天前,其中的信息可能已经天翻地覆

记一次代码重构

有这么一个下载 zip 文件的功能函数

函数的目的是要下载一个文件到指定目录,第一要检查大小,第二要检查是不是 zip 文件,然后下载到指定目录。
现在的问题是这个没有超时控制,网络慢或者出问题的时候就死死的卡在那儿了

private File downloadZipFile(Task task) throws DeployScheduleException, IOException { LOGGER.debug("#Scheduler#{}#download start", task.getId()); HttpClient client = HttpClientBuilder.create().build(); HttpGet httpGet = new HttpGet(task.getArchive_url()); dbHelper.updateTaskStatusAndReason(task.getId(), "FETCHING", ""); HttpResponse response = client.execute(httpGet); long contentLength = response.getEntity().getContentLength(); long limit = sizeLimit << 20; if (contentLength > limit) { dbHelper.updateTaskLog(task.getId(), "Site archive size is more than " + sizeLimit + "M."); throw new DeployScheduleException("#Scheduler#" + task.getId() + "#Archive too big#size: {}" + contentLength); } String contentType = response.getEntity().getContentType().getValue(); if (!contentType.contains("octet-stream") && !contentType.contains("zip") && !task.getArchive_url().endsWith(".zip")) { dbHelper.updateTaskLog(task.getId(), "Project or branch not found."); throw new DeployScheduleException("#Scheduler#" + task.getId() + "#invalid content type: " + contentType); } InputStream inputStream = response.getEntity().getContent(); LOGGER.debug("tempPath" + getTempOutFile(task)); FileOutputStream outFile = new FileOutputStream(getTempOutFile(task)); LOGGER.info("#Scheduler#{}#download file at {}", task.getId(), getTempOutFile(task)); byte[] buffer=new byte[1024]; int ch; while ((ch = inputStream.read(buffer)) != -1) { outFile.write(buffer,0,ch); } inputStream.close(); outFile.flush(); outFile.close(); return new File(getTempOutFile(task)); }

第一个想法就是: 没超时控制,加上呗。

//downloadTimeout= 1200s private File downloadZipFile(Task task) throws DeployScheduleException, IOException { ... long start = System.currentTimeMillis(); String outFilePath = getTempOutFile(task); boolean successful = false; try (InputStream inputStream = response.getEntity().getContent(); FileOutputStream fos = new FileOutputStream(outFilePath)) { LOGGER.info("#Scheduler#{}#download file at {}", task.getId(), getTempOutFile(task)); byte[] buffer = new byte[1024]; int ch; while ((ch = inputStream.read(buffer)) != -1) { fos.write(buffer, 0, ch); if (System.currentTimeMillis() - start > downloadTimeout * 1000) { throw new IOException(format("#Scheduler#%d#fetching timeout: %ds", task.getId(), downloadTimeoutSeconds)); } } successful = true; fos.flush(); } finally { if (!successful) { FileUtils.deleteQuietly(new File(outFilePath)); } } return new File(getTempOutFile(task)); }

Review 时 大神们的意见就来了:

  • timeout 加上单位吧
  • httpClient 自己有超时控制,自己造轮子有风险
  • 判断条件长了语义化一下比较好

嗯嗯嗯,有道理,改改改。。。

//downloadTimeoutSeconds= 1200s private File downloadZipFile(Task task) throws DeployScheduleException, IOException { LOGGER.debug("#Scheduler#{}#download start", task.getId()); SocketConfig socketConfig = SocketConfig.custom() .setSoTimeout(downloadTimeoutSeconds * 1000) .build(); HttpGet httpGet = new HttpGet(task.getArchive_url()); final String tempOutFile = getTempOutFile(task); try(CloseableHttpClient client = HttpClientBuilder.create().setDefaultSocketConfig(socketConfig).build()) { dbHelper.updateTaskStatusAndReason(task.getId(), "FETCHING", ""); HttpEntity resEntity = client.execute(httpGet).getEntity(); long contentLength = resEntity.getContentLength(); long limit = sizeLimit << 20; if (contentLength > limit) { dbHelper.updateTaskLog(task.getId(), "Site archive size is more than " + sizeLimit + "M."); throw new DeployScheduleException("#Scheduler#" + task.getId() + "#Archive too big#size: {}" + contentLength); } String contentType = resEntity.getContentType().getValue(); if (isZipFile(task, contentType)) { dbHelper.updateTaskLog(task.getId(), "Project or branch not found."); throw new DeployScheduleException("#Scheduler#" + task.getId() + "#invalid content type: " + contentType); } try (InputStream inputStream = response.getEntity().getContent(); FileOutputStream fos = new FileOutputStream(outFilePath)) { LOGGER.info("#Scheduler#{}#download file at {}", task.getId(), getTempOutFile(task)); byte[] buffer = new byte[1024]; int ch; while ((ch = inputStream.read(buffer)) != -1) { fos.write(buffer, 0, ch); if (System.currentTimeMillis() - start > downloadTimeout * 1000) { throw new IOException(format("#Scheduler#%d#fetching timeout: %ds", task.getId(), downloadTimeoutSeconds)); } } fos.flush(); } } catch (SocketTimeoutException e) { FileUtils.deleteQuietly(new File(tempOutFile)); throw new IOException(String.format("#Scheduler#%d#fetching timeout: %ds", task.getId(), downloadTimeoutSeconds)); } return new File(tempOutFile); } private boolean isZipFile(Task task, String contentType) { return !contentType.contains("octet-stream") && !contentType.contains("zip") && !task.getArchive_url().endsWith(".zip"); }

这下应该没问题了吧,继续提交
又有人质疑了这段

try (InputStream inputStream = response.getEntity().getContent(); FileOutputStream fos = new FileOutputStream(outFilePath)) { LOGGER.info("#Scheduler#{}#download file at {}", task.getId(), getTempOutFile(task)); byte[] buffer = new byte[1024]; int ch; while ((ch = inputStream.read(buffer)) != -1) { fos.write(buffer, 0, ch); if (System.currentTimeMillis() - start > downloadTimeout * 1000) { throw new IOException(format("#Scheduler#%d#fetching timeout: %ds", task.getId(), downloadTimeoutSeconds)); } } fos.flush(); }
都什么时候还用这么原始的方式, JDK nio 啊,Files.copy() 一下搞定,还不比你写的优秀啊

去翻翻源码还真有这么个

/** * Copies all bytes from an input stream to a file. On return, the input * stream will be at end of stream. * * <p> By default, the copy fails if the target file already exists or is a * symbolic link. If the {@link StandardCopyOption#REPLACE_EXISTING * REPLACE_EXISTING} option is specified, and the target file already exists, * then it is replaced if it is not a non-empty directory. If the target * file exists and is a symbolic link, then the symbolic link is replaced. * In this release, the {@code REPLACE_EXISTING} option is the only option * required to be supported by this method. Additional options may be * supported in future releases. * * <p> If an I/O error occurs reading from the input stream or writing to * the file, then it may do so after the target file has been created and * after some bytes have been read or written. Consequently the input * stream may not be at end of stream and may be in an inconsistent state. * It is strongly recommended that the input stream be promptly closed if an * I/O error occurs. * * <p> This method may block indefinitely reading from the input stream (or * writing to the file). The behavior for the case that the input stream is * <i>asynchronously closed</i> or the thread interrupted during the copy is * highly input stream and file system provider specific and therefore not * specified. * * <p> <b>Usage example</b>: Suppose we want to capture a web page and save * it to a file: * <pre> * Path path = ... * URI u = URI.create("http://java.sun.com/"); * try (InputStream in = u.toURL().openStream()) { * Files.copy(in, path); * } * </pre> * * @param in * the input stream to read from * @param target * the path to the file * @param options * options specifying how the copy should be done * * @return the number of bytes read or written * * @throws IOException * if an I/O error occurs when reading or writing * @throws FileAlreadyExistsException * if the target file exists but cannot be replaced because the * {@code REPLACE_EXISTING} option is not specified <i>(optional * specific exception)</i> * @throws DirectoryNotEmptyException * the {@code REPLACE_EXISTING} option is specified but the file * cannot be replaced because it is a non-empty directory * <i>(optional specific exception)</i> * * @throws UnsupportedOperationException * if {@code options} contains a copy option that is not supported * @throws SecurityException * In the case of the default provider, and a security manager is * installed, the {@link SecurityManager#checkWrite(String) checkWrite} * method is invoked to check write access to the file. Where the * {@code REPLACE_EXISTING} option is specified, the security * manager's {@link SecurityManager#checkDelete(String) checkDelete} * method is invoked to check that an existing file can be deleted. */ public static long copy(InputStream in, Path target, CopyOption... options) throws IOException {...}

最后忽然觉得用 throw 来终止过程好像还可以精简。
哟西,最终结果出炉。

private File downloadZipFile(Task task) throws DeployScheduleException, IOException { LOGGER.debug("#Scheduler#{}#download start", task.getId()); SocketConfig socketConfig = SocketConfig.custom() .setSoTimeout(downloadTimeoutSeconds * 1000) .build(); HttpGet httpGet = new HttpGet(task.getArchive_url()); final String tempOutFile = getTempOutFile(task); try(CloseableHttpClient client = HttpClientBuilder.create().setDefaultSocketConfig(socketConfig).build()) { dbHelper.updateTaskStatusAndReason(task.getId(), "FETCHING", ""); HttpEntity responseEntity = client.execute(httpGet).getEntity(); checkContentLength(task, responseEntity); checkContentType(task, responseEntity); try (InputStream inputStream = responseEntity.getContent()) { Files.copy(inputStream, Paths.get(tempOutFile), StandardCopyOption.REPLACE_EXISTING); } } catch (SocketTimeoutException e) { FileUtils.deleteQuietly(new File(tempOutFile)); throw new IOException(String.format("#Scheduler#%d#fetching timeout: %ds", task.getId(), downloadTimeoutSeconds)); } return new File(tempOutFile); } private void checkContentType(Task task, HttpEntity resEntity) throws DeployScheduleException { String contentType = resEntity.getContentType().getValue(); if (isZipFile(task, contentType)) { dbHelper.updateTaskLog(task.getId(), "Project or branch not found."); throw new DeployScheduleException("#Scheduler#" + task.getId() + "#invalid content type: " + contentType); } } private void checkContentLength(Task task, HttpEntity resEntity) throws DeployScheduleException { long contentLength = resEntity.getContentLength(); // 1 << 20 = 1M long limit = sizeLimit << 20; if (contentLength > limit) { dbHelper.updateTaskLog(task.getId(), "Site archive size is more than " + sizeLimit + "M."); throw new DeployScheduleException("#Scheduler#" + task.getId() + "#Archive too big#size: {}" + contentLength); } } private boolean isZipFile(Task task, String contentType) { return !contentType.contains("octet-stream") && !contentType.contains("zip") && !task.getArchive_url().endsWith(".zip"); }

重构小结

  • 对类库的使用要熟悉,多测试,少写很多代码而且语义清晰
  • 长判断条件提炼方法,语义化更方便看
  • 要多看看 JDK 的东西,很多基础的操作里面都有
  • 配置参数如果有单位最好加上单位,减少使用风险
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3201 引用 • 8216 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • yangyujiao 1 via macOS

    92364c4dced24a9d8f0174fe89fb7f9c.png

    我去,,,我保存图片直接网上找了一个。。。上图。。
    是不是很 low,是不是可以用这个啥 files.copy 的。。。

    2 回复
  • yangyujiao via macOS

    哎,我的 chrome,又罢工了,一点回复就挂了。。
    最近都不代码走查了,也没人给我写的烂代码优化优化了。。。

  • Angonger

    饺子,你家老董叫董建文吧

    1 回复
  • yangyujiao via macOS

    哇,你好聪明噢 👏
    @88250 过了一天了,我的 chrome,还是点开评论,以打字就挂了···

    1 回复
  • yangyujiao via macOS

    d0f3a99c34b64880bade3d8d45f1efde.png

    我网上找的那个方法,测试时候用了论坛里我跟随便一个人的头像链接。就是如上的地址,是可以下载到我的本地的。

    但是用 Files.copy 是不行的。

    然后我又用了本地的两个图片。 下图:1023a436f2a546348faa1ab55a7738c6.png

    然后程序用下图两种定义都是可以的:
    3bfc094ec7294387b0bbe4b18658c9d1.png

    但是我现在获取的图片会是从另一台服务器。那是不是就不能用 Files.copy()这个方法???

    1 回复
  • tiangao via macOS

    只要是一个正确的 inputStream Files.copy() 肯定是可以用的。

  • tiangao via macOS

    代码问题还是贴代码吧,我还能拷到 IDE 里看看,这上个图片看起来真费劲

    3 回复
  • yangyujiao via macOS

    我不会用 markdown 形式处理代码。。。。
    直接粘贴了。。。能看就看,看不懂就不用解决了,等我用到这里的时候选一种就行了。。。
    多谢。
    controller:
    public Response imageSave() {
    // String imageUrl1 = "https://b3logfile.com/avatar/1429237272905_1483064274321.undefined?imageView2/1/w/242/h/242/interlace/0/q/100";
    // String imageUrl2 = "https://b3logfile.com/avatar/1474337542814_1483764752915.jpeg?imageView2/1/w/242/h/242/interlace/0/q/100";
    String imageUrl1 = "/Users/dongjianwen/Downloads/test/test0.png";
    String imageUrl2 = "/Users/dongjianwen/Downloads/test/test1.png";
    List list = new ArrayList<>();
    list.add(imageUrl1);
    list.add(imageUrl2);
    boolean tof = true;
    if (list.size() > 0) {
    for (String imageUrl : list) {
    String imagePath = ImageHandel.imageSave(imageUrl);
    if (StringUtil.isEmpty(imagePath)) {
    tof = false;
    break; } else {
    // TODO 将路径保存到数据库中
    }
    }
    }
    if (tof) {
    return new Response<>(ResponseCode.SUCCESS);
    } else {
    return new Response<>(ResponseCode.SERVICE_ERR.getCode(), "保存图片异常");
    }
    }

  • yangyujiao via macOS

    保存的方法 超长了······

  • yangyujiao via macOS

    保存图片的 imageSave 方法主要是下面几行

    // 输出的文件流
    File sf = new File("/Users/dongjianwen/Downloads/test/");
    if (!sf.exists()) {
    sf.mkdirs();
    }
    // 取得文件扩展名
    String imgExt = imageUrl.substring(imageUrl.lastIndexOf(".") + 1, imageUrl.length()).toLowerCase();
    // TODO 测试的网上 url,扩展名有问题,设定默认.png
    imgExt = "png";

    // 随机为文件取得名字(随机 10 个字母数字的组合)
    String imgName = StringUtil.createRandomString(10, false) + "." + imgExt;
    // 服务器保存的图片路径
    path = sf.getPath() + File.separator + imgName;

    // os = new FileOutputStream(path);
    // // 开始读取
    // while ((len = is.read(bs)) != -1) {
    // os.write(bs, 0, len);
    // }

    // method1:File source = new File(imageUrl);
    File dest = new File(path);

    Files.copy(source.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING);

    1 回复
  • tiangao via macOS

    path 获取可以用 Paths.get("") 这个静态方法.

    看起来没有问题,不过你这里参数是 Files.copy(Path, Path, options) 这个了,建议进去看看方法的注释

    2 回复
  • yangyujiao via macOS

    // method2:
    // Path source = Paths.get(imageUrl);
    // Path dest = Paths.get(path);
    //
    // Files.copy(source, dest);

    这个 我也用过了 也是不行的。。。
    我其实测试用的是 Files.copy(Path, Path) 后面那个参数是后来看网上加了 我就随便加上了,好像是个//的转义什么的。

  • yangyujiao via macOS

    // String imageUrl1 = "https://b3logfile.com/avatar/1429237272905_1483064274321.undefined?imageView2/1/w/242/h/242/interlace/0/q/100";
    // String imageUrl2 = "https://b3logfile.com/avatar/1474337542814_1483764752915.jpeg?imageView2/1/w/242/h/242/interlace/0/q/100";

    String imageUrl1 = "/Users/dongjianwen/Downloads/test/test0.png";
    String imageUrl2 = "/Users/dongjianwen/Downloads/test/test1.png";

    用上面那个两个 url copy 这个方法是不行的,用我之前那个构造 url 的方式可以。所以我不知道这个 copy 是不是不能针对比如 网上的图片链接。

    2 回复
  • tiangao via macOS

    网上的不能用 path ,要用我用的那个 inputStream 签名的方式。

    我那个下载的就是网络文件

  • tiangao via macOS

    获取到网络的输入流就行了, 最后一个参数看命名嘛,你用的那个意思是 如果文件已经存在就替换掉。

    2 回复
  • yangyujiao via macOS

    明白了 d9482da2964146678a49112df9a022e8.png

    这样是可以的,多谢
    我之前没有做过从别的服务器传过来的图片保存,但是我觉得应该类似网上的一个图片链接这种,等真的链接运营调试时候在看看。

    多谢

  • yangyujiao via macOS

    那是不是我的那个保存 path,如果不是本地电脑,也是另一台服务器。用 ip 来链接,也不能用 Paths 了。。。

    1 回复
  • tiangao via macOS

    这个需求本身有问题,图片从 A 服务器到 B 服务器 为什么要 先下载到 C 服务器,如果真需要这样做那应该是 B 服务器上的程序来做,C 服务器只要调用一下,由 B 服务器来工作,把文件下载到自己这里。

    1 回复
  • yangyujiao via macOS

    我们的需求是:
    运营服务器的图片同步到 java 服务器,java 里面处理转存到图片服务器。所以其实是 A--B--C。

    1 回复
  • tiangao via macOS

    哦,那第二个过程属于上传,这里是下载。是需要换个方式了

    1 回复
  • yangyujiao via macOS

    多谢 等后面开始写了 再改。

请输入回帖内容 ...