SpringBoot 整合SpringAI实现简单的RAG (检索增强生成)

使用SpringAI进行RAG

本文属于我的AI应用学习笔记的一部分,更多内容请见:我的专栏

🤖检索增强生成 (英语:Retrieval-augmented generation, RAG ) 是赋予生成式人工智能模型信息检索能力的技术。检索增强生成优化大型语言模型(LLM) 的交互方式,让模型根据指定的一组文件回应用户的查询,并使用这些信息增强模型从自身庞大的静态训练数据中提取的信息。检索增强生成技术促使大型语言模型能够使用特定领域或更新后的信息。[1]应用案例,包括让聊天机器人访问公司内部资料,或来自权威来源的事实信息。(来自维基百科)

注意:使用SpringAI进行开发需要JDK17+,这里使用的是JDK21。

SpringAI是一个主流的Java大模型开发框架,可以简化我们的AI应用开发,本文将从文本向量嵌入与检索开始,逐步整合一个可用的RAG系统。

项目地址:ai-chat-demo

阅读本文前应该有的知识基础

  • Java基础

  • Java Web基础

  • 数据库基础

  • 包管理 (Maven、gradle等)

  • Spring基础、SSM整合、SpringBoot等

  • docker、容器化基础

  • Elasticsearch基础


一、准备向量数据库

你可以选择任何你想用的向量数据库,这里使用的是Elasticseach 8.15.5和对应的Java Client,如果你已经有了向量储存方案,请跳过本章。

安装ElasticSearch

安装ElasticSearch有多种方式,推荐使用docker,我这里为了方便起见,在windows上使用docker-compose+dockerhub部署Elasticsearch和可视化界面工具Kibana,大家也可以安装到自己的Linux虚拟机和服务器上。

😎如果你已经安装了Elasticsearch,请跳过这一步。

1. 拉取镜像
  • 打开你的dockerHub,搜索elasticsearchkibana的镜像

这里使用的版本是8.15.5,推荐使用8.6+的版本

验证安装,打开powershellcmd,输入:

arduino 复制代码
docker image ls -a

如果看到如下输出,说明镜像拉取成功:

复制代码
REPOSITORY      TAG       IMAGE ID       CREATED        SIZE
elasticsearch   8.15.5    e983a5cfa418   6 months ago   1.27GB
kibana          8.15.5    e76560fb8141   6 months ago   1.14GB

当然,你也可以在docker-hub中直接查看。

2. 使用docker-compose运行容器

编写docker-compose.yml

yml 复制代码
    version: '3.8'

    services:
      elasticsearch:
        image: elasticsearch:8.15.5
        container_name: elasticsearch
        environment:
          - node.name=es-node
          - cluster.name=es-cluster
          - discovery.type=single-node
          - bootstrap.memory_lock=true
          - xpack.security.enabled=false
          - xpack.security.http.ssl.enabled=false
          - ES_JAVA_OPTS=-Xms1g -Xmx1g
        ulimits:
          memlock:
            soft: -1
            hard: -1
        volumes:
          - ./es-data:/usr/share/elasticsearch/data
        ports:
          - 9200:9200
          - 9300:9300

      kibana:
        image: kibana:8.15.5
        container_name: kibana
        depends_on:
          - elasticsearch
        environment:
          - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
        ports:
          - 5601:5601

    volumes:
      es-data:  

其中的参数可以自行调整,包括设置内存选项和端口号。

注意:数据文件会储存在docker-compose.yml目录下的es-data中。

执行docker-compose.yml

执行命令:

bash 复制代码
cd ${你的docker-compose.yml所在的绝对目录}
docker-compose up -d

如果输出类似于:

erlang 复制代码
Creating network "docker_default" ...
Creating volume "docker_es-data" ...
Creating elasticsearch ...
Creating kibana ...

说明安装成功了

验证容器运行情况

执行命令:

复制代码
docker ps

这个命令会列出所有运行中的容器,输出类似于:

