Все още съм доста нов в областта на JVM. След известен опит с JRuby и сега работа с Clojure в производство в продължение на почти две години, мисля, че знам как да се справя с някои от нещата за взаимодействие на Java, да настройвам JVM и да науча, че механизмът за регулярни изрази на JVM не работи както на други езици (по отношение на производителността).
Има обаче друга концепция за реалния свят, с която ще се сблъска всеки реален софтуерен проект: зависимости. Приложенията на Clojure, които зависят от библиотеки, написани на Clojure, са лесни — Leiningen решава всички проблеми. Изтеглянето на съществуващи библиотеки, написани на Java от, да кажем, Maven Central също е добре — въпреки че по-сложни неща като Spark понякога са проблематични.
Там, където стана наистина трудно, беше как да се използва библиотека, написана основно на Java, със слой Clojure отгоре (за да се скрият нещата на Java ;-)) и да се използва в проект на Clojure.
Очаквания
Например – когато опаковате Ruby проект в скъпоценен камък, е доста безопасно да приемете, че ако вашата библиотека се доставя с файл с ресурси (например html шаблон), той може да се чете по следния начин:
class Foobar TEMPLATE = File.read(File.expand_path('./tmpl.txt', __FILE__)) end
Това до голяма степен гарантира, че ако foobar.rb
и tmpl.txt
са в една и съща директория, без значение къде и как е инсталиран gem или ако просто изпълняваме кода чрез ruby ./lib/foo/foobar.rb
.
В Java/JVM земята нещата не са толкова прости.
Горният фрагмент ще работи, но ако създаваме и разпространяваме библиотека като jar
файл, нещата стават сложни.
Реалния свят
Моят сценарий беше следният:
- Имам услуга, написана 100% в Clojure (нека я наречем
foo
, очевидно) - Имам библиотека
bar
, също 100% в Clojure - Библиотека
baz
е 99% Java и 1% е обвивка на Clojure, освен товаbaz
се нуждае от файлове със статични ресурси, за да работи (файл с конфигурация и данни за NLP модел)
foo
зависи от bar
и baz
.
Тъй като използвам Bintray за хостване на частно репо на Maven, нещата са доста прости. С Leiningen всичко, което трябва да направя, е да добавя:
;; used for publishing as a lib :deploy-repositories [["releases" {:url "https://api.bintray.com/maven/repo/maven/bar/;publish=1" :sign-releases false :username :env/bintray_username :password :env/bintray_api_key}] ["snapshots" {:url "https://api.bintray.com/maven/repo/maven/bar/;publish=1" :sign-releases false :username :env/bintray_username :password :env/bintray_api_key}]]
до project.clj
и стартирайте lein deploy
. Това ще компилира всичко, ще създаде пакет maven и ще го качи в Bintray.
След това в foo
на project.clj
:
:repositories [["bintray" {:url "https://repo.bintray.com/maven" :snapshots true :username :env/bintray_username :password :env/bintray_api_key}]] :dependencies [["bar" "0.0.1"]]
ще направи частните библиотеки достъпни като зависимости.
Дотук добре
Както може да се очаква baz
Java/Clojure lib се оказа малко проблематичен:
- допълнителен ресурсен файл беше прочетен по време на изпълнение и кодът прие, че е наличен под
resources/db.txt
- когато се внедри като jar (дори локално, използвайки
lein install
), файлът ще бъде включен - обаче използването на
baz
като зависимост вfoo
няма да работи, тъй като пътят на файла вече няма да бъде път на файловата система, а вместо това ще бъде превърнат в ресурс.
Първият ми подход беше да конвертирам целия код от просто четене на файлове от пътища към използване на ресурси:
// before class SomeStuff { private final db; public void SomeStuff(String pathToDB) { db = new BufferedReader(new FileReader(pathToDB)); } } // after // in tests InputStream in = this.getClass().getResourceAsStream(pathToDB); class SomeStuff { private final db; public SomeStuff(InputStream db) db = new BufferedReader(new InputStreamReader(db)); } }
след това в Clojure:
;; before (SomeStuff. "resources/db.txt") ;; after (require '[clojure.java.io :as io] (SomeStuff. (-> "db.txt" io/resource io/file io/input-stream))
Проведох теста и го насочих към нашия CI сървър. всичко работи Тествах кода в REPL, всичко е наред.
Чисто
След lein install
с удоволствие използвах baz
код в foo
и стартирах тестовете и...
billion lines of stacktraces
Caused by: java.lang.IllegalArgumentException: Not a file: jar:file:/home/vagrant/.m2/repository/baz/baz/1.0.7-SNAPSHOT/baz-1.0.7-SNAPSHOT.jar!/db.txt
Тъй като jar
файловете са само zip файлове, надникнах вътре и db.txt
беше там. И двата теста на Clojure и JUnit преминаха добре в baz
така че... какво се случи?
Започнах да проверявам как другите хора правят това, тъй като търсенето в Гугъл не помогна много. Много бързо разбрах грешката си. Виждате, че java.io.InputStream
знае как да се справя с много неща - не само с файлове, но и с... ресурси.
So:
;; before (require '[clojure.java.io :as io] (SomeStuff. (-> "db.txt" io/resource io/file io/input-stream)) ;; after (SomeStuff. (-> "db.txt" io/resource io/input-stream))
Изглеждаше като малко случайна промяна, но:
- тестовете в
baz
работиха добре - след инсталиране в локално хранилище на Maven
foo
дръпнаbaz
съвсем добре - тестовете в
foo
работиха според очакванията
Резюме
ДО как да:
- изпратете смесен проект Clojure/Java като lib
- тази библиотека има някои ресурси (които не са код)
- и как да използвате всичко това в друг проект на Clojure