记录一次在线word转PDF的问题处理过程
一、业务场景
一个较老框架的合同系统(Struts2+Spring+jsp+tomcat部署),合同审批流程中会上传各种合同附件,一般是word文档,要求审批过程成加签(加水印)审核,然后进行预览。实现方案是先将word转PDF,然后在PDF文件上加水印。
二、问题描述
一开始选用Aspose.Words15.8 破解版本
--> 后来由于出现含有图片的word转PDF时没有正确的转换含有图片的页,然后改用OpenOffice4.15+jodconverter-2.2.2.jar,转换含图片word没问题
-->过了一段时间有个word转换失败,也没有报错,然后就升级为当前最新的LibreOffice25.2(是openoffice的衍生版),转换这个word成功了;
-->过了一段时间,有几个word里本来是一页内容转为PDF后变成两页
-->然后就各种查资料来解决...
三、技术选型
1、通过使用AI搜索,有的推荐开源apache poi,但是也说明转换PDF会出现格式问题,直接PASS
2、然后有的推荐documents4j、kkFileView,但是需要外部安装开源LibreOffice,那就是换汤不换药,还是会出现格式问题,也PASS
2、既然上面开源的不行、开源也在迭代,但是毕竟赶不上office升级的速度,所以我考虑闭源组件,那闭源有哪些呢,还是Aspose,如果再回到Aspose就要解决含图片word转换的问题?怎么解决呢?思路就是使用Aspose高版本。
小结:由于客户使用的office软件(一般是microsoft office、WPS)一直在升级,而合同系统里的转换组件并不是一直升级的,那么新版本新建的word文档使用低版本转换组件就可能出现各种格式问题,所以要想一直保持转换word不出问题,就要配套升级转换组件。
四、选择哪个版本
既然选择使用Aspose,那该用哪个版本呢?原先系统里使用的是15.8,所以应该要高于这个版本,那只接用最新版本不就行了,这样最好,但是有两个限制:
(1)合同系统jdk是1.8,最新版本的Aspose依赖的JDK要远高于1.8
(2)新版本的加密技术越来越高级,破解越来越困难。
基于以上问题,我分别尝试了15.12、19.3、21.11,发现21.11可以解决上述转换问题。
附破解代码示例:
Aspose.java:

