# SpringBoot面试

汇总Spring、SpringBoot、SpringCloud的面试题。

# IoC相关

控制反转(Inversion of Control,缩写为IoC)是面向对象或者说是编程语言中最重要的一个概念,也是Spring这艘巨轮的“龙骨”,没有之一。

# 什么是IoC?

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。

# Spring如何实现IoC?

在Spring的早期,IoC是基于项目中resources目录的xml配置实现,Spring容器启动的时候会加载这个配置文件,通过解析配置文件获取到bean的信息,以及bean之间的依赖关系,并且完成依赖关系的注入。
在Spring 3.x之后,Spring逐渐放弃了xml配置的实现方式,改由注解扫描的方式实现bean定义的获取。

# 单例Bean和单例模式的区别?

单例模式指的是设计模式中的单例对象模式,即使用了单例设计模式设计的类,在一个JVM中只能创建一个对象。单例Bean则指的是Spring的对象复用理念,如果一个类没有特殊要求,在Spring容器中创建一个对象即可满足所有请求的使用,那么Spring只会创建一次,这个bean也就称之为“单例bean”。当然也有一些特殊场景,Spring要在每次依赖注入的时候创建一个新的对象,这种bean就称之为“prototype bean(原型bean)”。

# Spring中Bean是线程安全的吗?

不是,Spring中Bean一般情况下只是单例Bean,和线程安全没有任何关系,如果多个线程需要操作同一个bean,需要使用同步锁机制干预,否则就会出现线程安全问题。

# Spring事务相关

# 什么是Spring事务?

Spring事务指的Spring针对我们日常开发中操作数据库最常用的数据库事务场景所做的统一封装,底层实现还是基于数据库事务完成。如果我们在一个方法中要针对数据库做一系列操作,希望这些操作整体(也就是这个方法)具备事务的特性,我们就可以通过Spring提供的声明式事务注解@Transaction来实现。

# Spring事务是如何实现的? (仅讨论声明式事务)

  • 底层数据库支持事务
    比如我们底层dataSource使用MySQL,MySQL使用InnoDB引擎,只有满足这两条,上层的Spring才有可能支持事务。
  • 使用@Transactional注解修饰bean的方法。
  • 在调用使用@Transactional注解修饰的方法的时候,Spring首先会创建一个数据库链接,然后将数据库的autocommit属性修改为false。
  • 执行方法
    方法中包含sql,这里实际上是sql的执行过程。
  • 方法执行无异常,提交事务。
  • 方法执行异常,且异常需要回滚,即回滚事务。
  • tips:Spring的事务隔离级别对应的是数据库的事务隔离级别

# @Transaction声明式事务注解的原理?

Transaction声明式事务注解的底层原理是AOP+ThreadLocal,使用了@Transaction修饰方法,Spring就会对方法做增强,在方法开始时开启事务,在方法结束时提交事务,在方法出异常的时候回滚事务。具体来说是Spring AOP增强的代码中会通过ThreadLocal获取到当前线程的数据库连接对象,通过数据库连接对象操作事务。

# 什么是Spring的事务传播机制?

Spring事务传播机制指的是多个事务方法之间互相调用的时候,事务如何在这些方法中传播的策略。(我觉得这个知道概念就行,下面7种机制不用记,用到的时候看一下就好)
Spring事务传播有七种机制,分别是:

  1. REQUIRED:默认值,支持当前事务,如果没有事务会创建一个新的事务
  2. SUPPORTS:支持当前事务,如果没有事务的话以非事务方式执行
  3. MANDATORY:支持当前事务,如果没有事务抛出异常
  4. REQUIRES_NEW:创建一个新的事务并挂起当前事务
  5. NOT_SUPPORTED:以非事务方式执行,如果当前存在事务则将当前事务挂起
  6. NEVER:以非事务方式进行,如果存在事务则抛出异常
  7. NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与REQUIRED类似的操作

# Spring事务什么时候会失效?

Spring事务失效的本质是:Spring声明的Transactional注解没有被AOP代理正确处理,导致事务管理器没有介入方法调用。以下是几种Spring事务失效的场景:

# 方法未经public修饰

@Transactional注解默认只对public方法生效。如果你在protected、private或包级可见的方法上使用该注解,Spring会忽略它,不会创建代理事务。

# 自调用

当你在一个类的一个非事务方法A中,调用同一个类的事务方法B时,调用的是this.B(),而不是Spring生成的代理对象。由于是类内部的直接调用,绕过了代理,所以事务不会生效。

@Service
public class UserService {

    public void createUser() {
        // 这是一个非事务方法
        this.insertUser(); // 自调用,事务失效!
    }

