😎自己也是好久没有更新了，先说下这段时间都在干嘛？学校里功课有点多，忙着做实验写报告；平时自己也要去继续自己的方向的学习，几乎没什么时间；每天都安排的满满的，在图书馆和教室来回奔波，累~~，晚上还要回到宿舍抽出一些仅有的时间来学习算法；有的时候还要网上冲浪，和云服务器的群友们play。自己像打算再部署一个非常长期的halo博客用于记录我的生活，和写写平日里生活中的笔记的想法了，但是太忙了，一直拖着。 这是自己的安排，发现会安排事的人一直都不会闲下来(自己在假期做了和一个小伙伴做了一个项目，前段时间好不容易抽出时间有空结束了整个项目。5.2的时候自己得意洋洋的开始部署项目，但是到5.4自己都没部署好，然后一整个五一也是这样(,人麻了，最后在5.10的时候还是要感谢一位前端的群友帮助下，成功部署上去了。首先来说说自己为什么没有部署上去(愚蠢，大佬请忽略，老实说，我忘的差不多了😭~~）首先自己第一次用1panel，运维面板之前只用过宝塔，为什么改用？是因为前段时间专业版一折券，和云服务器的群友们一起买的。通俗来说，是一个像宝塔一样的面板，只不过自己真的不太熟悉怎么用新面板。好多位置上的东西都不太一样。自己和平时的群友聊天的时候，自己试着摸索了下CF(cloudflare)，生成了个证书，然后结构变得就很复杂，报错的话自己也是没什么头绪然后部署项目的时候，刚开始起始开发环境的时候(一开始的时候实际上非常接近线上了已经)，我已经准备了多环境配置，但是还是报错。一度让我从https先切换为http，为什么要切换，因为用https需要生成证书什么的，非常麻烦（实际上也不麻烦，后面也是生成了，并且是端对端的完全加密）主要是想看清为啥报错。然后自己改http的时候修改了好多的配置，改成http最后部署项目上去的时候。我的项目是首次访问，不管是访问哪个页面，都会重定向到登录页，登陆后在切换回来的这样一个逻辑。结果发现无法重定向，原来是需要在nginx/openresty(1p中的）中中配置一下。然后发现http不安全，不保存session到浏览器中，很奇怪。然后我就从http>>https又花费了好久。最主要的是在解决一系列问题后，发现出现了反复重定向到登录页的问题，由于项目中封装了一个myAxios.ts的这样一个请求响应封装类，找不到报错。最后在请求中右键复制为cURL才一度发现问题。问题在于原来是自己在配置线上application-prod.yml的时候，复制的时候在种cookie的时候，没有配置对应的域名/ip。导致了session一度种在了本地，因为没有配置，所以默认是localhost，唉，血泪的教训。下次记住了。、这里是开发的配置application.yml 但是线上的删除了，其实也是localhost还是来稍微总结一下把，打开线上部署要注意一些细节。🌟前端的baseUrl直接指向配置的域名，不要跟端口号；后端的application-prod.yml中要配置好cookie的地址，并且如果用了redis要配置好redis的配置（和本地可能不太一样）MySQL也是如此~~；服务器中的 [label color="green"]80和443[/label] 端口要留给 [label color="green"]nginx/openresty[/label] ,不要乱占用。如果用https，后端也千万不可直接监听443端口（我糊涂了呜呜）；有CF证书的话，就是完全加密；后端跨域在调试问题的时候可以全开，后面有需要可以再具体调整；面板中创建网站文件的时候，执行后端文件的时候，最好是先单独用java -jar执行下，没有报错的话。然后使用nohup开启守护线程执行项目。🤑好了就先说这么多把，好久没有写过文章了，这会有些迟了，先休息了，再见啦。我一直在，请不要忘记我，让我们一起加油！
在有了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注入就可以使用 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <!--<exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions>--> </dependency> <!--<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>-->我录了一段视频，来记录，主要是过程还是有点复杂，单纯两句话说不明白。[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] 规范，适配性、跨平台非常好。远程调用无论是哪种远程调用，都需要使用响应式的包来支持 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency>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(); } }我书写这篇文章的初衷就是总结学习的进度，遗忘之际可以拿出来翻看，如有不对的地方还望指正，多多海涵。
基于Redis7的学习笔记，包含有Redis管道，目前仍在持续更新。Redis管道Redis管道和Redis事务截然不同的两个东西。可以类比为Java和JavaScript之间的关系。把所有命令排成一行，像流水线一样去处理。管道(pipeline)可以一次性发送多条命令给服务端，服务端依次处理完完毕后， [label color="red"]通过一条响应一次性将结果返回，通过减少客户端与redis的通信次数来实现降低往返延时时间[/label] 。pipeline实现的原理是队列，先进先出特性就保证数据的顺序性。我们之前学过的批量添加无法一次性添加多种数据类型，这个也是我们出现的问题，用管道来解决。我们可以将一次性添加的命令放在myredis/下的一个新的txt文件。在服务器外部执行语句 cat cmd.txt | redis-cli -a abc123 --pipe服务器内部可以使用，如图。[note type="primary flat"]管道小总结：Ⅰ.Pipeline与原生批量命令对比①原生的批量命令是原子性，pipeline是非原子性。②pipeline支持批量执行不同的命令。③原生批命令是服务器实现，而pipeline需要服务端与客户端共同完成。Ⅱ.Pipeline与事务对比①事务具有原子性，管道不具有原子性②事务一条一发，只有在接收到exec命令后才会执行。而管道是一次性将多条命令发送到服务器。③执行事务会阻塞其他命令执行，而执行管道中的命令不会。Ⅲ.管道自身注意事项①管道的缓冲指令只是会依次执行，不保证原子性，如果执行中指令发生异常，将会继续执行后续的指令②使用pipeline组装的命令个数不能太多，不然数据量大客户端阻塞的时间可能过久，同时服务端此时也被迫回复一个队列答复，占用很多内存。[/note]官方解释So if you need to send a lot of commands with pipelining, it is better to send them as batches each containing a reasonable number, for instance 10k commands, read the replies, and then send another 10k commands again, and so forth.发布订阅类似于MQ消息中间件，了解即可，我们还是需要将专业的事情交给专业的人去干。pub|sub作为第一代消息中间件，stream作为第二代消息中间件(就是说可以理解为MQ的前身雏形)是一种消息通信模式，发送者发送消息，订阅者接收消息，可以实现进程见的消息传递。Redis可以实现消息中间件的功能，即通过发布订阅实现消息的引导和分流。专业的事情交给专业的中间件处理，redis做好分布式缓存的功能就够了。开启三个客户端，演示客户端A、B订阅消息，客户端C发布消息演示批量订阅和发布取消订阅[note type="warning flat"]存在的问题(缺点)：①没有持久化，必须先订阅再发布。如果先发布消息没有订阅者，消息直接丢弃。②没有ACK机制，无法保证消息的消费成功③以上缺点导致该模式在生产结构中无用武之地，因此Redis5.0新增了Stream。[/note]Redis复制Redis复制非常重要，生产工作中，redis不可能是单机，后面的哨兵和集群都要建立在这个基础之上。主机(master)以写为主，从机(slave)以读为主。当主机(master)数据发生变化的时候，自动将新的数据异步同步到其他slave数据库。能干嘛？读写分离容灾恢复，对之前持久化的一种更好的补充数据备份水平扩容支撑高并发使用：配从库不配主库。主机配置了requirepass参数，需要密码登录；从机要配置masterauth来设置校验密码，否则主机就会拒绝主机的访问请求。基本操作命令[note type="info flat"]主从复制:replicaof 主库IP 主库端口;配从库不配主库改换门庭:slave of 新主库IP 新主库端口自立为王:slaveof no one[/note]案例需要使用三个虚拟机来模拟这个操作，配置，我用视频的形式来解释我只使用一个虚拟机来模拟三个不同的虚拟机的场景。实际上就是开三个进程，使用不同的配置文件，配置文件中配置好对应的信息和端口号什么的就可以。[video title="主从复制配置准备 " url="https://img.kaijavademo.top/typecho/uploads/2023/09/videos/%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E5%87%86%E5%A4%87.mkv " container="b9u58112bu" subtitle=" " poster=" "] [/video]后面启动的时候，从机一定要一边指定密码，一边指定端口号 redis-cli -a abc123 -p 6381一主二仆将其他两个从机打开，然后主机连接到服务器后，无论是主机还是从机，都可以输入 info replication 查看主从关系。从机可以执行写命令吗？127.0.0.1:6380> set k2 slave6380 (error) READONLY You can't write against a read only replica. [label color="red"]从机只可以读取，不可以写操作，没有写的权限，实现读写分离。[/label]从机切入点问题假设我的6381端口从机关机了，之后6379端口主机执行写操作。6380从机可以正常执行读操作。而再次启动6381端口的从机，仍然存在之前6379所写的内容。主机shutdown后，从机会上位吗？从机不动，原地待命，从机数据可以正常使用；等待主机从重启动归来。主机shutdown后，重启后主从关系还在吗？从机能否顺利复制？主从关系仍然存在，并且从机可以顺利复制。某台从机down后，master继续，从机重启后它能跟上大部队吗？同第二种情况，从机切入点问题。仍然可以跟上大部队。[note type="default flat"]命令操作手动指定：去掉从机配置文件中的配置项。使用SLAVEOF 主库IP 主库端口.让从机模拟重新找新的主机。仍然可以连上6379，数据也同步过来了。命令指定：临时命令，单次生效。如果确定是主从关系，将其配置进入配置文件。[/note][label color="red"]配置持久稳定，命令当次生效。[/label]薪火相传slave同样可以接收其他slaves的连接和同步请求，可以通过一个slave作为链条中下一个master，可以有效减轻主master的写压力。中途变更转向，会清除之前的数据，重新建立拷贝最新的。 SLAVEOF 主库IP 主库端口 将6381 [label color="default"]改换门庭[/label] 改换为6380，这样类似于继承的关系。 [label color="red"]而6380作为6379的从机，作为6381的主机，仍然无法进行写命令。[/label]反客为主使用 SLAVEOF no one127.0.0.1:6381> SLAVEOF no one OK 127.0.0.1:6381> info replication # Replication role:master connected_slaves:0 master_failover_state:no-failover master_replid:1b866515c61d3c90b2da31dc1356c6dc338514a7 master_replid2:54429140a07f7a5e5a15170ae290f36895038584 master_repl_offset:3371 second_repl_offset:3372 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:739 repl_backlog_histlen:2633[note type="warning flat"]复制的缺点：①复制延时，信号衰减。②master挂了怎么办？默认情况下，不会在slave节点中重选一个master。如果没有master，系统几乎就是半瘫痪，只能读取，无法写入。我们需要一个高可用的恢复机制。如果每次都要人工干预那么会很麻烦。无人值守变成了刚需。至此，就有Redis哨兵和Redis集群。[/note]Redis哨兵(☆)主机shutdown后，长期宕机，我们只读不可写，无法满足系统高可用。我们达到的效果，我们需要寻找一个监控者，在master宕机后，能够顺利把某个slave通过某个选举算法和机制，变成新的主机。吹哨人巡查监控后台master主机是否故障，如果故障了根据 [label color="red"]投票数[/label] 自动将某一个从库转换为新主库，继续对外服务。[font size="16px" color="#ff69b4"]Redis Sentinel架构：前提说明三个哨兵：自动监控和维护集群，不存放数据，只是吹哨人。一主二从：用于数据读取和存放。[/font]配置说明[video title="使用一台虚拟机代替六台配置模拟哨兵 " url="https://img.kaijavademo.top/typecho/uploads/2023/09/videos/%E5%93%A8%E5%85%B5%E5%87%86%E5%A4%87.mkv " container="bkr2dtaf2m5" subtitle=" " poster=" "] [/video]采用一台虚拟机，模拟三台 一主二从的关系，现在继续模拟三台哨兵在6379端口，修改哨兵的配置文件，调整哨兵认可客观下线等等的参数。先开启一主二仆在当前这台虚拟机再开启一个终端，输入[root@redis100 myredis]# redis-sentinel sentinel26379.conf --sentinel [root@redis100 myredis]# redis-sentinel sentinel26380.conf --sentinel [root@redis100 myredis]# redis-sentinel sentinel26381.conf --sentinel开启哨兵监控。通过关闭主机，然后等待一会之后发现6381端口号已经变成了主机。当再次启动6379的时候，仍然是6381作为主机，6379降级为从机。redis-cli -p 26379 shutdown 关闭后台哨兵监控[note type="primary flat"]①生成都是不同机房不同服务器，很少出现3个哨兵全挂掉的情况②可以监控多个master，一行一个[/note]哨兵的运行流程和选举原理[font size="17px" color="#ff0000"]三个哨兵监控一主二从，正常运行中...Ⅰ.SDown主观下线：SDOWN(主观不可用)是单个sentinel自己主观上，如果发送了PING心跳后，在一定时间内没有收到合法的回复，就达到了SDOWN的条件。Ⅱ.ODown客观下线：ODOWN需要一定数量的sentinel，多个哨兵达成一致意见,才能认为一个master客观上已经宕掉。Ⅲ.选举出领导者哨兵：当主节点被判断客观下线以后，各个哨兵节点会进行协商，先选举出一个领导者哨兵节点（兵王）并由该领导者节点，也即被选举出的兵王进行failover（故障迁移）。（像神仙下凡）兵王如何选出？Raft算法。Ⅳ.由兵王(leader)开始推动故障切换流程并选出一个新master①新主登基：选出新的master规则，即在剩余slave节点健康前提下，按照以下三个原则依次比较在redis.conf文件中，优先级replica-priority最高的从节点(数字越小优先级越高)复制偏移位置offset最大的从节点最小Run ID的从节点(字典顺序,ASCII码)②群臣俯首执行slaveof no one命令让选出来的从节点成为新的主节点，并通过slaveof命令让其他节点成为其从节点Sentinel leader会对选举出的新master执行slaveof no one操作，将其提升为master节点Sentinel leader向其他slave发送命令，让剩余的slave成为新的master节点的slave③旧主拜服之前的master也回来认怂将之前已下线的老master设置为新选出的新的master的从节点，当老master重新上线后，它会成为新的master的从节点Sentinel leader会让原来的master降级为slave并恢复正常工作[/font][note type="info flat"]上述的故障切换操作，均由sentinel自己独立完成，完全无需人工干预[/note][note type="default flat"]使用建议：哨兵节点的数量应为多个，哨兵本身应该集群，保证高可用哨兵节点的数量应该是奇数各个哨兵节点的配置应一致如果哨兵节点部署在Docker等容器里面，尤其要注意端口的正确映射哨兵集群+主从复制，并不能保证数据零丢失(实际生产中考虑到master宕机后，选举投票一系列操作)之后我们会有集群，redis之父也推荐大家使用集群[/note]Redis集群(☆)Redis集群是一个提供在多个Redis节点间共享数据的程序集， [label color="red"]Redis集群可以支持多个Master[/label] [label color="blue"]由于Cluster自带Sentinel的故障转移机制，内置了高可用的支持，[/label] [label color="red"]无需再去使用哨兵功能[/label]分片|槽位slotRedis集群没有使用一致性哈希算法，而是引入了哈希槽的概念。集群有16384个哈希槽，每个key通过CRC16校验后对16384取模来决定放置哪个槽，集群的每个节点负责一部分哈希槽，将16384经可能平均分摊。The cluster's key space is split into 16384 slots, effectively setting an upper limit for the cluster size of 16384 master nodes (however, the suggested max size of nodes is on the order of ~ 1000 nodes).官网建议最大节点约为1000个节点(槽)。slot槽位映射，这里只记录了哈希槽分区，一致性hash分区的做法，实际上有其他两种做法。一致性Hash算法分区目的是当服务器个数发生变动时，尽量减少影响客户端到服务器的映射关系算法构建一致性哈希环一致性Hash算法是对2^32取模，简单来说，一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环服务器IP节点映射将各个服务器使用Hash进行一个哈希，具体可以选择服务器的IP或主机名作为关键字进行哈希，这样每台机器就能确定其在哈希环上的位置。key落到服务器的落键规则当我们需要存储一个kv键值对时，首先计算key的hash值，hash(key)，将这个key使用相同的函数Hash计算出哈希值并确定此数据在环上的位置， [label color="red"]从此位置沿环顺时针“行走”[/label] ，第一台遇到的服务器就是其应该定位到的服务器，并将该键值对存储在该节点上。[label color="red"]优点：加入和删除节点只影响哈希环中顺时针方向的相邻的节点，对其他节点无影响。[/label] [label color="green"]缺点：数据倾斜。服务节点太少，容易因为节点分布不均匀而造成数据倾斜[/label]哈希槽分区解决均匀分配的问题， [label color="red"]在数据和节点之间又加入了一层，把这层称为哈希槽（slot），用于管理数据和节点之间的关系[/label] ，现在就相当于节点上放的是槽，槽里放的是数据。[font size="18px" color="#ff0000"]为什么redis集群的最大槽数是16384个？[/font]redis的作者在他的官网有所解释原址 [label color="blue"]一.如果槽位为65536，发送心跳信息的消息头达8k，发送的心跳包过于庞大。[/label] 在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为65536时，这块的大小是: 65536÷8÷1024=8kb 在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为16384时，这块的大小是: 16384÷8÷1024=2kb 因为每秒钟，redis节点需要发送一定数量的ping消息作为心跳包，如果槽位为65536，这个ping消息的消息头太大了，浪费带宽。 [label color="blue"]二.redis的集群主节点数量基本不可能超过1000个。[/label] 集群节点越多，心跳包的消息体内携带的数据越多。如果节点过1000个，也会导致网络拥堵。因此redis作者不建议redis cluster节点数量超过1000个。 那么，对于节点数在1000以内的redis cluster集群，16384个槽位够用了。没有必要拓展到65536个。 [label color="blue"]三.槽位越小，节点少的情况下，压缩比高，容易传输[/label] Redis主节点的配置信息中它所负责的哈希槽是通过一张bitmap的形式来保存的，在传输过程中会对bitmap进行压缩，但是如果bitmap的填充率slots / N很高的话(N表示节点数)，bitmap的压缩率就很低。 如果节点数很少，而哈希槽数量很多的话，bitmap的压缩率就很低。Redis不保证强一致性。分片集群中的每个Redis实例都被认为是整个数据的一个分片。[note type="info flat"]为了找到给定key的分片，我们对key进行CRC16(key)算法处理并通过对总分片数量取模。然后，使用确定性哈希函数，这意味着给定的key将多次始终映射到同一个分片，我们可以推断将来读取特定key的位置。[/note]分片 + 槽位 这种结构容易添加或者删除节点案例模拟先将6台redis主机启动redis-server /myredis/cluster/redisCluster6381.conf ..... redis-server /myredis/cluster/redisCluster6386.conf使用一台虚拟机配置6份配置文件的形式，模拟三主机三从机redis-cli -a abc123 --cluster create --cluster-replicas 1 192.168.29.12x:6381 192.168.29.12x:6382 192.168.29.12x:6383 192.168.29.12x:6384 192.168.29.12x:6385 192.168.29.12x:6386--cluster-replicas 1 表示为每个master创建一个slave节点 表示以6381端口号创建slave节点6382，但是在执行后，还需要以实际的分配为主。连接进入6381为切入点，查看节点状态。info replication 查看当前节点的主从关系cluster info 查看当前集群相关环境cluster nodes 查看所有集群的主从关系，还有哈希槽注意事项以后连集群，启动需要使用 redis-cli -a abc123 -p 6381 -c 这样在添加数据的时候，直接可以理解为重定向到对应端口。-c可以避免路由失效[root@redis100 cluster]# redis-cli -a abc123 -p 6381 -c Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. 127.0.0.1:6381> keys * (empty array) 127.0.0.1:6381> set k1 v1 -> Redirected to slot [12706] located at 192.168.29.128:6383 OK 192.168.29.128:6383> 查看某个key 属于对应的槽位值 CLUSTER KEYSLOT 键名称如果6381宕机了，那么6384能否作为从机上位成功？6384成功上位，并可以正常使用。随后，6381原来的主机回来了，是否会上位？6381不会上位， [label color="red"]并且以从节点的形式回归[/label] 。我们也可以手动调整，节点从属调整，将原先的6381作为master，6384作为从机的结构调整回来。 CLUSTER FAILOVER 节点从属调整扩容|缩容埋个坑，回来填。ToT~~~SpringBoot集成Redis类比JDBC，我们需要使用一个中间件来控制Redis。JedisJedisClient是Redis官网推荐的一个面向java客户端，库文件实现了对各类API进行封装调用。pom文件中导入jedis依赖 <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.3.1</version> </dependency>修改redis配置文件(之前修改过)，更改防火墙配置，放行6379端口需要在虚拟机上修改。 firewall-cmd --zone=public --add-port=6379/tcp --permanent systemctl restart firewalld[root@redis100 ~]# firewall-cmd --zone=public --add-port=6379/tcp --permanent success [root@redis100 ~]# systemctl restart firewalld 连接在 [label color="default"]boot工程[/label] 原有的基础上，单独书写一个类，如果显示结果为PONG，那么就表示当前客户端连接成功。public class JedisDemo { public static void main(String[] args) { //连接，创建，告诉Jedis客户端访问哪台机器上面的，端口号是多少的服务器 //1.connnection通过执行ip和端口号 //这里并不规范，要么提出去书写，要么写配置文件 Jedis jedis = new Jedis("192.168.29.xxx", 6379); //2.指定访问服务器的密码 jedis.auth("abc123"); //3.获得了jedis客户端，可以像jdbc一样,访问我们的redis System.out.println(jedis.ping());lettucepom文件中导入依赖 <dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>6.2.0.RELEASE</version> </dependency>测试public class LettuceDemo { public static void main(String[] args) { //1.使用构建器链式编程来builder我们的RedisURI //构建器模式 RedisURI uri = RedisURI.builder() .redis("192.168.29.xxx") .withAuthentication("default","abc123") .build(); //2.创建连接客户端 RedisClient redisClient = RedisClient.create(uri); StatefulRedisConnection conn = redisClient.connect(); //3.创建操作的command,通过conn创建command RedisCommands commands = conn.sync(); //=====================业务逻辑========================= //===================================================== //4.各种关闭释放资源 conn.close(); redisClient.shutdown(); } }RedisTemplate(☆)[label color="red"]务必掌握[/label]连接单机[video title="连接单机 " url="https://img.kaijavademo.top/typecho/uploads/2023/09/videos/%E8%BF%9E%E6%8E%A5%E5%8D%95%E6%9C%BA.mkv " container="bzqxtcczz3c" subtitle=" " poster=" "] [/video]第一种解决乱码问题使用RedisTemplate的子类StringRedisTemplate，并在启动redis的时候指定使用指令 redis-cli -a abc123 -p 6379 --raw第二种解决乱码问题配置一个配置类 [label color="red"]@Configuration[/label] RedisConfig,并在其中配置序列化的方式，覆盖默认的JDK序列化。在启动的时候依然使用 redis-cli -a abc123 -p 6379 --raw 指令配置类RedisConfig配置@Configuration public class RedisConfig { //用默认确实将程序写进去了，但是由于序列化问题，在redis中显示出现了问题 /** * redis序列化的工具配置类，下面这个请一定开启配置 * 127.0.0.1:6379> keys * * 1) "ord:102" 序列化过 * 2) "\xac\xed\x00\x05t\x00\aord:102" 野生，没有序列化过 * this.redisTemplate.opsForValue(); //提供了操作string类型的所有方法 * this.redisTemplate.opsForList(); // 提供了操作list类型的所有方法 * this.redisTemplate.opsForSet(); //提供了操作set的所有方法 * this.redisTemplate.opsForHash(); //提供了操作hash表的所有方法 * this.redisTemplate.opsForZSet(); //提供了操作zset的所有方法 * @param lettuceConnectionFactory * @return */ @Bean public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) { RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(lettuceConnectionFactory); //设置key序列化方式string redisTemplate.setKeySerializer(new StringRedisSerializer()); //设置value的序列化方式json，使用GenericJackson2JsonRedisSerializer替换默认序列化 redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.afterPropertiesSet(); return redisTemplate; } }连接集群启动六台redis集群环境下，修改YML配置文件，启动微服务端口。注意使用这样的指令连接，指定端口，还有重定向并显示中文。 redis-cli -a abc123 -p 6381 -c --raw 修改yml配置文件。改成redis集群那一套使用就可以了。server.port=7777 spring.application.name=redis7_study # ========================logging===================== logging.level.root=info logging.level.com.atguigu.redis7=info logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n logging.file.name=D:/mylogs2023/redis7_study.log logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n # ========================swagger===================== spring.swagger2.enabled=true #在springboot2.6.X结合swagger2.9.X会提示documentationPluginsBootstrapper空指针异常， #原因是在springboot2.6.X中将SpringMVC默认路径匹配策略从AntPathMatcher更改为PathPatternParser， # 导致出错，解决办法是matching-strategy切换回之前ant_path_matcher spring.mvc.pathmatch.matching-strategy=ant_path_matcher # ========================redis单机===================== #spring.redis.database=0 # 修改为自己真实IP #spring.redis.host=192.168.29.128 #spring.redis.port=6379 #spring.redis.password=abc123 #spring.redis.lettuce.pool.max-active=8 #spring.redis.lettuce.pool.max-wait=-1ms #spring.redis.lettuce.pool.max-idle=8 #spring.redis.lettuce.pool.min-idle=0 # ========================redis集群===================== spring.redis.password=abc123 # 获取失败 最大重定向次数 spring.redis.cluster.max-redirects=3 spring.redis.lettuce.pool.max-active=8 spring.redis.lettuce.pool.max-wait=-1ms spring.redis.lettuce.pool.max-idle=8 spring.redis.lettuce.pool.min-idle=0 #支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新，默认false关闭 spring.redis.lettuce.cluster.refresh.adaptive=true #定时刷新(自适应时间) spring.redis.lettuce.cluster.refresh.period=2000 spring.redis.cluster.nodes=192.168.29.12x:6381,192.168.29.12x:6382,192.168.29.12x:6383,192.168.29.12x:6384,192.168.29.12x:6385,192.168.29.12x:6386我书写这篇文章的初衷就是总结学习的进度，遗忘之际可以拿出来翻看，如有不对的地方还望指正，多多海涵。
Redis笔记：十大数据类型指令|Redis键|持久化|事务
Redis是一种键值对类型的缓存数据库，在Linux下使用，基于Redis7的入门笔记,写的很入门,已完结。包括Redis键，value中所对应的所有数据结构，以及持化双雄AOF + RDB，还有Redis的事务。初始化启动redis-a 密码 -a后面跟在redis.config中设置的密码 -p默认不写端口号就是6379.redis-server /myredis/redis7.conf redis-cli -a abc123关闭如果是在服务器内部直接使用 SHUTDOWN 关闭即可如果是在远程可以使用单实例关闭redis-cli -a abc123 shutdown退出 quit十大数据类型这里我们所说的数据类型是指的是value的数据类型，key的数据类型都是字符串。命令不区分大小写，key是区分大小写的。StringRedis中用的最频繁的，最重要，绝绝对对的一号男神。String类型是 [label color="red"]二进制安全[/label] 的，支持序列化。相当于String可以包含任何数据，一个key对应一个value，是最重要最基本的数据类型，一个redis中字符串value最多是512M。set key value [...] # 这里面有五个关键参数，留意keepttl get key # 同时设置/获取多个值 mset key value [key value...] mget key [key...] getrange key 0 -1# 获取指定区间范围内的值(0 -1 表示获取所有值，以后不再赘述) setrange key index abc # 将abc开始赋值给key对应value的下标index incr key # 递增数字，等同于++ incrby key number #为key的value值递增指定的数字number decr key # 递减数字，等同于-- strlen key # 获取key的对应的值的长度 append key value # 将value追加到key对应值之后 setnx key value # 当key不存在的时候创建 setex key secounds value # 创建key的时候设置过期时间secounds getset # 等同于set key value GET [label color="blue"]应用场景：抖音的无限点赞某个视频[/label]List(列表)类比Java，Redis列表是简单的字符串列表，可以添加元素到 [label color="blue"]头部[/label] 或者 [label color="blue"]尾部[/label] ，底层是一个双端链表。lpush/rpush list(key) 1 2 3 4 # 从左边添加数据/从右边添加数据，注意从左边添加数据的时候数据会像进栈一样，先进入的在右边 lrange list(key) 0 -1 #从左侧遍历 lpop/rpop list(key) #从左端点弹出数据/从右端点弹出数据 lindex list(key) 0 # 按照索引下标获得元素 llen list(key) # 返回当前list集合的长度，相当于Java中的size lrem list(key) N v1 # 删除N个值等于v1的元素 ltrim list startIndex endIndex # 删除起始到终止索引的元素 rpoplpush list1 list2 # 将list1的第一个元素(尾部)弹出，压入list2中。 lset list(key) index value # 将某个集合(key)的index下标对应的元素替换为value值 linsert list(key) before/after value1 value2 # 在某个集合value1的前面/后面插入value2[label color="blue"]应用场景：微信号订阅的消息[/label]Hash哈希表类比Java Map<String,Map<Object,Object>> [note type="default flat"]以前 key1 value1现在 key1 field value1[/note]hset key field value [field value...] # 添加一个数据 hget key field # 查询某个数据 hmset key field value... # 一次性添加多个数据 hmget key field field ..# 一次性查询某个key的多个数据 hgetall key # 遍历key的值所有数据，包括field 和 value hdel key field # 删除key中值的field属性 hlen key # 返回当前key中的所有属性个数 hexists key field # 查询当前属性存不存在，如果存在返回1，不存在返回0 hkeys key # 遍历当前key的值的所有属性(field) hvals key # 遍历当前key的值的所有属性值(value) hincrby/hincrbyfloat key field number(value) # 为某个属性值添加任意大小的整数或者是浮点数 hsetnx key field value # 为某个不存在的key设置某个属性，如果已经存在返回0 [label color="blue"]应用场景：早期JD购物车[/label]SetRedis中Set是String类型的无序集合，无序无重复，集合对象的编码可以是intset或者是hashtable。sadd key member [member...] # 添加元素(自动去重) smembers key(set) # 遍历集合中的所有元素 sismember key member # 判断元素member是否在集合key中 srem key member [member...] # 删除元素 scard key # 获取集合里面元素个数 srandmember key number #从集合中随机展示number个元素个数，元素不删除 spop key number # 从集合中随机弹出number个元素，并删除。 smove key1 key2 number # 将key1中存在的number赋给key2集合 sdiff key1 key 2 [key3...] # 做集合的差集运算 (key1-key2) sunion key1 key 2 [key3...]# 做集合的并集运算 sinter key1 key2 # 做集合的交集运算 sintercard numkeys key1 key2 [key3] [LIMIT limit] # 显示numkeys个集合的交集中的元素个数(基数)，并可以限制展示数limit。[label color="blue"]应用场景：微信抽奖小程序，朋友圈点赞查看同赞朋友，猜你喜欢，QQ推荐你可能认识的人[/label]ZSet(Sorted Set)可排序的Set，同Set也是String类型，不允许重复。 [label color="red"]不同的是每个元素都会关联一个double类型的分数，zset的成员是唯一的，但是分数却可以重复。[/label] ,通过分数为集合中的成员进行从小到大的排序。[note type="flat"]k1 v1k1 score v1 score v2[/note]zadd key score member(value) [score member ...] # 添加元素 zrange key start end [withscores] # 按照元素分数从小到大的顺序，返回索引从start到end之间的元素 zrevrange key start end [withscores]# 按照元素分数逆序(从大到小)排序 zrangebyscore key min max [ ( ][withscores] [limit offset count] # 获取指定分数范围内的元素，(代表不包含，limit限制返回条数。 zscore key member # 获取member元素在key中的分数 zcard key # 获取集合中的元素数量 zrem key value(member) # 删除集合中的某个value元素。 zincrby key increment member # 往对应集合元素member中添加increment的分数 zcount key min max # 获取指定分数范围内的元素个数 zmpop numkeys key min/max count number # 弹出多个key中的最高或者是最低的元素number个。他们是成员分数对。 zrank key values #获得value在key中的下标 zrevrank key values # 逆序获得value在key中的下标[label color="blue"]应用场景：根据商品销售队商品进行排序显示[/label]地理空间GEO就是经纬度，主要存储地理位置信息，对存储的信息进行操作，包括添加计算。高德地图附近的酒店..geoadd key latitude longitude name [latitude longitude name] # 为当前key添加name的经纬度坐标 geopos key name [name...] # 返回name的经纬度坐标 geohash key name [name...] #返回name的经纬度坐标用geohash算法生成的base32编码值 geodist key name1 name2 m/km... # 返回name1和name2位置之间的距离，用m/km...单位表示 georadius key latitude longitude distance(10km/..) [withdist withcoord withhash ]count number desc/asc #以半径为中心，查找当前经纬度附近的其他点 georadiusbymember key name distance(10km/..) [withdist withcoord withhash ] count number # 通过名称查找指定范围内的元素基数统计HyperLogLog在输入元素的数量或者体积非常大的时候，计算基数所需的空间固定往往是很小的。统计某个网站的UV、统计某个文章的UV(需要去重考虑),用户搜索网站关键词的数量。对于大型网站，UV大概是亿级别的，为了不需要占用太多内存。就出现了这样一种去重统计功能的基数估计算法HyperLogLog。淘宝，京东，天猫的每天访问量很大，记录每天的访问量就需要有一种基数(不重复的ip，不重复的数字)，我们要想在节约内存的情况下，还要统计的尽量精确，就要使用HyperLogLog。pfadd key element[element...] # 添加指定元素到HyperLogLog中(去重) pfcount #返回对应key中的基数估计值 prmerge key sourcekey [sourcekey...] # 将多个HyperLogLog sourcekey合并为一个keyBitmap位图由0和1状态表现的二进制位的bit数组，比如每日签到，钉钉打卡...setbit key offset value # 在offset的偏移位上赋值0/1 getbit key offset # 查询offset的偏移位上的数值 strlen key # 查询key的字节数(8bit为一个字节)占用多少 bitcount # 统计key里面含有1的有多少个 bitop and key key1 key2 # 将key1和key2中共有的偏移量相同且同时为1的结果赋值给key [label color="blue"]应用场景：签到，按照全年天天登录[/label] Bitfield位域可以一次性操作比特位域，对比特位域进行值的替换，实行实时数据的替换和查找。所用不多，了解即可位域修改bitfield key get type offset # 从第offset位开始，获取type类型的二进制数并以十进制返回 bitfield key set type offset value# 从第offset位开始，获取type类型，并设置value。 bitfield key incrby type offset increment # 从第offset位开始，获取type类型，并自增increment #溢出控制第二种案例 BITFIELD test overflow sat set i8 0 128 #溢出控制第三种案例 BITFIELD test overflow fail set i8 0 888溢出控制WRAP：默认，使用回绕方法处理有符号整数和无符号整数的溢出情况SAT：使用饱和计算方法处理溢出，即下溢计算的结果为最小的整数值(-128)，而上溢计算的结果为最大的整数值(127)。FAIL：命令将拒绝那些会导致上下溢出的情况出现的计算，向用户返回空值表示计算未被执行。Redis流(Stream)Redis版本的消息队列，消息中间件(MQ) + 阻塞队列。消息队列的两种方案List实现消息队列 点对点模式，[label color="red"]常用来做异步队列使用[/label] ，将需要延后处理的任务结构体序列化成字符串塞进Redis的列表，另一个线程从这个列表中轮询数据进行处理。Pub/Sub基于以上两种各有痛点，5.0版本新增了一个强大的数据结构Stream。队列+生产者指令xadd key id/* key value # 添加消息到队列末尾，*表示让系统自动生成id xrange key - + [count number] #获取消息列表，可以指定范围,-代表最小值，+代表最大值 xrevrange key + - [count number] #获取消息列表的元素是相反的，是从大到小的。 xdel key id # 通过id删除一条消息 xlen key # 查看队列中消息的个数 xtrim key maxlen number # 允许最大长度，队流进行限制长度。 xtrim key minid # 允许最小的id ，从某个id开始比它小的将会被抛弃 #用于获取消息 #count表示最多读取多少条消息 #block表示是否以阻塞的方式读取消息，默认不阻塞，0为永远阻塞 #非阻塞 xread count number streams key $ # $代表特殊ID，表示以当前Stream已经存储的最大的ID作为最后一个ID，当前Stream中不存在大于当前最大ID的消息，因此此时返回nil xread count number streams key 0-0 # 表示从最小的id开始获取stream中的消息，不指定count，返回所有消息。 #阻塞 xread count 1 block 0 streams key $ #获取比当前队列中已经存储的最大消息还要大(新) 的消息消费组指令xgroup create stream group $/0 # 用于创建消费者组 $表示从stream尾部开始消费，0表示从stream头部开始消费。 xreadgroup group groupname consumer stream streamname > # 指定消费组中的消费者，从第一条消息未被消费的消息开始读取，将会读取消息队列中所有消息。但是，不同消费组消费者可以消费同一条消息。 xreadgroup group groupname consumer count countNumber stream streamname > # 消费者将从未被消费的消息开始按照指定的行数读取。 xpending stream group # 查看消费组内所有消费者的读取，但未确认的信息；并查看某个消费者具体读取了哪些数据。 xack stream group id # 根据id向消息队列确认消息已经处理完成 xinfo stream streamName # 用于打印Stream\Consumer\Group的详情信息 Redis键(key)常用key命令keys * # 查看当前库所有的key exists key # 判断某个key是否存在 type key # 查看key是什么类型 del key # 删除指定的key数据 unlink key # 非阻塞删除，仅仅将keys从keyspace元数据中删除，真正的删除会在后续的异步中操作。 expire key 秒数 # 为给定的key设置过期时间 ttl key # 查看还有多少秒过期，-1表示永不过期，-2表示已经过期 move key dbindex 0-15 # 将当前数据库的key移动到指定数据库的db(16个 0-15)中 select dbindex 0-15 # 切换数据库[0-15] ，默认为0数据库 dbsize # 查看当前所在的数据库中key数量 flushdb # 清空当前库，慎用 flushall # 通杀全部库，慎用持久化我们需要Redis的缓存里面的数据长期持有，宕机后还是可以从硬盘中读回之前保存的数据。RDB在指定的时间间隔以内，执行数据集的时间点快照。将内存数据整体打包形成快照文件，写进磁盘上，让数据可靠性得到保证。全称dump.rdb在7以后，RDB变化频率和6相比有所不同6的快照Snapshotting7的快照官网上，RDB分为手动触发和自动触发自动触发You can configure Redis to have it save the dataset every N seconds if there are at least M changes in the dataset, or you can manually call the SAVE or BGSAVE commands.案例5秒2次修改配置文件433行下设置 save 5 2修改dump文件保存路径505行中书写 dir /myredis/dumpfiles修改dump文件名称建议配置上端口号 dbfilename dump6379.rdb触发备份情况一情况二恢复 [label color="red"]执行flushall/flushdb命令也会产生dump.rdb文件，但里面是空的，无意义。[/label] [label color="red"]将redis服务关闭也会产生一个dump.rdb文件。[/label]物理恢复，一定要将有正确数据的rdb文件进行备份迁移，将服务和备份进行分机隔离。当redis启动的时候，会按照我们的配置，将会在指定路径下面读取dump文件进行数据恢复，已达到数据一致性。手动触发实际生产中对于配置多少分钟多少次修改是写死的，将重要数据读取读取进来的时候，我需要保存，就需要手动触发，将手动触发覆盖自动触发，以最新的命令进行保存。SAVE主程序执行会阻塞当前的redis服务器，直到持久化工作完成，在执行此命令期间，redis不能处理其他命令， [label color="red"]线上禁止使用[/label] 。BGSAVERedis会在后台异步进行快照操作，不阻塞快照的同时还可以响应客户端请求，该触发方式会fork一个子进程由子进程复制持久化的过程。Redis会使用bgsave对当前内存中的所有数据做快照，这个操作是在子进程在后台完成的，这就允许主进程同时可以修改数据。开启 bgsave 可以通过lastsave命令获取最后一次成功执行的快照的时间,显示时间戳。可以使用指令转化为具体时间。date -d @1694676572[note type="warning flat"]RDB优缺点优点：①是Redis数据的一个非常紧凑的单文件时间点表示。RDB非常适合备份，非常适合在发生灾难的时候轻松恢复不同版本的数据集。②RDB非常适合灾难恢复，可以传输到远程数据中心。③RDB最大程度提高了Redis的性能，因为Redis父进程为了持久化而需要做的唯一工作就是派生一个将完成其余工作的子进程，父进程永远不会执行磁盘IO类似的操作简言之：适合大规模的数据恢复；按照业务定时备份；对数据完整性和一致性要求不高；RDB文件在内存中的加载速度要比AOF快得多。缺点：①一定间隔时间作为备份，如果意外宕机，就会丢失从当前最近的一次快照期间的数据， [label color="red"]快照之间的数据会丢失[/label] ②内存数据的全量同步，如果数据量太大，就会导致I/O严重影响服务器性能。③RDB依赖于主进程的fork，在更大的数据集中，这可能会导致服务请求的瞬间延迟。fork的时候内存中的数据被克隆了一份，大致两倍的膨胀性，需要考虑。[/note]AOF持久化的第二种技术，以日志的形式来记录每个写操作，将执行过的所有指令记录下来(读操作不记录)，只许追加文件，不可以改写文件。redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。默认情况下，redis没有开启AOF的功能，需要在配置文件中设置。AOF保存的是appendonly.aof文件。(AOF好像作为一名学渣抄学霸的作业啊)官网解释Snapshotting is not very durable. If your computer running Redis stops, your power line fails, or you accidentally kill -9 your instance, the latest data written to Redis will be lost. While this may not be a big deal for some applications, there are use cases for full durability, and in these cases Redis snapshotting alone is not a viable option.写回策略appendfsyncAlways 同步写回，每个写命令执行完立刻同步地将日志写回磁盘。(持续的磁盘IO降低了服务器的性能)no 操作系统控制的写回，每个写命令执行完，只是先把日志写到AOF文件的内存缓冲区，由操作系统决定何时将缓冲区的内容写回磁盘。以上两个属于两种极端，第二种直接躺平了哈哈，很容易丟取数据。everysec 每秒写回，每个写命令执行完，只是先把日志写到AOF文件的内存缓冲区，每隔一秒把缓冲区中的内容写入磁盘。也是磁盘默认的写入策略。配置项写回时机优点缺点Always同步写回可靠性高，数据基本不丢失每个写命令都要落盘，性能影响较大Everysec每秒写回性能适中宕机时丢失1秒内的数据No操作系统控制的写回性能好宕机时丢失数据较多正常恢复开启AOF在配置文件1380行 appendonly yes重新调整保存路径开启AOF功能后，写操作继续，生成AOF文件到指定的目录重启redis，然后重新加载。异常恢复在高并发中，有可能出现内容写一小半，没有写完整，突然redis挂了，怎么样修复AOF文件完成数据的恢复。在appendonly路径下输入改行对于incr.aof文件进行修复，命令中一定要添加 --fix[root@XXX appendonlydir] # redis-check-aof --fix appendonly.aof.1.incr.aof该异常修复命令，将文件不符合语法规则的统统清空，再重新启动。重写机制AOF重写不仅降低了文件的占用空间，同时更小的AOF也可以更快地被Redis加载。开启aof修改重写峰值1k自动触发AOF重写机制(瘦身计划)手动触发AOF重写机制使用 BGREWRITEAOF 手动写入，直接更新文件名。也就是说，AOF文件重写并不是对原文件进行重新整理，而是直接读取服务器现有的键值对，然后用一条命令去代替之前记录的这个键值对的多条命令，生成一个新的文件后去替换原来的AOF文件。[note type="info flat"]总结1.AOF文件是一个只进行追加的日志文件2.Redis可以在AOF文件体积变得过大时，自动地在后台对AOF进行重写3.AOF文件有序的保存了对数据库执行的所有写操作，这些写入操作以Redis协议的格式保存，因此AOF的文件内容非常容易被人读懂，对文件进行分析也很轻松4.对于相同的数据集来说，AOF文件的体积通常要大于RDB文件的体积5.根据所使用的fsync策略，AOF的速度可能会慢于RDB。[/note]RDB+AOF混合持久化官方文档The general indication you should use both persistence methods is if you want a degree of data safety comparable to what PostgreSQL can provide you.告诉我们RDB和AOF可以共存。数据恢复流程和加载流程。如果同时开启rdb和aof持久化时，重启时只会加载aof文件，不会加载rdb文件开启混合方式设置 [label color="red"]设置aof-use-rdb-preamble的值为 yes[/label] yes表示开启，设置为no表示禁用RDB + AOF的混合方式RDB镜像做全量持久化，AOF做增量持久化。先使用RDB进行快照存储，然后使用AOF持久化记录所有的写操作，当重写策略满足或手动触发重写的时候，将最新的数据存储为新的RDB记录。重启服务的时候会从RDB和AOF两部分恢复数据，既保证了数据的完整性，又提高了恢复数据的性能。简单来说混合持久化方式产生的文件一部分是RDB格式，一部分是AOF格式。Redis事务[note type="flat"]回顾：事务：在一次与数据库的连接会话中，所有执行的sql要么一起成功，要么一起失败。这是传统的数据库事务。[/note]redis事务本质是一组命令的集合。一个事务中的所有命令都会序列化， [label color="red"]按照顺序地串化执行而不会被其他命令插入，不许加塞[/label] 。在一个队列中，一次性、顺序性、排他性的执行一系列操作正常执行将 MULTI 和 EXEC 中间插入事务的相关语句，在输入EXEC同时一起执行。127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> set k1 v1 QUEUED 127.0.0.1:6379(TX)> set k2 v2 QUEUED 127.0.0.1:6379(TX)> set k3 v3 QUEUED 127.0.0.1:6379(TX)> incr count QUEUED 127.0.0.1:6379(TX)> EXEC 1) OK 2) OK 3) OK 4) (integer) 2 127.0.0.1:6379> get k3 "v3" 放弃事务将 MULTI 和 DISCARD 中间插入事务的相关语句，在输入DISCARD后将不再执行该队列中的事务。127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> set k1 v1 QUEUED 127.0.0.1:6379(TX)> set k2 v22 QUEUED 127.0.0.1:6379(TX)> incr count QUEUED 127.0.0.1:6379(TX)> DISCARD OK 127.0.0.1:6379> get count "2" 全体连坐假设四条命令一条命令出错，将全部打回。在事务语句中出现错误，在执行exec之前，会返回所有命令，均不执行。冤头债主表示对的指令将放行，错的将打回。编译通过，运行出现错误，就会导致问题。出错了对于传统数据库可以rollback，但是Redis事务不支持事务回滚。Watch监控Watch命令是一种乐观锁的实现。Redis在修改的时候会检测数据是否被更改，如果更改了，则执行失败。举例：给一个key增加监控，开启事务，然后另一个线程优先修改了这个key的value值。那么事务将修改失败，key的value值以另一个线程的优先修改为主。事务中的其他指令将执行失败unwatch放弃监控依然监控一个key，但是我发现另一个线程优先修改了它的value值，那么我可以采用放弃监控 unwatch 策略，并在此线程开启事务中可以再次修改这个key。提交后以当前线程修改的key值为主，并且不会报nil(修改失败)。[note type="flat"]总结①一旦执行了exec之前加的监控锁都会被取消掉了②客户端退出连接，或连接丢失，所有东西都会被取消监视[/note][note type="success flat"]事务总结：开启：以MULTI开启一个事务入队：将多个命令入队到事务中，接到这些命令并不会立即执行，而是放到等待执行的事务队列里面执行：由EXEC命令触发事务提交，执行事务队列里的所有命令[/note]我书写这篇文章的初衷就是总结学习的进度，遗忘之际可以拿出来翻看，如有不对的地方还望指正，多多海涵。
这是我用于分享和复习算法笔记的地方，此篇主要是围绕关于数据结构的一些算法题。我在AcWing上刷题，可以和我一起:)AcWing 826. 单链表原址解题过程import java.io.*; import java.util.*; public class Main { private static final int N = 100010; private static final int[] e = new int[N]; private static final int[] ne = new int[N]; private static int idx; private static int head; public static void main(String[] args) throws IOException{ init(); //单链表 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String[] arrStr = br.readLine().split(" "); int n = Integer.parseInt(arrStr[0]); while(n -- > 0) { arrStr = br.readLine().split(" "); String let = arrStr[0]; if("H".equals(let)) { int x = Integer.parseInt(arrStr[1]); addToHead(x); } if("D".equals(let)) { int k = Integer.parseInt(arrStr[1]); if(k == 0) head = ne[head]; else remove(k - 1); } if("I".equals(let)) { int k = Integer.parseInt(arrStr[1]); int x = Integer.parseInt(arrStr[2]); add(k - 1, x); } } for(int i = head ;i != -1;i= ne[i]) System.out.print(e[i] + " "); br.close(); } private static void init() { //初始化的方法 idx = 0; head = -1; } //将x插入到头结点 private static void addToHead(int x ) { e[idx] = x; ne[idx] = head; head = idx ++; } //将x插入到下标为k的结点后,尾插法 private static void add(int k,int x) { e[idx] = x; ne[idx] = ne[k]; ne[k] = idx ++; } //删除下标为k后面的结点删除 private static void remove(int k) { ne[k] = ne[ne[k]]; } } 附上我的视频，(●'◡'●)[video title="单链表 " url="https://img.kaijavademo.top/typecho/uploads/2023/09/videos/%E5%8D%95%E9%93%BE%E8%A1%A8.mkv " container="b8fo4ln3enn" subtitle=" " poster=" "] [/video]AcWing 829. 模拟队列原址解题过程import java.io.*; import java.util.*; public class Main { private static final int N = 100010; private static final int[] q = new int[N]; private static int hh = 0;//队头 private static int tt = -1;//队尾 //往队尾插入元素，队头弹出元素 public static void main(String[] args) throws IOException{ //829. 模拟队列 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String[] arrStr = br.readLine().split(" "); int m = Integer.parseInt(arrStr[0]); while(m-- > 0) { arrStr = br.readLine().split(" "); String str = arrStr[0]; switch(str) { case "push": int x = Integer.parseInt(arrStr[1]); insert(x); break; case "pop": popFirst(); break; case "empty": String res = isEmpty(); System.out.println(res); break; default : int ans = query(); System.out.println(ans); } } br.close(); } //队尾插入 private static void insert(int x ) { q[++tt] = x; } //队头弹出 private static void popFirst() { hh++; } //判断队列是否为空 private static String isEmpty() { return hh > tt ? "YES": "NO"; } //查询对头元素 队尾元素q[tt] private static int query() { return q[hh]; } } 附上我的视频，用于复习。[video title="模拟队列 " url="https://img.kaijavademo.top/typecho/uploads/2023/09/videos/%E9%98%9F%E5%88%97.mkv " container="b9rk5gti6ee" subtitle=" " poster=" "] [/video]AcWing 831. KMP字符串原址解题过程 import java.util.*; import java.io.*; public class Main { private static final int N = 1000010; private static final int[] ne = new int[N]; public static void main(String[] args) throws IOException{ //831. KMP字符串 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out)); String[] arrStr = br.readLine().split(" "); int n = Integer.parseInt(arrStr[0]); String str = br.readLine(); char[] p = new char[n + 5]; for(int i = 1 ;i <= n ; i++) p[i] = str.charAt(i - 1); arrStr = br.readLine().split(" "); int m = Integer.parseInt(arrStr[0]); str = br.readLine(); char[] s = new char[m + 5]; for(int i = 1; i <= m; i++) s[i] = str.charAt(i - 1); kmp(p,n,s,m); bw.flush(); br.close(); bw.close(); } private static void kmp(char[] p ,int n ,char[] s,int m) throws IOException{ BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out)); //next最大公共前后缀字符串的创建 for(int i = 2 ,j = 0 ; i <= n; i++) { while(j > 0 && p[i] != p[j+1]) j = ne[j]; if(p[i] == p[j +1]) j++; ne[i] = j; } //kmp匹配 for(int i= 1,j = 0 ; i <= m ; i++) { while( j > 0 &&s[i] != p[j + 1] ) j = ne[j]; if(s[i] == p[j + 1]) j++; //匹配到模式串尾了的处理输出 if(j == n) { bw.write(i - n + " "); //继续 j = ne[j]; } } bw.flush(); bw.close(); } } 附上我的视频，用于复习~~。：)[video title="KMP字符串 " url="https://img.kaijavademo.top/typecho/uploads/2023/09/videos/KMP.mkv " container="bbm9bo70lpz" subtitle=" " poster=" "] [/video]AcWing 837. 连通块中点的数量原址解题过程import java.io.*; import java.util.*; public class Main{ //并查集的额外拓展，查询某个集合中元素的个数 private static final int N = 100010; private static final int[] p = new int[N]; //创建一个记录集合中元素个数的数组，等效于连通块中点的数量 private static final int[] size = new int[N]; public static void main(String[] args) throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out)); String[] arrStr = br.readLine().split(" "); int n = Integer.parseInt(arrStr[0]); int m = Integer.parseInt(arrStr[1]); for(int i = 1;i <= n ; i++) { p[i] = i; //初始化每个集合的元素个数为1 size[i] = 1; } while( m-- > 0 ){ arrStr = br.readLine().split(" "); String str = arrStr[0]; if("C".equals(str)){ int a = Integer.parseInt(arrStr[1]); int b = Integer.parseInt(arrStr[2]); merge(a,b); } if("Q1".equals(str)){ int a = Integer.parseInt(arrStr[1]); int b = Integer.parseInt(arrStr[2]); String ans = find(a) == find(b) ? "Yes" : "No"; bw.write(ans + "\n"); } if("Q2".equals(str)){ int a = Integer.parseInt(arrStr[1]); bw.write(size[find(a)] + "\n"); } } bw.flush(); br.close(); bw.close(); } private static void merge(int a,int b ){ int aP = find(a); int bP = find(b); if(aP == bP) return ; //将a中(集合的个数)连接块中点的数量赋值给b,合并,size记录每个集合的祖先节点 size[find(b)] += size[find(a)]; p[aP] = bP; } private static int find(int x){ if(p[x] != x) p[x] = find(p[x]); return p[x]; } }我也录视频用于记录，方便复习啦。[video title="并查集 " url="https://img.kaijavademo.top/typecho/uploads/2023/09/videos/%E5%B9%B6%E6%9F%A5%E9%9B%86.mkv " container="bxd9bcw90pj" subtitle=" " poster=" "] [/video]AcWing 840. 模拟散列表原址解题过程import java.io.*; import java.util.*; public class Main{ private static final int N = 100003; private static final Integer[] h = new Integer[N]; public static void main(String[] args) throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out)); String[] arrStr = br.readLine().split(" "); int n = Integer.parseInt(arrStr[0]); Arrays.fill(h,null); while(n-- > 0){ arrStr = br.readLine().split(" "); String str = arrStr[0]; int x = Integer.parseInt(arrStr[1]); if("I".equals(str)) h[find(x)] = x; if("Q".equals(str)){ String ans = h[find(x)] != null ? "Yes" : "No"; bw.write(ans + "\n"); } } bw.flush(); br.close(); bw.close(); } private static int find(int x){ int k = (x % N + N ) % N; while(h[k] != null && h[k] != x){ k++; if(k == N) k = 0; } return k ; } }附上我的视频用于复习捏····[video title="两种哈希表的实现介绍 " url="https://img.kaijavademo.top/typecho/uploads/2023/09/videos/%E5%93%88%E5%B8%8C%E8%A1%A8.mkv " container="by96joo7ml" subtitle=" " poster=" "] [/video]AcWing 843. n-皇后问题原址解题过程按行枚举import java.io.*; import java.util.*; public class Main{ private static final int N = 20; private static final char[][] path = new char[N][N]; private static final boolean[] col = new boolean[N]; private static final boolean[] dg = new boolean[N]; private static final boolean[] udg = new boolean[N]; private static int n; public static void main(String[] args) throws IOException{ BufferedReader br= new BufferedReader(new InputStreamReader(System.in)); BufferedWriter bw =new BufferedWriter(new OutputStreamWriter(System.out)); n = Integer.parseInt(br.readLine()); for(int i = 0 ; i < n ; i++){ for(int j = 0 ; j < n ; j++){ path[i][j] = '.'; } } dfs(0,br,bw); bw.flush(); br.close(); bw.close(); } private static void dfs(int u,BufferedReader br ,BufferedWriter bw)throws IOException{ if(u == n){ for(int i = 0 ; i <n ;i ++){ for(int j =0 ; j<n ; j ++){ bw.write(path[i][j]); } bw.write("\n"); } bw.write("\n"); return; } for(int i = 0 ; i < n ; i ++){ if(!col[i] && !dg[u + i] && !udg[u - i + n]){ path[u][i] = 'Q'; col[i] = dg[u + i] = udg[u - i + n] = true; dfs(u + 1,br,bw); col[i] = dg[u + i] = udg[u - i + n] = false; path[u][i] = '.'; } } } }附上我的视频用于复习捏····[video title="n皇后问题 " url="https://img.kaijavademo.top/typecho/uploads/2023/10/videos/n%E7%9A%87%E5%90%8E%E9%97%AE%E9%A2%98.mkv " container="b45ozgltrzh" subtitle=" " poster=" "] [/video]我书写这篇文章的初衷就是总结学习的进度，遗忘之际可以拿出来翻看，如有不对的地方还望指正，多多海涵。
在单独学习完SSM的三个部分后，需要对于三个部分做一个整合，整合过程中我发现了自己很多的不足。一方面我更新了之前的内容，由于之前的内容太长，不方便在原有基础上再添加一些补充一些自己的遗漏，特写一篇文章，发现整合过程中的不足。看上去比较简短，博主想了好久最终还是决定发出来了。然后现在要开学了，博主目前也在学习springboot3和算法，平时将一些有意义的算法题在对应博客继续更新，由于springboot3的版本太新问题有点多(已经踩了不少坑了，心痛)，暂时不做boot的笔记，往后会学习redis，后面的更新会比较慢,等到假期就会恢复回来，敬请期待~@Bean注解谈到这个注解，一定和配置类有关系，顺便熟悉一下配置类pom文件中依赖还是导一下，需要使用对应的spring组件 <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.0.6</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.3.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.14</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.2.4.RELEASE</version> </dependency> </dependencies>我们先创建配置类代替xml配置文件[video title="配置类 " url="https://img.kaijavademo.top/typecho/uploads/2023/08/videos/2023-08-26%2019-32-21.mkv " container="b3unv1gktx" subtitle=" " poster=" "] [/video]然后我们使用@Bean标签[video title="@Bean详解 " url="https://img.kaijavademo.top/typecho/uploads/2023/08/videos/2023-08-26%2021-02-00.mkv " container="btgzcxwab3t" subtitle=" " poster=" "] [/video]配置类源码JavaConfigurationpackage com.atguigu.config; import com.alibaba.druid.pool.DruidDataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.*; import org.springframework.jdbc.core.JdbcTemplate; import javax.sql.DataSource; import javax.xml.crypto.Data; /** * @author: TangZhiKai * @create: 2023-08-26 19:05 * @description: Java的配置类，替代xml配置文件 * 配置类中可以做三件事 * 1.包扫描注解配置 * 2.引用外部配置文件 * 3.声明第三方依赖的bean组件 * * 步骤一：添加@Configuration 代表我们是配置类 * 步骤二：实现上面的三个功能注解 **/ @ComponentScan(value = "com.atguigu.ioc_01") @PropertySource(value = "classpath: jdbc.properties") @Configuration public class JavaConfiguration { /* 第三方Bean标签 ——>一个方法 方法的返回值类型 = Bean组件的类型或者他的接口和父类 方法的名字 = Bean的id值(标识) 方法体可以自定义实现过程即可！ 最重要一步：@Bean，会真正让配置类方法创建的对象存储到IoC容器。 问题一：beanName的问题 默认：方法名 指定：name/value 属性起名字，覆盖方法名 问题二：周期方法如何指定 方法一： 原有注解方案:PostConstruct+PreDestroy 注解指定 方法二： bean属性指定 通过initMethod/ destoryMethod属性指定 问题三：作用域 和以前还是一样@Scope注解，默认是单实例SCOPE_SINGLETON 问题四：如何引用其他的IoC容器组件 1.直接调用对方的Bean方法即可 2.直接形参变量进行引用，要求必须有对应的组件，如果有多个，形参名= 组件id表示即可 */ //声明在全局使用，如果只想在对应方法中使用，声明在方法的形参列表 //@Value外部文件属性赋值 @Value("${atguigu.url}") private String url; @Value("${atguigu.driver}") private String driver; @Value("${atguigu.username}") private String username; @Value("${atguigu.password}") private String password; @Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON) @Bean(name = "ergouzi",initMethod ="",destroyMethod = "") public DruidDataSource dataSource(){ //实现具体的实例化过程 DruidDataSource dataSource = new DruidDataSource(); /* 属性来自于外部的配置文件,定义在成员方法*/ dataSource.setUrl(url); dataSource.setDriverClassName(driver); dataSource.setUsername(username); dataSource.setPassword(password); return dataSource; } //JdbcTemplate ->DataSource @Bean public JdbcTemplate jdbcTemplate1(){ JdbcTemplate jdbcTemplate = new JdbcTemplate(); //需要DataSource 需要IoC容器中的其他组件 //方案一：如果其他组件也是@Bean方法，可以直接调用,看着在调用方法，本质上从IoC容器中获取组件 jdbcTemplate.setDataSource(dataSource()); return jdbcTemplate; } @Bean public JdbcTemplate jdbcTemplate2(DataSource dataSource){ JdbcTemplate jdbcTemplate = new JdbcTemplate(); /*方案二：直接使用形参列表传值,IoC容器自动帮我们进行装配 形参列表直接声明想要的组件类型，IoC容器也会注入； 使用形参变量注入，要求必须要有对应的类型组件，如果没有，抛异常！ 如果有多个类型(IoC容器中假设有多个datasource)：可以使用形参名=Bean的id标识即可 */ jdbcTemplate.setDataSource(dataSource); return jdbcTemplate; } } 框架整合父工程中导入依赖，子工程中书写对应的配置类信息父工程中导入依赖<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.atguigu</groupId> <artifactId>ssm-integration-part</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <modules> <module>ssm-integration-01</module> </modules> <properties> <!--<thymeleaf-spring5.version>3.0.12.RELEASE</thymeleaf-spring5.version>--> <!--<javax.servlet-api.version>3.1.0</javax.servlet-api.version>--> <spring.version>6.0.6</spring.version> <jakarta.annotation-api.version>2.1.1</jakarta.annotation-api.version> <jakarta.jakartaee-web-api.version>9.1.0</jakarta.jakartaee-web-api.version> <jackson-databind.version>2.15.0</jackson-databind.version> <hibernate-validator.version>8.0.0.Final</hibernate-validator.version> <mybatis.version>3.5.11</mybatis.version> <mysql.version>8.0.25</mysql.version> <pagehelper.version>5.1.11</pagehelper.version> <druid.version>1.2.8</druid.version> <mybatis-spring.version>3.0.2</mybatis-spring.version> <jakarta.servlet.jsp.jstl-api.version>3.0.0</jakarta.servlet.jsp.jstl-api.version> <logback.version>1.2.3</logback.version> <lombok.version>1.18.26</lombok.version> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <!-- 需要依赖清单分析: spring ioc/di spring-context / 6.0.6 jakarta.annotation-api / 2.1.1 jsr250 aop spring-aspects / 6.0.6 tx spring-tx / 6.0.6 spring-jdbc / 6.0.6 springmvc spring-webmvc 6.0.6 jakarta.jakartaee-web-api 9.1.0 jackson-databind 2.15.0 hibernate-validator / hibernate-validator-annotation-processor 8.0.0.Final mybatis mybatis / 3.5.11 mysql / 8.0.25 pagehelper / 5.1.11 整合需要 加载spring容器 spring-web / 6.0.6 整合mybatis mybatis-spring x x 数据库连接池 druid / x lombok lombok / 1.18.26 logback logback/ 1.2.3 --> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>jakarta.annotation</groupId> <artifactId>jakarta.annotation-api</artifactId> <version>${jakarta.annotation-api.version}</version> </dependency> <!--<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency>--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-web-api</artifactId> <version>${jakarta.jakartaee-web-api.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>jakarta.servlet.jsp.jstl</groupId> <artifactId>jakarta.servlet.jsp.jstl-api</artifactId> <version>${jakarta.servlet.jsp.jstl-api.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson-databind.version}</version> </dependency> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>${hibernate-validator.version}</version> </dependency> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator-annotation-processor</artifactId> <version>${hibernate-validator.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>${pagehelper.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis-spring.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid.version}</version> </dependency> <!--<dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring5</artifactId> </dependency>--> </dependencies> </project>业务层书写ServiceJavaConfig/** * @description: 业务层配置类：service,aop,tx * @author: TangZhiKai * @create: 2023-08-26 18:07 * 1.service * 2.开启aop注解支持 aspect:@Before @After @AfterReturning @AfterThrowing @Around @Aspect @Order * 3.声明式事务管理：1.对应的事务管理器实现[TransactionManager DataSource... Hibernate...Jpa] 2.开启事务注解的支持@Transactional **/ @Configuration @EnableAspectJAutoProxy//开启注解支持 @EnableTransactionManagement//开启事务注解支持 @Transactional @ComponentScan("com.atguigu.service")//扫描包 public class ServiceJavaConfig { //需要项目的连接池 @Bean public TransactionManager transactionManager(DataSource dataSource){ DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(); dataSourceTransactionManager.setDataSource(dataSource); return dataSourceTransactionManager; } } 控制层书写对应控制层的配置不是很完全，详情可见，而且这里用了过时的jsp，可以使用thymeleaf配置视图解析器WebMvcJavaConfig/** * @description: 配置控制层的配置类(controller, springmvc) * @author: TangZhiKai * @create: 2023-08-26 17:33 * * 1.Controller * 2.全局异常处理 * 3.handlerMapping handlerAdapter * 4.静态资源处理 * 5.jsp视图解析器前后缀 * 6.json转化器 * 7.拦截器 **/ //3.handlerMapping handlerAdapter|6.json转化器 @EnableWebMvc //声明当前类为一个配置类 @Configuration //开启MVC的自动扫描 @ComponentScan("com.atguigu.controller") public class WebMvcJavaConfig implements WebMvcConfigurer { @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { //开启静态资源处理 configurer.enable(); } /* 配置拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new ) } */ /*配置jsp视图解析器*/ @Override public void configureViewResolvers(ViewResolverRegistry registry) { registry.jsp("/WEB-INF/views/","jsp"); } }持久层组件提供不需要配置mapper-config.xml的写法MapperJavaConfigNewpackage com.atguigu.config; import com.alibaba.druid.support.logging.SLF4JImpl; import com.github.pagehelper.PageInterceptor; import org.apache.ibatis.logging.slf4j.Slf4jImpl; import org.apache.ibatis.session.AutoMappingBehavior; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.mapper.MapperScannerConfigurer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import javax.sql.DataSource; import java.util.Properties; /** * @author: TangZhiKai * @create: 2023-08-27 16:28 * @description: 持久层配置类：连接池信息，以及sqlSessionFactory,Mapper代理对象 * 方式二：不保留外部配置文件，全部mybatis的属性都在代码中设置! * * **/ @Configuration public class MapperJavaConfigNew { @Bean public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){ SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); //指定配置文件的信息 //1.指定数据库连接池对象 sqlSessionFactoryBean.setDataSource(dataSource); org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); //设置驼峰式映射 configuration.setMapUnderscoreToCamelCase(true); //开启日志输出 configuration.setLogImpl(Slf4jImpl.class); //开启resultMap的自动映射 configuration.setAutoMappingBehavior(AutoMappingBehavior.FULL); //2.指定mybatis配置文件的功能，使用代码的形式 sqlSessionFactoryBean.setConfiguration(configuration);//存储setting的配置文件 //设置类型别名 sqlSessionFactoryBean.setTypeAliasesPackage("com.atguigu.pojo"); //设置分页插件 PageInterceptor pageInterceptor = new PageInterceptor(); Properties properties = new Properties(); properties.setProperty("helperDialect","mysql"); pageInterceptor.setProperties(properties); sqlSessionFactoryBean.addPlugins(pageInterceptor); return sqlSessionFactoryBean; } //mapper代理对象加入到ioc @Bean public MapperScannerConfigurer mapperScannerConfigurer(){ //Mapper代理对象的factoryBean ->指定一个包 -> mapper接口->sqlSessionFactory->getMapper-> mapper代理对象->ioc MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer(); //mapper接口和mapperxml文件所在的共同包 mapperScannerConfigurer.setBasePackage("com.atguigu.mapper"); return mapperScannerConfigurer; } } DataSourceJavaConfig/** * @author: TangZhiKai * @create: 2023-08-27 16:48 * @description: 连分开配置,接池配置类 **/ @Configuration @PropertySource("classpath:jdbc.properties") public class DataSourceJavaConfig { @Value("${jdbc.user}") private String user; @Value("${jdbc.password}") private String password; @Value("${jdbc.url}") private String url; @Value("${jdbc.driver}") private String driver; @Bean public DataSource dataSource(){ DruidDataSource dataSource = new DruidDataSource(); dataSource.setUsername(user); dataSource.setUrl(url); dataSource.setPassword(password); dataSource.setDriverClassName(driver); return dataSource; } } 最后在SpringIoCInit中初始化即可附上最后的整合[video title="整合流程 " url="https://img.kaijavademo.top/typecho/uploads/2023/08/videos/2023-08-27%2018-48-56.mkv " container="bi95otagsy" subtitle=" " poster=" "] [/video]9月8日，我做出来了SSM终极实战项目，微头条，我放在了博主日志中留于纪念，代表学习的一个阶段。我书写这篇文章的初衷就是总结学习的进度，遗忘之际可以拿出来翻看，如有不对的地方还望指正，多多海涵。
这里是我刷算法题的笔记，来自于acwing，我会将我做算法题的心得和改进写在笔记中用于分享和交流，你也可以和我一起刷一起交流。AcWing 786. 第k个数题目一开始我想到的是，用快排，然后遍历做一个判断(看起来笨笨的，新手只能想到这个QAQ)import java.util.Scanner; import java.util.stream.IntStream; public class Main{ public static void main(String[] args){ Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int k = sc.nextInt(); int[] arr = IntStream.range(0,n).map(i -> sc.nextInt()).toArray(); int[] sortedArr = quickSort(arr,0,n-1); for(int i = 0; i < sortedArr.length; i++){ if(i == (k-1)){ System.out.println(sortedArr[i]); } } } private static int[] quickSort(int[] q,int l,int r){ if(l >= r) return q; int p = q[l+ r >>1],i = l-1,j = r+1; while(i<j){ while(q[++i] < p); while(q[--j] > p); if(i<j){ int t = q[i]; q[i] = q[j]; q[j] = t; } } quickSort(q,l,j); quickSort(q,j+1,r); return q; } }这样写勉勉强强过了，但是这样处理笨笨的，我在看了y总的题解之后知道这道题有一种更好的办法。实际上我们先回顾快排的步骤确定分解点（一共有四种，听说现在使用中点和随机不会超时）处理分界点左边和右边的数，保证左边的最大值小于右边的最小值，当然分界点的元素相同的话分布在左右两边递归处理左边和右边的数快排的时间复杂度平均是 [label color="blue"]O(nlogn)[/label] ,这道题其实上使用的是快速选择排序。我们可以这样考虑，找第k个数，无非是在处理完分解点之后，将一组数分成两部分，我们可以比较第k个数的位置，那么此会产生两种情况当左区间的个数sl大于k，说明k在左边当左区间的个数sl小于k，说明k在右边。此时k在右边的位置应该是k-sl。通过这两种思路我们只需要把k作为形参放在快排方法中，方法的第三步我们稍作调整，递归中加入k，根据不同的情况返回不同的递归值。如果遍历有右区间，那么应该寻找的是第k - sl个数。 [label color="red"]会保证第k小的数一直在递归的区间中，那么当区间里只有一个数的时候，就一定是要找的数了。[/label] 那么我们需要调整返回值，和一开始的出口返回数组的左边界（q[l]）即可。import java.util.Scanner; import java.util.stream.IntStream; public class Main{ public static void main(String[] args){ Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int k = sc.nextInt(); int[] arr = IntStream.range(0,n).map(i -> sc.nextInt()).toArray(); System.out.println(quickSort(arr,0,n-1,k)); } private static int quickSort(int[] q,int l,int r, int k){ if(l >= r) return q[l]; int p = q[l+ r >>1],i = l-1,j = r+1; while(i<j){ while(q[++i] < p); while(q[--j] > p); if(i<j){ int t = q[i]; q[i] = q[j]; q[j] = t; } } //计算左区间的长度 int sl = j - l + 1; //始终保持k在递归区间，当区间只有一个数的时候,那么就是我们要找的数 if(sl >= k){ return quickSort(q,l,j,k); }else{ return quickSort(q,j+1,r,k-sl); } } }这样的话第一遍是n，第二遍就是n/2，第三遍就是n/4....T(n) = n + n/2 + n/4 + ... < 2n考虑一下时间复杂度就是 [label color="blue"]O(n)[/label]AcWing 788. 逆序对的数量看到了不会做，一点思路都没有看了y总的题解之后觉得真的牛，实际上核心思路就是通过不断的递归+分治，实际上的三种情况只需要考虑，黄球的情况，而递归可以将整个部分连续划分，就不存在红球和绿球的情况了，定义变量res，一方面逆序数等于左右两个半的和，另一部分左半边元素大于右半边元素记录逆序数。我才用API+Stream的方式拿下。import java.util.*; import java.util.stream.*; public class Main{ public static void main(String[] args){ Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int[] arr = IntStream.range(0,n).map(i->sc.nextInt()).toArray(); System.out.println(mergeSort(arr,0,n-1)); } private static long mergeSort(int[] q,int l ,int r){ if(l >= r) return 0; int mid = l+r >> 1; long res = mergeSort(q,l,mid)+ mergeSort(q,mid+1,r); int i =l,j=mid+1,k=0; int[] t = new int[r-l+1]; while(i<=mid && j<= r){ if(q[i] <= q[j]){ t[k++] = q[i++]; }else{ t[k++] = q[j++]; res += mid - i + 1; } } while(i<=mid) t[k++] = q[i++]; while(j<=r) t[k++] = q[j++]; //物归原主 for(i=l,j=0; i<= r; i++,j++) q[i] = t[j]; return res; } }随后我想使用缓冲流来试试，结果发现缓冲流使用比Scanner类快得多，但是代码太长了不好写。import java.util.*; import java.io.*; public class MergeSortTest5{ //优先定义一个数据规模 static int N = 100010; static int[] arr = new int[N]; public static void main(String[] args) throws IOException{ //使用缓冲流来录入数据 BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); int n = Integer.parseInt(reader.readLine()); String[] arrStr = reader.readLine().split(" "); for (int i = 0; i < n; i++) { arr[i] = Integer.parseInt(arrStr[i]); } //输出逆序数 System.out.println(mergeSort(arr, 0, n-1)); //关闭流 reader.close(); } private static long mergeSort(int[] q,int l ,int r){ if(l >= r) return 0; int mid = l+r >> 1; long res = mergeSort(q,l,mid)+ mergeSort(q,mid+1,r); int i =l,j=mid+1,k=0; int[] t = new int[r-l+1]; while(i<=mid && j<= r){ if(q[i] <= q[j]){ t[k++] = q[i++]; }else{ t[k++] = q[j++]; res += mid - i + 1; } } while(i<=mid) t[k++] = q[i++]; while(j<=r) t[k++] = q[j++]; //物归原主 for(i=l,j=0; i<= r; i++,j++) q[i] = t[j]; return res; } }AcWing 789. 数的范围这道题给我感觉也是比较有难度，刚开始左右二分不太会套，后面也是看了几遍题解才有收获，还是要多刷一刷。算法给我总体感觉是要有思路，技巧性比较强.我单纯一两句话说不明白，附上视频把[video title="acwing.789.数的范围 " url="https://img.kaijavademo.top/typecho/uploads/2023/08/videos/2023-08-25%2015-32-55.mkv " container="bxb9du5xq5f" subtitle=" " poster=" "] [/video]我在视频中是初始化开辟了一块空间，但是我实际上并没有使用arr这个数组，在main方法内部，我声明了一次，相当于白开辟了一个数组，只需要把main方法中，直接让arr = 那一部分就可以了，稍微改一下(我录完才知道问题的。。)import java.util.*; import java.util.stream.*; public class Main{ //789. 数的范围 public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int q = sc.nextInt(); int[] arr = IntStream.range(0, n).map(i->sc.nextInt()).toArray(); while(q-- > 0) { int k = sc.nextInt(); int[] ans = binarySarch(arr, q); IntStream.range(0, 2).mapToObj(i-> ans[i] + " ").forEach(System.out::print); System.out.println(); } } private static int[] binarySarch(int[] q,int tar) { int[] res = new int[2]; int n = q.length; int l = 0; int r = n-1; //左边界二分查找 while(l<r) { int mid = l+r>>1; //我要求寻找的内容位于右侧，意味着全部大于tar if(q[mid] >= tar) r = mid; else l = mid + 1; } //如果没有找到，则默认-1 -1 if(q[l] != tar) return new int[] {-1,-1}; //将初始索引归位，并初始化 res[0] = l; l = 0; r = n-1; //右边界二分查找 while(l<r) { int mid = l+r +1>>1; //此时我要求我要寻找的内容位于左侧，全部小于tar if(q[mid] <= tar) l=mid; else r = mid -1; } //物归原主 res[1] = l; return res; } } AcWing 798. 差分矩阵可以看看视频[video title="差分矩阵 " url="https://img.kaijavademo.top/typecho/uploads/2023/09/videos/2023-09-01%2002-26-18.mkv " container="bi9ibzhavb" subtitle=" " poster=" "] [/video]附上代码import java.io.*; public class Main{ private static final int N = 1010; private static final int[][] a = new int[N+5][N+5]; private static final int[][] b = new int[N+5][N+5]; public static void main(String[] args) throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String[] arrStr = br.readLine().split(" "); int n = Integer.parseInt(arrStr[0]); int m = Integer.parseInt(arrStr[1]); int q = Integer.parseInt(arrStr[2]); for(int i = 1; i<= n;i++){ arrStr = br.readLine().split(" "); for(int j = 1; j<= m; j++){ a[i][j] = Integer.parseInt(arrStr[j -1]); diff(i,j,i,j,a[i][j]); } } while(q-- >0){ arrStr = br.readLine().split(" "); int x1 = Integer.parseInt(arrStr[0]); int y1 = Integer.parseInt(arrStr[1]); int x2 = Integer.parseInt(arrStr[2]); int y2 = Integer.parseInt(arrStr[3]); int c = Integer.parseInt(arrStr[4]); diff(x1,y1,x2,y2,c); } sumPrefix(n , m); for(int i = 1;i<= n ;i++){ for(int j = 1; j <= m ; j++){ System.out.print(a[i][j] +" "); } System.out.println(); } br.close(); } private static void diff(int x1,int y1,int x2,int y2,int c){ b[x1][y1] += c; b[x2 + 1][y1] -= c;b[x1][y2 + 1] -= c; b[x2 + 1][y2 + 1] += c; } private static void sumPrefix(int n ,int m){ for(int i =1;i<= n ; i++){ for(int j = 1; j<= m;j++){ a[i][j] = a[i-1][j] + a[i][j - 1] -a[i - 1][j -1] + b[i][j]; } } } }原先时间复杂度如果是扫一边二维数组应该是O(n^2)我好像说错了捏。AcWing 803. 区间合并原址import java.io.*; import java.util.*; public class Main { public static void main(String[] args) throws IOException{ //803. 区间合并 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String[] arrStr = br.readLine().split(" "); int n = Integer.parseInt(arrStr[0]); int[][] a = new int[n][8]; for(int i=0;i< n;i++) { arrStr = br.readLine().split(" "); int l = Integer.parseInt(arrStr[0]); int r = Integer.parseInt(arrStr[1]); a[i][0] = l; a[i][9] = r; } int[][] ans = merge(a); System.out.println(ans.length); br.close(); } private static int[][] merge(int[][] a){ if( a==null || a.length == 0||a[0].length == 0) { return new int[0][10]; } //升序排列a数组 Arrays.sort(a,Comparator.comparingInt(i -> i[0])); List<int[]> res = new ArrayList<>(); for(int[] arr : a) { int l = arr[0]; int r = arr[1]; if(res.size() == 0 || res.get(res.size() - 1)[1] < l) { res.add(new int[]{l,r}); }else { int newRight = Math.max(r,res.get(res.size() - 1)[1]); res.set(res.size() - 1, new int[]{res.get(res.size() - 1)[0],newRight}); } } return res.toArray(new int[res.size()][11]); } } 附上我的视频讲解，哈哈[video title="合并区间 " url="https://img.kaijavademo.top/typecho/uploads/2023/09/videos/2023-09-05%2022-54-32.mkv " container="bntyqzgbkpb" subtitle=" " poster=" "] [/video]AcWing 802. 区间和这道题也是困扰我好久了，算是第一章最难的题了把，然后看了一下老哥这位老哥的题解，我觉得写的非常好，让我很快就明白了orz。原址解题过程import java.io.*; import java.util.*; import java.util.stream.*; public class Main { private static final int N = 300010; private static final int[] a = new int[N]; private static final int[] s = new int[N]; public static void main(String[] args) throws IOException{ //802. 区间和 //1.准备 //2.准备a数组，并前缀和s //3.返回离散化后的查询结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String[] arrStr = br.readLine().split(" "); int n = Integer.parseInt(arrStr[0]); int m = Integer.parseInt(arrStr[1]); //1.准备三个集合 List<Integer> alls = new ArrayList<Integer>(); //来存储n次插入 List<Pair> add = new ArrayList<Pair>(); //来储存m次查询 List<Pair> query = new ArrayList<Pair>(); //2.填充好集合 for(int i = 0; i< n; i++) { arrStr = br.readLine().split(" "); int x = Integer.parseInt(arrStr[0]); int c = Integer.parseInt(arrStr[1]); add.add(new Pair(x, c)); alls.add(x); } for(int i =0 ; i<m ;i++) { arrStr = br.readLine().split(" "); int l = Integer.parseInt(arrStr[0]); int r = Integer.parseInt(arrStr[1]); query.add(new Pair(l, r)); alls.add(l); alls.add(r); } alls = alls.stream().distinct().sorted().collect(Collectors.toList()); //4.离散化，将离散的稀疏的离散区间，整合到一个稠密区间，从而才可以通过前缀和运算 for(Pair item : add) { int i = find(item.first,alls); a[i] += item.secound; } //前缀和公式 for(int i = 1;i<= alls.size();i++) s[i] = s[i - 1] + a[i]; for(Pair item: query) { int l = find(item.first,alls); int r = find(item.secound,alls); System.out.println(s[r] - s[l - 1]); } br.close(); } //3.写二分查找 private static int find(int x,List<Integer> list) { int l = 0; int r = list.size()- 1; while(l< r) { int mid = l+r>>1; if(list.get(mid) > x) r = mid; else l = mid + 1; } return l + 1; } } //来记录操作的类，包括查询和存入 class Pair{ int first; int secound; public Pair(int x,int c) { this.first = x; this.secound = c; } }附上我的视频方便复习~~~[video title="离散化区间和 " url="https://img.kaijavademo.top/typecho/uploads/2023/09/videos/2023-09-06%2022-52-22.mkv " container="bb5asleqqwm" subtitle=" " poster=" "] [/video]一开始紧张说错了，首先应该想到的是前缀和 ToT~~AcWing 800. 数组元素的目标和原址解题过程import java.util.*; import java.io.*; public class Main{ private static final int N = 100010; private static final int[] a = new int[N]; private static final int[] b = new int[N]; public static void main(String[] args) throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String[] arrStr = br.readLine().split(" "); int n = Integer.parseInt(arrStr[0]); int m = Integer.parseInt(arrStr[1]); int x = Integer.parseInt(arrStr[2]); arrStr = br.readLine().split(" "); for(int i= 0 ;i < n ;i++) a[i] = Integer.parseInt(arrStr[i]); arrStr = br.readLine().split(" "); for(int i= 0 ;i < m ;i++) b[i] = Integer.parseInt(arrStr[i]); List<int[]> ans = doublePoint(a,n,b,m,x); //使用迭代(增强)for 循环进行输出 for(int[] arr: ans){ System.out.println(arr[0] + " " + arr[1]); } br.close(); } private static List<int[]> doublePoint(int[] a,int n ,int[] b,int m ,int x){ List<int[]> res = new ArrayList<>(); for(int i=0,j=m - 1; i < n; i++){ while(j >= 0 && a[i] + b[j] > x) j--; if(j >= 0 && a[i] + b[j] == x) res.add(new int[]{i,j}); } return res; } }附上我的视频便于复习~~[video title="双指针 " url="https://img.kaijavademo.top/typecho/uploads/2023/09/videos/%E5%8F%8C%E6%8C%87%E9%92%88.mkv " container="ba2ey700a6j" subtitle=" " poster=" "] [/video]我书写这篇文章的初衷就是总结学习的进度，遗忘之际可以拿出来翻看，如有不对的地方还望指正，多多海涵。
MyBatis笔记：各种查询方式|特殊SQL|自定义映射|多表联查|动态SQL|逆向工程
MyBatis笔记：各种查询功能,特殊SQL语句，动态SQL语句，MyBatis中的一二级缓存，清新版和奢华版的逆向工程的使用，包括我自己的对于多表联查的视频解释，分页查询的插件，还有我对于报错的问题解决思路。篇幅较长，可以选择所需查看。各种查询方式查询单条信息/多条信息SelectMapper接口public interface SelectMapper { /* 根据id来查询用户信息/返回值可以使用User或者是List<User> */ public abstract List<User> getUserById(@Param("id") Integer id); /* 查询所有的用户信息 */ public abstract List<User> getAllUser(); }SelectMapper.xml select * from t_user where id = #{id} <!--List<User> getAllUser();--> select * from t_user 测试类public class SelectMapperTest { /** * MyBatis的各种查询功能 * 1.若查询出的数据只有一条， * a>可以通过实体类对象 * b>可以通过List集合接收 * c>可以通过Map集合接收 * * 2.若查询出的数据有多条， * a>可以通过List集合接收 * b>可以通过Map集合接收 *注意：一定不能通过实体类对象来接收，此时会抛异常TooManyResultsException * */ @Test public void testGetAllUser(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(); SelectMapper mapper = sqlSession.getMapper(SelectMapper.class); //查询所有的用户信息 System.out.println("user = " + mapper.getAllUser()); } @Test public void testGetUserById(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(); SelectMapper mapper = sqlSession.getMapper(SelectMapper.class); //查询用户id为3的用户信息 System.out.println("user = " + mapper.getUserById(3)); } }使用聚合函数查询单行信息SelectMapper接口 /* 查询用户信息的总记录数 */ public abstract Integer getCount();SelectMapper.xml select count(*) from t_user 查询用户信息为Map集合我们可以把查询出来的结果转换为一个map集合，以字段为键，字段对应的值为值。查询单条数据为Map集合SelectMapper接口 /* 根据id查询用户信息为一个map集合 */ public abstract Map<String, Object> getUserByIdToMap(@Param("id") Integer id);SelectMapper.xml <!--Map<String, Object> getUserByIdToMap(@Param("id") Integer id);--> select * from t_user where id = #{id} 查询多条数据为Map集合把map集合放入List集合中需要调整返回值，把多条数据的返回值放到一个List集合中，每一条转换的map放到了List集合中在接口方法上添加@MapKey注解 [label color="red"]@MapKey设置当前map集合的键，会把我们查询出来数据的某一个字段作为键(唯一)，查询出来数据的map集合作为值[/label]SelectMapper接口 /* 查询所有用户信息为map集合 */ //public abstract List<Map<String, Object>> getAllUserToMap(); @MapKey("id") public abstract Map<String, Object> getAllUserToMap();SelectMapper.xml <!--Map<String, Object> getAllUserToMap();--> select * from t_user 测试类最终源码SelectMapperTestpublic class SelectMapperTest { /** * MyBatis的各种查询功能 * 1.若查询出的数据只有一条， * a>可以通过实体类对象 * b>可以通过List集合接收 * c>可以通过Map集合接收 *结果：Map = {password=123456, sex=男, id=3, age=23, email=12345@qq.com, username=admin} * * 2.若查询出的数据有多条， * a>可以通过实体类类型的List集合接收 * b>可以通过map类型的Map集合接收 * c>可以在mapper接口方法上添加@MapKey注解， * 此时就可以将每条数据转换的map集合作为值，以某个字段的值作为键，放在同一个map集合中 *注意：一定不能通过实体类对象来接收，此时会抛异常TooManyResultsException * * MyBatis中设置了默认的类型别名 * Java.lang.Integer -->int,Integer * int --> _int,integer * Map --> map * String -->string * */ @Test public void testGetALLUserToMap(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(); SelectMapper mapper = sqlSession.getMapper(SelectMapper.class); System.out.println("Map = " + mapper.getAllUserToMap()); } @Test public void testGetUserByIdToMap(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(); SelectMapper mapper = sqlSession.getMapper(SelectMapper.class); //查询id为3的用户信息的map集合 System.out.println("Map = " + mapper.getUserByIdToMap(3)); } @Test public void testGetCount(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(); SelectMapper mapper = sqlSession.getMapper(SelectMapper.class); //查询所有的用户信息 System.out.println("Count = " + mapper.getCount()); } @Test public void testGetAllUser(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(); SelectMapper mapper = sqlSession.getMapper(SelectMapper.class); //查询所有的用户信息 System.out.println("user = " + mapper.getAllUser()); } @Test public void testGetUserById(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(); SelectMapper mapper = sqlSession.getMapper(SelectMapper.class); //查询用户id为3的用户信息 System.out.println("user = " + mapper.getUserById(3)); } } SelectMapper接口public interface SelectMapper { /* 根据id来查询用户信息/返回值可以使用User或者是List<User> */ public abstract List<User> getUserById(@Param("id") Integer id); /* 查询所有的用户信息 */ public abstract List<User> getAllUser(); /* 查询用户信息的总记录数 */ public abstract Integer getCount(); /* 根据id查询用户信息为一个map集合 */ public abstract Map<String, Object> getUserByIdToMap(@Param("id") Integer id); /* 查询所有用户信息为map集合 */ //public abstract List<Map<String, Object>> getAllUserToMap(); @MapKey("id") public abstract Map<String, Object> getAllUserToMap(); } SelectMapper.xml<?xml version="1.0" encoding="UTF-8" ?> <mapper namespace="com.atguigu.mybatis.mapper.SelectMapper"> select * from t_user where id = #{id} <!--List<User> getAllUser();--> select * from t_user select count(*) from t_user <!--Map<String, Object> getUserByIdToMap(@Param("id") Integer id);--> select * from t_user where id = #{id} <!--Map<String, Object> getAllUserToMap();--> select * from t_user </mapper>特殊SQL的执行能用#{}绝对不使用${}，以下情况使用#{}将会出现一定问题。处理模糊查询处理模糊查询如果使用之前的方式可能会出现一些问题。这时候有两种方式解决使用${} select * from t_user where username like '%${username}%'依然使用#{}，只不过写SQL语句要用到 [label color="blue"]字符串拼接concat[/label] select * from t_user where username like concat('%',#{username},'%')依然使用#{},只不过在%两边加双引号 [label color="red"](最常用)[/label] select * from t_user where username like "%"#{username}"%"批量删除在使用批量删除的语句中， delete from t_user where id in () 括号中无法使用#{}，因为#{}会自动解析出来多一对单引号，SQL语句语法格式不对无法实现批量删除，而只能使用${}来获取参数。SQLMapper.xml <delete id="deleteMore"> delete from t_user where id in (${ids}) </delete>动态设置表名对于动态设置表名，只能是${}来获取参数 <!-- List<User> getUserByTableName(@Param("tableName") String tableName);--> select * from ${tableName} 添加功能获取自增的主键场景：如果有一个一对多的表关系，有一个班级表，有一个学生表。一个班级有多个学生，班级对应学生应该是一对多的关系，一对多和多对一都要把表设置在多的一方，我们需要把学生表中设置班级的id。我们可以为某个班级分配学生，就是为我们的学生设置班级，我们需要在中间获取当前添加班级的id，才能为班级分配学生。JDBC中本身就有这样的功能，来获取添加自增的主键，JDBC中的主键回显而mybatis中也可以实现 <insert id="insertUser" useGeneratedKeys="true" keyProperty="id"> insert into t_user values (null,#{username},#{password},#{age},#{sex},#{email}) </insert>自定义映射resultMap之前实现查询功能，使用resultType，默认映射关系，要求字段名和属性名一致。把我们查询出来对应的字段名为属性赋值，如果当前字段名和属性名 [label color="red"]不一致[/label] ，或者是多对一、一对多的关系就需要使用resultMap。java中属性名遵循驼峰命名，MySQL中习惯以_分隔。如果字段名和属性名不一致，如果不改变命名的前提下，解决字段名和属性名不一致有三种方式。场景：有一个员工表和一个部门表，员工表中有一个部门id对应一个部门表中的部门id，产生了多对一和一对多在编写SQL语句时使用别名的形式，查询出来的字段名和属性名一致，就可以正常赋值。EmpMapper.xml <!--List<Emp> getAllEmp();--> select eid,emp_name empName,age,sex,email from t_emp 添加全局配置[font size="16" color="#240a40"]mapUnderscoreToCamelCase[/font]mybatis-config.xml<?xml version="1.0" encoding="UTF-8" ?> <configuration> <properties resource="jdbc.properties"/> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> <typeAliases> <package name="com.atguigu.mybatis.pojo"/> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments> <mappers> <package name="com.atguigu.mybatis.mapper"/> </mappers> </configuration>EmpMapper.xml <!--List<Emp> getAllEmp();--> select * from t_emp 使用resultMap设置自定义映射EmpMapper.xml <resultMap id="empResultMap" type="Emp"> <id property="eid" column="eid"></id> <result property="empName" column="emp_Name"></result> <result property="age" column="age"></result> <result property="sex" column="sex"></result> <result property="email" column="email"></result> </resultMap>使用resultMap处理有点麻烦，一般resultMap是来处理一对多和多对一的问题的。多对一的映射处理表和表直接有关系，表所映射的实体类直接也是存在一定的关系。Emp类public class Emp { private Integer eid; //使用驼峰命名 private String empName; private Integer age; private String sex; private String email; //多对一，创建一所对应的对象就可以 private Dept dept; [font size="16.5" color="#ff0000"]多对一创建一所对应的对象就可以，一对多设置一个集合就可以。[/font]处理多对一的映射关系也有三种方法使用级联属性赋值EmpMapper.xml <resultMap id="empAndDeptResultMapOne" type="Emp"> <id property="eid" column="eid"></id> <result property="empName" column="emp_name"></result> <result property="age" column="age"></result> <result property="sex" column="sex"></result> <result property="email" column="email"></result> <result property="dept.did" column="did"></result> <result property="dept.deptName" column="dept_name"></result> </resultMap> select * from t_emp left join t_dept on t_emp.did = t_dept.did where t_emp.eid = #{eid} 使用association标签实现使用association标签处理多对一的关系EmpMapper.xml <resultMap id="empAndDeptResultMapTwo" type="Emp"> <id property="eid" column="eid"></id> <result property="empName" column="emp_name"></result> <result property="age" column="age"></result> <result property="sex" column="sex"></result> <result property="email" column="email"></result> <association property="dept" javaType="Dept"> <id property="did" column="did"></id> <result property="deptName" column="dept_name"></result> </association> </resultMap> select * from t_emp left join t_dept on t_emp.did = t_dept.did where t_emp.eid = #{eid} 使用分步查询我们可以通过多个SQL语句一步步来实现，我们可以先查员工，通过员工的部门id查询部门信息。我只提供两个xml文件的书写方式，附上视频。[video title="多对一分步查询 " url="https://img.kaijavademo.top/typecho/uploads/2023/08/videos/2023-08-22%2002-42-15.mkv " container="beyz03osczc" subtitle=" " poster=" "] [/video]EmpMapper.xml <resultMap id="empAndDeptByStepResultMap" type="Emp"> <id property="eid" column="eid"></id> <result property="empName" column="emp_name"></result> <result property="age" column="age"></result> <result property="sex" column="sex"></result> <result property="email" column="email"></result> <association property="dept" select="com.atguigu.mybatis.mapper.DeptMapper.getEmpAndDeptByStepTwo" column="did"></association> </resultMap> select * from t_emp where eid = #{eid} DeptMapper.xml select * from t_dept where did = #{did} 延迟加载[note type="info flat"]通过使用多表联查，将当前一个完整的过程分布进行，有什么好处？并不是很麻烦，两个SQL语句放在一起就可以实现一个功能，不放在一起，那么他们各自也可以实现对应的功能。分布查询可以开启延迟加载(懒加载)之后，当前只访问员工信息，只查询员工。如果访问员工以及员工所对应的部门，即会执行当前员工的SQL，又会执行查询部门的SQL。[/note]MyBatis默认不开启延迟加载，我们需要手动开启lazyLoadingEnabled：延迟加载的全局开关。当开启时，所有关联对象都会延迟加载aggressiveLazyLoading：当开启时，任何方法的调用都会加载该对象的所有属性。 否则，每个属性会按需加载我们需要设置lazyLoadingEnabled为 [label color="red"]true[/label] ，aggressiveLazyLoading为 [label color="default"]false[/label] 。全局配置中开启延迟加载的功能，对于所有分布查询都是会延迟加载。当前有些功能如果不需要延迟加载，就需要通过在分布查询中的一个属性[font size="15.5" color="#240a40"]fetchType[/font]延迟或立即加载，使得延迟加载可控。[font size="15.5" color="#240a40"]fetchType[/font]有两个属性eager立即加载lazy延迟加载EmpMapper.xml <resultMap id="empAndDeptByStepResultMap" type="Emp"> <id property="eid" column="eid"></id> <result property="empName" column="emp_name"></result> <result property="age" column="age"></result> <result property="sex" column="sex"></result> <result property="email" column="email"></result> <association property="dept" select="com.atguigu.mybatis.mapper.DeptMapper.getEmpAndDeptByStepTwo" column="did" fetchType="eager"></association> </resultMap> select * from t_emp where eid = #{eid} 一对多的映射处理一个部门对应多个员工，通过集合来表示部门中所有员工信息collection标签DeptMapper接口 /* 获取部门以及部门中所有的员工信息 */ public abstract Dept getDeptAndEmp(@Param("did") Integer did);DeptMapper.xml <resultMap id="deptAndEmpResultMap" type="Dept"> <id property="did" column="did"></id> <result property="deptName" column="dept_name"></result> <collection property="emps" ofType="Emp"> <id property="eid" column="eid"></id> <result property="empName" column="emp_name"></result> <result property="age" column="age"></result> <result property="sex" column="sex"></result> <result property="email" column="email"></result> </collection> </resultMap> select * from t_dept left join t_emp on t_dept.did = t_emp.did where t_dept.did = #{did} 分步查询[video title="一对多分步查询 " url="https://img.kaijavademo.top/typecho/uploads/2023/08/videos/2023-08-22%2015-11-36.mkv " container="b4k5xtx7xcj" subtitle=" " poster=" "] [/video]DeptMapper.xml <resultMap id="deptAndEmpByStepResultMap" type="Dept"> <id property="did" column="did"></id> <result property="deptName" column="dept_name"></result> <collection property="emps" select="com.atguigu.mybatis.mapper.EmpMapper.getDeptAndEmpByStepTwo" column="did" fetchType="eager"></collection> </resultMap> select * from t_dept where did = #{did} EmpMapper.xml <!--List<Emp> getDeptAndEmpByStepTwo(@Param("did") Integer did);--> select * from t_emp where did = #{did} 动态SQL多条件查询应该什么时候拼接SQL，使用java程序判断实现比较麻烦。mybatis为我们提供了一套动态文件的SQL，方便我们拼接SQL语句。本质就是一系列标签，帮助我们拼接SQL语句if标签根据标签中的test属性所对应的表达式决定标签中的内容是否需要拼接到SQL中写第一个条件判断之前需要加一个恒成立条件，防止第一个参数为null，语句拼接错误DynamicSQLMapper.xml <!--List<Emp> getEmpByCondition(Emp emp);--> select * from t_emp where 1=1 <if test="empName != null and empName != ''"> and emp_name = #{empName} </if> <if test="age != null and age != ''"> and age = #{age} </if> <if test="sex != null and sex != ''"> and sex = #{sex} </if> <if test="email != null and email != ''"> and email = #{email} </if> where标签上面的if标签中where标签写死，所以说必须添加恒成立条件，还有一种情况就是where后面的条件均不成立，那么where多余了。如果说我们在上述使用where标签，可以帮助我们动态生成where关键字，并且可以将where标签体中的内容前面，多余的and去掉(or)。即使我们将条件均不成立，那么SQL语句将不会生成where。[font size="16" color="#ff0000"]注意：where标签不能将其中内容后面多余的and或or去掉[/font] select * from t_emp <where> <if test="empName != null and empName != ''"> emp_name = #{empName} </if> <if test="age != null and age != ''"> and age = #{age} </if> <if test="sex != null and sex != ''"> and sex = #{sex} </if> <if test="email != null and email != ''"> and email = #{email} </if> </where> trim标签若标签中有内容时：prefix|suffix:将trim标签中内容前面或后面添加指定内容prefixOverrides|suffixOverrides:将trim标签中内容前面或后面去掉指定内容若标签中没有内容时，trim标签也没有任何效果。DynamicSQLMapper.xml select * from t_emp <trim prefix="where" suffixOverrides="and|or"> <if test="empName != null and empName != ''"> emp_name = #{empName} and </if> <if test="age != null and age != ''"> age = #{age} or </if> <if test="sex != null and sex != ''"> sex = #{sex} and </if> <if test="email != null and email != ''"> email = #{email} </if> </trim> choose,when,otherwise标签相当于java中的if...else if...elsechoose是一个副标签，表示一个完整的if...else if...else结构when和if标签一样 <when test=""></when>when至少要有一个，otherwise最多只能有一个DynamicSQLMapper.xml <!--List<Emp> getEmpByChoose(Emp emp);--> select * from t_emp <where> <choose> <when test="empName != null and empName != ''"> emp_name = #{empName} </when> <when test="age != null and age != ''"> age = #{age} </when> <when test="sex != null and sex != ''"> sex = #{sex} </when> <when test="email != null and email != ''"> email = #{email} </when> <otherwise> did = 1 </otherwise> </choose> </where> foreach标签从浏览器中获取到的是一个数组，获取数据id实现批量删除。我们需要循环数组，获取数组中每个数据，然后在 where id IN (1,2,3) 就可以实现,或者使用or分隔语句进行删除。场景：通过数组实现批量删除，通过集合实现批量添加DynamicSQLMapper接口 /* 通过数组实现批量删除 */ public abstract int deleteMoreByArray(@Param("eids") Integer[] eids);测试类 @Test public void testDeleteMoreByArray(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(); DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class); int result = mapper.deleteMoreByArray(new Integer[]{6, 7, 8, 9, 10, 11}); System.out.println("result = " + result); }使用IN进行批量删除DynamicSQLMapper.xml <delete id="deleteMoreByArray"> delete from t_emp where eid in <foreach collection="eids" item="eid" separator="," open="(" close=")"> #{eid} </foreach> </delete> 使用or连接进行删除DynamicSQLMapper.xml <delete id="deleteMoreByArray"> delete from t_emp where <foreach collection="eids" item="eid" separator="or"> eid = #{eid} </foreach> </delete>场景：通过集合实现批量添加DynamicSQLMapper接口 /* 通过List集合实现批量添加的功能 */ public abstract int insertMoreByList(@Param("emps") List<Emp> emps);DynamicSQLMapper.xml <!--int insertMoreByList(@Param("emps") List<Emp> emps);--> <insert id="insertMoreByList"> insert into t_emp values <foreach collection="emps" item="emp" separator=","> (null,#{emp.empName},#{emp.age},#{emp.sex},#{emp.email},null) </foreach> </insert>SQL标签将我们常用的sql片段进行记录，在我们需要使用的地方，直接 [label color="red"]include[/label] 标签引用该片段。DynamicSQLMapper.xml <sql id="empColumns">eid,emp_name,age,sex,email</sql> <!--List<Emp> getEmpByCondition(Emp emp);--> select <include refid="empColumns"></include> from t_emp <trim prefix="where" suffixOverrides="and|or"> <if test="empName != null and empName != ''"> emp_name = #{empName} and </if> <if test="age != null and age != ''"> age = #{age} or </if> <if test="sex != null and sex != ''"> sex = #{sex} and </if> <if test="email != null and email != ''"> email = #{email} </if> </trim> 测试类所有源码public class DynamicSQLMapperTest { /* 动态SQL： 1.if:根据标签中的test属性所对应的表达式决定标签中的内容是否需要拼接到SQL中 2.where:当where标签中有内容时，会自动生成where关键字，并且将内容前多余的and或or去掉 当while标签中没有内容时，此时where标签没有任何效果 注意：where标签不能将其中内容后面多余的and或or去掉 3.trim: prefix|suffix:将trim标签中内容前面或后面添加指定内容 prefixOverrides|suffixOverrides:将trim标签中内容前面或后面去掉指定内容 4.choose,when,otherwise,相当于java中的if...else if...else when至少要有一个，otherwise最多只能有一个 5.foreach标签 collection：设置需要循环的数组或集合 item：表示数组或集合中的每一个数据 separator：循环体之间的分隔符 open：表示foreach标签所循环的所有内容的开始符号 close：表示foreach标签所循环的所有内容的结束符号 6.sql标签 将我们常用的sql片段进行记录 设置SQL片段：<sql id="empColumns">eid,emp_name,age,sex,email</sql> 引用SQL片段：<include refid="empColumns"></include> */ @Test public void testInsertMoreByList(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(); DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class); Emp emp1 = new Emp(null,"a1",23,"男","123@qq.com",null); Emp emp2 = new Emp(null,"a2",23,"男","123@qq.com",null); Emp emp3 = new Emp(null,"a3",23,"男","123@qq.com",null); List<Emp> emps = Arrays.asList(emp1, emp2, emp3); System.out.println(mapper.insertMoreByList(emps)); } @Test public void testDeleteMoreByArray(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(); DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class); int result = mapper.deleteMoreByArray(new Integer[]{6, 7, 8, 9, 10, 11}); System.out.println("result = " + result); } @Test public void testGetEmptyByChoose(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(); DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class); List<Emp> list = mapper.getEmpByChoose(new Emp(null, "张三", 23, "男", "123@qq.com", null)); System.out.println("list = " + list); } @Test public void testGetEmpByCondition(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(); DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class); List<Emp> emp = mapper.getEmpByCondition(new Emp(null, "张三", 23, "", "", null)); System.out.println("emp = " + emp); } }MyBatis的缓存一级缓存缓存就是把我们查询出来的数据进行记录，下次查询相同数据就从缓存中去取，就不会从数据库中访问。MyBatis会把我们查询的数据进行缓存，再次查询，如果缓存中有，就会从缓存中去取。MyBatis有一级缓存和二级缓存，一级缓存默认开启。一级缓存和二级缓存级别(范围)不一样，一级缓存范围是SqlSession级别的，同一个SqlSession查询出来的数据会被缓存，如果说再通过SqlSession查询相同数据就会从缓存中取。缓存只对查询功能有效范围、作用同一个SqlSession中，所查询的数据是会被缓存的，再一次获取缓存中的数据，会从缓存中调用。四种使一级缓存失效的情况不同的SqlSession对应不同的一级缓存，我们没有办法从同一个一级缓存中获取数据同一个SqlSession，但是查询条件不同，缓存应该是查询过的数据，新数据不会从缓存中取，而是从数据库中访问同一个SqlSession，两次查询操作期间执行了任意一次增删改操作，缓存会清空。增删改会改变数据库中的数据。缓存方便查询，提高查询效率，但是不能影响查询的结果，如果还从缓存中获取，那么结果将不准确。缓存只是为了提高速度，而不是影响真实性。同一个SqlSession，两次查询期间手动清空缓存 sqlSession1.clearCache();//使用此方法手动清空sqlsession的一级缓存 [label color="blue"]clearCache()[/label] 只对一级缓存有效二级缓存二级缓存的范围要比一级缓存的范围要大一点，二级缓存是是SqlSessionFactory级别，而且二级缓存需要手动开启。我们通过工厂创建的所有的SqlSession都属于二级缓存范围。[font size="16" color="7f00ff"]开启条件：[/font]在核心配置文件中，设置全局配置属性cacheEnabled="true"，默认为true，不需要设置在对应映射文件中设置标签二级缓存必须在SqlSession关闭或提交之后有效，什么叫关闭或提交？ sqlSession1.commit();//设置自动提交 sqlSession1.close();//关闭sqlSession在我们没有关闭或提交SqlSession，查询的数据会被保存到一级缓存中，当我们关闭或者提交SqlSession，数据才会保存到二级缓存中查询的数据所转换的实体类类型必须实现序列化的接口 public class Emp implements Serializable {}四个条件缺一不可。一种使二级缓存失效的情况两次查询之间执行了任意的增删改(实际上这个行为一级二级均会被清空)MyBatis缓存查询的顺序[note type="primary flat"]先查询二级缓存，二级缓存中可能会有其他程序已经查询出来的结果，可以直接使用如果二级缓存没有查到，查询一级缓存一级缓存没有命中，查询数据库SqlSession关闭之前，查询出来的数据默认保存在一级缓存中，SqlSession关闭之后，一级缓存中的数据会写入二级缓存。[/note]整合第三方缓存EHCache(会配就行)MyBatis从内存中读取要比磁盘文件中读取要快的很多，但是MyBatis毕竟作为持久层框架，在缓存功能的时候不是很专业，MyBatis提供了一些关于缓存的接口，可以由其他的技术作为MyBatis的二级缓存。只能使用其他第三方技术代替MyBatis的二级缓存，而一级缓存无法替代。pom文件中导入依赖 <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.2.1</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency>创建EHCache的配置文件ehcache.xml（必须这样命名）<?xml version="1.0" encoding="utf-8" ?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd"> <diskStore path="E:\atguigu\ehcache"/> <defaultCache maxElementsInMemory="1000" maxElementsOnDisk="10000000" eternal="false" overflowToDisk="true" timeToIdleSeconds="120" timeToLiveSeconds="120" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </defaultCache> </ehcache>设置二级缓存的类型CacheMapper.xml <cache type="org.mybatis.caches.ehcache.EhcacheCache" />加入logback日志logback.xml<?xml version="1.0" encoding="UTF-8"?> <configuration debug="true"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n</pattern> </encoder> </appender> <root level="DEBUG"> <appender-ref ref="STDOUT" /> </root> <logger name="com.atguigu.crowd.mapper" level="DEBUG"/> </configuration>MyBatis的逆向工程(☆)(☆)本质就是一个代码生成器正向工程：根据我们创建的java实体类，对应生成数据库表，Hibernate支持，而且帮助我们生成SQL语句。逆向工程：先创建数据库表，由当前的框架负责根据表生成实体类，mapper接口，映射文件。清新简洁版pom文件中添加依赖和插件<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.atguigu.mybatis</groupId> <artifactId>MyBatis</artifactId> <version>1.0-SNAPSHOT</version> </parent> <groupId>com.atguigu.mybatis</groupId> <artifactId>MyBatis_MBG</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.2.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.0</version> <dependencies> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.7</version> </dependency> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> </dependencies> </plugin> </plugins> </build> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> </project>创建核心配置文件包括导入jdbc.properties|log4j.xml|mybatis-config.xml创建逆向工程的配置文件 [label color="red"]文件名必须是generatorConfig.xml[/label]<?xml version="1.0" encoding="UTF-8"?> <generatorConfiguration> <context id="DB2Tables" targetRuntime="MyBatis3Simple"> <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/mybatis" userId="root" password="abc123"> <property name="nullCatalogMeansCurrent" value="true" /> </jdbcConnection> <javaModelGenerator targetPackage="com.atguigu.mybatis.pojo" targetProject=".\src\main\java"> <property name="enableSubPackages" value="true" /> <property name="trimStrings" value="true" /> </javaModelGenerator> <sqlMapGenerator targetPackage="com.atguigu.mybatis.mapper" targetProject=".\src\main\resources"> <property name="enableSubPackages" value="true" /> </sqlMapGenerator> <javaClientGenerator type="XMLMAPPER" targetPackage="com.atguigu.mybatis.mapper" targetProject=".\src\main\java"> <property name="enableSubPackages" value="true" /> </javaClientGenerator> </context> </generatorConfiguration>就可以自动生成我们需要的东西了，那么当然这种属于 [label color="blue"]清新简洁版[/label] ，只有增删改查五个方法 [label color="red"]配置文件中对于8+版本的MySQL需要多添加一行信息，防止重复生成所有表问题。[/label] <property name="nullCatalogMeansCurrent" value="true" />奢华尊享版(☆)调整修改targetRuntime <context id="DB2Tables" targetRuntime="MyBatis3"> 为MyBatis3就可以对于生成尊享版接口的一点点注解EmpMapper接口public interface EmpMapper { /*只要看到了方法名带有Example根据条件操作， 可以以任意字段，任意字段的所有需求作为条件 */ //根据条件来获取总记录数 int countByExample(EmpExample example); //根据条件进行删除 int deleteByExample(EmpExample example); //根据主键进行删除 int deleteByPrimaryKey(Integer eid); /*普通添加：如果当前传输实体类对象中某个属性的值是null， 那么我们的普通添加直接会把null作为值赋值给字段，*/ int insert(Emp record); /*选择性添加：只会将不是null的属性值为字段赋值，如果有空字段属性值， 那么就不会出现*/ int insertSelective(Emp record); //根据条件查询 List<Emp> selectByExample(EmpExample example); //根据主键查询 Emp selectByPrimaryKey(Integer eid); //根据条件选择性修改 这里的选择性：如果属性值为null，那么就不会去修改所对应的字段 int updateByExampleSelective(@Param("record") Emp record, @Param("example") EmpExample example); //根据条件修改 int updateByExample(@Param("record") Emp record, @Param("example") EmpExample example); //根据主键选择性修改 int updateByPrimaryKeySelective(Emp record); //根据主键修改 int updateByPrimaryKey(Emp record); } 不得不说，这样用起来真的巨方便，巨爽，我使用过程中目前出现了一个问题public class MBGTest { @Test public void testMBG(){ try { InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is); SqlSession sqlSession = sqlSessionFactory.openSession(true); EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); //查询所有数据 List<Emp> list = mapper.selectByExample(null); list.forEach(emp -> System.out.println(emp)); } catch (IOException e) { e.printStackTrace(); } } } 测试类查询所有数据的时候[note type="warning flat"]org.apache.ibatis.exceptions.PersistenceException:\### Error querying database. Cause: java.sql.SQLException: Error setting driver on UnpooledDataSource. Cause: java.lang.ClassNotFoundException: Cannot find class: com.mysql.cj.jdbc.Driver[/note]我使用8+驱动任何地方都找遍了，我设置了 <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin"/>的配置依然还是会报错，到目前还是没有找到解决办法。后面我在这篇CSDN中找到答案了非常感谢老哥解决出错，目前我没有找到太好的解决方法，手动导入了一个jar包(唉，都是会用maven的人了怎么又导包)后面查看Maven我大概知道了，主要是因为我把导入的依赖全部放在 [label color="red"]标签[/label] 下了，没有真正加载出来，所以说报错了。我调整了导入依赖的文件位置就发现好了测试类最终源码public class MBGTest { @Test public void testMBG(){ try { InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is); SqlSession sqlSession = sqlSessionFactory.openSession(true); EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); //查询所有数据 /*List<Emp> list = mapper.selectByExample(null); list.forEach(emp -> System.out.println(emp));*/ /*根据条件查询 QBC风格，根据条件来查询 * 条件定义好的，只需要访问相对应的方法 * 就可以在相对应的SQL语句中生成对应条件 * */ //EmpExample example = new EmpExample(); //创建一个条件,可以使用链式编程添加条件 //example.createCriteria().andEmpNameEqualTo("张三").andAgeGreaterThanOrEqualTo(20); //通过or方法也可以使用or /*example.or().andDidIsNotNull(); List<Emp> list = mapper.selectByExample(example); list.forEach(emp -> System.out.println(emp));*/ //普通修改和选择性修改 //mapper.updateByPrimaryKey(new Emp(1,"admin",22,null,"456@qq.com",3)); //mapper.updateByPrimaryKeySelective(new Emp(1,"admin",22,null,"456@qq.com",3)); } catch (IOException e) { e.printStackTrace(); } } }分页插件分页功能可以在MyBatis设置一些相应的功能实现对应的分页，不需要通过书写SQL语句在pom文件中导入依赖 <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.2.0</version> </dependency>在核心配置文件中添加全局配置，配置插件mybatis-config.xml<plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin> </plugins> <?xml version="1.0" encoding="UTF-8" ?> <configuration> <properties resource="jdbc.properties"/> <typeAliases> <package name="com.atguigu.mybatis.pojo"/> </typeAliases> <plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin> </plugins> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments> <mappers> <package name="com.atguigu.mybatis.mapper"/> </mappers> </configuration>测试类测试我们在页面中设置相关的超链接，我们可以将PageInfo放在域对象中，然后在页面中进行访问。当我们有上一页/下一页我们才需要展示首页和尾页(使用关键字isFirstPage判断)。如果展示导航分页，将navigatepageNums数组进行循环，循环里面写超链接，展示页面，超链接上显示的哪一页就跳转到对应页面。在查询功能之前开启分页，在查询功能之后获取分页相关信息就行。[label color="blue"]常用数据：[/label]API功能pageNum当前页的页码pageSize每页显示的条数size当前页显示的真实条数total总记录数pages总页数prePage上一页的页码nextPage下一页的页码isFirstPage/isLastPage是否为第一页/最后一页hasPreviousPage/hasNextPage是否存在上一页/下一页navigatePages导航分页的页码数navigatepageNums导航分页的页码，[1,2,3,4,5]我书写这篇文章的初衷就是总结学习的进度，遗忘之际可以拿出来翻看，如有不对的地方还望指正，多多海涵。
作为java最流行的持久层技术，即学习完JDBC之后的另一门技术，以轻量级和性能出色的半自动成为了核心三大技术SSM中的之一。本篇文章是基于Mybatis的基础学习，包括框架的搭建，优化，还有工具类的封装和模板的添加。搭建MyBatis在pom依赖文件中添加打包方式为jar包 <packaging>jar</packaging>在依赖文件中导入依赖因为我的SQL版本是8+版本，所以导入对应版本的依赖,并添加log4j的日志文件(下面的文章中有提及，引入依赖还需要导入配置文件xml在resources下) <dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> </dependencies>核心配置文件创建核心配置文件对于一个框架，本质上来说就是 [label color="blue"]jar包 + 配置文件[/label] 文件没有默认的名字，但是建议是mybatis-config.xml,在整合SSM后，核心文件可以写可以不写，之后会交给Spring管理。 [label color="orange"]mybatis-config.xml[/label] ，核心配置文件应该创建在resources下<?xml version="1.0" encoding="UTF-8" ?> <configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mybatis"/> <property name="username" value="root"/> <property name="password" value="abc123"/> </dataSource> </environment> </environments> <mappers> <mapper resource="org/mybatis/example/BlogMapper.xml"/> </mappers> </configuration>创建mapper接口(实现添加功能)Mybatis中的mapper接口相当于之前的DAO，以前的DAO有接口和实现类，而mapper只需要一个接口就可以，所谓面向接口编程。当我们通过mybatis中的方式去mapper创建接口对象，调用接口中的方法，就可以对应某个SQL语句。在接口中只需要添加操作数据库的方法即可。我们可以创建mapper接口，同DAO的命名，参考数据库表和实体类命名。映射文件尽量和我们的mapper接口名保持一致，只不过是一个xml文件。一个DAO类对应一个数据库表，一个mapper对应一个数据库表，一个mapper的映射文件对应一个mapper。 [label color="red"]MyBatis面向接口编程的两个一致：通过Mapper接口对应映射文件[/label]映射文件的namespace要和mapper接口的全类名保持一致映射文件中SQL语句的id要和mapper接口中的方法名一致UserMapper.xml<?xml version="1.0" encoding="UTF-8" ?> <mapper namespace="com.atguigu.mybatis.mapper.UserMapper"> <insert id="insertUser"> insert into t_user values(null,'admin','123456',23,'男','12345@qq.com') </insert> </mapper>UserMapper接口public interface UserMapper { /** MyBatis面向接口编程的两个一致：通过Mapper接口对应映射文件 1.映射文件的namespace要和mapper接口的全类名保持一致 2.映射文件中SQL语句的id要和mapper接口中的方法名一致 一个表 --- 实体类 --- mapper接口 --- 映射文件 */ /* 添加用户信息 */ public abstract int insertUser(); }修改核心配置文件mybatis-config.xml与对于配置文件产生关系 <mapper resource="mappers/UserMapper.xml"/> 在测试类中测试使用SQL在测试类中流程 @Test public void testMyBatis() throws IOException { /*MyBatis为我们提供了一个数据库的操作对象SqlSessionFactoryBuilder*/ //通过字节/字符输入流加载核心配置文件 InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); //获取SqlSessionFactoryBuilder SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); //获取sqlSessionFactoryBuilder的工厂对象,使用工厂模式 SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is); //获取SqlSession,代表Java程序和数据库之间的会话 SqlSession sqlSession = sqlSessionFactory.openSession(); //获取mapper接口对象。而现在mapper只有接口，如何获取实现类对象？ /*当我们传递进入一个类的class的对象，方法将获取该类型的实例化对象 getMapper底层帮助我们创建了该接口的实现类，并且返回了接口实现类对象 使用的是代理模式，帮助我们返回一个接口的实现类对象 */ UserMapper mapper = sqlSession.getMapper(UserMapper.class); //测试功能 int result = mapper.insertUser(); System.out.println("result = " + result);//result = 1 受影响的行数是1 //根据配置文件中事务管理方式，需要手动设置提交事务 sqlSession.commit(); }框架优化每次实现增删改查的时候都需要手动提交事务显得比较麻烦，可以把事务的提交设置成自动的。JDBC中事务就是自动提交，如果需要手动管理，才会关闭。 SqlSession sqlSession = sqlSessionFactory.openSession(true);添加日志功能使用的是 [label color="blue"]log4j[/label] ，添加依赖pom.xml <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> 添加配置文件log4j的配置文件名固定，大多数日志配置文件名都是固定的log4j.xml<?xml version="1.0" encoding="UTF-8" ?> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"> <appender name="STDOUT" class="org.apache.log4j.ConsoleAppender"> <layout class="org.apache.log4j.PatternLayout"> </layout> </appender> <logger name="java.sql"> <level value="debug" /> </logger> <logger name="org.apache.ibatis"> <level value="info" /> </logger> <root> <level value="debug" /> <appender-ref ref="STDOUT" /> </root> </log4j:configuration>[note type="default flat"]日志的级别FATAL(致命)>ERROR(错误)>WARN(警告)>INFO(信息)>DEBUG(调试)从左到右级别越来越低，日志级别越高，输出信息越少；日志级别越低，输出信息越多；所选用的日志文件的级别输出信息≥本身级别信息。[/note]设置配置模板配置核心配置文件的模板通过设置IDEA配置模板的方式，可以增加我们开发的效率，不用每次核心配置文件去官方文档中或者是笔记里cv。spring-config.xml模板内容<?xml version="1.0" encoding="UTF-8" ?> <configuration> <properties resource="jdbc.properties"/> <typeAliases> <package name=""/> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments> <mappers> <package name=""/> </mappers> </configuration>创建映射文件模板mybatis-mapper.xml模板内容<?xml version="1.0" encoding="UTF-8" ?> <mapper namespace=""> </mapper>以上两种模板在添加完成之后，以后的操作中只需要添加文件名，并在模板中补充对应的自定义的部分，附一张文件的存放位置。修改和删除功能在上面框架搭建过程中以及实现了添加功能，可以翻看。实际上我们书写增删改最好都使用这样的流程在接口(UserMapper)中添加对应抽象方法。 /* 修改用户信息 返回值可以写int 也可以不用传递返回值 */ public abstract void updateUser(); /* 删除用户信息*/ public abstract void deleteUser(); 复制返回值+方法名，在xml文件中添加注释并书写sql语句UserMapper.xml <update id="updateUser"> update t_user set username = '张三' where id = 4 </update> <delete id="deleteUser"> delete from t_user where id = 5 </delete>书写对应的测试类/类，创建对象，调用对应方法。 @Test public void testCRUD() throws IOException { InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is); SqlSession sqlSession = sqlSessionFactory.openSession(true); UserMapper mapper = sqlSession.getMapper(UserMapper.class); //修改功能 //mapper.updateUser(); //删除功能 mapper.deleteUser(); }查询功能查询功能不像是增删改，返回值固定。查询必定有返回值，考虑查询结果是什么？是一条数据，还是多条数据的实体类对象集合？单行单列？...查询一条数据|多条数据一条数据对应实体类对象，多条数据对应实体类集合UserMapper接口 /* 根据id查询用户信息 */ public abstract User getUserById(); /*查询所有的用户信息*/ public abstract List<User> getAllUser(); UserMapper.xml select * from t_user where id = 3 <!--List<User> getAllUser();--> select * from t_user 测试类 @Test public void testCRUD() throws IOException { InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is); SqlSession sqlSession = sqlSessionFactory.openSession(true); UserMapper mapper = sqlSession.getMapper(UserMapper.class); //修改功能 //mapper.updateUser(); //删除功能 //mapper.deleteUser(); //查询一条语句 /* User user = mapper.getUserById(); System.out.println(user);*/ List<User> list = mapper.getAllUser(); //使用lambda遍历集合进行输出 list.forEach(user -> System.out.println(user)); }核心配置文件解读核心配置文件注释<?xml version="1.0" encoding="UTF-8" ?> <configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mybatis"/> <property name="username" value="root"/> <property name="password" value="abc123"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mappers/UserMapper.xml"/> </mappers> </configuration>核心配置文件properties我们可以通过properties标签引入 [label color="orange"]properties文件[/label] ，通过 [label color="red"]${}[/label] 访问properties文件中的内容。jdbc.propertiesjdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mybatis jdbc.username=root jdbc.password=abc123 对于核心配置文件稍作修改<?xml version="1.0" encoding="UTF-8" ?> <configuration> <properties resource="jdbc.properties"/> <environments default="development"> <environment id="development"> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mappers/UserMapper.xml"/> </mappers> </configuration>类型别名及标签顺序每次配置查询语句的时候，都需要写 这样的标签，resultType后要加上全类名，很麻烦。而mybatis中为我们提供了类型别名的功能。 <properties resource="jdbc.properties"/> <typeAliases> <!--<typeAlias type="com.atguigu.mybatis.pojo.User" alias="User"></typeAlias>--> <package name="com.atguigu.mybatis.pojo"/> </typeAliases>核心配置文件mappers映射文件mybatis中支持以包为单位引入映射文件 <mappers> <!--<mapper resource="mappers/UserMapper.xml"/>--> <package name="com.atguigu.mybatis.mapper"/> </mappers>封装SqlSession工具类我们发现，添加了模板之后，在测试类中创建SqlSession尤为关键，那么创建SqlSession过程比较麻烦，我们可以封装一个工具类SqlSessionUtils类public class SqlSessionUtils { /*工具类中的方法一般都是静态方法,私有化构造方法*/ private SqlSessionUtils() { } public static SqlSession getSqlSession() { //读取核心配置文件，获取当前配置文件对应的字节输入流,用try-catch处理 SqlSession sqlSession = null; try { InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is); //通过创建出来的sqlSession实现增删改可以自动提交 sqlSession = sqlSessionFactory.openSession(true); } catch (IOException e) { e.printStackTrace(); } return sqlSession; } }然后可以在测试类中使用，直接就可以得到sqlSession.获取参数值的两种方式(☆☆)观察到语句都是自己手动输入的值，以后mybatis会放到ssm中一块使用，需要去创建一个web工程，浏览器发送请求之后到服务器，服务器接收请求参数调用service处理业务逻辑，service调用dao(现在通过mapper接口代替dao)，service调用的mapper方法将当前浏览器获取的数据传输到映射文件中。映射文件该如何获取所传输的参数(获取的参数值是我们所调用的mapper接口中方法的参数，我们需要在映射文件中获取)，实现SQL语句的拼接？ [label color="blue"]MyBatis获取参数值的两种方式：${} 和 #{}[/label] 分别对应了JDBC中的两种方式${} 本质就是字符串拼接，有sql注入的问题，有一些sql必须使用。\#{} 本质就是占位符赋值。根据不同情况参数值的获取也不同。我将这五种情况对应的总结笔记放置在最上面，下面是对应的书写案例[note-ico type="flat" ico=""]MyBatis获取参数值的各种情况：mapper接口方法的参数为单个的字面量类型：可以通过${}和#{}以任意的字符串获取参数值，但是需要注意${}的单引号问题如果当前的mapper接口方法的参数为多个时：此时MyBatis会将这些参数放在一个map集合中，以两种方式进行存储a> 以arg0，arg1...为键，以参数为值b> 以param1，param2...为键，以参数为值因此只需要通过#{}和 ${}以键的方式访问值即可，需要注意${}的单引号问题若mapper接口方法的参数是一个map集合，可以手动将这些参数放在一个map中来存储因此只需要通过#{}和 ${}以键的方式访问值即可，需要注意${}的单引号问题mapper接口方法的参数是一个实体类类型的参数只需要通过#{}和 ${}以属性的方式访问属性值即可，但是需要注意${}的单引号问题使用@Param注解命名参数此时MyBatis会将这些参数放在一个map集合中，以两种方式进行存储a> 以@Param注解的值为键，以参数为值b> 以param1，param2...为键，以参数为值因此只需要通过#{}和 ${}以键的方式访问值即可，需要注意${}的单引号问题[/note-ico][font size="16" color="#ff0000"]实际上还有很多情况，包括数组集合，一共是七种情况，建议归结为两种[/font]实体类对象/map集合 的情况除实体类对象/map集合情况，手动添加 [label color="orange"]@Param注解[/label] 的情况获取单个字面量类型的参数ParameterMapper.xml select * from t_user where username = '${username}' @Test public void testGetUserByUsername() { SqlSession sqlSession = SqlSessionUtils.getSqlSession(); //通过接口获取到一个mapper实体类的对象 ParameterMapper mapper = sqlSession.getMapper(ParameterMapper.class); User user = mapper.getUserByUsername("admin"); System.out.println(user); } 获取多个字面量类型的参数 select * from t_user where username = '${param1}' and password = '${param2}' 获取Map类型的参数 <!--User checkLoginByMap(Map<String, Object> map);--> select * from t_user where username = #{username} and password = #{password} 获取实体类类型参数此处的username、password、age、sex、email均属于set方法后对应名称的小写。 <insert id="insertUser"> insert into t_user values (null,#{username},#{password},#{age},#{sex},#{email}) </insert> @Param命名参数注解自定义当前mybatis把参数存储在map集合的键ParameterMapper接口 /* 验证登录（使用@Param注解） 当我们在参数前添加@Param注解，Mybatis会帮我们把参数放在一个map集合中， 以@Param中的参数为键，以形参为值。 */ public abstract User checkLoginByParam(@Param("username") String username, @Param("password") String password); select * from t_user where username = #{username} and password = #{password} 我书写这篇文章的初衷就是总结学习的进度，遗忘之际可以拿出来翻看，如有不对的地方还望指正，多多海涵。列表项目