Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Грязная магия Java

CUSTIS
March 27, 2014

Грязная магия Java

Открытый семинар для студентов в компании CUSTIS (27 марта 2014 года).
Лектор: Сергей Кошель, ведущий Java-разработчик.

CUSTIS

March 27, 2014
Tweet

More Decks by CUSTIS

Other Decks in Programming

Transcript

  1. О себе  Окончил физфак МГУ  5+ лет работаю

    в компании  5+ лет разрабатываю на Java 2/55
  2. О компании Проектирование, разработка и бережное внедрение масштабных IT-систем >200

    человек >20 проектных групп Большинство использует SCRUM PL/SQL, C#, Java 3/55
  3.  Как работают mock-фреймворки  Насколько быстр reflection и как

    его еще ускорить  Разные трюки и приемы План 4/55
  4. public interface GreetingService { String sayHello(String name); } Пишем интерфейс

    public interface MessageRepository { String getMessage(Locale locale); } GreetingService MessageRepository 8/55
  5. public class GreetingServiceImpl implements GreetingService { private final MessageRepository messageRepository;

    public GreetingServiceImpl(MessageRepository messageRepository) {…} @Override public String sayHello(String name) { final Locale locale = Locale.getDefault(); final String message = messageRepository.getMessage(locale); return String.format(message, name); } } Пишем реализацию* * Её и будем тестировать 9/55
  6. MessageRepository messageRepository = …; GreetingService greetingService = new GreetingServiceImpl(messageRepository); assertEquals(greetingService.sayHello("Мир"),

    "Здравствуй, Мир!"); Пишем тест Нужна «тестовая» реализация MessageRepository 10/55
  7. MessageRepository messageRepository = new MessageRepository() { @Override public String getMessage(Locale

    locale) { if (Locale.getDefault().equals(locale)) { return "Здравствуй, %s!"; } else { return null; } } }; Реализуем «тестовое» поведение Многословно и невыразительно! 11/55
  8.  Когда вызывается такой-то метод с таким-то параметром, верни такое-то

    значение messageRepository.getMessage(Locale.getDefault()) => "Здравствуй, %s!" MessageRepository messageRepository = mock(MessageRepository.class); // when messageRepository.getMessage(Locale.getDefault()); thenReturn("Здравствуй, %s!"); Всего лишь хотим сказать… Хм… может, можно «записать» вызов метода, а потом его «воспроизвести»? 12/55
  9. Попробуем динамическое проксирование Object proxy = java.lang.reflect.Proxy.newProxyInstance( classLoader, new Class[]{MessageRepository.class},

    new InvocationHandler() {…}); public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; } Все вызовы направляются сюда 13/55
  10. public static <T> T mock(Class<T> iface) { ClassLoader classLoaderToUse =

    iface.getClassLoader(); Object proxy = Proxy.newProxyInstance(classLoaderToUse, new Class[]{iface}, new MockInvocationHandler()); return (T) proxy; } Создаем mock 14/55
  11. private MockBehavior behavior; @Override public Object invoke(Object mock, Method method,

    Object[] args) … { final MethodCall methodCall = new MethodCall(mock, method, args); lastMethodCallThreadLocal.set(methodCall); } «Записываем» вызов 15/55
  12. public static void thenReturn(Object retVal) { MethodCall methodCall = lastMethodCallThreadLocal.get();

    Object mock = methodCall.getMock(); MockInvocationHandler mockInvocationHandler = (MockInvocationHandler) Proxy.getInvocationHandler(mock); mockInvocationHandler.setBehavior(new MockBehavior(methodCall, retVal)); } Задаем возвращаемое значение 16/55
  13. private MockBehavior behavior; @Override public Object invoke(Object mock, Method method,

    Object[] args) … { final MethodCall methodCall = new MethodCall(mock, method, args); lastMethodCallThreadLocal.set(methodCall); if (behavior != null) { if (behavior.getMethodCall().equals(methodCall)) { return behavior.getRetVal(); } } return null; } «Воспроизводим» вызов 17/55
  14. MessageRepository messageRepository = mock(MessageRepository.class); // when messageRepository.getMessage(Locale.getDefault()); thenReturn("Здравствуй, %s!"); final

    GreetingService greetingService = new GreetingServiceImpl(messageRepository); assertEquals(greetingService.sayHello("Мир"), "Здравствуй, Мир!"); Вернемся к тесту 18/55
  15. MessageRepository messageRepository = mock(MessageRepository.class); when( messageRepository.getMessage(Locale.getDefault()) ) .thenReturn("Здравствуй, %s!"); final

    GreetingService greetingService = new GreetingServiceImpl(messageRepository); assertEquals(greetingService.sayHello("Мир"), "Здравствуй, Мир!"); Можно еще выразительнее 19/55
  16. public static <T> MockBehaviorDefinition<T> when(T mockCall) { return new MockBehaviorDefinition<>();

    } public static class MockBehaviorDefinition<T> { public void thenReturn(T retVal) { // реализация не изменилась } } Захватываем типизацию 20/55
  17. // создаем mock MessageRepository messageRepository = mock(MessageRepository.class); Еще раз по

    шагам // вызываем метод и запоминаем: mock, метод и аргументы String message = messageRepository.getMessage(Locale.getDefault()); // захватываем типизацию MockBehaviorDefinition<String> mockBehaviorDefinition = when(message); // программируем поведение mockBehaviorDefinition.thenReturn("Здравствуй, %s!"); 21/55
  18.  Mock-объект – фиктивная реализация интерфейса, предназначенная для тестирования; позволяет

    реализовать лишь важные в данном тесте аспекты поведения моделируемой системы  Mock-фреймворк – библиотека, упрощающая создание и использование mock-объектов, позволяет программировать их поведение в виде лаконичного DSL  Есть полноценные mock-фреймворки: Mockito, JMock, EasyMock Получился примитивный mock-фреймворк 22/55
  19. Грязная магия when( messageRepository.getMessage(Locale.getDefault()) ) .thenReturn("Здравствуй, %s!"); В Java нет

    языковой конструкции для понятия «вызов метода», но, используя динамическое проксирование, можно его выразить, то есть через синтаксис языка ввести несвойственную ему семантику 23/55
  20. "person.document.number" Как можно использовать Ошибки откладываются до стадии выполнения 

     Property literal – выражает свойство (или цепочку свойств) объекта  Увы, в Java его нет, приходится использовать строки: 24/55
  21. Пускаем в ход магию  Compile time checking  Code

    completion  Refactoring friendly Person person = root(Person.class); … = $(person.getDocument().getNumber()); Ошибки отлавливаются на этапе компиляции  25/55
  22. Проблема №1  java.lang.reflect.Proxy не умеет проксировать конкретный класс, только

    интерфейс…  Зато это умеет CGLib – он может проксировать конкретный класс, если он не final (String и primitive wrappers) 26/55
  23. Проблема №2  У конкретных классов есть конкретные конструкторы… 

    Аллоцировать объект без вызова конструктора в Java нельзя, но если очень хочется, то можно:  sun.misc.Unsafe.allocateInstance(Class) – интринсик, который это умеет  Objenesis – небольшая библиотечка, которая это умеет 27/55
  24. Проблема №3 class PersonArray extends ArrayList<Person> {}; Method getMethod =

    PersonArray.class.getMethod("get", new Class[]{int.class}); getMethod.getReturnType() // => class java.lang.Object getMethod.getGenericReturnType() // => E Type genericSuperclass = PersonArray.class.getGenericSuperclass(); ((ParameterizedType) genericSuperclass).getActualTypeArguments() // => [class ...Person] Type erasure! Формальный параметр типа Фактический параметр типа 28/55
  25. Найти «стертую» типизацию import com.google.common.reflect.TypeToken; com.google.common.reflect.TypeToken.of(PersonArray.class) .resolveType(getMethod.getGenericReturnType()); // => ...Person

     В общем случае сложная задача  Но спасибо ребятам из Google, они все уже написали – Guava 29/55
  26. Что будем мерить public class Bean { private String name

    = "The Bean"; public String getName() { return name; } } 32/55
  27. public interface FieldAccessor { Object get(Object target); } public interface

    FieldAccessorFactory { FieldAccessor createFieldAccessor(Field field); } Что будем сравнивать 33/55
  28. public class ReflectFieldAccessor implements FieldAccessor { … @Override public Object

    get(Object target) { try { return field.get(target); } catch (IllegalAccessException x) { throw new RuntimeException("IllegalAccessException", x); } } } Java reflection 34/55
  29. Чем будем мерить OpenJDK: jmh  Инструмент для написания и

    анализа (микро)тестов (микро)производительности  От разработчиков OpenJDK – для разработчиков OpenJDK (и не только)  Алексей Шипилёв (The Art of) (Java) Benchmarking Gentle Introduction in JMH 35/55
  30. sun.misc.Unsafe public native int getInt(java.lang.Object o, long l); public native

    void putInt(java.lang.Object o, long l, int i); public native java.lang.Object getObject(java.lang.Object o, long l); public native void putObject(java.lang.Object o, long l, java.lang.Object o1); // etc. Это не «натив», это «интринсик» (intrinsic) – метод, реализация которого будет подставлена JIT-компилятором 38/55
  31. UnsafeFieldAccessor final Field field = … // получаем Unsafe final

    Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafeField.setAccessible(true); final Unsafe unsafe = (Unsafe) theUnsafeField.get(null); // получаем смещение поля внутри объекта final long offset = unsafe.objectFieldOffset(field); // используем return unsafe.getObject(target, offset); 39/55
  32. Dynamic Code Generation  Идея в том, чтобы в runtime

    собрать из байт-кода следующую реализацию: public class CodeGenFieldAccessor implements FieldAccessor { @Override public Object get(Object target) { return ((Bean) target).name; } }  Имея конкретный field, получить из него:  тип target – Bean  имя поля – ‘name’  Используем ASM 40/55
  33. Байт-код, вы сказали? public java.lang.String getName(); Code: 0: aload_0 1:

    getfield #3 // Field name:Ljava/lang/String; 4: areturn public String getName() { return name; } } Исходный код Байт-код 42/55
  34. CodeGenFieldAccessor  Увы, так работать не будет, потому что поле

    – private и JVM это проверит  Наследуемся от sun.reflect.MagicAccessorImpl public class CodeGenFieldAccessor extends sun.reflect.MagicAccessorImpl implements FieldAccessor { @Override public Object get(Object target) { return ((Bean) target).name; } } 43/55
  35. 0 0,5 1 1,5 2 Dynamic Code Generation sun.misc.Unsafe Reflection

    Getter Baseline Результаты 44/55
  36. Грязная магия sun.misc.Unsafe и sun.reflect.MagicAccessorImpl  Использование внутреннего API делает

    код непереносимым  Позволяет обойти внутренние механизмы защиты JVM 46/55
  37. Sneaky Throw  Исключения в Java разделяются на проверяемые (Exception)

    и непроверяемые (RuntimeException)  Но это разделение существует только на уровне языка: про него знает Java-компилятор, но ничего не знает JVM  Остап знал, по крайней мере, четыре почти законных способа выбросить проверяемое исключение там, где этого делать нельзя… 48/55
  38. Sneaky Throw: способ №2 public class Thrower { private static

    Throwable t; private Thrower() throws Throwable { throw t; } public static synchronized void sneakyThrow(Throwable t) { Thrower.t = t; try { Thrower.class.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } finally { Thrower.t = null; // Avoid memory leak } } } 50/55
  39. Sneaky Throw: способ №3 class TigerThrower<T extends Throwable> { public

    static void sneakyThrow(Throwable t) { new TigerThrower<Error>().sneakyThrow2(t); } private void sneakyThrow2(Throwable t) throws T { throw (T) t; } } 51/55
  40. Sneaky Throw: способ №4 public final class Unsafe { public

    native void throwException(Throwable throwable); } 52/55
  41. Ссылки  Mockito  Jmock  EasyMock  jmh 

    CGlib  ASM  Алексей Шипилёв. (The Art of) (Java) Benchmarking Gentle Introduction in JMH  Objenesis  Google Guava 54/55