项目地址:https://github.com/w414034207/print-netty

业务场景

给客户开发一个web管理系统时,客户要求能够在浏览器点击打印,直接使用客户端的本地打印机打印服务端的文件,打印配置可以在网页进行设置。
客户要求静默打印,用户不需要看打印预览页面,可以打印服务端动态生成的PDF文件,可以在网页上选择打印份数,默认双面打印。

思路

网上看了各种提供打印的方式,使用js的一些打印控件都不符合要求,PDF文件中的电子签名解析不出来
还试用了两种收费的打印控件,都是需要在客户端安装启动一个可执行文件。
下图为当时试过的打印方式

其中一个收费的控件能一定程度满足我们的需求,直接传URL就能打印对应的PDF文件。且不会丢失PDF文件中的电子签名。(即上图中的第一个button对应的,简写是jcp,可以静默打印,也可以传各种打印参数,缺点是:1. 打印方法的回调不能确定是否打印成功;2. 如果url没有返回pdf,回调不会被调用;3. 收费)

这时我就想,如果考虑客户端安装控件的方式,好像自己写一个控件问题也不大:

  1. 实现一个http服务,可接收http请求,被请求时可用java调用打印机进行打印;
  2. 浏览器客户端请求本地一个固定端口即可访问到该服务;

技术选择

Spring Boot

现成的http服务,我直接就写了一个,验证上面思路的可行性,实验之后确实可以在浏览器端打印PDF文件;但是问题就是太大了,由于我的功能很简单,而使用了Spring Boot之后,运行时内存都200M以上,这对用户的机器性能就有可能造成较大的影响;

Java NIO

由于当时刚好在学习Java NIO的相关知识,既然Spring Boot现成的服务太大了,我或许可以自己实现http协议的编解码,使用Java NIO实现http服务,使服务更轻量,然后我就舍弃了Spring Boot,改用Java NIO实现(这个代码,在github上是Private的,如果有感兴趣的盆友可以发消息找我要);

netty

NIO版本的已经确认可以在FireFox、Chrome浏览器稳定使用,但是在使用IE进行测试时,经常会出现请求消息读取不完整的情况,我自己实现的http协议看来还需要优化。考虑到自己写的http协议还是太简陋,优化的空间很大,成本不好说。所以我决定还是使用netty提供的现成的http支持,用netty实现http服务,以兼顾轻量和稳定。

实现

netty实现http服务端
  1. 创建处理http请求的Handler(这里只贴出主要的方法,还有两个必须实现的接口方法没有贴)
@ChannelHandler.Sharable
public class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {DefaultFullHttpResponse response = executeRequest(msg);HttpHeaders heads = response.headers();// 返回内容的MIME类型heads.add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN + "; charset=UTF-8");// 响应体的长度heads.add(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());// 允许跨域访问heads.add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*");// 响应给客户端ctx.write(response);}private DefaultFullHttpResponse executeRequest(FullHttpRequest msg) throws UnsupportedEncodingException {// 200HttpResponseStatus responseStatus = HttpResponseStatus.OK;String returnMsg = "";try {// 自己实现的保存http请求信息的类HttpRequest httpRequest = new HttpRequest(msg);Class<PrintController> printControllerClass = PrintController.class;Method invokeMethod = printControllerClass.getMethod(httpRequest.getMethod(), HttpRequest.class);PrintController printController = PrintController.getInstance();// 使用反射,调用对应的方法,获取返回值returnMsg = (String) invokeMethod.invoke(printController, httpRequest);} catch (NoSuchMethodException e) {// 404responseStatus = HttpResponseStatus.NOT_FOUND;} catch (IllegalAccessException | InvocationTargetException e) {e.printStackTrace();// 500responseStatus = HttpResponseStatus.INTERNAL_SERVER_ERROR;}return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, responseStatus,Unpooled.wrappedBuffer(returnMsg.getBytes()));}
}
  1. 启动一个netty服务
