Регулярные выражения Java

facbadfb91b9

Регулярные выражения — тема, которую программисты, даже опытные, зачастую откладывают на потом. Однако большинству Java-разработчиков рано или поздно придётся столкнуться с обработкой текстовой информации. Чаще всего — с операциями поиска в тексте и редактированием. Без регулярных выражений продуктивный и компактный программный код, связанный с обработкой текстов, попросту немыслим. Так что хватит откладывать, разберёмся с «регулярками» прямо сейчас. Это не такая уж и сложная задача.

Что такое регулярное выражение RegEx?

На самом деле регулярное выражение (RegEx) – это шаблон для поиска строки в тексте. В Java исходным представлением этого шаблона всегда является строка, то есть объект класса String. Однако не любая строка может быть скомпилирована в регулярное выражение, а только та, которая соответствует правилам написания регулярного выражения – синтаксису, определенному в спецификации языка. Для написания регулярного выражения используются буквенные и цифровые символы, а также метасимволы – символы, имеющие специальное значение в синтаксисе регулярных выражений. Например:

String regex=”java”; // шаблон строки ”java”;
String regex=”\\d{3}; // шаблон строки из трех цифровых символов;

Создание регулярных выражений в Java

Чтобы создать RegEx в Java, нужно сделать два простых шага:

  1. написать его в виде строки с учётом синтаксиса регулярных выражений;
  2. скомпилировать эту строку в регулярное выражение;

Работа с регулярными выражениями в любой Java-программе начинается с создания объекта класса Pattern. Для этого необходимо вызвать один из двух имеющихся в классе статических методов compile. Первый метод принимает один аргумент – строковый литерал регулярного выражения, а второй – плюс еще параметр, включающий режим сравнения шаблона с текстом:

public static Pattern compile (String literal)
public static Pattern compile (String literal, int flags)

Список возможных значений параметра flags определен в классе Pattern и доступен нам как статические переменные класса. Например:

Pattern pattern = Pattern.compile("java", Pattern.CASE_INSENSITIVE);//поиск совпадений с шаблоном будет производиться без учета регистра символов.

По сути, класс Pattern — это конструктор регулярных выражений. Под «капотом» метод compile вызывает закрытый конструктор класса Pattern для создания скомпилированного представления. Такой способ создания экземпляра шаблона реализован с целью создания его в виде неизменяемого объекта. При создании производится синтаксическая проверка регулярного выражения. При наличии ошибок в строке – генерируется исключение PatternSyntaxException.

Синтаксис регулярных выражений

Синтаксис регулярных выражений основан на использовании символов <([{\^-=$!|]})?*+.>, которые можно комбинировать с буквенными символами. В зависимости от роли их можно разделить на несколько групп:

1. Метасимволы для поиска совпадений границ строк или текста

Метасимвол Назначение
^ начало строки
$ конец строки
\b граница слова
\B не граница слова
\A начало ввода
\G конец предыдущего совпадения
\Z конец ввода
\z конец ввода

2. Метасимволы для поиска символьных классов

Метасимвол Назначение
\d цифровой символ
\D нецифровой символ
\s символ пробела
\S непробельный символ
\w буквенно-цифровой символ или знак подчёркивания
\W любой символ, кроме буквенного, цифрового или знака подчёркивания
. любой символ

3. Метасимволы для поиска символов редактирования текста

Метасимвол Назначение
\t символ табуляции
\n символ новой строки
\r символ возврата каретки
\f переход на новую страницу
\u 0085 символ следующей строки
\u 2028 символ разделения строк
\u 2029 символ разделения абзацев

4. Метасимволы для группировки символов

Метасимвол Назначение
[абв] любой из перечисленных (а,б, или в)
[^абв] любой, кроме перечисленных (не а,б, в)
[a-zA-Z] слияние диапазонов (латинские символы от a до z без учета регистра )
[a-d[m-p]] объединение символов (от a до d и от m до p)
[a-z&&[def]] пересечение символов (символы d,e,f)
[a-z&&[^bc]] вычитание символов (символы a, d-z)

5. Метасимволы для обозначения количества символов – квантификаторы. Квантификатор всегда следует после символа или группы символов.

Метасимвол Назначение
? один или отсутствует
* ноль или более раз
+ один или более раз
{n} n раз
{n,} n раз и более
{n,m} не менее n раз и не более m раз

Жадный режим квантификатора

Особенностью квантификаторов является возможность использования их в разных режимах: жадном, сверхжадном и ленивом. Сверхжадный режим включается добавлением символа «+» после квантификатора, а ленивый – символа «?». Например:

"А.+а" // жадный режим
"А.++а" // сверхжадный режим
"А.+?а" // ленивый режим

Попробуем на примере этого шаблона разобраться в работе квантификаторов в различных режимах. По умолчанию квантификатор работает в жадном режиме. Это означает, что он ищет максимально длинное совпадение в строке. В результате выполнения этого кода:

public static void main(String[] args) {
    String text = "Егор Алла Александр";
    Pattern pattern = Pattern.compile("А.+а");
    Matcher matcher = pattern.matcher(text);
    while (matcher.find()) {
        System.out.println(text.substring(matcher.start(), matcher.end()));
    }
}

мы получим такой вывод: Алла Алекса Алгоритм поиска по заданному шаблону «А.+а«, выполняется в следующей последовательности:

  1. В заданном шаблоне первый символ – это русский символ буквы АMatcher сопоставляет его с каждым символом текста, начиная с нулевой позиции. На нулевой позиции в нашем тексте находиться символ Е, поэтому Matcher перебирает последовательно символы в тексте, пока не встретит совпадение с шаблоном. В нашем примере это символ на позиции №5.Регулярные выражения в Java - 2
  2. После того, как найдено совпадение с первым символом шаблона, Matcher сверяет соответствие со вторым символом шаблона. В нашем случае это символ «.», который обозначает любой символ.Регулярные выражения в Java - 3На шестой позиции – символ буквы л. Разумеется, он соответствует шаблону «любой символ».
  3. Matcher переходит к проверке следующего символа из шаблона. В нашем шаблоне он задан с помощью квантификатора «.+». Поскольку количество повторений «любого символа» в шаблоне – один и более раз, Matcher берет по очереди следующий символ из строки и проверяет его на соответствие шаблону, до тех пор, пока будет выполняться условие «любой символ», в нашем примере – до конца строки (с поз. №7 -№18 текста).Регулярные выражения в Java - 4По сути, Matcher, захватывает все строку до конца – в этом как раз и проявляется его «жадность».
  4. После того как Matcher дошел до конца текста и закончил проверку для части шаблона «А.+», Matcher начинает проверку для оставшейся части шаблона – символ буквы а. Так как текст в прямом направлении закончился, проверка происходит в обратном направлении, начиная с последнего символа:Регулярные выражения в Java - 5
  5. Matcher «помнит» количество повторений в шаблоне «.+» при котором он дошел до конца текста, поэтому он уменьшает количество повторений на единицу и проверяет соответствие шаблона тексту, до тех пор пока не будет найдено совпадение:Регулярные выражения в Java - 6

Сверхжадный режим квантификатора

В сверхжадном режиме работа матчера аналогична механизму жадного режима. Отличие состоит в том, что при захватывании текста до конца строки поиск в обратном направлении не происходит. То есть первые три этапа при сверхжадном режиме будут аналогичны жадному режиму. После захвата всей строки матчер добавляет остаток шаблона и сравнивает с захваченной строкой. В нашем примере при выполнении метода main с шаблоном «А.++а» совпадений не будет найдено. Регулярные выражения в Java - 7

Ленивый режим квантификатора

  1. В этом режиме на начальном этапе, как и в жадном режиме, ищется совпадение с первым символом шаблона:Регулярные выражения в Java - 8
  2. Далее ищется совпадение со следующим символом шаблона – любым символом:Регулярные выражения в Java - 9
  3. В отличие от жадного режима, в ленивом ищется самое короткое совпадение в тексте, поэтому после нахождения совпадения со вторым символом шаблона, который задан точкой и соответствует символу на позиции №6 текста, Matcher будет проверять соответствие текста остатку шаблона – символу «а»Регулярные выражения в Java - 10
  4. Поскольку совпадение с шаблоном в тексте не найдено (на позиции №7 в тексте находится символ «л»), Matcher добавляет еще один «любой символ» в шаблоне, так как он задан как один и более раз, и опять сравнивает шаблон с текстом на позициях с №5 по №8:Регулярные выражения в Java - 11
  5. В нашем случае найдено совпадение, но конец текста ещё не достигнут. Поэтому с позиции №9 проверка начинается с поиска первого символа шаблона по аналогичному алгоритму и далее повторяется вплоть до окончания текста.Регулярные выражения в Java - 12