    @Transactional
    public void insertUser() {
        // 插入用户数据
        userDao.insert(user);
        // 如果这里抛出异常,事务不会回滚
    }
}

针对自调用导致事务失效的情况,我们可以显式的注入Spring的applicationContext对象,通过applicationContext获取到bean之后,再执行bean的相关方法。

@Autowired
private ApplicationContext applicationContext;

public void createUser() {
    UserService proxy = applicationContext.getBean(UserService.class);
    proxy.insertUser(); // 通过代理对象调用,事务生效
}

# 异常类型不正确或被捕获

@Transactional 默认只在抛出运行时异常(RuntimeException) 和 Error 时回滚。如果抛出的是受检异常(Checked Exception),如Exception,事务不会回滚。

// 失效:抛出受检异常,默认不回滚
@Transactional
public void method() throws Exception {
    // ...
    throw new Exception("受检异常");
}

// 失效:异常被捕获,代理无法感知到异常
@Transactional
public void method() {
    try {
        // ...
        int i = 1 / 0; // 会抛出运行时异常
    } catch (Exception e) {
        e.printStackTrace(); // 异常被"吞掉"了,事务不会回滚
    }
}

针对上述受检异常导致事务失效的问题,我们可以通过指定rollbackFor属性来解决:

@Transactional(rollbackFor = Exception.class)
public void method() throws Exception {
    // ...
}

# Bean相关

# Bean的生命周期

Spring中Bean的生命周期由实例化、属性填充、初始化、Bean的使用、Bean的销毁组成,其实说白了就是创建,使用,销毁,只是创建过程拆分成了实例化、属性填充、初始化三个动作。

  1. 实例化
    Bean的实例化指的是Spring容器读取元数据(注解)获取Bean定义之后通过反射/动态代理创建一个Bean的实例对象(也就是某一个具体类的对象),此时该对象无法直接使用,因为还没完成属性填充和初始化两个动作。

  2. 属性填充
    一旦Bean实例被创建,Spring会通过配置的属性值来填充Bean的属性。这可以通过setter方法或者构造器注入来实现。

  3. 初始化
    在Bean的属性被正确设置之后,Spring会调用配置的初始化方法。这个过程其实是一个钩子函数,如果bean中有@PostConstruct修饰的方法,Spring就会执行这些方法。

  4. bean的使用
    经过初始化后,Bean已经准备就绪,可以被应用程序使用。

  5. 销毁‌
    当容器关闭时,Spring会调用配置的销毁方法,清理Bean的资源。这个过程会调用使用注解@PreDestroy标注的方法,一般我们不需要销毁这个动作,除非是要释放系统资源等必须要依赖具体方法执行才能完成的动作。

# 容器相关

# ApplicationContext和BeanFactory的区别?

ApplicationContext对应Spring容器本身,BeanFactory则对应Bean的创建的工厂对象,两者是包含关系,ApplicationContext中包含一个BeanFactory对象,以一个实际的容器启动为例,ApplicationContext会调用其中的Bean定义扫描组件,扫描出项目所需的Bean定义集合,将该集合放到BeanFactory中,同时ApplicationContext还会触发生命周期相关的监听器事件,读取配置信息,加载国际化信息,发布事件等功能。

# 谈谈你对Spring IOC的理解?

# @Autowired和@Resource注解的区别?

这里先说明一下什么是属性的类型,什么是属性的name,以及补充说明一下,@Autowired是Spring自己定义的注解,@Resource则是jdk的注解。然后再说明两者的区别
43_autowired和resource的区别

  • @Autowired注解
    优先根据属性类型去注入依赖,这里又分三种情况,找不到就报错,找到一个就刚刚好,找到多个的情况下会根据name做精准匹配,根据name找不到就报错,找到一个就刚刚好。(这里不存在根据name还找到多个的情况)

  • @Resource注解
    默认情况下根据属性name去找bean,找到一个就刚好(不存在找到多个的情况),找不到就根据type去找,如果根据type找到一个就刚刚好,找到多个就报错。(这里需要特别注意,如果使用@Resource(name="xxx")的话,就不会根据type去查找bean了)

# SpringMVC是怎么处理一个请求的?

