MongoDB Java Driver 4.2.2 - java.lang.noSuchMethodError when chaining .iterator or .into with .find

Viewed 2663

I’m attempting to build my own application and backend API to further my understanding of the Mongo Java driver after completing the M220J course, but am running into a blocker.

Context

I’m creating a bookmark managing app and rebuilding the backend in Java using the Mongo driver and Spring Boot. The error appears when using .iterator() or .into() after .find() to query my Atlas database. The error DOES NOT APPEAR when using .first() after .find().

My pom.xml includes version 4.2.2 of mongodb-driver-sync. I’m using Java 15.0.1.

The folders collection uses a CodecRegistry, much like how the Users or Sessions collection is created in the mflix app in the MongoDB University course M220J, meaning a query on foldersCollection should return documents of type Folder. I’ve verified that the data has the same fields in the database and Folder.java class.

Code

In the code, the user_id field is currently stored as a String, not ObjectId, in the Folder documents (acts like a foreign key) This does not cause the error. The .into() trick was taken from this article talking about Mongo and Java Pojos.

This is my current implementation, but it does not work.

  // Returns an array of folders, given a user id
  public List<Folder> getFolders(String id) {
    if (id == null || id.isEmpty()) return null;
    List<Folder> folders = foldersCollection.find(new Document("user_id", id)).into(new ArrayList<>());
    return folders;
  }

This code also does not work, which uses .iterator() after .find(), and excludes the id to get all folders. It also does not work if I include the id in the query.

  public List<Folder> getAllFolders(){
    List<Folder> folders = new ArrayList<>();
    foldersCollection.find().iterator().forEachRemaining(folders::add);
    return folders;
  }

I have verified that this code works with a test in Java and in Postman:

  // Gets a folder from db given folder _id
  public Folder getFolder(String id) {
    if (id == null || id.isEmpty()) return null;
    return foldersCollection.find(new Document("_id", new ObjectId(id))).first();
  }

NoSuchMethodError:

It looks like it’s pointing towards the .find() method when I use .into(), but I’ve confirmed that .find() works when chained with .first(), so I don’t think .find() is the issue. The same error appears when using .iterator().

java.lang.NoSuchMethodError: 'com.mongodb.internal.operation.ExplainableReadOperation com.mongodb.internal.operation.SyncOperations.find(org.bson.conversions.Bson, java.lang.Class, com.mongodb.internal.client.model.FindOptions)'

    at com.mongodb.client.internal.FindIterableImpl.asReadOperation(FindIterableImpl.java:236)
    at com.mongodb.client.internal.FindIterableImpl.asReadOperation(FindIterableImpl.java:40)
    at com.mongodb.client.internal.MongoIterableImpl.execute(MongoIterableImpl.java:135)
    at com.mongodb.client.internal.MongoIterableImpl.iterator(MongoIterableImpl.java:92)
    at com.mongodb.client.internal.MongoIterableImpl.forEach(MongoIterableImpl.java:121)
    at com.mongodb.client.internal.MongoIterableImpl.into(MongoIterableImpl.java:130)
    at com.bookmarkd.api.daos.FolderDao.getFolders(FolderDao.java:46)
    at com.bookmarkd.FolderTest.GetFolders(FolderTest.java:50) <31 internal lines>
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) <9 internal lines>
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) <23 internal lines>

Lastly, here’s my Folder class and an image a of a few documents in the Atlas database:

public class Folder {

  @BsonId
  @JsonIgnore
  private ObjectId oid;

  @JsonProperty("_id")
  @BsonIgnore
  private String id;

  @JsonProperty("user_id")
  private String userId;

  private String name;
  private String icon;
  private boolean shareable;

  // Constructor, getters, and setters... 
}

An image of a few Folder documents in the foldersCollection

Any help to debug this issue is greatly appreciated!


Additional code / explanation of issue:

(Though it makes the post really long, I'd thought it'd be easier to see all the code here rather than view it from a Github repo.)

Perhaps I'm over complicating things, but I'm attempting to learn and use Spring Boot MVC. There's a FolderController that handles the Http requests, which calls the FolderService, which calls the FolderDao that enacts CRUD methods on the Atlas cluster. All these files are stored in their respective packages: controllers, services, daos, models, and config.

The FolderDao extends an AbstractDao class that sets the client and database with values received from an @Autowired FolderDao constructor.

