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

Preview:

DESCRIPTION

Открытый семинар для студентов в компании CUSTIS (27 марта 2014). Лектор: Сергей Кошель, ведущий Java-разработчик. Аннотация: Java сложно упрекнуть в эзотеричности — это предельно утилитарный язык с низким «порогом вхождения». Однако и в нем есть место вещам, которые на первый взгляд кажутся магией. О них мы и поговорим на семинаре. Как работают перехватчики методов и mock-фреймворки? Когда не обойтись без кодогенерации и что такое intrinsic? Где найти, казалось бы, «стертую» типизацию в runtime? Как быстр reflection на самом деле и как его еще ускорить? Рассказ будет сопровождаться демонстрацией кода и примерами из практики, а в завершение мы рассмотрим несколько малоизвестных, порой опасных, а иногда и вовсе бесполезных (но интересных) трюков и приемов с Java. Видеозапись семинара: https://vimeo.com/90631772.

Citation preview

Москва, 27 марта 2014 года

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

Сергей Кошель

Ведущий Java-разработчик

О себе

Окончил физфак МГУ

5+ лет работаю в компании

5+ лет разрабатываю на Java

2/55

О компании

Проектирование, разработка и бережное

внедрение масштабных IT-систем >200 человек

>20 проектных группБольшинство

использует SCRUM

PL/SQL, C#, Java

3/55

Как работают mock-фреймворки

Насколько быстр reflection

и как его еще ускорить

Разные трюки и приемы

План

4/55

Как работают mock-фреймворки

5/55

Задача: юнит-тестирование

6/55

Знакомьтесь, GreetingService

GreetingService MessageRepository

sayHello("Мир")getMessage(locale)

"Здравствуй, %s!""Здравствуй, Мир!"

7/55

public interface GreetingService {

String sayHello(String name);

}

Пишем интерфейс

public interface MessageRepository {

String getMessage(Locale locale);

}

GreetingService

MessageRepository

8/55

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

MessageRepository messageRepository = …;

GreetingService greetingService = new GreetingServiceImpl(messageRepository);

assertEquals(greetingService.sayHello("Мир"), "Здравствуй, Мир!");

Пишем тест

Нужна «тестовая» реализация MessageRepository

10/55

MessageRepository messageRepository = new MessageRepository() {

@Override public String getMessage(Locale locale) {

if (Locale.getDefault().equals(locale)) {

return "Здравствуй, %s!";

} else {

return null;

}

}

};

Реализуем «тестовое» поведение

Многословно и невыразительно!

11/55

Когда вызывается такой-то метод

с таким-то параметром, верни такое-то значение

messageRepository.getMessage(Locale.getDefault()) => "Здравствуй, %s!"

MessageRepository messageRepository = mock(MessageRepository.class);

// when

messageRepository.getMessage(Locale.getDefault());

thenReturn("Здравствуй, %s!");

Всего лишь хотим сказать…

Хм… может, можно «записать» вызов метода,

а потом его «воспроизвести»?

12/55

Попробуем динамическое

проксирование

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

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

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

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

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

MessageRepository messageRepository = mock(MessageRepository.class);

// when

messageRepository.getMessage(Locale.getDefault());

thenReturn("Здравствуй, %s!");

final GreetingService greetingService = new GreetingServiceImpl(messageRepository);

assertEquals(greetingService.sayHello("Мир"), "Здравствуй, Мир!");

Вернемся к тесту

18/55

MessageRepository messageRepository = mock(MessageRepository.class);

when( messageRepository.getMessage(Locale.getDefault()) )

.thenReturn("Здравствуй, %s!");

final GreetingService greetingService =

new GreetingServiceImpl(messageRepository);

assertEquals(greetingService.sayHello("Мир"), "Здравствуй, Мир!");

Можно еще выразительнее

19/55

public static <T> MockBehaviorDefinition<T> when(T mockCall) {

return new MockBehaviorDefinition<>();

}

public static class MockBehaviorDefinition<T> {

public void thenReturn(T retVal) {

// реализация не изменилась

}

}

Захватываем типизацию

20/55

// создаем mock

MessageRepository messageRepository = mock(MessageRepository.class);

Еще раз по шагам

// вызываем метод и запоминаем: mock, метод и аргументы

String message = messageRepository.getMessage(Locale.getDefault());

// захватываем типизацию

MockBehaviorDefinition<String> mockBehaviorDefinition = when(message);

// программируем поведение

mockBehaviorDefinition.thenReturn("Здравствуй, %s!");

21/55

Mock-объект – фиктивная реализация интерфейса,

