可能需要显式配置和注册自定义编解码器或 PojoCodec 来处理此类型

A custom Codec or PojoCodec may need to be explicitly configured and registered to handle this type

UserProfileModel 有两个嵌入式模型。我在这里简明扼要地给出:

@Document(collection = "USER_PROFILE")
public class UserProfileModel extends BaseModel<UserModel>  {

    public static final String FIELD_USER_ID = "userId";
    public static final String FIELD_BASIC_INFO = "basicInfo";
    public static final String FIELD_CONTACT_INFO = "contactInfo";

    private String userId;
    private BasicInfo basicInfo;
    private ContactInfo contactInfo;
}

public class BasicInfo {
    private String gender;
    private LocalDate birthDate;
    private String[] langs;
    private String religiousView;
    private String politicalView;
}

public class ContactInfo {
    private String[] mobilePhones;
    private Address address;
    private SocialLink[] socialLinks;
    private String[] websites;
    private String[] emails;
}

写入UserProfileModel的转换器class:

@Component
@WritingConverter
public class UserProfileModelConverter implements Converter<UserProfileModel, Document> {

    @Override
    public Document convert(UserProfileModel s) {
        Document doc = new Document();

        if (null != s.getUserId())
            doc.put(UserProfileModel.FIELD_USER_ID, s.getUserId());

        if (null != s.getBasicInfo())
            doc.put(UserProfileModel.FIELD_BASIC_INFO, s.getBasicInfo());

        if (null != s.getContactInfo())
            doc.put(UserProfileModel.FIELD_CONTACT_INFO, s.getContactInfo());
    }
}

正在尝试像这样保存 UserProfileModel 对象:

@Autowired
private UserProfileRepository repo;

UserProfileModel profileModel = generateUserProfileModel();
repo.save(profileModel);

异常:

org.bson.codecs.configuration.CodecConfigurationException: An exception occurred when encoding using the AutomaticPojoCodec.
Encoding a BasicInfo: 'BasicInfo(gender=Male, birthDate=2020-05-05, langs=[Lang 1, Lang 2], religiousView=Islam (Sunni), politicalView=N/A)' failed with the following exception:

Failed to encode 'BasicInfo'. Encoding 'langs' errored with: Can't find a codec for class [Ljava.lang.String;.

A custom Codec or PojoCodec may need to be explicitly configured and registered to handle this type.

    at org.bson.codecs.pojo.AutomaticPojoCodec.encode(AutomaticPojoCodec.java:53)
    at org.bson.codecs.EncoderContext.encodeWithChildContext(EncoderContext.java:91)
    at org.bson.codecs.DocumentCodec.writeValue(DocumentCodec.java:185)
    at org.bson.codecs.DocumentCodec.writeMap(DocumentCodec.java:199)
    at org.bson.codecs.DocumentCodec.encode(DocumentCodec.java:141)
    at org.bson.codecs.DocumentCodec.encode(DocumentCodec.java:45)
    at org.bson.codecs.BsonDocumentWrapperCodec.encode(BsonDocumentWrapperCodec.java:63)
    at org.bson.codecs.BsonDocumentWrapperCodec.encode(BsonDocumentWrapperCodec.java:29)

如果我不通过添加 Mongo 配置为 UserProfileModel 使用转换器,则不会出现此异常并且一切正常。

但出于某种原因,我正在尝试使用转换器 class。

那么是转换器有问题还是需要修改class?

已针对此问题提出了 Jira 工单 here

Can't find a codec for class [Ljava.lang.String;

以及他们的回复

The Document class currently supports only List, not native Java array, so just replace with:

List codes = Arrays.asList("1112233", "2223344");

因此,不支持字符串数组。在模型中使用 List<String> 而不是 String[]

解决这个问题的两种方法:

  1. 要么将 String[] 更改为 List<String>,如前所述
  2. 编写您的自定义编解码器(如果您无法控制 class)

解决方法二如下:

  1. 创建自定义代码StringArrayCodec
  2. 将其与现有编解码器一起注册
  3. Start/restart 使用 mongo-java 驱动程序的应用程序

以下是 Kotlin 代码,但也可以转换为 java。

  1. StringArrayCodec.kt

import java.util.*
import org.bson.BsonReader
import org.bson.BsonType
import org.bson.BsonWriter
import org.bson.codecs.Codec
import org.bson.codecs.DecoderContext
import org.bson.codecs.EncoderContext

class StringArrayCodec : Codec<Array<String>> {