Lastly, the MongoDBConfiguration class configures the settings and connects to the Atlas cluster. I have configured the PojoRegistry here, as shown in this example. I believe this class actually is run immediately after the @SpringBootApplication in Application.java starts up. This was how it was done in the mflix app. You can see a completed mflix app here for reference.

Maybe the error is coming from Atlas? My M0 free-tier cluster is version 4.4.4, but I had no problems completing the M220J course where this logic was implemented with mflix.

Here's the full code for each class in the order I described it above:

FolderController

package com.bookmarkd.api.controllers;

import com.bookmarkd.api.models.Folder;
import com.bookmarkd.api.services.FolderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@CrossOrigin(origins = "*")
@RequestMapping("/api/folders")
@RestController
public class FolderController {

  @Autowired
  FolderService folderService;

  public FolderController() { super(); }

  public void getFolders() {}

  // Get all folders, given the user id
  @GetMapping("")
  public ResponseEntity getFoldersByUserId(@RequestParam(value = "userid", required = false) String folderId) {
    List<Folder> folders = folderService.getFolders(folderId);
    if (folders == null) return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
    return ResponseEntity.status(HttpStatus.OK).body(folders);
  }

  // Get a single public folder, given the folder id
  @GetMapping("/{id}")
  public ResponseEntity getFolder(@PathVariable(value = "id") String folderId) {
    Folder folder = folderService.getFolder(folderId);
    if (folder == null) return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
    return ResponseEntity.status(HttpStatus.OK).body(folder);
  }

  public void addFolder() {}

  public void updateFolder() {}

  public void deleteFolder() {}
}

FolderService

package com.bookmarkd.api.services;

import com.bookmarkd.api.daos.FolderDao;
import com.bookmarkd.api.models.Folder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;

import java.util.List;

@Configuration
@Service
public class FolderService {

  @Autowired
  private FolderDao folderDao;

  public FolderService() {
    super();
  }

  public List<Folder> getFolders(String id) {
    return folderDao.getFolders(id);
  }

  public Folder getFolder(String id) {
    return folderDao.getFolder(id);
  }

  // Add folder, update folder, delete folder, to-be-implemented later...
}

FolderDao

package com.bookmarkd.api.daos;

import com.bookmarkd.api.models.Folder;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

@Configuration
public class FolderDao extends AbstractDao {

  private final MongoCollection<Folder> foldersCollection;

  @Autowired
  public FolderDao(MongoClient mongoClient, @Value("${spring.mongodb.database}") String databaseName) {
    super(mongoClient, databaseName);

    foldersCollection = db.getCollection("folders", Folder.class);
  }

  // Returns an array of folders, given a user _id
  public List<Folder> getFolders(String id) {
    if (id == null || id.isEmpty()) return null;
    List<Folder> folders = foldersCollection.find(new Document("user_id", id)).into(new ArrayList<>());

//    List<Folder> folders = new ArrayList<>();
//    foldersCollection.find(new Document("user_id", id)); //.iterator().forEachRemaining(folders::add);

    return folders;
  }

  // Gets a folder from db given folder _id
  public Folder getFolder(String id) {
    if (id == null || id.isEmpty()) return null;
    return foldersCollection.find(new Document("_id", new ObjectId(id))).first();
  }
}

AbstractDao

package com.bookmarkd.api.daos;

import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoDatabase;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Configuration
public abstract class AbstractDao {

  protected String LBV_DATABASE;
  protected MongoDatabase db;
  protected MongoClient mongoClient;

  protected AbstractDao(MongoClient mongoClient, String databaseName) {

    this.mongoClient = mongoClient;
    LBV_DATABASE = databaseName;
    this.db = this.mongoClient.getDatabase(LBV_DATABASE);
  }
}

MongoDBConfiguration (this is where I register the Pojo codec, which works for the User model and folderCollection.find().first() implementation)

package com.bookmarkd.api.config;

import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.WriteConcern;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.PojoCodecProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static org.bson.codecs.configuration.CodecRegistries.fromRegistries;

// Used to configure connection to Atlas cluster
@Configuration
@Service
public class MongoDBConfiguration {

