Грязная магия Java
description
Transcript of Грязная магия Java
Москва, 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
Ссылки
Mockito
Jmock
EasyMock
jmh
CGlib
ASM
Алексей Шипилёв. (The Art of) (Java) Benchmarking
Gentle Introduction in JMH
Objenesis
Google Guava
54/55