在Servlet应用中保存上传文件的推荐方式

127
我在这里阅读到,不应该将文件保存在服务器上,因为它不具备可移植性、事务性并且需要外部参数。然而,考虑到我需要一个临时解决方案来处理tomcat(7),并且我对服务器有相对的控制权,我想知道:
  • 最佳的文件保存位置在哪里?我应该将它保存在/WEB-INF/uploads(不建议这里)或某个位于$CATALINA_BASE下的地方(参见这里)等等?JavaEE 6教程从用户获取路径(:wtf:)。注意:该文件不应以任何方式可下载。

  • 我是否应该设置一个详细的配置参数,如这里所述?我需要一些代码(我宁愿给出相对路径-这样至少是Tomcat便携的) - Part.write()看起来很有前途 - 但显然需要一个绝对路径。

  • 我对这种方法与数据库/JCR存储库方法的缺点感兴趣。

很遗憾,@BalusC的FileServlet只专注于下载文件,而他关于上传文件的answer省略了保存文件的部分。
最好使用可转换为使用DB或JCR实现(如jackrabbit)的解决方案。

请参见我最终的实现方式下面的答案 - Mr_and_Mrs_D
3个回答

171

除了IDE的项目文件夹(即服务器的部署文件夹)以外,可以将其存储在任何可访问位置。有关问题答案,请参见上传的图片只有在刷新页面后才能使用

  1. IDE的项目文件夹中的更改不会立即反映在服务器的工作文件夹中。IDE中有一种后台任务会确保服务器的工作文件夹与最新更新同步(在IDE术语中称为“发布”)。这是你看到的问题的主要原因。

  2. 在现实世界的代码中,将上传的文件存储在Web应用程序的部署文件夹中可能根本行不通。某些服务器(默认情况下或通过配置)不会将部署的WAR文件展开到本地磁盘文件系统中,而是完全存在于内存中。你无法在内存中创建新文件,除非基本上编辑已部署的WAR文件并重新部署它。

  3. 即使服务器将部署的WAR文件展开到本地磁盘文件系统中,所有新创建的文件也将在重新部署或仅仅是简单重启时丢失,因为这些新文件不是原始WAR文件的一部分。

对我或任何人来说,它在本地磁盘文件系统的确切位置并不重要,只要你绝不要使用getRealPath()方法。在任何情况下,使用该方法都是令人担忧的。

存储位置的路径可以通过多种方式定义,并且您必须自己完成。也许这就是您混淆的原因,因为您以某种方式预期服务器会自动处理所有内容。请注意,@MultipartConfig(location)并未指定最终上传目标,而是用于文件大小超过内存存储阈值的临时存储位置。

因此,最终存储位置的路径可以以下列任一方式定义:

  • 硬编码:

  •   File uploads = new File("/path/to/uploads");
    
  • 通过SET UPLOAD_LOCATION=/path/to/uploads设置环境变量:

      File uploads = new File(System.getenv("UPLOAD_LOCATION"));
    
  • 通过-Dupload.location="/path/to/uploads"在服务器启动期间设置VM参数:

  •   File uploads = new File(System.getProperty("upload.location"));
    
  • *.properties 文件中的条目为 upload.location=/path/to/uploads:

  •   File uploads = new File(properties.getProperty("upload.location"));
    
  • 使用名称为upload.location,值为/path/to/uploads<context-param>web.xml中:

  •   File uploads = new File(getServletContext().getInitParameter("upload.location"));
    
  • 如果有提供的话,请使用服务器提供的位置,例如在JBoss AS/WildFly中:

  •   File uploads = new File(System.getProperty("jboss.server.data.dir"), "uploads");
    

无论哪种方式,您都可以轻松地参考并保存文件,具体操作如下:

File file = new File(uploads, "somefilename.ext");

try (InputStream input = part.getInputStream()) {
    Files.copy(input, file.toPath());
}
或者,当您想要自动生成一个唯一的文件名以防止用户意外地覆盖现有文件时:
File file = File.createTempFile("somefilename-", ".ext", uploads);

