Кафедра Автоматизации Систем Вычислительных Комплексов диплом

Вид материалаДиплом

Содержание


Технические требования к реализации решения
Техническое описание решения
Работа заглушек
«new» [utf8]
«call» [utf8]
«ret» [utf8]
«exc» [utf8]
«free» [utf8]
«load» [utf8]
ПЕРЕДАЧА ЗНАЧЕНИЙ (общая часть)
Данная базовая функциональность поддерживается полностью корректно (за исключением случаев, указанных ниже).
Скриншот 1 - графический интерфейс утилиты Splitter в работе
Таблица 1: замеры производительности
Достигнутые результаты и выводы
Список литературы
Подобный материал:
1   2   3
^

Технические требования к реализации решения



Реализация решения должна состоять из трёх компонент:

  1. Инфраструктура, обеспечивающая связь между разделёнными машинами, реализующая протокол взаимодействия заглушек и объектов.
  2. Преобразователь, автоматически создающий класс-заглушку из заданного класса (на этапе сборки проекта, т.е. не в run-time)
  3. Программа-менеджер, предоставляющая билд-инженеру интерфейс настройки и осуществления для разделения проекта. Она анализирует зависимости между классами, предлагает разработчику возможные границы раздела, учитывая ограничения реализованного механизма. Вызывает Преобразователь для создания заглушек, и собирает разделённые части проекта.


При реализации механизма необходимо предусмотреть его работу в ракурсе всех аспектов возможностей языка Java; Разработать решения для поддержки тех аспектов, которые могут быть поддержаны, а остальные указать в ограничениях. Ниже приведён частичный список аспектов возможностей Java, на которые следует обратить отдельное внимание:

  • Классы, наследование, создание объектов, вызов виртуальных и статических методов
  • Передача параметров в методы, конструкторы, возврат значений из методов
  • Работа с объектами системных библиотечных классов
  • Доступ к публичным полям объекта, класса
  • Массивы
  • Вложенные, анонимные классы, native-реализация
  • Многопоточность, синхронизация, wait/notify
  • Исключения
  • Сборка мусора
  • Рефлексия, сериализация



^

Техническое описание решения



Здесь приводится описание «упрощенного» механизма разделения, который был реализован в качестве иллюстрации к данной дипломной работе, но обладает рядом ограничений по части полноты, т.е. может быть корректно применен далеко не к любому проекту.


Изложение построим по следующему плану:

  • Сначала описывается, каким принципам должно подчиняться разделение проекта на две части, какие бывают заглушки, и для чего они служат (подраздел «Классы и заглушки»).
  • Затем детально описываются алгоритмы работы системы с заглушками на примере нескольких сценариев выполнения. В этом подразделе («Сценарии работы заглушек») мы не пытаемся описать все возможные сценарии выполнения, а лишь проиллюстрировать примененные в разработанном решении принципы.
  • Затем приводится полное описание разработанного протокола взаимодействия между разделенными частями (подраздел «протокол»). Основываясь на ранее описанных алгоритмах работы на примере отдельных сценариев, описание протокола позволит читателю уяснить для себя полный спектр возможностей предложенного решения.
  • Затем в подразделе «возможности и ограничения» приводится подробное рассмотрение возможностей и ограничений реализованного решения по работе с различными нетривиальными аспектами языка Java (как было потребовано в «требованиях к реализации решения»).
  • В конце раздела приводится описание подходов, с помощью которых можно доработать данный механизм, сняв большинство ограничений, тем самым существенно расширив сферу его применения.



Классы и заглушки



Назовём машину, на которой выполняется основная масса кода приложения («толстая клиентская часть»), машиной «А». Назовем машину (защищенную), на которой выполняется вынесенная с целью защиты часть кода приложения («серверная часть»), машиной «B».


Для достижения поставленной цели все Java-классы (речь идёт как о классах приложения, так и о классах JRE) делятся на три категории:


  1. «Класс (всё дерево наследования) существует только на машине А»
  2. «Класс (всё дерево наследования) существует только на машине B»
  3. «Класс (всё дерево наследования) существует на обеих машинах A и B»


(Еще можно выделить четвертую неизменную категорию - «примитивные классы java.lang.String, Integer, Double и т.п.»)


Категория 1 – это большая часть классов приложения и JRE. В частности, в обязательном порядке, классы, обеспечивающие работу GUI, работу с файловой системой, сетью. То есть классы, чей код имеет какой-либо внешний (по отношению к Java-среде) эффект. Эти классы в неизменном виде включаются в состав части приложения, подлежащей запуску на машине «A».