В результате работы метода main при использовании шаблона «А.+?а» мы получим следующий результат: Алла Алекса Как видно из нашего примера, при использовании разных режимов квантификатора для одного и того же шаблона мы получили разные результаты. Поэтому необходимо учитывать эту особенность и выбирать нужный режим в зависимости от желаемого результата при поиске.

Экранирование символов в регулярных выражениях

Поскольку регулярное выражение в Java, а точнее — его исходное представление задается с помощью строкового литерала, необходимо учитывать те правила спецификации Java, которые касаются строковых литералов. В частности, символ обратной косой черты «\» в строковых литералах в исходном коде Java интерпретируется как символ управляющей последовательности, который предупреждает компилятор, что следующий за ним символ — специальный и что его нужно особым образом интерпретировать. Например:

String s=The root directory is \nWindows”;//перенос Windows на новую строку
String s=The root directory is \u00A7Windows”;//вставка символа параграфа перед Windows

Поэтому в строковых литералах, которые описывают регулярное выражение, и используют символ «\» (например, для метасимволов) его нужно удваивать, чтобы компилятор байт-кода Java не интерпретировал его по-своему. Например:

String regex=”\\s”; // шаблон для поиска символов пробела
String regex=”\\”Windows\\””; // шаблон для поиска строки ”Windows”

Двойной символ обратной косой черты также следует использовать для экранирования символов, задействованных в качестве специальных, если мы планируем их использовать как «обычные» символы. Например:

String regex=How\\?; // шаблон для поиска строки “How?”

Методы класса Pattern

В классе Pattern есть и другие методы для работы с регулярными выражениями: String pattern() – возвращает исходное строковое представление регулярного выражения, из которого был создан объект Pattern:

Pattern pattern = Pattern.compile("abc");
System.out.println(Pattern.pattern())//"abc"

static boolean matches(String regex, CharSequence input) – позволяет проверить регулярное выражение, переданное в параметре regex на соответствие тексту, переданному в параметре input. Возвращает: true – если текст соответствует шаблону; false – в противном случае; Пример:

System.out.println(Pattern.matches("А.+а","Алла"));//true
System.out.println(Pattern.matches("А.+а","Егор Алла Александр"));//false

int flags() – возвращает значения параметра flags шаблона, которые были установлены при его создании, или 0, если этот параметр не был установлен. Пример:

Pattern pattern = Pattern.compile("abc");
System.out.println(pattern.flags());// 0
Pattern pattern = Pattern.compile("abc",Pattern.CASE_INSENSITIVE);
System.out.println(pattern.flags());// 2

String[] split(CharSequence text, int limit) – разбивает текст, переданный в качестве параметра на массив элементов String. Параметр limit определяет предельное количество совпадений, которое ищется в тексте:

  • при limit>0 – выполняется поиск limit-1 совпадений;
  • при limit<0 – выполняется поиск всех совпадений в тексте
  • при limit=0 – выполняется поиск всех совпадений в тексте, при этом пустые строки в конце массива отбрасываются;

Пример:

public static void main(String[] args) {
    String text = "Егор Алла Анна";
    Pattern pattern = Pattern.compile("\\s");
    String[] strings = pattern.split(text,2);
    for (String s : strings) {
        System.out.println(s);
    }
    System.out.println("---------");
    String[] strings1 = pattern.split(text);
    for (String s : strings1) {
        System.out.println(s);
    }
}

Вывод на консоль: Егор Алла Анна ——— Егор Алла Анна Еще один метод класса для создания объекта Matcher рассмотрим ниже.

Методы класса Matcher

Matcher представляет собой класс, из которого создается объект для поиска совпадений по шаблону. Matcher – это «поисковик», «движок» регулярных выражений. Для поиска ему надо дать две вещи: шаблон поиска и «адрес», по которому искать. Для создания объекта Matcher предусмотрен следующий метод в классе Pattern: рublic Matcher matcher(CharSequence input) В качестве аргумента метод принимает последовательность символов, в котором будет производиться поиск. Это объекты классов, реализующих интерфейс CharSequence. в качестве аргумента можно передать не только String, но и StringBuffer, StringBuilder, Segment и CharBuffer. Шаблоном для поиска является объект класса Pattern, на котором вызывается метод matcher. Пример создания матчера:

Pattern p = Pattern.compile("a*b");// скомпилировали регулярное выражение в представление
Matcher m = p.matcher("aaaaab");//создали поисковик в тексте “aaaaab” по шаблону "a*b"

Теперь с помощью нашего «поисковика» мы можем искать совпадения, узнавать позицию совпадения в тексте, заменять текст с помощью методов класса. Метод boolean find() ищет очередное совпадение в тексте с шаблоном. С помощью этого метода и оператора цикла можно производить анализ всего текста по событийной модели (осуществлять необходимые операции при наступлении события – нахождении совпадения в тексте). Например, с помощью методов этого класса int start() и int end() можно определять позиции совпадения в тексте, а с помощью методов String replaceFirst(String replacement) и String replaceAll(String replacement) заменять в тексте совпадения на другой текст replacement. Пример:

public static void main(String[] args) {
    String text = "Егор Алла Анна";
    Pattern pattern = Pattern.compile("А.+?а");

    Matcher matcher = pattern.matcher(text);
    while (matcher.find()) {
        int start=matcher.start();
        int end=matcher.end();
        System.out.println("Найдено совпадение " + text.substring(start,end) + " с "+ start + " по " + (end-1) + " позицию");
    }
    System.out.println(matcher.replaceFirst("Ира"));
    System.out.println(matcher.replaceAll("Ольга"));
    System.out.println(text);
}

Вывод программы: Найдено совпадение Алла с 5 по 8 позицию Найдено совпадение Анна с 10 по 13 позицию Егор Ира Анна Егор Ольга Ольга Егор Алла Анна Из примера видно, что методы replaceFirst и replaceAll создают новый объект String – строку, представляющую собой исходный текст, в котором совпадения с шаблоном заменены на текст, переданный методу в качестве аргумента. Причём метод replaceFirst заменяет только первое совпадение, а replaceAll – все совпадения в тесте. Исходный текст остается без изменений. Использование других методов класса Matcher, а также примеры регулярных выражений можно посмотреть в этом цикле статей. Наиболее частые операции с регулярными выражениями при работе с текстом из классов Pattern и Matcher встроены в класс String. Это такие методы как split, matches, replaceFirst, replaceAll. Но на самом деле «под капотом» они используют классы Pattern и Matcher. Поэтому, если вам нужно заменить текст или сравнить строки в программе без написания лишнего кода, используйте методы класса String. Если же вам нужны расширенные возможности – вспомните о классах Pattern и Matcher.

 

Origin : https://javarush.ru/groups/posts/regulyarnye-vyrazheniya-v-java

Шпаргалка по Gradle, Android

Как мне кажется, большинство людей начинают разбираться с gradle только тогда, когда в проекте что-то надо добавить или что-то внезапно ломается — и после решения проблемы «нажитый непосильным трудом» опыт благополучно забывается. Причём многие примеры в интернете похожи на ускоспециализированные заклинания, не добавляющие понимания происходящего:

 

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.habr.hello"
        minSdkVersion 20
        targetSdkVersion 28
    }
    buildTypes {
        release {
            minifyEnabled false
        }
    }
}

 

Я не собираюсь подробно описывать, для чего нужна каждая строчка выше — это частные детали реализации андроид-плагина. Есть кое-что более ценное — понимание того, как всё организовано. Информация раскидана по различным сайтам/официальной документации/исходникам градла и плагинов к нему — в общем, это чуть более универсальное знание, которое не хочется забывать.

 

Дальнейший текст можно рассматривать как шпаргалку для тех, кто только осваивает gradle или уже забыл.

Полезные ссылки

 

 

Консоль

 

Android studio/IDEA старательно прячет команды gradle от разработчика, а ещё при изменении build.gradle файликов начинает тупить или перезагружать проект.

 

