在当今人工智能飞速发展的时代,大语言模型(LLM)已成为自然语言处理领域的核心力量。然而,这些模型在处理实时信息、专业领域知识以及确保回答准确性和可靠性方面仍面临挑战。为了解决这些问题,检索增强生成(Retrieval-Augmented Generation,RAG)技术应运而生。RAG 通过 “检索 - 生成” 模式,将传统信息检索系统与大语言模型相结合,有效解决了大模型知识更新滞后、容易产生幻觉等问题。
具体来说,RAG 的工作流程如下:用户提出问题后,系统首先从大量的外部知识源(如文档数据库、网页等)中检索与问题相关的信息。这些信息经过筛选和整理后,作为上下文与用户问题一起输入到大语言模型中。大语言模型基于这些上下文生成最终的回答,从而提高了回答的准确性和可靠性。例如,当用户询问 “最近有哪些新的科研成果?” 时,RAG 系统会从科研文献数据库中检索最新的科研成果信息,并将其作为上下文提供给大语言模型,帮助模型生成更准确的回答。
Spring AI 作为 Spring 生态系统中的重要一员,为 RAG 技术的实现提供了强大的支持。它提供了模块化的 API,将 RAG 流程拆解为多个可插拔的组件,使得开发者可以根据具体需求灵活组合检索策略、查询预处理及生成逻辑。这种模块化的设计具有诸多优势:
Spring AI 的模块化 RAG 架构采用了管道过滤器模式,将整个流程清晰地分解为预检索、检索和后检索三个阶段,每个阶段都包含多个关键组件,协同工作以实现高效的检索增强生成。
预检索阶段的核心功能是对用户输入的查询进行优化,以提高后续检索的准确性和召回率。这一阶段主要包括澄清用户意图、优化查询表达式以及适配检索策略等关键任务。
在澄清用户意图方面,QueryTransformer 家族发挥了重要作用。其中,CompressionQueryTransformer 能够对查询进行压缩,去除冗余信息,使查询更加简洁明了,从而提高检索效率。例如,对于查询 “请给我介绍一下苹果公司最新发布的手机,包括它的配置、价格以及用户评价等详细信息”,CompressionQueryTransformer 可能会将其压缩为 “苹果公司最新手机配置、价格和用户评价”。RewriteQueryTransformer 则专注于语义重写,通过对查询进行语义分析,将其转换为更适合检索的形式。比如,对于查询 “我想看一些好看的科幻电影”,RewriteQueryTransformer 可能会将其重写为 “推荐科幻电影”,以提高检索的准确性。TranslationQueryTransformer 则解决了多语言环境下的查询问题,能够将用户输入的查询翻译成不同语言,以便在多语言知识源中进行检索。这在全球化的应用场景中尤为重要,能够满足不同语言用户的需求。
MultiQueryExpander 是预检索阶段的另一个关键组件,它通过生成多变体查询来提升检索召回率。例如,当用户输入 “北京气候” 时,MultiQueryExpander 可能会生成 “北京全年气温分布”“北京降水特征”“北京四季气候特点” 等多个衍生查询。这些衍生查询从不同角度对用户问题进行了扩展,能够更全面地覆盖与用户问题相关的信息,从而提高检索的召回率。通过将原始查询和这些衍生查询一起发送到检索阶段,可以获取更多相关的文档,为后续的生成阶段提供更丰富的上下文信息。
检索阶段是 RAG 架构的核心环节之一,其主要任务是从各种知识源中获取与查询相关的信息。Spring AI 支持混合检索策略,结合了向量检索和关键词检索的优势,以提高检索的准确性和效率。
向量检索(VectorStoreDocumentRetriever)基于向量空间模型,将文本数据转换为向量表示,通过计算向量之间的相似度来检索相关文档。这种方式能够有效地捕捉文本的语义信息,对于语义相关但关键词不完全匹配的查询具有较好的检索效果。例如,当用户查询 “人工智能的发展趋势” 时,向量检索可以准确地找到包含类似语义的文档,即使文档中没有出现 “人工智能的发展趋势” 这一确切表述。在向量检索中,Spring AI 支持设置相似度阈值(similarityThreshold),只有相似度高于阈值的文档才会被返回,从而过滤掉不相关的结果。同时,还可以通过 Top-K 结果筛选,只返回最相关的 K 个文档,提高检索效率。此外,元数据过滤(filterExpression)功能允许根据文档的元数据(如创建时间、作者、文档类型等)进行筛选,进一步缩小检索范围,提高检索的准确性。例如,可以设置只检索最近一年内发布的文档,或者只检索特定作者撰写的文档。
关键词检索则基于传统的关键词匹配算法,能够快速准确地找到包含特定关键词的文档。这种方式对于一些需要精确匹配关键词的查询非常有效。例如,当用户查询 “苹果公司的股票代码” 时,关键词检索可以直接定位到包含 “苹果公司股票代码” 这一关键词的文档。在实际应用中,混合检索策略将向量检索和关键词检索结合起来,充分发挥两者的优势。首先通过向量检索获取一批语义相关的文档,然后再通过关键词检索在这些文档中进一步筛选,以确保检索结果的准确性和全面性。
Spring AI 还提供了与主流向量数据库(如 Milvus、Redis 等)的集成支持。通过统一的 VectorStore 接口,开发者可以方便地将不同的向量数据库集成到 RAG 架构中,实现对向量数据的高效管理和检索。例如,在使用 Milvus 作为向量数据库时,开发者只需按照 Spring AI 提供的接口规范进行配置和调用,即可实现对 Milvus 中向量数据的检索和管理,无需关注底层的数据库实现细节。
后检索阶段的主要任务是对检索到的结果进行处理和优化,以生成高质量的回答。这一阶段主要包括文档处理和上下文增强两个关键步骤。
在文档处理方面,需要解决上下文长度限制问题,确保输入到大语言模型中的上下文信息既包含关键信息,又不会超出模型的输入长度限制。为此,Spring AI 支持按相关性重排序,根据文档与查询的相关性对检索结果进行重新排序,将最相关的文档排在前面,以便模型能够优先处理关键信息。同时,还可以对检索结果进行去重处理,去除重复的文档,减少冗余信息。此外,内容压缩技术可以对文档进行压缩,去除不必要的细节,保留关键信息,从而在不损失重要内容的前提下,减少上下文的长度。
上下文增强是后检索阶段的另一个重要任务。通过 ContextualQueryAugmenter 组件,将检索到的结果与用户提问进行拼接,形成完整的上下文信息。在拼接过程中,可以根据实际需求配置空结果策略(allowEmptyContext)。当检索结果为空时,如果 allowEmptyContext 设置为 true,系统可以直接将用户提问输入到大语言模型中,避免模型因为没有上下文信息而虚构答案;如果设置为 false,则可以返回提示信息,告知用户没有找到相关信息。例如,当用户查询一个非常冷门的问题,检索结果为空时,系统可以返回 “没有找到相关信息,请尝试更换关键词查询” 的提示信息。这样可以提高用户体验,避免用户得到不准确或虚构的回答。
对于那些追求快速搭建 RAG 系统,且应用场景相对轻量的开发者来说,Spring AI 提供的 QuestionAnswerAdvisor 是一个理想的选择。它的设计理念是 “开箱即用”,让开发者能够迅速启动 RAG 流程,而无需花费大量时间和精力去处理复杂的文档拼接与上下文注入等细节。
在使用 QuestionAnswerAdvisor 时,配置向量库与检索参数是关键步骤。假设我们已经将数据加载到了向量存储(VectorStore)中,创建 QuestionAnswerAdvisor 实例并将其注册到聊天客户端(ChatClient),就能轻松实现 RAG 功能。例如:
// 假设已经有一个配置好的向量数据库(VectorStore)和聊天模型(ChatModel)
VectorStore vectorStore =...;
ChatModel chatModel =...;
// 创建QuestionAnswerAdvisor实例
QuestionAnswerAdvisor questionAnswerAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.similarityThreshold(0.5) // 只返回相似度高于0.5的结果
.topK(3) // 只返回前三个结果
.filterExpression(new FilterExpressionBuilder().eq("a", "b").build()) // 只检索a==b的文档
.build())
.build();
// 配置聊天客户端并使用QuestionAnswerAdvisor
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultOptions(OpenAiChatOptions.builder().model("gpt-3.5-turbo").build())
.build();
// 用户提问
String userQuestion = "你好";
ChatResponse response = chatClient.prompt()
.advisors(questionAnswerAdvisor)
.user(u -> u.text(userQuestion))
.call()
.chatResponse();
// 输出回答
System.out.println(response.getReply());
在上述代码中,我们首先创建了一个QuestionAnswerAdvisor
实例,并通过SearchRequest
配置了检索参数,包括相似度阈值、返回结果数量以及过滤表达式。然后,我们创建了一个ChatClient
实例,并将QuestionAnswerAdvisor
注册为默认顾问。最后,我们设置了一个用户问题,并通过聊天客户端发送请求到聊天模型,输出聊天模型生成的回答。
如果在构造QuestionAnswerAdvisor
时未指定过滤条件,在构建请求时也能动态添加,通过FILTER_EXPRESSION
增强器上下文参数在运行时更新SearchRequest
的过滤表达式:
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().build())
.build())
.build();
// Update filter expression at runtime
String content = chatClient.prompt()
.user("Please answer my question XYZ")
.advisors(a -> a.param(QuestionAnswerAdvisor.FILTER\_EXPRESSION, "type == 'Spring'"))
.call()
.content();
这种动态过滤的方式为开发者提供了更大的灵活性,能够根据不同的业务需求实时调整检索条件。
当面对复杂的业务场景,需要对检索流程进行精细控制,以及集成多阶段处理组件时,RetrievalArgumentAdvisor
就展现出了它的强大功能。它支持自定义检索前预处理、检索器及后处理逻辑,为企业级应用提供了高度的定制化能力。
在检索前预处理阶段,CompressionQueryTransformer
可以对用户提问进行压缩,适用于对话历史较长且当前问题基于上下文的场景。例如:
Query query = Query.builder()
.text("And what is its second largest city?")
.history(new UserMessage("What is the capital of Denmark?"),
new AssistantMessage("Copenhagen is the capital of Denmark."))
.build();
QueryTransformer transformer = CompressionQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder)
.build();
Query transformedQuery = transformer.transform(query);
当然,也可以让advisor
自动完成这一过程:
CompressionQueryTransformer compressionQueryTransformer = CompressionQueryTransformer.builder()
.chatClientBuilder(ChatClient.builder(openAiChatModel))
.build();
RetrievalAugmentationAdvisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder().build())
.queryTransformers(compressionQueryTransformer)
.build();
ChatClient.builder(openAiChatModel).build().prompt()
.user(u -> u.text("中国第二大的城市是哪里"))
.messages(new UserMessage("中国首都城市是哪里"))
.messages(new AssistantMessage("北京"))
.advisors(retrievalAugmentationAdvisor)
.call()
.chatResponse();
RewriteQueryTransformer
则可以使用大语言模型重写用户输入,适合语义模糊或冗长的查询。TranslationQueryTransformer
能够翻译用户查询为目标语言(通常为嵌入模型支持的语言),MultiQueryExpander
可以将原始查询扩展为多个不同形式的查询以获取更多相关结果。
在检索阶段,VectorStoreDocumentRetriever
负责从向量库中检索相似文档。可以通过配置topK
、filterExpression
、similarityThreshold
等参数来控制检索结果。例如:
VectorStoreDocumentRetriever vectorStoreDocumentRetriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.topK(4)
.filterExpression(new FilterExpressionBuilder().eq("a", "b").build())
.similarityThreshold(0.4)
.build();
List<Document> documents = vectorStoreDocumentRetriever.retrieve(new Query("What is the main character of the story?"));
在检索后处理阶段,需要解决文档内容过多导致的信息丢失、模型上下文长度限制、内容噪声或重复等问题。常见的操作包括根据相关性重新排序文档、删除无关或重复文档、压缩文档内容以减少干扰等。
ContextualQueryAugmenter
在生成阶段发挥着重要作用,它将检索到的相关内容拼接到用户提问中。可以通过设置allowEmptyContext
参数来控制当检索结果为空时的行为。例如:
ContextualQueryAugmenter contextualQueryAugmenter = ContextualQueryAugmenter.builder()
.allowEmptyContext(false)
.build();
当allowEmptyContext
设置为false
时,如果没有检索到内容,模型通常不会回答或回答不知道;当设置为true
时,即使检索内容为空,也会尝试回答。
在开始搭建基于 Spring AI 的模块化 RAG 架构应用之前,我们需要准备好相应的开发环境。本文中我们选择的技术栈为 Spring Boot 3.x + Spring AI 1.0 + OpenAI/GPT-3.5(或阿里云 Qwen 等国产大模型)。
首先,确保你的开发环境中安装了 Java 17 及以上版本。Spring Boot 3.x 对 Java 版本有一定要求,使用 Java 17 及以上版本可以确保获得更好的性能和功能支持。你可以通过在命令行中输入java -version
来检查 Java 版本。如果未安装或版本不符合要求,请前往 Oracle 官网或 OpenJDK 官网下载并安装合适的 Java 版本。
构建工具方面,我们使用 Maven 来管理项目依赖。Maven 是一个强大的项目管理和构建工具,能够方便地下载和管理项目所需的各种依赖库。确保你已经安装了 Maven,并配置好了环境变量。可以在命令行中输入mvn -v
来检查 Maven 是否安装成功以及查看其版本信息。如果尚未安装 Maven,可以从 Maven 官网下载并按照官方文档进行安装和配置。
接着是依赖配置。在pom.xml
文件中添加如下依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
</properties>
<dependencies>
<!-- Spring AI核心依赖 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>\${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- OpenAI集成依赖,如果使用其他模型,替换此依赖 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
</dependency>
<!-- 向量数据库集成依赖,以Milvus为例 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-milvus-store</artifactId>
</dependency>
<!-- Spring Boot Web支持,用于创建RESTful接口 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
上述依赖配置中,spring-ai-bom
用于统一管理 Spring AI 相关依赖的版本;spring-ai-openai
实现了与 OpenAI 的集成,若使用阿里云 Qwen 等其他大模型,需替换为相应的模型集成依赖;spring-ai-milvus-store
用于集成 Milvus 向量数据库;spring-boot-starter-web
则为项目提供 Web 支持,用于创建 RESTful 接口,方便与前端进行交互。
如果使用阿里云 Qwen 模型,还需要在application.yml
中配置相应的 API 密钥和模型参数:
spring:
ai:
dashscope:
api-key: your-api-key
chat:
options:
model: qwen-max
请将your-api-key
替换为你在阿里云获取的真实 API 密钥。
在 Spring Boot 应用中,我们首先需要配置 Milvus 向量数据库。创建一个配置类MilvusConfig
,用于初始化 Milvus 客户端并配置向量存储:
import io.milvus.client.MilvusServiceClient;
import io.milvus.param.ConnectParam;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.milvus.MilvusVectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MilvusConfig {
@Value("\${milvus.host}")
private String host;
@Value("\${milvus.port}")
private Integer port;
@Bean
public MilvusServiceClient milvusServiceClient() {
return new MilvusServiceClient(ConnectParam.newBuilder()
.withHost(host)
.withPort(port)
.build());
}
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel, MilvusServiceClient milvusServiceClient) {
return MilvusVectorStore.builder(milvusServiceClient, embeddingModel)
.collectionName("your-collection-name")
.initializeSchema(true)
.build();
}
}
在上述代码中,MilvusServiceClient
用于连接 Milvus 服务器,vectorStore
方法创建了一个MilvusVectorStore
实例,用于与 Milvus 向量数据库进行交互。通过@Value
注解从配置文件中读取 Milvus 服务器的地址和端口,并在vectorStore
方法中指定了要使用的集合名称。请将your-collection-name
替换为你实际使用的集合名称。同时,还需在application.yml
中添加 Milvus 的配置:
milvus:
host: 127.0.0.1
port: 19530
上述配置假设 Milvus 服务器运行在本地,端口为默认的 19530。如果你的 Milvus 服务器部署在其他地址或使用了不同的端口,请相应地修改配置。
接下来,创建一个控制器类RagController
,用于处理用户的请求并实现检索增强生成功能:
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.rag.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
public class RagController {
private final ChatClient chatClient;
private final QuestionAnswerAdvisor questionAnswerAdvisor;
public RagController(ChatClient chatClient, VectorStore vectorStore) {
this.chatClient = chatClient;
this.questionAnswerAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.similarityThreshold(0.5)
.topK(3)
.build())
.build();
}
@GetMapping("/rag")
public Mono<String> ragChat(@RequestParam String question) {
return chatClient.prompt()
.advisors(questionAnswerAdvisor)
.user(u -> u.text(question))
.call()
.map(response -> response.getReply());
}
}
在这个控制器中,RagController
构造函数接受ChatClient
和VectorStore
实例,并创建了一个QuestionAnswerAdvisor
实例,用于从向量数据库中检索相关文档。ragChat
方法处理/rag
路径的 GET 请求,接收用户的问题作为参数,通过ChatClient
发送请求,并结合QuestionAnswerAdvisor
的检索结果生成回答,最后返回回答结果。其中,similarityThreshold(0.5)
设置了相似度阈值为 0.5,只有相似度高于该阈值的文档才会被返回;topK(3)
表示只返回前三个最相关的文档。
完成服务端代码实现后,我们可以使用 Postman 来测试部署的 RAG 服务。启动 Spring Boot 应用后,打开 Postman,发送一个 GET 请求到http://localhost:8080/rag
,并在请求参数中添加question
参数,例如:question=根据《智能机器人操作指南》第3章,自动导航功能是如何实现的?
发送请求后,我们应该能够收到结合知识库内容的精准回答,例如:根据《智能机器人操作指南》第3章,自动导航功能通过激光雷达与视觉传感器融合实现,具体步骤如下:首先,激光雷达实时扫描周围环境,获取障碍物的距离信息;同时,视觉传感器对周围场景进行图像采集和分析...
通过这样的测试验证,我们可以确保基于 Spring AI 的模块化 RAG 架构应用能够正常工作,根据用户问题从知识库中检索相关信息,并结合大语言模型生成准确的回答。
在生产环境中,RAG 系统的性能至关重要。检索效率的优化直接影响到用户体验和系统的响应速度。我们可以从批量处理和缓存策略两个方面来提升检索效率。
批量处理是提高检索效率的重要手段之一。在实际应用中,我们常常需要一次性处理多个查询,若每个查询都单独进行处理,会导致资源的浪费和效率的低下。因此,我们可以使用TokenCountBatchingStrategy
来控制嵌入模型的输入长度,避免单次处理过载。TokenCountBatchingStrategy
会根据设定的最大令牌数(token)来对查询进行分组,将多个查询合并为一个批量请求发送到嵌入模型中进行处理。这样可以充分利用模型的并行处理能力,减少模型调用次数,从而提高处理效率。例如,假设我们设定最大令牌数为 2000,当有 10 个查询,每个查询的令牌数都在 200 左右时,TokenCountBatchingStrategy
会将这 10 个查询合并为一个批量请求,一次性发送到嵌入模型中进行处理,而不是分别发送 10 次请求。这样不仅减少了网络开销,还提高了模型的处理效率。
缓存策略也是优化检索效率的有效方法。对于高频查询的检索结果,我们可以进行缓存,避免重复查询向量库,从而降低向量库的压力,提高系统的响应速度。可以使用本地缓存(如 Caffeine)或分布式缓存(如 Redis)来实现缓存功能。以 Caffeine 为例,我们可以创建一个缓存实例,并设置缓存的最大容量和过期时间:
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
Cache<String, List<Document>> cache = Caffeine.newBuilder()
.maximumSize(1000) // 设置缓存的最大容量为1000
.expireAfterWrite(10, TimeUnit.MINUTES) // 设置缓存项在写入10分钟后过期
.build();
在进行检索时,首先检查缓存中是否已经存在该查询的结果。如果存在,则直接从缓存中获取结果并返回;如果不存在,则进行实际的检索操作,并将检索结果存入缓存中。例如:
public List<Document> retrieve(String query) {
List<Document> result = cache.getIfPresent(query);
if (result != null) {
return result;
}
// 实际的检索操作
result = vectorStore.similaritySearch(query);
cache.put(query, result);
return result;
}
通过上述缓存策略,对于高频查询,系统可以直接从缓存中获取结果,大大提高了响应速度,同时也减轻了向量库的压力,提高了系统的整体性能。
在生产环境中,系统的鲁棒性是至关重要的。一个鲁棒的 RAG 系统需要能够处理各种异常情况,确保系统的稳定运行和用户体验。
异常处理是鲁棒性设计的重要环节。在 RAG 系统中,可能会出现向量库连接异常、大模型调用超时等问题。为了确保系统的稳定性,我们需要捕获这些异常,并返回友好的提示信息给用户。例如,在进行向量库检索时,可能会因为网络问题或向量库服务故障而导致连接异常。我们可以使用try-catch
块来捕获这些异常,并返回相应的错误提示:
try {
List<Document> documents = vectorStore.similaritySearch(query);
// 处理检索结果
} catch (Exception e) {
logger.error("Vector store connection error", e);
return "Sorry, there was an error retrieving information from the knowledge base. Please try again later.";
}
在调用大模型生成回答时,也可能会出现超时等异常情况。同样地,我们可以捕获这些异常,并返回友好的提示:
try {
ChatResponse response = chatClient.prompt()
.advisors(questionAnswerAdvisor)
.user(u -> u.text(question))
.call()
.chatResponse();
return response.getReply();
} catch (TimeoutException e) {
logger.error("Large language model call timed out", e);
return "Sorry, the system is currently busy. Please try again later.";
} catch (Exception e) {
logger.error("Error calling large language model", e);
return "Sorry, there was an error processing your request. Please try again later.";
}
空结果处理也是鲁棒性设计的关键。在 RAG 系统中,若检索结果为空,可能会导致模型脱离知识库回答,生成不准确或虚构的回答。为了避免这种情况,我们可以通过allowEmptyContext(false)
来强制要求检索结果非空。当检索结果为空时,直接返回提示信息,告知用户没有找到相关信息,而不是让模型进行回答。例如:
ContextualQueryAugmenter contextualQueryAugmenter = ContextualQueryAugmenter.builder()
.allowEmptyContext(false)
.build();
通过上述异常处理和空结果处理机制,我们可以提高 RAG 系统的鲁棒性,确保系统在各种情况下都能稳定运行,为用户提供可靠的服务。
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。