Безопасно вызывайте сеттер после цепочки получателей, например, foo.getX (). GetY (). SetZ (...); - PullRequest
0 голосов
/ 15 ноября 2018

Как мне безопасно вызвать сеттер после цепочки получения, например, foo.getX().getY().setZ(...);?Например, предположим, что у меня есть вложенный POJO, и я хочу иметь возможность установить поле для вложенного объекта.

Foo foo = ...
foo.getX().getY().setZ(...);

Я хочу, чтобы поведение было таким, что если X и Y не существуют, тоони создаются автоматически;в противном случае он повторно использует существующий объект.

Другими словами, я хочу, чтобы он вел себя эквивалентно

Foo foo = ...
X x = foo.getX();
if (x == null) { 
  x = new X();
  foo.setX(x);
}

Y y = x.getY();
if (y == null) {
  y = newY();
  x.setY(y);
}

y.setZ(...);

Мне интересно, есть ли хитрость, использующая отражение / функционал, которыйприближается к этому.

У меня также есть следующие ограничения:

  • Я не могу изменить ни один из классов
  • Решение должно знать только об общедоступных методах получения и установки, а не о частныхпеременные экземпляра
  • Я хочу, чтобы получатель изменял внутреннее состояние только по особому запросу;Я не хочу, чтобы x = foo.getX() изменил foo.

Ответы [ 4 ]

0 голосов
/ 17 ноября 2018

В итоге я использовал комбинацию функционала и рефлексии и попытался сделать интерфейс похожим на Java Optional.Вот пример того, как я написал бы foo.getX().getY().setZ(val);

MutableGetter.of(foo).map(Foo::getX).map(x::getY).get().setZ(val);

Это код (он все еще WIP).Я использовал рефлексию, чтобы избежать необходимости передавать установщик и конструктор

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;

import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;
import lombok.Getter;
import lombok.NonNull;

public class MutableGetter<T>
{
    private T object;

    private MutableGetter(T object)
    {
        this.object = object;
    }

    public static <T> MutableGetter<T> of(@NonNull T object)
    {
        return new MutableGetter<>(object);
    }

    public <U> MutableGetter<U> map(Function<T, U> getter)
    {
        Method getterMethod = getGetterMethod(object.getClass(), getter);
        BiConsumer<T, U> setter = getSetter(getterMethod);
        Supplier<U> defaultValue = getDefaultValue(getterMethod);

        U nextObject = getter.apply(object);
        if (nextObject == null) {
            nextObject = defaultValue.get();
            setter.accept(object, nextObject);
        }

        return new MutableGetter<>(nextObject);
    }

    public T get()
    {
        return object;
    }