bash 复制代码
CONTAINER ID   IMAGE                  COMMAND                   CREATED             STATUS          PORTS                                            NAMES
15ee1a203ec9   kibana:8.15.5          "/bin/tini -- /usr/l..."   About an hour ago   Up 22 seconds   0.0.0.0:5601->5601/tcp                           kibana
cc9990df199d   elasticsearch:8.15.5   "/bin/tini -- /usr/l..."   About an hour ago   Up 23 seconds   0.0.0.0:9200->9200/tcp, 0.0.0.0:9300->9300/tcp   elasticsearch

如果STATUSUp,说明容器正在运行,也可以在docker-hub中查看容器运行情况。

二、整合Elasticsearch和SpringAI

准备工作

1.引入依赖

我们首先需要引入Maven依赖,完整依赖可以在文章开头的代码仓库中获取:

xml 复制代码
<!--SpringAI-->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-client-chat</artifactId>
    <version>${spring-ai.version}</version>
</dependency>

<!--SpringAI OpenAI组件-->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai</artifactId>
    <version>${spring-ai.version}</version>
</dependency>

<!--SpringAI ES整合-->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-elasticsearch</artifactId>
    <version>${spring-ai.version}</version>
</dependency>

<!--ES客户端-->
<dependency>
    <groupId>co.elastic.clients</groupId>
    <artifactId>elasticsearch-java</artifactId>
    <!--这里选择和你的实际ES版本匹配的版本-->
    <version>${es.version}</version> 
</dependency>

<!--SpringAI 矢量数据库-->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-advisors-vector-store</artifactId>
    <version>${spring-ai.version}</version>
</dependency>

<!--SpringAI RAG-->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-rag</artifactId>
    <version>${spring-ai.version}</version>
</dependency>

依赖版本参考:

xml 复制代码
<properties>
    <java.version>21</java.version>
    <spring-ai.version>1.0.0</spring-ai.version>
    <druid.version>1.2.25</druid.version>
    <mybatis.version>3.0.4</mybatis.version>
    <mysql.version>8.4.0</mysql.version>
    <es.version>8.15.5</es.version>
    <hutool.version>5.8.38</hutool.version>
    <swagger.version>2.2.31</swagger.version>
    <springboot.version>3.5.0</springboot.version>
</properties>
2.调整配置项

application.yml配置文件:

yml 复制代码
spring:
  ai:
    vectorstore:
      elasticsearch:
        initialize-schema: true
        index-name: your_index_name
        dimensions: 1536
        similarity: cosine
  elasticsearch:
    uris: http://192.168.200.132:9200 # 修改为你的ES地址,如果是集群,用逗号分隔
    username: elastic # 如果你的ES有安全认证就加上,没有的话和密码一起注掉
    password: your_password # 对应的密码

创建ES客户端实例:

java 复制代码
@Configuration
public class ElasticsearchConfig {

    @Value("${spring.elasticsearch.uris}")
    private String esUris;

    @Bean
    public ElasticsearchClient elasticsearchClient() throws URISyntaxException {
        URI uri = new URI(esUris);
        // 提取主机和端口
        String host = uri.getHost();
        int port = uri.getPort() == -1 ? 9200 : uri.getPort();  // 默认端口为9200
        // TODO 如果在设置里打开了认证,需要配置认证信息
        // 1. 创建 RestClient(无认证、无 SSL)
        RestClient restClient = org.elasticsearch.client.RestClient.builder(
                // 这里换成自己的ES服务器地址,如果是本地部署,直接localhost即可
                new HttpHost(host, port)).build();

        // 2. 使用 Jackson 映射器创建 Transport 层
        RestClientTransport transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper());


        // 3. 创建 Elasticsearch Java 客户端
        return new ElasticsearchClient(transport);
    }

    @Bean
    public RestClient restClient() {
        return RestClient.builder(new HttpHost(
                        // 这里换成自己的ES服务器地址,如果是本地部署,直接localhost即可
                        "192.168.200.132",
                        9200,
                        "http"))
                .build();
    }
}

指定Embedding模型,从数据库中取:

java 复制代码
/**
 * 动态加载数据库中的文本嵌入模型配置
 */
@Slf4j
@Configuration
@RequiredArgsConstructor
public class AiEmbeddingConfig {

    private final AiConfigMapper aiConfigMapper;

    private static final Integer id = 2;