Категория 2 – это классы приложения, реализующие достаточно сложную и объёмную функциональность, но, в то же время, не очень критичную к производительности. Эти классы в неизменном виде включаются в состав части приложения, подлежащей запуску на машине «B».


В обоих случаях в противоположную часть приложения кладутся классы-заглушки «первого рода». То есть классы, имеющие то же имя, наследующие от класса с тем же именем, и имеющие такую же сигнатуру по методам. Но вместо реализации методов заглушка делегирует их выполнение классу на противоположной машине. Для этого каждый экземпляр заглушки хранит «ссылку» (некий идентификатор) соответствующего ей объекта настоящего класса на другой машине. Создание объекта класса-заглушки на одной машине влечёт за собой создание соответствующего объекта оригинального класса на другой. Однако, создание объекта оригинального класса на одной машине сразу не влечет создания соответствующей заглушки. Заглушка для данного объекта создаётся при первой попытке передать ссылку на объект через границу раздела между машинами (как параметр или как возвращаемое значение) метода.


Категория 3 – это, в первую очередь, самые базовые классы JRE, без которых невозможен запуск Java-машины. Кроме того, это часто используемые, требовательные к производительности, и не имеющие «внешнего» эффекта классы: содержимое пакетов java.lang, java.util, и т.п, а также, возможно, обладающие аналогичными свойствами вспомогательные классы самого приложения. Эти классы включаются в состав обеих частей. Возможно создание и полноценная объекта «настоящего» класса с любой стороны, а для обеспечения распределенной работы создаются классы-заглушки «второго рода». Заглушка «второго рода» - это класс, наследующий от заданного. Например, java.lang.Hashtable – класс, Hashtable_$stub$ extends java.lang.Hashtable – заглушка. В случае создания объекта класса «категории 3» на одной стороне, и попытки передачи ссылки на него коду на другой стороне – с той стороны создаётся и используется объект класса-заглушки.

Поскольку Java не разрешает наследование от некоторых классов, и переопределение некоторых методов (объявленных как final), в процессе разделения все классы системы (как проекта, так и классы JRE) обрабатываются специальной процедурой, принудительно снимающих со всех классов и методов флаги final. Ясно, что корректности выполнения программы это не нарушает, зато позволяет функционировать нашим заглушкам.


Категория 4 – это класс java.lang.String и классы-обертки примитивных типов (Integer, Double и т.п.). Заглушки для них не создаются, передача через раздел между машинами производится всегда по значению, средствами среды созданного решения.


^

Работа заглушек



Работоспособность и взаимодействие всей системы классов-заглушек обеспечивается классами среды Splitter Runtime (пакет splitter.runtime). Эти классы включаются в обе части программы, инициализируются в самом начале запуска приложения, устанавливают сокетное соединение клиентской («Машина А») и серверной («Машина B) частями, поддерживают коммуникацию по собственному протоколу, и через него обеспечивают взаимодействие между объектами и их заглушками с каждой стороны.

Рассмотрим один сценарий работы классов-заглушек на примере инициализации системы и затем создания одного объекта через его заглушку.


Перед началом работы приложения должна быть инициализирована среда Splitter Runtime на обеих машинах, и установлено соединение.


Для этого требуется в обеих частях проекта запустить класс SplitterMain. Сначала следует запускать часть B («серверную»), указав в единственного качестве параметра запуска номер TCP-порта, на котором следует ожидать соединения (по умолчанию – 1543). Затем следует запустить часть A («клиентскую»), указав три параметра запуска: адрес для подключения к серверной части, порт для подключение, и имя класса, содержащего настоящий метод main данного приложения.


Пример запуска разделенного приложения:

  1. На машине B: java –X:bootclasspath=“ModifiedJRE.jar” –classpath “splitrt.jar;projectPartB.jar” SplitterMain 1543
  2. На машине A: java –X:bootclasspath=“ModifiedJRE.jar” –classpath “splitrt.jar;projectPartA.jar” SplitterMain localhost 1543 com.somecompany.someproject.Main


Продемонстрируем теперь создание объекта. Пусть есть класс Klass, отнесенный при разбиении к существованию на машине B. Соответственно, на машине A существует одноименная заглушка Klass. Пусть некий код на машине A создаёт объект класса Klass и вызывает его метод calculate:


{

Klass k = new Klass();

Int answer = k.calculate();

}


