[Java]简单的延迟初始化实现


本篇文章本来打算在上个月下半旬写的,不过个人状态一直都不太好,最近重新开始调整自己的技术心态,慢慢恢复,这篇也算是恢复中的一小步吧。

延迟初始化是我在上个月公司的一个项目中加入的一个技术,感觉小组对这种东西不感兴趣,所以还是放到自己的博客上来了。技术背景是业务对象是一个非常大的对象(胖领域模型),每次加载非常耗时,而且因为部分非关键服务的不稳定导致不需要这些服务数据的功能不可用。举个例子,导航页不需要用户的B信息,但是每次加载用户信息由于是在服务层(是的,传统的分层结构)加载的,无法直接控制加载的内容,所以导致在B服务失败之后,导航页不可用。

从技术角度来说,领域模型完全是保证业务执行的必要条件,但是实际情况并不是所有场景都需要这么严格的领域模型完整性的保证的,所以类似“降级”的处理方式应由而生。具体来说就是“按需加载”。实现“按需加载”的方式不仅仅只有延迟初始化一种,通过服务的参数显示地控制加载的内容也是一种方式,不过存在侵入性和严格的规约,对开发来说并不是那么方便。其实“按需加载”不是只在特定业务场景才有的需求,在ORM上,加载关联数据也是一种典型的“按需加载”。所以亦可以参考ORM在“按需加载”方面的实现。

在讲我的简化版延迟初始化之前,先大致讲下Hibernate这个ORM在按需加载上的处理。Hibernate主要支持one-to-one和one-to-many下的按需加载,Hibernate 3开始支持部分字段的按需加载(CLOB和BLOB不算在内,3之前就有支持)。实现上,部分字段的加载需要bytecode替换,one-to-many是通过改写集合的处理,one-to-one是通过代理对象,具体来说是CGLIB,因为JDK动态代理对象只支持接口。

了解这些对于我的实现有所帮助,不过除了one-to-many很快就能模仿写之外,部分字段的延迟初始化和one-to-one的处理都比较麻烦。one-to-one在查到结果为null时会比较麻烦,从调用上看应该是代理整个领域模型而不是单个延迟加载的对象,这块个人没有细究,因为我后来想想我不需要非常严格的模型字段规约,小小地修改模型理论上也不会有太大问题。而且部分字段的延迟初始化不是类似单个延迟初始化对象就可以解决的。

所以我实际上在修改了模型的字段类型后完成了一个非常简单的延迟初始化。试着比较

class Model1 {
  private String name;
  private InnerModel model;
}

class Model2 {
  private Option<String> name;
  private Option<InnerModel> model;
}

在原有字段上套了一个Option泛型。当然集合对象是不用套的,因为不会有null问题,最多空集合罢了。这个Option其实是参照了Scala的Option,而且有Some和None两个实现类,为了在Java下更好使用,我还加了OptionUtils方便处理。

public class OptionUtils {

    @SuppressWarnings("rawtypes")
    private static final None NONE = new None();

    @SuppressWarnings("unchecked")
    public static <T> Option<T> of(T value) {
        return value != null ? new Some<T>(value) : NONE;
    }

    public static <T> T get(Option<T> option) {
        return isEmpty(option) ? null : option.get();
    }

    public static <T> boolean isEmpty(Option<T> option) {
        return option == null || option.isEmpty();
    }

    @SuppressWarnings("unchecked")
    public static <T> Option<T> none() {
        return NONE;
    }
    
    public static <T> Option<T> some(T value) {
        return new Some<T>(value);
    }

}

不过重点在于我扩展了Scala的Option,增加了一个叫做FutureValue的实现类,具体实现如下

public class FutureValue<T> implements Option<T> {

    public static <T> FutureValue<T> of(Function0<T> f) {
        if (f == null) {
            throw new IllegalArgumentException("generator must not be null");
        }
        return new FutureValue<T>(f);
    }

    protected final Function0<T> f;
    protected Option<T>          underlying;

    protected FutureValue(Function0<T> f) {
        super();
        this.f = f;
    }

    protected Option<T> delegate() {
        // not thread safe here
        if (underlying == null) {
            underlying = OptionUtils.of(f.apply());
        }
        return underlying;
    }

    @Override
    public boolean isEmpty() {
        return delegate().isEmpty();
    }

    @Override
    public T get() {
        return delegate().get();
    }

    @Override
    public T getOrElse(T defaultValue) {
        return delegate().getOrElse(defaultValue);
    }

    @Override
    public String toString() {
        return "FutureValue [underlying=" + underlying + "]";
    }

}

延迟初始化的魔力在于underlying开始并没有值,只有第一次调用之后才会有,而且考虑到基本领域模型不需要跨线程使用,所以没有加并发控制。具体使用时:

Model m = new Model();
m.setFoo(new Function1<String>() {
  public String apply() {
    System.out.println("load");
    return "Hello, world!";
  }
});

通过一个回调来实现初始化。之后第一次调用会加载,第二次第三次不会再加载。

总的来说,这块并不是很复杂,核心想法还是FutureValue这个东西。如果你的模型可以小改的话,可以试试。不过个人估计也就在一般不那么严格的项目中用用吧。

最后附上整理后代码的Github地址