Graphics2D.drawImage为何抛出NullPointerException?
解决Printable接口drawImage异步抛出NullPointerException的问题
问题场景
- 基于Java/Kotlin开发打印应用,通过
Printable接口实现页面打印 - 调用
drawImage时异步抛出NullPointerException,但图像可正常显示 - 已在Temurin 17.0.5、Oracle OpenJDK 19.0.1版本验证,仅出现在Ubuntu 22.04系统
- 异常在
drawImage返回后触发,即使等待ImageObserver通知仍无法避免 - 两条逻辑一致的
drawImage调用中,仅第一条触发异常
异常栈信息
java.lang.NullPointerException: Cannot invoke "java.awt.image.ImageConsumer.setColorModel(java.awt.image.ColorModel)" because "this.theConsumer" is null at java.desktop/sun.awt.image.OffScreenImageSource.sendPixels(OffScreenImageSource.java:171) at java.desktop/sun.awt.image.OffScreenImageSource.produce(OffScreenImageSource.java:187) at java.desktop/sun.awt.image.OffScreenImageSource.addConsumer(OffScreenImageSource.java:66) at java.desktop/sun.awt.image.OffScreenImageSource.startProduction(OffScreenImageSource.java:80) at java.desktop/java.awt.image.FilteredImageSource.startProduction(FilteredImageSource.java:184) at java.desktop/sun.awt.image.ImageRepresentation.startProduction(ImageRepresentation.java:732) at java.desktop/sun.awt.image.ToolkitImage.addWatcher(ToolkitImage.java:221) at java.desktop/sun.awt.image.ToolkitImage.getHeight(ToolkitImage.java:142) at java.desktop/sun.print.PeekGraphics$ImageWaiter.waitForDimensions(PeekGraphics.java:1888) at java.desktop/sun.print.PeekGraphics$ImageWaiter.<init>(PeekGraphics.java:1876) at java.desktop/sun.print.PeekGraphics.drawImage(PeekGraphics.java:955) ... 后续为业务代码栈
业务代码片段
class MyPrintable : Printable { override fun print(graphics: Graphics, pageFormat: PageFormat, pageIndex: Int): Int { if (pageIndex > 0) { return NO_SUCH_PAGE } val g2d = graphics as Graphics2D drawString(...) drawString(...) val image1: BufferedImage = loadImage1() // 该行异步抛出NPE drawImage(image1.getScaledInstance(width, height, Image.SCALE_SMOOTH), x, y, null) val image2: BufferedImage = loadImage2() drawImage(image1.getScaledInstance(width, height, Image.SCALE_SMOOTH), x2, y2, null) return PAGE_EXISTS } }
问题原因
从异常栈可定位到:OffScreenImageSource中的theConsumer引用为null,根源是getScaledInstance返回的是延迟加载的ToolkitImage,打印系统的PeekGraphics在处理该图像时,内部ImageWaiter等待图像尺寸的流程中,出现了图像消费者引用丢失的竞态条件。Ubuntu 22.04的AWT实现与打印上下文的异步图像加载逻辑存在兼容性问题,第一条图像的加载流程刚好触发了该竞态。
解决方案
方案1:提前生成BufferedImage(推荐)
直接手动完成图像缩放,生成已完全加载的BufferedImage,彻底规避延迟加载的异步问题:
private fun scaleImage(source: BufferedImage, targetWidth: Int, targetHeight: Int): BufferedImage { val scaledImage = BufferedImage(targetWidth, targetHeight, source.type) val g2d = scaledImage.createGraphics() try { // 设置平滑插值,替代SCALE_SMOOTH效果 g2d.setRenderingHint( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR ) g2d.drawImage(source, 0, 0, targetWidth, targetHeight, null) } finally { g2d.dispose() } return scaledImage }
修改打印方法中的调用:
val image1: BufferedImage = loadImage1() val scaledImage1 = scaleImage(image1, width, height) g2d.drawImage(scaledImage1, x, y, null) val image2: BufferedImage = loadImage2() val scaledImage2 = scaleImage(image2, width, height) g2d.drawImage(scaledImage2, x2, y2, null)
方案2:等待图像完全加载后再使用
若必须使用getScaledInstance,可通过MediaTracker强制等待图像加载完成:
private fun waitForImage(image: Image) { val tracker = MediaTracker(java.awt.Component()) tracker.addImage(image, 0) try { tracker.waitForAll() } catch (e: InterruptedException) { Thread.currentThread().interrupt() } }
调用方式:
val scaledImage1 = image1.getScaledInstance(width, height, Image.SCALE_SMOOTH) waitForImage(scaledImage1) g2d.drawImage(scaledImage1, x, y, null)
方案3:传入有效的ImageObserver
避免传入null作为ImageObserver,直接传递打印上下文的Graphics2D(它实现了ImageObserver接口),帮助打印系统正确跟踪图像加载状态:
g2d.drawImage(scaledImage1, x, y, g2d)
总结
优先选择方案1,直接生成BufferedImage是Java打印场景中处理图像的最佳实践,能彻底规避不同平台AWT实现的差异问题,消除异步加载导致的竞态条件。
内容的提问来源于stack exchange,提问作者Sean
相关产品推荐
相关产品推荐

