当前位置: 首页 > 面试题库 >

动态创建的内容可供下载,而无需在Vaadin Flow Web应用程序的服务器端写入文件

东明德
2023-03-14
问题内容

在我的 Vaadin Flow
网络应用程序(版本14或更高版本)中,我想向我的用户显示下载数据文件的链接。

此下载内容可能很大。因此,我不想一次全部实现内存中的全部内容。我想批量生产大量内容,一次提供一个下载块,以最大程度地减少内存使用量。例如,想象一下,数据库中有很多行,我们一次将一行送入下载。

我知道AnchorVaadin Flow
中的小部件。但是,如何将一些动态创建的内容连接到此类小部件?

另外,鉴于此数据是动态动态生成的,因此我希望用户计算机上已下载文件的名称默认为某个前缀,后跟YYYYMMDDTHHMMSS格式的当前日期时间。


问题答案:

警告:
我在这件事上不是专家。我在这里提供的示例代码似乎运行正常。通过研究有限的文档并阅读了网络上的许多其他文章,我将这种解决方案拼凑在一起。矿山可能不是最好的解决方案。

有关更多信息,请参见Vaadin手册的“
动态内容” 页面。

我们在您的问题中包含三个主要部分:

  • Vaadin Web应用程序页面上的小部件,可为用户提供下载。
  • 动态内容创建者
  • 在用户计算机上创建的文件的默认名称

我对前两个有解决方案,但对第三个没有解决方案。

如课题中所述,我们确实使用了Anchor小部件(请参阅Javadoc)。

我们在布局上定义一个成员变量。

private Anchor anchor;

我们通过传递一个StreamResource对象来实例化。此类在Vaadin中定义。它的工作是包装我们制作的类,该类将产生扩展Java类的实现InputStream

输入流通过从其read方法返回int其值是预期的八位位组的数字,即0-255,一次提供一个八位位组的数据。到达数据末尾时,传回负数read

在我们的代码中,我们实现了makeStreamOfContent一种充当InputStream工厂的方法。

private InputStream makeInputStreamOfContent ( )
{
    return GenerativeInputStream.make( 4 );
}

在实例化我们的时StreamResource,我们传递了一个引用该方法的方法引用makeInputStreamOfContent。由于没有输入流或任何数据尚未生成,因此我们在这里变得有点抽象。我们只是在准备舞台;该动作稍后发生。

传递给的第一个参数new StreamResource是要在用户的客户端计算机上创建的文件的默认名称。在此示例中,我们使用的虚构名称report.text

anchor = 
    new Anchor( 
        new StreamResource( "report.text" , this :: makeInputStreamOfContent ) , 
        "Download generated content" 
    )
;

接下来,我们download在HTML5 anchor元素上设置属性。此属性向浏览器指示当用户单击链接时我们打算下载目标。

anchor.getElement().setAttribute( "download" , true );

您可以通过将锚小部件包装在内来显示图标Button

downloadButton = new Button( new Icon( VaadinIcon.DOWNLOAD_ALT ) );
anchor.add( downloadButton );

