REPL 和 jar 中的 Tika Parser 行为不同
Posted
技术标签:
【中文标题】REPL 和 jar 中的 Tika Parser 行为不同【英文标题】:Tika Parser behaviour different in REPL and jar 【发布时间】:2021-11-30 04:15:53 【问题描述】:说明
我们在pantomime 后面的包装版本中使用Tika Parser,没有配置(默认),它使用AutoDetectParser
。由于缺少功能,升级 Tika 依赖项至关重要。在将 tika 覆盖为 1.27 的依赖项以及其他依赖项之后,我们观察到了一些意想不到的行为。在lein repl
中运行服务时,tika 正确转换了相关文档 (.doc)。
之后我们用lein uberjar
打包了clj代码。
现在有趣的部分开始了:
随后是对 tika 的深度(th)和类型转换的野生虫子狩猎。
发现问题出在不同的解析数据类型上。在 REPL 中解析的数据类型是 javax.mail.internet.MimeMessage
,而生成的 .jar 中解析的数据类型是 javax.mail.util.SharedByteArrayInputStream
。
日志摘录:
REPL
[2021-10-08 16:56:30,406] TRACE xxx - Unpacking MimeMessage -1
[2021-10-08 16:56:30,407] DEBUG xxx - |> type javax.mail.internet.MimeMessage
[2021-10-08 16:56:30,415] DEBUG xxx - ||> javax.mail.internet.MimeMultipart@397fca9a
[2021-10-08 16:56:30,415] DEBUG xxx - |-> transforming multipart/mixed to Mime for instance javax.mail.internet.MimeMultipart
罐子
[2021-10-08 17:02:55,181] TRACE xxx - Unpacking MimeMessage -1
[2021-10-08 17:02:55,183] DEBUG xxx - |> type javax.mail.internet.MimeMessage
[2021-10-08 17:02:55,191] DEBUG xxx - ||> javax.mail.util.SharedByteArrayInputStream@b83be05
[2021-10-08 17:02:55,193] WARN xxx - Mime unpacking unsuccessful: multipart/mixed - javax.mail.util.SharedByteArrayInputStream
调查结果
我怀疑可能存在依赖冲突,因为 tika-parser 和 simplejavamail 模块都使用 MimeMessage
的实现,jakarta.mail
和 javax.mail
都提供了该实现。我已经解决了 leiningen 暗示的依赖冲突,但怀疑lein repl
如何解决依赖关系可能会发生一些事情,这使得它在这种情况下工作。
使用干净的profiles.clj 复制了此行为。
对此函数.getContent 的日志记录和堆栈跟踪提示,该函数负责返回适当的对象。
project.clj
(defproject my-project :lein-v
:plugins [[lein-parent "0.3.8"]
[com.roomkey/lein-v "7.2.0"]]
:parent-project :path "../../project.clj"
:inherit [:managed-dependencies :repositories :manifest :url :prep-tasks]
:dependencies [[org.clojure/clojure]
[...]
[com.novemberain/pantomime "2.11.0" :exclusions
[org.apache.commons/commons-compress
org.apache.pdfbox/fontbox
org.apache.tika/tika-parsers]]
[org.apache.tika/tika-parsers "1.27"]
[com.sun.mail/javax.mail "1.6.2"]
[net.htmlparser.jericho/jericho-html "3.4"]
[org.simplejavamail/simple-java-mail "6.6.1"]
[org.simplejavamail/outlook-module "6.6.1"]]
:profiles :uberjar :global-vars *warn-on-reflection* true
:aot :all
:omit-source true
:uberjar-exclusions ["project.clj" #"xxx/.*\.clj$"]
:jvm-opts ["-XX:-OmitStackTraceInFastThrow"]
:uberjar-name "service.jar"
:repl-options :init-ns xxx.init)
受影响的代码
(ns xxx.convert
(:import (java.time OffsetDateTime ZoneOffset)
(java.util Properties UUID Base64 Date)
(java.io InputStream ByteArrayInputStream)
(javax.activation MimeType)
(javax.mail.internet MimeMessage MimeMultipart MimeBodyPart InternetAddress MimeUtility)
(javax.mail Session Message Message$RecipientType)
(net.htmlparser.jericho Source Renderer)
(org.apache.commons.io IOUtils)
(defn- unpack-content [^Message message
:keys [max-text-length max-attachments] :as params
counter]
(log/debug "|> type" (type message))
(when (or (not max-attachments)
(< @counter max-attachments))
(let [base-type (-> message .getContentType MimeType. .getBaseType string/lower-case)
content (.getContent message)] ;; <<< [!] This is where the conversion happens
(log/debug "||>" (.toString ^MimeMultipart content))
:content-type base-type
:content (cond
(instance? String content)
(do
(log/trace "|-> transforming" base-type "to String")
(swap! counter inc)
(->> (if (html? base-type)
(render-html content)
content)
(truncate max-text-length)))
(and (instance? InputStream content)
(.getFileName message))
(let [filename (MimeUtility/decodeText (.getFileName message))
encoded-file (do
(swap! counter inc)
(base64-file content filename))]
(log/trace "|-> transforming" base-type "to InputStream")
(.close ^InputStream content)
encoded-file)
(or (instance? MimeMessage content)
(instance? MimeMultipart content)
(instance? MimeBodyPart content))
(do
(log/debug "|-> transforming" base-type "to Mime for instance" (type content))
(unpack-mime content params counter))
:else
(do
(log/warn "Mime unpacking unsuccessful:" base-type "-" (type content))
)))))
很遗憾,我无法共享相应的 .doc 文件。然而,它是一个官方文档,带有表格、图像等普通邮件的通用设置。
为什么会发生,我该如何解决?
【问题讨论】:
这全是猜测:在不同的包上拥有两个类不是冲突或问题(如果某些代码尝试了 smart things™,则可能是这样)。从 javax 到 jakarta 的切换很可能是在上游的某个主要版本上完成的,不应该打击你。我的猜测是,你有 deps,可能会将两者都放在同一个类中——并且你在 repl-time 和 run-time 有不同的类路径顺序(通过生成的 uberjar)。我会检查,最终在 uberjar 中的文件是否实际上来自您期望的 dep。并且仍然进行三次检查,如果这不仅仅是由于输入而导致的侥幸...... 如何检查这种实际的依赖关系? 如果您想排除类或 META-INF 中的冲突,您可以尝试运行生产代码,而不是作为 uberjar,而是作为常规 jar。例如。使用lein with-profile uberjar cp
获取所有库。然后使用这些和你的工件(不是 uberjar)并运行它。如果这再次起作用,您可以开始在您的部门中找到冲突(或放弃 uberjar)
谢谢@cfrick。我用java -cp $(lein with-profile uberjar) xxx.init
运行了这个工件并且可以重现正确的行为
那么我的下一个猜测将是 META-INF 中的内容。我对 javax.mail 一无所知,但通常那些“旧”库允许通过一些工厂/配置/属性/...配置实现 - 很可能您的至少两个部门包含此类文件,错误的一个获胜。如果您可以查明问题,一些库还允许通过-D
属性进行设置。
【参考方案1】:
Leiningen 在依赖顺序方面有一个众所周知的“特性”[1]。 Lein 按顺序加载每个依赖项,递归地包括所有传递依赖项。想象一下,您有 lein deps,例如:
[aaa/a "5.0"]
[bbb/b "5.0"]
但是,aaa/a
对 bbb/b
版本 2.1
具有传递依赖。然后 Lein 将引入 2.1
的版本 bbb/b
并忽略您要求的显式版本。
解决这个问题的唯一方法是在project.clj
中使用分层的依赖关系列表,例如:
; priority 1 libs
[bbb/b "5.0"]
[zzz/z "10.3"]
...
; priority 2 libs
[org.clojure/clojure "1.10.3"]
[aaa/a "5.0"]
...
这将迫使 Lein 按照您选择的优先顺序引入库。
请尝试上述方法并报告结果。您可能还需要从代码中打印出 Classpath 顺序,以验证您获得了预期的顺序和版本号。
旁注:
我始终建议您将 lein-ancient
插件添加到您的配置中,例如:
:plugins [[com.jakemccrary/lein-test-refresh "0.24.1"]
[lein-ancient "0.7.0"]
]
然后您可以定义别名:
lanc ()
echo ""
echo ""-----------------------------------------------------------------------------
echo "project.clj:"
echo ""
lein ancient check :all
echo ""
echo ""-----------------------------------------------------------------------------
echo "profiles.clj:"
echo ""
lein ancient check-profiles :all
echo ""-----------------------------------------------------------------------------
echo ""
确定哪些依赖项需要升级。请注意,这两部分都很重要!
[1] 古代电脑笑话:
Q: How do you recognize a computer salesman?
A: He says, "That's not a bug, that's a feature!"
【讨论】:
嗨艾伦,谢谢您的回答。我知道 lein Ancient,实际上用它来升级依赖项。我不知道可以使用check-profiles :all
,但lein ancient
和lein ancient check :all
没有区别。我也尝试交换订单,但不幸的是这并没有改变行为。我什至尝试在lein ancient show-versions
的帮助下使用不同的版本。问题显然只发生在我打包 uberjar 时。删除工件 jar 并仅运行带有 cp 的 uberjar 可以正常工作。【参考方案2】:
leiningen uberjar 的工作方式基本上是压缩所有的 deps 放入一个 jar 文件中。
这在大多数情况下都没有问题,因为文件结构存在冲突 非常罕见,因为 java 包或 clojure 命名空间将事物分开。 当然有奇怪的命名冲突,有时图书馆复制 来自不同图书馆的东西供当地收养(一个好的图书馆 维护者将“隐藏”这些副本,因此没有机会 冲突)。
也就是说,jar 中的文件更有可能发生冲突:
/META-INF/
下的东西。在您的情况下,javax.mail
和
jakarta.mail
正在努力成为邮件接口的提供者
通过META-INF/services/javax.mail.Provider
。
如果这些文件是从类路径加载的,那么最好的情况是 场景,库显式搜索所有此类文件和 在出现多个结果时制定解决策略。更可能, 库只是让类加载器决定(很可能是第一项 在 CP 中,包含文件,但这当然是一个实现 细节,你不应该依赖它)。在这种情况下,你很可能 正是在你的 REPL 中开发时幸运的地方并且得到了正确的 捡起来。
现在,如果 uberjar 压缩了所有这些 jar,则最后一个文件获胜。所以 解决策略和您的“幸运首选”都没有帮助。所以呢 可以做到:
摆脱两个部门之一。您很可能希望保留jakarta
的,因为较新的名称暗示较新的库;这
取决于您正在使用的其他库,它们是否可以工作
使用新版本,当然可能需要更改您的代码
也是。
不要滚动 uberjar:我不知道有什么插件可以给你
类似于 gradle 中的 application
插件。但是你可以得到
所有使用lein with-profile uberjar cp
和非uberjar 的部门
神器。然后基本上从java -cp libs/* my.main.ns
开始
【讨论】:
以上是关于REPL 和 jar 中的 Tika Parser 行为不同的主要内容,如果未能解决你的问题,请参考以下文章