引言
本教程将介绍如何从文件中读取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对象。这样就能轻松分隔对象。我们将这些功能封装到两个类中:ImportUtils和ImportJsonService。先从服务类开始:
@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获取。