在有了Web开发的前提下，如果对数据访问场景进行开发，那么就是一个非常完整的业务流程了。通过SpringBoot整合SSM使用 [label color="green"]Spring Initializr[/label] 创建向导快速构建boot工程。以后使用这样的方式。在向导中勾选对应需要的启动器，比如新版的数据库驱动、mybatis场景、web场景...SpringBoot整合MyBatis在 [label color="green"]application.properties[/label] 中配置对应的基本数据源参数#1.三大框架整合，最先配置数据源信息 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.type=com.zaxxer.hikari.HikariDataSource #配置对应的数据库信息 spring.datasource.url=jdbc:mysql://localhost:3306/test spring.datasource.username= spring.datasource.password=继续在此文件中，配置整合MyBatis。在启动类的子包创建Bean包，添加对应Bean类和@Data注解，根据对应的数据库，来封装表的数据。在启动类的子包创建Mapper包，添加一个接口，用来查表数据的，与此还需要有一个XML文件与之对应。在resources(类路径)创建一个mapper包，用来存放XML文件。单机UserMapper接口类使用插件快速创建XML文件。安装MyBatisX插件，帮我们生成Mapper接口的XML文件即可。我们在Mapper接口中书写方法，使用 [label color="red"]alt + enter[/label] 快速识别生成XML标签，我们需要手动在标签体书写对应的SQL语句。为了方便传参(#{})，我们需要在对应的接口形参位置添加 [label color="pink"]@Param("")[/label] 注解。UserMapper接口/** * @author: TangZhiKai * @create: 2023-10-25 15:38 * @description: 接口，用来查t_user表数据的，还需要有个xml文件与之对应 **/ public interface UserMapper { /** * 1.每个方法都在Mapper文件中有一个SQL标签对应， * 2.所有参数都应该用@Param进行签名，以后使用指定的名字在SQL中取值 * @param id * @return */ //使用插件给形参添加@Param注解，以后XML文件中就用 #{}来取,这是一个良好的习惯 public TUser getUserById(@Param("id") Long id); }启动类中添加注解@MapperScan/** * 1.使用@MapperScan 告诉MyBatis，扫描哪个包下面的所有接口 * 2.通过properties文件配置配置项，使用mybatis.mapper-locations告诉MyBatis，每个接口的XML文件都在哪里。 * 3.MyBatis自动关联绑定 */ @MapperScan(basePackages = "com.atguigu.boot3.ssm.mapper") @SpringBootApplication public class Boot305SsmApplication { public static void main(String[] args) { SpringApplication.run(Boot305SsmApplication.class, args); } }修改配置文件配置文件properties中需要指定，扫描类路径下对应的包文件 mybatis.mapper-locations=classpath:/mapper/*.xml 那么实际上就会从当前包中寻找XML文件，然后对应再文件的命名空间，在找到实际的Mapper接口。实现接口和XML文件的关联。打开对应的驼峰命名规则，这样可以不用在SQL中起别名。# 2.配置整合MyBatis #指定接口的XML文件为类路径下mapper中的所有文件 mybatis.mapper-locations=classpath:/mapper/*.xml #开启驼峰命名规则 mybatis.configuration.map-underscore-to-camel-case=true写Controller、(Service）测试/** * @author: TangZhiKai * @create: 2023-10-25 16:11 * @description: 测试MyBatis的整合 **/ @RestController public class UserController { //理论上还有一层Service,这里直接跳过 @Autowired UserMapper userMapper;//如果有报错，本质IDEA不知道UserMapper是容器中的组件 /** * 返回User的json数据 * @param id * @return */ @GetMapping("/user/{id}") public TUser getUser(@PathVariable("id") Long id){ TUser user = userMapper.getUserById(id); return user; } } 流程看上去有点复杂，没事我录视频用于复习回顾了捏:)[video title="SpringBoot整合MyBatis " url="https://img.kaijavademo.top/typecho/uploads/2023/10/videos/%E6%95%B4%E5%90%88MyBatis.mkv " container="by3aau4du5w" subtitle=" " poster=" "] [/video]基础特性SpringApplicationSpringApplication程序的主入口有一些特性。package com.atguigu.boot3features; import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; @SpringBootApplication //主程序类 public class Boot306FeaturesApplication { public static void main(String[] args) { //1.SpringApplication:Boot应用的核心API入口 /* run方法 参数一：注解标注的主程序类 参数二：main中的参数args */ //SpringApplication.run(Boot306FeaturesApplication.class, args); //我们可以拆成两步来写，这样的好处是自定义Spring应用 //1.自定义SpringApplication的底层设置 //SpringApplication application = new SpringApplication(Boot306FeaturesApplication.class); //调整SpringApplication的参数 //application.setDefaultProperties(); //以Banner为例,之前我们以配置文件来配置，现在我们以程序来配置 /* 这个配置不优先.【配置文件优先级高于程序化调整的优先级】 表明我们既可以通过配置文件来调整底层行为，也可以通过程序化调整底层参数 */ //application.setBannerMode(Banner.Mode.OFF); //2.SpringApplication运行起来 //application.run(args); //------------------------------------------------ //2.通过Builder的方式，来构建出Spring应用,通过FluentBuilder API进行链式调用 new SpringApplicationBuilder() //告诉SpringBoot的主程序是哪个 .main(Boot306FeaturesApplication.class) //除了主程序外还需要启动源 .sources(Boot306FeaturesApplication.class) .bannerMode(Banner.Mode.CONSOLE) //.listeners(null) //.environment(null) .run(args); } }ProfilesSpring Profiles 提供一种隔离配置的方式，使其仅在特定环境生效；Profiles提供了环境的隔离能力，便于我们快速切换开发、测试、生产环境。使用步骤：标识环境：指定哪些组件、配置在哪个环境下生效package com.atguigu.boot3.features; /** * Profiles * 1.标识环境 * 1)、区分出几个环境：dev(随便起,这里我们认为是开发环境)、test(测试环境)、prod(生产环境) * 2)、指定每个组件在哪个环境下生效：default环境:默认环境 * 通过：@Profile({"test"})标注 * 标注在类上，只有指定环境被激活，整个类的所有配置才能生效 * 如果组件没有标注@Profile 代表任意时候都生效 * 3)、通过测试得知：默认只有激活指定的环境，这些组件才会生效 * 2.激活环境 * 配置文件激活：spring.profiles.active=dev; * 命令行激活:java -jar xxx.jar --spring.profiles.active=dev * 在Edit Configurations中找到Programs arguments 然后Build ans run 设置--spring.profiles.active=dev * */ @Slf4j @SpringBootApplication //主程序类 public class Boot306FeaturesApplication { public static void main(String[] args) { //1.SpringApplication:Boot应用的核心API入口 /* run方法 参数一：注解标注的主程序类 参数二：main中的参数args */ //SpringApplication.run(Boot306FeaturesApplication.class, args); //我们可以拆成两步来写，这样的好处是自定义Spring应用 //1.自定义SpringApplication的底层设置 //SpringApplication application = new SpringApplication(Boot306FeaturesApplication.class); //调整SpringApplication的参数 //application.setDefaultProperties(); //以Banner为例,之前我们以配置文件来配置，现在我们以程序来配置 /* 这个配置不优先.【配置文件优先级高于程序化调整的优先级】 表明我们既可以通过配置文件来调整底层行为，也可以通过程序化调整底层参数 */ //application.setBannerMode(Banner.Mode.OFF); //2.SpringApplication运行起来 //application.run(args); //------------------------------------------------ //2.通过Builder的方式，来构建出Spring应用,通过FluentBuilder API进行链式调用 ConfigurableApplicationContext context = new SpringApplicationBuilder() //告诉SpringBoot的主程序是哪个 .main(Boot306FeaturesApplication.class) //除了主程序外还需要启动源 .sources(Boot306FeaturesApplication.class) .bannerMode(Banner.Mode.CONSOLE) //.listeners(null) //.environment(null) .run(args); //测试环境标识检查组件中是否有这几个对象 try { Cat cat = context.getBean(Cat.class); log.info("组件Cat:{}", cat); } catch (Exception e) { } try { Dog dog = context.getBean(Dog.class); log.info("组件Dog:{}", dog); } catch (Exception e) { } try { Pig pig = context.getBean(Pig.class); log.info("组件Pig:{}", pig); } catch (Exception e) { } try { Sheep sheep = context.getBean(Sheep.class); log.info("组件Sheep:{}", sheep); } catch (Exception e) { } } }这里写了好多类，以某个类为例。package com.atguigu.boot3.features.bean; /** * @author: TangZhiKai * @create: 2023-09-30 17:22 * @description: 组件多环境识别能力 ：标识Cat在测试环境下才有 **/ @Profile({"dev"}) //标注dev环境下生效，数组可以写多个 //@Component public class Cat { private Long id; private String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Cat(Long id, String name) { this.id = id; this.name = name; } public Cat() { } } 当然，这里指定环境也可以使用配置类中添加Bean组件的方式提供Bean类对象，并在其方法上添加@Profile注解。但是需要注意类上的注解和方法上注解的区别。切换环境：这个环境对应的所有组件和配置就应该生效激活环境两种方式，在上面的 [label color="green"]Boot306FeaturesApplication[/label] 中提到了，具体自行查阅配置文件激活： spring.profiles.active=dev命令行激活: java -jar xxx.jar --spring.profiles.active=dev包含环境#包含指定的环境，不管激活哪个环境，这个都要有,总是要生效的环境 spring.profiles.include=dev,test[note type="primary flat"]1.最佳实战：生效的环境 = 激活的环境 /默认的环境 + 包含的环境2.项目里面这么用①基础的配置mybatis、log、xxx写到包含环境中②需要动态切换变化的用database、redis、mq写到激活的环境中[/note]环境分组#指定激活指定的一个或多个环境 spring.profiles.active=haha #profiles分组，需要使用的时候激活整个组就行 spring.profiles.group.haha=dev,test spring.profiles.group.hehe=prod,abc #也可以写成这种形式 #spring.profiles.group.haha[0]=dev #spring.profiles.group.haha[1]=testprofile配置文件/** * Profiles * 1.标识环境 * 1)、区分出几个环境：dev(随便起,这里我们认为是开发环境)、test(测试环境)、prod(生产环境) * 2)、指定每个组件在哪个环境下生效：default环境:默认环境 * 通过：@Profile({"test"})标注 * 标注在类上，只有指定环境被激活，整个类的所有配置才能生效 * 如果组件没有标注@Profile 代表任意时候都生效 * 3)、通过测试得知：默认只有激活指定的环境，这些组件才会生效 * 2.激活环境 * 配置文件激活：spring.profiles.active=dev; * 命令行激活:java -jar xxx.jar --spring.profiles.active=dev * 在Edit Configurations中找到Programs arguments 然后Build ans run 设置--spring.profiles.active=dev * 3.配置文件如何使用Profile功能 * 1)、application.properties：主配置文件，任何情况下都生效 * 2)、其他profile环境下命名规范：application-{profile标识}.properties/yaml: * 比如：application-dev.properties * 3)、激活指定环境即可：配置文件、命令行激活 * 4)、效果： * 项目的所有生效配置项 = 激活环境配置文件的所有项 + 主配置文件和激活文件不冲突的所有项 * 如果发生了配置冲突，以激活的环境配置文件为准。 * application-{profile标识}.properties 优先级 application.properties * * 主配置和激活的配置都生效，优先以激活的配置为准 */核心原理package com.atguigu.boot3.core.listener; /** * @author: TangZhiKai * @create: 2023-10-29 17:48 * @description: 应用监听器，监听SpringBoot应用生命周期 * 启动的顺序就是这些方法默认调用的顺序 **/ /** * 生命周期的流程分为引导、启动、运行 * Listener先要从 META-INF/spring.factories 读到 * 1.引导： * 利用BootstapContext 引导整个项目启动 * starting: 应用开始，SpringApplication的run方法一调用，只要有了BootstrapContext 就直接就执行 * environmentPrepared: 环境准备好(把启动参数等绑定到环境变量中), 但是IoC容器还没有创建；【调一次】 * 2.启动： * contextPrepared： IoC容器创建并准备好，但是sources(主配置类)还没有加载。并关闭引导上下文；组件都没有创建【调一次】 * contextLoaded： IoC容器加载，主配置类加载进去了。但是IoC还没刷新(我们的Bean没创建). * ==============截止以前，IoC容器里面还没造bean呢============== * started： IoC容器刷新了(所有Bean造好了)，但是runner没调用。 * ready： IoC容器刷新了(所有Bean造好了),runner调用完成了。context.isRunning() * 3.运行： * 如果以前步骤都正确执行，代表容器running。 */ public class MyAppListener implements SpringApplicationRunListener { /** * @param bootstrapContext 传入的是启动上下文 * 先是启动，把基本的东西给启动起来后， 才启动后面的ConfigurableApplicationContext IoC容器 */ @Override public void starting(ConfigurableBootstrapContext bootstrapContext) { System.out.println("========starting=======正在启动============="); } @Override public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) { System.out.println("========environmentPrepared======环境准备完成==========="); } @Override public void contextPrepared(ConfigurableApplicationContext context) { System.out.println("========contextPrepared======上下文(IoC容器)准备完成==========="); } @Override public void contextLoaded(ConfigurableApplicationContext context) { System.out.println("========contextLoaded======IoC容器加载完成==========="); } @Override public void started(ConfigurableApplicationContext context, Duration timeTaken) { System.out.println("========started======启动完成==========="); } @Override public void ready(ConfigurableApplicationContext context, Duration timeTaken) { System.out.println("========ready======准备就绪==========="); } @Override public void failed(ConfigurableApplicationContext context, Throwable exception) { System.out.println("========failed======应用启动失败==========="); } } 自定义starter我们可以通过自定义starter的方式，让别人导入我们的starter，就实现对应的功能，并且对于我们提供的启动器具有一定的可扩展性。[note type="primary flat"]场景：抽取聊天机器人场景，它可以打招呼。效果：任何项目导入此starter都具有打招呼功能，并且问候语中的人名需要可以在配置文件中修改[/note]场景整合整合Redis导入场景启动器，然后 [label color="orange"]配置文件[/label] 中配置主机和密码，然后使用@Autowired注入就可以使用 org.springframework.boot spring-boot-starter-data-redis 我录了一段视频，来记录，主要是过程还是有点复杂，单纯两句话说不明白。[video title="整合Redis " url="https://img.kaijavademo.top/typecho/uploads/2023/11/%E6%95%B4%E5%90%88Redis.mkv " container="bkzab6to3" subtitle=" " poster=" "] [/video]接口文档前后分离的方式，后台人员写好功能以后。需要给前端暴漏接口文档，描述我们的功能，前端应该如何发请求，带什么参数，如何响应...传统的方式是写成一个word交给前端，可维护性太差了，联调不方便。我们通过整合Swagger，生成一个实时的接口文档，方便整个前后的沟通，而且Swagger遵循 [label color="blue"]OpenAPI[/label] 规范，适配性、跨平台非常好。远程调用无论是哪种远程调用，都需要使用响应式的包来支持 org.springframework.boot spring-boot-starter-webflux WebClient除了使用postman来测试，我们可以使用WebClient模拟远程调用，WebClient是一个非阻塞、响应式的HTTP客户端，我们只需要创建一个WebClient，然后配置请求方式、请求路径等向服务器发送请求，并接收请求即可。通过远程调用阿里云天气控制API [label color="blue"]WeatherController[/label]package com.atguigu.boot3.rpc.controller; import com.atguigu.boot3.rpc.service.WeatherService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; /** * @author: TangZhiKai * @create: 2023-11-29 14:25 * @description: 天气控制器 **/ @RestController public class WeatherController { @Autowired WeatherService weatherService; /** * 测试远程 * @param city * @return */ //@GetMapping("/weather/{city}") @GetMapping("/weather") public Mono<String> weather(@RequestParam("city") String city){//PathVariable路径变量，或者用请求参数RequestParam来传递 //查询天气 Mono<String> weather = weatherService.weather(city); return weather; } } [label color="green"]WeatherService [/label]package com.atguigu.boot3.rpc.service; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; import java.util.HashMap; import java.util.Map; /** * @author: TangZhiKai * @create: 2023-11-29 14:27 * @description: **/ @Service public class WeatherService { /** * 使用非阻塞、响应式HTTP客户端WebClient 调用阿里云API * @param city * @return */ public Mono<String> weather(String city){ //远程调用阿里云API //1.创建WebClient,得到客户端 //WebClient client = WebClient.create("https://ali-weather.showapi.com/area-to-weather-date?"); WebClient client = WebClient.create(); //2.准备数据 Map<String,String> params = new HashMap<>(); params.put("area",city); //3.定义发请求的行为,如果接收者是一个Mono代表非阻塞的方式 CompletableFuture Mono<String> mono = client.get() //定义发送请求路径信息 {}代表变量的方式 .uri("https://ali-weather.showapi.com/area-to-weather-date?area={area}", params) //定义响应的内容类型 .accept(MediaType.APPLICATION_JSON) //定义请求头 .header("Authorization", "APPCODE xxx") //定义请求头 //如果是POST请求，还需要定义请求体 //.body() //调用查询方法 .retrieve() //接收响应数据,响应体里面的数据转换成想要的类型，json->String .bodyToMono(String.class); //使用响应式编程 //mono相当于一个发布者，我们可以使用subscribe来订阅数据 /*Mono<String> mono = client.get() .uri("https://ali-weather.showapi.com/area-to-weather-date?area={area}", params) .accept(MediaType.APPLICATION_JSON) .header("Authorization", "APPCODE xxxx") .retrieve() .bodyToMono(String.class); mono.subscribe(val->{ System.out.println(); })*/ //返回字符串的天气数据 return mono; } } 发送对应请求，测试，返回到页面上的都是json数据。HttpInterface上面的远程调用写起来感觉并不是很方便，Spring还为我们提供一种更抽象的方式。我们只需要写一个接口，并定义好这个接口，然后为接口创建好代理对象，调用代理对象就行。[label color="green"]WeatherInterface [/label]package com.atguigu.boot3.rpc.service; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.service.annotation.GetExchange; import reactor.core.publisher.Mono; /** * @author: TangZhiKai * @create: 2023-11-29 15:18 * @description: 天气查询接口 **/ public interface WeatherInterface { /** * 在接口中定义好，发什么请求，要什么东西 * @param city * @return * @GetExchange 参数url代表给哪里发送请求，基本路径 * 参数accept表明接收什么类型的数据 application/json */ @GetExchange(url = "/area-to-weather-date",accept = "application/json") //发送Get请求，所以使用GetExchange //使用@RequestParam注解，谁给我传递的city参数，未来我给某个地方发送请求，我给某个值以area的参数名发送出去 Mono<String> getWeather(@RequestParam("area") String city, //定义请求头 @RequestHeader("Authorization") String auth); } [label color="green"]WeatherService [/label]@Service public class WeatherService { //未来我们可以把接口写这里直接自动注入进来 //WeatherInterface weatherInterface; /** * 使用HTTP Interface * @param city * @return */ public Mono<String> weather(String city){ //Mono<String> mono = getByWebClient(city); //使用接口来调用 //1、创建客户端 WebClient client = WebClient.builder() .baseUrl("https://ali-weather.showapi.com") .codecs(clientCodecConfigurer -> { clientCodecConfigurer .defaultCodecs() .maxInMemorySize(256*1024*1024); //响应数据量太大有可能会超出BufferSize，所以这里设置的大一点 }) .build(); //2、创建工厂 HttpServiceProxyFactory接口代理工厂 HttpServiceProxyFactory factory = HttpServiceProxyFactory .builder(WebClientAdapter.forClient(client)).build(); //3、获取代理对象 WeatherInterface weatherAPI = factory.createClient(WeatherInterface.class); Mono<String> weather = weatherAPI.getWeather(city, "APPCODE xxxx"); //返回字符串的天气数据,如果是String需要封装成一个Mono.just(weather) return weather; } }这样的方式我们还可以进行一定升级，方便我们可以把接口写好，直接自动注入进来。我们可以写一个配置类[note type="primary flat"]抽取[/note]WeatherConfigurationpackage com.atguigu.boot3.rpc.config; import com.atguigu.boot3.rpc.service.WeatherInterface; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.support.WebClientAdapter; import org.springframework.web.service.invoker.HttpServiceProxyFactory; /** * @author: TangZhiKai * @create: 2023-11-29 15:58 * @description: 配置类，负责容器中放组件 **/ @Configuration public class WeatherConfiguration { @Bean WeatherInterface weatherInterface(){ //1、创建客户端 WebClient client = WebClient.builder() .baseUrl("https://ali-weather.showapi.com") .codecs(clientCodecConfigurer -> { clientCodecConfigurer .defaultCodecs() .maxInMemorySize(256*1024*1024); //响应数据量太大有可能会超出BufferSize，所以这里设置的大一点 }) .build(); //2、创建工厂 HttpServiceProxyFactory接口代理工厂 HttpServiceProxyFactory factory = HttpServiceProxyFactory .builder(WebClientAdapter.forClient(client)).build(); //3、获取代理对象 WeatherInterface weatherInterface = factory.createClient(WeatherInterface.class); //以后自动注入的就是weatherInterface代理对象 return weatherInterface; } } [note type="primary flat"]进一步更通用的抽取[/note]假设我这里不止一个功能，比如我还有发短信和其他的功能，我可以将工厂抽取成一个Bean对象，放到容器中，提供一个工厂。 [label color="green"]WeatherConfiguration[/label]package com.atguigu.boot3.rpc.config; import com.atguigu.boot3.rpc.service.WeatherInterface; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.support.WebClientAdapter; import org.springframework.web.service.invoker.HttpServiceProxyFactory; /** * @author: TangZhiKai * @create: 2023-11-29 15:58 * @description: 配置类，负责容器中放组件 **/ @Configuration public class WeatherConfiguration { @Bean HttpServiceProxyFactory httpServiceProxyFactory(){ //1、创建客户端 WebClient client = WebClient.builder() .codecs(clientCodecConfigurer -> { clientCodecConfigurer .defaultCodecs() .maxInMemorySize(256*1024*1024); //响应数据量太大有可能会超出BufferSize，所以这里设置的大一点 }) .build(); //2、创建工厂 HttpServiceProxyFactory接口代理工厂 HttpServiceProxyFactory factory = HttpServiceProxyFactory .builder(WebClientAdapter.forClient(client)).build(); return factory; } @Bean WeatherInterface weatherInterface(HttpServiceProxyFactory httpServiceProxyFactory){ //3、获取代理对象 WeatherInterface weatherInterface = httpServiceProxyFactory.createClient(WeatherInterface.class); //以后自动注入的就是weatherInterface代理对象 return weatherInterface; } } [note type="primary flat"]最后抽取，请求头相同的部分，我们可以抽取出来，写成一个默认的[/note] [label color="green"]WeatherConfiguration[/label]package com.atguigu.boot3.rpc.config; import com.atguigu.boot3.rpc.service.ExpressApi; import com.atguigu.boot3.rpc.service.WeatherInterface; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.support.WebClientAdapter; import org.springframework.web.service.invoker.HttpServiceProxyFactory; /** * @author: TangZhiKai * @create: 2023-11-29 15:58 * @description: 配置类，负责容器中放组件 **/ @Configuration public class WeatherConfiguration { @Bean HttpServiceProxyFactory httpServiceProxyFactory(){ //1、创建客户端 WebClient client = WebClient.builder() //如果我需要使用阿里云的APIcode我可以在此处传递一个默认头 .defaultHeader("Authorization","APPCODE xxxx") .codecs(clientCodecConfigurer -> { clientCodecConfigurer .defaultCodecs() .maxInMemorySize(256*1024*1024); //响应数据量太大有可能会超出BufferSize，所以这里设置的大一点 }) .build(); //2、创建工厂 HttpServiceProxyFactory接口代理工厂 HttpServiceProxyFactory factory = HttpServiceProxyFactory .builder(WebClientAdapter.forClient(client)).build(); return factory; } @Bean WeatherInterface weatherInterface(HttpServiceProxyFactory httpServiceProxyFactory){ //3、获取代理对象 WeatherInterface weatherInterface = httpServiceProxyFactory.createClient(WeatherInterface.class); //以后自动注入的就是weatherInterface代理对象 return weatherInterface; } /** * 模拟直接使用工厂帮他创建出代理对象，放在容器中 * @param httpServiceProxyFactory 工厂 * @return */ @Bean ExpressApi expressApi(HttpServiceProxyFactory httpServiceProxyFactory){ ExpressApi client = httpServiceProxyFactory.createClient(ExpressApi.class); return client; } } 实际生产中，因为账号不一样，自然不能直接简单抽取成默认请求头中放入appcode，我们可以在 [label color="green"]配置文件[/label] 中配置， [label color="green"]aliyun.appcode=xxx[/label] ,我们可以通过字符串拼接来形成默认的请求头。项目一启动，我们创建好代理工厂，工厂帮我们创建好每个HttpInterface代理对象，以后所有的Controller自动注入代理对象，就能远程调用。每个接口声明了远程调用的地址，什么样的数据媒体类型，发送什么样的参数[label color="green"]WeatherInterface [/label]public interface WeatherInterface { /** * 在接口中定义好，发什么请求，要什么东西 * @param city * @return * @GetExchange 参数url代表给哪里发送请求，基本路径 * 参数accept表明接收什么类型的数据 application/json */ @GetExchange(url = "https://ali-weather.showapi.com/area-to-weather-date",accept = "application/json") //发送Get请求，所以使用GetExchange //使用@RequestParam注解，谁给我传递的city参数，未来我给某个地方发送请求，我给某个值以area的参数名发送出去 Mono<String> getWeather(@RequestParam("area") String city); }我书写这篇文章的初衷就是总结学习的进度，遗忘之际可以拿出来翻看，如有不对的地方还望指正，多多海涵。
基于JDK17 + SpringBoot3的入门笔记，快速搭建 + 常用注解[video title="快速搭建 " url="https://img.kaijavademo.top/typecho/uploads/2023/10/videos/%E5%BF%AB%E9%80%9F%E6%90%AD%E5%BB%BA.mkv " container="beu1prgsiin" subtitle=" " poster=" "] [/video]常用注解[video title="常用注解 " url="https://img.kaijavademo.top/typecho/uploads/2023/10/videos/%E5%B8%B8%E7%94%A8%E6%B3%A8%E8%A7%A3.mkv " container="baqik9157t" subtitle=" " poster=" "] [/video]/** * @author: TangZhiKai * @create: 2023-09-30 17:24 * @description: 配置类 **/ @Import(FastsqlException.class) //导入第三方类进入IoC容器，给容器中放指定类型的组件，组件的名字默认是全类名 @SpringBootConfiguration //这是一个配置类，替代以前的配置文件.配置类本身也是容器中的组件 //@Configuration //这是一个配置类，替代以前的配置文件.配置类本身也是容器中的组件 public class AppConfig { /** * 1.组件默认是单实例的 * @return */ @Scope("prototype") //可以修改为多实例 @Bean("userHaha") //替代以前的Bean标签.组件在容器中的名字默认是方法名 //可以直接修改注解的值在Bean标签中 public User user(){ User user = new User(); user.setId(1L); user.setName("张三"); return user; } /*@Bean public FastsqlException fastsqlException(){ return new FastsqlException(); }*/ } 条件注解如果注解指定的条件成立，则触发指定行为@ConditionalOnClass：如果类路径中存在这个类，则触发指定行为@ConditionalOnMissingClass：如果类路径中不存在这个类，则触发指定行为@ConditionalOnBean：如果容器中存在这个Bean(组件)，则触发指定行为@ConditionalOnMissingBean：如果容器中不存在这个Bean(组件)，则触发指定行为....//@ConditionalOnClass可以直接标到类上，当系统中存在这个类，以下所有东西全部生效 //放在类级别，如果注解判断生效，则整个配置类才生效 @ConditionalOnClass(name = "com.alibaba.druid.FastsqlException") @SpringBootConfiguration public class AppConfig2 { //当类路径中存在这个FastsqlException类，则生效 //放在方法级别，只是单独对这个方法进行注解判断 @ConditionalOnClass(name = "com.alibaba.druid.FastsqlException") @Bean public Cat cat01() { return new Cat(); } @ConditionalOnMissingClass(value = "com.alibaba.druid.FastsqlException") @Bean public Dog dog01() { return new Dog(); } //当容器中有dog的对象，就向容器中添加一个张三 @ConditionalOnBean(value = Dog.class) @Bean public User zhangsan() { return new User(); } //当容器中没有dog对象，就向容器中添加一个李四 @ConditionalOnMissingBean(value = Dog.class) @Bean public User lisi() { return new User(); } }属性绑定将容器中任意组件(Bean)的属性值和配置文件的配置项进行绑定@ConfigurationProperties： 声明组件的属性和配置文件哪些前缀开始项进行绑定@EnableConfigurationProperties：快速注册注解,也用于导入第三方写好的组件进行属性绑定。(=@Import + @ConfigurationProperties)SpringBoot默认只扫描自己主程序所在的包，如果导入第三方包，即使组件标注了@Component、@ConfigurationProperties注解也没用，因为组件扫描不进来。使用方法：给容器中注册组件(@Component、@Bean)使用 [label color="purple"]@ConfigurationProperties声明组件和配置文件的哪些配置项进行绑定[/label][video title="属性绑定 " url="https://img.kaijavademo.top/typecho/uploads/2023/10/videos/%E5%B1%9E%E6%80%A7%E7%BB%91%E5%AE%9A.mkv " container="bbbqtqjhwcw" subtitle=" " poster=" "] [/video]核心流程总结对于自动配置流程的一个核心流程总结：导入starter 场景启动器 ,就会导入autoconfigure包。autoconfigure包里面有一个文件META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports，里面指定所有启动要加载的自动配置类。@EnableAutoConfiguration 会自动的把上面的文件里面写的所有自动配置类都导入进来。xxxAutoConfiguration是有条件注解进行按需加载。xxxAutoConfiguration给容器中导入一堆组件，组件都是从xxxProperties中提取属性值xxxProperties又是和配置文件进行了绑定效果：导入starter、修改配置文件，就能修改底层行为。自我的角度谈谈如何学好下面是我自己对于springboot学习的一个流程小总结，留有视频以便回顾。[video title="浅谈 " url="https://img.kaijavademo.top/typecho/uploads/2023/10/videos/%E6%B5%85%E8%B0%88%E7%90%86%E8%A7%A3%E5%AD%A6%E4%B9%A0.mkv " container="bl0yl656oip" subtitle=" " poster=" "] [/video]Web开发最佳实践：给容器中写一个配置类@Configuration实现 WebMvcConfigurer但是不要标注 @EnableWebMvc注解(此注解表示禁用自动配置效果)，实现手自一体的效果。自定义静态资源的配置规则自定义静态资源路径、自定义缓存规则配置方式spring.mvc:静态资源访问前缀路径spring.web:①静态资源目录②静态资源缓存策略# 一.spring.web: # 1.配置国际化的区域信息 # 2.配置静态资源策略(是否功能开启、处理链、缓存) #开启静态资源映射规则 spring.web.resources.add-mappings=true # 设置缓存 spring.web.resources.cache.period=3600 # 缓存详细合并项控制，覆盖period配置：浏览器第一次请求服务器，服务器告诉浏览器此资源缓存7200秒，浏览器就会把它放到本地7200秒 # 7200秒以内的所有此资源访问不用发给服务器请求，7200秒以后发请求给服务器 spring.web.resources.cache.cachecontrol.max-age=7200 # 设置共享缓存，表明这些静态资源是共享缓存 spring.web.resources.cache.cachecontrol.cache-public=true # 使用资源last-modified 最后一次修改时间，来对比服务器和浏览器的资源是否相同没有发生变化 ,如果相同返回304 spring.web.resources.cache.use-last-modified=true #spring.web下面专门负责静态资源的规则配置，缓存时间... #spring.web. # 自定义静态资源文件夹，会扫描当前静态资源文件夹下的文件 spring.web.resources.static-locations=classpath:/a/,classpath:/b/,classpath:/static/ #2. spring.mvc ## 2.1 自定义webjars访问路径前缀 spring.mvc.webjars-path-pattern=/wj/** ## 2.2 静态资源通用的访问路径前缀 spring.mvc.static-path-pattern=/static/** 代码方式[label color="red"]容器中只要有一个 WebMvcConfigurer 组件。配置的底层行为都会生效[/label]第一种实现方式采用手动挡+自动挡的实现方式，在主程序下面书写一个配置类，然后添加@Configuration,并实现 [label color="blue"]WebMvcConfigurer[/label] 接口，重写接口中的方法。/** * @author: TangZhiKai * @create: 2023-10-06 14:12 * @description: 配置类 **/ //@EnableWebMvc //禁用boot的默认配置，慎用，注意这里不要添加 @Configuration //告诉SpringBoot这是一个配置类，采用手自一体的方式 public class MyConfig implements WebMvcConfigurer { /*配置类最好在主程序下面，因为SpringBoot默认一启动，会扫描主程序下面的包以及子包*/ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { //保留以前规则，可不写 WebMvcConfigurer.super.addResourceHandlers(registry); //自己添加一些静态资源规则，也就是在默认配置的情况下添加了许多其他配置 //添加一个addResourceHandler，指明访问路径 类似于localhost:8080/static/** registry.addResourceHandler("/static/**") //访问static路径下的所有，就来到 a文件夹，b文件夹 .addResourceLocations("classpath:/a/","classpath:/b/") //设置缓存设置，指定最大存储时间，存储时间单位 .setCacheControl(CacheControl.maxAge(1180, TimeUnit.SECONDS)); } }第二种实现方式给容器中放一个WebMvcConfigurer组件，就能自定义底层。//@EnableWebMvc //禁用boot的默认配置，慎用，注意这里不要添加 @Configuration //告诉SpringBoot这是一个配置类，采用手自一体的方式 public class MyConfig /*implements WebMvcConfigurer*/ { /*配置类最好在主程序下面，因为SpringBoot默认一启动，会扫描主程序下面的包以及子包*/ @Bean //或者是给容器中放一个WebMvcConfigurer组件，就能自定义底层。 public WebMvcConfigurer webMvcConfigurer(){ //采用适配器思路，只实现我们需要实现的方法即可 return new WebMvcConfigurer() { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") //访问static路径下的所有，就来到 a文件夹，b文件夹 .addResourceLocations("classpath:/a/","classpath:/b/") //设置缓存设置，指定最大存储时间，存储时间单位 .setCacheControl(CacheControl.maxAge(1180, TimeUnit.SECONDS)); } }; } /* @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { //保留以前规则，可不写 WebMvcConfigurer.super.addResourceHandlers(registry); //自己添加一些静态资源规则，也就是在默认配置的情况下添加了许多其他配置 //添加一个addResourceHandler，指明访问路径 类似于localhost:8080/static/** registry.addResourceHandler("/static/**") //访问static路径下的所有，就来到 a文件夹，b文件夹 .addResourceLocations("classpath:/a/","classpath:/b/") //设置缓存设置，指定最大存储时间，存储时间单位 .setCacheControl(CacheControl.maxAge(1180, TimeUnit.SECONDS)); } */ } [note type="primary flat"]为什么容器中放一个WebMvcConfigurer就能配置底层行为(生效)?1.WebMvcAutoConfiguration是一个自动配置类，它里面有一个EnableWebMvcConfiguration。2.EnableWebMvcConfiguration继承于DelegatingWebMvcConfiguration,这两个都生效。3.DelegatingWebMvcConfiguration利用DI(依赖注入)把容器中所有WebMvcConfigurer注入进来。4.别人调用DelegatingWebMvcConfiguration的方法配置底层规则，而它调用所有WebMvcConfigurer的配置底层的方法。[/note]流程优点麻烦，视频讲两句啦:)[video title="静态资源配置 " url="https://img.kaijavademo.top/typecho/uploads/2023/10/videos/%E7%BC%96%E5%86%99%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90%E9%85%8D%E7%BD%AE.mkv " container="bgm9fbu7tb" subtitle=" " poster=" "] [/video]路径匹配以前MVC中我们使用Ant的风格路径，现在新版使用PathPatternParser[note type="info flat"]总结：Ⅰ.直接使用默认的路径匹配规则，是由 PathPatternParser提供的；Ⅱ.如果路径中间需要有**，在配置文件中替换成ant风格路径[/note]/** * @author: TangZhiKai * @create: 2023-10-06 15:02 * @description: **/ @RestController @Slf4j public class HelloController { /** * 默认使用新版本的路径匹配器PathPatternParser * 优点：性能极高 * 缺点：不能适配ant风格路径 ** 在中间的，剩下的都可以和antPathMatcher语法兼容 * * @param request * @param path * @return */ @GetMapping("/a*/b?/{p1:[a-f]+}/**")//所谓的ant风格路径 public String hello(HttpServletRequest request , @PathVariable("p1") String path){ //* 很多字符 ?一个字符 [a-f]限定范围 log.info("路径变量p1: {}" ,path); String url = request.getRequestURI(); return url; } }当然我们可以手动在配置文件中修改application.properties#改变路径匹配策略： # ant_path_matcher 老版策略； # path_pattern_parser 新版策略； spring.mvc.pathmatch.matching-strategy=ant_path_matcherThymeleaf整合关于对于Thymeleaf的学习，我将Springboot整合，并写在thymeleaf的文章中了，点我跳转错误处理SpringMVC的错误处理机制依然保留，MVC处理不了的，才会交给boot进行处理。SpringBoot对于错误处理有一套自己的机制：[note type="info flat"]1.解析一个错误页：a.如果发生了500、404、503、403这些错误①如果有模板引擎，默认在classpath:/templates/error/精确码.html寻找②如果没有模板引擎，在静态资源文件夹static下找精确码.htmlb.如果匹配不到精确码.html这些精确的错误页，就去找模糊码对应的错误页，就去找5xx.html,4xx.html模糊匹配①如果有模板引擎，默认在classpath:/templates/error/5xx.html寻找②如果没有模板引擎，在静态资源文件夹static下找5xx.html2.如果模板引擎templates下有error.html页面，就直接渲染，如果没有就自动提供一个error的页面。[/note]大体来说就是能精确匹配就精确匹配，有模板引擎就找对应模板引擎目录下面的页面，没有就去找静态资源。函数式Web以前我们需要通过 [label color="blue"]@Controller + @RequestMapping[/label] 这种方式，而SpringMVC认为这种是 耦合式 的方式敲代码。所谓耦合，就是路由、业务耦合，所有的路由匹配，和业务，都写到Controller层里面。而SpringMVC推荐一种 [label color="red"]函数式[/label] 写法。将路由业务抽离出来，业务集中写道一个地方，路由抽离出来写到另外一个地方，这样路由如果出问题，不需要翻各个Controller类，集中在一处看我们的路由定义就行。[note type="flat"]场景：实现Restful的CRUD案例来对用户user进行增删改查[/note]GET /user/1 获取1号用户GET /users 获取所有用户POST /user 请求体携带JSON，新增一个用户PUT /user/1 请求体携带JSON，修改1号用户DELETE /user/1 删除1号用户以前就是在Controller层中写对应请求的Mapping注解，然后写对应的业务逻辑的实现。WebFunctionConfig.controller /** * @author: TangZhiKai * @create: 2023-10-20 18:01 * @description: 实现Restful的对于用户的CRUD功能 **/ /** * ● GET /user/1 获取1号用户 * ● GET /users 获取所有用户 * ● POST /user 请求体携带JSON，新增一个用户 * ● PUT /user/1 请求体携带JSON，修改1号用户 * ● DELETE /user/1 删除1号用户 */ @Configuration public class WebFunctionConfig { //定义路由信息 /** * 函数式Web的步骤： * 1.给容器中放一个Bean: 类型是RouterFunction<ServerResponse> * userRoute用户的路由定义信息，所谓的路由定义就是什么请求跳到哪里 * 2.每个业务准备一个自己的Handler，抽取成一个biz文件夹 * * 核心四大对象 * 1.RouterFunction: 定义路由信息，发什么请求，谁来处理？ * 2.RequestPredicate：请求谓语。请求方式(GET、POST、DELETE..）、请求参数 * 3.ServerRequest：封装请求完整数据 * 4.ServerResponse：服务器响应，封装响应完整数据 * */ @Bean public RouterFunction<ServerResponse> userRoute(){ //RouterFunction的工具类，构建RouterFunction RouterFunctions.route() //开始定义路由信息 /* 参数一pattern： 路径 /user/{id} 我希望id是动态的 参数二predicate：由谁处理,启用RequestPredicates工具类帮我们构建请求谓语，accept()表示接收什么样的类型 如果是 MediaType.ALL表示接收任意的媒体类型的参数 参数三HandlerFunction:函数式接口，接收request返回ServerResponse 使用lambda表达式，接收请求，做方法体内部做 业务处理，构造响应 ServerResponse.ok()通过build构建器方法返回。 因为要查一个用户，就模拟有一个person，如果想要将查到的用户以Json的形式响应, 通过.body(person)传递 这只是一个Get请求,如果真的写这个，业务处理非常长，优化的写法我们专门写一个处理器handler专门来做参数三的事情 抽取出来，请看biz文件夹 */ .GET("/user/{id}", RequestPredicates.accept(MediaType.ALL),request -> { //业务处理 Person person = new Person(1L,"haha","aa@qq.com",18,"admin"); //构造响应 return ServerResponse .ok() .body(person); }); return null; } } [note type="primary flat"]这样写比较冗长，我们把参数三的业务处理和响应部分抽取出来，抽取成一个方法，然后加入到IOC容器中，通过形参注入，然后使用方法引用简化格式。[/note]完整的CRUDWebFunctionConfig/** * @author: TangZhiKai * @create: 2023-10-20 18:01 * @description: 实现Restful的对于用户的CRUD功能 **/ /** * ● GET /user/1 获取1号用户 * ● GET /users 获取所有用户 * ● POST /user 请求体携带JSON，新增一个用户 * ● PUT /user/1 请求体携带JSON，修改1号用户 * ● DELETE /user/1 删除1号用户 */ @Configuration public class WebFunctionConfig { //定义路由信息 /** * 函数式Web的步骤： * 1.给容器中放一个Bean: 类型是RouterFunction<ServerResponse>,集中所有路由信息 * userRoute用户的路由定义信息，所谓的路由定义就是什么请求跳到哪里 * 2.每个业务准备一个自己的Handler(业务处理器)，抽取放在一个biz文件夹 * 业务处理器中对应的方法格式同HandlerFunction接口一致，然后我们需要在userRoute形参处传递处理器 * 表明如果方法里面参数是一个对象，默认就会从容器中拿取(这就是为什么处理器要添加Component注解，这样写就自动注入进来了) * * 核心四大对象 * 1.RouterFunction: 定义路由信息，发什么请求，谁来处理？ * 2.RequestPredicate：请求谓语。请求方式(GET、POST、DELETE..)、请求参数 * 3.ServerRequest：封装请求完整数据 * 4.ServerResponse：服务器响应，封装响应完整数据 * */ @Bean public RouterFunction<ServerResponse> userRoute(UserBizHandler userBizHandler/*这个会被自动注入进来*/){ //RouterFunction的工具类，构建RouterFunction return RouterFunctions.route() //开始定义路由信息 /* 参数一pattern： 路径 /uer/{id} 我希望id是动态的 参数二predicate：由谁处理,启用RequestPredicates工具类帮我们构建请求谓语，accept()表示接收什么样的类型 如果是 MediaType.ALL表示接收任意的媒体类型的参数 参数三HandlerFunction:函数式接口，接收request返回ServerResponse 使用lambda表达式，接收请求，做方法体内部做 业务处理，构造响应 ServerResponse.ok()通过build构建器方法返回。ServerResponse.ok().build(); 因为要查一个用户，就模拟有一个person，如果想要将查到的用户以Json的形式响应, 通过.body(person)构造响应 这只是一个Get请求,如果真的写这个，业务处理非常长，优化的写法我们专门写一个处理器handler专门来做参数三的事情 抽取出来，请看biz文件夹 */ .GET("/user/{id}", RequestPredicates.accept(MediaType.ALL),userBizHandler::getUser/*调用当前实例的getUser方法*/) .GET("/users",userBizHandler::getUsers) //因为这里是需要请求体需要携带JSON数据，所以需要指定媒体类型APPLICATION_JSON .POST("/user",RequestPredicates.accept(MediaType.APPLICATION_JSON),userBizHandler::saveUser) .PUT("/user/{id}",RequestPredicates.accept(MediaType.APPLICATION_JSON),userBizHandler::updateUser) .DELETE("user/{id}",userBizHandler::deleteUser) //构建好所有路由信息.build(); ,而这样最后就会是一个RouterFunction<ServerResponse> //直接添加到方法返回值的位置就可以了 .build(); } //未来其他的增删改查..... @Bean public RouterFunction<ServerResponse> groupRoute(UserBizHandler userBizHandler/*这个会被自动注入进来*/){ return RouterFunctions.route() .GET("/user/{id}", RequestPredicates.accept(MediaType.ALL) //请求谓语 //这里可以拓展请求规则，通过and，不仅接收json类型的数据，而且请求参数里面还得是aa的值为bb才给予处理 .and(RequestPredicates.param("aa","bb")) ,userBizHandler::getUser/*调用当前实例的getUser方法*/) .GET("/users",userBizHandler::getUsers) .POST("/user",RequestPredicates.accept(MediaType.APPLICATION_JSON),userBizHandler::saveUser) .PUT("/user/{id}",RequestPredicates.accept(MediaType.APPLICATION_JSON),userBizHandler::updateUser) .DELETE("user/{id}",userBizHandler::deleteUser) .build(); } } /biz/UserBizHandler/** * @author: TangZhiKai * @create: 2023-10-20 18:24 * @description: 用户的业务处理器，专门处理User有关的业务,提供一个方法来专门做这件事(代替路由信息的参数三) **/ @Slf4j @Component //Service也可以 public class UserBizHandler { /** * 查询指定id的用户 * 该方法是要替换路由信息参数三位置的lambda表达式 * 参数三需要HandlerFunction，我们也需要返回ServerResponse，接收ServerRequest */ public ServerResponse getUser(ServerRequest request) throws Exception { /* 形参中request封装了所有的请求数据 这里我们获取路径变量，日志输出查询的几号用户。除此之外可以获取请求路径，请求头 */ String id = request.pathVariable("id"); log.info("查询用户[{}]信息完成,数据库正在检索", id); log.info("查询某个用户信息完成"); //业务处理 Person person = new Person(1L, "haha", "aa@qq.com", 18, "admin"); //构造响应 return ServerResponse .ok() .body(person); } /** * 获取所有用户 * * @param request * @return * @throws Exception */ public ServerResponse getUsers(ServerRequest request) throws Exception { log.info("查询所有用户信息完成"); //业务处理 //模拟所有用户 List<Person> list = Arrays.asList(new Person(1L, "haha", "aa1@qq.com", 18, "admin"), new Person(2L, "哈哈2", "aa2@qq.com", 12, "admin2")); //构造响应 return ServerResponse .ok() //查询到的所有用户写出去 .body(list);//凡是body中的对象，就是以前的@ResponseBody原理，利用HttpMessageConverter写出为json } /** * 保存用户 * * @param request * @return */ public ServerResponse saveUser(ServerRequest request) throws ServletException, IOException { //提取请求体：请求体中body数据封装成json返回 Person body = request.body(Person.class); //使用@Slf4j日志记录，没必要真连接数据库 log.info("保存用户信息:{}", body); //响应成功 return ServerResponse.ok().build(); } /** * 更新用户 * * @param request * @return */ public ServerResponse updateUser(ServerRequest request) throws ServletException, IOException { Person body = request.body(Person.class); log.info("用户信息更新:{}", body); return ServerResponse.ok().build(); } /** * 删除用户 * @param request * @return */ public ServerResponse deleteUser(ServerRequest request) { String id = request.pathVariable("id"); log.info("删除[{}]用户信息",id); return ServerResponse.ok().build(); } }我书写这篇文章的初衷就是总结学习的进度，遗忘之际可以拿出来翻看，如有不对的地方还望指正，多多海涵。