    /**
     * 注册一个 EmbeddingModel Bean,用于 SpringAI VectorStore
     */
    @Bean
    @Lazy // 延迟加载,避免启动时数据库尚未准备好
    public EmbeddingModel embeddingModel() throws IllegalStateException {
        // 从数据库读取 id=2 的配置(文本嵌入模型)
        AiConfig config = aiConfigMapper.selectByPrimaryKey(id);
        if (config == null) {
            log.error("未找到ID={}的嵌入模型配置", id);
        }
        if (config != null && config.getIsEnabled() != null && config.getIsEnabled() == 0) {
            log.error("ID={}的嵌入模型嵌入模型已禁用", id);
        }

        // 根据 apiDomain 判定使用哪家服务
        String domain = null;
        String apiKey = null;
        String modelName = null;
        if (config != null) {
            domain = config.getApiDomain();
            apiKey = config.getApiKey();
            modelName = config.getModelId();
        }

        // 构造 EmbeddingOptions
        OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder()
                .model(modelName)
                .dimensions(1536)
                //.user("模型对用户的称呼")
                .build();

        OpenAiApi openAiApi = OpenAiApi.builder()
                // 填入自己的API KEY
                .apiKey(apiKey)
                // 填入自己的API域名,如果是百炼,即为https://dashscope.aliyuncs.com/compatible-mode
                // 注意:这里与langchain4j的配置不同,不需要在后面加/v1
                .completionsPath("/chat/completions")
                .embeddingsPath("/embeddings")
                .baseUrl(domain)
                .build();

        return new OpenAiEmbeddingModel(
                openAiApi,
                MetadataMode.NONE,
                options);
    }
}

对应数据库实体类,如需要完整代码,请移步至我的代码仓库:

java 复制代码
/**
 * AI配置信息表
 * TableName  ai_config
 */
@Data
public class AiConfig implements Serializable {

    /**
     * 主键,自增ID
     */
    private Integer id;
    /**
     * 显示名称
     */
    private String displayName;
    /**
     * API域名
     */
    private String apiDomain;
    /**
     * 模型名称
     */
    private String modelName;
    /**
     * 模型类型:0-大模型,1-文本向量,2-视觉模型
     */
    private Integer modelType;
    /**
     * 模型ID
     */
    private String modelId;
    /**
     * API密钥
     */
    private String apiKey;
    /**
     * 上下文最大消息数
     */
    private Integer maxContextMsgs;
    /**
     * 相似度TopP
     */
    private Double similarityTopP;
    /**
     * 随机度temperature
     */
    private Double temperature;
    /**
     * 相似度TopK
     */
    private Double similarityTopK;
    /**
     * 是否为默认模型(是0/否1)
     */
    private Integer isDefault;
    /**
     * 标签
     */
    private String caseTags;
    /**
     * 简介
     */
    private String caseBrief;
    /**
     * 备注
     */
    private String caseRemark;
    /**
     * 是否启用
     * */
    private Integer isEnabled;
    /**
     * 创建时间
     */
    private Date createTime;
    /**
     * 更新时间
     */
    private Date updateTime;

}

以上配置文件生效后,SpringAI会自动配置VectorStore,直接在应用中注入就能用于向量的嵌入和查询,这种方式很方便但是灵活性不足,如果想要增强灵活性,可以使用手动配置,在测试时给大家演示。

单元测试

1.编写向量嵌入与查找的单元测试

在下面的测试方法中,ragStoreWithSpringAI()负责将文档转为向量插入Elasticsearch,而ragRetrieveWithSpringAI()方法负责查询关键字,并按照相关度排序返回,这是RAG能够正常工作的基础。

java 复制代码
@Slf4j
@SpringBootTest
public class AiApplicationTests {

    @Autowired
    private AiConfigMapper aiConfigMapper;

    @Autowired
    private RestClient restClient;

    @Autowired
    private ElasticsearchClient esClient;

    @Autowired
    private VectorStore vectorStore;

    private static final Integer id = 2;
    