В таких случаях вызывать gradle из консоли оказывается намного проще и быстрее. Враппер для gradle обычно идёт вместе с проектом и прекрасно работает в linux/macos/windows, разве что в последнем надо вызывать bat-файлик вместо враппера.

 

Вызов задач

 

./gradlew tasks

 

пишет доступные задачи.

 

./gradlew subprojectName:tasks --all

 

Можно вывести задачи отдельного подпроекта, а ещё с опцией --all будут выведены все задачи, включая второстепенные.

 

Можно вызвать любую задачу, при этом будут вызваны все задачи, от которых она зависит.

 

./gradlew app:assembleDevelopDebug

 

Если лень писать название целиком, можно выкинуть маленькие буковки:

 

./gradlew app:assembleDD

 

Если градл не сможет однозначно угадать, какую именно задачу имели ввиду, то выведет список подходящих вариантов.

 

Логгинг

 

Количество выводимой в консоль информации при запуске задачи сильно зависит от уровня логгинга.
Кроме дефолтного есть -q, -w, -i, -d, ну или --quiet, --warn, --info, --debug по возрастанию количества информации. На сложных проектах вывод с -d может занимать больше мегабайта, а поэтому его лучше сразу сохранять в файл и там уже смотреть поиском по ключевым словам:

 

./gradlew app:build -d > myLog.txt

 

Если где-то кидается исключение, для stacktrace опция -s.

 

Можно и самому писать в лог:

 

logger.warn('A warning log message.')

 

логгер является имплементацией SLF4J.

 

Groovy

 

Происходящее в build.gradle файликах — просто код на groovy.

 

Groovy как язык программирования почему-то не очень популярен, хотя, как мне кажется, он сам по себе достоин хотя бы небольшого изучения. Язык появился на свет ещё в 2003 году и потихоньку развивался. Интересные особенности:

 

  • Практически любой java код является валидным кодом на groovy. Это очень помогает интуитивно писать работающий код.
  • Одновременно вместе со статической, в груви поддерживается динамическая типизация, вместо String a = "a" можно смело писать def a = "a" или даже def map = ['one':1, 'two':2, 'list' = [1,false]]
  • Есть замыкания, для которых можно динамически определить контекст исполнения. Те самые блоки android {...} принимают замыкания и потом исполняют их для какого-то объекта.
  • Есть интерполяция строк "$a, ${b}", multiline-строки """yep, ${c}""", а обычные java-строки обрамляются одинарными кавычками: 'text'
  • Есть подобие extension-методов. В стандартной коллекции языка уже есть методы типа any, every, each, findAll. Лично мне названия методов кажутся непривычными, но главное что они есть.
  • Вкусный синтаксический сахар, код становится намного короче и проще. Можно не писать скобки вокруг аргументов функции, для объявления списков и хеш-табличек приятный синтаксис: [a,b,c], [key1: value1, key2: value2]

 

В общем, почему языки типа Python/Javascript взлетели, а Groovy нет — для меня загадка. Для своего времени, когда в java даже лямбд не было, а альтернативы типа kotlin/scala только-только появлялись или ещё не существовали, Groovy должен был выглядеть реально интересным языком.

 

Именно гибкость синтаксиса groovy и динамическая типизация позволила в gradle создавать лаконичные DSL.

 

Сейчас в официальной документации Gradle примеры продублированы на Kotlin, и вроде как планируется переходить на него, но код уже не выглядит таким простым и становится больше похожим на обычный код:

 

task hello {
    doLast {
        println "hello"
    }
}

 

vs

 

tasks.register("hello") {
    doLast {
        println("hello")
    }
}

 

Впрочем, переименование в Kradle пока не планируется.

 

Стадии сборки

 

Их делят на инициализацию, конфигурацию и выполнение.

 

Идея состоит в том, что gradle собирает ациклический граф зависимостей и вызывает только необходимый минимум их них. Если я правильно понял, стадия инициализации происходит в тот момент, когда исполняется код из build.gradle.

 

Например, такой:

 

copy {
   from source
   to dest
}

 

Или такой:

 

task epicFail {
   copy{
      from source
      to dest
   }
}

 

Возможно, это неочевидно, но вышеуказанное будет тормозить инициализацию. Чтобы не заниматься копированием файлов при каждой инициализации, нужно в задаче использоваль блок doLast{...} или doFirst{...} — тогда код завернётся в замыкание и его позовут в момент выполнения задачи.

 

task properCopy {
    doLast {
        copy {
            from dest
            to source
        }
    }
}

 

или так

 

task properCopy(type: Copy) {
    from dest
    to source
}

 

В старых примерах вместо doLast можно встретить оператор <<, но от него потом отказались из-за неочевидности поведения.

 

task properCopy << {
    println("files copied")
}

 

tasks.all

 

Что забавно, с помощью doLast и doFirst можно навешивать какие-то действия на любые задачи:

 

tasks.all {
    doFirst {
        println("task $name started")
    }
}

 

IDE подсказывает, что у tasks есть метод whenTaskAdded(Closure ...), но метод all(Closure ...) работает намного интереснее — замыкание вызывается для всех существующих задач, а так же на новых задачах при их добавлении.

 

Создадим задачу, которая распечатает зависимости всех задач:

 

task printDependencies {
    doLast {
        tasks.all {
            println("$name dependsOn $dependsOn")
        }
    }
}

 

или так:

 

task printDependencies {
    doLast {
        tasks.all { Task task ->
            println("${task.name} dependsOn ${task.dependsOn}")
        }
    }
}

 

Если tasks.all{} вызвать во время выполнения (в блоке doLast), то мы увидим все задачи и зависимости.
Если сделать то же самое без doLast (т.е., во время инициализации), то у распечатанных задач может не хватать зависимостей, так как они ещё не были добавлены.

 

Ах да, зависимости! Если другая задача должна зависеть от результатов выполнения нашей, то стоит добавить зависимость:

 

anotherTask.dependsOn properCopy

 

Или даже так:

 

tasks.all{  task ->
   if (task.name.toLowerCase().contains("debug")) {
       task.dependsOn properCopy
   }
}

 

inputs, outputs и инкрементальная сборка

 

Обычная задача будет вызываться каждый раз. Если указать, что задача на основе файла А генерирует файл Б, то gradle будет пропускать задачу, если эти файлы не изменились. Причём gradle проверяет не дату изменения файла, а именно его содержимое.

 

task generateCode(type: Exec) {
    commandLine "generateCode.sh", "input.txt", "output.java"
    inputs.file "input.txt"
    output.file "output.java"
}

 

Аналогично можно указать папки, а так же какие-то значения: inputs.property(name, value).

 

task description

 

При вызове ./gradlew tasks --all стандартные задачи имеют красивое описание и как-то сгруппированы. Для своих задач это добавляется очень просто:

 

task hello {
    group "MyCustomGroup"
    description "Prints 'hello'"
    doLast{
        print 'hello'
    }
}

 

task.enabled

 

можно «выключить» задачу — тогда её зависимости будут всё равно вызваны, а она сама — нет.

 

taskName.enabled false

 

несколько проектов (модулей)

 

multi-project builds в документации

 

В основном проекте можно расположить ещё несколько модулей. Например, такое используется в андроид проектах — в рутовом проекте почти ничего нет, в подпроекте включается android плагин. Если захочется добавить новый модуль — можно добавить ещё один, и там, например, тоже подключить android плагин, но использовать другие настройки для него.

 

Ещё пример: при публикации проекта с помощью jitpack в рутовом проекте описывается, с какими настройками публиковать дочерний модуль, который про факт публикации может даже не подозревать.

 

Дочерние модули указываются в settings.gradle:

 

include 'name'

 

Подробнее про зависимости между проектами можно почитать здесь

 

buildSrc

 

Если кода в build.gradle много или он дублируется, его можно вынести в отдельный модуль. Нужна папка с магическим именем buildSrc, в которой можно расположить код на groovy или java. (ну, вернее, в buildSrc/src/main/java/com/smth/ код, тесты можно добавить в buildSrc/src/test). Если хочется что-то ещё, например, написать свою задачу на scala или использовать какие-то зависимости, то прямо в buildSrc надо создать build.gradle и в нём указать нужные зависимости/включить плагины.

 

К сожалению, с проектом в buildSrc IDE может тупить c подсказками, там придётся писать импорты и классы/задачи оттуда в обычный build.gradle тоже придётся импортировать. Написать import com.smth.Taskname — не сложно, просто надо это помнить и не ломать голову, почему задача из buildSrc не найдена).

 

