Добрый вечер, коллеги. Ровно месяц назад мы получили контракт на перевод книги "Modern Java" от издательства Manning, которая должна стать одной из наших самых заметных новинок в будущем году. Проблема «Modern» и «Legacy» в Java настолько остра, что необходимость такой книги довольно назрела. Масштабы бедствия и способы решения возникающих проблем в Java 9 кратко описаны в статье Уэйна Ситрина (Wayne Citrin), перевод которой мы и хотим вам сегодня предложить.
Раз в несколько лет, с выходом новой версии Java, докладчики на JavaOne начинают смаковать новые языковые конструкции и API, хвалить их достоинства. А ретивым разработчикам тем временем не терпится внедрить новые возможности. Такая картина далека от реальности – она совершенно не учитывает, что большинство программистов заняты поддержкой и доработкой уже существующих приложений, а не пишут новые приложения с нуля.
Большинство приложений – в особенности коммерческие — должны быть обратно совместимы с более ранними версиями Java, в которых не поддерживаются все эти новые супер-пупер возможности. Наконец, большинство заказчиков и конечных пользователей, особенно в сегменте больших предприятий, настороженно относятся к радикальному обновлению Java-платформы, предпочитая выждать, пока она окрепнет.
Поэтому, как только разработчик собирается попробовать новую возможность, он сталкивается с проблемами. Вы бы стали использовать у себя в коде методы интерфейсов по умолчанию? Возможно – если вы счастливчик, и вашему приложению не требуется взаимодействовать с Java 7 или ниже. Хотите использовать класс
java.util.concurrent.ThreadLocalRandom
для генерации псевдослучайных чисел в многопоточном приложении? Не выйдет, если ваше приложение должно работать одновременно на Java 6, 7, 8 или 9.
С выходом нового релиза разработчики, занятые поддержкой унаследованного кода, чувствуют себя как дети, вынужденные таращиться на витрину кондитерского магазина. Внутрь их не пускают, поэтому их удел — разочарование и фрустрация.
Итак, есть ли в новом релизе Java 9 что-нибудь для программистов, занятых поддержкой унаследованного кода? Что-то, способное облегчить им жизнь? К счастью — да.
Что приходилось делать при поддержке legacy-кода то появления Java 9
Конечно, можно впихнуть возможности новой платформы в унаследованные приложения, в которых нужно соблюдать обратную совместимость. В частности, всегда есть возможности воспользоваться преимуществами новых API. Однако, может получиться немного некрасиво.
Например, можно применить позднее связывание, если вы хотите получить доступ к новому API, когда вашему приложению также требуется работать со старыми версиями Java, не поддерживающими этот API. Допустим, вам требуется использовать класс
java.util.stream.LongStream
, появившийся в Java 8, и вы хотите применить метод anyMatch(LongPredicate)
этого класса, но приложение должно быть совместимо с Java 7. Можно создать вспомогательный класс, вот так:public classLongStreamHelper {
private static Class longStreamClass;private static Class longPredicateClass;private static Method anyMatchMethod;static {try {longStreamClass = Class.forName("java.util.stream.LongStream");longPredicateClass = Class.forName("java.util.function.LongPredicate");anyMatchMethod = longStreamClass.getMethod("anyMatch", longPredicateClass):} catch (ClassNotFoundException e) {longStreamClass = null;longPredicateClass = null;anyMatchMethod = null} catch (NoSuchMethodException e) {longStreamClass = null;longPredicateClass = null;anyMatchMethod = null;}public static boolean anyMatch(Object theLongStream, Object thePredicate)throws NotImplementedException {if (longStreamClass == null) throw new NotImplementedException();try {Boolean result= (Boolean) anyMatchMethod.invoke(theLongStream, thePredicate);return result.booleanValue();} catch (Throwable e) { // lots of potential exceptions to handle. Let’s simplify.throw new NotImplementedException();}}}
Есть способы упростить эту операцию, либо сделать ее более общей, либо более эффективной – идею вы уловили.
Вместо того, чтобы вызывать
theLongStream.anyMatch(thePredicate)
, как вы поступили бы в Java 8, можно вызвать LongStreamHelper.anyMatch(theLongStream, thePredicate)
в любой версии Java. Если вы имеете дело с Java 8 – это сработает, но, если с Java 7 – то программа выбросит исключение NotImplementedException
.
Почему это некрасиво? Потому что код может чрезмерно усложниться, если требуется обращаться ко множеству API (на самом деле, даже сейчас, с единственным API, это уже неудобно). Кроме того, такая практика и не типобезопасна, поскольку в коде нельзя прямо упомянуть
LongStream
или LongPredicate
. Наконец, такая практика гораздо менее эффективна, из-за издержек, связанных с рефлексией, а также из-за дополнительных блоков try-catch
. Следовательно, хотя и можно так сделать, это не слишком интересно, и чревато ошибками по невнимательности.
Да, вы можете обращаться к новым API, а ваш код при этом сохраняет обратную совместимость, но с новыми языковыми конструкциями вам это не удастся. Например, допустим, что нам нужно использовать лямбда-выражения в коде, который должен оставаться обратно-совместимым и работать в Java 7. Вам не повезло. Компилятор Java не позволит указать версию исходного кода выше целевой. Так, если задать уровень соответствия исходного кода 1.8 (т.е., Java 8), а целевой уровень соответствия будет 1.7 (Java 7), то компилятор вам этого не позволит.
Вам помогут разноверсионные JAR-файлы
Разноверсионные JAR-файлы почти не отличаются от старых добрых JAR-файлов, но с одной важнейшей оговоркой: в новых JAR-файлах появилась своеобразная «ниша», куда можно записывать классы, использующие новейшие возможности Java 9. Если вы работаете с Java 9, то JVM найдет эту «нишу», станет использовать классы из нее и игнорировать одноименные классы из основной части JAR-файла.
Однако, при работе с Java 8 или ниже, JVM неизвестно о существовании этой «ниши». Она игнорирует ее и использует классы из основной части JAR-файла. С выходом Java 10 появится новая аналогичная «ниша» для классов, использующих наиболее актуальные возможности Java 10 и так далее.
В JEP 238 – предложении на доработку Java, где описаны ращновенсионные JAR-файлы, приводится простой пример. Допустим, у нас есть JAR-файл с четырьмя классами, работающими в Java 8 или ниже:
JAR root
- A.class- B.class- D.class- C.class
JAR root
- A.class- B.class- D.class- C.classVersions- META-INF- 9- A.class- B.class- 10- A.class
\META-INF\Versions
, поскольку даже не подозревает о нем и не ищет его. Используются лишь оригинальные классы A, B, C и D.
При запуске под Java 9, используются классы, находящиеся в
\META-INF\Versions\9
, причем, они используются вместо оригинальных классов A и B, но классы в \META-INF\Versions\10
игнорируются.
При запуске под Java 10 используются обе ветки
\META-INF\Versions
; в частности, версия A от Java 10, версия B от Java 9 и используемые по умолчанию версии C и D.
Итак, если в вашем приложении вам нужен новый ProcessBuilder API из Java 9, но нужно обеспечить, чтобы приложение продолжало работать и под Java 8, просто запишите в раздел
\META-INF\Versions\9
JAR-файла новые версии ваших классов, использующие ProcessBuilder, а старые классы оставьте в основной части архива, используемой по умолчанию. Именно так проще всего использовать новые возможности Java 9, не жертвуя при этом обратной совместимостью.
В Java 9 JDK есть версия инструмента jar.exe, поддерживающая создание разноверсионных JAR-файлов. Такую поддержку также обеспечивают и другие инструменты, не входящие в JDK.
Java 9: модули, повсюду модули
Система модулей Java 9, (также известная под названием Project Jigsaw) – это, несомненно, крупнейшее изменение в Java 9. Одна из целей модуляризации — усилить действующий в Java механизм инкапсуляции, чтобы разработчик мог указывать, какие API предоставляются другим компонентам и мог рассчитывать, что JVM будет навязывать инкапсуляцию. При модуляризации инкапсуляция получается сильнее, чем при применении модификаторов доступа
public/protected/private
у классов или членов классов.
Вся система модулей большая и сложная, и ее подробное обсуждение выходит за рамки этой статьи (Вот хорошее, подробное объяснение). Здесь я уделю внимание именно тем аспектам модуляризации, которые помогают разработчику при поддержке унаследованных приложений.
Во-первых, JAR-файл становится модуляризован (и превращается в модуль) с появлением в нем файла module-info.class (скомпилированного из module-info.java) у корня файла JAR.
module-info.java
содержит метаданные, в частности, название модуля, пакеты которого экспортируются (т.e., становятся видны извне), каких модулей требует данный модуль и некоторая иная информация.
Информация в
module-info.class
видна лишь в случаях, когда JVM ищет ее – то есть, система трактует модуляризованные JAR-файлы точно как обычные, если работает со старыми версиями Java (предполагается, что код был скомпилирован для работы с более старой версией Java. Строго говоря, требуется немного похимичить, и все равно указывать в качестве целевой версии module-info.class именно Java 9, но это реально).
Таким образом, у вас должна остаться возможность запускать модуляризованные JAR-файлы с Java 8 и ниже при условии, что в иных отношениях они также совместимы с более ранними версиями Java. Также отметим, что файлы
module-info.class
можно, с оговорками, помещать в версионируемых областях разноверсионных JAR-файлов.
В Java 9 существует как classpath, так и путь к модулю. and a module path. Classpath работает как обычно. Если поставить модуляризованный JAR-файл в classpath, он тратуется как любой другой JAR-файл. То есть, если вы модуляризовали JAR-файл, а ваше приложение еще не готово обращаться с ним как с модулем, его можно поставить в classpath, он будет работать как всегда. Ваш унаследованный код должен вполне успешно с ним справиться.
Также отметим, что коллекция всех JAR-файлов в classpath считается частью единственного безымянного модуля. Такой модуль считается самым обычным, однако, всю информацию он экспортирует другим модулям, и может обращаться к любым другим модулям. Таким образом, если у вас еще нет модуляризованного Java-приложения, но есть некие старые библиотеки, которые тоже пока не модуляризованы (и, вероятно, никогда не будут) – можно просто положить все эти библиотеки в classpath, и вся система будет нормально работать.
Не составляет труда перенести JAR-файл из classpath в путь модулей – и пользоваться всеми преимуществами модуляризации. Во-первых, можно добавить файл
module-info.class
в файл JAR, а затем поставить модуляризованный JAR-файл в путь модулей. Такой новоиспеченный модуль все равно сможет обращаться ко всем оставшимся JAR-файлам в classpath JAR, поскольку они входят в безымянный модуль и остаются в доступе.
Также возможно, что вы не хотите модуляризовать JAR-файл, либо, что JAR-файл принадлежит не вам, а кому-то еще, так что модуляризовать его сами вы не можете. В таком случае JAR-файл все равно можно поставить в путь модулей, он станет автоматическим модулем.
module-info.class
. Этот модуль одноименен тому JAR-файлу, в котором содержится, и другие модули могут явно затребовать его. Он автоматически экспортирует все свои публично доступные API и читает (то есть, требует) все прочие именованные модули, а также безымянные модули.
Не каждый немодуляризованный JAR-файл можно переместить в путь модулей и превратить его в автоматический модуль. Существует правило: пакет может входить в состав всего одного именованного модуля. То есть, если пакет находится более чем в одном JAR-файле, то всего один JAR-файл с этим пакетом в составе можно превратить в автоматический модуль. Остальные могут остаться в classpath и войти в состав безымянного модуля.
На первый взгляд, описанный здесь механизм кажется сложным, но на практике он весьма прост. На самом деле, речь в данном случае лишь о том, что можно оставить старые JAR-файлы в classpath или переместить их в путь модулей. Можно разбить их на модули или не делать этого. А когда ваши старые JAR-файлы будут модуляризованы, можно оставить их в classpath или переместить в путь модулей.
Java 9 «делает сама»: Модульный JDK и Jlink
Процесс немного хлопотный: необходимо поддерживать иерархию установочных файлов, быть при этом внимательным, поэтому вы не пропускаете ни одного файла, ни одного каталога. Само по себе это не повредит, однако, все-таки хочется избавиться от всего лишнего, поскольку эти файлы занимают место. Да, легко поддаться и совершить такую ошибку.
Так что почему бы не перепоручить эту работу JDK?
Ключевой ресурс для создания таких самодостаточных исполняемых образов – это модульная система. Модуляризовать теперь можно не только собственный код, но и сам Java 9 JDK. Теперь библиотека классов Java – это коллекция модулей, из модулей же состоят и инструменты JDK. Система модулей требует указывать модули базовых классов, которые необходимы в вашем коде, и при этом вы указываете необходимые элементы JDK.
Чтобы собрать все это вместе, в Java 9 предусмотрен специальный новый инструмент под названием jlink. Запустив jlink, вы получаете иерархию файлов – именно тех, что нужны для запуска вашего приложения, ни больше, ни меньше. Такой набор будет гораздо меньше стандартной JRE, причем, он будет платформо-специфичным (то есть, подбираться для конкретной операционной системы и машины). Поэтому, если вы захотите создать такие исполняемые образы для других платформ, потребуется запустить jlink в контексте установки на каждой конкретной платформе, для которой вам нужен такой образ.
Также обратите внимание: если запустить jlink с приложением, в котором ничего не модуляризовано, у инструмента просто не будет нужной информации, позволяющей ужать JRE, поэтому jlink ничего не останется, кроме как упаковать целую JRE. Даже в таком случае вам будет немного удобнее: jlink сам упакует для вас JRE, поэтому можете не волноваться о том, как правильно скопировать файловую иерархию.
С jlink становится легко упаковать приложение и все, что необходимо для его запуска – и можете не волноваться, что сделаете что-нибудь неправильно. Инструмент упакует только ту часть среды исполнения, которая требуется для работы приложения. То есть, унаследованное Java-приложение гарантированно получит такую среду, в которой оно окажется работоспособным.
Встреча старого и нового
Следует отдать должное проектировщикам Java 9: по-видимому, они это учли и хорошо поработали, чтобы предоставить эти новые возможности и тем разработчикам, которым приходится поддерживать старые версии Java.
Разноверсионные JAR-файлы позволяют разработчикам применять новые возможности Java 9 и выносить их в отдельную часть JAR-файла, где более ранние версии Java их не заметят. Таким образом, разработчику легко писать код для Java 9, оставить старый код для Java 8 и ниже и предоставить среде исполнения выбор – какие классы она сможет запустить.
Благодаря модулям Java, разработчику проще проверять зависимости, достаточно писать все новые JAR-файлы в модульном виде, а старый код оставлять немодуляризованным. Система очень щадящая, она ориентирована на постепенную миграцию и практически всегда поддерживает работу с унаследованным кодом, который «слыхом не слыхивал» о модульной системе.
Благодаря модульному JDK и jlink, можно с легкостью создавать исполняемые образы и гарантированно обеспечивать приложение такой средой исполнения, в которой будет все необходимое для работы. Ранее такой процесс был чреват множеством ошибок, но современный инструментарий Java позволяет его автоматизировать – и все просто работает.
В отличие от предыдущих релизов Java, в Java 9 вы можете свободно пользоваться всеми новыми возможностями, а если имеете дело со старым приложением и должны гарантировать его поддержку – то сможете удовлетворить потребности всех ваших пользователей, независимо от того, горят ли они желанием обновляться до самой актуальной версии Java.
Источник: https://habr.com/company/piter/blog/417733/?utm_source=habrahabr&utm_medium=rss&utm_campaign=417733
Смотри также:
Зачем нужна Java. http://fetisovvs.blogspot.com/2014/07/java.html
Разбор основных концепций параллелизма. http://fetisovvs.blogspot.com/2018/04/java.html
Первый контакт с «var» в Java 10. http://fetisovvs.blogspot.com/2018/01/var-java-10-java.html
Разбор основных концепций параллелизма. http://fetisovvs.blogspot.com/2018/04/java.html
Первый контакт с «var» в Java 10. http://fetisovvs.blogspot.com/2018/01/var-java-10-java.html
JAVA 9. Что нового? http://fetisovvs.blogspot.com/2017/10/java-9-java.html
Концепции объектно-ориентированного программирования — ООП в Java. http://fetisovvs.blogspot.com/2017/01/java-java.html
Анимации в Android по полочкам (Часть 1. Базовые анимации). http://fetisovvs.blogspot.com/2018/02/android-1-java.html
Концепции объектно-ориентированного программирования — ООП в Java. http://fetisovvs.blogspot.com/2017/01/java-java.html
Анимации в Android по полочкам (Часть 1. Базовые анимации). http://fetisovvs.blogspot.com/2018/02/android-1-java.html
Двести пятьдесят русскоязычных обучающих видео докладов и лекций о Java. http://fetisovvs.blogspot.com/2015/12/java-5-java-java.html
Абстрактные классы и методы. http://fetisovvs.blogspot.com/2017/02/java.html
Полное руководство по Java Reflection API. Рефлексия на примерах. http://fetisovvs.blogspot.com/2017/02/java-reflection-api-java.html
Микросервисы для Java программистов. Практическое введение во фреймворки и контейнеры. http://fetisovvs.blogspot.com/2017/10/java-java.html
Микросервисы для Java программистов. Практическое введение во фреймворки и контейнеры. (Часть 3). http://fetisovvs.blogspot.com/2017/10/java-3-java.html
ТОП-3 способа конвертировать массив в ArrayList. Пример на Java. http://fetisovvs.blogspot.com/2016/09/3-arraylist-java-java.html
Ввод–вывод в Java. http://fetisovvs.blogspot.com/2016/05/java-java_28.html
Enum-Всемогущий. http://fetisovvs.blogspot.com/2017/02/enum-java.html
Массивы в Java. Создание и обработка. http://fetisovvs.blogspot.com/2017/10/java-java_18.html
Arrays, Collections: Алгоритмический минимум. http://fetisovvs.blogspot.com/2017/12/arrays-collections.html
Популярные методы для работы с Java массивами. http://fetisovvs.blogspot.com/2016/09/java-java_29.html
Пример использования метода replace в Java. Как заменить символ в строке? http://fetisovvs.blogspot.com/2017/01/replace-java-java.html
Класс Scanner в Java — описание и пример использования. http://fetisovvs.blogspot.com/2017/01/scanner-java-java.html
Пример использования метода trim в Java: как удалить пробелы в начале и конце строки? http://fetisovvs.blogspot.com/2017/01/trim-java-java.html
Spark — Потрясающий веб-микрофреймворк для Java. http://fetisovvs.blogspot.com/2017/10/spark-java-java.html
Чтение и запись CSV файла с помощью SuperCSV. http://fetisovvs.blogspot.com/2017/01/csv-supercsv-java-java.html
Конструкция try/catch/finally (исключения). http://fetisovvs.blogspot.com/2017/01/trycatchfinally-java.html
1000+ часов видео по Java на русском. http://fetisovvs.blogspot.nl/2017/06/1000-java-java.html
Шпаргалка Java программиста 7.1 Типовые задачи: Оптимальный путь преобразования InputStream в строку. http://fetisovvs.blogspot.com/2016/04/java-71-inputstream-java.html
Шпаргалки Java программиста 10: Lombok. http://fetisovvs.blogspot.nl/2017/12/java-10-lombok-java.html
Шпаргалки Java программиста 9: Java SE — Шпаргалка для собеседований и повторений. http://fetisovvs.blogspot.com/2017/12/java-9-java-se-java.html
Шпаргалка Java программиста 8. Библиотеки для работы с Json (Gson, Fastjson,
LoganSquare, Jackson, JsonPath и другие). http://fetisovvs.blogspot.com/2016/04/java-8-json-gson-fastjson-logansquare.html
Java 8 и паттерн Стратегия. http://fetisovvs.blogspot.com/2018/03/java-8-java.html
Абстрактные классы и методы. http://fetisovvs.blogspot.com/2017/02/java.html
Полное руководство по Java Reflection API. Рефлексия на примерах. http://fetisovvs.blogspot.com/2017/02/java-reflection-api-java.html
Микросервисы для Java программистов. Практическое введение во фреймворки и контейнеры. http://fetisovvs.blogspot.com/2017/10/java-java.html
Микросервисы для Java программистов. Практическое введение во фреймворки и контейнеры. (Часть 3). http://fetisovvs.blogspot.com/2017/10/java-3-java.html
ТОП-3 способа конвертировать массив в ArrayList. Пример на Java. http://fetisovvs.blogspot.com/2016/09/3-arraylist-java-java.html
Ввод–вывод в Java. http://fetisovvs.blogspot.com/2016/05/java-java_28.html
Enum-Всемогущий. http://fetisovvs.blogspot.com/2017/02/enum-java.html
Массивы в Java. Создание и обработка. http://fetisovvs.blogspot.com/2017/10/java-java_18.html
Arrays, Collections: Алгоритмический минимум. http://fetisovvs.blogspot.com/2017/12/arrays-collections.html
Популярные методы для работы с Java массивами. http://fetisovvs.blogspot.com/2016/09/java-java_29.html
Пример использования метода replace в Java. Как заменить символ в строке? http://fetisovvs.blogspot.com/2017/01/replace-java-java.html
Класс Scanner в Java — описание и пример использования. http://fetisovvs.blogspot.com/2017/01/scanner-java-java.html
Пример использования метода trim в Java: как удалить пробелы в начале и конце строки? http://fetisovvs.blogspot.com/2017/01/trim-java-java.html
Spark — Потрясающий веб-микрофреймворк для Java. http://fetisovvs.blogspot.com/2017/10/spark-java-java.html
Чтение и запись CSV файла с помощью SuperCSV. http://fetisovvs.blogspot.com/2017/01/csv-supercsv-java-java.html
Конструкция try/catch/finally (исключения). http://fetisovvs.blogspot.com/2017/01/trycatchfinally-java.html
1000+ часов видео по Java на русском. http://fetisovvs.blogspot.nl/2017/06/1000-java-java.html
Шпаргалка Java программиста 7.1 Типовые задачи: Оптимальный путь преобразования InputStream в строку. http://fetisovvs.blogspot.com/2016/04/java-71-inputstream-java.html
Шпаргалки Java программиста 10: Lombok. http://fetisovvs.blogspot.nl/2017/12/java-10-lombok-java.html
Шпаргалки Java программиста 9: Java SE — Шпаргалка для собеседований и повторений. http://fetisovvs.blogspot.com/2017/12/java-9-java-se-java.html
Шпаргалка Java программиста 8. Библиотеки для работы с Json (Gson, Fastjson,
LoganSquare, Jackson, JsonPath и другие). http://fetisovvs.blogspot.com/2016/04/java-8-json-gson-fastjson-logansquare.html
Java 8 и паттерн Стратегия. http://fetisovvs.blogspot.com/2018/03/java-8-java.html
Реализация ООП-наследования в классах, работающих с SQL и MS Entity Framework. http://fetisovvs.blogspot.com/2017/02/sql-ms-entity-framework.html
Как установить соединение с СУБД MySQL в IntelliJ IDEA в редакции Community. http://fetisovvs.blogspot.com/2016/04/mysql-intellij-idea-community-java.html
Как с помощью maven работать с библиотеками, которых в maven нет. http://fetisovvs.blogspot.com/2017/03/maven-maven-java.html
Проекты по созданию компиляторов из Java в JavaScript и исполняемые файлы. http://fetisovvs.blogspot.com/2018/01/java-javascript-java.html
Как установить соединение с СУБД MySQL в IntelliJ IDEA в редакции Community. http://fetisovvs.blogspot.com/2016/04/mysql-intellij-idea-community-java.html
Как с помощью maven работать с библиотеками, которых в maven нет. http://fetisovvs.blogspot.com/2017/03/maven-maven-java.html
Проекты по созданию компиляторов из Java в JavaScript и исполняемые файлы. http://fetisovvs.blogspot.com/2018/01/java-javascript-java.html
Диагностика утечек памяти в Java. http://fetisovvs.blogspot.com/2017/03/java-java_18.html
Spring AOP и JavaConfig в плагинах для Atlassian Jira. http://fetisovvs.blogspot.com/2018/04/spring-aop-javaconfig-atlassian-jira.html
Блеск и нищета Java для настольных систем. http://fetisovvs.blogspot.com/2018/04/java-haulmont-java.htmlSpring AOP и JavaConfig в плагинах для Atlassian Jira. http://fetisovvs.blogspot.com/2018/04/spring-aop-javaconfig-atlassian-jira.html
Разбор задачек от Одноклассников на JPoint 2018. http://fetisovvs.blogspot.com/2018/04/jpoint-2018-java.html
Программируем… выход из лабиринта. http://fetisovvs.blogspot.com/2015/10/java.html
Основы работы с IntelliJ IDEA. Интерфейс программы. http://fetisovvs.blogspot.com/2016/09/intellij-idea-java.html
Основы работы с IntelliJ IDEA. Интерфейс программы. http://fetisovvs.blogspot.com/2016/09/intellij-idea-java.html
Ускоряем время сборки и доставки java web приложения. http://fetisovvs.blogspot.com/2018/03/java-web-java.html
Комментариев нет:
Отправить комментарий