Магія відображень.

Опубліковано: 2009-07-09   21:26:06

JavaНіколи не замислювались, яким чином прицюють IDE, що дозволяють на льоту підхоплювати список методів та полів класу, з усіма правилами доступу та списком параметрів. Коли вперше побачив таку штуку, це здавалось фактично ідеалом. Не знаю як там в з іншими мовами програмування, а для Java є досить проста технологія, що дозволяє роботи такі цікаві штуки, як доступ до властивостей та методів будь-якого класу, там самим відкриваючи можливості легкого написання інтегрованих середовищ для розробки, різноманітних відлогоджувачів, середовищ для тестування. Сьогодні хочу розповісти про чудову можливість, що присутня в мові java - Java Reflection API.

Для того, щоб цікавіше було розбиратись з усим цим, пропоною в процесі роботи поставити собі за мету розробку такого "велосипеду" - простої автоматизованої системи тестування коду написаного на java. Звичайно ж системи простої, однак основні принципи роботи Reflection API та систем автоматизованого тестування зрозуміти можна і на невеликому прикладі

Трошки про тестування

Хочу для початку розповісти, що саме в даному випадку я буду розуміти під автоматизованим тестуванням програмного коду. Для прикладу створемо простий клас, що буде мати змогу викликати довільний метод об'єкту з парадачею параметрів та перевірки результату, який поверне цей метод. В результаті отримаємо просту бібліотеку для функціонального тестування коду на java.

Який виглад має мати наш клас (назовемо його BaseTest)? По-перше, необхідно мати саму функцію для виклику тесту:

public boolean makeTest(Object object, String methodName, Object result, Object [] args)

Першим параметром в цю функцію має бути переданий об'єкт, метод якого буде викликаний. Другим - назва методу для виклику, третім - результат, який має повернути метод, а четвертим - список параметрів з якими буде викликано метод. В результаті виклику, цей метод поверне істину, якщо результат, повернений в результаті виклику методу обраного для даного об'єкту за іменем і списком парамтрів співпадає з результатом переданим в метод-тест.

Для зручності перевантажимо цей метод для випадків, коли параметрів у методу зовсім немає, або має бути переданий лише один параметр:

public boolean makeTest(Object object, String methodName, Object result) {
return makeTest(object, methodName, result, new Object[0]);
}
public boolean makeTest(Object object, String methodName, Object result, Object arg) {
Object [] args = {arg};
return makeTest(object, methodName, result, args);
}

Одразу ж слід сказати, що значення true/false не є дуже інформативним. Цікаво було б знати, який саме результат було повернено, дізнатись про можливі помилки, що виникли в результаті виклику. Для цього необхідно додати додаткові поля:

/** Last test result */
protected Object lastResult;
/** Stack for errors during evaluation */
protected String stack;

ну і метод, що буде певним чином відображати результати виклику тесту, з урахуванням ци полів:

public void print(String method, Object result, boolean flag) {
if (flag)
// All ok
System.out.println(method+"\t\tOK");
else {
// Test failed. Print additional information
System.out.println("!\t"+method+"\t\tFAIL\n\tRequire: "+result+"\n\tReturns: "+lastResult);
if (!"".equals(stack)) {
System.out.println(stack);
stack = "";
}
}
}

Для простоти та зручності виведення відбувається просто в термінал. Тепер залишається для більшої зручності написати обгортки, що дозволять запускати тест і отримувати результати на екран одним простим рядком:

public void print(Object object, String methodName, Object result) {
print(methodName, result, makeTest(object, methodName, result));
}
public void print(Object object, String methodName, Object result, Object arg) {
print(methodName, result, makeTest(object, methodName, result, arg));
}
public void print(Object object, String methodName, Object result, Object [] args) {
print(methodName, result, makeTest(object, methodName, result, args));
}

На цьому підготовку повністю закінчено. Можно перейти до магічного обряду виклику довільного методу, для довільного об'єкту :)

Отримуємо клас об'єкту

Для того, щоб дістатись до властивостей та методів об'єкту, яким би він не був, java вимагає працювати не тільки з самим об'єктом а й з його класом. Клас кожного об'єкту мови java наслідується від спеціального класу Class. Саме цей клас дає можливсті для роботи з ополями методами довільний об'єктів.

Тепер постає питання, як же отримати клас об'єкту? В java є ціла купа методів, роботи з класом об'єкту, таких як поле class для всіх елементів (навіть примітивних типів), поле TYPE для класів, що є обгортками для примітивів (це класи Integer, Double і т.д.). А сам Class ще ряд функцій для роботи, що дозволяють працювати з цілою ієрархією класів.

Однак, ми домовились (себто я вирішив, а Ви погодились з моїм рішенням :)), що для тестування будемо передавати об'єкт, себто екземепляр якогось класу. Будь-який об'єкт має метод getClass, який повертає його клас.

Class a = System.console.getClass();
byte[] bytes = new byte[1024];
Class b = bytes.getClass();
Class c = "Some string".getClass();

