Все още съм доста нов в областта на 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