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

本文深入讲解Java中的IO流,包括字节流、字符流、文件读写、缓冲流的使用技巧,以及通过REST模板访问服务的多种方式。同时,探讨了文件流在不同场景下的应用,如文件复制、大文件下载和文件名处理。


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返回文件流(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返回文件流(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返回文件流的方式(jsonResult这种形式不好,仅供参考)

/*
 * 下载文件
 */
@RequestMapping("/web/file/downloadFile")
public JsonResult downloadFile(@RequestParam(name = "filePath") String filePath, HttpServletResponse response) {
    String methodName = "文件功能_下载文件";
    JsonResult result = new JsonResult<>();
    try {

        log.info(methodName + "方法开始,filePath={}", filePath);
        // 这里相当于一个完全的地址来下载了
        CustomAssertUtils.isNotEmpty(filePath, "filePath不能为空");

        File file = new File(filePath);
        String fileName = filePathUtils.generateFileNameFromPath(filePath);
        FileInputStream in = new FileInputStream(file);
        response.setContentType("application/octet-stream");
        response.setContentLength((int) file.length());
        String headerKey = URLEncoder.encode(fileName, "UTF-8");
        response.setHeader("Content-Disposition", "attachment; filename=\"" + headerKey + "\"");

        // 输出流写入客户端
        BufferedInputStream bis = new BufferedInputStream(in);
        OutputStream out = response.getOutputStream();
        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = bis.read(buffer)) != -1) {
            out.write(buffer, 0, bytesRead);
        }
        bis.close();
        out.close();
        log.info(methodName + "方法完成");
        return result;
    } catch (Exception e) {
        log.error(methodName + "方法异常,error=", e);
        return JsonResult.error("-1", methodName + "方法异常,error=" + e.getMessage());
    }
}

注:文件名乱码的问题,用这行代码就可以解决。

String headerKey = URLEncoder.encode(fileName, "UTF-8");

为什么要编码呢? 因为文件名是在url中传给客户端的,不支持中文,所以要先编码,到达客户端(这里的客户端是浏览器)会自动进行解码。

下载文件(推荐)

1、正确方式应该只有两种,void或ResponseEntity
2、返回的格式只能是确定的一种
方案可以这样做
(1)成功时返回文件流,即下载文件
(2)失败时返回json报文,但是不是通过返回值定义的,而是response.write写的json结构体

@RequestMapping("/file/download")
    public void download(HttpServletResponse response, String relativePath){
        String methodName = "下载文件"; // 建议将这里的“上传文件”修正为“下载文件”
        try {
            // 1. 安全校验:将相对路径转换为绝对路径,并校验是否越权
            File baseDir = new File(rootFilePath); // 允许的根目录
            File targetFile = new File(baseDir, relativePath).getCanonicalFile();

            if (!targetFile.exists() || !targetFile.getPath().startsWith(baseDir.getCanonicalPath())) {
//                response.sendError(HttpServletResponse.SC_NOT_FOUND, "File not found or access denied");
                throw new IllegalArgumentException("文件未找到或无权限");
            }

            // 2. 设置规范的响应头
            response.setContentType("application/octet-stream");
            String originalFileName = targetFile.getName();
            String decodedFileName = URLDecoder.decode(originalFileName, StandardCharsets.UTF_8);
            ContentDisposition disposition = ContentDisposition.attachment()
                    .filename(decodedFileName, StandardCharsets.UTF_8)
                    .build();
            response.setHeader("Content-Disposition", disposition.toString());

            response.setHeader("Content-Length", String.valueOf(targetFile.length()));
            // 防止 Nginx 等反向代理缓存整个响应体
            response.setHeader("X-Accel-Buffering", "no");

            // 3. 使用 NIO 零拷贝技术进行高效流式传输
            try (RandomAccessFile raf = new RandomAccessFile(targetFile, "r");
                 FileChannel channel = raf.getChannel();
                 OutputStream os = response.getOutputStream();
                 WritableByteChannel wbChannel = Channels.newChannel(os)) {
                // transferTo 直接在内核空间传输,绕过 JVM 堆内存,性能极高
                channel.transferTo(0, channel.size(), wbChannel);
            } catch (IOException e) {
                // 客户端中途取消下载会抛出异常,生产环境应捕获并优雅处理
                log.warn("文件下载中断或失败: {}", targetFile.getName(), e.getMessage());
            }
        } catch (IllegalArgumentException e){
            log.error(methodName + " 操作失败, error=", e);
            IOResultUtils.error(response,HttpServletResponse.SC_INTERNAL_SERVER_ERROR,"-1",e.getMessage());
        } catch (Exception e){
            log.error(methodName + " 操作异常, error=", e);
            IOResultUtils.error(response,HttpServletResponse.SC_INTERNAL_SERVER_ERROR,"-1",e.getMessage());
        }
    }

IOResultUtils代码见另外一篇笔记。

文件名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(); 就可以获取到。

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

multipartFile和file相互转换

根据file获取multipartFile:

MultipartFile multipartFile = new MockMultipartFile("copy"+file.getName(),file.getName(),ContentType.APPLICATION_OCTET_STREAM.toString(),fileInputStream);

multipartFile转换为file:

String fileName = file.getoriginalFilename();
byte[]filecontent = file.getBytes();
file.transferTo(new File("path/to/save/"+ fileName));
fileController 上传和根据url下载文件
/*
 * 定制文件功能
 */
@RestController
@RequestMapping("/file/interface")
public class FileController {
    private static Logger logger = LoggerFactory.getLogger(FileController.class);

    @Value("${fileUploadPath:d:/data/upload/jxcustom}")
    private String fileUploadPath;

    @ResponseBody
    @RequestMapping(value="/upload",name="文件上传")
    public JsonResult upload(@RequestParam(value="file",required = false) MultipartFile file){
        try {

            JsonResult result = new JsonResult<>();
            logger.info("定制文件功能_文件上传请求开始");
            if(file==null || file.isEmpty()){
                return JsonResult.error("-1","file是必填项");
            }
            // 保存文件
            Path filePath = Paths.get(fileUploadPath + File.separator + file.getName());
            logger.info("定制文件功能_文件上传,filePath={}",filePath.toString());
            long copyResult = Files.copy(file.getInputStream(), filePath);
            result=JsonResult.success(copyResult);
            logger.info("定制文件功能_文件上传请求完成,result={}", JSON.toJSONString(result));
            return result;
        }catch (Exception e){
            logger.error("定制文件功能_文件上传请求异常,error=",e);
            return JsonResult.error("-1","定制文件功能_文件上传请求异常");
        }
    }


    /*
     * 下载byUrl
     */
    @ResponseBody
    @RequestMapping(value="/downloadByUrl",name="文件上传")
    public JsonResult downloadByUrl(@RequestBody FileDownloadRequest request){
        JsonResult result = new JsonResult<>();
        String methodName="定制文件功能_下载byUrl";
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            logger.info(methodName+"请求开始,request={}",JSON.toJSONString(request));
            if(StringUtils.isEmpty(request.getUrl())){
                logger.info(methodName+"请求失败,url不能为空");
                return JsonResult.error("-1",methodName+"请求失败,url不能为空");
            }
            if(StringUtils.isEmpty(request.getFileName())){
                logger.info(methodName+"请求失败,fileName不能为空");
                return JsonResult.error("-1",methodName+"请求失败,fileName不能为空");
            }


            URL url = new URL(request.getUrl());
            //这里没有使用 封装后的ResponseEntity 就是也是因为这里不适合一次性的拿到结果,放不下content,会造成内存溢出
            HttpURLConnection connection =(HttpURLConnection) url.openConnection();

            //使用bufferedInputStream 缓存流的方式来获取下载文件,不然大文件会出现内存溢出的情况
            inputStream = new BufferedInputStream(connection.getInputStream());
            String path=fileUploadPath+File.separator +request.getFileName();
            File file = new File(path);
            if (file.exists()) {
                file.delete();
            }
            outputStream = new FileOutputStream(file);
            //这里也很关键每次读取的大小为5M 不一次性读取完
            byte[] buffer = new byte[1024 * 1024 * 5];// 5MB
            int len = 0;
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
            connection.disconnect();
            return result;
        }catch (Exception e){
            logger.error(methodName+"请求异常,error=",e);
            return JsonResult.error("-1",methodName+"请求异常");
        }finally {
            IOUtils.closeQuietly(outputStream);
            IOUtils.closeQuietly(inputStream);
            logger.info(methodName+"请求关闭流完毕");
        }
    }
}

注:上传的功能是否好用先持保留态度,根据url下载实测可用。(解决了应急问题)

流式下载(零拷贝下载)
@RequestMapping("/file/download")
public JsonResult download(HttpServletResponse response, String relativePath){
    String methodName = "下载文件"; // 建议将这里的“上传文件”修正为“下载文件”
    JsonResult result = JsonResult.ok();
    try {
        // 1. 安全校验:将相对路径转换为绝对路径,并校验是否越权
        File baseDir = new File(rootFilePath); // 允许的根目录
        File targetFile = new File(baseDir, relativePath).getCanonicalFile();

        if (!targetFile.exists() || !targetFile.getPath().startsWith(baseDir.getCanonicalPath())) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND, "File not found or access denied");
            throw new IllegalArgumentException("文件未找到或无权限");
        }

        // 2. 设置规范的响应头
        response.setContentType("application/octet-stream");
        String originalFileName = targetFile.getName();
        String decodedFileName = URLDecoder.decode(originalFileName, StandardCharsets.UTF_8);
        ContentDisposition disposition = ContentDisposition.attachment()
                .filename(decodedFileName, StandardCharsets.UTF_8)
                .build();
        response.setHeader("Content-Disposition", disposition.toString());

        response.setHeader("Content-Length", String.valueOf(targetFile.length()));
        // 防止 Nginx 等反向代理缓存整个响应体
        response.setHeader("X-Accel-Buffering", "no");

        // 3. 使用 NIO 零拷贝技术进行高效流式传输
        try (RandomAccessFile raf = new RandomAccessFile(targetFile, "r");
             FileChannel channel = raf.getChannel();
             OutputStream os = response.getOutputStream();
             WritableByteChannel wbChannel = Channels.newChannel(os)) {
            // transferTo 直接在内核空间传输,绕过 JVM 堆内存,性能极高
            channel.transferTo(0, channel.size(), wbChannel);
        } catch (IOException e) {
            // 客户端中途取消下载会抛出异常,生产环境应捕获并优雅处理
            log.warn("文件下载中断或失败: {}", targetFile.getName(), e.getMessage());
        }
        return result;
    } catch (IllegalArgumentException e){
        log.error(methodName + " 操作失败, error=", e);
        return JsonResult.fail("-1", "操作失败," + e.getMessage());
    } catch (Exception e){
        log.error(methodName + " 操作异常, error=", e);
        return JsonResult.fail("-1", "操作异常,请联系系统管理员");
    }
}