По этой причине удобно сначала написать что-то работающее прямо в build.gradle, и только потом переносить код в buildSrc.

 

Свой тип задачи

 

Задача наследуется от DefaultTask, в которой есть много-много полей, методов и прочего. Код AbstractTask, от которой унаследована DefaultTask.

 

Полезные моменты:

 

  • вместо ручного добавления inputs и outputs можно использовать поля и аннотации к ним: @Input, @OutputFile и т.п.
  • метод, который будут запускать при выполнении задачи: @TaskAction.
  • удобные методы типа copy{from ... , into... } всё ещё можно вызвать, но придётся их явно вызывать для проекта: project.copy{...}

 

Когда для нашей задачи кто-то в build.gradle пишет

 

taskName {
    ... //some code
}

 

у задачи вызывается метод configure(Closure).

 

Я не уверен, что это правильных подход, но если у задачи есть несколько полей, взаимное состояние которых сложно контролировать геттерами-сеттерами, то кажется вполне удобным переопределить метод следующим образом:

 

override def configure(Closure closure){
    def result = super().configure(closure)
    // здесь проверить состояние полей/установить что-нибудь
    return result;
}

 

Причём даже если написать

 

taskName.fieldName value

 

то метод configure всё равно будет вызван.

 

Свой плагин

 

Подобно задаче, можно написать свой плагин, который будет что-то настраивать или создавать задачи. Например, происходящее в android{...} — полностью заслуга тёмной магии андроид плагина, который вдобавок создаёт целую кучу задач типа app:assembleDevelopDebug на все возможные сочетания flavor/build type/dimenstion. Ничего сложного в написании своего плагина нет, для лучшего понимания можно посмотреть код других плагинов.

 

Есть ещё третья ступенька — можно код расположить не в buildSrc, а сделать его отдельным проектом. Потом с помощью https://jitpack.io или ещё чего-то опубликовать плагин и подключать его аналогично остальным.

 

The end

 

В примерах выше могут быть опечатки и неточности. Пишите в личку или отмечайте с ctrl+enter — исправлю. Конкретные примеры лучше брать из документации, а на эту статью смотреть как на списочек того «как можно делать».

Original : https://habr.com/ru/post/458046/

Как удаленно отлаживать приложение, запущенное на Tomcat, из Intellij IDEA

This post would look into how to tackle and debug issues in scenarios where they only occur in production (or other remote environment) but not in development environment. As anybody who has been in this kind of situation would acknowledge, trying to pinpoint the cause of these kind of “issues” might quickly end up being a practice at taking shots in the dark: a very time-consuming and inefficient process.

It was this kind of situation I recently found myself, where, I had to rectify certain issues that were occurring in the production environment but could not be reproduced on the development machine.

Fortunately enough, the said issues could be reproduced in the testing environments (which is as close to the production environment as possible). But having the issues reproducible in the test environment was good In that it confirms the issues needed to be fixed, but it was of little help in actually tracking the issues down, finding the cause and fixing it. Relying just on log outputs was not enough…What if I could debug the test environment from my machine?


It was at this stage that I thought about remote debugging and if there exist the possibility of having an application run in a remote JVM and still be able to intercept its execution from the copy of the source code running on a local IDE: sounds just like what would get the job done.

And sure this is very possible. I looked into what is needed; the set up and all. It did not sound complicated. So, together with the help of a colleague, Thijs Schnitger, was able to get it up and running without much hassles.

This post thus describes the procedure of setting up the ability to remotely debug a JVM application from within an IDE. The post outlines the procedure using IntelliJ IDEA as the IDE, and the remote application to be debugged would be web application running on Tomcat. The steps outlined below should apply with any remote JVM application and any IDE, although the exact steps may differ, the general idea would remain same. The post also gives a brief overview of the technologies that makes remote debugging possible.

The Configuration Instructions

The process of getting remote debugging working involves two steps.

  1. Starting Tomcat with remote debugging enabled and
  2. having your IDE, in our case IntelliJ IDEA, to be able to debug the remote tomcat application.

There are couple of ways to get the first part done and it slightly differs depending on which OS environment your Tomcat instance is running on. But, regardless of the method used, the main idea behind the configuration remains the same; which is: pass specific start up options to the JVM that would enable remote debugging.

The JVM start up arguments needed to have remote debugging activated can be set via JPDA_OPTSCATALINA_OPTS and JAVA_OPTS although using JAVA_OPTS is not usually advised, reason being that the setting specified via JAVA_OPTS, is exposed to all JVM applications, but with CATALINA_OPTS the settings defined is limited only to Tomcat.

USING JPDA_OPTS

Using the JPDA_OPTS, option you would have the needed start-up argument set in a file named setenv.sh (or setenv.bat if on windows). Create the file if it does not exist already. Have it in the CATALINA_HOME/bin directory. And add this to the content:

1 export JPDA_OPTS=&quot;-agentlib:jdwp=transport=dt_socket, address=1043, server=y, suspend=n&quot;

and if on Windows:

1 set JPDA_OPTS=&quot;-agentlib:jdwp=transport=dt_socket, address=1043, server=y, suspend=n&quot;

What these settings basically do is to enable remote debugging and configure available options: specifying the communication protocol between the running application and the debugger, (ie transport=dt_socket) the port at which the remote application should debugged (ie address=1403). The server=y setting indicates that this JVM would be the one to be debugged while suspend=n is used to tell the JVM to execute right away and not wait for an attached debugger. If set to “y” then the application would be suspended and not run until a debugger is attached.

A good situation where you would want to have suspend=y is when debugging an issue that prevents an application from starting successfully, having suspend=y would make sure that the JVM waits for the remote debugger to connect before attempting to start and run the application.

Although the settings can be put directly inside catalina.sh (or catalina.bat) it is always preferable to have extra configurations in the setenv.* file. It would be automatically be picked up by Tomcat.

Note that another options you may encounter that may be used to enable remote debugging is:

1 -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=1043, suspend=n

The difference between this and the recommended setting is that the -Xdebug and -Xrunjdwp option is the old way of enabling remote debugging; it applies to JVM prior to JAVA 5.0 (JAVA 1.5.0) while -agentlib:jdwp option applies to JAVA 5.0 and beyond…And with this configuration in place, you can then start Tomcat via the command line arguments:

1 $CATALINA_HOME/bin/catalina.sh jpda start

USING JAVA_OPTS OR CATALINA_OPTS

If you have Tomcat running as a windows service, then configuring Tomcat to start up with ability to be debugged remotely is done by simply specifying the start up arguments in the run properties.

Open up the Apache Tomcat properties dialog box, and under the Java tab add the required start up option:

1 -agentlib:jdwp=transport=dt_socket,address=1043,server=y,suspend=n

Make sure that each entry is on a new line and there are no spaces between the options

With this added to the options, starting the Tomcat service would have remote debugging enabled.

If per chance you are not running Tomcat on Windows as a service, to enable remote debugging, you need to specify the required options in the setenv.bat file:

1 set CATALINA_OPTS=&quot;-agentlib:jdwp=transport=dt_socket,address=1043,server=y,suspend=n&quot;

If you are running on linux you have:

1 export CATALINA_OPTS=&quot;-agentlib:jdwp=transport=dt_socket,address=1043,server=y,suspend=n&quot;

Start Tomcat like you would normally then do by running the catalina.bat or catalina.sh script.

STARTING TOMCAT WITH JPDA

Another way of running Tomcat with remote debugging is to use the JPDA switch, to start Tomcat; this would automactically have remote debugging enabled with its options set to default values.

For example, this:

1 catalina jpda start

would start Tomcat with the following settings:

1 -agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n

But what if you want to change any of these default settings and still use the JPDA start switch? This can be done by setting the appropiate environment variable for the options as required by Tomcat. The environment variables are:

JPDA_TRANSPORT: to specify JPDA transport used
JPDA_ADDRESS: to specify port for remote debugging
JPDA_SUSPEND: to specify if to suspend JVM after startup

so running:

1 &amp;amp;amp;amp;amp;amp;nbsp;export&amp;amp;amp;amp;amp;amp;nbsp;JPDA_ADDRESS=&quot;8080&quot;

and then

1 catalina jpda start

would have remote debugging possible on port 8080.

Configuring IntelliJ IDEA