    /**
     * Encode an instance of the type parameter `T` into a BSON value.
     * @param writer the BSON writer to encode into
     * @param value the value to encode
     * @param encoderContext the encoder context
     */
    override fun encode(writer: BsonWriter?, value: Array<String>?, encoderContext: EncoderContext?) {
        writer?.writeStartArray()
        val isNonNull = value != null
        if (isNonNull) {
            writer?.writeBoolean(isNonNull)
            value?.size?.let { writer?.writeInt32(it) }
            for (i in value!!) {
                writeValue(writer, i, encoderContext)
            }
        } else {
            writer?.writeBoolean(!isNonNull)
        }
        writer?.writeEndArray()
    }

    private fun writeValue(writer: BsonWriter?, s: String, encoderContext: EncoderContext?) {
        if (s == null) {
            writer?.writeNull()
        } else {
            writer?.writeString(s)
        }
    }

    /**
     * Returns the Class instance that this encodes. This is necessary because Java does not reify generic types.
     *
     * @return the Class instance that this encodes.
     */
    override fun getEncoderClass(): Class<Array<String>>? {
        return Array<String>::class.java
    }

    /**
     * Decodes a BSON value from the given reader into an instance of the type parameter `T`.
     *
     * @param reader         the BSON reader
     * @param decoderContext the decoder context
     * @return an instance of the type parameter `T`.
     */
    override fun decode(reader: BsonReader?, decoderContext: DecoderContext?): Array<String>? {
        reader?.readStartArray()
        val isNonNull = reader?.readBoolean()
        val tempArray: Array<String?>?
        if (isNonNull == true) {
            val size = reader.readInt32()
            tempArray = arrayOfNulls(size)
            for (i in 0 until size) {
                tempArray[i] = readValue(reader, decoderContext)
            }
        } else {
            tempArray = null
        }
        val array: Array<String>? = if (isNonNull == true) {
            Arrays.stream(tempArray)
                .filter { s ->
                    s != null
                }
                .toArray() as Array<String>
        } else {
            null
        }
        reader?.readEndArray()
        return array
    }

    private fun readValue(reader: BsonReader, decoderContext: DecoderContext?): String? {
        val bsonType: BsonType = reader.currentBsonType
        return if (bsonType == BsonType.NULL) {
            reader.readNull()
            null
        } else {
            reader.readString()
        }
    }
}

  1. 将自定义编解码器注册为 CodecRegistries.fromCodecs(StringArrayCodec())

MongodbConfig.kt


import com.mongodb.ConnectionString
import com.mongodb.MongoClientSettings
import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients
import com.mongodb.client.MongoCollection
import com.mongodb.client.MongoDatabase
import org.bson.Document
import org.bson.codecs.configuration.CodecRegistries
import org.bson.codecs.configuration.CodecRegistries.fromProviders
import org.bson.codecs.configuration.CodecRegistries.fromRegistries
import org.bson.codecs.configuration.CodecRegistry
import org.bson.codecs.pojo.PojoCodecProvider
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.stereotype.Component


@EnableConfigurationProperties
@ConfigurationProperties(prefix = "mongodb")
@ConditionalOnProperty(name = ["mongodb.enable"], havingValue = "true", matchIfMissing = false)
@Component
class MongodbConfig
    (
    @Value("${mongodb.uri}")
    val connectionUri: String,

    @Value("${mongodb.database}")
    val database: String,

    @Value("${mongodb.collection-name}")
    val collectionName: String
) {

    @Bean
    fun mongoClient(): MongoClient {
        return MongoClients.create(mongodbSettings())
    }

    @Bean
    fun mongoDatabase(mongoClient: MongoClient): MongoDatabase {
        return mongoClient.getDatabase(database)
    }

    @Bean
    fun mongodbCollection(mongoDatabase: MongoDatabase): MongoCollection<Document> {
        return mongoDatabase.getCollection(collectionName)
    }

    fun mongodbSettings(): MongoClientSettings {
        val pojoCodecRegistry: CodecRegistry = fromRegistries(
            CodecRegistries.fromCodecs(StringArrayCodec()), // <---- this is the custom codec
            MongoClientSettings.getDefaultCodecRegistry(),
            fromProviders(PojoCodecProvider.builder().automatic(true).build())
        )

        val connectionString = ConnectionString(connectionUri)
        return MongoClientSettings.builder()
            .codecRegistry(pojoCodecRegistry)
            .applyConnectionString(connectionString)
            .build()
    }
}
  1. 依赖和驱动版本
implementation("org.mongodb:mongodb-driver-sync:4.2.0") {
    because("To connect mongodb instance")
}