"Да не изобрети ты велосипед" - одно из главных правил успешной и эффективной работы. Но что делать, когда свой велосипед изобретать не хочется, а у чужого руль оказался кривой, а колёса квадратными? Данный обзор предназначен для по возможности краткого ознакомления с приёмом исправления в чужих библиотеках "на крайний случай" и о том, как это дело распространить дальше своего компьютера.
Введение
Все мы пользуемся теми или иными инструментами. Но иногда инструменты подходят не до конца или имеют в себе ошибки. Благодаря особенностям языка Java мы можем исправить поведение инструментов там, где нам это нужно. Хорошо, когда мы вносим свой вклад проекты и отсылаем пулл реквесты (подробнее можно прочитать тут: «GitHub - Внесение собственного вклада в проекты»). Но принимать их могут не сразу, а могут даже и не принять. Но для нужд проекта нужно вот сейчас. И тут, надеюсь, данная статья покажет доступные нам, как разработчику, средства. Нам понадобится выполнить следующие действия, о которых мы будем говорить:- Подготовить испытуемое приложение для примера (на примере Hibernate проекта)
- Поиск изменяемого места
- Выполнение изменения
- Разворачивание репозитория
Подготовка испытуемого
Итак, нам нужен подопытный проект. Нам идеально подойдёт Hibernate, т.к. это "стильно, модно, современно". Не буду особо вдаваться в детали, т.к. статья не про Hibernate. Будем делать всё быстро и по делу. И будем, как правильные разработчики , использовать систему сборки. Например, нам так же подойдёт Gradle, который для данной статьи должен быть установлен (https://gradle.org/install/). Сначала, нам нужно создать проект. У Maven’а для этого есть архетипы, а у Gradle для этого есть особенный плагин: Gradle Init. Итак, открываем командную строку любым известным вам способом. Создаём каталог для проекта, переходим в него и выполняем комманду:
mkdir javarush
cd javarush
gradle init --type java-application
Прежде чем выполнять импорт проекта внесём некоторые изменения в файл, описывающий, каким образом нужно выполнять сборку. Этот файл называется build script’ом и имеет имя build.gradle. Находится он в том каталоге, в котором мы выполнили gradle init. Поэтому, просто открываем его (например, в windows командой start build.gradle).
Находим там блок «dependencies», т.е. зависимости. Тут описываются все сторонние jar, которые мы будем использовать. Теперь надо понять, что тут описывать.
Перейдём на сайт Hibernate (http://hibernate.org/). Нас интересует Hibernate ORM. Нам нужен последний релиз. В меню слева есть подраздел «Releases». Выбираем «latest stable». Проматываем вниз и находим «Core implementation (includes JPA)». Раньше нужно было поддержку JPA подключать отдельно, но теперь всё стало проще и достаточно только одной зависимости.
Также нам понадобится при помощи Hibernate работать с базой данных. Для этого возьмём самый простой вариант – H2 Database. Выбор сделан, вот наши зависимости:
dependencies {
// Базовая зависимость для Hibernate (новые версии включают и JPA)
compile 'org.hibernate:hibernate-core:5.2.17.Final'
// База данных, к которой мы будем подключаться
compile 'com.h2database:h2:1.4.197'
// Use JUnit test framework
testCompile 'junit:junit:4.12'
}
Отлично, что дальше? Надо настроить Hibernate. У Hibernate есть «Getting Started Guide», но он дурацкий и больше мешает, чем помогает. Поэтому пойдём как правильные люди сразу в «User Guide».
В оглавлении видим раздел «Bootstrap», что переводится как «Начальная загрузка». То что надо. Там написано много умных слов, но смысл в том, что на classpath должен быть каталог META-INF, а там файл persistence.xml. На classpath по стандарту попадает каталог «resources». Поэтому создаём указанный каталог: mkdir src\main\resources\META-INF
Создаём там файл persistence.xml и открываем его.
Там же в документации есть пример «Example 268. META-INF/persistence.xml configuration file» из которого мы возьмём содержимое и вставим в файл persistence.xml.
Далее запускаем IDE и импортируем в неё наш созданный проект.
Теперь нам нужно что-то сохранять в базу. Это что-то называется сущности. Сущности представляют что-то из так называемой доменной модели. И в оглавлении, о чудо, видим «2. Domain Model». Спускаемся ниже по тексту и видим в главе "2.1. Mapping types" простой пример сущности. Забираем его к себе, чуть сократив:
package entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity(name = "Contact")
public class Contact {
@Id
@GeneratedValue
private Integer id;
private String name;
public Contact(String name) {
this.name = name;
}
}
Теперь у нас появился класс, представляющий сущность. Вернёмся в persistence.xml и поправим там одно место: Там где указан class
укажем свой класс entity.Contact
.
Отлично, осталось запуститься.
Возвращаемся в главу «Bootstrap». Так как у нас нет сервера приложений, который нам предоставит особое EE окружение (т.е. окружение, которое реализует для нас определённое поведение системы), то мы работаем в SE окружении. Для него нам подойдёт только пример «Example 269. Application bootstrapped EntityManagerFactory». Например, сделаем так:
public class App {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("CRM");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Contact contact = new Contact("Vasya");
em.persist(contact);
em.getTransaction().commit();
Query sqlQuery = em.createNativeQuery("select count(*) from contact");
BigInteger count = (BigInteger) sqlQuery.getSingleResult();
emf.close();
System.out.println("Entiries count: " + count);
}
}
Ура, наш испытуемый готов. Эту часть я не хотел опускать, т.к. для следующих глав желательно понимать то, как появился наш испытуемый.
Поиск изменяемого поведения
Давайте встанем на место инициализации поля count типа BigInteger и поставим там точки останова (BreakPoint). Встав на нужной строке это можно сделать при помощи Ctrl+F8 или через меню Run -> Toggle Line Breakpoint. После чего запускаем наш main метод в дебаге (Run -> Debug):Немного неуклижий пример, но, допустим, мы хотим изменить количество query spaces при старте. Как мы видим, наш sqlQuery это NativeQueryImpl. Нажимаем
Ctrl+N
, пишем название класса, переходим в него. Чтоб при переход в класс нас перебрасывало на то место, где лежит этот класс включил автоскрол:
Сразу заметим, что Idea не знает сейчас, где можно найти исходный код программы (исходники, то есть). Поэтому она милостиво декомпилировала для нас из class файла содержимое:
Заметим так же, что в заголовке окна IntelliJ Idea пишется, где Gradle сохраняет для нас артефакт.
Теперь, получим в Idea путь, где лежит наш артефакт:
Перейдём в этот каталог в командной строке при помощи команды
cd путь к каталогу
. Сразу сделаю замечание: если есть возможность собрать проект из исходников, лучше собирать из исходников. Например, исходный код Hibernate доступен на официальном сайте. Лучше забрать его для нужной версии и сделать все изменения там и собраться при помощи скриптов сборки, которые указаны в проекте. Я же привожу в статье самый ужасный вариант – есть jar, а исходного кода нет. И замечание номер 2: Gradle может получить исходный код при помощи плагинов. Подробнее см. «How to download javadocs and sources for jar using Gradle.
Выполнение изменения
Нам нужно воссоздать структуру каталогов в соответствии с тем, в каком пакете лежит изменяемый нами класс. В данном случае:mkdir org\hibernate\query\internal
, после чего создаём в этом каталоге файл NativeQueryImpl.java
.
Теперь открываем данный файл и копируем туда всё содержимое класса из IDE (то самое, которое для нас декомпилировала Idea). Изменяем нужные строки. Например:
Теперь, компилируем файл. Выполняем:
javac org\hibernate\query\internal\NativeQueryImpl.java
.
Надо же, нельзя просто взять и скомпилировать без ошибок. Получили кучу ошибок Cannot Find Symbol, т.к. изменяемый класс завязан на другие классы, которые обычно IntelliJ Idea за нас добавляет в classpath. Ощущаете всю полезность наших IDE? =) Чтож, давайте добавим сами, мы тоже можем.
Скопируем отдельно в блокнот пути для:
- [1] - hibernate-core-5.2.17.Final.jar
- [2] - hibernate-jpa-2.1-api-1.0.0.Final.jar
Ctrl+Shift+C
.
Теперь сформируем и выполним такую комманду:
javac -cp [1];[2] org\hibernate\query\internal\NativeQueryImpl.java
В результате рядом с java файлом появятся новые class файлы, которые нужно обновить в jar файле:
Ура, теперь можно выполнить jar update. Можем руководствоваться официальными материалами:
jar uf hibernate-core-5.2.17.Final.jar org\hibernate\query\internal\*.class
Открытая IntelliJ Idea, скорей всего, не даст изменять файлы. Поэтому до выполнения jar update, скорей всего, придётся закрыть Idea, а после обновления - открыть. После этого можно заново открываем IDE, опять выполняем dubug. Break Points не сбрасываются между перезапусками IDE. Поэтому, выполнение программы остановится там, где и раньше. Вуаля, мы видим как работают наши изменения:
Отлично. Но тут возникает вопрос – благодаря чему? Просто благодаря тому, что когда gradle собирает проект, он анализирует блок dependencies и repositories. У грэдла есть некий build cache, который лежит в определённом месте (см. «How to set gradle cache location?». Если в кэше нет зависимости, то Gradle её скачает из репозитория. Т.к. мы меняли jar в самом кэше, то Gradle думает, что в кэше библиотека есть и ничего не выкачивает. Но любая очистка кэша приведёт к тому, что наши изменения пропадут. Плюс, никто кроме нас не может просто взять и получить их. Сколько неудобств, не правда ли? Что же делать. Хм, скачивает из репозитория? Значит, нам нужен наш репозиторий, с преферансом и поэтессами. Об этом следующий этап.
Разворачивание репозитория
Для разворачивания своего репозитория существуют разные бесплатные решения: одно из них Artifactory, а другое - Apache Archive. Артифактори выглядит модно, стильно, современно, но с ним у меня возникли трудности, никаких не хотел правильно размещать артефакты и генерировал ошибочные мавен метаданные. Поэтому, неожиданно для себя, у меня заработал апачевский вариант. Он оказался не такой красивый, зато работает надёжно. На странице загрузки ищем Standalone версию, распаковываем. У них есть свой "Quick Start". После запуска надо дождаться, когда по адресуhttp://127.0.0.1:8080/#repositorylist
. После этого выбираем "Upload Artifact":
Нажимаем "Start Upload", а после "Save Files". После этого появится зелёное сообщение об успешности и артефакт станет доступен в разделе "Browse".
Так надо сделать для jar и для pom файлов:
Это связано с тем, что в pom файле прописаны дополнительные зависимости хибернейта.
А нам осталось только 1 шаг - указать репозиторий в нашем билд скрипте:
repositories {
jcenter()
maven {
url "http://127.0.0.1:8080/repository/internal/"
}
}
И, соответственно, версия нашего хибернейта станет: compile 'org.hibernate:hibernate-core:5.2.17.Final-JAVARUSH'
.
Вот и всё, теперь наш проект использует исправленный нами вариант, а не изначальный.
Заключение
Вот вроде и ознакомились. Надеюсь, было интересно. Подобные "трюки" делаются редко, но если вдруг ваши бизнес требования поставят условия, которые не могут удовлетворить используемые Вами библиотеки - вы знаете что делать. И да, вот пару примеров, что может так исправляться:- Есть такой веб-сервер Undertow. До некоторого времени был баг, который при использовании прокси не давал узнать IP конечного пользователя.
- До поры до времени WildFly JPA определённым образом обрабатывал один не учтённый спефицикацией момент, из-за этого сыпались Exception. И это не настраивалось.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