With the remote JVM running the Tomcat started with the required start up arguments, the next thing to do is to configure the debugger in IntelliJ IDEA.

There are two ways to this configuration: The Remote Tomcat settings options or Remote settings option.

Using The Remote Tomcat Settings Option.

Click on Run ➝ Edit Configuration:

 

intellij-remote-debug

 

 

The Edit Configuration… settings dialog box, pops up.

Click on the + button on the top left ➝ Tomcat Server ➝ Remote. As shown below:

 

intellij-remote-debug-2

 

Next is to fill in the required settings.

On the Server tab, specify the host details and the port the remote Tomcat is running on:

 

intellij-remote-debug-3

 

Then switch to the Startup/Connection tab, where you would specify the details of the remote JVM needed to be debugged from intellij IDEA.

Click on Debug and specify the port you specified while configuring the Tomcat; 1043 in our case.

 

intellij-remote-debug-4

 

And that is it. Click on Ok to save your changes, then start the debugging session by pressing the debug icon or by using the keyboard shortcut (shift + f9)

If all the configuration is set up correctly, you should see a notice that the IDE has successfully connected to the target VM in the Debug tab in the bottom console:

 

intellij-remote-debug-5

 

Using the Remote settings option

Open the Edit Configuration settings as done in the previous option but instead of selecting the Remote Tomcat, select the Remote option:

 

intellij-remote-debug-6

 

The Remote settings dialog box appears where you can specify the required configuration; remote host, port, project etc:

 

intellij-remote-debug-7

 

As you can see from above, the interesting thing about the Remote option is that, apart from allowing you to configure intelliJ IDEA, it also list the various configurations required to start up the remote JVM in other for remote debugging to work. Might come in handy.

Specify the required settings, click Ok to save changes, and start the debugging session. You should also see the notice that IntelliJ has successfully connected to the remote VM.

Once this is done, you should then open the source code of the application you have running on the remote Tomcat, put a breakpoint where required and you can go ahead and start debugging as if the application is running on your local machine.

So that is it with the configuration, the next question is, what makes all these fit and work together?

Let us take a quick work through of the technology at play that makes remote debugging JVM possible.

How Remote JVM Debugging Works

It all starts with what is referred to as Agents.

The JVM, which runs the complied .class sources has a feature that allows externally libraries (written in either Java or C++) to be injected into the JVM, just about runtime. These external libraries are referred to as Agents and they have the ability to modify the content of the .class files been run. These Agents have access to functionality of the JVM that is not accessible from within a regular Java code running inside the JVM and they can be used to do interesting stuff like injecting and modify the running source code, profiling etc. Tools like JRebel makes use of this piece of functionality to achieve their magic.

And to pass an Agent Lib to a JVM, you do so via start up arguments, using the -agentlib:libname[=options] format.

So in the configuration we had above:

1 -agentlib:jdwp=transport=dt_socket,address=1043,server=y,suspend=n

We were actually passing an Agent Lib named jdwp to the JVM running Tomcat. The jdwp is a JVM specific, optional implementation of the JDWP (Java Debug Wire Protocol) that is used for defining communication between a debugger and a running JVM. It’s implementation, if present is supplied as a native library of the JVM as either jdwp.so or jdwp.dll

So what does it do?

In simple terms, the jdwp agent we pass is basically serving the function of being a link between the JVM instance running an application and a Debugger (which can be located either remote or local). Since it is an Agent Library, It does have the ability to intercept the running code, create a bridge between the JVM and a debugger, and have the functionality of a debugger applied on the JVM.

Since in the JVM architecture, the debugging functionality is not found within the JVM itself but is abstracted away into external tools (that are aptly referred to as debuggers), these tools can either reside on the local machine running the JVM being debugged or be run from am external machine. It is this de-coupled, modular architecture that allows us to have a JVM running on a remote machine and using the JDWP, have a remote debugger be able to communicate with it.

When IntelliJ IDEA was configured above, what was actually been done is to specify to the debugger tool within IntelliJ IDEA the host where the running JVM it needs to debug resides and the port through which it should connect with.

All the specification that outlines this modular architecture is contained in what is referred to as the Java Platform, Debugger Architecture, JPDA (this explains the JPDA in the JPDA_OPTS method used above) and you can read a much detailed overview of it here: Java Platform Debugger Architecture Overview.

Диагностика утечек памяти в Java, PermGen

В данной заметке я хочу показать каким образом можно определять и устранять утечки памяти в Java на примере из моей повседневной работы. Мы не будем здесь рассматривать возможные причины появления утечек, об этом будет отдельная статья, так как тема достаточно обширная. Стоит заметить, что речь пойдет о диагностике именно Heap Memory, об утечках в других областях памяти будет отдельная статья.

Инструменты

Для успешной диагностики нам понадобятся два инструмента: Java Mission Control (jmc) и Eclipse Memory Analyzer. Вобщем-то можно обойтись только Memory Analyzer, но с JMC картина будет более полной.

  • JMC входит в состав JDK (начиная с 1.7)
  • Memory Analyzer может быть загружен отсюда: MAT

 

Анализ использования памяти

Прежде всего, нужно запустить приложение со следующими флагами JVM:
-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder

Не используйте эти опции на production системе без приобретения специальной лицензии Oracle!

Эти опции позволят запустить Flight Recorder – утилита, которая поможет собрать информацию об использовании памяти (и много другой важной информации) во время выполнения программы. Я не буду описывать здесь как запустить Flight Recorder, эта информация легко гуглится. В моем случае было достаточно запустить FR на 10-11 минут.

Рассмотрим следующий рисунок, на котором показана классическая «пила» памяти, а так же важный сигнал, что что-то не так с использованием памяти:

Запись Fight recorder

Можно увидеть, что после каждого цикла очистки памяти, heap все больше заполняется, я выделил это желтым треугольником. «Пила» все время как бы ползет вверх. Это значит, что какие-то объекты не достижимы для очистки и накапливаются в old space, что со временем приведет к переполнению этой области памяти.

Выявление утечки

Следующим шагом нужно выявить, что именно не доступно для очистки и в этом нам поможет Memory Analyzer. Прежде всего, нужно загрузить в программу heap dump работающего приложения с предполагаемой утечкой памяти. Это можно сделать с помощью «File → Acquire Heap Dump». После загрузки в диалоге «Getting Started Wizard» выбрать «Leak Suspects Report» после этого откроется краткий обзор возможных утечек памяти:

Leak suspects report

Если вернуться на вкладку «Overview» и выбрать «Dominator Tree», то можно увидеть более подробную картину:

Overview

Denominator tree

Дерево показывает структуру «тяжелого» объекта, а так же размер его полей (по типу). Можно видеть, что одно из полей объекта MasterTenant занимает более 45% памяти.

Устранение утечки

Имея результат анализа из предыдущего пункта, следующим шагом идет устранение накапливания объектом памяти. Тут все сильно зависит от конкретного кода. Общая рекоменация – нужно найти и проанализировать все места, где происходит инициализация или изменение соответствующего поля или полей, чтобы понять механизм накапливания памяти. В моем случае в коллекцию постоянно добавлялись записи из множества (около 150) потоков при определенных условиях.

После находжения и устранения утечки, не лишним будет пройти все шаги снова, проанализировать память и отчет Memory Analyzer, чтобы убедиться что фикс помог.

 

PermGen памяти в Java

О чем речь?

Кто занимался веб-разработкой на Java, наверняка сталкивался с такой проблемой как java.lang.OutOfMemoryError: PermGen space. Возникает она, как правило, после перезапуска веб-приложения внутри сервера без перезапуска самого сервера. Перезапуск веб-приложения без перезапуска сервера может понадобиться в процессе разработки, чтобы не ждать лишнее время запуска самого сервера. Если у вас задеплоено несколько веб-приложений, перезапуск всего сервера может быть гораздо дольше перезапуска одного веб-приложения. Или же весь сервер просто нельзя перезапускать, так как другие веб-приложения используются. Первое решение, которое приходит на ум – увеличить максимальный объем PermGen памяти, доступный JVM (сделать это можно опцией -XX:MaxPermSize), но это лишь отсрочит падение, после нескольких перезапусков вы снова получите OutOfMemoryError. Хорошо было бы иметь возможность сколько угодно раз перезапускать и передеплоивать веб-приложение на работающем сервере. О том, как побороть PermGen, и пойдет дальнейший разговор.

