О грустном
Сидел я вчера на очередном интервью, грустил, что javax в меня какашками кидается, и слушал печальную историю соискателя о том, как он мучался пытаясь прикрутить сериализацию в JSON к модели на Java не имея ее исходников. От вида его попыток настроение мое не улучшилось.
Мы попробуем лучше, потому что, в отличие от него, мы знаем про Groovy.
Этот пост является более-менее продолжением вчерашнего. Им хотелось бы убить мешок зайцев:
- Показать реальный пример мета-программирования на Groovy
- Показать немного более навороченный способ работы с мета-классами
- Показать работу с json-lib в Groovy
- Рассказать про dependency management для бедных
Что мы имеем с гуся Java
Имеем мы модель из JavaBean-ов написаную на Java, и для примера допустим, что сорцов у нас нет. Также для интересу предположим что у этой модели нет общего интерфейса и/или суперкласса, и все что эту модель объеденяет это package, так что забудьте про всякий полиморфизм. Для примера допустим, что в модели аж 2 класса — model.Clock и model.Radio, оба extend Object, и у них есть парочка полей — стринги и примитивы.
Чего надо-то?
Вот тест, который будет проходить к концу этой статьи:
import model.Radio
import service.Service
Radio radio = new Radio()
radio.frequency = 91.2
radio.volume = 20
def json = radio.toJson()
assert json == '{"frequency":91.2,"volume":20}'
Radio radioClone = Radio.parseJson(json)
assert radioClone == radio
try {
new Service().toJson()
assert false
} catch (MissingMethodException e){}
Естественно, ни метода toJson(), ни метода parseJson() в классе Radio не существует, и поэтому наш тест падает с «groovy.lang.MissingMethodException: No signature of method: model.Radio.toJson() is applicable for argument types: () values: []».
Проверка с try в конце — у класса Service, не относящегося к package-у model не должены отрости методы toJson и parseJson в результате нашего шаманства.
Кто виноват Что делать?
Как вы, конечно, уже знаете (как минимум со вчерашнего дня) что с помощью MetaClass-а любого класса можно добавлять и заменять методы. Значит, нам нужно добавить методы toJson() и parseJson() в MetaClass-ы всех классов из package model. А как? В Java с этим все плохо — нельзя получить список классов у package. Ну, то есть можно, но не совсем, не всегда, только из jar-ов, и еще куча ограничений и простыня кода. Это все не наш метод. Мы — Groovy.
Groovy совершенно точно вклинивается в classloading. Например, для создания MetaClass-ов. И раз уж мы все равно собрались с ними пошаманить, мы можем вклиниться в процесс их создания!
В классе groovy.lang.MetaClassRegistry смотрим на код создания Handler-a, который занимается созданием MetaClass-ов (я слегка код упростил, но смысл вот):
try {
Class customHandle = Class.forName("groovy.runtime.metaclass.CustomMetaClassCreationHandle");
this.handle = customHandle.newInstance();
} catch (ClassNotFoundException e) { //нету custom handler, будем использовать стандартный
this.metaClassCreationHandle = new MetaClassCreationHandle();
}
— Шерлок, но что это?
— Это элементарно, Ватсон. Это — один из extension points, позволяющий нам написать наш собственный MetaClassCreationHandle, в котором мы будем создавать наши собственные MetaClassы, с новыми методами и джейсоном.
Тут все ясно, Groovy ищет класс под названием «groovy.runtime.metaclass.CustomMetaClassCreationHandle», и мы ему его дадим:
public class CustomMetaClassCreationHandle extends MetaClassRegistry.MetaClassCreationHandle {
protected MetaClass createNormalMetaClass(Class theClass, MetaClassRegistry registry) {
Package thePackage = theClass.getPackage();
if (thePackage != null && thePackage.getName().equals("model")) {
return new Jsonizer(theClass);
} else {
return super.createNormalMetaClass(theClass, registry);
}
}
}
Если package наш — даем свой MetaClass, если нет — родной. Я считаю — зачет.
Обратите внимание на дурацкие точки-с-запятой. Все верно, CustomMetaClassCreationHandle обязан быть классом Java, не Groovy, потому как тут курица и яйцо — для создания Groovy класса нужен уже созданный MetaClassCreationHandle.
Все что нам осталось, это написать наш собственый MetaClass, в котором мы добавим один обычный (instance) и один static метод.
Тут, наверное надо набросать небольшой план работ:
- Наследовать от ExpandoMetaClass (это тот, в который можно добавлять методы)
- Добавить метод toJson, в котором Json-lib-ом сериализовать this в json string
- Добавить статический метод parseJson в в котором Json-lib-ом десериализовать json string в объект
А вот вам список вопросов и ответов по плану работ (походу, я люблю списки):
- B: json-lib это стороняя библиотека! Где и как брать?
O: Gradle, Maven или Ivy вам в помощь. Overkill? Groovy умеет делать dependency management для бедных — Grapes. Он неплох для таких забав как эта (в нем можно даже менять откуда брать артифакты, что я и сделал). - B: this? Какой нафиг this, когда мы находимся в замыкании в ExpandoMetaClass?
O: Groovy подменяет delegate этого замыкания на нужный нам объект. #спасибоgroovyзаэто. - B: А как добавлять статический метод?
O: Очень, очень странной конструкцией, использующей «static» keyword нетрадиционным способом. Error parser в IntelliJ IDEA негодуэ.
Смотрим:
(нововеденная поддержка «собачки» для маркировки юзеров ломает код объявления аннотаций, так что там «а» вместо сами знаете чего. Ну, вы поняли.)
аgroovy.lang.Grapes([
аGrabResolver(name = 'rjo', root = 'http://repo.jfrog.org/artifactory/libs-releases'),
аGrab(group = 'net.sf.json-lib', module = 'json-lib', version = '2.4', classifier = 'jdk15')])
class Jsonizer extends ExpandoMetaClass {
Jsonizer(Class theClass) {
super(theClass)
toJson << {->
fromObject(delegate).toString()
}
static.parseJson << {String json ->
toBean(fromObject(json), theClass);
}
}
}
Ну, в общем-то profit. Тест проходит, у Clock и Radio отросли методы, у Service — нет, а вы, я надеюсь, получили удовольствие от процесса.
Выводы тоже надо, а то.
- Если вы думаете «а как бы мне тут прикрутить, не трогая то, что уже есть, и/или чтобы работало ВЕЗДЕ», скорее всего вы думаете о мета-программировании.
- Мета-программирование в Java лучше всего делать в Groovy.
- В Groovy есть очень много extension points, ищите, и скорее всего найдете (а если не найдете, откройте фичу в JIRA, и в следущюей версии точно найдете).
Автор: jbaruch