Главная » Хабрахабр » Java Script != JavaScript. Пять джав в одном классе. Скриптуем так, чтобы запомнили навсегда

Java Script != JavaScript. Пять джав в одном классе. Скриптуем так, чтобы запомнили навсегда

На этой неделе у JUG.ru Group, скорее всего, выйдет анонс. Пока не скажу чего. Участие в тайных проектах будит креатив, поэтому вот вам очередной ночной видосик про джаву.

Чуть менее чем полностью он состоит из скринкаста. Невероятные новости: теперь он не полтора часа длиной, а около 20 минут, и там даже есть что смотреть. Вэлкам, и да пребудет с вами Джава.
Скоро выйдет 12 джава, а многие всё ещё сидят на Cемёрочке и считают, что в случае апгрейда ничего особо нового или интересного они не увидят. Кто на дух не переносит этой видеодряни и любит потреблять текстовые расшифровки, пришлось запилить много текста после ката. Ну и мракобесы, конечно же, от прогресса не спасутся. В этом суперкоротком выпуске мы научимся превращать жизнь наших коллег в ад с помощью скриптовой джавы, и в паре мест я вверну новые примочки.

Там постоянно вылезают новые фичи, какие-то новые шорткаты, в десять раз повышающие вашу продуктивность как разработчика. Скажите, зачем вы обновляете свою Идею? Вы действительно читаете эти новости? Но вы то о них не знаете. Вам, в принципе, большую часть времени — наплевать. Если вы — среднестатистический пользователь, то, наверное, нет. Потому что скинчики красивые, тёмная тема, тачбар на Макбуке. Вы обновляете Идею просто потому что… можете. Но такое объяснение не проканает перед начальством как ответ «зачем покупать Идею».

Идея в бете, строки — в превью, но это уже никому не важно. Вместо этого можно сказать, что 29 октября, совсем недавно, JetBrains запилил поддержку сырых строк в бета-версии Идеи. По себе знаю. Если вы уж настолько упоротый, что поставили 12 джаву, то у вас есть проблемы посерьезней.

Для этого попробуем решить какую-нибудь демонстрационную задачу. Давайте посмотрим, как это выглядит на практике. Например, в компьютерных играх это квесты, в дженкинсах это билд-скрипты, и так далее. Довольно часто возникает проблема что-нибудь заскриптовать. Почему, зачем? Обычно для этого используют Пайтон или Груви, а давайте возьмём и заиспользуем голую джаву! Звучит как отличная идея 🙂 Потому что можем, в три строчки, и даже без хаков.

Всё есть на Гитхабе.
Представим, что у нас есть файл типа такого:

package io.javawatch; public class HelloHabrPrototype
};

Мы хотим, чтобы он выполнялся не при компиляции всего приложения, а как строка — уже после запуска. Как скрипт, то есть.
Вначале нужно перегнать всё в строку.

private static final String SOURCE = "\n" + " package io.javawatch;\n" + " public class HelloHabr {\n" + " public String get() {\n" + " return \"Hello Habr!\";\n" + " }\n" + " };"; public static final String CNAME = "io.javawatch.HelloHabr"; private static String CODE_CACHE_DIR = "C:/temp/codeCache";

Все эти "\n", плюсики и отступы выглядят крайне убого.

Наверное, это хорошее решение, но оно подходит не всегда. Раньше всё что мы могли сделать — положить код в файл и прочитать его. Даже вышеприведённая конструкция куда лучше, чем просто голословная отсылка, что у тебя где-то там есть код, который что-то там делает. Например, ты — докладчик на конференции и демонстрируешь код со слайдов. Ну и так далее. Переключение слайдов жрёт время и внимание слушателей. Короче, кейсы, когда нужен код именно инлайном, придумать можно.

Встаем курсором на код, жмём Alt+Enter (или что там на вашей операционке запускает Quck Fix в Идее). Теперь у нас есть возможность избавиться от мусора с помощью сырых строк. Выбираем «convert to raw string literal» и получаем вот такую няшку:

private static final String SOURCE = ` package io.javawatch; public class HelloHabr { public String get() { return "Hello Habr!"; } };`; public static final String CNAME = "io.javawatch.HelloHabr"; private static String CODE_CACHE_DIR = "C:/temp/codeCache";

По-моему, ради этой фичи уже стоит бежать устанавливать JDK 12.