这里以tomcat作为web服务器为例进行解答,要想解答这个问题,需要对Servlet规范有一定了解~重要的其实还是思想(Spring使用了一个Servlet处理了所有的请求,如果连这点都理解不了,死记硬背这道题的话就是个笑话了)
44_springmvc工作原理

  • 第一步:Tomcat解析到对应的请求,根据Servlet规范定义,转发请求到满足ServletMapping规则的Servlet中。这里SpringMVC配置了一个特定的Servlet-DispatcherServlet,它的映射路径是/*,即只要Tomcat获取到请求,都会交给这个Servlet做处理。
  • 第二步及其他步骤:参照上图即可理解,这个步骤里其实大部分功能也用不到了,现如今的前后端分离架构,后端一般都是接口返回json格式的数据~

# bean的实例化和bean的初始化的区别?

  • bean的实例化
    Spring在创建一个bean对象之前,首先会通过反射创建一个java对象,这个过程就叫做bean的实例化
  • bean的初始化
    在bean实例化完成之后,Spring会对bean对象进行依赖注入,依赖注入完成后,就会进行初始化了。具体来说如果java对象实现了InitlalizingBean接口,Spring会执行该接口下的afterPropertiesSet()方法。

# Spring AOP是如何实现的?它和AspectJ有什么区别?

  • AspectJ
    AspectJ的AOP在编译阶段完成
  • Spring AOP
    Spring AOP基于jdk的动态代理模式实现,如果类不满足增强条件(没有实现接口),Spring会采用CGLIB(底层基于ASM字节码增强)来完成AOP的动作。利用AOP,我们可以做登录校验,权限控制,日志记录等功能。
    Spring AOP和AspectJ其实没多大关系,AOP是一种思想,只不过AspectJ的注解定义的非常好,所以Spring直接拿来主义借鉴了一下。

# Spring中bean的生命周期有哪些步骤?

  • 推断构造方法
  • 实例化
  • 填充属性(依赖注入)
  • 处理Aware回调
  • 初始化前,处理@PostConstruct注解
  • 初始化,处理InitializingBean接口
  • 初始化后,进行AOP处理

# Spring中bean是线程安全的吗?

Spring本身并没有对bean做线程安全的处理,所以:

  • 如果bean是无状态的,那么它是线程安全的
  • 如果bean是有状态的,不做任何处理的情况下它不是线程安全的

# BeanFactory和ApplicationContext的区别?

BeanFactory是bean的工厂,ApplicationContext是Spring容器本身,BeanFactory是ApplicationContext的一个重要组成部分,其他的ApplicationContext组成重要组成部分还有Environment,Scanner扫描器等。

# Spring容器启动流程是什么样的?

  • 首先扫描所有的BeanDefintion,并且存储到Map中
  • 然后再BeanDefintion Map中找出非懒加载的单例Bean定义,实例化并且初始化bean对象。(其他Bean定义有它的实例化以及初始化场景)
  • 利用Bean定义创建bean的过程对应的就是bean的生命周期。
  • 单例bean实例化以及初始化完成后,容器会触发启动完成事件,事件的触发也代表着容器的启动完成。

# Spring中用到了哪些设计模式?

  1. 工厂模式
    Spring中BeanFactory、FactoryBean、ProxyFactory等类用到了工厂模式

  2. 建造者模式
    Spring中BeanDefinitionBuilder用到了建造者模式

  3. 装饰器模式
    Spring中BeanWrapper用到了装饰器模式

  4. 代理模式
    Spring中AOP相关的注解用到了动态代理。

  5. 模板方法模式
    Spring中各种中间件对象都用到了模板方法模式,如数据库连接模板类,Redis模板类等。

# Spring中Aware接口的作用?

Aware接口从字面上翻译过来是感知捕获的含义。单纯的bean(未实现Aware系列接口)是没有知觉的;实现了Aware系列接口的bean可以访问Spring容器。这些Aware系列接口增强了Spring bean的功能,但是也会造成对Spring框架的绑定,增大了与Spring框架的耦合度。(Aware是“意识到的,察觉到的”的意思,实现了Aware系列接口表明:可以意识到、可以察觉到)

# 什么是循环依赖?Spring怎么解决循环依赖?

  • 什么是循环依赖?
    A类有一个属性B,给B类对象b直接赋值或者在A类的构造器中创建B对象,B类包含C对象,同理,C类包含A对象,那么这3个对象都没办法创建

  • 如何解决循环依赖?
    循环依赖有两种,构造器循环依赖和属性循环依赖,构造器循环依赖没有任何解决办法。Spring解决的是属性循环依赖,先加载对象,对象创建完成之后再给对象赋值。

# beanFactory和factoryBean的区别?

  • beanFactory是用来产生bean的工厂,一个容器里只有一个beanFactory,通过getBean方法可以从beanFactory里获取bean对象

  • factoryBean和beanFactory是两个完全不一样的东西,真正理解这两个东西的差异的时候也就能明白为什么面试喜欢面这个问题了。

  • factoryBean的知识点涉及spring的三级缓存机制,理解了三级缓存机制也就理解了factoryBean是干什么的了。

# Spring单例bean的三级缓存?

  • 一级缓存
    存储在singletonObjects Map对象中,一级缓存里的是构建完成的单例bean

  • 二级缓存
    存储在earlySingletonObjects Map对象中,二级缓存存储的是构建中的单例bean

  • 三级缓存
    存储在singletonFactories Map对象中,三级缓存里的是factoryBean对象,通过factoryBean对象的getObject()方法可以创建bean对象,创建的bean对象会被放到二级缓存中,同时,三级缓存里的factoryBean对象会被移除

# SpringBoot中有几种定义bean的方式?

  • @Bean注解
    作用于方法上,方法的返回值必须是一个对象,这个对象默认会作为bean加载到Spring容器里。需要特别注意,使用@Bean注解的方法也是要被Spring管理的(Spring要知道去哪里找被@Bean注解修饰的方法),即需要有@BeansScanner注解配合使用。

  • @Component注解
    作用于类,会创建类的一个实例对象加载到Spring容器中。同理,这里也需要@ComponentScan注解配合使用。

  • @Controller,@RestController,@Service,@Repository注解
    这些注解是Spring自己规定好的一些特定注解,被这些注解修饰的类的对象也会加载到Spring容器中,这些注解我们用的多了,但是问到这个问题的时候,我们很容易遗忘掉这个答案。

  • @ControllerAdvice,@RestControllerAdvice注解
    这两个注解的作用是对Spring的@Controller以及@RestController做增强操作,首先我们要了解默认的@Controller注解的注解处理器做了些什么,我们有什么做增强的场景(比如全局异常处理全局数据绑定全局数据预处理),同时这里也体现了Spring的强大之处(可根据实际业务场景做客制化调整)。

  • @Configuration注解

  • @Import注解

# 如何理解SpringBoot的自动配置?

这里的自动配置指的是Spring基于约定优于配置的原则,通过反射等操作加载特定class类预先给我们创建好我们在开发环境需要用到的bean对象,省去了我们自己配置bean的麻烦~自动装配的核心注解是@SpringBootApplication
在Spring中:
如果我们用到了MyBatis,我们需要配置SqlSessionFactory的Bean对象
如果我们用到了AOP,我们需要配置@EnableAspectJAutoProxy注解
如果用Spring事务,我们需要配置DataSourceTransactionManager的Bean对象
等等...

在SpringBoot中:
我们就不需要做上面的步骤了,因为SpringBoot已经帮我们配置好了,怎么做到的呢? 其实就是SpringBoot预先做的配置,且会通过反射判断是否有核心类,有的话就自动装配对应的bean对象。
如果我们用到了AOP,SpringBoot预先配置好了AopAutoConfiguration对象
如果用Spring事务,SpringBoot预先配置好了DataSourceTransactionManagerAutoConfiguration的Bean对象

# SpringBoot中常用注解及其底层实现?

  1. @SpringBootApplication注解
    该注解是一个复合注解,标注在项目入口类的main入口方法上,标识项目是一个SpringBoot应用,它由下面三个注解复合而成:
  • @SpringBootConfiguration:这个注解标识了SpringBoot启动类也是一个Spring配置类,该注解继承了@Configuration注解。
  • @EnableAutoConfiguration:这个注解标识项目支持自动装配,如数据源dataSource、Mybatis、Spring声明式事务,这些在Spring项目中需要人为配置的内容在SpringBoot中都可以自动装配。
  • @ComponentScan:标识项目的扫描路径,也就是当前入口类所在的包下面的类都会被扫描,如果类中有Spring相关注解,就会被装配到Spring容器。
  1. @Bean注解
    用来定义Bean对象,Bean对象会被加载到BeanDefinitionMap,最终生成一个bean对象并放入Spring容器接受容器的管理调度。
  2. @Controller、@Service、@Mapping、@Autowired等注解

# @SpringBootApplication有什么用,为什么一定要写它?

@SpringBootApplication由三个注解复合而成,分别是@EnableAutoConfiguration,@ComponentScan,@SpringBootConfiguration,分别代表SpringBoot容器支持自动扫描,组件扫描路径是当前包的包路径,且当前类是一个配置类。这3个注解包含了Spring最核心的特性自动装配,因此我们需要写它。

# SpringBoot中spring.factories文件有什么作用?

spring.factories文件是SpringBoot SPI机制(表示扩展机制)。所以这个文件的作用就是对SpringBoot进行扩展的,比如我们会在这个文件中添加ApplicationListener,ApplicationContextInitializer,配置类等等。
SpringBoot在启动的初期就会扫描这些文件,因此对SpringBoot的扩展也变得更加容易了,我们只需要在jar包中添加spring.factories文件即可调整SpringBoot容器。

# SpringBoot启动过程中做了哪些事情?

  • 推断应用类型,一般推断出来是SERVLET容器
  • 根据应用类型创建Spring容器
  • 解析启动类,从而进行扫描,导入自动配置并解析
  • 启动嵌入式容器对象(Tomcat/Jetty)

# 其他

# 在Spring中实现流式调用,文件上传,反向代理

import cn.hutool.http.HttpUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.*;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.io.PrintWriter;
import java.time.Duration;
import java.util.Map;

/**
 * @author xuhaodi
 * dify代理,将请求转发到dify处理
 */