Что такое PermGen?

PermGen – Permanent Generation – область памяти в JVM, предназначенная для хранения описания классов Java и некоторых дополнительных данных. Таким образом, при рестарте веб-приложения все классы загружаются по новой и заполняют PermGen память. Веб-приложение может содержать кучу библиотек, и описания классов могут занимать десятки мегабайт. Кто следит за нововведениями в Java, может быть слышал о том, что в Java 8 отказались от PermGen. Тут можно подумать, что вечную проблему, наконец, исправили, и больше не будет падений от недостатка PermGen памяти. К сожалению это не так, грубо говоря, PermGen теперь просто называется Metaspace, и вы все равно получите OutOfMemoryError.

Стоп. А как же сборщик мусора?

Всем нам известно, что в Java есть сборщик мусора, который собирает все неиспользуемые объекты. Неиспользуемые классы в PermGen он тоже должен собирать, но только если он правильно настроен, и отсутствуют утечки памяти.

Что касается настройки – официальной документации довольно мало, в интернетах есть множество советов использовать различные опции, например -XX:+CMSClassUnloadingEnabled-XX:+CMSPermGenSweepingEnabled-XX:+UseConcMarkSweepGC. Я не стал глубоко копать и искать официальную документацию, а методом проб и ошибок определил, что для Java 7 и Tomcat 7 необходимо и достаточно добавить JVM опцию -XX:+UseConcMarkSweepGC. Эта опция изменит алгоритм сборки мусора, если вы не уверены, что ваше приложение не станет хуже работать из-за этого, то поищите документацию и сравнения работы разных алгоритмов сборки мусора, чтобы определить, стоит использовать эту опцию или нет. Возможно, вам будет достаточно включить эту опцию, чтобы избавиться от проблем с PermGen. Если нет – то у вас, скорее всего, утечка памяти, что с этим делать – читаем дальше.

Почему происходит утечка PermGen памяти?

Для начала пара слов о class loader-ах. Class loader-ы – это объекты в Java, ответственные за загрузку классов. В веб-серверах существует иерархия class loader-ов, на каждое веб-приложение существует по одному class loader-у, плюс несколько общих class loader-ов. Классы внутри веб-приложения загружаются class loader-ом, который соответствует этому веб-приложению. Системные классы и классы, необходимые самому серверу, загружаются общими class loader-ами. Например, как устроена иерархия class loader-ов для Tomcat-а, можно почитать тут.

Чтобы сборщик мусора смог собрать все классы веб-приложения, на них не должно быть ссылок вне этого веб-приложения. Теперь вспомним, что каждый объект в Java хранит ссылку на свой класс, т.е. на объект класса java.lang.Class, а каждый класс хранит ссылку на class loader, который загрузил этот класс, а каждый class loader хранит ссылки на все классы, которые он загрузил. Получается, что всего одна ссылка извне на объект веб-приложения тянет за собой все классы веб-приложения и невозможность собрать их сборщиком мусора.

Еще одной причиной утечки может быть поток, который был запущен из веб-приложения, и который не удалось остановить при остановке веб-приложения. Он также хранит ссылку на class loader веб-приложения.

Также популярным вариантом утечки является ThreadLocal переменная, которой присвоен объект из веб-приложения для потока из общего пула. В этом случае поток хранит ссылку на объект. Поток из общего пула не может быть уничтожен, значит объект не может быть уничтожен, значит и весь class loader со всеми классами не может быть уничтожен.

Стандартные средства Tomcat-а

К счастью в Tomcat-е существует целый ряд средств для анализа и предотвращения утечек PermGen памяти.

Во-первых, в стандартном Tomcat Manager Application есть кнопка «Find leaks» (подробности), которая проанализирует какие веб-приложения оставили после себя мусор после перезапуска.

Но это лишь покажет, какие веб-приложения возможно содержат утечку, толку от этого мало.

Во-вторых, в Tomcat-е есть JreMemoryLeakPreventionListener — решение для общеизвестных возможных вариантов утечек памяти, конфигурируется в server.xml (подробности). Возможно, включение каких-либо опций этого listener-а поможет избавиться от утечек памяти.

И в-третьих самое главное – при остановке веб-приложения Tomcat пишет в лог что именно могло привести к утечке памяти. Например, так:

SEVERE: The web application [/drp] appears to have started a thread named [AWT-Windows] but has failed to stop it. This is very likely to create a memory leak.
SEVERE: The web application [/drp] created a ThreadLocal with key of type [org.apache.log4j.helpers.ThreadLocalMap] (value [org.apache.log4j.helpers.ThreadLocalMap@7dc1e95f]) and a value of type [java.util.Hashtable] (value [{session=*2CBFB7}]) but failed to remove it when the web application was stopped. Threads are going to be renewed over time to try and avoid a probable memory leak.

Вот это как раз то, что нам нужно, чтобы продолжить анализ утечек.

И раз уж мы всерьез взялись за дело, надо знать, как правильно проверять очищается у нас PermGen или нет. В этом нам опять же поможет Tomcat Manager Application, который умеет показывать использование памяти, в том числе PermGen.

Еще одна особенность – очистка происходит только после достижения маскимального объема PermGen памяти, так что нужно выставить небольшое значение максимальной доступной PermGen памяти (например, так: -XX:MaxPermSize=100M), чтобы после двух-трех рестартов веб-приложения занятая память достигала 100%, и либо происходила очистка, либо падал OutOfMemoryError если утечки еще остались.

Теперь рассмотрим, как избавиться от утечек на примерах

Возьмем следующее сообщение:

SEVERE: The web application [/drp] appears to have started a thread named [AWT-Windows] but has failed to stop it. This is very likely to create a memory leak.

Оно говорит нам о том, что веб-приложение запустило и не остановило поток AWT-Windows, следовательно, у него contextClassLoader оказался class loader-ом веб-приложения, и сборщик мусора не может его собрать. Тут мы можем отследить с помощью breakpoint-а с условием по имени потока, кто создал этот поток, и, покопавшись в исходниках, найти, какие есть возможности его остановить, например, проставить какой-то флаг или вызвать какой-то метод, например Thread#interrupt(). Эти действия надо будет выполнить при остановке веб-приложения.

Но еще можно заметить, что название потока похоже на что-то системное… Может JreMemoryLeakPreventionListener, про который мы узнали выше, что-то может сделать с этим потоком? Идем в документацию и видим, что действительно у listener-а есть параметр AWTThreadProtection, который почему-то false по умолчанию. Проставляем его в true в server.xml и убеждаемся, что больше такого сообщения Tomcat не выдает.

В данном случае поток AWT-Windows создавался из-за генерации капчи на сервере с использованием классов работы с изображениями из JDK.

Ок, тут мы отделались простой опцией в Tomcat-е, попробуем что-нибудь посложнее:

SEVERE: The web application [/drp] created a ThreadLocal with key of type [org.apache.log4j.helpers.ThreadLocalMap] (value [org.apache.log4j.helpers.ThreadLocalMap@7dc1e95f]) and a value of type [java.util.Hashtable] (value [{session=*2CBFB7}]) but failed to remove it when the web application was stopped. Threads are going to be renewed over time to try and avoid a probable memory leak.

Тут мы видим, что кто-то положил в ThreadLocal переменную класса ThreadLocalMap некоторое значение и не убрал его. Ищем, где используется класс ThreadLocalMap, находим org.apache.log4j.MDC, а этот класс уже непосредственно используется в нашем веб-приложении для логгирования дополнительной информации. Видим, что вызывается метод put класса MDC, а метод remove не вызывается. Похоже, что вызов remove для каждого put в правильном месте должен помочь. Исправляем, проверяем – работает!

После исправления всех таких ошибок велика вероятность, что вы избавитесь от OutOfMemoryError: PermGen space, по крайней мере, на моей практике это было так.

Анализ с помощью VisualVM

Если вы не используете Tomcat, или если исправление ошибок указанных Tomcat-ом в логе не помогло, то можно продолжить анализ с помощью профайлера. Я взял бесплатный профайлер VisualVM входящий в состав JDK.

Для начала запустим сервер с одним задеплоенным веб-приложением и перезапустим его, чтобы была видна утечка. Откроем VisualVM, выберем нужный процесс и сделаем heap dump, выбрав соответствующий пункт в выпадающем меню.