При этом в системе произойдёт следующая последовательность событий:

  • Создаётся объект класса-заглушки Klass. Единственное значащее поле в этом объекте – приватное поле _ref, хранящее идентификатор соответствующего объекта на удаленной машине, пока равно нулю. Конструктор заглушки при этом выполняет ряд действий, инициирующих создание соответствующего объекта на удалённой машине. Для этого:
    • У системы запрашивается «уникальный порядковый номер удаленного вызова». В специальную хэш-таблицу заносится соответствие между этим номером, и ссылкой на текущую нить выполнения (Thread.getCurrentThread()).
    • Ожидается синхронизация на отправку данных, после чего в исходящий поток соединения с удаленной машиной отправляется команда протокола, соответствующая созданию нового объекта класса Klass, используя конструктор без параметров.
    • После этого текущая нить выполнения переводится в состояние ожидания.


  • Класс среды splitter.runtime, отвечающий за обработку входящего потока данных соединения в машине B (в отдельной нити выполнения), принимает команду, разбирает её, и инициирует новую нить выполнения, которая исполняет команду. Конкретно, она совершает следующее:
    • Создаётся новый объект класса Klass, используя конструктор без параметров.
    • Этому объекту выдаётся «уникальный порядковый номер локального объекта» в соответствии с ведущимся системой счетчиком. В специальную хэш-таблицу заносится соответствие между этим номером и ссылкой на созданный объект.
    • Ожидается синхронизация на исходящий поток соединения с машиной A, и в него направляется команда протокола, являющаяся ответом за запрос о создании объекта. Команда содержит ранее полученный в запросе «порядковый номер вызова», и порядковый номер созданного ответа.


  • Класс среды splitter.runtime, отвечающий за обработку входящего потока данных соединения в машине A (в отдельной нити выполнения), принимает команду с ответом, разбирает её.
    • По номеру вызова из хэш-таблицы извлекается находящая в режиме ожидания нить исполнения.
    • Нить выводится из режима ожидания, и ей передаётся полученный номер удалённого объекта.


  • Полученный номер записывается в поле _ref объекта-заглушки, и конструктор успешно завершается.



Протокол.



Для взаимодействия между разделенными частями системы был разработан собственный протокол над прямым сокетным соединением. Ради упрощения разработки протокол был создан потоковым контекстно-зависимым бинарным неупакованным (используются стандартные классы DataInputStream / DataOutputStream). При разработке аналогичного решения продуктивного уровня предпочтительнее перейти к более производительному чисто бинарному протоколу с кэшированием строк.


Протокол предполагает передачу каждой из сторон последовательности команд другой стороне. Одна команда описывает единичный этап взаимодействия объектов. Команда состоит из текстового наименования команды, и затем какого-то количества информационных полей (разного для различных команд). Протокол приспособлен для удобной потоковой обработки команд.


Полный перечень допустимых команд протокола:

  • NEW – запрашивает создание нового объекта. Посылка этой команды инициируется конструкторами заглушек. Создание нового объекта-заглушки на одной стороне требует немедленного создания нового объекта оригинального класса на другой стороне, что и обеспечивается данной командой. Семантика нагрузки команды:


^ «NEW» [UTF8]

идентификатор команды.

“sessionID” [Long]

идентификатор удаленного вызова.

“className” [UTF8]

полное имя класса, чей объект создается.

“paramsCount” [Int]

число параметров вызываемого конструктора.

Далее по числу paramsCount передаются параметры конструктора:

“paramType” [UTF8]

“paramValue” [в зависимости от paramType]

Подробнее о типах и значениях параметров см. ниже общее описание «передача значений».


  • CALL – запрашивает вызов метода объекта. Посылка этой команды инициируется телами методов заглушки. Семантика нагрузки команды:


^ «CALL» [UTF8]

идентификатор команды

“sessionID” [Long]

идентификатор удаленного вызова.

“className” [UTF8]

полное имя класса, чей метод вызывается создается.

“objectID” [Int]

идентификатор локального объекта, либо 0 для вызова статического метода.

“methodName” [UTF8]

имя вызываемого метода.

“paramsCount” [Int]

число параметров метода.

Далее по числу paramsCount передаются параметры метода:

“paramType” [UTF8]

“paramValue” [в зависимости от paramType]

Подробнее о типах и значениях параметров см. ниже общее описание «передача значений».


  • RET – означает возврат после выполнения удалённо инициированного конструктора или метода. Посылка этой команды осуществляется самой средой Splitter Runtime по завершению вызова удаленно запрошенного создания объекта или вызова метода. Семантика нагрузки команды:


^ «RET» [UTF8]

идентификатор команды