上传下载方案

1、通用方案,上传下载都在fileController实现 # 简单快速,不细分功能(如果要细分功能也可以在上传接口加功能字段)
2、上传在不同controller,下载传相对路径,共用fileController的download方法 # 细分功能
3、上传下载都用单独的文件系统,如docid # 需要单独的文件微服务,或建一张document表

其他

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

文件入口的几种方式

主要是写接口的时候用到,整理下会更清晰。

这也是io流较复杂的原因之一,它的形态较多,每种都需要掌握。

1、filePath

本地调试的时候用这个,filePath一般传实际文件路径。

2、url

传入在线url地址,客户可以操作。

3、file

文件的形式。和filePath有点不同,一个是地址,一个直接是文件。

4、multipartFile

类似于界面的点文件上传,客户可以操作。

5、base64文件流

一般是中台服务接口,因为用户肯定不会直接操作base64。

6、inputstream

这应该算一个中间的形式,但是肯定也有这个口。

以上状态都是实际存在的,如果输入输出转换,那么实际是乘积的关系,确实有点多。

另外,有时还伴随着删除目录等操作,那么复杂度就更高了。

资源加载

见资源加载笔记。

filePath为什么难以理解

在不同的工具中,表示的意思也不一样。
例如在minio中,filePath和fileName是分开的。
在文件流中,new File(filePath+fileName) # 这个路径一般是全路径,因为java的file是支持目录也支持文件的。

看到了吧,所以要点就2点。
1、区分清楚filePath是路径还是实际文件
2、区分清楚,该插件传的路径是dir还是文件名

另外,不同场景还有些区分点,如磁盘路径前缀要以"/“开头,但是minio路径恰恰不以”/"开头,一定要区分开

生成多级目录

这个实际特别简单,只有一个小小的注意点。

代码:

public static void main(String[] args) {
    String dirs="d://data//a.txt";
    File fileDir = new File(dirs);
    // 是这样,执行这个方法就会穿件多级目录,但是需要确认这个路径是文件夹
    // 如果计划是文件名 如 d://data/a.txt 会创建a.txt文件夹,这一点需要注意
    boolean mkdirs = fileDir.mkdirs();
    log.info("mkdirs={}",mkdirs);
}

用file.mkdirs()方法即可。
注:需要确定该file是文件夹的路径,如果带有文件名,也会创建为文件,这点需要注意

如:
d:/data/file # /data/file文件夹。
d:/data/a.txt # a.txt也会创建为文件夹,可能和我们期望的不一致

io流中诡异的404问题

遇到过一个问题,接口路径明明对,就是报错404,可太奇怪了。

这个可以下载文件:
{{url}}/mysite/api/file/download?relativePath=操作手册.docx

这个报错404:
{{url}}/mysite/api/file/download?relativePath=操作手册_777.docx

后来找到了问题:

 if (!targetFile.exists() || !targetFile.getPath().startsWith(baseDir.getCanonicalPath())) {
		// 关键就在这句行,response发送404错误,被spring拦截到了,所以返回的404
    response.sendError(HttpServletResponse.SC_NOT_FOUND, "File not found or access denied");
    throw new IllegalArgumentException("文件未找到或无权限");
}

解决方案:
弃用response.sendError()这种方式,改为自定义处理异常,再通过response.write()输出。

io流中没响应的问题

场景:
文件没下载,也没返回json。

后来找到问题了,httpStatus返回的是自定义的-1,servlet底层容器解析异常,所以就处理不正确。

解决方案:
1、错误码用http可以识别的错误码,如500
2、然后用response.write()输出自定义的jsonResult

印象中好像确实见过点下载既返回了报文,又下载了文件的

先说结论:
一次请求的返回结构固定的,不可能既返回报文,又下载文件。

如果确实看到了这个效果,可能是前端调用了两次请求:
例如导出excel
1、点导出先返回报文,里面包含下载地址
2、前端再下载这个文件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值