    @Test
    public void ragStoreWithSpringAI() throws IOException {
        /// 向量嵌入阶段
        List<Document> documents = List.of(
                new Document("怪物猎人荒野在2025年10月31日的在线玩家峰值为15万"),
                new Document("怪物猎人世界在2025年10月31日的在线玩家峰值为12万"),
                new Document("怪物猎人崛起在2025年10月31日的在线玩家峰值为7万"));

        // 从数据库读取 id=2 的配置(文本嵌入模型)
        AiConfig config = aiConfigMapper.selectByPrimaryKey(id);
        if (config == null) {
            log.error("未找到ID={}的嵌入模型配置", id);
        }
        if (config != null && config.getIsEnabled() != null && config.getIsEnabled() == 1) {
            log.error("ID={}的嵌入模型嵌入模型已禁用", id);
        }

        // 根据 apiDomain 判定使用哪家服务
        String domain = null;
        String apiKey = null;
        String modelName = null;
        if (config != null) {
            domain = config.getApiDomain();
            apiKey = config.getApiKey();
            modelName = config.getModelId();
        }

        // 构造 EmbeddingOptions
        OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder()
                .model(modelName)
                .dimensions(1536)
                //.user("模型对用户的称呼")
                .build();

        OpenAiApi openAiApi = OpenAiApi.builder()
                // 填入自己的API KEY
                .apiKey(apiKey)
                // 填入自己的API域名,如果是百炼,即为https://dashscope.aliyuncs.com/compatible-mode
                // 注意:这里与langchain4j的配置不同,不需要在后面加/v1
                .baseUrl(domain)
                .completionsPath("/chat/completions")
                .embeddingsPath("/embeddings")
                .build();

        OpenAiEmbeddingModel openAiEmbeddingModel = new OpenAiEmbeddingModel(
                openAiApi,
                MetadataMode.NONE,
                options);

        ElasticsearchIndexUtils.createIndex("test_vector", esClient);

        ElasticsearchVectorStoreOptions esOptions = new ElasticsearchVectorStoreOptions();
        esOptions.setDimensions(1536);
        esOptions.setIndexName("test_vector");
        esOptions.setSimilarity(SimilarityFunction.cosine);

        // Add the documents to Elasticsearch
        ElasticsearchVectorStore elasticsearchVectorStore = ElasticsearchVectorStore.builder(restClient, openAiEmbeddingModel)
                .options(esOptions).build();

        elasticsearchVectorStore.add(documents);
    }

    @Test
    public void ragRetrieveWithSpringAI() throws IOException {
        /// 文档查询阶段
        // 从数据库读取 id=2 的配置(文本嵌入模型)
        AiConfig config = aiConfigMapper.selectByPrimaryKey(id);
        if (config == null) {
            log.error("未找到ID={}的嵌入模型配置", id);
        }
        if (config != null && config.getIsEnabled() != null && config.getIsEnabled() == 1) {
            log.error("ID={}的嵌入模型嵌入模型已禁用", id);
        }

        // 根据 apiDomain 判定使用哪家服务
        String domain = null;
        String apiKey = null;
        String modelName = null;
        if (config != null) {
            domain = config.getApiDomain();
            apiKey = config.getApiKey();
            modelName = config.getModelId();
        }

        // 构造 EmbeddingOptions
        OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder()
                .model(modelName)
                .dimensions(1536)
                //.user("模型对用户的称呼")
                .build();

        OpenAiApi openAiApi = OpenAiApi.builder()
                // 填入自己的API KEY
                .apiKey(apiKey)
                // 填入自己的API域名,如果是百炼,即为https://dashscope.aliyuncs.com/compatible-mode
                // 注意:这里与langchain4j的配置不同,不需要在后面加/v1
                .baseUrl(domain)
                .completionsPath("/chat/completions")
                .embeddingsPath("/embeddings")
                .build();

        OpenAiEmbeddingModel openAiEmbeddingModel = new OpenAiEmbeddingModel(
                openAiApi,
                MetadataMode.NONE,
                options);

        ElasticsearchIndexUtils.createIndex("test_vector", esClient);

        ElasticsearchVectorStoreOptions esOptions = new ElasticsearchVectorStoreOptions();
        esOptions.setDimensions(1536);
        esOptions.setIndexName("test_vector");
        esOptions.setSimilarity(SimilarityFunction.cosine);

        // Add the documents to Elasticsearch
        ElasticsearchVectorStore elasticsearchVectorStore = ElasticsearchVectorStore.builder(restClient, openAiEmbeddingModel)
                .options(esOptions).build();

        List<Document> mh = elasticsearchVectorStore.doSimilaritySearch(
                SearchRequest.builder().query("怪物猎人荒野").build());

        log.info("相似度搜索结果:{}", mh);
    }

}