“sessionID” [Long]

идентификатор удаленного вызова (ранее полученный командами NEW или CALL)

“returnType” [UTF8]

“returnValue” [в зависимости от returnType]

Подробнее о типе и значении возвращаемого значения см. ниже общее описание «передача значений»


  • EXC – означает возникновение исключения при выполнении удалённо инициированного конструктора или метода. Посылка этой команды осуществляется самой средой Splitter Runtime при исключении в вызове удаленно запрошенного создания объекта или вызова метода. Семантика нагрузки команды:



^ «EXC» [UTF8]

идентификатор команды

“sessionID” [Long]

идентификатор удаленного вызова (ранее полученный командами NEW или CALL)

“excClass” [UTF8]

Класс возникшего исключения

“excObjectID ” [Long]

Идентификатор локального объекта, через который можно обратиться к исключению.

  • FREE – означает, что какие-то объекты-заглушки был подобраны Garbage Collector’ом на своей машине. Соответственно, вторая машина по получении данной команды должна удалить ссылки на соответствующие объекты из хэш-таблицы, чтобы не препятствовать сбору данных объектов, если на них не осталось других ссылок. Команда инициируется из метода finalize заглушек. Семантика нагрузки команды:


^ «FREE» [UTF8]

идентификатор команды

“numObjects” [Int]

Количество объектов, на которые освобождаются ссылки

“objectID” [Long] – по числу numObjects

идентификаторы локальных объектов, чья заглушки были подобраны сборщиком мусора.

  • LOAD – загружает динамическую библиотеку (как правило, содержит реализацию native методов). Семантика нагрузки команды:


^ «LOAD» [UTF8]

идентификатор команды

“name” [UTF8]

Путь к библиотеке, либо системное библиотечное имя

“isLibname” [Boolean]

Является ли name системным библиотечным именем (иначе name это путь)

  • SHUTDOWN – означает завершение работы приложения. Инициируется либо из метода main класса SplitterMain на машине A после завершения метода main приложения, либо из метода Runtime.exit() на любой из машин. В классе System был подменен данный метод, класс был перекомпилирован из исходного кода для поддержки данной возможности.




    ^ ПЕРЕДАЧА ЗНАЧЕНИЙ (общая часть). При передаче команд NEW, CALL, RET (см. выше) возникает необходимость передавать через границу раздела значения полей (параметров и возвращаемых значений). В предлагаемом решении для передачи значения сначала указывается его тип (в виде UTF8 строки). Тип может принимать значения:



    • “i” – примитивный тип integer
    • “l” – примитивный тип long
    • “b” – примитивный тип byte
    • “B” – примитивный тип boolean
    • “d” – примитивный тип double
    • “f” – примитивный тип float
    • “S” – строка (java.lang.String)
    • “L” + имя класса (например, Ljava.util.Vector) – ссылка на объект класса (класс существует в настоящем виде для передающей стороны, не как заглушка)
    • “R” + имя класса (например, Rjava.awt.Frame) – ссылка на объект класса, являющегося для передающей стороны заглушкой.
    • “[“ + обозначение типа – массив соответствующего типа
    • Например, “[[i” обозначает типа массива int[][]


После передачи наименования типа, в выходной поток соединения передаются данные самого значения. Для примитивных типов используются соответствующие методы класса DataOutputStream (writeInt(), writeDouble(), и т.п.). Для передачи строки используется writeUTF() с её значением.


Для передачи ссылки на объект-заглушку (тип R…) в выходной поток записывается идентификатор (_ref) этой заглушки. Принимающая сторона по этому идентификатору извлечет из хэш-таблицы соответствующий ей объект.

Если же объект не является заглушкой (тип L…) – то среда Splitter Runtime определяет числовой идентификатор объекта, который можно было бы передать. Для этого она смотрит в специальной хэш-таблице, передавался ли данный объект через границу ранее. Если передавался, то по таблице восстанавливается идентификатор, и передаётся в качестве значения. Если объект ранее не передавался, то ему присваивается новый порядковый идентификатор, о чём делается запись в хэш-таблице. Присвоенный идентификатор передаётся в качестве значения (типа long).


Принимающая сторона смотрит в специальной хэш-таблице (WeakHashmap, чтобы не мешать заглушкам быть собранными GC), существует ли уже для данного идентификатора заглушка. Если существует – используется ссылка на неё. Если не существует – то такая заглушка создаётся, используя специальный, определенный в любой заглушке, конструктор с сигнатурой (StubMethodAttribute, long). При этом, если класс с данным именем в этой части сам является заглушкой (реализует интерфейс Stub) – создаётся объект этого класса, т.е. заглушка первого рода. Если же такой класс не является заглушкой (т.е. класс существует в обеих частях проекта), то создаётся объект заглушки второго рода (ClassName_$Stub$).


Передача массивов организуется несколько сложнее. После указания типа массива передаётся количество элементов в нём (как integer). Затем последовательно передаётся значение каждого элемента. Опять же, как пара «тип-значение». Например, при передаче двумерного массива 2x2 Bar[][] , в выходной поток соединения будут записаны (в бинарной упаковке, без символов переноса строк) следующие данные:


]]Lexample.Bar

2

]Lexample.Bar

