spring-projects / spring-data-mongodb

Provides support to increase developer productivity in Java when using MongoDB. Uses familiar Spring concepts such as a template classes for core API usage and lightweight repository style data access.
https://spring.io/projects/spring-data-mongodb/
Apache License 2.0
1.62k stars 1.08k forks source link

BeanInstantiationException: Failed to instantiate []: Specified class is an interface [DATAMONGO-2391] #3251

Open spring-projects-issues opened 5 years ago

spring-projects-issues commented 5 years ago

Ilya Zinkevich opened DATAMONGO-2391 and commented

Hi,

I get error with package org.springframework.boot:spring-boot-starter-data-mongodb:2.1.5.RELEASE

Description: I have interface Call and class TokBoxCall (implements Call) in the code. There is default typeKey in the records in Mongo DB: "_class":"com.xxxxx.server.data.model.call.TokBoxCall".

And I didn't overwrite it in Spring Mongo Config. However, when I perform reading operations, e.g. callRepository.findById(id), I get Exception:

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.xxxxx.server.data.model.Call]: Specified class is an interface
        at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:119)
        at org.springframework.data.convert.ReflectionEntityInstantiator.createInstance(ReflectionEntityInstantiator.java:64)}}

Affects: 2.1.11 (Lovelace SR11)

1 votes, 4 watchers

spring-projects-issues commented 5 years ago

Christoph Strobl commented

Thanks for reporting the issue.

I tried to reproduce the issue but everything seems to work as designed. Maybe I'm missing something here. Do you have a failing example at hand?

spring-projects-issues commented 5 years ago

Ilya Zinkevich commented

Due to NDA, I cannot provide a real code.

The difference is that

gradle build && java -jar

allows to reproduce the issue, whereas

gradle bootRun

works fine

spring-projects-issues commented 5 years ago

Ilya Zinkevich commented

Error occurs when I call this findOne:

 

public interface CallRepository extends MongoRepository<Call, String>, CallRepositoryCustom {
default Call findOne(String callId) {
 return findById(callId).orElse(null);
}
}
spring-projects-issues commented 5 years ago

Ilya Zinkevich commented

Correct logs look:

2019-10-24 19:44:15,472 [    qtp192486017-26 ] DEBUG [   o.s.d.m.c.MongoTemplate:2430 ] find using query: { "$or" : [{ "$or" : [{ "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXX"] } }, { "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXX"] } }] }, { "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXXX"] } }] } fields: Document{{}} for class: class com.xxxxxxxx.server.data.model.User in collection: users
2019-10-24 19:44:15,497 [ mmonPool-worker-13 ] DEBUG [   o.s.d.m.c.MongoTemplate:2430 ] find using query: { "$or" : [{ "$or" : [{ "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXX"] } }, { "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXX"] } }] }, { "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXXX"] } }] } fields: Document{{}} for class: class com.xxxxxxxx.server.data.model.User in collection: users
2019-10-24 19:44:15,497 [ mmonPool-worker-15 ] DEBUG [   o.s.d.m.c.MongoTemplate:2430 ] find using query: { "$or" : [{ "$or" : [{ "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXX"] } }, { "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXX"] } }] }, { "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXXX"] } }] } fields: Document{{}} for class: class com.xxxxxxxx.server.data.model.User in collection: users
2019-10-24 19:44:15,497 [    qtp192486017-26 ] DEBUG [   o.s.d.m.c.MongoTemplate:2430 ] find using query: { "$or" : [{ "$or" : [{ "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXX"] } }, { "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXX"] } }] }, { "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXXX"] } }] } fields: Document{{}} for class: class com.xxxxxxxx.server.data.model.User in collection: users
2019-10-24 19:44:15,501 [ mmonPool-worker-13 ] DEBUG [   o.s.d.m.c.MongoTemplate:2356 ] findOne using query: { "id" : "5d91df3e46e0fb0001ff4d0a" } fields: Document{{}} for class: interface com.xxxxxxxx.server.data.model.Call in collection: calls
2019-10-24 19:44:15,503 [ mmonPool-worker-13 ] DEBUG [   o.s.d.m.c.MongoTemplate:2811 ] findOne using query: { "_id" : { "$oid" : "5d91df3e46e0fb0001ff4d0a" } } fields: { } in db.collection: xxxx-debug.calls
2019-10-24 19:44:15,506 [    qtp192486017-26 ]  INFO [     o.m.driver.connection:  71 ] Opened connection [connectionId{localValue:12, serverValue:1148}] to 127.0.0.1:47017
2019-10-24 19:44:15,512 [ mmonPool-worker-15 ]  INFO [     o.m.driver.connection:  71 ] Opened connection [connectionId{localValue:13, serverValue:1149}] to 127.0.0.1:47017
2019-10-24 19:44:15,516 [ mmonPool-worker-15 ] DEBUG [   o.s.d.m.c.MongoTemplate:2356 ] findOne using query: { "id" : "5d91df3e46e0fb0001ff4d0a" } fields: Document{{}} for class: interface com.xxxxxxxx.server.data.model.Call in collection: calls
2019-10-24 19:44:15,516 [    qtp192486017-26 ] DEBUG [   o.s.d.m.c.MongoTemplate:2356 ] findOne using query: { "id" : "5d91df3e46e0fb0001ff4d0a" } fields: Document{{}} for class: interface com.xxxxxxxx.server.data.model.Call in collection: calls
2019-10-24 19:44:15,517 [ mmonPool-worker-15 ] DEBUG [   o.s.d.m.c.MongoTemplate:2811 ] findOne using query: { "_id" : { "$oid" : "5d91df3e46e0fb0001ff4d0a" } } fields: { } in db.collection: xxxx-debug.calls
2019-10-24 19:44:15,518 [    qtp192486017-26 ] DEBUG [   o.s.d.m.c.MongoTemplate:2811 ] findOne using query: { "_id" : { "$oid" : "5d91df3e46e0fb0001ff4d0a" } } fields: { } in db.collection: xxxx-debug.calls
2019-10-24 19:44:15,538 [    qtp192486017-26 ]  INFO [     o.m.driver.connection:  71 ] Opened connection [connectionId{localValue:14, serverValue:1150}] to 127.0.0.1:47017
2019-10-24 19:44:15,652 [    qtp192486017-26 ] DEBUG [   o.s.d.m.c.MongoTemplate:2356 ] findOne using query: { "id" : "5d91df3e46e0fb0001ff4d0a" } fields: Document{{}} for class: interface com.xxxxxxxx.server.data.model.Call in collection: calls
2019-10-24 19:44:15,653 [    qtp192486017-26 ] DEBUG [   o.s.d.m.c.MongoTemplate:2811 ] findOne using query: { "_id" : { "$oid" : "5d91df3e46e0fb0001ff4d0a" } } fields: { } in db.collection: xxxx-debug.calls

 

spring-projects-issues commented 5 years ago

Ilya Zinkevich commented

Incorrect case logs:

