java中io流(文件流)的使用、资源加载笔记


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();

资源加载

资源加载不只一种方式。看情况用哪一种即可。