记一次代码重构

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

记一次代码重构

有这么一个下载 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 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3194 引用 • 8214 回帖

相关帖子

欢迎来到这里!

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

注册 关于
请输入回帖内容 ...