引言

本教程将介绍如何从文件中读取JSON数据,并通过Spring Boot将其导入MongoDB。这种操作在多种场景下都很有用:数据恢复、批量插入新数据或插入默认值。 MongoDB内部使用JSON结构化文档,因此我们自然也使用JSON来存储可导入文件。作为纯文本,这种策略还具有易于压缩的优势。

此外,我们还将学习如何在必要时根据自定义类型验证输入文件。最后,我们将暴露一个API接口,以便在Web应用运行时使用此功能。

依赖

pom.xml中添加以下Spring Boot依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

还需要一个运行中的MongoDB实例,这需要正确配置application.properties文件。

导入JSON字符串

将JSON导入MongoDB最简单的方式是先将其转换为"org.bson.Document"对象。 这个类表示一个通用的无特定类型的MongoDB文档。因此我们无需为可能导入的各种对象类型创建仓库。

我们的策略是:获取JSON(来自文件、资源或字符串),将其转换为Document对象,然后使用MongoTemplate保存。批量操作通常性能更好,因为相比逐个插入对象,减少了网络往返次数。

最重要的是,我们将输入视为每个换行符包含一个JSON对象。这样就能轻松分隔对象。我们将这些功能封装到两个类中:ImportUtilsImportJsonService。先从服务类开始:

@Service
public class ImportJsonService {

    @Autowired
    private MongoTemplate mongo;
}

接下来添加解析JSON行到文档的方法:

private List<Document> generateMongoDocs(List<String> lines) {
    List<Document> docs = new ArrayList<>();
    for (String json : lines) {
        docs.add(Document.parse(json));
    }
    return docs;
}

然后添加将Document列表插入指定集合的方法。注意批量操作可能部分失败,此时可通过检查异常的cause返回成功插入的文档数:

private int insertInto(String collection, List<Document> mongoDocs) {
    try {
        Collection<Document> inserts = mongo.insert(mongoDocs, collection);
        return inserts.size();
    } catch (DataIntegrityViolationException e) {
        if (e.getCause() instanceof MongoBulkWriteException) {
            return ((MongoBulkWriteException) e.getCause())
              .getWriteResult()
              .getInsertedCount();
        }
        return 0;
    }
}

最后组合这些方法。该方法接收输入并返回读取行数与成功插入数的对比字符串:

public String importTo(String collection, List<String> jsonLines) {
    List<Document> mongoDocs = generateMongoDocs(jsonLines);
    int inserts = insertInto(collection, mongoDocs);
    return inserts + "/" + jsonLines.size();
}

使用案例

准备好处理输入后,我们可以构建一些使用案例。创建ImportUtils类辅助处理。该类负责将输入转换为JSON行列表,仅包含静态方法。 先从处理简单字符串的方法开始:

public static List<String> lines(String json) {
    String[] split = json.split("[\\r\\n]+");
    return Arrays.asList(split);
}

由于使用换行符作为分隔符,正则表达式能很好地将字符串分割为多行。该正则同时支持Unix和Windows换行符。接下来是将文件转换为字符串列表的方法:

public static List<String> lines(File file) {
    return Files.readAllLines(file.toPath());
}

类似地,完成将类路径资源转换为列表的方法:

public static List<String> linesFromResource(String resource) {
    Resource input = new ClassPathResource(resource);
    Path path = input.getFile().toPath();
    return Files.readAllLines(path);
}

通过CLI在启动时导入文件

第一个使用案例:通过应用参数实现文件导入功能。利用Spring Boot的ApplicationRunner接口在启动时执行。例如,我们可以读取命令行参数定义要导入的文件:

@SpringBootApplication
public class SpringBootJsonConvertFileApplication implements ApplicationRunner {
    private static final String RESOURCE_PREFIX = "classpath:";

    @Autowired
    private ImportJsonService importService;

    public static void main(String ... args) {
        SpringApplication.run(SpringBootPersistenceApplication.class, args);
    }

