文章目录

  • ​​脉络​​
  • ​​FileInputStream读取​​
  • ​​Reader​​
  • ​​按行读取文件和输出文件​​
  • ​​读取目录下的所有文件(不递归所有目录)​​
  • ​​bufferWriter 为什么最后要flush一下​​
  • ​​bufferWriter设置大小无效​​
  • ​​写文件无效​​
  • ​​字节流读取并写入到一个文件​​
  • ​​restTempalte访问rest服务(小文件一次性)​​
  • ​​restTemplate访问rest服务(大文件以流的方式)​​
  • ​​原始的response返回文件流​​
  • ​​spring返回文件流​​
  • ​​原始的response返回文件流的方式​​
  • ​​文件名fileName​​
  • ​​设置文件名​​
  • ​​如何从文件流中获取文件名​​
  • ​​MultipartFile获取文件名很方便​​
  • ​​其他​​
  • ​​Content-Disposition 中的小细节​​
  • ​​httpclient获取文件流​​
  • ​​图片在浏览器展示和下载的区别​​


io流是很基础,很常用的工具。掌握好会很有用。


另,nio比io功能强大了不少,建议了解下。

为了方便演示,try catch,finally代码在这里不写了,实际中一定要加上。
相关的文件要先准备好。
inputstream是一个接口提供读的方法。
实际中要和FileInputStream配合

脉络

- InputStream
- InputStreamReader # 可以读char
- FileInputStream # 可以传入文件名

- Reader
- InputStreamReader
- FileReader
- BufferedReader

FileInputStream读取

例子代码:

public static void main(String[] args) throws Exception {
String userHome = System.getProperties().getProperty("user.home"); // 用户目录,如:C:\Users\chushiyun
String fileName = userHome+"/01.png"; // 文件路径
InputStream in = new FileInputStream(fileName);
byte[] bytes = new byte[100]; // 一次读取多少
int temp = 0; // 记录读取的byte数

// read() 读取一个字节
// read(bytes) 读满bytes字节
// read(bytes,5,20) 从5开始 读取20个字节
while ((temp = in.read(bytes,5,20)) != -1) { // 实际中别这么写
System.out.println(bytes);
}
}

单字节读取有个缺点,如果文件很大,要和磁盘做很多次交互,非常耗时。
多字节指定一次读取的字节,减少和io流的交互,速度会提高很多。

曾经做过测试1G文件测试:
单字节读取 30分钟没有读完,不等了。
100长度的bytes读取 24秒。

Reader

为什么要引入reader?
字节流很快,但有时我们需要要查看或操作读取的数据,还需要转换。
reader按字符来读取,可以直接展示。

Reader和InputStream最大的区别就是一个读取byte一个读取char。

public static void main(String[] args) throws Exception {
String userHome = System.getProperties().getProperty("user.home"); // 用户目录,如:C:\Users\chushiyun
String fileName = userHome+"/ttt.txt"; // 文件路径
Reader reader = new InputStreamReader(new FileInputStream(fileName));
char[] chars=new char[100]; // 100个字符的容器
int temp;
while ((temp = reader.read(chars,5,20)) != -1) { //实际中别这么写
System.out.println(chars);
}
}

按行读取文件和输出文件

代码:

