项目地址:https://github.com/w414034207/print-netty
业务场景
给客户开发一个web管理系统时,客户要求能够在浏览器点击打印,直接使用客户端的本地打印机打印服务端的文件,打印配置可以在网页进行设置。
客户要求静默打印,用户不需要看打印预览页面,可以打印服务端动态生成的PDF文件,可以在网页上选择打印份数,默认双面打印。
思路
网上看了各种提供打印的方式,使用js的一些打印控件都不符合要求,PDF文件中的电子签名解析不出来。
还试用了两种收费的打印控件,都是需要在客户端安装启动一个可执行文件。
下图为当时试过的打印方式
其中一个收费的控件能一定程度满足我们的需求,直接传URL就能打印对应的PDF文件。且不会丢失PDF文件中的电子签名。(即上图中的第一个button对应的,简写是jcp,可以静默打印,也可以传各种打印参数,缺点是:1. 打印方法的回调不能确定是否打印成功;2. 如果url没有返回pdf,回调不会被调用;3. 收费)
这时我就想,如果考虑客户端安装控件的方式,好像自己写一个控件问题也不大:
- 实现一个http服务,可接收http请求,被请求时可用java调用打印机进行打印;
- 浏览器客户端请求本地一个固定端口即可访问到该服务;
技术选择
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服务端
- 创建处理http请求的Handler(这里只贴出主要的方法,还有两个必须实现的接口方法没有贴)
@ChannelHandler.Sharable
public class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Override
protected 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 {
// 200
HttpResponseStatus 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)