  @Bean
  @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
  public MongoClient mongoClient(@Value("${spring.mongodb.uri}") String connectionString) {

    CodecRegistry pojoCodecRegistry = fromRegistries(
            MongoClientSettings.getDefaultCodecRegistry(),
            fromProviders(PojoCodecProvider.builder().automatic(true).build())
    );

    ConnectionString connString = new ConnectionString(connectionString);
    WriteConcern writeConcern = WriteConcern.MAJORITY; //.withWTimeout(2500, TimeUnit.MILLISECONDS);
    MongoClientSettings settings = MongoClientSettings.builder()
            .writeConcern(writeConcern)
            .applyConnectionString(connString)
            .codecRegistry(pojoCodecRegistry)
            .build();
    MongoClient mongoClient = MongoClients.create(settings);

    return mongoClient;
  }
}

Hopefully this full view of all the code gives you an insight into what is going wrong!

3 Answers

Turns out I needed three Mongo-specific dependencies, all the same version.

Adding org.mongo:bson:4.2.3 and org.mongo.mongodb-driver-code:4.2.3 to the existing org.mongodb-driver-sync:4.2.3 dependency in pom.xml solves the issue (I previously had version 4.1.2 of mongodb-driver-core and mongodb-driver-bson).

Wow, such stress just involving a dependency issue! Unfortunately the documentation was unclear as to needing all three, I thought you only needed mongodb-driver-sync.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.bookmarkd</groupId>
    <artifactId>LBV</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>LBV</name>
    <description>Bookmarkd Java REST API backend try 2</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.mongodb/mongodb-driver-sync -->
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongodb-driver-sync</artifactId>
            <version>4.2.3</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.mongodb/mongodb-driver-core -->
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongodb-driver-core</artifactId>
            <version>4.2.3</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.mongodb/bson -->
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>bson</artifactId>
            <version>4.2.3</version>
        </dependency>

    </dependencies>

<!--    <build>-->
<!--        <plugins>-->
<!--            <plugin>-->
<!--                <groupId>org.springframework.boot</groupId>-->
<!--                <artifactId>spring-boot-maven-plugin</artifactId>-->
<!--            </plugin>-->
<!--        </plugins>-->
<!--    </build>-->

</project>

It is not quite clear as to why your code didn't work (may be you have missed something and all the code associated with your application is not there in the post).

I tried this simple and similar example and this also uses a POJO class to map to the MongoDB collection documents, and uses codecs. I am using MongoDB v4.2.8, MongoDB Java Driver 4.2.0 and Java SE 8 (1.8.0_271).

The example uses a collection called as movie with sample documents as follows:

{ "_id" : ObjectId("607082bf20e9199b269d9268"), "name" : "Star Wars", "year" : 1977 }
{ "_id" : ObjectId("607082d020e9199b269d9269"), "name" : "Return of Jedi", "year" : 1983 }

In the code below I used both the find methods you had tried and with success.

The POJO class:

public class Movie {

    @BsonProperty("_id")
    private ObjectId id;
    private String name;
    private int year;

    // default constructor, get set methods, override toString()
    // ...
}

The application class's main method:

CodecRegistry pojoCodecRegistry =
        fromRegistries(
                MongoClientSettings.getDefaultCodecRegistry(),
                fromProviders(PojoCodecProvider.builder().automatic(true).build()));

try(MongoClient client = MongoClients.create()) {

    MongoDatabase db = client.getDatabase("test");
    MongoCollection<Movie> movies =
            db.getCollection("movie", Movie.class)
                    .withCodecRegistry(pojoCodecRegistry);

    List<Movie> list1 = movies.find().into(new ArrayList<>());
    System.out.println(list1);

    List<Movie> list2 = new ArrayList<>();
    movies.find().forEach(list2::add);
    System.out.println(list2);
}

The problem is that mongodb-driver-sync 4.2.3 for some reason depends on mongodb-driver-core 4.1.2 and org.mongodb 4.1.2, and mongodb-driver-core 4.2.3 depends on org.mongodb 4.1.2

Since Maven resolves the dependencies by the shortest path, try to put them in the pom.xml in the following order

    <dependency>
        <groupId>org.mongodb</groupId>
        <artifactId>bson</artifactId>
        <version>4.2.3</version>
    </dependency>
    <dependency>
        <groupId>org.mongodb</groupId>
        <artifactId>mongodb-driver-core</artifactId>
        <version>4.2.3</version>
    </dependency>
    <dependency>
        <groupId>org.mongodb</groupId>
        <artifactId>mongodb-driver-sync</artifactId>
        <version>4.2.3</version>
    </dependency>

then you will get this picture of dependencies(see below) and mongodb-driver will work correctly

picture of mongodb-driver dependencies

Related