Выберем вкладку «OQL Console» и выполним такой запрос:
select x from org.apache.catalina.loader.WebappClassLoader x
(для других реализаций сервлета класс будет другим).

Один из двух экземпляров остался от первого остановленного веб-приложения, сборщик мусора не смог его собрать. Чтобы определить какой из них является старым – кликаем по одному из них и ищем поле started. У старого started будет false.

В окне «References» показываются все ссылки на этот class loader, нам нужно найти ту, из-за которой сборщик мусора не может его собрать. Для этого щелкаем правой кнопкой мыши по this и выбираем «Show Nearest GC Root».

Отлично, мы нашли какой-то поток, у которого наш старый class loader является contextClassloader-ом. Кликаем по нему правой кнопкой мыши и выбираем «Show Instance».

Смотрим на поля объекта и думаем, за что мы можем зацепиться, чтобы понять, что это за объект, как-то найти код который его создает, поймать в дебаггере, и т.п. В данном случае это имя потока – знакомый нам AWT-Windows. Мы нашли ту же проблему, о которой писал нам Tomcat, только с помощью VisualVM. Как ее решить вы уже знаете.

Итог

Мы научились определять, анализировать и исправлять утечки PermGen памяти. Оказалось это не так уж сложно, особенно благодаря встроенным средствам Tomcat-а. Я не могу гарантировать, что приведенными выше способами можно избавиться от всех видов утечек, однако мне удалось таким образом избавиться от утечек в нескольких крупных проектах.

Ссылки

Original : https://m.habr.com/ru/post/324144/

https://habr.com/ru/post/222443/

Введение в Android NDK

Для разработки приложений под ОС Android, Google предоставляет два пакета разработки: SDK и NDK. Про SDK существует много статей, книжек, а так же хорошие guidelines от Google. Но про NDK даже сам Google мало что пишет. А из стоящих книг я бы выделил только одну, Cinar O. — Pro Android C++ with the NDK – 2012.

Эта статья ориентирована на тех, кто ещё не знаком (или мало знаком) с Android NDK и хотел бы укрепить свои знания. Внимание я уделю JNI, так как мне кажется начинать нужно именно с этого интерфейса. Так же, в конце рассмотрим небольшой пример с двумя функциями записи и чтения файла. Кто не любит много текста, тот может посмотреть видео версию.

Что такое Android NDK?

Android NDK (native development kit) – это набор инструментов, которые позволяют реализовать часть вашего приложения используя такие языки как С/С++.

Для чего используют NDK?

Google рекомендует прибегать к использованию NDK только в редчайших случаях. Зачастую это такие случаи:

  • Нужно увеличить производительность (например, сортировка большого объема данных);
  • Использовать стороннюю библиотеку. Например, много уже чего написано на С/С++ языках и нужно просто заиспользовать существующий материал. Пример таких библиотек, как, Ffmpeg, OpenCV;
  • Программирование на низком уровне (например, всё что выходит за рамки Dalvik);

 

Что такое JNI?

Java Native Interface – стандартный механизм для запуска кода, под управлением виртуальной машины Java, который написан на языках С/С++ или Assembler, и скомпонован в виде динамических библиотек, позволяет не использовать статическое связывание. Это даёт возможность вызова функции С/С++ из программы на Java, и наоборот.

Преимущества JNI

Основное преимущество перед аналогами (Netscape Java Runtime Interface или Microsoft’s Raw Native Interface and COM/Java Interface) является то что JNI изначально разрабатывался для обеспечения двоичной совместимости, для совместимости приложений, написанных на JNI, для любых виртуальных машин Java на конкретной платформе (когда я говорю о JNI, то я не привязываюсь к Dalvik машине, потому как JNI был написан Oracle для JVM который подходит для всех Java виртуальных машин). Поэтому скомпилированный код на С/С++ будет выполнятся в не зависимости от платформы. Более ранние версии не позволяли реализовывать двоичную совместимость.

Двоичная совместимость или же бинарная совместимость – вид совместимости программ, позволяющий программе работать в различных средах без изменения её исполняемых файлов.

Как устроен JNI


JNI таблица, организована как таблица виртуальных функций в С++. VM может работать с несколькими такими таблицами. Например, одна будет для отладки, вторая для использования. Указатель на JNI интерфейс действителен только в текущем потоке. Это значит, что указатель не может гулять с одного потока в другой. Но нативные методы могут быть вызваны из разных потоков. Пример:

jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (JNIEnv *env, jobject obj, jint i, jstring s)
{
     const char *str = (*env)->GetStringUTFChars(env, s, 0); 
     (*env)->ReleaseStringUTFChars(env, s, str); 
     return 10;
}

 

  • *env – указатель на интерфейс;
  • оbj – ссылка на объект в котором описан нативный метод;
  • i and s – передаваемые аргументы;

Примитивные типы копируются между VM и нативным кодом, а объекты передаются по ссылке. VM обязана отслеживать все ссылки которые передаются в нативный код. Все переданные ссылки в нативный код не могут быть освобождены GC. Но нативный код в свою очередь должен информировать VM о том что ему больше не нужны ссылки на переданные объекты.

Локальные и глобальные ссылки

JNI делит ссылки на три типа: локальные, глобальные и слабые глобальные ссылки. Локальные действительны пока не завершиться метод. Все Java объекты которые возвращает функции JNI являются локальными. Программист должен надеется на то что VM сама подчистит все локальные ссылки. Локальные ссылки доступны лишь в том потоке в котором были созданы. Однако если есть необходимость то их можно освобождать сразу методом JNI интерфейса DeleteLocalRef:

jclass clazz;
clazz = (*env)->FindClass(env, "java/lang/String");
//ваш код
(*env)->DeleteLocalRef(env, clazz);

Глобальные ссылки остаются пока они явно не будут освобождены. Что бы зарегистрировать глобальную ссылку следует вызвать метод NewGlobalRef. Если же глобальная ссылка уже не нужна, то её можно удалить методом DeleteGlobalRef:

jclass localClazz;
jclass globalClazz;
localClazz = (*env)->FindClass(env, "java/lang/String");
globalClazz = (*env)->NewGlobalRef(env, localClazz);
//ваш код
(*env)->DeleteLocalRef(env, localClazz);

 

Обработка ошибок

JNI не проверяет ошибки такие как NullPointerException, IllegalArgumentException. Причины:

  • снижение производительности;
  • в большинстве функций C библиотек очень и очень трудно защитится от ошибок.

JNI позволяет использовать Java Exception. Большинство JNI функций возвращают код ошибок а не сам Exception, и поэтому приходится обрабатывать сам код, а в Java уже выбрасывать Exception. В JNI следует проверять код ошибки вызываемых функций и после них следует вызвать ExceptionOccurred(), которая в свою очередь возвращает объект ошибки:

jthrowable ExceptionOccurred(JNIEnv *env);

Например, некоторые функции JNI доступа к массивам не возвращают ошибки, но могут вызвать исключения ArrayIndexOutOfBoundsException или ArrayStoreException.

Примитивные типы JNI

В JNI существуют свои примитивные и ссылочные типы данных.

Java Type Native Type Description
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits
void void N/A

 

Ссылочные типы JNI

Модифицированный UTF-8

JNI использует модифицированную кодировку UTF-8 для представления строк. Java в свою очередь использует UTF-16. UTF-8 в основном используется в С, потому что он кодирует \u0000 в 0xc0, вместо привычной 0x00. Изменённые строки кодируются так, что последовательность символов, которые содержат только ненулевой ASCII символы могут быть представлены с использованием только одного байта.

Функции JNI

Интерфейс JNI содержит в себе не только собственный набор данных, но и свои собственные функции. На их рассмотрение уйдёт много времени, так как их не один десяток. Ознакомится с ними вы сможете в официальной документации.

Пример использования функций JNI

Небольшой пример, что бы вы усвоили пройденный материал:

#include <jni.h>
    //...
    JavaVM *jvm;
    JNIEnv *env;
    JavaVMInitArgs vm_args;
    JavaVMOption* options = new JavaVMOption[1];
    options[0].optionString = "-Djava.class.path=/usr/lib/java";
    vm_args.version = JNI_VERSION_1_6;
    vm_args.nOptions = 1;
    vm_args.options = options;
    vm_args.ignoreUnrecognized = false;
    JNI_CreateJavaVM(&jvm, &env, &vm_args);
    delete options;
    jclass cls = env->FindClass("Main");
    jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V");
    env->CallStaticVoidMethod(cls, mid, 100);
    jvm->DestroyJavaVM();

