在getter链之后安全地调用setter,例如foo.getX()。getY()。setZ(...);

时间:2018-11-15 08:26:05

标签: java reflection java-8 functional-programming

如何在getter链之后安全调用setter,例如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 :(得分:6)

使用函数式编程。创建一个接受默认值的getter,setter和provider的方法,该方法将返回封装您所需逻辑的getter:

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(...);

编辑:这假设XY都具有一个分别由X::newY::new引用的无参数构造函数。但是您可以使用任何东西作为Supplier,即已经创建的实例,或方法的返回值,等等。

答案 1 :(得分:3)

TL; DR:不要试图在显然没有地方的地方强制使用Java。


在Java 8 中无需修改任何类即可在功能上实现此目的的唯一方法是使用i及其Optional方法。它真的很长很长,但是很快,但这是只有在一行中才使用功能才有意义的唯一方法。

.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(...);

如果找到了元素,这可能会带来不必要的副作用,即两次调用getter,您可能不喜欢这种副作用,但是如果getter以某种方式缓存了值,则可能会被忽略。

答案 2 :(得分:2)

首先,我只想提一下这可能不是最佳解决方案,而且我敢肯定有优化此方法的方法。也就是说,我想再次尝试使用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()返回了null。即使我们设法产生一个默认的X值,我们仍将需要一个默认的Y值才能在getZ()getAlpha()上不获取空指针,依此类推。我们创建的代理需要通用,并且能够递归包装其子组件。

好的,让我们开始吧。我们需要做的第一件事是创建一个MethodInterceptor。每当任何呼叫到达我们的代理实例时,它将执行MethodInterceptor的逻辑。我们首先需要确定所调用的方法是否是吸气剂。如果没有,我们将忽略它。在此getter调用期间,如果在我们的数据中不存在该值,我们将创建它并更新该对象。如果getter包含的值是原始的未包装类,我们将用包装的版本替换它。最后,我们将返回包装的实例。 编辑,我对此进行了更新,以不将包装实例插入到实际的Data对象中。如果以这种方式多次访问对象,则性能会降低

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将允许您构造类的实例而无需调用构造函数。参见here for a CGLib examplehere for a ObjenesisHelper example

        ...
        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);
    }
}

注意事项

  • 这不是线程安全操作。
  • 反射会降低您的代码速度。
  • 需要为反射调用添加更好的错误处理。
  • 如果类没有no-arg构造函数,则将无法使用。
  • 不考虑数据类的继承
  • 首先检查无参数的控制器/设置器,这可能是最大的努力。

答案 3 :(得分:1)

我最终结合使用了功能和反射,并试图使该接口类似于Java的Optional。这是我写foo.getX().getY().setZ(val);

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

这是代码(仍为WIP)。 我使用反射来避免必须传递setter和构造函数

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
        }
    }

}

如果您有任何建议,请告诉我