try (InputStream input = part.getInputStream()) {
    Files.copy(input, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
在JSP/Servlet中如何获取part的方法可以在How to upload files to server using JSP/Servlet?找到; 在JSF中如何获取part的方法可以在How to upload file using JSF 2.2 <h:inputFile>? Where is the saved File?找到。请注意:不要使用Part#write(),因为它会将路径解释为相对于 @MultipartConfig(location) 中定义的临时存储位置。同时,一定要确保在读写过程中不会通过错误使用Reader/Writer而将字节转换为字符来损坏二进制文件(如PDF文件或图像文件),而应该正确使用InputStream/OutputStream。另请参见:How to save uploaded file in JSFSimplest way to serve static data from outside the application server in a Java web applicationHow to save generated file temporarily in servlet based web application

@MultipartConfig(location) 指定了服务器在文件大小超过内存存储阈值时应使用的临时存储位置,而不是最终希望将其存储的永久存储位置。此值默认为由 java.io.tmpdir 系统属性标识的路径。还请参见与 JSF 尝试失败相关的答案:http://stackoverflow.com/questions/18478154/write-file-into-disk-using-jsf-2-2-inputfile/18533832#18533832 - BalusC
1
谢谢 - 希望我听起来不像个白痴,但是这段引用来自 Part.write >> This allows a particular implementation to use, for example, file renaming, where possible, rather than copying all of the underlying data, thus gaining a significant performance benefit 与某些未知的“剪切”(而非复制)方法结合使用,例如来自某些Apache库,可以节省我手动编写字节并重新创建已存在文件的麻烦(请参见此处)。 - Mr_and_Mrs_D
非常感谢您保持帖子的更新 - Tomcat 是否有类似于 "jboss.server.data.dir" 的属性? - Mr_and_Mrs_D
1
没有,它没有。 - BalusC
GlassFish 显然不支持在 Part.write() 中使用绝对文件路径:请参见此处 - Mr_and_Mrs_D
显示剩余4条评论

7

我基于已接受的答案,发布了最终的解决方案:

@SuppressWarnings("serial")
@WebServlet("/")
@MultipartConfig
public final class DataCollectionServlet extends Controller {

    private static final String UPLOAD_LOCATION_PROPERTY_KEY="upload.location";
    private String uploadsDirName;

    @Override
    public void init() throws ServletException {
        super.init();
        uploadsDirName = property(UPLOAD_LOCATION_PROPERTY_KEY);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        // ...
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        Collection<Part> parts = req.getParts();
        for (Part part : parts) {
            File save = new File(uploadsDirName, getFilename(part) + "_"
                + System.currentTimeMillis());
            final String absolutePath = save.getAbsolutePath();
            log.debug(absolutePath);
            part.write(absolutePath);
            sc.getRequestDispatcher(DATA_COLLECTION_JSP).forward(req, resp);
        }
    }

    // helpers
    private static String getFilename(Part part) {
        // courtesy of BalusC : https://dev59.com/9HE95IYBdhLWcg3wKq2q#2424824
        for (String cd : part.getHeader("content-disposition").split(";")) {
            if (cd.trim().startsWith("filename")) {
                String filename = cd.substring(cd.indexOf('=') + 1).trim()
                        .replace("\"", "");
                return filename.substring(filename.lastIndexOf('/') + 1)
                        .substring(filename.lastIndexOf('\\') + 1); // MSIE fix.
            }
        }
        return null;
    }
}

地点:

@SuppressWarnings("serial")
class Controller extends HttpServlet {

    static final String DATA_COLLECTION_JSP="/WEB-INF/jsp/data_collection.jsp";
    static ServletContext sc;
    Logger log;
    // private
    // "/WEB-INF/app.properties" also works...
    private static final String PROPERTIES_PATH = "WEB-INF/app.properties";
    private Properties properties;

    @Override
    public void init() throws ServletException {
        super.init();
        // synchronize !
        if (sc == null) sc = getServletContext();
        log = LoggerFactory.getLogger(this.getClass());
        try {
            loadProperties();
        } catch (IOException e) {
            throw new RuntimeException("Can't load properties file", e);
        }
    }

    private void loadProperties() throws IOException {
        try(InputStream is= sc.getResourceAsStream(PROPERTIES_PATH)) {
                if (is == null)
                    throw new RuntimeException("Can't locate properties file");
                properties = new Properties();
                properties.load(is);
        }
    }

    String property(final String key) {
        return properties.getProperty(key);
    }
}

以及 /WEB-INF/app.properties 文件:

upload.location=C:/_/

如果您发现了漏洞,请告诉我,祝你好运。


1
如果我想要一种不依赖于操作系统的解决方案,在win和ux情况下都能正常工作,该怎么办?我需要设置不同的upload.location路径还是有其他提示? - pikimota

0
Servlet规范为每个部署的Web应用程序提供了一个临时的物理文件存储位置。您应该将其用于此目的。例如,在使用multipart-form-upload时:
Part part = request.getPart("param-name");
File tmpDir = (File)getServletContext().getAttribute("javax.servlet.context.tempdir");
File targetFile = new File(tmpDir, "tmp-file-name");
part.write(targetFile.getAbsolutePath());

现在,你可以随心所欲地处理 targetFile

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接