工具类:

java 复制代码
@Slf4j
public class ElasticsearchIndexUtils {

    public static void createIndex(String indexName, ElasticsearchClient esClient) throws IOException {
        // 1.判断是否有索引
        ExistsRequest existsRequest = new ExistsRequest.Builder()
                .index(indexName)
                .build();

        boolean value = esClient.indices().exists(existsRequest).value();
        if (!value) {
            log.info("索引不存在,创建索引:{}", indexName);
            CreateIndexRequest createIndexRequest = CreateIndexRequest.of(builder ->
                    builder.index(indexName)
            );
            esClient.indices().create(createIndexRequest);
        }
    }

}

以上的测试方法采用手动配置,指定了索引为test_vector,如果想用自动配置的方法,直接使用下面的方法,SpringAI会自动配置,在配置文件中编写配置项即可:

java 复制代码
@Slf4j
@SpringBootTest
public class AiApplicationTests {

    @Autowired
    private VectorStore vectorStore;

    @Test
    public void ragStoreWithSpringAI() throws IOException {
        List<Document> documents = List.of(
                new Document(
                "怪物猎人荒野在2025年10月31日的在线玩家峰值为15万", 
                Map.of("title", "荒野")),
                new Document("怪物猎人世界在2025年10月31日的在线玩家峰值为12万", 
                Map.of("title", "世界")),
                new Document("怪物猎人崛起在2025年10月31日的在线玩家峰值为7万", 
                Map.of("title", "崛起")));
        vectorStore.add(documents);
    }

    @Test
    public void ragRetrieveWithSpringAI() throws IOException {
        List<Document> mh = vectorStore.similaritySearch(
                SearchRequest.builder().query("怪物猎人荒野").build());

        log.info("相似度搜索结果:{}", mh);
    }

}

上面的文档列表中,Map.of()传入一个参数Map,作为插入文档的metadata,可以在后续的RAG中做检索和筛选时使用。

2.编写带有RAG功能对话流的单元测试

参考 :SpringAI官方文档-RAG

SpringAI的官方提供多种RAG的实现方式,这里先介绍入门的Naive RAGAdvanced RAG等内容将在未来更新。

测试带有RAG的对话回复:

java 复制代码
@Slf4j
@SpringBootTest
public class AiApplicationTests {

    @Autowired
    private AiConfigMapper aiConfigMapper;

    @Autowired
    private ElasticsearchClient esClient;

    @Autowired
    private VectorStore vectorStore;

    @Autowired
    private MybatisChatMemory mybatisChatMemory;

    private static final Integer id = 2;

    @Test
    void SpringAIRAGChat() throws InterruptedException {

        CountDownLatch latch = new CountDownLatch(1);

        AiConfig aiConfig = aiConfigMapper.selectByPrimaryKey(1);

        OpenAiApi openAiApi = OpenAiApi.builder()
                // 填入自己的API KEY
                .apiKey(aiConfig.getApiKey())
                // 填入自己的API域名,如果是百炼,即为https://dashscope.aliyuncs.com/compatible-mode
                // 注意:这里与langchain4j的配置不同,不需要在后面加/v1
                .baseUrl(aiConfig.getApiDomain())
                .completionsPath("/chat/completions")
                .build();

        // 模型选项
        OpenAiChatOptions chatOptions = OpenAiChatOptions.builder()
                // 模型生成的最大 tokens 数
                .maxTokens(aiConfig.getMaxContextMsgs())
                // 模型生成的 tokens 的概率质量范围,取值范围 0.0-1.0 越大的概率质量范围越大
                .topP(aiConfig.getSimilarityTopP())
                // 模型生成的 tokens 的随机度,取值范围 0.0-1.0 越大的随机度越大
                .temperature(aiConfig.getTemperature())
                // 模型名称
                .model(aiConfig.getModelId())
                // 打开流式对话token计数配置,默认为false
                .streamUsage(true)
                .build();

        ChatModel chatModel = OpenAiChatModel.builder()
                .openAiApi(openAiApi)
                .defaultOptions(chatOptions)
                .build();

        ChatClient chatClient = ChatClient.builder(chatModel)
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(mybatisChatMemory).build())
                .build();

        Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
                .documentRetriever(VectorStoreDocumentRetriever.builder()
                        // 相似度阈值,0.5表示只有当检索结果的相似度分数 ≥ 0.50 时,才返回上层
                        .similarityThreshold(0.50)
                        // 注入的向量存储
                        .vectorStore(vectorStore)
                        .build())
                .queryAugmenter(ContextualQueryAugmenter.builder()
                        // 使用参数,允许查找的结果为空
                        .allowEmptyContext(true)
                        .build())
                .build();