2

Lexample.Bar

43 – идентификатор локального объекта, по нему будет создана заглушка.

Lexample.Bar

44

]Lexample.Bar

2

Lexample.Bar

45

Lexample.Bar

46


Создание заглушек и модификация JRE



Для создания заглушек используется библиотека Apache BCEL (ByteCode Engineering Library) [10]. С её помощью извлекается вся необходимая информация о классе, и на основе неё формируется Java-код заглушки, который затем компилируется стандартным компилятором Javac. В целях наглядности, и для увеличения длины текста данной работы, приведём пример исходного кода простейшего класса, и его «заглушки» (первого рода):


Класс


package example;


public class Bar

{

public int performCalculation()

{

Foo foo = new Foo();

int sum = foo.getSum(foo.get1543(), foo.get1543());

return sum;

}

}


Заглушка (комментарии и indentation расставлены вручную, автогенерируемый код их не содержит)


package example;


import SplitRuntime.*;


public class Bar extends java.lang.Object implements Stub

{

private long _ref;


/*Вместо конструктора по умолчанию*/

public Bar ()

{

CallResult cr = SplitManager.createNew("example.Bar");

if (cr.exception!=null)

throw new SplitException(cr.exception);

_ref = (Long)cr.result;

}


/*Конструктор, создающий заглушку по имеющемуся идентификатору

уже существующего объекта.

Используется, когда "с той стороны" перадаётся "на эту сторону"

ссылка на объект класса Bar, созданный на той стороне.

StubMethodAttribute - пустой класс, служащий только для цели

однозначного определения данного конструктор

отлично от любых других конструкторов класса Bar*/

public Bar (StubMethodAttribute sma, long ref)

{

this._ref = ref;

}


/*Такой метод должен быть у всех заглушек. По-хорошему, следовало бы назвать его.

StubMethodAttribute в параметрах, опять, служит только для защиты от перекрытия с

одноименным методом самого класса*/

public long getRemoteRef(StubMethodAttribute sma)

{

return _ref;

}


/*SplitManager отправит удаленной машине команду FREE,

чтобы удаленный объект мог быть собран GC*/

protected void finalize()

{

SplitManager.notifyFinalized(this);

}


/*Заглушка для единственного метода класса Bar*/

public int performCalculation()

{

CallResult cr = SplitManager.call(_ref, "example.Bar", "performCalculation");

if (cr.exception!=null)

throw new SplitException(cr.exception);

return (Integer) cr.result;

}


/*Здесь и далее - заглушки для методов, унаследованных от Object*/

public void wait(long p0) throws java.lang.InterruptedException

{

CallResult cr = SplitManager.call(_ref, "java.lang.Object", "wait", "Llong", p0);

if (cr.exception!=null)

{

/*Настоящего InterruptedException быть, конечно, не может,

может быть его заглушка второго рода*/

if (cr.exception instanceof java.lang.InterruptedException)

throw (java.lang.InterruptedException) cr.exception;


/*Если это не объявленное InterruptedException - тогда это может быть только

подкласс RuntimeException (заглушка второго рода от него)*/

throw (RuntimeException)cr.exception;

}

}


public void notify()

{

CallResult cr = SplitManager.call(_ref, "java.lang.Object", "notify");

if (cr.exception!=null)

throw (RuntimeException) cr.exception;

}


public void notifyAll()

{

CallResult cr = SplitManager.call(_ref, "java.lang.Object", "notifyAll");

if (cr.exception!=null)

throw (RuntimeException) cr.exception;

}


public java.lang.String toString()

{

CallResult cr = SplitManager.call(_ref, "java.lang.Object", "toString");

if (cr.exception!=null)

throw (RuntimeException) cr.exception;

return (java.lang.String) cr.result;

}


public int hashCode()

{

CallResult cr = SplitManager.call(_ref, "java.lang.Object", "hashCode");

if (cr.exception!=null)

throw (RuntimeException) cr.exception;

return (Integer) cr.result;

}

}


