Как нам отджейсонить недоступную модель

в 14:48, , рубрики: groovy, java, json, metaclass, metaprogramming, метки: , , , ,

О грустном

Сидел я вчера на очередном интервью, грустил, что javax в меня какашками кидается, и слушал печальную историю соискателя о том, как он мучался пытаясь прикрутить сериализацию в JSON к модели на Java не имея ее исходников. От вида его попыток настроение мое не улучшилось.
Мы попробуем лучше, потому что, в отличие от него, мы знаем про Groovy.
Этот пост является более-менее продолжением вчерашнего. Им хотелось бы убить мешок зайцев:

  1. Показать реальный пример мета-программирования на Groovy
  2. Показать немного более навороченный способ работы с мета-классами
  3. Показать работу с json-lib в Groovy
  4. Рассказать про 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 метод.
Тут, наверное надо набросать небольшой план работ:

  1. Наследовать от ExpandoMetaClass (это тот, в который можно добавлять методы)
  2. Добавить метод toJson, в котором Json-lib-ом сериализовать this в json string
  3. Добавить статический метод 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 — нет, а вы, я надеюсь, получили удовольствие от процесса.

Выводы тоже надо, а то.

  1. Если вы думаете «а как бы мне тут прикрутить, не трогая то, что уже есть, и/или чтобы работало ВЕЗДЕ», скорее всего вы думаете о мета-программировании.
  2. Мета-программирование в Java лучше всего делать в Groovy.
  3. В Groovy есть очень много extension points, ищите, и скорее всего найдете (а если не найдете, откройте фичу в JIRA, и в следущюей версии точно найдете).

Автор: jbaruch

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js