使用p:graphicImage和StreamedContent从数据库或远程源显示动态图像

57

我正在尝试在<p:graphicImage>中显示保存在数据库中的图像字节,其以StreamedContent形式保存:

<p:graphicImage  value="#{item.imageF}" width="50"  id="grpImage" height="80"/>
private StreamedContent content; // getter and setter

public StreamedContent getImageF() {

    if (student.getImage() != null) {
        InputStream is = new ByteArrayInputStream(student.getImage());
        System.out.println("Byte :"+student.getImage());
        content = new DefaultStreamedContent(is, "", student.getStuID());
        System.out.println("ddd ------------------------------- " + content);
        return content;
    }

    return content;
}

这会返回一张空白图片。这是由什么原因引起的,我该如何解决?

标准输出打印如下:

INFO: Byte :[B@a2fb48
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@b0887b
INFO: Byte :[B@a2fb48
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@1d06a92
INFO: Byte :[B@d52f0b
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@39a60
INFO: Byte :[B@d52f0b
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@8c3daa
INFO: Byte :[B@124728a
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@1dbe05b
INFO: Byte :[B@124728a
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@66a266
INFO: Byte :[B@a2fb48
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@1293976
INFO: Byte :[B@a2fb48
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@17b7399
INFO: Byte :[B@d52f0b
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@1e245a5
INFO: Byte :[B@d52f0b
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@4a7153
INFO: Byte :[B@124728a
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@1561bfd
INFO: Byte :[B@124728a
INFO: ddd ------------------------------- org.primefaces.model.DefaultStreamedContent@47a8c2
4个回答

112

<p:graphicImage>需要一个特殊的getter方法。它会在生成图像时被调用两次,每次都在完全不同的HTTP请求中。

第一次HTTP请求是请求JSF页面的HTML结果,它将首次调用getter以生成包含正确唯一和自动生成URL的HTML <img>元素,该URL包含哪个bean和getter应该在Web浏览器请求图像时被调用的信息。请注意,此时getter不需要返回图像内容。因为这不是HTML的工作方式(图像不会“内联”在HTML输出中,而是单独请求)。

一旦Web浏览器作为HTTP响应检索到HTML结果,它将解析HTML源代码以向最终用户呈现结果。一旦Web浏览器在解析HTML源代码时遇到<img>元素,则会根据其src属性中指定的URL发送全新的HTTP请求,以下载该图像的内容并将其嵌入视觉呈现中。这将调用getter方法第二次,进而返回实际的图像内容。
在您的特定情况下,PrimeFaces显然无法识别和调用getter以检索实际的图像内容,或者getter未返回预期的图像内容。使用#{item}变量名称和日志中的大量调用表明您正在使用<ui:repeat><h:dataTable>。最可能的是支持bean是请求范围的,并且数据模型在请求图像期间未正确保存,因此JSF无法在正确的迭代轮回中调用getter。视图作用域bean也不起作用,因为当浏览器实际请求图像时,JSF视图状态无处可用。
为了解决这个问题,最好的方法是将getter方法重写,以便可以按请求调用它,其中您通过<f:param>传递唯一的图像标识符,而不是依赖于某些后端bean属性,这些属性可能在随后的HTTP请求中失去同步。最好使用一个没有任何状态的单独应用程序范围托管的bean来完成此操作。此外,InputStream只能读取一次,而不能多次读取。
换句话说:永远不要将StreamedContent或任何InputStream甚至UploadedFile声明为bean属性;仅在无状态@ApplicationScoped bean的getter中创建全新的内容,当Web浏览器实际请求图像内容时。 例如:
<p:dataTable value="#{bean.students}" var="student">
    <p:column>
        <p:graphicImage value="#{studentImages.image}">
            <f:param name="studentId" value="#{student.id}" />
        </p:graphicImage>
    </p:column>
</p:dataTable>

学生图像的后备bean可以如下所示:

@Named // Or @ManagedBean
@ApplicationScoped
public class StudentImages {

    @EJB
    private StudentService service;

    public StreamedContent getImage() throws IOException {
        FacesContext context = FacesContext.getCurrentInstance();

        if (context.getCurrentPhaseId() == PhaseId.RENDER_RESPONSE) {
            // So, we're rendering the HTML. Return a stub StreamedContent so that it will generate right URL.
            return new DefaultStreamedContent();
        }
        else {
            // So, browser is requesting the image. Return a real StreamedContent with the image bytes.
            String studentId = context.getExternalContext().getRequestParameterMap().get("studentId");
            Student student = studentService.find(Long.valueOf(studentId));
            return new DefaultStreamedContent(new ByteArrayInputStream(student.getImage()));
        }
    }

}

请注意,这是一种非常特殊的情况,在其中在getter方法中执行业务逻辑是完全合法的,考虑到<p:graphicImage>在底层的工作方式。通常不建议在getter方法中调用业务逻辑,参见Why JSF calls getters multiple times。不要将此特殊情况用作其他标准(非特殊)情况的借口。还请注意,您无法像这样使用EL 2.2功能传递方法参数#{studentImages.image(student.id)},因为该参数不会出现在图像URL中。因此,您确实需要将它们作为<f:param>传递。
如果你使用 OmniFaces 2.0 或更高版本,则可以考虑使用其 <o:graphicImage>,它可以更直观地使用,具有应用程序作用域的getter方法直接委托给服务方法并支持EL 2.2方法参数。因此,如下所示:
<p:dataTable value="#{bean.students}" var="student">
    <p:column>
        <o:graphicImage value="#{studentImages.getImage(student.id)}" />
    </p:column>
</p:dataTable>

使用

@Named // Or @ManagedBean
@ApplicationScoped
public class StudentImages {

    @EJB
    private StudentService service;

    public byte[] getImage(Long studentId) {
        return studentService.find(studentId).getImage();
    }

}

请参阅有关此主题的博客

3
BalusC,这正是我寻找的内容,但我无法弄清楚响应应该如何发送回客户端。你真是一个传奇。非常感谢。 :) - Lyubomyr Shaydariv
1
@Tiny:EJB和CDI托管的bean被注入为代理。代理实例上的方法调用基本上定位其范围中的当前实例,然后在其上调用所需的方法。您可以在来自这些方法的异常的堆栈跟踪中轻松看到这一点。它们的范围因此不与客户端的范围绑定,并且可以轻松地更小。 - BalusC
1
@BalusC,你好,ApplicationScoped不是在整个应用程序中共享吗?这难道不会使所有请求的用户共享所有数据吗?例如,用户A将看到与用户B相同的文档吗?我在JSF方面有点新手,如果答案很明显,请原谅。 - Julios_Rodmax
1
@Julios_Rodmax:数据不会保存为bean的实例变量以供将来重用。它在每次调用getter方法时都会全新创建。事实上,您永远不应该将任何InputStream保存为实例变量以供将来重用。它只能读取一次!这不是JSF特定的,这只是基本的Java知识。 - BalusC
1
@Julios_Rodmax:如果该方法没有同步,那么绝对不会有延迟。你知道Java支持多线程的。 - BalusC
显示剩余12条评论

6

尝试包含一个MIME类型。在您发布的示例中,它是""。空白图像可能是因为它无法将流识别为图像文件,因为您将该字段设置为空字符串。因此,请添加image/png或image/jpg的MIME类型,看看是否有效:

String mimeType = "image/jpg";
StreamedContent file = new DefaultStreamedContent(bytes, mimeType, filename);  

5

这里有几种可能性(如果不是,请发布整个类):

1)您没有正确初始化图像 2)您的流为空,因此您什么也没有得到

我假设student.getImage()的签名为byte [],所以首先要确保该数据实际上是完好且表示图像。其次,您没有指定内容类型,应为“image/jpg”或您正在使用的任何其他类型。

以下是一些用于检查的样板代码,我在此使用Primefaces 2。

/** 'test' package with 'test/test.png' on the path */
@RequestScoped
@ManagedBean(name="imageBean")
public class ImageBean
{
    private DefaultStreamedContent content;

    public StreamedContent getContent()
    {
        if(content == null)
        {
            /* use your database call here */
            BufferedInputStream in = new BufferedInputStream(ImageBean.class.getClassLoader().getResourceAsStream("test/test.png"));
            ByteArrayOutputStream out = new ByteArrayOutputStream();

            int val = -1;
            /* this is a simple test method to double check values from the stream */
            try
            {
                while((val = in.read()) != -1)
                    out.write(val);
            }
            catch(IOException e)
            {
                e.printStackTrace();
            }

            byte[] bytes = out.toByteArray();
            System.out.println("Bytes -> " + bytes.length);
            content = new DefaultStreamedContent(new ByteArrayInputStream(bytes), "image/png", "test.png");
        }

        return content;
    }
}

and some markup...

<html 
    xmlns="http://www.w3.org/1999/xhtml" 
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:p="http://primefaces.prime.com.tr/ui"
>

    <h:head>

    </h:head>

    <h:body>
        <p:graphicImage value="#{imageBean.content}" />
    </h:body>
</html>

如果这个代码能够工作,那么你就已经正确设置了。尽管它是针对流的垃圾代码(不要在生产中使用),但它应该给你一个排除问题的起点。我猜测你可能在JPA或其他数据库框架中遇到了一些问题,其中你的byte[]可能为空或者格式不正确。或者你可能只是有一个内容类型的问题。
最后,我建议从bean中克隆数据,这样student.getImage()就只会被复制到一个新数组中并被使用。这样,如果你遇到了一些未知的问题(其他对象移动或更改了byte[]),你就不会干扰流。
可以像这样做:
byte[] data = new byte[student.getImage().length]
for(int i = 0; i < data.length; i++)
  data[i] = student.getImage()[i];

这样你的bean就有了一份副本(或者Arrays.copy()--随你喜欢)。我无法强调足够简单的内容类型通常是问题所在。祝你好运。


4
BalusC的答案通常是正确的,但要记住一件事情(正如他已经指出的那样)。最终请求是从浏览器发出的,以获取构造的标签的URL。这不是在“jsf上下文”中完成的。因此,如果您尝试访问phaseId(用于记录或其他原因),请注意。
context.getCurrentPhaseId().getName()

这将导致一个NullPointerException,而你会收到一个有些误导性的错误信息:
org.primefaces.application.resource.StreamedContentHandler () - Error in streaming dynamic resource. Error reading 'image' on type a.b.SomeBean

我花了很长时间才弄清楚问题所在。


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