Поскольку для корректного функционирования заглушки должны переопределять все методы классов, в том числе унаследованные от Object, в том числе объявленные как финальные, необходима небольшая модификация всех классов проекта и всех классов библиотеки JRE: со всех методов принудительно снимаются флаги final. Это осуществляет следующий простой код с использованием BCEL:


ClassGen cg = new ClassGen(ClassParser.parse(путь к исходному

классу));

cg.isFinal(false);

for (Method m : cg.getMethods())

m.isFinal(false);

cg.getJavaClass().dump(путь к модифицированному классу);


Кроме того, для корректной обработки завершения работы приложения, был модифицирован класс java.lang.Runtime. В начало метода exit была вставлена строчка, инициирующая посылку команды SHUTDOWN; Модификация метода exit класса System не потребовалась, так как он просто делегирует управление соответствующему методу Runtime. Также в классе java.lang.Runtime необходимо модифицировать методы load(String) и loadLibrary(String), чтобы не случилось ситуации, когда класс, имеющий native методы, находится в одной части, а вызов загрузки библиотеки – в другой части. В начало методов load и loadLibrary были вставлены строки, инициирующие посылку команды LOAD. Таким образом, загрузка любой библиотеки произойдет сразу в обеих частях проекта. Класс Runtime перекомпилируется из измененного исходного кода (как известно, исходные Java-коды всех классов JRE открыты).


В результате разделения для каждой из созданных частей проекта формируется также и архив с модифицированными для этой части классами JRE. Во-первых, из классов убраны все флаги final. Во-вторых, использован подменённый класс Runtime. В-третьих, вместо части классов (которые были отмечены в процессе разбиения) подставлены классы-заглушки.


Изначально при реализации решения использовалась стандартная Java-машина SUN JRE 1.6. Однако вскоре было обнаружено, что по непонятной мне причине машина перестаёт стартовать (выводит native runtime exception) после того, как метод notify() в классе Object перестаёт быть финальным. Я опробовал разные пути модификации класса Object – пересборка из исходного кода, вышеописанная правка с помощью BCEL, даже ручная замена флага через HEX-редактор (требуется заменить 1 байт), но ничего не помогало. Снятие final с методов wait() система переносила нормально, но с notify() подобного уже не позволяла. В результате было выбрано обходное решение: вместо SUN JRE я стал использовать альтернативную машину BEA JRockit 1.6 R27.5, с которой подобных проблем не наблюдается. Соответственно, на текущий момент существует требование, чтобы разделенные части проекта запускались из-под JVM JRockit [11].


Корректность работы (сфера применимости) решения



Рассмотрим корректность работы предлагаемого решения в отношении следующих аспектов возможностей языка Java:

  • Классы, наследование, создание объектов, вызов виртуальных и статических методов.

Решение полностью корректно поддерживает данную базовую функциональность применительно к классам, относимым к одной из двух машин. Для классов, относимых сразу к обеим машинам, есть проблема:

При создании объекта в одной части, и передаче ссылки на него в другую часть, если код в другой части вызывает метод этого объекта (на самом деле – заглушки второго рода), применяя явное (explicit) преобразование вниз (downcast) – например, ((Object)foo).toString() – это вызовет ошибочную ситуацию. К счастью, явное преобразование вниз для вспомогательных классов (а именно вспомогательные классы чаще всего будут относиться к категории «существование на обеих машинах») применяется программистами не очень часто.

  • Передача параметров в методы, конструкторы, возврат значений из методов.

^ Данная базовая функциональность поддерживается полностью корректно (за исключением случаев, указанных ниже).

  • Исключения

Доставка исключений из удаленного кода поддерживается в полной мере, доставляются как объявляемые исключения, так и runtime. Заглушки для большинства исключений – второго рода, но маловероятно чтобы это вызвало какие-то проблемы.
  • Работа с объектами системных библиотечных классов.

Данная базовая функциональность поддерживается полностью, классы системной библиотеки подлежат разделению наравне с классами проекта (могут относиться только к одной из частей, или сразу к обеим).

  • Доступ к публичным полям объекта, класса.

Это – основное ограничение предложенного решения. Действуя только созданием заглушек, невозможно перехватить доступ к публичным полям объектов. К счастью, прямой доступ к полям (без использования getters/setters) считается плохим стилем в Java и во многих проектах не применяется вовсе.

  • Массивы.