Разберём построчно:

  • JavaVM – предоставляет интерфейс для вызова функций, которые позволяют создавать и уничтожать JavaVM;
  • JNIEnv – обеспечивает большинство функций JNI;
  • JavaVMInitArgs – аргументы для JavaVM;
  • JavaVMOption – опции для JavaVM;

Метод JNI_CreateJavaVM() инициализирует JavaVM и возвращает на неё указатель. Метод JNI_DestroyJavaVM() выгружает созданную JavaVM.

Потоки

Всеми потоками в Linux управляет ядро, но они могут быть прикреплены к JavaVM функциями AttachCurrentThread и AttachCurrentThreadAsDaemon. Пока поток не присоединён он не имеет доступа к JNIEnv. Важно, Android не приостанавливает потоки которые были созданы JNI, даже если срабатывает GC. Но перед тем как поток завершиться он должен вызвать метод DetachCurrentThread что бы отсоединиться от JavaVM.

Первые шаги

Структура проекта у вас должна выглядеть следующим образом:

Как мы видим из рисунка 3, весь нативный код находится в папке jni. После сборки проекта, в папке libs создастся четыре папки под каждую архитектуру процессора, в которой будет лежать ваша нативная библиотека (количество папок зависит от количество выбранных архитектур).

Для того, чтобы создать нативный проект, нужно создать обычный Android проект и проделать следующие шаги:

  • В корне проекта нужно создать папку jni, в которую поместить исходники нативного кода;
  • Создать файл Android.mk, который будет собирать проект;
  • Создать файл Application.mk, в котором описываются детали сборки. Он не является обязательным условием, но позволяет гибко настроить сборку;
  • Создать файл ndk-build, который будет запускать процесс сборки (тоже не является обязательным).

 

Android.mk

Как упоминалось уже выше, это make файл для сборки нативного проекта. Android.mk позволяет группировать ваш код в модули. Модули могут быть как статические библиотеки (static library, только они будут скопированные в ваш проект, в папку libs), разделяемые библиотеки (shared library), автономный исполняемый файл (standalone executable).

Пример минимальной конфигурации:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    := NDKBegining
LOCAL_SRC_FILES := ndkBegining.c
include $(BUILD_SHARED_LIBRARY)

Рассмотрим детально:

  • LOCAL_PATH := $(call my-dir) – функция call my-dir возвращает путь папки в которой вызывается файл;
  • include $(CLEAR_VARS) – очищает переменные которые использовались до этого кроме LOCAL_PATH. Это необходимо так как все переменные являются глобальными, потому что сборка происходит в контексте одного GNU Make;
  • LOCAL_MODULE – имя выходного модуля. В нашем примере имя выходной библиотеки установлено как NDKBegining, но после сборки в папке libs создадутся библиотеки с именами libNDKBegining. Android добавляет к названию префикс lib, но в java коде при подключении вы должны указывать название библиотеки без префикса (то есть названия должны совпадать с установленными в make файлах);
  • LOCAL_SRC_FILES – перечисление исходных файлов из которых следует создать сборку;
  • include $(BUILD_SHARED_LIBRARY) – указывает тип выходного модуля.

В Android.mk можно определить свои переменные, но они не должны иметь такой синтаксис: LOCAL_, PRIVATE_, NDK_, APP_, my-dir. Google, рекомендует называть свои переменные, как MY_. Например:

MY_SOURCE := NDKBegining.c
Что бы обратится к переменной: $(MY_SOURCE)
Переменные, так же можно конкатенировать, например:
LOCAL_SRC_FILES += $(MY_SOURCE)

 

Application.mk

В этом make файле описывается несколько переменных, которые помогут сделать сборку более гибкой:

  • APP_OPTIM – дополнительная переменная которая устанавливается в значения release или debug. Используется для оптимизации при сборке модулей. Отлаживать можно как release так и debug, но debug предоставляет больше информации для отладки;
  • APP_BUILD_SCRIPT – указывает на альтернативный путь к Android.mk;
  • APP_ABI – наверное одна из самых важных переменных. Она указывает для какой архитектуры процессоров собирать модули. По умолчанию стоит armeabi которая соответствует ARMv5TE архитектуры. Например для поддержки ARMv7 следует использовать armeabi-v7a, для IA-32 – x86, для MIPS – mips, или если вам нужно поддерживать все архитектуры то значение должно быть таким: APP_ABI := armeabi armeabi-v7a x86 mips. Если вы использует ndk версии 7 и выше, то можно не перечислять все архитектуры, а установить так APP_ABI := all.
  • APP_PLATFORM – таргет платформы;
  • APP_STL – Android использует runtime библиотеку libstdc++.so которая является урезанной и разработчику доступен не весь функционал С++. Однако, переменная APP_STL позволяет включить в сборку поддержку расширений;
  • NDK_TOOLCHAIN_VERSION – позволяет выбрать версию компилятора gcc (по умолчанию 4.6);

 

NDK-BUILDS

Ndk-build из себя представляет обёртку GNU Make. После 4-й версии ввели флаги для ndk-build:

  • clean – очищает все сгенеренные бинарные файлы;
  • NDK_DEBUG=1 – генерирует отладочный код;
  • NDK_LOG=1 – показывает лог сообщений (используется для отладки);
  • NDK_HOST_32BIT=1 – Android имеет средства для поддержки 64-х битных версий утилит (например NDK_PATH\toolchains\mipsel-linux-android-4.8\prebuilt\windows-x86_64 и т.д.);
  • NDK_APPLICATION_MK — указывается путь к Application.mk.

В 5-й версии NDK был введён такой флаг как NDK_DEBUG. Если он установлен в 1 то создаётся отладочная версия. Если флаг не установлен то ndk-build по умолчанию проверяет стоит ли атрибут android:debuggable=«true» в AndroidManifest.xml. Если вы используете ndk выше 8-й версии, то Google не рекомендует использовать атрибут android:debuggable в AndroidManifest.xml (потому что если вы используете «ant debug» или строите отладочную версию с помощью ADT плагина то они автоматически добавляют флаг NDK_DEBUG=1).

По умолчанию устанавливается поддержка 64-х разрядной версии утилит, но вы можете принудительно собрать только для 32-х установив флаг NDK_HOST_32BIT=1. Google, рекомендует всё же использовать 64-х разрядность утилит для повышения производительности больших программ.

Как собрать проект?

Раньше это было мучением. Нужно было установить CDT плагин, скачать компилятор cygwin или mingw. Скачать Android NDK. Подключить это всё в настройках Eclipse. И как на зло это всё оказывалось не рабочим. Я первый раз когда столкнулся с Android NDK, то настраивал это всё 3 дня (а проблема оказалось в том что в cygwin нужно было дать разрешение 777 на папку проекта).

Сейчас с этим всё намного проще. Идёте по этой ссылке. Качаете Eclipse ADT Bundle в котором уже есть всё то что необходимо для сборки.

Вызов нативных методов из Java кода

Для того что бы использовать нативный код из Java вам сперва следует определить нативные методы в Java классе. Например:

native String nativeGetStringFromFile(String path) throws IOException;
native void nativeWriteByteArrayToFile(String path, byte[] b) throws IOException;

Перед методом следует поставить зарезервированное слово «native». Таким образом компилятор знает, что это точка входа в JNI. Эти методы нам нужно реализовать в С/С++ файле. Так же Google рекомендует начинать именовать методы со слова nativeХ, где Х – реальное название метода. Но перед тем как реализовывать эти методы вручную, следует сгенерировать header файл. Это можно сделать вручную, но можно использовать утилиту javah, которая находится в jdk. Но пойдём дальше и не будет использовать её через консоль, а будем это делать при помощи стандартных средств Eclipse.

Теперь можете запускать. В директории bin/classes будут лежать ваши header файлы.

Далее копируем эти файлы в jni директорию нашего нативного проекта. Вызываем контекстное меню проекта и выбираем пункт Android Tools – Add Native Library. Это позволит нам использовать jni.h функции. Дальше вы уже можете создавать cpp файл (иногда Eclipse его создаёт по умолчанию) и писать тела методов, которые уже описаны в header файле.

Пример кода я не стал добавлять в статью, чтобы не растягивать её. Пример вы можете посмотреть/скачать с github.

Original : https://habr.com/ru/post/203014/