相信对大部分从事JAVA开发的同学来说,相对于复杂的中间件或者是底层软件的设计开发工作,更多的时候做的是普通的常规功能的开发。可能有人会觉得这些功能开发已经烂大街而忽视,这些我们平时常常开发的功能其实并没有想象中简单,下面总结了一些避坑指南,希望能对JAVA新手们有帮助,更好地完成平时的开发工作。
文件下载
用Springboot实现一个下载功能,总共不过十来行,so easy!
的确是简单,但也要注意一个不小心掉坑里。看下面的代码,是实现此功能的其中一种常见方式,有没有发现有什么问题?
@GetMapping("/download")
public void download(String fileName, HttpServletResponse httpServletResponse) {
String fileBasePath = "目标文件所在目录";
File file = new File(String.format("%s/%s", fileBasePath, fileName));
try(FileInputStream fis = new FileInputStream(file)) {
byte[] fileBytes = new byte[fis.available()];
fis.read(fileBytes);
httpServletResponse.reset();
httpServletResponse.setHeader("Content-Disposition", "attachement;filename=" + URLEncoder.encode(file.getName(), "utf-8"));
httpServletResponse.setHeader("Pragma", "no-cache");
httpServletResponse.setHeader("Cache-Control", "no-cache");
httpServletResponse.setContentType("application/otect-stream;charset=UTF-8");
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(httpServletResponse.getOutputStream());
bufferedOutputStream.write(fileBytes);
bufferedOutputStream.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
这种写法最大的问题在于将文件数据写到响应流之前会先将目标文件的所有内容读入到内存中,当文件较小且该接口并发量不高的时候,功能正常运转。可是当下载的是大文件(比如说几十个G的文件。。。),或者接口有大的并发压力时,这种写法占用JVM内存的影响就会相当明显,甚至会耗尽进程剩余所有内存,导致系统崩溃。
内存耗尽的问题在申请内存分配这行便会出现:
byte[] fileBytes = new byte[fis.available()];
安全的实现方式是申请一个固定大小的数组做缓存,一边读文件一边将内容写到输出流中:
@GetMapping("/download")
public void download(String fileName, HttpServletResponse httpServletResponse) {
String fileBasePath = "目标文件所在目录";
File file = new File(String.format("%s/%s", fileBasePath, fileName));
try(FileInputStream fis = new FileInputStream(file)) {
httpServletResponse.reset();
httpServletResponse.setHeader("Content-Disposition", "attachement;filename=" + URLEncoder.encode(file.getName(), "utf-8"));
httpServletResponse.setHeader("Pragma", "no-cache");
httpServletResponse.setHeader("Cache-Control", "no-cache");
httpServletResponse.setContentType("application/otect-stream;charset=UTF-8");
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(httpServletResponse.getOutputStream());
byte[] buffer = new byte[1024];
int length;
while ((length = fis.read(buffer)) > -1) {
bufferedOutputStream.write(buffer, 0, length);
}
bufferedOutputStream.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
线程创建与线程池
线程创建尽量用线程池,这是业界给出使用线程的最佳实践。
用new Thread() API的方式创建线程的问题,更多地是出现在线程被频繁创建和销毁的场景。如创建线程进行异步调用:执行耗时较长的批处理,发送短信,发送邮件等场景。当并发量较高时,线程就会被大量地创建,造成短时间内线程积压,占用系统内存以及频繁的线程上下文切换,使得应用的性能开始下降。
然而,更严重的问题在于,通常情况下操作系统会对进程可创建的线程数作了限制。例如linux系统默认允许创建的最大线程数是1024,当线程数超过系统默认阀值时,JAVA便会出现 java.lang.OutOfMemoryError: Unable to create new native thread 错误导致程序失败。
线程不应该是朝生夕死的对象,因为它的创建和销毁不是轻量级的,频繁的创建和销毁只会浪费操作系统资源。所以当遇到需要频繁使用线程的场景,比起直接创建线程,我们更应该的是使用线程池。线程池的作用就是管理着一定数量的可用线程供调用代码使用,这些线程的生命周期由线程池负责管理。
下面是线程池的简单使用例子:
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
Future<Boolean> future = executorService.submit(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
Thread.sleep(1000);//模拟执行耗时的异步调用
return true; //返回执行后的返回值,如果不需要返回值,也可选择实现Runnable接口
}
});
//同步等待结果,看需求,不是必须
try {
boolean result = future.get();
System.out.println("执行结果:" + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
executorService.shutdown();
}
细节决定成败,无论多么小的开发工作都值得我们认真对待,所有伟大的软件都是由一个个小功能堆砌而成的,不是吗?
文章讲述了JAVA开发中常见的两个问题及解决方案:一是文件下载时一次性加载大文件到内存可能导致内存耗尽,应使用缓冲流分块读写;二是频繁创建线程会消耗资源,推荐使用线程池管理线程,以提高效率和避免系统崩溃。
13万+

被折叠的 条评论
为什么被折叠?