        // 返回反应式对话流
        String userMsg = "我们刚才谈论了任何包含数据库的问题吗?"+
        "怪物猎人荒野在2025年10月31日的在线玩家峰值为多少?`";
        Flux<ChatResponse> chatResponseFlux = chatClient.prompt()
                .user(userMsg)
                // 这里还可以整合对话记忆,这里的65是我之前测试时的一个会话
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "65"))
                // 这里可以指定Metadata元数据匹配筛选
                .advisors(a -> a.param(VectorStoreDocumentRetriever.FILTER_EXPRESSION, "title == '荒野'"))
                .advisors(retrievalAugmentationAdvisor)
                .stream()
                .chatResponse();

        chatResponseFlux
                .subscribe(
                        token -> {
                            // 获取当前输出内容片段
                            String text = "";
                            if (token.getResult() != null) {
                                text = token.getResult().getOutput().getText();
                                if (StrUtil.isNotBlank(text)) {
                                    log.info("当前段数据:{}", text);
                                }
                            }
                        },
                        // 反应式流在报错时会直接中断
                        e -> {
                            log.error("ai对话 流式输出报错:{}", e.getMessage());
                        }, // 错误处理
                        () -> {// 流结束
                            log.info("\n回答完毕!");
                        });

        // 等待响应完成,最多等待20秒
        boolean await = latch.await(20, TimeUnit.SECONDS);
        if (!await) {
            log.error("等待响应超时");
        }
    }
}

输出:

bash 复制代码
当前段数据:我们
当前段数据:刚才
当前段数据:谈论
当前段数据:了
当前段数据:任何包含数据库的问题
当前段数据:吗?  
是
当前段数据:的,你问
当前段数据:过:"pg数据库
当前段数据:好还是Mysql
当前段数据:好?"  

怪物
当前段数据:猎人荒野
当前段数据:在202
当前段数据:5年10
当前段数据:月31日
当前段数据:的在线玩家峰值
当前段数据:为15万
当前段数据:。

🎉到这里我们可以看出,模型同时集成了对话记忆和RAG功能,我们的整合成功了,下一步就是将这个功能整合到我们的应用中。在下一篇文章中,我将整合一个知识库管理后台,并改造我们的应用,使其基于我们投喂的文档,支持RAG的问答。

相关推荐
合作小小程序员小小店3 小时前
web网页开发,旧版在线%考试,判题%系统demo,基于python+flask+随机分配考试题目,基于开发语言python,数据库mysql
开发语言·后端·python·mysql·flask·html5
ss2733 小时前
基于Springboot + vue3实现的药材中药资源共享平台
java·spring boot·后端
rengang663 小时前
353-Spring AI Alibaba ARK 多模型示例
java·人工智能·spring·多模态·spring ai·ai应用编程
程序新视界3 小时前
MySQL的数据库事务、ACID特性以及实战案例
数据库·后端·mysql
kaikaile19953 小时前
深入理解RESTful API设计
后端·restful
bst@微胖子4 小时前
阿里通义千问推理优化上下文缓存之隐式缓存和显式缓存
java·spring·缓存
ss2734 小时前
手写Spring第20弹:JDK动态代理:深入剖析Java代理模式
后端·spring·代理模式
后端小张4 小时前
【JAVA 进阶】重生之我要学会 JUC 并发编程
java·spring boot·spring·java-ee·并发编程·安全架构·juc
重整旗鼓~4 小时前
33.点赞功能
java