Наведений вище приклад в результаті роботи створить змінні a, b та c. Змінна a отримає значення Console, а змінна c - клас String, внаслідок того, що рядкові константи також є об'єктами. Аналогічно будь-який масив - об'єкт, тому змінна b примайме значення класс Array.

Саме те, що нам потрібно. Можно почати написання нашого методу для тестування. Поки-що він набуде такого вигляду:

public boolean makeTest(Object object, String methodName, Object result, Object [] args) {
try {
// Check is object exists
if (object == null)
throw new NullPointerException("Object for testing is not exists");
// Get class for object
Class contentsClass = object.getClass();
return true;
} catch(Exception exception) {
stack = exception.getMessage()+"\n";
stack += exception.toString()+"\n";
StackTraceElement [] trace = exception.getStackTrace();
for (StackTraceElement el : trace)
stack += el.toString()+"\n";
return false;
}
}

Спочатку відбувається перевірка, чи встановлено параметр-об'єкт. Якщо ні, то генеруємо виключення з повідомленням про відсутність об'єтку для виклику методу. Якщо все нормально, то отримуємо класс об'єкту, та повертаємо true (поки що :)).

Слід звернути увагу на систему обробки виключень. Тут треба перехоплювати будь-які виключення, незалежно від типу, до додавати їх виведення в стек, з метою подальшого виведення. Таким чином, якщо виклик методу з тим чи іншим параметром в результаті роботи згенерує виключення, то необхідно про це повідомити. Зрозуміло, що коли виклик методу завершується виключенням, а не поверненням результату, то це скоріш за все не вірна робота методу, а отже слід повернути false (на практиці це далеко не завжди так, однак і клас для тестування простенький).

Робота з методами класу

Тут мова програмування java також дає потужний інструментарій для роботи - можно отримати продекларовані методи для даного класу, можна всі з правами доступу, які дозволяють дістатись до методу, включаючи методі батьківських класів. Однак нас цікавить метод getMethod, який дозволяє отримати за іменем та списком параметрів сам метод.

Для роботи з методами класів використовується клас Method, що знаходиться в пакеты java.lang.reflect, який включає також і інші цікавинки для роботи з відораженнями. Тому необхідно його також включити:

import java.lang.reflect.*;

Тепер до нашого методу-тесту можна додати наступний код:

// Get method arguments
Class [] types = new Class[args.length];
for (int i = 0; i < args.length; i++)
types[i] = args[i].getClass();
// Get method
Method method = contentsClass.getMethod(methodName, types);

Найбільша частина коду відведена побудову масиву класів для параметрів методу, який необхідний щоб отримати саме той метод, що нам потрібен (не слід забувати, що в Java присутнє перевантаження методів, тобто одного імені для точного отримання методу буде недостатньо).

Залишається викликати метол, та перевірити отриманий результат на правильність. Для виклику методу використовується метод invoke класу Method. Параметрами він приймає об'єкт, методо якого слід викликати та масив об'єктів, що будуть передані методу в якості аргументів. Для класу-тестера необхідно написати такий код:

method.setAccessible(true);
lastRes = method.invoke(object, args);

Я попередньо не пояснив призначення методу setAccessible і взагалі нічого про нього не казав, бо тут і розповідати особливо ні про що - цей метод просто змінює права видимості для об'єкту (в даному випадку методу) на такі, що роблять його доступним ззовні (якщо параметр true) чи навпаки. Його використовують для можливості доступу до приватний полів та методів класу.

Збиремо все до купи

Залишається дописати перевірку результату і зібрати все докупи:

import java.lang.reflect.Method;