如果使用这样的图标,则应从小Anchor部件中删除文本标签。而是将任何所需的文本放在中Button。因此,我们将空字符串(""newAnchor传递给,并将标签文本作为第一个参数传递给new Button

anchor = 
    new Anchor( 
        new StreamResource( "report.text" , this :: makeInputStreamOfContent ) , 
        "" 
    )
;
anchor.getElement().setAttribute( "download" , true );
downloadButton = 
    new Button( 
        "Download generated content" , 
        new Icon( VaadinIcon.DOWNLOAD_ALT ) 
    )
;
anchor.add( downloadButton );

动态内容创建者

我们需要实现一个InputStream子类,以提供给我们的下载小部件。

InputStream抽象类提供了所有,但其中的一个方法的实现。我们只需要实现read满足项目需求的方法即可。

这是一种可能的实现方式。实例化GenerativeInputStream对象时,传递要生成的行数。一次生成一行数据,然后逐个八位字节地将数据提供给客户端。完成该行后,将生成另一行。因此,我们一次只处理一行就可以节省内存。

馈送到客户端的八位位组是构成我们行的UTF-8文本的八位位组。预期文本的每个字符都可以包含一个或多个八位字节。如果您不理解这一点,请阅读Joel
Spolsky撰写的有趣且内容丰富的文章,
《绝对最低限度每个软件开发人员绝对,肯定必须了解Unicode和字符集》(无借口!)

package work.basil.example;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.time.Instant;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.function.IntSupplier;

// Generates random data on-the-fly, to simulate generating a report in a business app.
//
// The data is delivered to the calling program as an `InputStream`. Data is generated
// one line (row) at a time. After a line is exhausted (has been delivered octet by octet
// to the client web browser), the next line is generated. This approach conserves memory
// without materializing the entire data set into RAM all at once.
//
// By Basil Bourque. Use at your own risk.
// © 2020 Basil Bourque. This source code may be used by others agreeing to the terms of the ISC License.
// https://en.wikipedia.org/wiki/ISC_license
public class GenerativeInputStream extends InputStream
{
    private int rowsLimit, nthRow;
    InputStream rowInputStream;
    private IntSupplier supplier;
    static private String DELIMITER = "\t";
    static private String END_OF_LINE = "\n";
    static private int END_OF_DATA = - 1;

    // --------|  Constructors  | -------------------
    private GenerativeInputStream ( int countRows )
    {
        this.rowsLimit = countRows;
        this.nthRow = 0;
        supplier = ( ) -> this.provideNextInt();
    }

    // --------|  Static Factory  | -------------------
    static public GenerativeInputStream make ( int countRows )
    {
        var gis = new GenerativeInputStream( countRows );
        gis.rowInputStream = gis.nextRowInputStream().orElseThrow();
        return gis;
    }

    private int provideNextInt ( )
    {
        int result = END_OF_DATA;

        if ( Objects.isNull( this.rowInputStream ) )
        {
            result = END_OF_DATA; // Should not reach this point, as we checked for null in the factory method and would have thrown an exception there.
        } else  // Else the row input stream is *not*  null, so read next octet.
        {
            try
            {
                result = rowInputStream.read();
                // If that row has exhausted all its octets, move on to the next row.
                if ( result == END_OF_DATA )
                {
                    Optional < InputStream > optionalInputStream = this.nextRowInputStream();
                    if ( optionalInputStream.isEmpty() ) // Receiving an empty optional for the input stream of a row means we have exhausted all the rows.
                    {
                        result = END_OF_DATA; // Signal that we are done providing data.
                    } else
                    {
                        rowInputStream = optionalInputStream.get();
                        result = rowInputStream.read();
                    }
                }
            }
            catch ( IOException e )
            {
                e.printStackTrace();
            }
        }

        return result;
    }

    private Optional < InputStream > nextRowInputStream ( )
    {
        Optional < String > row = this.nextRow();
        // If we have no more rows, signal the end of data feed with an empty optional.
        if ( row.isEmpty() )
        {
            return Optional.empty();
        } else
        {
            InputStream inputStream = new ByteArrayInputStream( row.get().getBytes( Charset.forName( "UTF-8" ) ) );
            return Optional.of( inputStream );
        }
    }

    private Optional < String > nextRow ( )
    {
        if ( nthRow <= rowsLimit ) // If we have another row to give, give it.
        {
            nthRow++;
            String rowString = UUID.randomUUID() + DELIMITER + Instant.now().toString() + END_OF_LINE;
            return Optional.of( rowString );
        } else // Else we have exhausted the rows. So return empty Optional as a signal.
        {
            return Optional.empty();
        }
    }

    // --------|  `InputStream`  | -------------------
    @Override
    public int read ( ) throws IOException
    {
        return this.provideNextInt();
    }
}

默认文件名

我找不到完成最后一部分的方法,默认情况下文件的名称包含生成内容的时间。

我什至在这一点上发布了有关堆栈溢出的问题: 在Vaadin
Flow应用程序中使用文件名默认为用户事件的日期时间进行下载

问题在于,在加载页面并Anchor实例化该窗口小部件时,链接窗口小部件背后的URL仅创建一次。之后,在用户阅读页面时,时间流逝。当用户最终单击链接以开始下载时,当前时刻晚于URL中记录的时刻。

似乎没有简单的方法可以将该URL更新到用户的click事件或download事件的当前时刻。

提示

顺便说一下,对于实际工作,我不会使用自己的代码构建导出的行。相反,我将使用 Apache Commons
CSV之

类的库来编写制表符分隔或逗号分隔值(CSV)的内容。

资源资源

  • 论坛: Vaadin 10让用户下载文件
  • 论坛: 字节数组中的图像
  • 手册: 动态内容


 类似资料:
  • 问题内容: 有什么方法可以在客户端上创建文本文件并提示用户下载文本文件,而无需与服务器进行任何交互?我知道我不能直接写给他们的机器(安全性和全部),但是我可以创建并提示他们保存吗? 问题答案: 您可以使用数据URI。浏览器支持各不相同。 例: 八位字节流将强制进行下载提示。否则,它可能会在浏览器中打开。 对于CSV,您可以使用:

  • 问题内容: 我正在Node.js中构建文本编辑器,用户可以在其中在textarea中创建文件。完成文件编辑后,他可以按下“导出”按钮,该按钮触发一个Jquery函数,该函数读取textarea并将文本发布到node.js服务器上。服务器应读取该信息并返回文件。我想避免在服务器上创建文件并提供服务,而是希望使用流动态创建文件。我已经尝试使用以下方法,但是没有用: 有人对如何实现这一目标有任何见识吗?

  • 4.3 创建/使用内容供应器 由于ContentResolver和SQLiteDatabase的接口非常相似,所以常常有个误解,Content Provider与SQLiteDatabase的关系如此密切。 但是,实际上内容供应器只是提供了应用间数据共享的接口,所以需要注意的是它不会影响每种数据保存格式。 为了保存内容供应器中的数据,可以使用SQLiteDatabase,也可以使用其他保存格式,如

  • 4.3.1.4 创建/使用内部内容供应器 内部内容供应器禁止除内部应用以外的应用使用。 下面展示了如何实现内部内容供应器的示例代码。 要点(创建内容供应器): 定义内部签名权限。 需要内部签名权限。 将导出属性显式设置为true。 验证内部签名权限是否由内部应用定义。 验证参数的安全性,即使这是来自内部应用的请求。 由于请求应用是内部的,因此可以返回敏感信息。 导出 APK 时,请使用与请求应用相

  • 我正在考虑跨移动平台(iOS和Android)使用反应原生组件。我见过的将反应原生组件添加到Android应用程序的唯一方法是创建扩展的活动。 是否有任何方法可以在较低的颗粒度级别添加/使用反应原生组件,例如?

  • 我们想推出一个新的开源项目,将作为一个桌面应用程序。我们希望使用Java Spring for business logic(后端)和Flutter构建GUI(就像我们可以创建移动和web应用程序一样)。 其想法是,用户可以选择将其Flutter desktop应用程序作为独立应用程序使用(业务逻辑在本地用java spring处理),或者将其桌面应用程序连接到我们提供的外部服务器java spr