@RestController
@RequestMapping("/dify")
@RequiredArgsConstructor
public class DifyProxyController {

    private final WebClient webClient;

    /**
     * todo Dify-发送会话消息
     *
     * @param requestHeader 请求头
     * @param requestBody   请求体
     * @return Flux<String> 流式json文本输出
     */
    @PostMapping(value = "/v1/chat-messages", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> chatMessages(@RequestHeader Map<String, Object> requestHeader, @RequestBody Map<String, Object> requestBody, HttpServletResponse response) {
        return webClient.post().uri("/v1/chat-messages").header("Authorization", (String) requestHeader.get("authorization")).bodyValue(requestBody).retrieve().bodyToFlux(String.class)
                /*
                    针对流式调用中返回的非200状态码(如401 UNAUTHORIZED),以非流式调用的方式返回响应对象Response
                    lamda表达式所对应的方法要求返回特定返回值,因此最后返回了Flux.empty();后续需要识别时候可以这样返回以及本方法是否可以优化
                 */.onErrorResume(WebClientResponseException.class, webClientResponseException -> {
                    // 获取http状态码
                    HttpStatusCode httpStatusCode = webClientResponseException.getStatusCode();
                    // 响应体(json)
                    String responseBodyJson = webClientResponseException.getResponseBodyAsString();
                    // 设置状态码
                    response.setStatus(httpStatusCode.value());
                    // 设置响应头
                    response.setContentType("application/json");
                    // 写入响应体
                    PrintWriter writer;
                    try {
                        writer = response.getWriter();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    // 写入输出流
                    writer.write(responseBodyJson);
                    // 刷新
                    // 特殊说明:通过HttpServletResponse创建的PrintWriter不需要手动关闭
                    writer.flush();
                    // 返回空对象用以终止流
                    return Flux.empty();
                });
    }

    /**
     * Dify-上传文件(仅支持上传单个文件)
     *
     * @param requestHeader 请求头
     * @param multipartFile 文件对象
     * @param user          用户账号
     * @return @return Mono<ResponseEntity < String>>
     * @throws IOException 文件读写异常
     */
    @PostMapping(value = "/v1/files/upload")
    public Mono<ResponseEntity<String>> filesUpload(@RequestHeader Map<String, Object> requestHeader, @RequestParam("file") MultipartFile multipartFile, @RequestParam("user") String user) throws IOException {
        // 将 MultipartFile 转换为 ByteArrayResource
        ByteArrayResource resource = new ByteArrayResource(multipartFile.getBytes()) {
            @Override
            public String getFilename() {
                return multipartFile.getOriginalFilename(); // 设置文件名
            }
        };
        // 封装builder对象
        MultipartBodyBuilder builder = new MultipartBodyBuilder();
        // 获取前端传入的file对象
        builder.part("file", resource);
        // 获取前端传入的user
        builder.part("user", user);
        // post请求
        return webClient.post()
                // 请求路径
                .uri("/v1/files/upload")
                // 请求头
                .header("Authorization", (String) requestHeader.get("authorization"))
                // contentType
                .contentType(MediaType.MULTIPART_FORM_DATA)
                // 请求体
                .body(BodyInserters.fromMultipartData(builder.build()))
                // 执行请求,获取资源
                .retrieve()
                // 将资源转换成实体(Json字符串)
                .toEntity(String.class)
                // 针对异常做处理,如404 NOT FOUND状态码 将Dify的响应HTTP状态码及响应体(Json字符串)一起返回
                .onErrorResume(WebClientResponseException.class, HttpUtils::difyProxyHttpExceptionResume)
                // 设置接口60s超时
                .timeout(Duration.ofSeconds(60))
                // 设置重试次数
                .retry(3);
    }

    /**
     * Dify-停止响应
     *
     * @param requestHeader 请求头
     * @param requestBody   请求体
     * @return Mono<ResponseEntity < String>>
     */
    @PostMapping(value = "/v1/chat-messages/{task_id}/stop", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Mono<ResponseEntity<String>> stopChatMessages(@PathVariable("task_id") String taskId, @RequestHeader Map<String, Object> requestHeader, @RequestBody Map<String, Object> requestBody) {
        // post请求
        return webClient.post()
                // 请求路径
                .uri("v1/chat-messages/" + taskId + "/stop")
                // 请求头
                .header("Authorization", (String) requestHeader.get("authorization"))
                // 请求体
                .bodyValue(requestBody)
                // 执行请求,获取资源
                .retrieve()
                // 将资源转换成实体(Json字符串)
                .toEntity(String.class)
                // 针对异常做处理,如404 NOT FOUND状态码 将Dify的响应HTTP状态码及响应体(Json字符串)一起返回
                .onErrorResume(WebClientResponseException.class, HttpUtils::difyProxyHttpExceptionResume)
                // 设置接口60s超时
                .timeout(Duration.ofSeconds(60))
                // 设置重试次数
                .retry(3);
    }

    /**
     * Dify-消息反馈(点赞)
     *
     * @param requestHeader 请求头
     * @param requestBody   请求体
     * @return Mono<ResponseEntity < String>>
     */
    @PostMapping(value = "/v1/messages/{message_id}/feedbacks", produces = MediaType.APPLICATION_JSON_VALUE)
    public Mono<ResponseEntity<String>> feedbacks(@PathVariable("message_id") String messageId, @RequestHeader Map<String, Object> requestHeader, @RequestBody Map<String, Object> requestBody) {
        // post请求
        return webClient.post()
                // 请求路径
                .uri("v1/messages/" + messageId + "/feedbacks")
                // 请求头
                .header("Authorization", (String) requestHeader.get("authorization"))
                // 请求体
                .bodyValue(requestBody)
                // 执行请求,获取资源
                .retrieve()
                // 将资源转换成实体(Json字符串)
                .toEntity(String.class)
                // 针对异常做处理,如404 NOT FOUND状态码 将Dify的响应HTTP状态码及响应体(Json字符串)一起返回
                .onErrorResume(WebClientResponseException.class, HttpUtils::difyProxyHttpExceptionResume)
                // 设置接口60s超时
                .timeout(Duration.ofSeconds(60))
                // 设置重试次数
                .retry(3);
    }

    /**
     * Dify-获取下一轮建议问题列表
     *
     * @param requestHeader 请求头
     * @param params        参数
     * @return Mono<ResponseEntity < String>>
     */
    @GetMapping(value = "v1/messages/{message_id}/suggested")
    public Mono<ResponseEntity<String>> messageSuggested(@PathVariable("message_id") String messageId, @RequestHeader Map<String, Object> requestHeader, @RequestParam Map<String, String> params) {
        // get请求
        return webClient.get()
                // 请求路径
                .uri("v1/messages/" + messageId + "/suggested?user=" + params.get("user"))
                // 请求头
                .header("Authorization", (String) requestHeader.get("authorization"))
                // 执行请求,获取资源
                .retrieve()
                // 将资源转换成实体(Json字符串)
                .toEntity(String.class)
                // 针对异常做处理,如404 NOT FOUND状态码 将Dify的响应HTTP状态码及响应体(Json字符串)一起返回
                .onErrorResume(WebClientResponseException.class, HttpUtils::difyProxyHttpExceptionResume)
                // 设置接口60s超时
                .timeout(Duration.ofSeconds(60))
                // 设置重试次数
                .retry(3);
    }


    /**
     * Dify-获取会话历史消息
     *
     * @param requestHeader 请求头
     * @param params        参数
     * @return Mono<ResponseEntity < String>>
     */
    @GetMapping(value = "v1/messages")
    public Mono<ResponseEntity<String>> messages(@RequestHeader Map<String, Object> requestHeader, @RequestParam Map<String, String> params) {
        // get请求
        return webClient.get()
                // 请求路径
                .uri("v1/messages?" + HttpUtil.toParams(params))
                // 请求头
                .header("Authorization", (String) requestHeader.get("authorization"))
                // 执行请求,获取资源
                .retrieve()
                // 将资源转换成实体(Json字符串)
                .toEntity(String.class)
                // 针对异常做处理,如404 NOT FOUND状态码 将Dify的响应HTTP状态码及响应体(Json字符串)一起返回
                .onErrorResume(WebClientResponseException.class, HttpUtils::difyProxyHttpExceptionResume)
                // 设置接口60s超时
                .timeout(Duration.ofSeconds(60))
                // 设置重试次数
                .retry(3);
    }

    /**
     * 获取会话列表
     *
     * @param requestHeader 请求头
     * @param params        参数
     * @return Mono<ResponseEntity < String>>
     */
    @GetMapping(value = "v1/conversations")
    public Mono<ResponseEntity<String>> conversations(@RequestHeader Map<String, Object> requestHeader, @RequestParam Map<String, String> params) {
        // get请求
        return webClient.get()
                // 请求路径
                .uri("v1/conversations?" + HttpUtil.toParams(params))
                // 请求头
                .header("Authorization", (String) requestHeader.get("authorization"))
                // 执行请求,获取资源
                .retrieve()
                // 将资源转换成实体(Json字符串)
                .toEntity(String.class)
                // 针对异常做处理,如404 NOT FOUND状态码 将Dify的响应HTTP状态码及响应体(Json字符串)一起返回
                .onErrorResume(WebClientResponseException.class, HttpUtils::difyProxyHttpExceptionResume)
                // 设置接口60s超时
                .timeout(Duration.ofSeconds(60))
                // 设置重试次数
                .retry(3);
    }

    /**
     * Dify-删除会话
     *
     * @param requestHeader 请求头
     * @param requestBody   请求体
     * @return Mono<ResponseEntity < String>>
     */
    @DeleteMapping(value = "/v1/conversations/{conversation_id}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Mono<ResponseEntity<String>> deleteConversation(@PathVariable("conversation_id") String conversationId, @RequestHeader Map<String, Object> requestHeader, @RequestBody Map<String, Object> requestBody) {
        // 特别说明,如果直接调用webClient.delete()方法,无法传递bodyValue(requestBody)参数,因此需要调整为method(HttpMethod.DELETE)
        return webClient.method(HttpMethod.DELETE)
                // 请求路径
                .uri("v1/conversations/" + conversationId)
                // 请求头
                .header("Authorization", (String) requestHeader.get("authorization"))
                // 请求体
                .bodyValue(requestBody)
                // 执行请求,获取资源
                .retrieve()
                // 将资源转换成实体(Json字符串)
                .toEntity(String.class)
                // 针对异常做处理,如404 NOT FOUND状态码 将Dify的响应HTTP状态码及响应体(Json字符串)一起返回
                .onErrorResume(WebClientResponseException.class, HttpUtils::difyProxyHttpExceptionResume)
                // 设置接口60s超时
                .timeout(Duration.ofSeconds(60))
                // 设置重试次数
                .retry(3);
    }

    /**
     * Dify-会话重命名
     *
     * @param requestHeader 请求头
     * @param requestBody   请求体
     * @return Mono<ResponseEntity < String>>
     */
    @PostMapping(value = "/v1/conversations/{conversation_id}/name")
    public Mono<ResponseEntity<String>> renameConversation(@PathVariable("conversation_id") String conversationId, @RequestHeader Map<String, Object> requestHeader, @RequestBody Map<String, Object> requestBody) {
        // post请求
        return webClient.post()
                // 请求路径
                .uri("v1/conversations/" + conversationId + "/name")
                // 请求头
                .header("Authorization", (String) requestHeader.get("authorization"))
                // 请求体
                .bodyValue(requestBody)
                // 执行请求,获取资源
                .retrieve()
                // 将资源转换成实体(Json字符串)
                .toEntity(String.class)
                // 针对异常做处理,如404 NOT FOUND状态码 将Dify的响应HTTP状态码及响应体(Json字符串)一起返回
                .onErrorResume(WebClientResponseException.class, HttpUtils::difyProxyHttpExceptionResume)
                // 设置接口60s超时
                .timeout(Duration.ofSeconds(60))
                // 设置重试次数
                .retry(3);
    }

    /**
     * todo Dify-语音转文字
     *
     * @param requestHeader 请求头
     * @param requestBody   请求体
     * @return Mono<ResponseEntity < String>>
     */
    @PostMapping(value = "/v1/audio-to-text", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Mono<ResponseEntity<String>> audioToText(@RequestHeader Map<String, Object> requestHeader, @RequestBody Map<String, Object> requestBody) {
        return webClient.post().uri("v1/audio-to-text").header("Authorization", (String) requestHeader.get("authorization")).bodyValue(requestBody).retrieve().toEntity(String.class).onErrorResume(WebClientResponseException.class, HttpUtils::difyProxyHttpExceptionResume);
    }

    /**
     * todo Dify-文字转语音
     *
     * @param requestHeader 请求头
     * @param requestBody   请求体
     * @return Mono<ResponseEntity < String>>
     */
    @PostMapping(value = "/v1/text-to-audio")
    public Mono<ResponseEntity<String>> textToAudit(@RequestHeader Map<String, Object> requestHeader, @RequestBody Map<String, Object> requestBody) {
        // post请求
        return webClient.post()
                // 请求路径
                .uri("v1/text-to-audio")
                // 请求头
                .header("Authorization", (String) requestHeader.get("authorization"))
                // 请求体
                .bodyValue(requestBody)
                // 执行请求,获取资源
                .retrieve()
                // 将资源转换成实体(Json字符串)
                .toEntity(String.class)
                // 针对异常做处理,如404 NOT FOUND状态码 将Dify的响应HTTP状态码及响应体(Json字符串)一起返回
                .onErrorResume(WebClientResponseException.class, HttpUtils::difyProxyHttpExceptionResume)
                // 设置接口60s超时
                .timeout(Duration.ofSeconds(60))
                // 设置重试次数
                .retry(3);
    }

    /**
     * Dify-获取应用基本信息
     *
     * @param requestHeader 请求头
     * @return Mono<ResponseEntity < String>>
     */
    @GetMapping(value = "v1/info")
    public Mono<ResponseEntity<String>> info(@RequestHeader Map<String, Object> requestHeader) {
        // get请求
        return webClient.get()
                // 请求路径
                .uri("v1/info")
                // 请求头
                .header("Authorization", (String) requestHeader.get("authorization"))
                // 执行请求,获取资源
                .retrieve()
                // 将资源转换成实体(Json字符串)
                .toEntity(String.class)
                // 针对异常做处理,如404 NOT FOUND状态码 将Dify的响应HTTP状态码及响应体(Json字符串)一起返回
                .onErrorResume(WebClientResponseException.class, HttpUtils::difyProxyHttpExceptionResume)
                // 设置接口60s超时
                .timeout(Duration.ofSeconds(60))
                // 设置重试次数
                .retry(3);
    }

    /**
     * Dify-获取应用参数
     *
     * @param requestHeader 请求头
     * @return Mono<ResponseEntity < String>>
     */
    @GetMapping(value = "v1/parameters")
    public Mono<ResponseEntity<String>> parameters(@RequestHeader Map<String, Object> requestHeader) {
        // get请求
        return webClient.get()
                // 请求路径
                .uri("v1/parameters")
                // 请求头
                .header("Authorization", (String) requestHeader.get("authorization"))
                // 执行请求,获取资源
                .retrieve()
                // 将资源转换成实体(Json字符串)
                .toEntity(String.class)
                // 针对异常做处理,如404 NOT FOUND状态码 将Dify的响应HTTP状态码及响应体(Json字符串)一起返回
                .onErrorResume(WebClientResponseException.class, HttpUtils::difyProxyHttpExceptionResume)
                // 设置接口60s超时
                .timeout(Duration.ofSeconds(60))
                // 设置重试次数
                .retry(3);
    }

    /**
     * Dify-获取应用Meta信息
     * @param requestHeader 请求头
     * @return Mono<ResponseEntity < String>>
     */
    @GetMapping(value = "v1/meta")
    public Mono<ResponseEntity<String>> meta(@RequestHeader Map<String, Object> requestHeader) {
        // get请求
        return webClient.get()
                // 请求uri
                .uri("v1/meta")
                // 请求头
                .header("Authorization", (String) requestHeader.get("authorization"))
                // 执行请求,获取资源
                .retrieve()
                // 将资源转换成实体(Json字符串)
                .toEntity(String.class)
                // 针对异常做处理,如404 NOT FOUND状态码 将Dify的响应HTTP状态码及响应体(Json字符串)一起返回
                .onErrorResume(WebClientResponseException.class, HttpUtils::difyProxyHttpExceptionResume)
                // 设置接口60s超时
                .timeout(Duration.ofSeconds(60))
                // 设置重试次数
                .retry(3);
    }
}

import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;

import java.util.Map;

public class HttpUtils {
    /**
     * 针对Dify接口返回的非200状态码做封装处理
     * @param exception 异常
     * @return Mono<? extends ResponseEntity < String>>
     */
    public static Mono<? extends ResponseEntity<String>> difyProxyHttpExceptionResume(WebClientResponseException exception) {
        // 获取http状态码
        HttpStatusCode httpStatusCode = exception.getStatusCode();
        // 响应体(json)
        String responseBodyJson = exception.getResponseBodyAsString();
        // 处理错误
        return Mono.just(ResponseEntity.status(httpStatusCode).contentType(MediaType.APPLICATION_JSON).body(responseBodyJson));
    }
}