package com.wjy; import javassist.*; import java.io.IOException; /** * 破解aspose * https://blog.csdn.net/m0_51363655/article/details/144979730 * https://www.cnblogs.com/cxll863/p/16887080.html */ public class Aspose { //修改指定类中的返回值 public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException { crackAsposeWordsJarAddLicense1("D:\\apache-maven-3.6.3\\repo\\com\\aspose\\aspose-words\\21.11"); } private static void crackAsposeWordsJarAddLicense1(String jarName){ try { //这一步是完整的jar包路径,选择自己解压的jar目录 ClassPool.getDefault().insertClassPath(jarName); //获取指定的class文件对象 CtClass zzZJJClass = ClassPool.getDefault().getCtClass("com.aspose.words.zzXDb"); //从class对象中解析获取指定的方法 CtMethod[] methodA = zzZJJClass.getDeclaredMethods("zzY0J"); //遍历重载的方法 for (CtMethod ctMethod : methodA) { CtClass[] ps = ctMethod.getParameterTypes(); if (ctMethod.getName().equals("zzY0J")) { System.out.println("ps[0].getName==" + ps[0].getName()); //替换指定方法的方法体 ctMethod.setBody("{this.zzZ3l = new java.util.Date(Long.MAX_VALUE);this.zzWSL = com.aspose.words.zzYeQ.zzXgr;zzWiV = this;}"); } } //这一步就是将破译完的代码放在桌面上 zzZJJClass.writeFile("E:\\workspace\\aspose\\src\\main\\resources"); //获取指定的class文件对象 CtClass zzZJJClassB = ClassPool.getDefault().getCtClass("com.aspose.words.zzYKk"); //从class对象中解析获取指定的方法 CtMethod methodB = zzZJJClassB.getDeclaredMethod("zzWy3"); //替换指定方法的方法体 methodB.setBody("{return 256;}"); //这一步就是将破译完的代码放在桌面上 zzZJJClassB.writeFile("E:\\workspace\\aspose\\src\\main\\resources"); } catch (Exception e) { System.out.println("错误==" + e); } } private static void viewSetLicenseCode()throws Exception{ com.aspose.words.License wordsLicense = new com.aspose.words.License(); //请点击这里的setLicense方法进入查看源码,不同版本对words 的破解方法名不一样,但道理差不多 wordsLicense.setLicense(""); } }
license.xml:

<License> <Data> <Products> <Product>Aspose.Total for Java</Product> <Product>Aspose.Words for Java</Product> </Products> <EditionType>Enterprise</EditionType> <SubscriptionExpiry>20991231</SubscriptionExpiry> <LicenseExpiry>20991231</LicenseExpiry> <SerialNumber>8bfe198c-7f0c-4ef8-8ff0-acc3237bf0d7</SerialNumber> </Data> <Signature> sNLLKGMUdF0r8O1kKilWAGdgfs2BvJb/2Xp8p5iuDVfZXmhppo+d0Ran1P9TKdjV4ABwAgKXxJ3jcQTqE/2IRfqwnPf8itN8aFZlV3TJPYeD3yWE7IT55Gz6EijUpC7aKeoohTb4w2fpox58wWoF3SNp6sK6jDfiAUGEHYJ9pjU= </Signature> </License>
pom.xml:

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.wjy</groupId> <artifactId>aspose</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency><!--word--> <groupId>com.aspose</groupId> <artifactId>aspose-words</artifactId> <version>21.11</version> <classifier>jdk17</classifier> </dependency> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.27.0-GA</version> </dependency> </dependencies> <repositories> <repository> <id>AsposeJavaAPI</id> <name>Aspose Java API</name> <url>https://repository.aspose.com/repo/</url> </repository> </repositories> <!-- 构建插件配置 --> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.0.0-M5</version> </plugin> </plugins> </build> </project>
参考:
五、新的问题
使用J2SE应用运行转换含图片word是成功的,但是放到java web应用里运行转换就失败了,还没有报错,很奇怪。
附J2SE转换代码:
WordToPdfUtil.java

package com.wjy; import com.aspose.words.Shape; import com.aspose.words.*; import java.awt.*; import java.io.*; //work转pdf工具类 public class WordToPdfUtil { private static boolean license = false; public static void main(String[] args) { WordToPdfUtil wordToPdfUtil = new WordToPdfUtil(); try { wordToPdfUtil.wordToPdf("E:\\code\\project\\sxy\\contract\\file_upload\\file_upload\\202502\\402880e994fdf03a0194fe018c480008","C:\\Users\\Lenovo\\Desktop\\402880e994fdf03a0194fe018c480008.pdf"); wordToPdfUtil.wordToPdf("E:\\code\\project\\sxy\\contract\\file_upload\\file_upload\\202502\\402880e994fe8b9d0194febedc040007","C:\\Users\\Lenovo\\Desktop\\402880e994fe8b9d0194febedc040007.pdf"); } catch (Exception e) { e.printStackTrace(); } } /** * 将Word文档转换为PDF格式-无水印 * 使用Apose库实现文档转换功能,需要先加载License以激活无水印转换。 * * @param wordPath Word文档的路径,包括文件名和扩展名。 * @param pdfPath 生成的PDF文档的路径,包括文件名和扩展名。 * @throws Exception 如果转换过程中发生错误,将抛出异常。 * @return 转换失败时返回null,成功时返回非null值。 */ public static String wordToPdf(String wordPath, String pdfPath) throws Exception { FileOutputStream os = null; try { // 从classpath中加载Apose的License文件,以启用无水印转换。 // 凭证 不然切换后有水印 //InputStream is = new ClassPathResource("/license.xml").getInputStream(); InputStream is =new FileInputStream(new File("E:\\workspace\\Test\\src\\main\\resources\\license.xml")); License aposeLic = new License(); aposeLic.setLicense(is); license = true; // 检查License是否成功加载,如果未成功,则输出错误信息并中止转换。 if (!license) { System.out.println("License验证不通过..."); return null; } // 创建一个空的PDF文件,准备写入转换后的内容。 //生成一个空的PDF文件 File file = new File(pdfPath); os = new FileOutputStream(file); // 创建一个Document对象,指定要转换的Word文档路径。 //要转换的word文件 Document doc = new Document(wordPath); // 将Word文档保存为PDF格式,写入到之前创建的PDF文件中。 doc.save(os, SaveFormat.PDF); } catch (Exception e) { e.printStackTrace(); } finally { // 确保关闭文件输出流。 if (os != null) { try { os.close(); } catch (IOException e) { e.printStackTrace(); } } } return null; } /** * 将指定文件转换为PDF格式,并添加水印。 * * @param toFilePath 目标文件夹路径,用于保存转换后的PDF文件。 * @param fileName 原始文件名。 * @param type 原始文件类型,支持".doc"和".docx"。 * @return 转换后的PDF文件名,如果类型不支持则返回null。 * @throws Exception 如果转换过程中发生错误,则抛出异常。 */ public static String file2pdf(String toFilePath, String fileName, String type ) throws Exception { String htmFileName; // 根据原始文件类型确定转换后的PDF文件名。 //获取转换成PDF之后文件名 if(".doc".equals(type)){ htmFileName = fileName+".pdf"; }else if(".docx".equals(type)){ htmFileName = fileName+".pdf"; }else{ // 如果不支持的文件类型,则返回null。 return null; } // 创建转换后的PDF文件对象。 //通过转换之后的PDF文件名,创建PDF文件 File htmlOutputFile = new File(toFilePath + File.separatorChar + htmFileName); // 获取文件输出流,用于写入转换后的PDF文件。 //获取文件输出流 FileOutputStream os = new FileOutputStream(htmlOutputFile); // 创建Doc文档对象模型,用于读取原始文档并进行转换。 //获取Doc文档对象模型 Document doc = new Document(toFilePath+ File.separatorChar + fileName+type); // 为文档添加水印文本。 //为doc文档添加水印 insertWatermarkText(doc, "");//这里是水印内容 // 将文档保存为PDF格式,并写入到输出流中。 //将doc文旦转换成PDF文件并输出到之前创建好的pdf文件中 doc.save(os, SaveFormat.PDF); // 关闭文件输出流。 //关闭输出流 if(os!=null){ os.close(); } // 返回转换后的PDF文件名。 return htmFileName; } /** * 为Word文档添加文本水印。 * * @param doc 要添加水印的Word文档对象。 * @param watermarkText 水印文本内容。 * @throws Exception 如果操作文档过程中发生错误,则抛出异常。 */ private static void insertWatermarkText(Document doc, String watermarkText) throws Exception { // 创建一个文本形状对象作为水印 Shape watermark = new Shape(doc, ShapeType.TEXT_PLAIN_TEXT); // 设置水印文本内容 watermark.getTextPath().setText(watermarkText); // 设置水印字体及大小 watermark.getTextPath().setFontFamily("宋体"); // 设置水印的宽度和高度 watermark.setWidth(500); watermark.setHeight(100); // 设置水印的旋转角度 watermark.setRotation(-40); // 设置水印的颜色 watermark.getFill().setColor(Color.lightGray); watermark.setStrokeColor(Color.lightGray); // 设置水印的位置属性 watermark.setRelativeHorizontalPosition(RelativeHorizontalPosition.PAGE); watermark.setRelativeVerticalPosition(RelativeVerticalPosition.PAGE); watermark.setWrapType(WrapType.NONE); watermark.setVerticalAlignment(VerticalAlignment.CENTER); watermark.setHorizontalAlignment(HorizontalAlignment.CENTER); // 创建一个段落对象,用于包含水印形状 Paragraph watermarkPara = new Paragraph(doc); watermarkPara.appendChild(watermark); // 遍历文档中的每个节(section),在每个节的主头、首个头和偶数头中插入水印 for (Section sect : doc.getSections()) { // 在指定类型的头部插入水印 insertWatermarkIntoHeader(watermarkPara, sect, HeaderFooterType.HEADER_PRIMARY); insertWatermarkIntoHeader(watermarkPara, sect, HeaderFooterType.HEADER_FIRST); insertWatermarkIntoHeader(watermarkPara, sect, HeaderFooterType.HEADER_EVEN); } // 提示水印添加完成 System.out.println("Watermark Set"); } /** * 在指定的页面头部插入水印。 * <p> * 本方法用于向指定的页面头部插入一个水印。如果指定类型的头部不存在,则会创建一个新的头部并添加到文档中。 * 使用深度克隆来确保水印段落在每个页面头部的独立性。 * * @param watermarkPara 水印段落对象,包含水印的文本和格式信息。 * @param sect 目标节对象,水印将被插入到该节的头部。 * @param headerType 指定的头部类型,用于获取或创建相应类型的头部。 * @throws Exception 如果操作失败,抛出异常。 */ private static void insertWatermarkIntoHeader(Paragraph watermarkPara, Section sect, int headerType) throws Exception{ // 根据指定的头部类型获取现有的头部对象,如果不存在则返回null。 HeaderFooter header = sect.getHeadersFooters().getByHeaderFooterType(headerType); // 如果指定类型的头部不存在,则创建一个新的头部对象,并将其添加到节的头部集合中。 if (header == null) { header = new HeaderFooter(sect.getDocument(), headerType); sect.getHeadersFooters().add(header); } // 将水印段落深度克隆,并添加到头部对象中,确保每个页面的水印都是独立的。 header.appendChild(watermarkPara.deepClone(true)); } }
pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.wjy</groupId> <artifactId>test</artifactId> <version>1.0-SNAPSHOT</version> <description>test</description> <packaging>jar</packaging> <dependencies> <!--word转pdf--> <dependency> <groupId>com.aspose</groupId> <artifactId>aspose-words</artifactId> <version>21.11</version> <scope>system</scope> <systemPath>${project.basedir}/src/main/resources/lib/aspose-words-21.11-jdk17.jar</systemPath> </dependency> </dependencies> <repositories> <repository> <id>aliyunmaven</id> <name>aliyun maven</name> <url>http://maven.aliyun.com/nexus/content/groups/public/</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> <!-- 构建插件配置 --> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.0.0-M5</version> </plugin> </plugins> </build> </project>
license.xml同上
既然在java web里转换失败,那怀疑是不是环境差异,我单独在java web里写了一个简单的转换类,然后单独执行这个文件:
Wtp.java

package com.supporter.prj.bm.contract.util; import com.aspose.words.Document; import com.aspose.words.License; import com.aspose.words.SaveFormat; import com.supporter.prj.log4j.XLogger; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; public class Wtp { public static void main(String[] args) { String docPath = "C:\\Users\\Lenovo\\Desktop\\a.docx"; String pdfPath = "C:\\Users\\Lenovo\\Desktop\\a.pdf"; docTopdf(docPath,pdfPath); } public static boolean getLicense() { boolean result = false; try { InputStream is = new FileInputStream(new File("E:\\workspace\\bmv5\\web\\WEB-INF\\classes\\license.xml")); License aposeLic = new License(); aposeLic.setLicense(is); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } public static void docTopdf(String inPath,String outPath) { // 验证License 若不验证则转化出的pdf文档会有水印产生 if (!getLicense()) { return; } try { long old = System.currentTimeMillis(); FileOutputStream os = new FileOutputStream(new File(outPath)); //inPath是要被转化的word文档 Document doc = new Document(inPath); /*PdfSaveOptions options = new PdfSaveOptions(); options.setCompliance(PdfCompliance.PDF_17); options.setImageCompression(PdfImageCompression.AUTO); // 指定PDF页面大小和方向 options.pageSetup.paperSize = Aspose.Words.PaperSize.Letter; options.pageSetup.orientation = Aspose.Words.Saving.PageOrientation.Portrait; doc.save(os, options);*/ doc.save(os, SaveFormat.PDF); if (os != null) { os.close(); } long now = System.currentTimeMillis(); XLogger.getLogger().myDebug("Aspose转PDF共耗时:" + ((now - old) / 1000.0) + "秒"); } catch (Exception e) { e.printStackTrace(); } } }
运行设置:
然后运行,控制台出现报错:

Exception in thread "main" java.lang.NoClassDefFoundError: javax/media/jai/util/ImagingListener at com.aspose.words.internal.zzWts.zzX2X(Unknown Source) at com.aspose.words.internal.zzY4C.zzWuo(Unknown Source) at com.aspose.words.internal.zzY4C.<init>(Unknown Source) at com.aspose.words.internal.zzY4C.<init>(Unknown Source) at com.aspose.words.internal.zzY4C.<init>(Unknown Source) at com.aspose.words.internal.zzY4C.<init>(Unknown Source) at com.aspose.words.internal.zzEu.zzic(Unknown Source) at com.aspose.words.internal.zzEu.zzYzG(Unknown Source) at com.aspose.words.internal.zzMs.zzYpp(Unknown Source) at com.aspose.words.internal.zzMs.zzX2J(Unknown Source) at com.aspose.words.internal.zzWBl.zzYnz(Unknown Source) at com.aspose.words.internal.zzX5J.zzXou(Unknown Source) at com.aspose.words.internal.zzNh.zzYnz(Unknown Source) at com.aspose.words.internal.zzXmw.zzv8(Unknown Source) at com.aspose.words.internal.zzYvz.zzv8(Unknown Source) at com.aspose.words.zzb9.zzYq6(Unknown Source) at com.aspose.words.zzZjM.zzWuo(Unknown Source) at com.aspose.words.zzh8.zzWuo(Unknown Source) at com.aspose.words.Document.zzXou(Unknown Source) at com.aspose.words.Document.zzWuo(Unknown Source) at com.aspose.words.Document.zzWuo(Unknown Source) at com.aspose.words.Document.zzWTv(Unknown Source) at com.aspose.words.Document.save(Unknown Source) at ....docTopdf(Wtp.java:54) at ....Wtp.main(Wtp.java:17) Caused by: java.lang.ClassNotFoundException: javax.media.jai.util.ImagingListener at java.net.URLClassLoader.findClass(URLClassLoader.java:382) at java.lang.ClassLoader.loadClass(ClassLoader.java:418) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355) at java.lang.ClassLoader.loadClass(ClassLoader.java:351) ... 25 more Process finished with exit code 1
发现缺少:javax/media/jai/util/ImagingListener类,上网查了一下:说是要引入jai_codec-1.1.3.jar和jai-core-1.1.3.jar,然后从网上下载放进去,重新运行又报了如下错误:

java.lang.SecurityException: sealing violation: package javax.media.jai.util is sealed at java.net.URLClassLoader.getAndVerifyPackage(URLClassLoader.java:400) at java.net.URLClassLoader.definePackageInternal(URLClassLoader.java:420) at java.net.URLClassLoader.defineClass(URLClassLoader.java:452) at java.net.URLClassLoader.access$100(URLClassLoader.java:74) at java.net.URLClassLoader$1.run(URLClassLoader.java:369) at java.net.URLClassLoader$1.run(URLClassLoader.java:363) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:362) at java.lang.ClassLoader.loadClass(ClassLoader.java:418) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355) at java.lang.ClassLoader.loadClass(ClassLoader.java:351) at com.aspose.words.internal.zzWts.zzX2X(Unknown Source) at com.aspose.words.internal.zzY4C.zzWuo(Unknown Source) at com.aspose.words.internal.zzY4C.<init>(Unknown Source) at com.aspose.words.internal.zzY4C.<init>(Unknown Source) at com.aspose.words.internal.zzY4C.<init>(Unknown Source) at com.aspose.words.internal.zzY4C.<init>(Unknown Source) at com.aspose.words.internal.zzEu.zzic(Unknown Source) at com.aspose.words.internal.zzEu.zzYzG(Unknown Source) at com.aspose.words.internal.zzMs.zzYpp(Unknown Source) at com.aspose.words.internal.zzMs.zz9h(Unknown Source) at com.aspose.words.internal.zzMs.zzX2J(Unknown Source) at com.aspose.words.internal.zzWBl.zzYnz(Unknown Source) at com.aspose.words.internal.zzX5J.zzXou(Unknown Source) at com.aspose.words.internal.zzNh.zzYnz(Unknown Source) at com.aspose.words.internal.zzXmw.zzv8(Unknown Source) at com.aspose.words.internal.zzYvz.zzv8(Unknown Source) at com.aspose.words.zzb9.zzYq6(Unknown Source) at com.aspose.words.zzZjM.zzWuo(Unknown Source) at com.aspose.words.zzh8.zzWuo(Unknown Source) at com.aspose.words.Document.zzXou(Unknown Source) at com.aspose.words.Document.zzWuo(Unknown Source) at com.aspose.words.Document.zzWuo(Unknown Source) at com.aspose.words.Document.zzWTv(Unknown Source) at com.aspose.words.Document.save(Unknown Source) at ....Wtp.docTopdf(Wtp.java:51) at ....Wtp.main(Wtp.java:17)
java.lang.SecurityException: sealing violation: package javax.media.jai.util is sealed,上网查资料说是违反jar封装安全,那我想了一下,是不是原系统下是不是有其他版本的 含有javax.media.jai.util目录的jar,把lib排序后认真找了一下,果然下面存在低版本的jai_core.jar、jai_codec.jar,把这俩移除,重新运行后,成功转换,然后启动java web在线转换也成功。
小结:
(1)首先得找到错误信息,在java web环境下模拟j2se运行,看看是否还能成功
(2)最后发现在java web环境下,升级Aspose,也要配套升级对应的jai_codec和jai-core,我这里实验结果就是aspose-words-21.11-jdk17.jar+jai_codec-1.1.3.jar+jai-core-1.1.3.jar.
总结:
业务系统中经常存在这样的场景,要处理office文档:格式转换、加签、在线编辑、比对......。一般使用开源组件都有这样或那样的问题,不能满足所有场景,毕竟有一个事实:office升级的速度远远快于开源升级的速度。上述升级组合,只能解决当下问题,可能在不久的将来还会出现其他格式转换问题. 所以,涉及此类场景,尽量建议公司直接采购第三方组件,不要浪费太多时间,花费大量时间解决一两个格式转换问题,得不偿失。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
2019-02-14 【Hive学习之四】Hive 案例