public static void main(String[] args) {
try {
// 设置输出目录
String outputDirectory= "C:\\Users\\chushiyun\\Desktop\\xiaoxiang-output";
// 获取目录下所有文件
List<String> files = FileUtils.getFiles("C:\\Users\\chushiyun\\Desktop\\xiaoxiang-prd");
for (String file: files) {
// 获得File对象,当然也可以获取输入流对象
File fileInput = new File(file);
File fileOutput = new File(outputDirectory+"\\"+fileInput.getName());
BufferedReader bufferedReader = new BufferedReader(new FileReader(fileInput));
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(fileOutput));
String line = null;
while ((line = bufferedReader.readLine() )!=null ) {
logger.info("读取line的内容为: {}",line);
// 包含password,这个toLowerCase() 很精髓,因为配置文件里面命名很不规则,既有Password,也有passWord
// 不是注释
// 不包含ENC,防止重复编码
// 不包含salt值
if(line.toLowerCase().contains("password")
&& !line.startsWith("#")
&& !line.contains("ENC")
&& !line.contains("MazBcy1F6AwLwhkaPkg")) {
if (file.endsWith("yml")) {
line = JasyptStaticUtils.handlePasswordYml(line);
} else if (file.endsWith("properties")) {
line = JasyptStaticUtils.handlePasswordProperties(line);
}
bufferedWriter.write(line+"\r\n");
}else{
bufferedWriter.write(line+"\r\n"); // 原样输出
}
}
bufferedWriter.flush();
bufferedReader.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}

读取目录下的所有文件(不递归所有目录)

代码:

public class FileUtils {
private static org.slf4j.Logger logger = LoggerFactory.getLogger(FileUtils.class);
public static List<String> getFiles(String directory) {
List<String> files = new ArrayList<String>();
File file = new File(directory);
File[] tempList = file.listFiles();
for (int i = 0; i < tempList.length; i++) {
if (tempList[i].isFile()) {
files.add(tempList[i].toString());
//文件名,不包含路径
//String fileName = tempList[i].getName();
}
if (tempList[i].isDirectory()) {
// 文件夹递归 这里不递归
}
}
logger.info("目录{} 的文件列表是: {}", directory,JSON.toJSON(files));
return files;
}

public static void main(String[] args) {
getFiles("C:\\Users\\chushiyun\\Desktop\\xiaoxiang-prd");
}
}

bufferWriter 为什么最后要flush一下

首先要知道bufferWriter的作用是什么?
提供缓冲,先将内容放到buffer中。这样减少和io的交互次数,提高性能。

bufferWriter什么时候把数据写到文件?
1、bufferWriter满了之后,会自动写。
2、调用bufferWriter.flush() 方法。

当while循环结束时,bufferWriter不一定是满的,所以要手动flush,将里面的数据写到文件里,否则少内容。

bufferWriter设置大小无效

设置了buffer的大小,但是没有效果,也不知道为什么?

BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(fileOutput),2);

写文件无效

用idea或者eclipse的时候,有时会发现写不到文件里面。 这是什么原因呢?

如果用的class路径。 那么很有可能出现这个问题。 因为我们代码执行的目录是target,而我们idea中看到的是源码路径。 输出文件输出到target下了。 所以我们在idea中看不到。

字节流读取并写入到一个文件

代码:

public static void main(String[] args) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("D://a.jpg");
fos = new FileOutputStream("D://a-dest.jpg");
//io读数据的时候,数据存的位置(相当于传输数据的管子)
byte bs[] = new byte[1024];
int i = 0;
//read()方法返回的int类型,是表示数据下一个字节的字节码,如果已经到达流的最后面了,那就返回-1
while(i!=-1){
//read()的内容就写入新的文件
fos.write(bs,0,i);
i= fis.read(bs);
}
System.out.println("数据复制完成");
}

restTempalte访问rest服务(小文件一次性)

这种方式一次性读取,大文件的时候不好用。
代码:

@ResponseBody
@RequestMapping(value="/httpGetFile",name="http请求获取文件")
public String httpGetFile(HttpResponse response){
// 待下载的文件地址
String url = "http://test.download.net/api/download/17091547d75244e59d7450c73bddb581";
ResponseEntity<byte[]> rsp = restTemplate.getForEntity(url, byte[].class);
System.out.println("文件下载请求结果状态码:" + rsp.getStatusCode());

// 将下载下来的文件内容保存到本地
String targetPath = "D://7306.ofd";
try {
Files.write(Paths.get(targetPath), Objects.requireNonNull(rsp.getBody(),
"未获取到下载文件"));
} catch (IOException e) {
e.printStackTrace();
}
return "success";
}

restTemplate访问rest服务(大文件以流的方式)

流的方式读取,大文件也好用。
代码:

@ResponseBody
@RequestMapping(value="/httpGetBigFile",name="http请求获取大文件")
public String httpGetBigFile(HttpResponse response){
// 待下载的文件地址
String url = "http://test.download.net/api/docs/17091547d75244e59d7450c73bddb581";
// 文件保存的本地路径
String targetPath = "D://7306.ofd";
//定义请求头的接收类型
RequestCallback requestCallback = request -> request.getHeaders()
.setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
//对响应进行流式处理而不是将其全部加载到内存中
restTemplate.execute(url, HttpMethod.GET, requestCallback, clientHttpResponse -> {
Files.copy(clientHttpResponse.getBody(), Paths.get(targetPath));
return null;
});
return "success";
}

原始的response返回文件流

代码:

@ResponseBody
@GetMapping("/responseDownload")
public void responseDownload(HttpServletResponse response) throws IOException {
String fileName = "我的.jpg";
// 获取输入流
FileInputStream fis = new FileInputStream("d://" + fileName);
// 输出流
ServletOutputStream sos = response.getOutputStream();
response.setHeader("Content-Disposition","attachment;filename="+new String(fileName.getBytes(),StandardCharsets.ISO_8859_1));
// 输出
int len = 1;
byte[] b = new byte[1024];
while ((len = fis.read(b)) != -1) {
sos.write(b, 0, len);
}
fis.close();
sos.close();
}

