55
Москва, 27 марта 2014 года Грязная магия Java Сергей Кошель Ведущий Java-разработчик

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

  • Upload
    custis

  • View
    229

  • Download
    4

Embed Size (px)

DESCRIPTION

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

Citation preview

Page 1: Грязная магия Java

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

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

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

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

Page 2: Грязная магия Java

О себе

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

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

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

2/55

Page 3: Грязная магия Java

О компании

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

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

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

использует SCRUM

PL/SQL, C#, Java

3/55

Page 4: Грязная магия Java

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

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

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

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

План

4/55

Page 5: Грязная магия Java

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

5/55

Page 6: Грязная магия Java

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

6/55

Page 7: Грязная магия Java

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

GreetingService MessageRepository

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

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

7/55

Page 8: Грязная магия Java

public interface GreetingService {

String sayHello(String name);

}

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

public interface MessageRepository {

String getMessage(Locale locale);

}

GreetingService

MessageRepository

8/55

Page 9: Грязная магия Java

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

Page 10: Грязная магия Java

MessageRepository messageRepository = …;

GreetingService greetingService = new GreetingServiceImpl(messageRepository);

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

Пишем тест

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

10/55

Page 11: Грязная магия Java

MessageRepository messageRepository = new MessageRepository() {

@Override public String getMessage(Locale locale) {

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

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

} else {

return null;

}

}

};

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

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

11/55

Page 12: Грязная магия Java

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

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

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

MessageRepository messageRepository = mock(MessageRepository.class);

// when

messageRepository.getMessage(Locale.getDefault());

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

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

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

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

12/55

Page 13: Грязная магия Java

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

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

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

Page 14: Грязная магия Java

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

Page 15: Грязная магия Java

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

Page 16: Грязная магия Java

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

Page 17: Грязная магия Java

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

Page 18: Грязная магия Java

MessageRepository messageRepository = mock(MessageRepository.class);

// when

messageRepository.getMessage(Locale.getDefault());

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

final GreetingService greetingService = new GreetingServiceImpl(messageRepository);

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

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

18/55

Page 19: Грязная магия Java

MessageRepository messageRepository = mock(MessageRepository.class);

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

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

final GreetingService greetingService =

new GreetingServiceImpl(messageRepository);

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

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

19/55

Page 20: Грязная магия Java

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

return new MockBehaviorDefinition<>();

}

public static class MockBehaviorDefinition<T> {

public void thenReturn(T retVal) {

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

}

}

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

20/55

Page 21: Грязная магия Java

// создаем mock

MessageRepository messageRepository = mock(MessageRepository.class);

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

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

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

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

MockBehaviorDefinition<String> mockBehaviorDefinition = when(message);

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

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

21/55

Page 22: Грязная магия Java

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

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

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

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

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

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

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

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

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

JMock, EasyMock

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

mock-фреймворк

22/55

Page 23: Грязная магия Java

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

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

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

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

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

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

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

23/55

Page 24: Грязная магия Java

"person.document.number"

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

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

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

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

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

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

24/55

Page 25: Грязная магия Java

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

Compile time checking

Code completion

Refactoring friendly

Person person = root(Person.class);

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

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

25/55

Page 26: Грязная магия Java

Проблема №1

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

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

интерфейс…

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

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

не final (String и primitive wrappers)

26/55

Page 27: Грязная магия Java

Проблема №2

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

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

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

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

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

sun.misc.Unsafe.allocateInstance(Class) –

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

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

умеет

27/55

Page 28: Грязная магия Java

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

Page 29: Грязная магия Java

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

import com.google.common.reflect.TypeToken;

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

.resolveType(getMethod.getGenericReturnType());

// => ...Person

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

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

написали – Guava

29/55

Page 30: Грязная магия Java

Кофе-брейк

30/55

Page 31: Грязная магия Java

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

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

31/55

Page 32: Грязная магия Java

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

public class Bean {

private String name = "The Bean";

public String getName() {

return name;

}

}

32/55

Page 33: Грязная магия Java

public interface FieldAccessor {

Object get(Object target);

}

public interface FieldAccessorFactory {

FieldAccessor createFieldAccessor(Field field);

}

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

33/55

Page 34: Грязная магия Java

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

Page 35: Грязная магия Java

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

OpenJDK: jmh

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

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

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

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

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

(The Art of) (Java) Benchmarking

Gentle Introduction in JMH

35/55

Page 36: Грязная магия Java

0 0,5 1 1,5 2

Reflection

Getter

Baseline

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

36/55

Page 37: Грязная магия Java

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

sun.misc.Unsafe

Dynamic Code Generation

37/55

Page 38: Грязная магия Java

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

Page 39: Грязная магия Java

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

Page 40: Грязная магия Java

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

Page 41: Грязная магия Java

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

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

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

Машинный код

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

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

JVM

41/55

Page 42: Грязная магия Java

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

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

Page 43: Грязная магия Java

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

Page 44: Грязная магия Java

0 0,5 1 1,5 2

Dynamic Code Generation

sun.misc.Unsafe

Reflection

Getter

Baseline

Результаты

44/55

Page 45: Грязная магия Java

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

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

field.setAccessible(true);

return field.get(target);

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

45/55

Page 46: Грязная магия Java

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

sun.misc.Unsafe и sun.reflect.MagicAccessorImpl

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

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

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

защиты JVM

46/55

Page 47: Грязная магия Java

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

47/55

Page 48: Грязная магия Java

Sneaky Throw

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

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

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

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

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

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

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

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

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

48/55

Page 49: Грязная магия Java

Sneaky Throw: способ №1

public static void sneakyThrow(Throwable t) {

Thread.currentThread().stop(t);

}

49/55

Page 50: Грязная магия Java

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

Page 51: Грязная магия Java

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

Page 52: Грязная магия Java

Sneaky Throw: способ №4

public final class Unsafe {

public native void throwException(Throwable throwable);

}

52/55

Page 53: Грязная магия Java

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

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

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

53/55

Page 55: Грязная магия Java

Спасибо!

Вопросы?

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

[email protected]

55/55