Данная функциональность поддерживается частично. Передача массивов через границу раздела возможна, но производится «по значению», что, во-первых, не соответствует обычному поведению Java (=> может нарушить корректность выполнения программы), а, во-вторых, не производительно.

  • Вложенные, анонимные классы, native-реализация.

Вложенные и анонимные классы на уровне скомпилированного проекта не отличаются принципиально от обычных классов, это просто языковая возможность. Их поддержка не вызывает сложностей. Native-реализация также не составляет проблем (учитывая, что модификацией класса Runtime – см. выше мы добились загрузки динамических библиотек в обеих частях проекта).

  • Многопоточность, синхронизация, wait/notify.

Простая многопоточность поддерживается в полной мере. Все исходящие команды синхронизируются при отправке в соединение. Обработчик приёма команд запускает поступившие запросы на вызовы методов в новых нитях. Простая работа wait/notify на объекте поддерживается: эти методы делегируются заглушкой. А когда среда Splitter Runtime, при получении соответствующего запроса на вызов, видит что это метод wait или notify, она окружает вызов метода синхронизацией на данном объекте. К сожалению, синхронизация (помимо такой примитивной) через границу раздела частей проекта пока не поддерживается.

  • Сборка мусора.

За счет аккуратной работы с глобальными hash-таблицами объектов и заглушек, перехвата финализации заглушек, и команды FREE, сборка мусора в принципе будет работать. Но мусор на одной стороне может быть собран только после того, как пройдёт финализация на другой стороне. При этом одна сторона может не знать, что на другой подходит к концу свободная память. Поэтому нормальным решением будет являться запуск System.gc() и System.runFinalization() с каждой стороны по таймеру. В таком случае сборка мусора будет нормально работать за исключением одной существенной проблемы – наличия циклических ссылок через границу раздела. Неиспользуемые объекты связанные такой циклической ссылкой не будут собраны.
  • Рефлексия

Заглушки первого рода полностью повторяют интерфейс оригинальных классов, поэтому при работе с ними средствами рефлексии никаких проблем не будет. Однако, возможны проблемы при работе средствами рефлексии с заглушками второго рода, аналогичные проблемам с explicit downcast. Впрочем, избыточное использование рефлексии сверх необходимости в любом случае не является хорошим тоном в программировании.


Пути расширения сферы применимости



Вышеупомянутые ограничения вводятся в рамках моей дипломной работы, поскольку я решил действовать только созданием заглушек. Но, теоретически, существует возможность решить почти все вышеупомянутые проблемы, взявшись за модификацию байт-кода самих оригинальных классов. Существует open-source проект Terracotta [9] – библиотека распределённого хранения объектов в оперативной памяти кластера (Distributed Shared Objects). В этом проекте вставали схожие проблемы, которые были успешно решены разработчиками c помощью модификации байт-кода классов в момент их загрузки.

Применительно к нашему решению, модификация байткода классов при загрузке может помочь решить следующие проблемы:
  • с доступом к публичным полям объекта
  • с downcasting’ом вызовов заглушек второго рода
  • с передачей массивов по ссылке, а не по значению (еще вопрос, во всех ли случаях эту проблему следует решать: выиграв в корректности поведения, мы сильно проиграем в производительности доступа к массиву. Скорее всего, такую опцию надо сделать настраиваемой).
  • с синхронизацией через границу раздела



В этом случае единственной предполагаемой сейчас серьезной проблемой останется только невозможность garbage collection’а по циклическим ссылкам через границу раздела. Возможно, эту проблему также можно решить, написав собственный GC в качестве плагина к JVM.

Функциональность и пользовательский интерфейс утилиты Splitter



Разделение Java-проекта осуществляется с использованием минималистичного графического интерфейса разработанной программы Splitter. Билд-инженер указывает пути к классам проекта и к распакованным классам библиотеки JRE, нажимает кнопку “Load classes”. Загружается информация обо всех классах, строится дерево наследования. Это дерево отображается в двух древовидных окнах. По умолчанию, классы JRE относятся к обеим машинам, классы проекта – только к машине A. Любой класс можно перемещать между машинами. После настройки желаемого разделения, указываются пути к создаваемым папкам частей проекта.




^ Скриншот 1 - графический интерфейс утилиты Splitter в работе


