Problem Description.
This is a simplified version of the example that is currently in the immutables site.
So I have an item that I want to use with MongoDB.
@Value.Immutable
@Mongo.Repository("items")
public abstract class Item {
@Mongo.Id
public abstract long id();
public abstract String name();
}
For simplicity I create a Junit test. Just like the example. The problem is that this junit test, while it is presented in the example at the immutables site throws an exception:
public class ItemTest{
@Test
public void testRepository() {
try {
//Simple ItemRepository creation
ItemRepository items = new ItemRepository(
RepositorySetup.forUri("mongodb://localhost/test")
);
// Create item Item item = ImmutableItem.builder()
.id(1)
.name("one")
.build();
//Insert is async. Returns a Future.
FluentFuture<Integer> future=items.insert(item);
future.get(); // get the result (ensure it was saved)
}catch(Exception e){
e.printStackTrace();
Assert.fail(e.getMessage());
}
}//end test
}//end class
Running this test will result in the following Exception:
org.bson.codecs.configuration.CodecConfigurationException: Can't find a codec for interface spyros.stackoverflow.example.Item
A complete stack trace is provided below.
java.lang.AssertionError: java.util.concurrent.ExecutionException: **org.bson.codecs.configuration.CodecConfigurationException: Can't find a codec for interface spyros.stackoverflow.example.Item.**
at com.google.common.util.concurrent.AbstractFuture.getDoneValue(AbstractFuture.java:552)
at com.google.common.util.concurrent.AbstractFuture.get(AbstractFuture.java:533)
at com.google.common.util.concurrent.FluentFuture$TrustedFuture.get(FluentFuture.java:82)
at com.google.common.util.concurrent.ForwardingFuture.get(ForwardingFuture.java:62)
at spyros.stackoverflow.example.ItemTest.testRepository(ItemTest.java:78)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: org.bson.codecs.configuration.CodecConfigurationException: Can't find a codec for interface spyros.stackoverflow.example.Item.
at org.bson.codecs.configuration.CodecCache.getOrThrow(CodecCache.java:46)
at org.bson.codecs.configuration.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:63)
at org.bson.codecs.configuration.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:37)
at com.mongodb.MongoCollectionImpl.getCodec(MongoCollectionImpl.java:591)
at com.mongodb.MongoCollectionImpl.insertMany(MongoCollectionImpl.java:333)
at com.mongodb.MongoCollectionImpl.insertMany(MongoCollectionImpl.java:322)
at org.immutables.mongo.repository.Repositories$Repository$2.call(Repositories.java:130)
at org.immutables.mongo.repository.Repositories$Repository$2.call(Repositories.java:127)
at com.google.common.util.concurrent.TrustedListenableFutureTask$TrustedFutureInterruptibleTask.runInterruptibly(TrustedListenableFutureTask.java:125)
at com.google.common.util.concurrent.InterruptibleTask.run(InterruptibleTask.java:69)
at com.google.common.util.concurrent.TrustedListenableFutureTask.run(TrustedListenableFutureTask.java:78)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
at org.junit.Assert.fail(Assert.java:88)
at spyros.stackoverflow.example.ItemTest.testRepository(ItemTest.java:84)
I use gradle and the build.gradle
is the following.
plugins {
id 'java'
id 'java-library'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenLocal()
mavenCentral()
jcenter()
}
dependencies {
implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.7'
implementation 'org.apache.logging.log4j:log4j-core:2.7'
implementation 'javax.persistence:javax.persistence-api:2.2'
implementation 'javax.xml.bind:jaxb-api:2.1'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8'
implementation 'com.google.guava:guava:27.1-jre'
annotationProcessor 'org.immutables:value:2.7.4'
implementation 'org.immutables:value:2.7.4'
implementation 'org.immutables:mongo:2.7.4'
implementation 'org.apache.commons:commons-lang3:3.4'
implementation 'org.apache.commons:commons-collections4:4.1'
implementation 'org.apache.commons:commons-configuration2:2.3'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
Identified cause
The Immutables JSON library needs to be able to find the autogenerated GsonAdaptersItem.class
that implements com.google.gson.TypeAdapterFactory
. In fact for every interface
or abstract class
named XXX
with a similar annotation as in the 'class Item' (provided above) Immutables will generate a similar GsonAdapterXXX
that implements com.google.gson.TypeAdapterFactory
In order to be able to find these classes automatically RepositorySetup
uses java.util.ServiceLoader
to automatically locate them.
This is the related code part:
GsonBuilder gsonBuilder = new GsonBuilder();
// there are no longer auto-registed from class-path, but from here or if added manually.
gsonBuilder.registerTypeAdapterFactory(new TypeAdapters());
for (TypeAdapterFactory factory : ServiceLoader.load(TypeAdapterFactory.class)) {
gsonBuilder.registerTypeAdapterFactory(factory);
}
return gsonBuilder.create();
The cause of the Exception thrown is that for some reason this does not seem to work and the following exception is thrown.
UPDATE
Further research shows that the example works, but only if the jar has been compiled. This is because Immutables produces the following file that will actually be used to find all the required GsonAdapterXXX
classes.
build\classes\java\main\META-INF\services\com.google.gson.TypeAdapterFactory
org.bson.codecs.configuration.CodecConfigurationException: Can't find a codec for interface spyros.stackoverflow.example.Item This is a permalink to the source code.
Work around
The straight forward work arround is to specifically register the GsonAdaptersItem
to the GsonBuilder
.
The following Junit Test works.
@Test
public void testItemVerbose() {
try {
//repeat what the RepositorySetup#createGson does
GsonBuilder gsonBuilder = new GsonBuilder();
//iterating in this loop
for (TypeAdapterFactory factory : ServiceLoader.load(TypeAdapterFactory.class)) {
//You can see that the ServiceLoader does not find the GsonAdaptersItem
//System.out.println("Factory:"+factory.getClass().getCanonicalName()+" "+factory.toString());
gsonBuilder.registerTypeAdapterFactory(factory);
}
//add the GsonAdaptersItem manually
gsonBuilder.registerTypeAdapterFactory(new GsonAdaptersItem());
Gson gson = gsonBuilder.create();
Item item = ImmutableItem.builder()
.id(1)
.name("one")
.build();
final MongoClient mongo = new MongoClient( "localhost" , 27017 );
final MongoDatabase mongoDatabase=mongo.getDatabase("test");
ItemRepository items =new ItemRepository(
RepositorySetup.builder()
.executor(MoreExecutors.newDirectExecutorService())
.database(mongoDatabase)
.gson(gson).build()
);
// Insert async and get
items.insert(item).get(); // returns future, works
}catch (final Exception e){
e.printStackTrace();
Assert.fail(e.getMessage());
}
}
This workaround could be used in order to manually create an api that is generic and the programmer needs to provide the GsonAdapterXXX
. E.g. ItemRepository itemRepository=reporitoryProvider.getRepositoryWithRegisteredGsonAdapter(new GsonAdaptersItem());
More specific XXXRepository
providers can also be created.
public ItemRepository getItemRepository(){
return reporitoryProvider.getRepositoryWithRegisteredGsonAdapter(new GsonAdaptersItem());
}
However, all the above require maintenance, are more error prone and will anyway require writting boilerplate code, which is what someone using Immutables expects to avoid.
Question
I expect that the example should work as shown in the immutables site. My best guess is that since I use gradle instead of maven perhaps I need to change some setting or dependency. The question is: What needs to be changed so that the simple ItemRepository creation (shown below) works?
//Simple ItemRepository creation
ItemRepository items = new ItemRepository(
RepositorySetup.forUri("mongodb://localhost/test")
);
Is it possible to do it with Immutables and without manually writting code that registers codecs class by class or package by package? I would really appreciate an explanation of why it did not work in the first place.
I am revisiting this old question after some time. Currently the suggested solution would be to use the criteria functionality of immutables.
@Value.Immutable
@Criteria.Repository
public abstract class Item {
@Criteria.Id
public abstract long id();
public abstract String name();
}