/**
* Base class for all tests
*/
public class BaseTest {
/** Last test result */
protected Object lastResult;
/** Stack for errors during evaluation */
protected String stack;
/**
* Print result for test
* @param object object for testing
* @param methodName name of method for test
* @param result result we want to get
*/
public void print(Object object, String methodName, Object result) {
print(methodName, result, makeTest(object, methodName, result));
}
/**
* Print result for test
* @param object object for testing
* @param methodName name of method for test
* @param result result we want to get
* @param arg signle argument for method call
*/
public void print(Object object, String methodName, Object result, Object arg) {
print(methodName, result, makeTest(object, methodName, result, arg));
}
/**
* Print result for test
* @param object object for testing
* @param methodName name of method for test
* @param result result we want to get
* @param args arguments array for method call
*/
public void print(Object object, String methodName, Object result, Object [] args) {
print(methodName, result, makeTest(object, methodName, result, args));
}
/**
* Print information about test process
* @param methodName called method's name
* @param result result we want to get
* @param flag flag that shows, is test was succesfull
*/
public void print(String method, Object result, boolean flag) {
if (flag)
// All ok
System.out.println(method+"\t\tOK");
else {
// Test failed. Print additional information
System.out.println("!\t"+method+"\t\tFAIL\n\tRequire: "+result+"\n\tReturns: "+lastResult);
if (!"".equals(stack)) {
System.out.println(stack);
stack = "";
}
}
}
/**
* Method for making test on specified object
* @param object object for testing
* @param methodName name of emthod for calling
* @param result planned result
*/
public boolean makeTest(Object object, String methodName, Object result) {
return makeTest(object, methodName, result, new Object[0]);
}
/**
* Method for making test on specified object
* @param object object for testing
* @param methodName name of emthod for calling
* @param result planned result
* @param arg single argument for method
*/
public boolean makeTest(Object object, String methodName, Object result, Object arg) {
Object [] args = {arg};
return makeTest(object, methodName, result, args);
}
/**
* Method for making test on specified object
* @param object object for testing
* @param methodName name of emthod for calling
* @param result planned result
* @param args arguments array
*/
public boolean makeTest(Object object, String methodName, Object result, Object [] args) {
try {
// Check is object exists
if (object == null)
throw new NullPointerException("Object for testing is not exists");
// Check method name
if (methodName == null)
throw new NullPointerException("Method name can't be null");
if ("".equals(methodName))
throw new IllegalArgumentException("Method name can't be empty");
// Get class for object
Class contentsClass = object.getClass();
// Get method arguments
Class [] types = new Class[args.length];
for (int i = 0; i < args.length; i++)
types[i] = args[i].getClass();
// Get method
Method method = contentsClass.getMethod(methodName, types);
// Call method
lastResult = null;
method.setAccessible(true);
lastResult = method.invoke(object, args);
// Check result
return (lastResult == result) || (lastResult != null && result != null &&
((lastResult instanceof StringBuffer && result instanceof StringBuffer && lastResult.toString().equals(result.toString()))
|| result.equals(lastResult)));
} catch(Exception exception) {
stack = exception.getMessage()+"\n";
stack += exception.toString()+"\n";
StackTraceElement [] trace = exception.getStackTrace();
for (StackTraceElement el : trace)
stack += el.toString()+"\n";
return false;
}
}
}

Перевірка результатів включає перевірки на повну відповідність, на еквівалентність, та на перевірику елментів як рядків (у випадку, якщо це екземпляр класу StringBuffer).

Спробуємо в дії

Для перевірки, як працює система тестування, напишемо таких два класи:

public class Test extends BaseTest {
public static void main(String [] args) {
TestObject object = new TestObject();
Test test = new Test();
test.print(object, "sayHello", "Hello");
test.print(object, "say", "Hello", "Hello");
test.print(object, "say", "Hello", "World");
String [] a = {"Hello", "world"};
test.print(object, "say", "Hello,world", a);
test.print(object, "cry", "Hello, world");
}
}

class TestObject {
public String sayHello() {
return "Hello";
}
public String say(String str) {
return str;
}
public String say(String first, String second) {
return first+","+second;
}
public String cry() throws Exception {
throw new Exception("Exception here");
}
}
</div>
Однак клас тут тест, а інший підослідний. В досліджуваному класі є чотири методи: sayHello (повертає рядок "Hello"), аж цілих два say (повертає те, що йому передали, а для випадку з двома параметрами, записує їх через кому) та cry (який генерує виключення). Клас-тест, виконує п'ять тестів, три з яких мають бути успішними, один навмисно написано невырно, а останній не виконається та згенерує при цьому виключення. Запустимо тест:
<div class="code">
grandse@grandse:~/temp/test-suite$ java -cp . Test
sayHello OK
say OK
! say FAIL
Require: Hello
Returns: World
null
say OK
! cry FAIL
Require: Hello, world
Returns: null
null
java.lang.reflect.InvocationTargetException
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
java.lang.reflect.Method.invoke(Method.java:597)
BaseTest.makeTest(BaseTest.java:107)
BaseTest.makeTest(BaseTest.java:66)
BaseTest.print(BaseTest.java:18)
Test.main(Test.java:10)

Ось такий результат отримали. Три "ОК", там де це й мало бути. Зрозуміло, що рядок test.print(object, "say", "Hello", "World") мав повернути в будь-якому разі помилку, а виклик методу cry, який генерує виключення, не міг бути успішним.

Перше з багатьох..

Ну ось і все. Клас для роботи вже готовий.

Сподіваюсь написане в матеріалі є зрозумілим. Взагалі то, я показав лише невелику частину того, що може бути написане у такий спосіб мовою java. Можливості ж насправді значно ширші. Мабуть я ще повернусь до них.

А що до написаного класу, то можна їм користуватись вдосконалювати і таке інше, без яких би то не було вказівок на автора (себто мене :)). Зрозуміло, що це іграшка і не може стояти поряд з повноцінними системами тестування коду, особливо комерційними, в яких є практично все. Однак особисто я іноді користуюсь і ним для того, щоб перевірити функціонал, бо реалізація і спосіб використання доволі прості та легкі. Вважаю, що своє головне завдання на сьогодні, тобто показати що таке "магія відображень", він виконав просто чудово ;)

Дякую всім за увагу!

Теги: java , reflection
Коментарі: 0
 

Коментарі:

Додати коментар

user

email

url

text

Повідомляти про новікоментарі