    @Override
    public void run(ApplicationArguments args) {
        if (args.containsOption("import")) {
            String collection = args.getOptionValues("collection")
              .get(0);

            List<String> sources = args.getOptionValues("import");
            for (String source : sources) {
                List<String> jsonLines = new ArrayList<>();
                if (source.startsWith(RESOURCE_PREFIX)) {
                    String resource = source.substring(RESOURCE_PREFIX.length());
                    jsonLines = ImportUtils.linesFromResource(resource);
                } else {
                    jsonLines = ImportUtils.lines(new File(source));
                }
                
                String result = importService.importTo(collection, jsonLines);
                log.info(source + " - result: " + result);
            }
        }
    }
}

使用getOptionValues()可处理一个或多个文件。**这些文件可来自类路径或文件系统。 我们通过RESOURCE_PREFIX区分它们。以"classpath:"开头的参数将从资源文件夹读取,而非文件系统。之后所有文件将导入指定集合。

创建测试文件src/main/resources/data.json.log

{"name":"Book A", "genre": "Comedy"}
{"name":"Book B", "genre": "Thriller"}
{"name":"Book C", "genre": "Drama"}

构建后,使用以下命令运行(为可读性添加了换行)。示例中导入两个文件:一个来自类路径,一个来自文件系统:

java -cp target/spring-boot-persistence-mongodb/WEB-INF/lib/*:target/spring-boot-persistence-mongodb/WEB-INF/classes \
  -Djdk.tls.client.protocols=TLSv1.2 \
  com.baeldung.SpringBootPersistenceApplication \
  --import=classpath:data.json.log \
  --import=/tmp/data.json \
  --collection=books

通过HTTP POST上传JSON文件

创建REST控制器后,我们将获得上传并导入JSON文件的接口。这需要一个*MultipartFile*参数:

@RestController
@RequestMapping("/import-json")
public class ImportJsonController {
    @Autowired
    private ImportJsonService service;

    @PostMapping("/file/{collection}")
    public String postJsonFile(@RequestPart("parts") MultipartFile jsonStringsFile, @PathVariable String collection)  {
        List<String> jsonLines = ImportUtils.lines(jsonStringsFile);
        return service.importTo(collection, jsonLines);
    }
}

现在可通过POST导入文件,其中"/tmp/data.json"指向现有文件:

curl -X POST http://localhost:8082/import-json/file/books -F "parts=@/tmp/books.json"

将JSON映射到特定Java类型

我们一直使用未绑定任何类型的JSON,这是使用MongoDB的优势之一。现在需要验证输入。 为此,在服务中添加*ObjectMapper*:

private <T> List<Document> generateMongoDocs(List<String> lines, Class<T> type) {
    ObjectMapper mapper = new ObjectMapper();

    List<Document> docs = new ArrayList<>();
    for (String json : lines) {
        if (type != null) {
            mapper.readValue(json, type);
        }
        docs.add(Document.parse(json));
    }
    return docs;
}

这样若指定了type参数,mapper会尝试将JSON字符串解析为该类型。默认配置下,存在未知属性时会抛出异常。 这是用于MongoDB仓库的简单Bean定义:

@Document("books")
public class Book {
    @Id
    private String id;
    private String name;
    private String genre;
    // getters and setters
}

为使用改进的文档生成器,同时修改该方法:

public String importTo(Class<?> type, List<String> jsonLines) {
    List<Document> mongoDocs = generateMongoDocs(jsonLines, type);
    String collection = type.getAnnotation(org.springframework.data.mongodb.core.mapping.Document.class)
      .value();
    int inserts = insertInto(collection, mongoDocs);
    return inserts + "/" + jsonLines.size();
}

现在不再传递集合名称,而是传递Class。假设它包含如Book中使用的Document注解,因此可获取集合名称。注意由于注解和Document类同名,需指定完整包名。

结论

本文介绍了如何从文件、资源或简单字符串中解析JSON输入并导入MongoDB。我们将这些功能集中在服务类和工具类中,以便在任何地方复用。使用案例包括CLI和REST选项,并提供了使用示例命令。

源代码可在GitHub获取。


原始标题:Import Data to MongoDB From JSON File Using Java