Factory from generic Cache adapter with class pointer using FactoryBuilder

时间:2017-07-10 15:25:08

标签: java generics caching factory ignite

I am trying to provide a generic javax.cache compliant adapter class to the javax.cache.configuration.FactoryBuilder to retrieve a factory which is then used by ignite to instantiate a cache. The described problem may use Apache Ignite, however, I believe it's not necessarily related to Ignite but more to how generics and closures work in Java.

The Ignite CacheStoreAdapter interface is inherited from javax.cache.CacheLoader and javax.cache.CacheWriter and I am providing an adapter implementation. The implementation requires two (generic) types for cache key and value, as well as the value class reference to be able to instantiate the value in the adapter. See partial class for MyCacheAdapter below.

public class MyCacheAdapter<K,V extends StorableModel> extends CacheStoreAdapter<K,V> implements LifecycleAware {
    private final Class<V> valueClazz;
    public MyCacheAdapter(Class<V> valueClass) {
        this.valueClazz = valueClass;
    }
    @Override
    public V load(K key) throws CacheLoaderException {
        // load from database
        return valueClazz.newInstance(); // dummy instantiation
    }
    @Override
    public void write(Cache.Entry<? extends K, ? extends V> entry) throws CacheWriterException {
        // write to database
    }
}

Now when I explicitly declare an adapter and provide it to the FactoryBuilder everything works fine...

public class MyPersonAdapter extends MyCacheAdapter<String,Person> {
    public MyPersonAdapter() {
        super(Person.class);
    }
}

... when cache is instantiated in my service starter.