    private static <U> Supplier<U> getDefaultValue(Method getterMethod)
    {
        return () -> {
            try {
                Constructor<?> constructor = getterMethod.getReturnType().getConstructor();
                constructor.setAccessible(true);
                return (U) constructor.newInstance();
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        };
    }

    private static <T, U> BiConsumer<T,U> getSetter(Method getterMethod)
    {
        return (obj, arg) -> {
            Method setterMethod = getSetterFromGetter(getterMethod);
            setterMethod.setAccessible(true);

            try {
                setterMethod.invoke(obj, arg);
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        };
    }

    private static Method getSetterFromGetter(Method getter)
    {
        if (!getter.getName().startsWith("get")) {
            throw new IllegalStateException("The getter method must start with 'get'");
        }

        String setterName = getter.getName().replaceFirst("get", "set");

        Method[] methods = getter.getDeclaringClass().getMethods();

        for (Method method: methods) {
            if (method.getName().equals(setterName)) {
                return method;
            }
        }

        throw new IllegalStateException(String.format("Couldn't find setter in class %s with name %s", getter.getDeclaringClass(), setterName));
    }

    private static <T, U> Method getGetterMethod(Class<?> clazz, Function<T, U> getter)
    {
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.setSuperclass(clazz);

        MethodRecorder methodRecorder = new MethodRecorder();

        T proxy;
        try {
            proxy = (T) proxyFactory.create(new Class<?>[0], new Object[0], methodRecorder);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }

        getter.apply(proxy);

        return methodRecorder.getLastInvokedMethod();
    }

    private static class MethodRecorder implements MethodHandler
    {
        @Getter
        private Method lastInvokedMethod;

        @Override
        public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args)
        {
            this.lastInvokedMethod = thisMethod;
            return null; // the result is ignored
        }
    }

}

Дайте мне знать, если у вас есть какие-либо предложения

0 голосов
/ 15 ноября 2018

Для начала я просто хочу упомянуть, что это, вероятно, не лучшее решение, и я уверен, что есть способы оптимизировать это.Тем не менее, я хотел попробовать свои силы в CGLIB и ObjenesisHelper снова.

Используя CGLIB и ObjenesisHelper, мы можем обернуть объект данных в прокси, который будет перехватывать методы get.Используя этот перехватчик, мы можем добавить логику, которую вы описали в своем посте.Давайте начнем с предположения, что это наши типы данных (для краткости используем lombok).

@Data class W { private X x; }
@Data class X { private Y y; }
@Data class Y { private Z z; }
@Data class Z { private int alpha; }

Наше окончательное решение можно использовать следующим образом:

public static void main(String[] args) {
    final W w = ProxyUtil.withLazyDefaults(new W());
    System.out.println(w.getX().getY().getZ().getAlpha());
}

Реализация

В настоящее время, если мы попытаемся вызвать new W().getX().getY().getZ().getAlpha(), мы получим NullPointerException при вызове getY(), поскольку getX() вернул ноль.Даже если нам удастся создать значение X по умолчанию, нам все равно потребуется значение Y по умолчанию, чтобы не получить нулевой указатель на getZ() и getAlpha() и так далее.Прокси, который мы создаем, должен быть универсальным и иметь возможность рекурсивно оборачивать его подкомпоненты.

Хорошо, так что давайте начнем.Первое, что нам нужно сделать, это создать MethodInterceptor.Всякий раз, когда какой-либо вызов попадает в наш экземпляр прокси, он выполняет логику нашего MethodInterceptor.Сначала нам нужно определить, является ли вызываемый метод геттером.Если нет, мы проигнорируем это.Во время этого вызова геттера, если значение не присутствует в наших данных, мы создадим его и обновим объект.Если значение, содержащееся в получателе, является исходным развернутым классом, мы заменим его упакованной версией.Наконец мы вернем завернутый экземпляр. Редактировать Я обновил это, чтобы не вставлять упакованные экземпляры в реальные объекты данных.Это будет менее производительным, если доступ к объекту будет несколько раз таким образом

public class ProxyUtil {
    public static <T> T withLazyDefaults(final T data) {
        final MethodInterceptor interceptor = (object, method, args, proxy) -> {
            if (method.getName().startsWith("get")) {
                final Class<?> returnType = method.getReturnType();
                Object response = method.invoke(data, args);
                if (response == null) {
                    response = returnType.newInstance();
                    data.getClass()
                        .getDeclaredMethod(
                            method.getName().replaceFirst("get", "set"),
                            returnType)
                        .invoke(data, response);
                }
                if (!returnType.isPrimitive()) {
                    response = withLazyDefaults(response);
                }
                return response;
            }
            return method.invoke(data, args);
        };
        ...

Остальная часть этого метода включает использование CGLIB и Objenisis Helper для создания экземпляра оболочки.CGLib позволит вам проксировать и классы, и интерфейсы, а ObjenesisHelper позволит вам создать экземпляр класса без необходимости вызова конструктора.См. здесь для примера CGLib и здесь для примера ObjenesisHelper .

        ...
        final Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(data.getClass());
        final Set<Class<?>> interfaces = new LinkedHashSet<>();
        if (data.getClass().isInterface()) {
            interfaces.add(data.getClass());
        }
        interfaces.addAll(Arrays.asList(data.getClass().getInterfaces()));
        enhancer.setInterfaces(interfaces.toArray(new Class[interfaces.size()]));
        enhancer.setCallbackType(interceptor.getClass());

        final Class<?> proxyClass = enhancer.createClass();
        Enhancer.registerStaticCallbacks(proxyClass, new Callback[]{interceptor});
        return (T) ObjenesisHelper.newInstance(proxyClass);
    }
}

Предостережения

  • Это не потокобезопаснооперация.
  • Отражение замедлит ваш код.
  • Для вызовов отражения необходимо добавить лучшую обработку ошибок.
  • Если класс не имеет конструктора без аргументов, это не сработает.
  • Не учитывает наследование классов данных
  • Это может быть наилучшим способом проверкисначала нет аргументов ctor / setter.
0 голосов
/ 15 ноября 2018

Используйте функциональное программирование. Создайте метод, который принимает получатель, установщик и поставщика для значения по умолчанию, который возвращает получатель, инкапсулирующий необходимую логику:

public static <T, U> Function<T, U> getOrSetDefault(
        Function<T, U> getter,
        BiConsumer<T, U> setter,
        Supplier<U> defaultValue) {

    return t -> {
        U u = getter.apply(t);
        if (u == null) {
            u = defaultValue.get();
            setter.accept(t, u);
        }
        return u;
    };
}

Затем создайте эти украшенные геттеры:

Function<Foo, X> getX = getOrSetDefault(Foo::getX, Foo::setX, X::new);
Function<X, Y> getY = getOrSetDefault(X::getY, X::setY, Y::new);

Наконец, объедините их в цепочку и примените полученную функцию, передав в качестве аргумента foo:

Foo foo = ...
getX.andThen(getY).apply(foo).setZ(...);

РЕДАКТИРОВАТЬ: Это предполагает, что оба X и Y имеют конструктор без аргументов, на который ссылаются X::new и Y::new, соответственно. Но вы можете использовать что угодно как Supplier, то есть уже созданный экземпляр или возвращаемое значение метода и т. Д.

0 голосов
/ 15 ноября 2018

TL; DR: не пытайтесь заставить функциональную Java там, где ей явно не место.


Единственный способ сделать это функционально в Java 8 без изменения каких-либо классов - это использовать Optional s и их метод .orElse(). Это очень долго и очень быстро, но это единственный способ, который действительно имеет смысл, используя функциональность, если вы хотите сделать это только в одну строку.

Optional.ofNullable(foo.getX()).orElseGet(() -> { foo.setX(new X()); return foo.getX(); }).setY(...);

Если foo.setX() также возвращает установленное значение, его можно упростить как:

Optional.ofNullable(foo.getX()).orElseGet(() -> foo.setX(new X())).setY(...);

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

Еще один вариант, даже если он не слишком функционален, - это использовать оператор tristate, однако , только если установщик возвращает установленное значение :

(foo.getX() == null ? foo.setX(new X()) : foo.getX()).setY(...);

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...