public class HttpServer {private final static int PORT = PrintConstants.SERVER_PORT;public static void start() {final HttpHandler httpHandler = new HttpHandler();// 创建EventLoopGroupEventLoopGroup bossGroup = new NioEventLoopGroup();EventLoopGroup workerGroup = new NioEventLoopGroup();ServerBootstrap b = new ServerBootstrap();b.group(bossGroup, workerGroup)//指定所使用的NIO传输Channel.channel(NioServerSocketChannel.class)//使用指定的端口设置套接字地址.localAddress(new InetSocketAddress(PORT))// 添加Handler到Channle的ChannelPipeline.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) {// 获取管道socketChannel.pipeline()// 解码+编码.addLast(new HttpServerCodec())/* aggregator,消息聚合器,处理POST请求的消息 */.addLast(new HttpObjectAggregator(1024 * 1024))// HttpHandler被标注为@shareable,所以我们可以总是使用同样的案例.addLast(httpHandler);}});try {// 异步地绑定服务器;调用sync方法阻塞等待直到绑定完成ChannelFuture f = b.bind().sync();// 获取Channel的CloseFuture,并且阻塞当前线程直到它完成f.channel().closeFuture().sync();} catch (InterruptedException e) {e.printStackTrace();} finally {// 关闭EventLoopGroup,释放所有的资源bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}
打印功能
  1. 获取所有打印机
PrintService[] printServices = PrinterJob.lookupPrintServices();
  1. 获取默认打印机
PrintService defaultService = PrintServiceLookup.lookupDefaultPrintService();
  1. 打印功能实现,由于业务需要,可能会一次打印多个PDF文件。所以我使用多线程下载文件的方式,先把所有文件都下载到内存。都下载成功才启动打印任务。
    启动多线程下载的代码如下:
    private LinkedBlockingQueue<byte[]> downloadAllDocument(List<String> urlList, LinkedBlockingQueue<byte[]> queue) throws InterruptedException, IOException {ExecutorService executorService = Executors.newCachedThreadPool(r -> new Thread(r, "t_iop_download_" + r.hashCode()));CountDownLatch count = new CountDownLatch(urlList.size());AtomicBoolean downFlag = new AtomicBoolean(true);// 多线程下载文件,都下载成功之后再统一打印。for (String url : urlList) {if (StringUtils.isEmpty(url)) {count.countDown();continue;}executorService.submit(() -> {try {queue.add(download(url));} catch (Exception e) {// 下载失败downFlag.set(false);} finally {count.countDown();}});}//等待计数器计数count.await();// 如果下载失败,则清空队列if (!downFlag.get()) {throw new IOException("获取文件失败");}return queue;}

然后通过下面的代码进行打印

        LinkedBlockingQueue<byte[]> documentsByte = new LinkedBlockingQueue<>();documentsByte = downloadAllDocument(urlList, documentsByte);byte[] fileByte;// 都下载结束,没问题,开始打印PrinterJob printerJob = PrinterJob.getPrinterJob();printerJob.setPrintService(printer);while ((fileByte = documentsByte.poll()) != null) {try (PDDocument document = PDDocument.load(fileByte)) {printerJob.setPageable(new PDFPageable(document));printerJob.print(attr);}}

同时通过查看javax.print的源码,增加了打印机不可用的判断,测试过打印机脱机和打印队列有未处理的失败的打印任务都不可用。(该判断在jre1.8.0_25下生效,在8u201、8u212下都没有生效)

        // javax.print.PrintService printer if (printer.getAttribute(PrinterIsAcceptingJobs.class) == PrinterIsAcceptingJobs.NOT_ACCEPTING_JOBS) {throw new PrinterException("打印机不可用");}
js提供客户端API

api代码如下

const printServerAddress = "http://127.0.0.1:31777/";
const downloadUrl = "print.zip";function getPrinters(callbackFunc) {$.ajax({type: "GET",url: printServerAddress + "getPrinters?_m=" + Math.random(),success: function (msg) {let dataObj = eval("(" + msg + ")");callbackFunc(dataObj);},error: downloadIoPrint});
}function getDefaultPrinter(callbackFunc) {$.ajax({type: "GET",url: printServerAddress + "getDefaultPrinter?_m=" + Math.random(),success: callbackFunc,error: downloadIoPrint});
}function printPdf(url, config) {$.ajax({type: "POST",data: {'printer': config.printer, 'copies': config.copies, 'duplex': config.duplex, 'url': url},url: printServerAddress + "printPdf?_m=" + Math.random(),success: config.done});
}function downloadIoPrint() {if (confirm("请下载解压并运行打印服务后重试!点击【确定】下载。")) {location.href = downloadUrl;}
}

到这里,已经满足我们的需求。
(这里贴出来的只是主要代码,具体的项目代码可以查看github,github上面也有完整的调用demo)

基于netty的浏览器客户端打印控件实现相关推荐

  1. 兼容所有浏览器的Web打印控件的设计方案

    兼容所有浏览器的Web打印控件的设计方案 设计方案的简单实现网址:http://www.lc-simple.com/PrintTest/ 第一章:Web打印控件的原理 Web打印控件的工作的原理如下: ...

  2. 自己开发基于Web的打印控件,真正免费不是共享

    1.1.0.121 版的控件,更新如下 SetPrintBackground 已被取消          SetMediaHeader        去掉了两个参数,见被划掉的文字部分 在做项目中发现 ...

  3. RAD Studio 10.4.1新的基于Chromium的Microsoft Edge浏览器的TEdgeBrowser控件用法

    目录 RAD Studio 10.4.1新的基于Chromium的Microsoft Edge浏览器的TEdgeBrowser控件用法 一.TEdgeBrowser安装部署说明 1.1.TEdgeBr ...

  4. 推荐打印打印控件Lodop,支持IE,firefox,chrome等位内核的浏览器

    WEB打印控件Lodop(V6.x)使用说明及样例   Lodop(标音:劳道谱,俗称:露肚皮)是专业WEB控件,用它既可裁剪输出页面内容,又可用程序代码直接实现 复杂打印.控件功能强大,却简单易用, ...

  5. PAZU WEB打印控件

    PAZU WEB打印控件 PAZU 组件在国内我们提供授权给包括中国电信.移动和银行在内的超过300家大中型企业和IT企业应用于基于WEB的开发 PAZU 支持包括Java, Dot Net, JSP ...

  6. 发现了一个好用的WEB项目打印控件--四方打印

    PAZU 与WEB打印  PAZU 组件应用于基于WEB的开发  PAZU 提供以下三大类功能:             1.控制IE窗口的外观和行为 1.1 通过 JavaScript 隐藏IE的地 ...

  7. 锐洋java web打印控件_锐洋Java web打印控

    ReYoPrint产品介绍 ReYoPrint (锐洋.打印控件)是一款实现网页套打的专用工具.作为web应用开发者,我们经常会遇到在浏览器中打印报表.票据的需求,这些需求浏览器本身的打印功能一般不能 ...

  8. lodop打印html内容,Lodop打印控件在页面如何使用

    Lodop打印控件部署到web服务器简单,在页面的使用方法也简单,是非常容易和方便使用的打印控件. 客户端本地打印角色(即用户访问网站后 用自己链接的打印机进行客户端本地打印),步骤很少,部署简单: ...

  9. PAZU 是4Fang 为配合“四方在线”软件于2004年开发的WEB打印控件,适用于各种WEB软件项目的打印。...

    PAZU 是4Fang 为配合"四方在线"软件于2004年开发的WEB打印控件,适用于各种WEB软件项目的打印. PAZU是客户端软件,使用于IE作为客户端的所有应用,与服务器端开 ...

最新文章

  1. MindSpore部署图像分割示例程序
  2. json对象转为url参数_Day48_Ajaxamp;Json
  3. 克服过拟合和提高泛化能力的20条技巧和诀窍
  4. Ubuntu14.04下安装wineqq国际版和卸载QQ
  5. 思科路由器全局、接口、协议调试(下)
  6. 关于/etc/init.d/nfs脚本解读中的疑问解答
  7. Ivy Bridge处理器
  8. 贪心算法哈夫曼java_贪心算法_哈夫曼编码问题(Huffman Coding)
  9. NanoPC T4 移植 HPSocket
  10. 硬件知识:固态硬盘和机械硬盘区别
  11. pointer-event属性详解
  12. struts2从入门到精通
  13. 第四章(第二节)没有人,在年少时想成为一个普通人
  14. Debian11.5和Ubuntu22.04安装fcitx5中文输入法(五笔拼音)
  15. 五, Hive-数据的导入和导出
  16. 香颂花园是申市最有名的别墅区
  17. sql语句查询出重复的数据
  18. 三种保存电脑本地图片方法
  19. Mac - 加密压缩文件(免费)
  20. 阿里云超算:高性能容器方案实战之Singularity

热门文章

  1. foxmail for linux 64,foxmail server 1.2 for linux数据备份手册_邮件服务器
  2. python三大,Python三大器
  3. python获取js变量_Python和读取JavaScript变量valu
  4. 《约会专家》拖车【约会宝典】总结
  5. 初识对象,对象的创建以及方法的定义规则
  6. vector的sort操作
  7. 面对日益激烈的互联网各类电商的竞争,你是如何看待电商之间的价格战的呢?
  8. Teamcity的安装与使用
  9. Excel 2010 VBA 入门 013 导入或导出VBA代码
  10. 计算机测绘专业,测绘类专业