предназначенная для тестирования; позволяет

реализовать лишь важные в данном тесте аспекты

поведения моделируемой системы

Mock-фреймворк – библиотека, упрощающая

создание и использование mock-объектов,

позволяет программировать их поведение в виде

лаконичного DSL

Есть полноценные mock-фреймворки: Mockito,

JMock, EasyMock

Получился примитивный

mock-фреймворк

22/55

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

when( messageRepository.getMessage(Locale.getDefault()) )

.thenReturn("Здравствуй, %s!");

В Java нет языковой конструкции для понятия «вызов

метода», но, используя динамическое проксирование,

можно его выразить, то есть через синтаксис языка

ввести несвойственную ему семантику

23/55

"person.document.number"

Как можно использовать

Ошибки откладываются до стадии выполнения

Property literal – выражает свойство

(или цепочку свойств) объекта

Увы, в Java его нет, приходится

использовать строки:

24/55

Пускаем в ход магию

Compile time checking

Code completion

Refactoring friendly

Person person = root(Person.class);

… = $(person.getDocument().getNumber());

Ошибки отлавливаются на этапе компиляции

25/55

Проблема №1

java.lang.reflect.Proxy не умеет

проксировать конкретный класс, только

интерфейс…

Зато это умеет CGLib – он может

проксировать конкретный класс, если он

не final (String и primitive wrappers)

26/55

Проблема №2

У конкретных классов есть конкретные

конструкторы…

Аллоцировать объект без вызова

конструктора в Java нельзя, но если очень

хочется, то можно:

sun.misc.Unsafe.allocateInstance(Class) –

интринсик, который это умеет

Objenesis – небольшая библиотечка, которая это

умеет

27/55

Проблема №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

Найти «стертую» типизацию

import com.google.common.reflect.TypeToken;

com.google.common.reflect.TypeToken.of(PersonArray.class)

.resolveType(getMethod.getGenericReturnType());

// => ...Person

В общем случае сложная задача

Но спасибо ребятам из Google, они все уже

написали – Guava

29/55

Кофе-брейк

30/55

Насколько быстр reflection

и как его еще ускорить

31/55

Что будем мерить

public class Bean {

private String name = "The Bean";

public String getName() {

return name;

}

}

32/55

public interface FieldAccessor {

Object get(Object target);

}

public interface FieldAccessorFactory {

FieldAccessor createFieldAccessor(Field field);

}

Что будем сравнивать

33/55

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

Чем будем мерить

OpenJDK: jmh

Инструмент для написания и анализа

(микро)тестов (микро)производительности

От разработчиков OpenJDK – для разработчиков

OpenJDK (и не только)

Алексей Шипилёв

(The Art of) (Java) Benchmarking

Gentle Introduction in JMH

35/55

0 0,5 1 1,5 2

Reflection

Getter

Baseline

Промежуточные результаты

36/55

Как будем ускорять

sun.misc.Unsafe

Dynamic Code Generation

37/55

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

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

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

Байт-код, вы сказали?

Исходный код (.java)

Байт-код (.class)

Машинный код

Java-компилятор (javac)

JIT-компилятор

JVM

41/55

Байт-код, вы сказали?

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

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

0 0,5 1 1,5 2

Dynamic Code Generation

sun.misc.Unsafe

Reflection

Getter

Baseline

Результаты

44/55

На заметку: «неправильный» Reflection

Field field =target.getClass().getDeclaredField(fieldName);

field.setAccessible(true);

return field.get(target);

Дольше в ~82 раза

45/55

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

sun.misc.Unsafe и sun.reflect.MagicAccessorImpl

Использование внутреннего API делает

код непереносимым

Позволяет обойти внутренние механизмы

защиты JVM

46/55

Разные трюки и приемы

47/55

Sneaky Throw

Исключения в Java разделяются

на проверяемые (Exception)

и непроверяемые (RuntimeException)

Но это разделение существует только

на уровне языка: про него знает

Java-компилятор, но ничего не знает JVM

Остап знал, по крайней мере, четыре почти

законных способа выбросить проверяемое

исключение там, где этого делать нельзя…

48/55

Sneaky Throw: способ №1

public static void sneakyThrow(Throwable t) {

Thread.currentThread().stop(t);

}

49/55

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

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

Sneaky Throw: способ №4

public final class Unsafe {

public native void throwException(Throwable throwable);

}

52/55

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

Даже боюсь представить, где это может

понадобиться

53/55

Спасибо!

Вопросы?

Сергей Кошель

skoshel@custis.ru

55/55