这是我作为 C# 开发在工作中遇到的第一个 JAVA 项目,项目不是从头参与开发的,是开发项目的人员离职了我才接手的(只有我一个后端开发),以下内容均是我接手后独立开发的。
需求很简单,但是开发很复杂,涉及到了 LaTex 公式渲染、 HTML 转 PDF、文件下载、 Docker 和 Nginx 及 Rancher 配置。可以说从开发到 IT 到运维都体验了一下,DevOps 对后端开发来说,真的是一个趋势啊。
一、项目背景#
教育项目,需要将试题集(包含数学公式)生成电子版并打印出来。
大致涉及到两个方面的问题:
- 如何渲染公式
- 电子版文件格式,换句话说:Word or PDF?
经过技术调研、实现难易度以及最终效果,最终结果如下:
- 采用 Selenium 实现公式渲染
- 采用 itextpdf.html2pdf 实现 PDF 生成
二、环境搭建#
1. 搭建 Selenium 环境#
1.1 通过 Docker 安装#
执行以下命令即可快速搭建出 Selenium + Chrome 的环境
$ docker run -d -p 4444:4444 -v /dev/shm:/dev/shm selenium/standalone-chrome该命令会获取最新的 Selenium 镜像,目前版本是 3.141.59。
需要注意的是,官方文档上会带有版本号,即:
$ docker run -d -p 4444:4444 -v /dev/shm:/dev/shm selenium/standalone-chrome:3.141.59-20210128如果带版本号,镜像下载的会非常慢,不带版本号很快就下载下来了。
1.2 通过 Rancher 安装#
在 Rancher 上添加服务也很简单,以下是精简的 YAML:
apiVersion: apps/v1
kind: Deployment
metadata:
name: selenium-chrome
namespace: edu-selenium
spec:
template:
spec:
containers:
- env:
- name: LANG
value: C.UTF-8
- name: TZ
value: Asia/Shanghai
image: selenium/standalone-chrome
imagePullPolicy: Always
name: selenium-chrome
ports:
- containerPort: 4444
name: http
protocol: TCP
readinessProbe:
failureThreshold: 3
initialDelaySeconds: 10
periodSeconds: 20
successThreshold: 2
tcpSocket:
port: 4444
timeoutSeconds: 2
volumeMounts:
- mountPath: /dev/shm
name: shm-volume
workingDir: /home/seleuser
volumes:
- hostPath:
path: /dev/shm
type: ""
name: shm-volume1.3 检测服务是否可用#
访问以下地址,即可测试服务是否可用:
[服务器IP]:4444/status1.4 查看 Chrome 版本#
访问以下地址,并创建一个 chrome session ,鼠标悬停在 Capabilities 上,即可在弹出的窗口看到 browserVersion
[服务器IP]:4444/wd/hub/static/resource/hub.html2. 添加包依赖#
在 build.gradle 或 pom.xml 添加包依赖,这里以 Gradle 配置为例:
// import WebDriverWait
implementation 'org.seleniumhq.selenium:selenium-support:3.141.59'
implementation 'org.seleniumhq.selenium:selenium-chrome-driver:3.141.59'
implementation 'com.itextpdf:html2pdf:3.0.3'注意 selenium-chrome-driver 版本与安装的 Selenium 版本一致。
3. 下载 chrome-driver 二进制文件#
下载对应浏览器版本的驱动,并放到 resources/webdriver 目录下(可放在任意目录)
三、使用示例#
1. 配置 webdriver 路径#
// 设置驱动路径,调用一次即可
static final String webDriverKey = "webdriver.chrome.driver";
static {
try {
ClassPathResource webdriver = new ClassPathResource("webdriver");
if (OS.isFamilyWindows()) {
System.setProperty(webDriverKey, webdriver.getFile().getAbsolutePath() + "/chromedriver.exe");
} else {
System.setProperty(webDriverKey, webdriver.getFile().getAbsolutePath() + "/chromedriver");
}
} catch (IOException e) {
log.error("初始化chromedriver失败", e);
}
}2. 渲染 HTML#
// 渲染指定url的html文件,并返回选然后的html
public static String render(String url) throws MalformedURLException {
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless");
options.addArguments("--lang=zh-CN");
RemoteWebDriver chromeDriver;
// seleniumUrl为配置值,指定Selenium服务的地址
// 示例:http://ip:4444/wd/hub
// 此判断用于本地调试和真实环境的切换
if (StringUtils.isBlank(seleniumUrl) || "localhost".equals(seleniumUrl)) {
chromeDriver = new ChromeDriver(options);
} else {
chromeDriver = new RemoteWebDriver(new URL(seleniumUrl), options);
}
String html = "";
try {
chromeDriver.navigate().to(url);
// 等待echart渲染完成
synchronized (chromeDriver) {
chromeDriver.wait(1000);
}
// 得到渲染后的html
html = (String) chromeDriver.executeScript("return document.documentElement.innerHTML;");
} finally {
// 退出chromeDriver进程
// chromeDriver.close()用于关闭当前窗口,不会退出chromeDriver进程,多次调用会导致chrome进程增多
chromeDriver.quit();
}
return html;
}3. itextpdf.html2pdf 中文字体支持#
3.1 添加中文字体#
注意:如果服务器支持中文字体,则此步骤可以跳过
推荐使用开源的中文字体,比如:思源黑体
根据需要,将字体复制到 resources/fonts 目录下(可以是任意目录)
3.2 配置 ConverterProperties#
static final ConverterProperties converterProperties = new ConverterProperties();
static {
// 添加字体支持
FontProvider fontProvider = new FontProvider();
fontProvider.addStandardPdfFonts();
fontProvider.addSystemFonts();
// 添加自定义字体
// 对于ttc格式字体,需要在路径后加',1',所以无法调用fontProvider.addDirectory方法
// fontProvider.addDirectory方法如果添加字体失败,会忽略异常,不会抛出异常
try {
ClassPathResource msyhFont = new ClassPathResource("fonts/msyh.ttc");
FontProgram font = FontProgramFactory.createFont(msyhFont.getFile().getAbsolutePath()+",1");
fontProvider.addFont(font);
} catch (IOException e) {
log.error("初始化字体失败", e);
}
converterProperties.setFontProvider(fontProvider);
converterProperties.setCharset(StandardCharsets.UTF_8.name());
}4. 生成 PDF#
// 传入渲染后的html和生成pdf保存的路径
public static void convert(String html, String pdfFileName) throws FileNotFoundException {
PdfWriter pdfWriter = new PdfWriter(pdfFileName);
HTMLConverter.convertToPdf(html, pdfWriter, converterProperties);
}四、总结#
以上主要技术问题就介绍完了,剩余的问题是:
- HTML 模板
- PDF 文件下载
模板是需要不断优化不断改进的,是日后主要的修改点。
文件下载服务就简单了,使用 Nginx 服务器就可以了。
实际项目是容器化部署,涉及到主机目录到容器目录的映射。docker -v 命令就可以解决。
docker -v /data/template:/data/template -v /download:/data/download ... your app
# 用于渲染,应用生成的html通过nginx暴露出url用于渲染
docker -v /data/template:/nginx/root ... nginx
# 用于pdf下载
docker -v /download:/nginx/root ... nginx