2019-10-24 19:46:31,500 [   qtp1709700394-23 ] DEBUG [   o.s.d.m.c.MongoTemplate:2430 ] find using query: { "$or" : [{ "$or" : [{ "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXX"] } }, { "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXX"] } }] }, { "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXXX"] } }] } fields: Document{{}} for class: class com.xxxxxxxx.server.data.model.User in collection: users
2019-10-24 19:46:31,527 [   qtp1709700394-23 ]  INFO [     o.m.driver.connection:  71 ] Opened connection [connectionId{localValue:11, serverValue:1161}] to 127.0.0.1:47017
2019-10-24 19:46:31,561 [ mmonPool-worker-15 ] DEBUG [   o.s.d.m.c.MongoTemplate:2430 ] find using query: { "$or" : [{ "$or" : [{ "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXX"] } }, { "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXX"] } }] }, { "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXXX"] } }] } fields: Document{{}} for class: class com.xxxxxxxx.server.data.model.User in collection: users
2019-10-24 19:46:31,561 [ ommonPool-worker-3 ] DEBUG [   o.s.d.m.c.MongoTemplate:2430 ] find using query: { "$or" : [{ "$or" : [{ "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXX"] } }, { "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXX"] } }] }, { "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXXX"] } }] } fields: Document{{}} for class: class com.xxxxxxxx.server.data.model.User in collection: users
2019-10-24 19:46:31,561 [   qtp1709700394-23 ] DEBUG [   o.s.d.m.c.MongoTemplate:2430 ] find using query: { "$or" : [{ "$or" : [{ "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXX"] } }, { "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXX"] } }] }, { "_id" : { "$oid" : "593aa90bbe55b4134533f7c7" }, "allRoles" : { "$in" : ["XXXXXX"] } }] } fields: Document{{}} for class: class com.xxxxxxxx.server.data.model.User in collection: users
2019-10-24 19:46:31,568 [ ommonPool-worker-3 ] DEBUG [   o.s.d.m.c.MongoTemplate:2356 ] findOne using query: { "id" : "5d91df3e46e0fb0001ff4d0a" } fields: Document{{}} for class: interface com.xxxxxxxx.server.data.model.Call in collection: calls
2019-10-24 19:46:31,571 [ ommonPool-worker-3 ] DEBUG [   o.s.d.m.c.MongoTemplate:2811 ] findOne using query: { "_id" : { "$oid" : "5d91df3e46e0fb0001ff4d0a" } } fields: { } in db.collection: xxxx-debug.calls
2019-10-24 19:46:31,575 [   qtp1709700394-23 ]  INFO [     o.m.driver.connection:  71 ] Opened connection [connectionId{localValue:13, serverValue:1162}] to 127.0.0.1:47017
2019-10-24 19:46:31,579 [   qtp1709700394-23 ] DEBUG [   o.s.d.m.c.MongoTemplate:2356 ] findOne using query: { "id" : "5d91df3e46e0fb0001ff4d0a" } fields: Document{{}} for class: interface com.xxxxxxxx.server.data.model.Call in collection: calls
2019-10-24 19:46:31,580 [   qtp1709700394-23 ] DEBUG [   o.s.d.m.c.MongoTemplate:2811 ] findOne using query: { "_id" : { "$oid" : "5d91df3e46e0fb0001ff4d0a" } } fields: { } in db.collection: xxxx-debug.calls
2019-10-24 19:46:31,581 [ ommonPool-worker-3 ] ERROR [ x.x.s.s.PermissionService:  51 ] permission CALL_XXXXXX is unavailable for this resource
org.springframework.data.mapping.model.MappingInstantiationException: Failed to instantiate com.xxxxxxxx.server.data.model.Call using constructor NO_CONSTRUCTOR with arguments
        at org.springframework.data.convert.ReflectionEntityInstantiator.createInstance(ReflectionEntityInstantiator.java:67)
        at org.springframework.data.convert.ClassGeneratingEntityInstantiator.createInstance(ClassGeneratingEntityInstantiator.java:84)
        at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:274)
        at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:247)
        at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:196)
        at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:192)
        at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:80)
        at org.springframework.data.mongodb.core.MongoTemplate$ReadDocumentCallback.doWith(MongoTemplate.java:3024)
        at org.springframework.data.mongodb.core.MongoTemplate.executeFindOneInternal(MongoTemplate.java:2633)
        at org.springframework.data.mongodb.core.MongoTemplate.doFindOne(MongoTemplate.java:2360)
        at org.springframework.data.mongodb.core.MongoTemplate.findById(MongoTemplate.java:850)
        at org.springframework.data.mongodb.repository.support.SimpleMongoRepository.findById(SimpleMongoRepository.java:118)
spring-projects-issues commented 5 years ago

Ilya Zinkevich commented

@Document
public interface Call {
 public class TokBoxCall implements Call {

Maybe it helps in invvestigations: only interface is marked with annotation Document

spring-projects-issues commented 4 years ago

Robert Zilke commented

I had the same issue. The following code solved my problem:

import org.bson.Document;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
@ReadingConverter
public class CallReadingConverter implements Converter<Document, Call> {