Кстати, чтобы фича заработала, нужно будет проделать несколько действий:

  • Скачать JDK 12 и прописать в настройках проекта
  • В глобальных настройках javac выставить флаг --release, версию байткода 12, дополнительные флаги --enable-preview -Xlint:preview
  • Теперь в любой run/debug конфигурации нужно добавлять VM-флаг --enable-preview

Если не врубились, как это делается, смотрите мой скринкаст из шапки поста, там всё довольно наглядно.

Теперь для запуска нужно выполнить три нехитрых шага: превратить строку в удобную внутреннюю репрезентацию исходника, скомпилировать его и запустить:

public static void main(String[] args) throws Exception { /* 1 */ RuntimeSource file = RuntimeSource.create(); //SimpleJavaFileObject /* 2 */ compile(Collections.singletonList(file)); /* 3 */ String result = run(); /* 4 */ /* ??? */ /* 5 */ /* PROFIT! */ System.out.println(result); }

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

/** * This implementation always throws {@linkplain * UnsupportedOperationException}. Subclasses can change this * behavior as long as the contract of {@link FileObject} is * obeyed. */ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { throw new UnsupportedOperationException(); }

Так что приходится написать какого-то своего наследника. Обратите внимание, что оригинальный конструктор SimpleJavaFileObject хочет себе URI компилируемого класса, а где ж мы его возьмём? Поэтому я предлагаю просто клеить его максимально очевидным образом как здесь в функции buildURI:

public static class RuntimeSource extends SimpleJavaFileObject { private String contents = null; @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { return contents; } private static RuntimeSource create() throws Exception { return new RuntimeSource(CNAME, SOURCE); } public RuntimeSource(String className, String contents) throws Exception { super(buildURI(className), Kind.SOURCE); this.contents = contents; } public static URI buildURI(String className) { // io.javawatch.HelloHabr -> // string:///io/javawatch/HelloHabr.java URI uri = URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension); System.out.println(uri); return uri; }

Теперь переходим к компиляции:

public static void compile(List<RuntimeSource> files) throws IOException { File ccDir = new File(CODE_CACHE_DIR); if (ccDir.exists()) { FileUtils.deleteDirectory(ccDir); FileUtils.forceMkdir(ccDir); } JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); Logger c = new Logger(); StandardJavaFileManager fileManager = compiler.getStandardFileManager(c, Locale.ENGLISH, null); Iterable options = Arrays.asList("-d", CODE_CACHE_DIR, "--release=12", "--enable-preview", "-Xlint:preview"); JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, c, options, null, files); if (task.call()) { System.out.println("compilation ok"); } }

Обратите внимание: мы передаем для сборки четыре флага, три из которых отвечают за прописывание ровно тех же самых опций, что мы делали мышкой в настройках javac в Идее.

Ну и наконец, запускаем наш временный класс:

public static String run() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, MalformedURLException { // Загрузка класса File ccDir = new File(CODE_CACHE_DIR); ClassLoader loader = new URLClassLoader(new URL[]{ccDir.toURL()}); var clаss = loader.loadClass("io.javawatch.HelloHabr"); // Java 10 // Запуск метода рефлекшеном Object instance = clаss.getConstructor().newInstance(); // Java 9
// Object instance = clаss.newInstance(); Method thisMethod = clаss.getDeclaredMethod("get"); Object result = thisMethod.invoke(instance); return (String) result; }

Обратите внимание, что var clаss = loader.loadClass удобней для написания, чем Class<?> clаss = loader.loadClass, и не привносит никаких новых ворнингов. Ключевое слово var появилось в Десятке.

Он глотает исключения, что плохо. Еще обратите внимание, что clаss.newInstance() предлагается выпилить, начиная с Девятки. В Девятке предлагают вначале звать getConstructor, который параметризован и бросает правильные исключения.

Домашнее задание: сделать это можно несколькими способами, надо понять, какой из них наиболее интересный и почему. Еще обратите внимание, что я назвал переменную словом class, но Java не стала ругаться.

Оно работает. Ну в общем, и всё.

Тут критик воскликнет, здесь всё в чёрном цвете, ведь есть в джава-мире библиотеки, которые сами тебе скомпилируют всё в одну строчку.

Вот код на библиотеке JOOR, находящейся в топе гугла: Окей, давайте поглядим.

package io.javawatch; import org.joor.Reflect; import java.util.function.Supplier; public class Automatic { public static void main(String[] args) { Supplier<String> supplier = Reflect.compile( "io.javawatch.HelloHabr", ` package io.javawatch; public class HelloHabr implements java.util.function.Supplier<String> { public String get() { return "Hello Habr!"; } };` ).create().get(); System.out.println(supplier.get()); }
}

Как будто бы всё отлично. Действительно, в одну строчку, разве что пришлось саплаер притащить.

Попробуйте вернуть «Hello Habr!» как raw string literal: Но есть нюанс.

public String get() { return ``Hello Habr!``;
}

Всё мгновенно упадёт с ошибкой "(use --enable-preview to enable raw string literals)". Но мы же его уже включили? Да чёрта с два. Мы его включили в Идее, а JOOR собирает системным компилятором! Давайте глянем, что там внутри:

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
ompiler.getTask(out, fileManager, null, null, null, files).call();

А как у нас было, когда мы сами звали тот же JavaCompiler?

Iterable options = Arrays.asList("-d", CODE_CACHE_DIR, "--release=12", "--enable-preview", "-Xlint:preview"); JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, c, options, null, files);

А в JOOR там просто голый null стоит вместо опций. Их даже нельзя передать внутрь!

Если я сам не соберусь, вы его сделайте, ага? Наверное, это хорошая идея, чтобы запушить им в JOOR такой pull request на Гитхабе.

Иногда проще написать небольшую стену текста, но иметь над ней контроль. А мораль простая, бойтесь опенсорцев даров приносящих.

Есть способ и не писать стену текста, и не использовать подозрительные библиотеки. Начиная с Java 9 у нас имеется интерактивный шелл (похожий на тот, что есть в Python, Ruby и других скриптовых языках), который умеет выполнять джавовские команды. Ну и конечно, он сам написан на Java и доступен в виде Java-класса. Можно создать его экземпляр и просто выполнить нужное присваивание напрямую:

public class Shell { public static void main(String[] args) { var jShell = JShell.create(); //Java 9 jShell.eval("String result;"); jShell.eval(`result = "Hello Habr!";`); //Java 12 var result = jShell.variables() //Streams: Java 8, var: Java 10 .filter((@Nullable var v) -> v.name().equals("result")) //var+lambda: Java 11 .findAny() .get(); System.out.println(jShell.varValue(result)); }
}

Стримы появились в Java 8, и теперь их используют повсеместно, и даже не нужно самому вызывать stream(), его за тебя позовут другие. В данном случае вызов variables() возвращает именно стрим, а не ArrayList, как это сделал бы кто-то из непростого семёрочного детства. Результат стрима можно тут же влить в var.

Эта возможность с нами начиная с Java 11. Обратите внимание, что теперь можно писать var ещё и в параметрах лямбды.

В нём используется куча фич из разных поколений Java, и всё это выглядит целостно и гармонично. В целом, это очень показательный класс. Я уверен, что такие вещи будут использовать все и повсеместно, поэтому код на Java 12 будет прямо визуально отличаться от того, что мы имели в Java 7.

Мы посмотрели несколько фичей:

  • 8: Стримы (для тех, кто в танке)
  • 9: JShell и новые deprecated методы
  • 10: Ключевое слово var
  • 11: Развитие var для параметров лямбды
  • 12: Raw String Literals и их поддержка в IntelliJ IDEA

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

Вы его знаете как чувака из топа хаба Java на Хабре, lany. Если хотите больше узнать про джаву как язык, то стоит, например, смотреть на доклады Тагира Валеева. Там и про язык Java, и про IntelliJ IDEA, всё есть. На Joker рассказывал про Amber, а на JPoint — свои знаменитые паззлеры, и так далее и тому подобное. Собственно, из зависти к Тагиру, который может что-то рассказать про сам язык, а я — нет и получился этот пост. Всё это можно найти на YouTube. А еще будут новые джокеры и JPoint, но это мы потом обсудим.

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


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Перевод] Вышел Rust 2018… но что это такое?

Статья написана Лин Кларк в сотрудничестве с командой разработчиков Rust («мы» в тексте). Можете прочитать также сообщение в официальном блоге Rust. В этом релизе мы сосредоточились на производительности, чтобы разработчики Rust стали работать максимально эффективно. 6 декабря 2018 года вышла ...

50 лет спустя. The Mother of All Demos

«Компьютерная революция еще не случилась.(The computer revolution hasnt happened yet)»— Алан Кей Всем привет. И я стартую проект «Энгельбарт» (чтобы это ни было и что бы это ни значило). Сегодня 50 лет с исторического события, известного как "Мать всех демонстраций" ...