// run init cache (on 1st node only)
public <K,V extends StorableModel> void init() {
    CacheConfiguration cacheConfiguration = new CacheConfiguration<K,V>();
    cacheConfiguration.setCacheStoreFactory(FactoryBuilder.factoryOf(MyPersonAdapter.class));
    // add node filter to prevent other nodes from failing on cache distribution
    cacheConfiguration.setNodeFilter((node) -> { // exclude 2nd node });
    ignite.getOrCreateCache(cacheConfiguration);
}

Working example so far! Now I don't want to explicitly declare MyPersonAdapter (and dozens more) but instead, let my starter take care of the adapter specifics based on provided type and key/value classes. So I can either provide my own factory...

public static class AdapterFactory<K,V extends StorableModel> implements Factory<CacheStore<? super K, ? super V>> {
    private final Class<V> valueClass;
    public AdapterFactory(Class<V> valueClass) {
        this.valueClass = valueClass;
    }
    @Override public CacheStore<? super K, ? super V> create() {
        return new MyCacheAdapter<K,V>(valueClass);
    }
}

... which is then used on cache initialization like this:

// run init cache (on 1st node only)
public <K,V extends StorableModel> void init(Class<V> valueClass) {
    CacheConfiguration cacheConfiguration = new CacheConfiguration<K,V>();
    cacheConfiguration.setCacheStoreFactory(new AdapterFactory<K,V>(valueClass));
    // add node filter to prevent other nodes from failing on cache distribution
    cacheConfiguration.setNodeFilter((node) -> { // exclude 2nd node });
    ignite.getOrCreateCache(cacheConfiguration);
}

But this throws me java.lang.ClassNotFoundException for the value class on my 2nd Ignite node since the class is not in the 2nd node's classpath. I definitely do not want to provide that class and I am asking myself what's the difference to the first implementation. I understand that the factory does create an instance when needed and to that time (as a cache is distributed to the other node) it has to know the value class which it doesn't. So I tried another implementation to get closer to the first (working) one. Instead of providing my own Factory I wanted to have my explicit adapter declared right before initialization as nested class:

public <K,V extends StorableModel> void init(Class<V> valueClass) {
    class DynamicAdapter extends MyCacheAdapter<K,V> {
        public DynamicAdapter() {
            super(valueClass);
        }
    }
    CacheConfiguration cacheConfiguration = new CacheConfiguration<K,V>();
    cacheConfiguration.setCacheStoreFactory(FactoryBuilder.factoryOf(DynamicAdapter.class));
    // add node filter to prevent other nodes from failing on cache distribution
    cacheConfiguration.setNodeFilter((node) -> { // exclude 2nd node });
    ignite.getOrCreateCache(cacheConfiguration);
}

This again throws me an InstationException since the class is out of scope for the factory I guess.

    java.lang.RuntimeException: Failed to create an instance of DynamicAdapter
    Caused by: java.lang.InstantiationException: DynamicAdapter
    Caused by: java.lang.NoSuchMethodException: DynamicAdapter.<init>()

So, I am wondering if there's a way to achieve my goal without a custom factory and class distribution across services (which is no option) but still having some dynamic adapter declaration.

UPDATE

Stacktrace for InstantiationException when class is returned by a static method

private static <X,Y extends StorableModel> Class getAdapterClass(Class<Y> valueClass) {
    class MyClass extends MongoIgniteCacheAdapter<X,Y> {
        MyClass() {
            super(valueClass);
        }
    }
    return MyClass.class;
}

// run init cache (on 1st node only)
public <K,V extends StorableModel> void init() {
    CacheConfiguration cacheConfiguration = new CacheConfiguration<K,V>();
    cacheConfiguration.setCacheStoreFactory(FactoryBuilder.factoryOf(getAdapterClass(valueClass)));
    // add node filter to prevent other nodes from failing on cache distribution
    cacheConfiguration.setNodeFilter((node) -> { // exclude 2nd node });
    ignite.getOrCreateCache(cacheConfiguration);
}

// output

class org.apache.ignite.IgniteCheckedException: Failed to create an instance of IgniteServiceStarter$1MyClass
at org.apache.ignite.internal.util.IgniteUtils.cast(IgniteUtils.java:7242)
at org.apache.ignite.internal.util.future.GridFutureAdapter.resolve(GridFutureAdapter.java:258)
at org.apache.ignite.internal.util.future.GridFutureAdapter.get0(GridFutureAdapter.java:206)
at org.apache.ignite.internal.util.future.GridFutureAdapter.get(GridFutureAdapter.java:158)
at org.apache.ignite.internal.processors.cache.GridCachePartitionExchangeManager$ExchangeWorker.body(GridCachePartitionExchangeManager.java:1812)
at org.apache.ignite.internal.util.worker.GridWorker.run(GridWorker.java:110)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.RuntimeException: Failed to create an instance of IgniteServiceStarter$1MyClass
at javax.cache.configuration.FactoryBuilder$ClassFactory.create(FactoryBuilder.java:134)
at org.apache.ignite.internal.processors.cache.GridCacheProcessor.createCache(GridCacheProcessor.java:1458)
at org.apache.ignite.internal.processors.cache.GridCacheProcessor.prepareCacheStart(GridCacheProcessor.java:1931)
at org.apache.ignite.internal.processors.cache.GridCacheProcessor.prepareCacheStart(GridCacheProcessor.java:1833)
at org.apache.ignite.internal.processors.cache.CacheAffinitySharedManager.onCacheChangeRequest(CacheAffinitySharedManager.java:379)
at org.apache.ignite.internal.processors.cache.distributed.dht.preloader.GridDhtPartitionsExchangeFuture.onCacheChangeRequest(GridDhtPartitionsExchangeFuture.java:688)
at org.apache.ignite.internal.processors.cache.distributed.dht.preloader.GridDhtPartitionsExchangeFuture.init(GridDhtPartitionsExchangeFuture.java:529)
at org.apache.ignite.internal.processors.cache.GridCachePartitionExchangeManager$ExchangeWorker.body(GridCachePartitionExchangeManager.java:1806)
... 2 more
Caused by: java.lang.InstantiationException: IgniteServiceStarter$1MyClass
at java.lang.Class.newInstance(Class.java:427)
at javax.cache.configuration.FactoryBuilder$ClassFactory.create(FactoryBuilder.java:132)
... 9 more
Caused by: java.lang.NoSuchMethodException: IgniteServiceStarter$1MyClass.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.newInstance(Class.java:412)
... 10 more

2 个答案:

答案 0 :(得分:0)

这可能是部分答案,但我认为第二个示例(使用本地类)失败,因为本地类是内部类。内部类隐式地将外部类的实例作为构造函数的第0个参数,因此它们没有无参数构造函数。

class NewInstanceExample {
    public static void main(String[] args) throws Exception {
        try {
            staticMethod();
            new NewInstanceExample().instanceMethod();
        } catch (Exception x) {
            System.out.println("4. " + x);
            System.out.println("   caused by " + x.getCause());
        }
    }
    static void staticMethod() throws Exception {
        class StaticInner {}
        System.out.println("1. " + StaticInner.class.newInstance());
    }
    void instanceMethod() throws Exception {
        class InstanceInner {}
        System.out.println("2. " + java.util.Arrays.toString(InstanceInner.class.getDeclaredConstructors()));
        System.out.println("3. " + InstanceInner.class.newInstance());
    }
}

输出如下:

1. mcve.NewInstanceExample$1StaticInner@7f31245a
2. [mcve.NewInstanceExample$1InstanceInner(mcve.NewInstanceExample)]
4. java.lang.InstantiationException: mcve.NewInstanceExample$1InstanceInner
   caused by java.lang.NoSuchMethodException: mcve.NewInstanceExample$1InstanceInner.<init>()

请注意粗体行,该行显示InstanceInner的构造函数采用NewInstanceExample的实例,因此Class.newInstance()抛出InstantiationException的原因:

  

InstantiationException - 如果此Class表示抽象类,接口,数组类,基元类型或void;或如果类没有空构造函数;或者如果实例化由于其他原因而失败。

解决方案是确保嵌套类没有封闭实例,可以使用static修饰符(如在您的示例中有效),也可以在static上下文中声明类(就像我在static方法中的例子一样。)

如果没有更多信息,我认为我无法对ClassNotFoundException进行太多猜测。

答案 1 :(得分:0)

不确定为什么你的第一个例子有效,但实际上我也失败了。

但无论如何,整个方法都没有意义。如果您不部署类,则无法使用这些类的实例,并且您的商店最终都会失败。例如,load方法在newInstance()调用时将失败。

要避免部署类,您需要在商店中使用BinaryObject并将CacheConfiguration#keepBinaryInStore标志设置为true,以便Ignite在调用CacheStore时不会序列化/反序列化对象。有关详细信息,请参阅此页:https://apacheignite.readme.io/docs/binary-marshaller#binaryobject-and-cachestore