    @Override
    public Call convert(Document source) {
        return new TokBoxCall(source.get("key"));
    }
}

And register the CallReadingConverter:

@Bean
public MongoCustomConversions customConversions() {
    return new MongoCustomConversions(
            List.of(
                    new CallReadingConverter()
            )
    );
}

Additional information may be found here: https://stackoverflow.com/a/63992362/4471199

monosoul commented 2 years ago

@christophstrobl here's a repository with a reproducer for the issue: https://github.com/monosoul/spring-data-mongo-issue/tree/main

The worst thing about it is that the behavior is inconsistent. You can't reproduce it if you save the document first using the same repository. Check SaveAndGetWithRepositoryTest and SaveWithTemplateAndGetWithRepositoryTest.

I also wrote a blog post explaining the issue and providing multiple ways to solve it: https://blog.monosoul.dev/2022/09/16/spring-data-mongodb-polymorphic-fields/

christophstrobl commented 2 years ago

@monosoul thanks for the reproducer. Unless I'm missing something the behaviour is the expected one as documented in the Customizing Type Mapping section of the reference documentation. Providing an initial entity set should solve the issue.

class MongoDbConfig : AbstractMongoClientConfiguration() {

    override fun getInitialEntitySet(): Set<Class<*>> {
        return setOf(
            DocumentWithInterfaceField::class.java, 
            FieldType.FieldTypeImpl::class.java, 
            FieldType.OtherFieldTypeImpl::class.java)
    }

    // ...
monosoul commented 2 years ago

Hey @christophstrobl , thanks for the prompt reply!

Providing an initial entity set should solve the issue.

Yeah, initial entity set seems to do the job, although I'd go with something like this:

    override fun getInitialEntitySet() = super.getInitialEntitySet().apply {
        add(FieldType.FieldTypeImpl::class.java)
        add(FieldType.OtherFieldTypeImpl::class.java)
    }

Could it have any unexpected side effects though? Is there a chance FieldTypeImpl and OtherFieldTypeImpl will be treated as separate collections in mongo?

Unless I'm missing something the behaviour is the expected one as documented in the Customizing Type Mapping section of the reference documentation.

Yeah, the documentation does mention it, that's why I came up with a solution like this:

    override fun mappingMongoConverter(
        databaseFactory: MongoDatabaseFactory,
        customConversions: MongoCustomConversions,
        mappingContext: MongoMappingContext
    ) = super.mappingMongoConverter(databaseFactory, customConversions, mappingContext).apply {
        setTypeMapper(
            DefaultMongoTypeMapper(
                DEFAULT_TYPE_KEY,
                listOf(
                    ConfigurableTypeInformationMapper(
                        mapOf(
                            FieldTypeImpl::class.java to "field_type_impl",
                            OtherFieldTypeImpl::class.java to "other_field_type_impl",
                        )
                    ),
                    SimpleTypeInformationMapper(),
                )
            )
        )
    }

But the thing is, you wouldn't go to the documentation when having a polymorphic field seems to just work out of the box. Having a test like SaveAndGetWithRepositoryTest might create an impression it just works. And the worst thing is that even if you save a document without using repository, you might still have other tests that save it with repository, thus affecting the test via a shared context.

I think the behavior should be consistent here: if there is no mapping for the type then it should fail while saving it. It shouldn't only fail when you didn't save a document first before getting another one from the DB.

ehardy commented 1 year ago

Just bumped into this issue. getInitialSet() scans for classes annotated with @Document. Would it make sense for it to also look for classes annotated with @TypeAlias? Seems to me like it would solve this issue, as that's essentially what we are doing explicitly when overriding getInitialSet().

christophstrobl commented 1 year ago

@TypeAlias, other than @Document, is not tied to a specific store, which is insufficient to associate entity classes with the MongoMappingContext.

viacheslav-dobrynin commented 1 year ago

Met with this error too. Moreover, it was floating for me, if I first update and then read, then information about the children is added to the cache and everything works fine, but if I make read request right away after startup, then this error appeared. Finally solved it like this:

initialEntitySet is a field in the org.springframework.data.mapping.context.AbstractMappingContext class:

image

You can put a breakpoint here and see which models are added during the scan:

image

And the logic of scanning and populating the given context is executed in the given configuration:

image
christophstrobl commented 1 year ago

Thank you @DobryninVyacheslav for the additional context. EntityScanner is part of spring-boot and using an AnnotationTypeFilter that by default does not consider interface types. There's however an open issue spring-projects/spring-boot#12828 to allow more fine grained filtering. At this point I'm inclined to close this issue in favor of the spring-boot one.

viacheslav-dobrynin commented 1 year ago

Thank you too!