那么,如果返回值设置为String,还可以下载吗?
实测可以,但是界面效果还是下载。返回的字符串不知道有什么用。

spring返回文件流

代码:

@ResponseBody
@RequestMapping(value="/downloadDemo",name="下载演示")
public ResponseEntity<byte[]> downloadDemo(HttpResponse response){
String fileName="我的.jpg";
File file = new File("d://"+fileName);
HttpHeaders headers = new HttpHeaders();
try {
headers.setContentDispositionFormData("fileName", new String(fileName.getBytes(),"ISO-8859-1"));
// headers.setContentDispositionFormData("fileName", fileName); // 如果不设置为ISO-8859-1的话,中文会乱码
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}

headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
ResponseEntity<byte[]> result = null;
try {
result = new ResponseEntity<byte[]>(FileUtil.readAsByteArray(file), headers,
HttpStatus.OK);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}

原始的response返回文件流的方式

文件名fileName

主要有两部分:设置文件名、获取文件名。

设置文件名

关键点就一个,header里面不能传中文,所以要先转换为iso-8859-1编码。

response.setHeader("Content-Disposition","attachment;filename="
+new String(fileName.getBytes(),StandardCharsets.ISO_8859_1));

如何从文件流中获取文件名

常用的有两种方法:

URL url = new URL(downloadUrl);
conn = (HttpURLConnection)url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(20 * 1000);
final ByteArrayOutputStream output = new ByteArrayOutputStream();
IOUtils.copy(conn.getInputStream(),output);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(output.toByteArray());

String fileName = "";
String raw = conn.getHeaderField("Content-Disposition");
if (raw != null && raw.indexOf("=") > 0) {
fileName = raw.split("=")[1];
fileName = new String(fileName.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
}

方法二:

// 方法二
String newUrl = conn.getURL().getFile();
if (newUrl == null || newUrl.length() <= 0) {
newUrl = java.net.URLDecoder.decode(newUrl, "UTF-8");
int pos = newUrl.indexOf('?');
if (pos > 0) {
newUrl = newUrl.substring(0, pos);
}
pos = newUrl.lastIndexOf('/');
fileName = newUrl.substring(pos + 1);
}

MultipartFile获取文件名很方便

MultipartFile 那么获取文件名很简单,​​file.getOriginalFilename();​​ 就可以获取到。

所以上传文件的时候不用上传文件名,不用上传文件名,不用上传文件名

其他

Content-Disposition 中的小细节

返回的结果是个数组还是字符串。
要分情况。

数组格式:
"Content-Disposition":["attachment;fileName=??.jpg"]
字符串格式:
"Content-Disposition":"attachment;fileName=??.jpg"

使用 fileName= 作为substring来判断可以吗?
一般可以,不过有时 fileName*= ,这种情况要考虑到。

httpclient获取文件流

HttpURLConnection conn = null;
try {
URL url = new URL(downloadUrl);
conn = (HttpURLConnection)url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(20 * 1000);
final ByteArrayOutputStream output = new ByteArrayOutputStream();
IOUtils.copy(conn.getInputStream(),output);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(output.toByteArray());

String fileName = "";
String raw = conn.getHeaderField("Content-Disposition");
if (raw != null && raw.indexOf("=") > 0) {
fileName = raw.split("=")[1];
fileName = new String(fileName.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
}

File destFile = new File("d://"+fileName);
logger.info("fileName:{}",fileName);
OutputStream out = new FileOutputStream(destFile);
byte [] bytes=new byte[1000];
while(byteArrayInputStream.read( bytes )!=-1){
//将缓存区中的内容写到afterfile文件中
out.write( bytes );
out.flush();

}
out.close();
} catch (Exception e) {
logger.error(e+"");
}finally {
try{
if (conn != null) {
conn.disconnect();
}
}catch (Exception e){
logger.error(e+"");
}
}

图片在浏览器展示和下载的区别

这两种用responseEntity都是可以的。
设置不同的contentType类型即可。

attachmeng是下载:

String filename = file.getName();  
response.setHeader("Content-Type","text/plain");
response.addHeader("Content-Disposition","attachment;filename=" + new String(filename.getBytes(),"utf-8"));
response.addHeader("Content-Length","" + file.length());

inline是预览:

String filename = file.getName();  
response.setHeader("Content-Type","text/plain");
response.addHeader("Content-Disposition","inline;filename=" + new String(filename.getBytes(),"utf-8"));
response.addHeader("Content-Length","" + file.length());

用原生的response预览:

OutputStream out=  response.getOutputStream();
out.write(bytes);//将缓冲区的数据输出到浏览器
out.flush();
out.close();