По нажатию кнопки GO утилита производит следующие действия:
  • Классы, отнесенные к каждой машине, копируются в папку для соответствующей машины. При этом они обрабатываются на предмет снятия всех final флагов.
  • Для классов, отнесенных только к одной машине, создаётся Java-код заглушек первого рода. Вызывается javac для компиляции заглушек.
  • Для классов, отнесенных к обеим машинам, создаётся Java-код заглушек второго рода. Вызывается javac для компиляции заглушек.
  • В обе папки помещается файл splitrt.jar – архив с классами среды Splitter Runtime, необходимыми для работы разделенной системы, и файл модифицированного класса java/lang/Runtime.class
  • Классы, скопированные в папки каждой из частей, собираются в jar-архивы:
    • rt.jar – классы JVM, отнесенные к этой машине (включая модифицированный java.lang.Runtime)
    • rt_stubs.jar – заглушки (первого и второго рода) к классам JRE
    • project.jar – классы проекта (и проектных библиотек), отнесенные к этой машине
    • project_stubs.jar – заглушки (первого и второго рода) к классам проекта
  • В обеих папках создаются cmd-файлы для запуска по умолчанию (обе части на локальной машине, для тестирования):



Для машины B: java –Xbootclasspath:”rt.jar;rt_stubs.jar” –cp “project.jar;project_stubs.jar;splitrt.jar” SplitterMain 1543

Для машины A: java –Xbootclasspath:”rt.jar;rt_stubs.jar” –cp “project.jar;project_stubs.jar;splitrt.jar” SplitterMain localhost 1543 example.Main


Производительность



Был проведен ряд экспериментов по определению производительности разработанного решения. Была создана простая программа из двух классов, которые поочередно вызывает пустые методы друг друга. Замерялись метрики «количество вызовов через границу раздела в секунду» и «задержка выполнения одного вызова». Затем в программу вносились изменения по части количества и «тяжести» передаваемых параметров и возвращаемых значений методов. Тестирование проводилось на ноутбуке класса Pentium Mobile 1.5GHz. Обе части приложения работали локально, т.е. сетевая задержка не учитывалась. При интерпретации результатов следует учитывать, что на текущий момент решение реализовано без оглядки на производительность, и может быть очень серьезно оптимизировано.


^ Таблица 1: замеры производительности

Характеристика сложности тестового примера

Кол-во вызовов в секунду

Задержка одного вызова, мс

Простые вызовы: без параметров, возвращаемое значение – ссылка на объект. Конкретно, проверялось итерирование по удаленному объекту Vector

~100

~13мс

Вызовы средней сложности: 3 ссылки, 2 строки по 50 символов в параметрах.

~70

~20мс

Работа в 4 параллельных нити. Сложные вызовы: 4 ссылки на объекты, 2 строки по 50 символов, 1 массив из 20 целых чисел и 1 массив из 20 ссылок.

~40

~50мс



^

Достигнутые результаты и выводы




Основным результатом данной работы является демонстрация возможности реализовать автоматизированное прозрачное разделение Java-программ. Практически все встающие на этом пути проблемы могут быть так или иначе решены.

После доработки до продуктивного уровня, предлагаемое решение может быть эффективно использовано для надежной, принципиально не взламываемой, защиты ПО от нелегального использования. Достижимая производительность, даже после учета сетевой задержки, не будет существенно ухудшать характеристики приложения, если грамотно определять границу раздела по классам между машинами

Приобретен опыт работы с «внутренностями» языка Java. Созданные наработки и выдвинутые идеи могут также найти применение в других проблемах IT. Например, в организации высокопроизводительных кластерных вычислений.

^

Список литературы



Книги:
  1. Аншина М., Цимбал А. А. Технологии создания распределенных систем. Для профессионалов. Спб.: Питер, 2003. 576с.
  2. Герберт Шилдт, Патрик Ноутон. Java2 в подлиннике. Минск: BHV, 2008. 1072с.
  3. Bill Venners. Inside the Java Virtual Machine. McGraw-Hill, 2000. 624с.


On-line ресурсы:
  1. Business Software Alliance. 2007 Global Software Piracy Study. [PDF] (ссылка скрыта)
  2. Trusted Computing Group. TCG Work Group Specifications. [подборка ресурсов] (https://www.trustedcomputinggroup.org/specs/)
  3. Червоная Ольга. Полный доступ к системным папкам смартфона на базе Symbian OS 9.x [HTML] (ссылка скрыта)
  4. Георгий Мешков. Google сосредоточится на веб-приложениях (новостная заметка) [HTML] (ссылка скрыта)


Проекты:
  1. Diablo 2 Close Server Project (ссылка скрыта)
  2. Terracotta (ссылка скрыта)
  3. Apache BCEL (ссылка скрыта)
  4. BEA Jrockit JVM (om/jrockit/)