# SpringBoot学习 **Repository Path**: yuanmatansuo/spring-boot-learning ## Basic Information - **Project Name**: SpringBoot学习 - **Description**: springboot的学习仓库 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2022-03-01 - **Last Updated**: 2025-11-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 一、SpringBoot简介 ### 1.1 介绍 ![image-20220825091928504](README.assets/image-20220825091928504.png) **官网对SpringBoot的定义:** Spring Boot可以轻松创建基于Spring的独立、生产级应用程序,您可以“直接运行”(内嵌容器运行)。 我们对Spring平台(Spring以及相关衍生框架)和第三方库(Mybatis等)有自己的习惯,这样您就可以轻松开始了。大多数Spring引导应用程序需要最少的Spring配置。 **习惯优于配置** ![image-20220825092401543](README.assets/image-20220825092401543.png) 百度百科对SpringBoot的定义 ### 1.2 发展 2014年4月,Pivotal团队发布全新开源的轻量级框架的第一个SpringBoot版本。 翻为:派维托(重要,枢纽) ### 1.3 Pivotal团队(公司)介绍 Pivotal起源于Rob Mee于**1989年**创立的Pivotal Labs,该公司总部位于旧金山,专注于快速的互联网风格的软件开发,被称为敏捷编程,发现灵活的、快速迭代的、由测试驱动的软件开发方法,多年来,Pivotal Labs一直被誉为塑造硅谷最有影响力和最有价值公司的软件开发文化。 **2012年**,Pivotal Labs被EMC公司收购。Pivotal 构建的面向下一代企业级 IT 系统基础平台和服务的框架“Pivotal One”,是业界新的企业级 PaaS标准。 Pivotal 成立于 **2013 年 4 月**,由 EMC(易安信)、VMware 和 GE(通用电气) 投资成立,专注于下一代企业级云计算与大数据基础平台,以及下一代应用程序运行框架实现,在敏捷与快速应用程序开发、数据科学、云计算、开放源代码软件、大规模并行处理和实时数据系统领域颇有建树。 ### 1.4 Pivotal团队的产品 公司的开源产品有:Spring 以及 Spring 衍生产品、Web 服务器 Tomcat、缓存中间件 Redis、消息中间件 RabbitMQ、平台即服务的 Cloud Foundry、Greenplum 数据引擎、还有大名鼎鼎的 GemFire(12306 系统解决方案组件之一) ![img](README.assets/921b18af049a448fb9e210a83bd316fa.jpeg) ### 1.5 对SpringBoot的总结 1. 为基于Spring的开发提供更快的入门体验 2. 开箱即用,没有代码生成,也无需XML配置。同时也可以修改默认值来满足特定的需求 3. 提供了一些大型项目中常见的非功能性特性,如嵌入式服务器、安全、指标,健康检测、外部配置等 4. SpringBoot不是对Spring功能上的增强,而是提供了一种快速使用Spring的方式 2.6 Starter 仔细观察,不难发现spring-boot-starter-***的形式,其实这是springboot的官方命名规则。 非官方的命名方式就是***-spring-boot starter的形式(如下mybatis框架的依赖),当然这些都是约定俗成的,不是强制性的,你说我就想自定义一个spring-boot-starter-***形式的,你开心就好,完全可以,做好被骂的觉悟就行了。 ![SpringBoot Starter原理](README.assets/d2yup1in.jyj.jpg) ### 1.6开源项目整合 1. 开发工具包 https://gitee.com/sanri/sanri-tools-maven 2. mybatisplus整合 https://gitee.com/baomidou/mybatisplus-maven-plugin ## 二、SpringBoot工程搭建 因为现在大部分产品都是直接以`SpringBoot`为脚手架开发了,它的核心就是实现了`自动装配`,当然它的整个使用的核心依然是`spring`, 只不过通过它可以很高效率的编写`spring`应用,技术都是不断迭代的,在以前,Spring起项目的时候,繁杂的配置太多了。 其实`Springboot`是`Spring`家族中的一个全新的框架,它是用来简单应用程序的创建和开发过程,化繁为简,它可以和其它的比如`mybatis`都可以很好的整合。 **主要特点** - 可以不使用xml配置文件,采用注解的方式 - 能快速构建spring的web程序 - 可以使用内嵌的Tomcat、jetty等服务器去运行SpringBoot程序(以前spring项目都是要放到tomcat里去运行的) - 可以使用maven来配置依赖 - 内置丰富功能 ### 2.1 新建工程 ![image-20220825094915488](README.assets/image-20220825094915488.png) 找到pom.xml添加以下配置 ```xml org.springframework.boot spring-boot-starter-parent 2.7.3 ``` SpringBoot要求,项目要继承SpringBoot的起步依赖spring-boot-starter-parent ### 2.2 通过继承关系推导 ![image-20220825102019955](README.assets/image-20220825102019955.png) **starter-parent又继承于spring-boot-dependencies** 在spring-boot-dependencies下面我们可以看到很多默认配置 ![image-20220825102200063](README.assets/image-20220825102200063.png) ![image-20220825102230735](README.assets/image-20220825102230735.png) 等等等等.... ### 2.3 创建启动类 ![image-20220825102758001](README.assets/image-20220825102758001.png) ```java package com.xxgc.springboot; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @program: SpringBoot备课 * @description: 启动类 * @author: Mr.abel(ShiJiaYi) * @create: 2022-08-25 10:24 **/ //声明该类是一个SpringBoot引导类 @SpringBootApplication public class SpringBootBKApplication { public static void main(String[] args) { //run方法 表示运行SpringBoot的引导类 run参数就是SpringBoot引导类的字节码对象 SpringApplication.run(SpringBootBKApplication.class); } } ``` ### 2.4 SpringBoot的启动流程 ```text 1、创建定时器stopWatch并启动 2、获取并运行listeners[SpringApplicationRunListeners] 3、打印banner 4、创建上下文ApplicationContext(判断是否webEnvironment加载AnnotationConfigApplicationContext或AnnotationConfigEmbeddedWebApplicationContext) 5、预处理上下文(context,listeners,args-ResourceLoader加载类) 6、刷新上下文(refresh添加后置处理器) 7、再刷新上下文(sort后置处理器排序) 8、listeners发布finish(callFinishedListener) 9、定时器stopWatch停止计时 10、打印启动日志 ``` ```java public ConfigurableApplicationContext run(String... args) { //创建一个计时器 long startTime = System.nanoTime(); DefaultBootstrapContext bootstrapContext = this.createBootstrapContext(); ConfigurableApplicationContext context = null; this.configureHeadlessProperty(); //SpringBoot应用监听器 SpringApplicationRunListeners listeners = this.getRunListeners(args); //运行监听器 listeners.starting(bootstrapContext, this.mainApplicationClass); try { ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments); this.configureIgnoreBeanInfo(environment); //打印Banner Banner printedBanner = this.printBanner(environment); //创建应用上下文 根据是否是web项目,来创建不同的ApplicationContext容器 context = this.createApplicationContext(); context.setApplicationStartup(this.applicationStartup); this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner); //refresh添加后置处理器 this.refreshContext(context); //sort后置处理器排序 //查找当前context中是否注册有CommandLineRunner和ApplicationRunner,如果有则遍历执行它们。 this.afterRefresh(context, applicationArguments); //启动时间 Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime); //打印启动日志 if (this.logStartupInfo) { (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), timeTakenToStartup); } listeners.started(context, timeTakenToStartup); this.callRunners(context, applicationArguments); } catch (Throwable var12) { this.handleRunFailure(context, var12, listeners); throw new IllegalStateException(var12); } try { Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime); listeners.ready(context, timeTakenToReady); return context; } catch (Throwable var11) { this.handleRunFailure(context, var11, (SpringApplicationRunListeners)null); throw new IllegalStateException(var11); } } ``` ### 2.5基于Spring Intializr的项目创建 Intializr(美 [ɪ'nɪʃəlaɪzə]) 初始化 通过骨架对项目进行初始化 ![image-20220906101014411](README.assets/image-20220906101014411.png) 通过阿里云《知行动手实验室》进行导入 ![image-20220906101502729](README.assets/image-20220906101502729.png) 绑定依赖关系 ### 2.6 基于Lombok的多种用法 ```java @Data @AllArgsConstructor @NoArgsConstructor public class Users { private Integer id; private String name; private Integer age; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date createDate; } ``` ```java @Data @AllArgsConstructor @NoArgsConstructor @Accessors(chain = true) public class Users { private String name; private Integer age; } ``` @Accessors 访问器 chain=true 链 链路访问器 ```java Users users = new Users().setName("张三").setAge(12); ``` ~~@Accessors(fluent = true) 的写法可省去set和get(失效时间2022-09-18)~~ ```java @Data @AllArgsConstructor @NoArgsConstructor @Accessors(fluent = true) public class Users { private String name; private Integer age; } ``` ```java Users users = new Users().name("张三").age(12); ``` ### 2.7内置Tomcat的解释 提问:为什么不需要在访问的时候加上项目名? 答:因为使用的是内置tomcat,通常tomcat可以部署多个项目,而内置tomcat只针对于当前项目。 ![ad917f8ed8744cc9978f7cd7c0076939](README.assets/ad917f8ed8744cc9978f7cd7c0076939.jpg) ### 2.8 application.yml YAML 全称 YAML Ain't Markup Language,它是一种以数据为中心的标记语言,比 XML 和 JSON 更适合作为配置文件。YAML文件格式是Spring Boot支持的一种JSON超集文件格式。相较于传统的Properties配置文件,YAML文件以数据为核心,是一种更为直观且容易被电脑识别的数据序列化格式。application.yaml文件的工作原理和application.properties一样。 想要使用 YAML 作为属性配置文件(以 .yml 或 .yaml 结尾),需要将 SnakeYAML 库添加到 classpath 下,Spring Boot 中的 spring-boot-starter-web 或 spring-boot-starter 都对 SnakeYAML 库做了集成, 只要项目中引用了这两个 Starter 中的任何一个,Spring Boot 会自动添加 SnakeYAML 库到 classpath 下。 Spring Boot 提供了大量的自动配置,极大地简化了spring 应用的开发过程,当用户创建了一个 Spring Boot 项目后,即使不进行任何配置,该项目也能顺利的运行起来。当然,用户也可以根据自身的需要使用配置文件修改 Spring Boot 的默认设置。 SpringBoot 默认使用以下 2 种全局的配置文件,其文件名是固定的。 1. **application.properties** 2. **application.yml** ### 2.9 bootstrap.yml(properties) bootstrap.yml 和 application.yml 都可以用来配置参数。 如果冲突,以application为准 bootstrap.yml 用来程序引导时执行,应用于更加早期配置信息读取。可以理解成系统级别的一些参数配置,这些参数一般是不会变动的。一旦bootStrap.yml 被加载,则内容不会被覆盖。 application.yml 可以用来定义应用级别的, 应用程序特有配置信息,可以用来配置后续各个模块中需使用的公共参数等。 ![image-20220907105215947](README.assets/image-20220907105215947.png) ![image-20220907105305906](README.assets/image-20220907105305906.png) ![image-20220907105321234](README.assets/image-20220907105321234.png) ![image-20220907105341814](README.assets/image-20220907105341814.png) ![image-20220907105359476](README.assets/image-20220907105359476.png) ### 2.10 配置文件的位置 ![image-20220907110145244](README.assets/image-20220907110145244.png) File代表当前项目,classpath代表资源文件夹 ![image-20220907110312383](README.assets/image-20220907110312383.png) 数字越小优先级越高 下课休息 50继续讲 ![image-20220907110725380](README.assets/image-20220907110725380.png) ### 2.11 第三方配置文件 jdbc.yml ```yaml jdbc: driver: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8 user: root pwd: 123456 ``` JdbcEntity.java ```java package com.xxgc.springboot.entity; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @ConfigurationProperties(prefix = "jdbc") @Component public class JdbcEntity { private String driver; private String url; private String user; private String pwd; } ``` 报错是因为没有依赖 ![image-20220907113033037](README.assets/image-20220907113033037.png) ```xml org.springframework.boot spring-boot-configuration-processor true ``` ```java @SpringBootApplication //指定哪个类能获取到 @EnableConfigurationProperties(JdbcEntity.class) public class SpringBootBKApplication { public static void main(String[] args) { //run方法 表示运行SpringBoot的引导类 run参数就是SpringBoot引导类的字节码对象 SpringApplication.run(SpringBootBKApplication.class); } } ``` 开启自动获取配置文件信息的功能和@Component二选一 ### 2.12 多环境配置 profile 可以让 Spring 对不同的环境提供不同配置的功能,可以通过激活、指定参数等方式快速切换环境。 换句话说,就是我们需要在不同的场景下使用不同的配置,profile的出现就是要解决我们多环境下切换配置复杂的问题。 在实际开发环境中,我们存在开发环境的配置,部署环境的配置,测试环境的配置等等,里面的配置信息很多时,例如:端口、上下文路径、数据库配置等等,若每次切换环境时,我们都需要进行修改这些配置信息时,会比较麻烦,profile的出现就是为了解决这个问题。 语法规则:application-{profile}.properties(.yaml/.yml) 如果需要创建自定义的的properties文件时,可以用application-xxx.properties/yml的命名方式,这也是官方提供的,其中xxx可以根据自己的需求来定义。 测试案例:创建application-dev.yml和application-prod.yml两个配置文件,指定不同的端口 application-dev.yml ```yaml # 开发环境 jdbc: driverclass: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/java79?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC username: zhangsan password: 123456 ``` application-test.yml ```yaml # 测试环境 jdbc: driverclass: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/java79?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC username: test password: test ``` 如果需要在两种环境下进行切换,只需要在application.yml中加入如下内容即可 ```yaml spring: profiles: active: dev ``` 常用环境 application.properties:主配置文件 application-dev.properties:开发环境配置文件 application-test.properties:测试环境配置文件 application-prod.properties:生产环境配置文件 ## 三、SpringBoot的运用 ### 3.1 手动注入Bean ```java package com.xxgc.springboot.config; import com.xxgc.springboot.entity.Users; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class CustomConfig { @Bean public Users getUser(){//默认用方法名做为Bean的id return new Users("admin",15); } } ``` [@Configuration](https://github.com/Configuration)用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被[@Bean](https://github.com/Bean)注解的方法,这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器。 ·[@Component](https://github.com/Component)注解表明一个类会作为组件类,并告知Spring要为这个类创建bean。 ·[@Bean](https://github.com/Bean)注解告诉Spring这个方法将会返回一个对象,这个对象要注册为Spring应用上下文中的bean。通常方法体中包含了最终产生bean实例的逻辑。 ### 3.2 SpringBoot热部署 添加依赖 ```xml org.springframework.boot spring-boot-devtools true runtime ``` 添加热部署配置 ```yaml spring: #热部署配置 devtools: restart: enabled: true #设置开启热部署 additional-paths: src/main/java #重启目录 exclude: WEB-INF/** freemarker: cache: false #页面不加载缓存修改及时生效 ``` **idea 添加配置 File -> settings ->Compiler 将Bulid project automatically 勾选上。** ![在这里插入图片描述](README.assets/3a61cabb011b4d74bad2e84fe22092d2.png) **Ctrl + Shift + Alt + / 快捷键 弹出Maintenance窗口,选择Registry选项** ![在这里插入图片描述](README.assets/bb07cc5ef6d64ed39052dffd17ba4c26.png) **找到key为compiler.automake.allow.when.app.running 将其勾选** ![在这里插入图片描述](README.assets/ef616769c92f4c97b51d6641c1c896b8.png) 重启项目即可 ![image-20220913161209773](README.assets/image-20220913161209773.png) ### 3.1 子父工程(模块)介绍 子父工程的介绍,利用Maven的继承,依赖传递性来为我们省略一些重复的配置。这样可以做到“**一处声明,处处使用**”。 **常用于被继承的元素** ``` groupId :项目组 ID ,项目坐标的核心元素; version :项目版本,项目坐标的核心元素; description :项目的描述信息; properties :自定义的 Maven 属性; dependencies :项目的依赖配置; dependencyManagement :醒目的依赖管理配置;(锁定版本) repositories :项目的仓库配置; build :包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等; ``` ### 3.2 pom.xml文件的解释 ```xml 4.0.0 com.winner.trade trade-core 1.0.0-SNAPSHOT jar ... com.winner.trade trade-test 1.0.0-SNAPSHOT test false org.slf4j slf4j-api UTF-8 1.5 1.5 ... ``` 新的工程目录结构 ![image-20220831110518261](README.assets/image-20220831110518261.png) ### ## 四、SpringBoot 学习与整合 更新时间:**2022年8月31日** 本小结主要对市面上常见的Java框架,在SpringBoot脚手架上进行学习整合,使用与理解。 ### 4.1 整合Spring MVC接收请求 ```xml org.springframework.boot spring-boot-starter-web ``` 常见的接收Get请求 ```java @RequestMapping("/users") @Controller public class getUserMsg { @RequestMapping("/getInfo") @ResponseBody public String userInfo(){ return "整合SpringMVC成功"; } } ``` 常见的restful风格接收(http://localhost:8080/users/getInfo/123) ```java //一个Restful风格请求 @RequestMapping("/getInfo/{userId}") @ResponseBody public String userInfoById(@PathVariable Integer userId){ return "id为"+userId; } ``` ##### 4.1.1**SpringMVC介绍** 1. springmvc是spring框架的一个模块,springmvc和spring无需通过中间整合层进行整合。 2. springmvc是一个基于mvc的web框架。 3. springmvc 表现层:方便前后端数据的传输 4. Spring MVC 拥有控制器,作用跟Servlet类似,接收外部请求,解析参数传给服务层 5. MVC是指,C控制层,M模块层,V显示层这样的设计理念,而SSM框架里面SPRING MVC本身就是MVC框架,作用是帮助(某种意义上也可以 理解为约束) ![img](README.assets/a843373a9b774286b818f16c5510683f.png) 1、交给IOC容器进行管理 2、映射给HandlerMapping ##### **4.1.2 Springmvc架构原理解析** 1. 发起请求到前端控制器(DispatcherServlet) 2. 前端控制器请求HandlerMapping查找 Handler,可以根据xml配置、注解进行查找 3. 处理器映射器HandlerMapping向前端控制器返回Handler 4. 前端控制器调用处理器适配器去执行Handler 5. 处理器适配器去执行Handler 6. Handler执行完成给适配器返回ModelAndView 7. 处理器适配器向前端控制器返回ModelAndView,ModelAndView是springmvc框架的一个底层对象,包括 Model和view 8. 前端控制器请求视图解析器去进行视图解析,根据逻辑视图名解析成真正的视图(jsp) 9. 视图解析器向前端控制器返回View 10. 前端控制器进行视图渲染,视图渲染将模型数据(在ModelAndView对象中)填充到request域 11. 前端控制器向用户响应结果 ##### 4.1.3 参数校验 ##### 4.1.4 统一的返回格式 见mybatisplus ##### 4.1.5 全局异常处理 **推荐使用二** ###### 4.1.5.1自定义异常一 AppHttpCodeEnum类 ```java package com.zhuicat.enums; public enum AppHttpCodeEnum { // 成功 SUCCESS(200,"操作成功啦,亲~~~"), // 登录 NEED_LOGIN(401,"需要登录后操作啦,亲~~~"), NO_OPERATOR_AUTH(403,"无权限操作啦,亲~~~"), SYSTEM_ERROR(500,"出现错误啦,亲~~~"), USERNAME_EXIST(501,"用户名已存在啦,亲~~~"), PHONENUMBER_EXIST(502,"手机号已存在啦,亲~~~"), EMAIL_EXIST(503, "邮箱已存在"), REQUIRE_USERNAME(504, "必需填写用户名啦,亲~~~"), COMMENT_NOT_NULL(506, "评论内容不能为空,亲~~~"), FILE_TYPE_ERROR(507,"文件类型错误,请上传png文件"), USERNAME_NOT_NULL(508,"用户名不能为空,亲~"), NICKNAME_NOT_NULL(509,"昵称不能为空,亲~"), PASSWORD_NOT_NULL(510,"密码不能为空,亲~"), EMAIL_NOT_NULL(510,"邮箱不能为空,亲~"), NICKNAME_EXIST(511,"昵称已存在啦,亲~~~"), LOGIN_ERROR(505,"用户名或密码错误啦,亲~~~"); int code; String msg; AppHttpCodeEnum(int code, String errorMessage){ this.code = code; this.msg = errorMessage; } public int getCode() { return code; } public String getMsg() { return msg; } } ``` 自定义异常类:SystemException ```java package com.zhuicat.exception; import com.zhuicat.enums.AppHttpCodeEnum; /** * 自定义异常 */ public class SystemException extends RuntimeException{ private int code; private String msg; public int getCode() { return code; } public String getMsg() { return msg; } public SystemException(AppHttpCodeEnum httpCodeEnum) { super(httpCodeEnum.getMsg()); this.code = httpCodeEnum.getCode(); this.msg = httpCodeEnum.getMsg(); } } ``` 全局[异常处理](https://so.csdn.net/so/search?q=异常处理&spm=1001.2101.3001.7020):GlobalExceptionHandler ```java @ControllerAdvice @ResponseBody public class GlobalExceptionHandler { /** * 如果抛出的的是ServiceException,则调用该方法 * @param se 业务异常 * @return Result */ @ExceptionHandler(ServiceException.class) public Result handle(ServiceException se){ return Result.error(se.getCode(), se.getMessage()); } } ``` 如何使用 ```java @PostMapping("/login") public ResponseResult login(@RequestBody User user) { if (!StringUtils.hasText(user.getUserName())) { // 提示必须传入用户名 throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME); } return bloginService.login(user); } ``` ```java @Slf4j @ControllerAdvice @ResponseBody public class GlobalException { /** * 创建定时器异常 * @param e * @return */ @ExceptionHandler(value = {org.quartz.ObjectAlreadyExistsException.class}) public Result cwm(Exception e){ //1、需要在日志中保留异常信息方便后期大数据分析 log.error("创建定时器异常"+e.toString()); //2、需要在日志表中记录错误信息 //3、返回封装过的错误信息给前端 return Result.error("创建定时器异常"); } } ``` ###### 4.1.5.2自定义异常二 预期异常 运行时异常 导入Gson工具类,主要是对对象转json数据格式进行操作 ```xml com.google.code.gson gson 2.8.9 ``` 定义一个BaseException类 ```java @Data public class BaseException extends RuntimeException { private static final long serialVersionUID = 1L; private Gson gson = new Gson(); /** * 所属模块 */ private String module; /** * 错误码 */ private String code; /** * 错误码对应的传入参数 */ private Object args; /** * 错误消息 */ private String defaultMessage; /** * 接口路径 通过AOP注入 */ private String path; public BaseException(String module, String code, Object args, String defaultMessage) { this.module = module; this.code = code; //对象转json this.args = gson.toJson(args); this.defaultMessage = defaultMessage; } @Override public String toString() { return "BaseException{" + "module='" + module + '\'' + ", code='" + code + '\'' + ", args=" + args + ", defaultMessage='" + defaultMessage + '\'' + ", path='" + path + '\'' + '}'; } } ``` ![image-20220921230037923](README.assets/image-20220921230037923.png) ```java public class NotQuartzClassException extends BaseException { public NotQuartzClassException(Object... args) { super("SpringBoot", "XXGC-V2022-Task-02", args, "Quartz的Job类未找到"); } } ``` controller层 ```java @ApiOperation(value = "定时任务表新增", notes = "新增") @PostMapping public Result save(@RequestBody TaskInfo taskInfo) throws SchedulerException { try { taskInfoService.saveOrTask(taskInfo); }catch (ClassNotFoundException e){ throw new NotQuartzClassException(taskInfo); } return Result.ok(); } ``` ```java @ExceptionHandler(value = NotQuartzClassException.class) public Result classNotFoundException(NotQuartzClassException e){ log.error(e.toString()); return Result.error("任务创建失败",e.getCode()); } ``` ### 4.2 整合Shiro #### 4.2.1 简介 Apache Shiro是一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。 - Apache Shiro是Java的一个安全(权限)框架。 - Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。 - Shiro可以完成:认证、授权、加密、会话管理、与Web集成、缓存等。 Shiro有三大核心组件: - **Subject:** 即**当前用户**,在权限管理的应用程序里往往需要知道谁能够操作什么,谁拥有操作该程序的权利,shiro中则需要通过Subject来提供基础的当前用户信息,Subject 不仅仅代表某个用户,与当前应用交互的任何东西都是Subject,如网络爬虫等。所有的Subject都要绑定到SecurityManager上,与Subject的交互实际上是被转换为与SecurityManager的交互。 - **SecurityManager:** 即所有Subject的**管理者**,这是Shiro框架的核心组件,可以把他看做是一个Shiro框架的全局管理组件,用于调度各种Shiro框架的服务。作用类似于SpringMVC中的DispatcherServlet,用于拦截所有请求并进行处理。 - **Realm:** **数据交互**Realm是用户的信息认证器和用户的权限人证器,我们需要自己来实现Realm来自定义的管理我们自己系统内部的权限规则。SecurityManager要验证用户,需要从Realm中获取用户。可以把Realm看做是数据源。 ![20201229213010436](README.assets/20201229213010436.jpg) #### 4.2.2 Shiro架构 ![image-20210702173443118](README.assets/20210702173443.png) 认证、授权、session管理、加密 ![img](README.assets/1825095-20200325213442255-736368514.png) - **subject**:任何可以与应用交互的“用户”; - **SecurityManager:**相当于SpringMVC中的DispatcherServlet;是shiro的心脏;所有具体的交互都会通过SecurityManager进行控制;它管理这所有Subject、且负责进行认证、授权、会话以及缓存的管理 - **Authenticator:负责Subject认证,是一个扩展点,可以自定义实现;**可以使用认证策略(Authenticator Strategy),即用户认证通过的逻辑; - **Authorizer:授权器,即访问控制器,用来决定主体是否有权限进行相应的操作**;即控制着用户能访问应用中的那些功能; - **Realm:可以有1个或多个Realm,**可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是内存实现等等;有用户提供;所以一般再应用中都需要实现自己的Realm; - **SessionManager:管理Session生命周期的组件;**而shiro并不仅仅可以用在Web环境,也可以用在普通的javaSE环境 - **CacheManager:缓存控制器,来管理用户、角色‘权限等的缓存的;**因为这些数据基本上很少改变,放到缓存后可以提高访问的性能 - **Cryptography:密码模块,**shiro提供了一些常见的加密组件用于密码加密/解密。 #### 4.2.3 单态登录、单点登录 **定义** **单态登录**就是一个账号只能在一台机器上登录,如果在其他机器上登录了,则原来的登录自动失效。单态登录的目的是防止多台机器同时使用一个账号。 **单点登录**全称Single Sign On(简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分。 单点登录是面对多系统的复杂性而设计的一种登录方式。 例子:在同一个浏览器中,网易主页登录网易账号后,既可以打开博客,又可以打开对应的邮箱,还可以用这个账号评论新闻等等。可以通过博客园、百度、csdn、淘宝等网站的登录过程加深对单点登录的理解,注意观察登录过程中的跳转url与参数 #### 4.2.4 导包 ```xml org.apache.shiro shiro-spring 1.9.0 ``` #### 4.2.5 配置 ```java package com.xxgc.springboot.config; import com.xxgc.springboot.realm.UserRealm; import org.apache.commons.collections.map.LinkedMap; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Configuration; import java.util.LinkedHashMap; import java.util.Map; /** * @program: springboot * @description: shiro的配置 * @author: Mr.abel(ShiJiaYi) * realm:域对象,用来访问数据库得到用户账号、密码、角色、权限信息等。需要程序员自己实现 * securityManager:安全管理器,整合shiro的核心,赋值校验账号密码、权限等操作 * 过滤器:用来指定哪些请求,资源需要认证或者授权(需要哪些数据) **/ @Configuration public class ShiroConfiguration { //1、realm public UserRealm userRealm(){ return new UserRealm(); } //2、配置SecurityManager public SecurityManager securityManager(UserRealm userRealm){ DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); //设置realm manager.setRealm(userRealm); return manager; } //3、配置shiro的过滤器 public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); //配置安全管理器 bean.setSecurityManager(securityManager); //设置登录页面 bean.setLoginUrl("/index.html"); //没有权限的页面 bean.setUnauthorizedUrl("error.html"); //指定哪些url需要验证、哪些不需要认证 Map map = new LinkedHashMap<>(); //anon表示可以匿名访问 map.put("/index.html","anon"); map.put("/user/login","anon"); //其他需要权限验证 map.put("/**","authc"); //设置到bean上 bean.setFilterChainDefinitionMap(map); return bean; } } ``` 接口登录验证 ```java //验证账号密码 //1、获取到单签“用户” Subject currentUser = SecurityUtils.getSubject(); //2、判断是否没有认证 if(!currentUser.isAuthenticated()){ //创建令牌 UsernamePasswordToken token = new UsernamePasswordToken("123", "432"); //去进行登录认证 currentUser.login(token); } ``` ```java package com.xxgc.springboot.realm; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; /** * @program: springboot * @description: * @author: Mr.abel(ShiJiaYi) * 授权Realm **/ public class UserRealm extends AuthorizingRealm { //去获取到授权的信息 //获取当前用户角色用户、权限信息 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } //从数据库获取当前用户的账号、密码信息(认证) @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //getName 当前realm的名字 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo("123", "321", getName()); return info; } } ``` ![image-20220926182131444](README.assets/image-20220926182131444.png) ### 4.3 整合SpringSecurity #### 4.3.1 简介 学习权限框架必须搞懂的两个词语 **认证 授权** 1. 认证:用户名,密码错误,认证失败 2. 授权:当前用户有哪些权限 ### 4.4 整合Swagger2 #### 4.4.1 Swagger导包 ```xml io.springfox springfox-swagger2 2.9.2 io.springfox springfox-swagger-ui 2.9.2 com.github.xiaoymin knife4j-spring-boot-starter 2.0.4 ``` #### 4.4.2 配置swagger ```java /** * @program: web * @description: swagger配置 * @author: Mr.abel(ShiJiaYi) **/ @Configuration @EnableSwagger2 public class Swagger2Config{ @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) // .enable(swaggerEnable) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.xlkg.web.controller")) //指定扫描包 // .apis(RequestHandlerSelectors.withClassAnnotation(Api.class)) .paths(PathSelectors.any()) //只显示api路径下的页面 //.paths(Predicates.and(PathSelectors.regex("/api/.*"))) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("标题") .description("描述") .version("1.0.0") .build(); } } ``` #### 4.4.3 Swagger拦截器无法访问 ```java /** * @program: web * @description: 拦截器规则 * @author: Mr.abel(ShiJiaYi) **/ @Configuration public class MyMvcConfig extends WebMvcConfigurationSupport { /** * 解决swagger-ui.html 404无法访问的问题 */ public void addResourceHandlers(ResourceHandlerRegistry registry) { // 解决静态资源无法访问 registry.addResourceHandler("/**") .addResourceLocations("classpath:/static/"); // 解决swagger无法访问 registry.addResourceHandler("/swagger-ui.html") .addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/doc.html") .addResourceLocations("classpath:/META-INF/resources/"); // 解决swagger的js文件无法访问 registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/"); } } ``` #### 4.4.4 常用注解 ```java @Api(tags = "类上的描述") ``` ```java @ApiOperation(value = "方法的描述", notes = "接口的描述") ``` ```java @ApiModel(value = "模型的描述", description = "") ``` ```java @ApiModelProperty("字段的描述") ``` ### 4.5 整合Mybatis-plus #### 4.5.1 简介 MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。 ![img](README.assets/relationship-with-mybatis.png) - **无侵入**:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑 - **损耗小**:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作 - **强大的 CRUD 操作**:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求 - **支持 Lambda 形式调用**:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错 - **支持主键自动生成**:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题 - **支持 ActiveRecord 模式**:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作 - **支持自定义全局通用操作**:支持全局通用方法注入( Write once, use anywhere ) - **内置代码生成器**:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用 - **内置分页插件**:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询 - **分页插件支持多种数据库**:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库 - **内置性能分析插件**:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询 - **内置全局拦截插件**:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作 #### 4.5.2 安装 ```xml com.baomidou mybatis-plus-boot-starter 最新版本 ``` #### 4.5.3 添加配置 ```yaml # DataSource Config spring: datasource: driver-class-name: org.h2.Driver schema: classpath:db/schema-h2.sql data: classpath:db/data-h2.sql url: jdbc:h2:mem:test username: root password: test ``` #### 4.5.4 开启Mapper扫描 ```java @SpringBootApplication @MapperScan("com.baomidou.mybatisplus.samples.quickstart.mapper") public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` #### 4.5.5 代码生成/逆向工程 添加依赖 ```xml mysql mysql-connector-java runtime com.baomidou mybatis-plus-boot-starter 3.5.1 com.baomidou mybatis-plus-generator 3.5.1 org.apache.velocity velocity 1.7 ``` 添加生成模板 ```java package com.xxgc.springboot.generator; import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.OutputFile; import java.util.Collections; public class CodeGenerator { public static void main(String[] args) { String url = "jdbc:mysql://localhost:3306/spring_boot_study?serverTimezone=GMT%2b8"; String username = "root"; String password = "123456"; FastAutoGenerator.create(url, username, password) .globalConfig(builder -> { builder.author("Mr.Abel") // 设置作者 .fileOverride() // 覆盖已生成文件 .disableOpenDir() //禁止打开输出目录 .outputDir("D:\\WorkSpace\\java-study\\SpringBoot备课\\SpringMVC\\src\\main\\java\\"); // 指定输出目录 }) .packageConfig(builder -> { builder.parent("com.xxgc.springboot") // 设置父包名 .moduleName(null) // 设置父包模块名 .pathInfo(Collections.singletonMap(OutputFile.mapperXml, "D:\\WorkSpace\\java-study\\SpringBoot备课\\SpringMVC\\src\\main\\resources\\mapper\\")); // 设置mapperXml生成路径 }) .strategyConfig(builder -> { builder.entityBuilder().enableLombok(); //启动lombok builder.mapperBuilder().enableMapperAnnotation().build(); //启用@mapper注释 builder.controllerBuilder().enableHyphenStyle().enableRestStyle(); //启用驼峰转连字符样式 builder.addInclude("user_info"); // 设置需要生成的表名 }) .execute(); } } ``` 添加接口模板(请使用新版) 新版 对于插入时间的控制,可以在mysql数据库用 CURRENT_TIMESTAMP 或 NOW()都可以 ![image-20220913113710090](README.assets/image-20220913113710090.png) ```java package ${package.Controller}; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import ${package.Service}.${table.serviceName}; import ${package.Entity}.${entity}; #if(${restControllerStyle}) import org.springframework.web.bind.annotation.RestController; #else import org.springframework.stereotype.Controller; #end #if(${superControllerClassPackage}) import ${superControllerClassPackage}; #end /** *

* $!{table.comment} 前端控制器 *

* * @author ${author} * @since ${date} */ @RestController @Api(tags = "$!{table.comment}相关接口") @RequestMapping("#if(${package.ModuleName})/${package.ModuleName}#end/#if(${controllerMappingHyphenStyle})${controllerMappingHyphen}#else${table.entityPath}#end") public class ${table.controllerName} { @Resource private ${table.serviceName} ${table.entityPath}Service; /** * 查询所有接口 * @return */ @ApiOperation(value = "$!{table.comment}查询所有接口", notes = "查询所有接口") @GetMapping public Result> findAll() { return Result.ok(${table.entityPath}Service.list()); } /** * 根据id查询数据接口 * @param id * @return */ @ApiOperation(value = "$!{table.comment}查更具id查询", notes = "根据id查询数据接口") @GetMapping("/{id}") public Result<${entity}> findOne(@PathVariable Integer id) { return Result.ok(${table.entityPath}Service.getById(id)); } /** * 分页查询接口 * @param pageNum * @param pageSize * @return */ @ApiOperation(value = "$!{table.comment}分页查询接口", notes = "分页查询接口") @GetMapping("/page") public Result> findPage(@RequestParam Integer pageNum,@RequestParam Integer pageSize) { QueryWrapper<${entity}> queryWrapper = new QueryWrapper<>(); return Result.ok(${table.entityPath}Service.page(new Page<>(pageNum, pageSize), queryWrapper)); } /** * 新增和更新接口 * @param * @return */ @ApiOperation(value = "$!{table.comment}新增和更新接口", notes = "新增和更新接口") @PostMapping public Result save(@RequestBody ${entity} ${table.entityPath}) { ${table.entityPath}Service.saveOrUpdate(${table.entityPath}); return Result.ok(); } /** * 删除接口 * @param id * @return */ @ApiOperation(value = "$!{table.comment}删除接口", notes = "删除接口") @DeleteMapping("/{id}") public Result delete(@PathVariable Integer id) { ${table.entityPath}Service.removeById(id); return Result.ok(); } /** * 批量删除接口 * @param ids * @return */ @ApiOperation(value = "$!{table.comment}批量删除接口", notes = "批量删除接口") @PostMapping("/del/batch") public Result deleteBatch(@RequestBody List ids) { ${table.entityPath}Service.removeByIds(ids); return Result.ok(); } } ``` 旧版 ```java package ${package.Controller}; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import ${package.Service}.${table.serviceName}; import ${package.Entity}.${entity}; #if(${restControllerStyle}) import org.springframework.web.bind.annotation.RestController; #else import org.springframework.stereotype.Controller; #end #if(${superControllerClassPackage}) import ${superControllerClassPackage}; #end /** *

* $!{table.comment} 前端控制器 *

* * @author ${author} * @since ${date} */ @RestController @RequestMapping("#if(${package.ModuleName})/${package.ModuleName}#end/#if(${controllerMappingHyphenStyle})${controllerMappingHyphen}#else${table.entityPath}#end") public class ${table.controllerName} { @Resource private ${table.serviceName} ${table.entityPath}Service; /** * 查询所有接口 * @return */ @GetMapping public JsonData findAll() { return JsonData.buildSuccess(${table.entityPath}Service.list()); } /** * 根据id查询数据接口 * @param id * @return */ @GetMapping("/{id}") public JsonData findOne(@PathVariable Integer id) { return JsonData.buildSuccess(${table.entityPath}Service.getById(id)); } /** * 分页查询接口 * @param pageNum * @param pageSize * @return */ @GetMapping("/page") public JsonData findPage(@RequestParam Integer pageNum,@RequestParam Integer pageSize) { QueryWrapper<${entity}> queryWrapper = new QueryWrapper<>(); return JsonData.buildSuccess(${table.entityPath}Service.page(new Page<>(pageNum, pageSize), queryWrapper)); } /** * 新增和更新接口 * @param user * @return */ @PostMapping public JsonData save(@RequestBody ${entity} ${table.entityPath}) { ${table.entityPath}Service.saveOrUpdate(${table.entityPath}); return JsonData.buildSuccess(); } /** * 删除接口 * @param id * @return */ @DeleteMapping("/{id}") public JsonData delete(@PathVariable Integer id) { ${table.entityPath}Service.removeById(id); return JsonData.buildSuccess(); } /** * 批量删除接口 * @param ids * @return */ @PostMapping("/del/batch") public JsonData deleteBatch(@RequestBody List ids) { ${table.entityPath}Service.removeByIds(ids); return JsonData.buildSuccess(); } } ``` ![image-20220908080512008](README.assets/image-20220908080512008.png) 添加统一返回工具类(使用新版) **新版** 2022年9月更新 ```java package com.xxgc.springboot.util; import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.annotations.Api; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.io.Serializable; /** * @program: springboot * @description: 统一返回格式 * @author: Mr.abel(ShiJiaYi) **/ @Data @ApiModel public class Result implements Serializable { private static final String SUCCESSMSG = "操作成功!"; @ApiModelProperty(value = "请求状态码") private int code; @ApiModelProperty(value = "响应信息") private String msg; //属性为 空("") 或者为 NULL 都不序列化 @JsonInclude(JsonInclude.Include.NON_EMPTY) @ApiModelProperty(value = "数据") private T data; public Result(int code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; } public static Result ok(){ return new Result<>(200,SUCCESSMSG,null); } public static Result ok(T data){ return new Result<>(200,SUCCESSMSG,data); } public static Result error(String error) { return new Result<>(500, error, null); } /**--------------当公司还有其他业务状态返回可以在以下添加---------------*/ } ``` 旧版 ```java package com.xxgc.springboot.util; public class JsonData { /** * 状态码 */ private Integer code; /** * 数据 */ private Object data; /** * 状态信息 */ private String msg; public JsonData(){} public JsonData(Integer code,Object data,String msg){ this.code = code; this.data = data; this.msg = msg; } /** * 成功 */ public static JsonData buildSuccess(){ return new JsonData(0,null,null); } /** * 成功 * @param data 数据 */ public static JsonData buildSuccess(Object data){ return new JsonData(0,data,null); } /** * 成功 * @param data 数据 * @param msg 状态信息 */ public static JsonData buildSuccess(Object data,String msg){ return new JsonData(0,data,msg); } /** * 失败 * @param msg 数据 */ public static JsonData buildError(String msg){ return new JsonData(-1,null,msg); } /** * 失败 * @param code 状态码 * @param msg 数据 */ public static JsonData buildError(Integer code,String msg){ return new JsonData(code,null,msg); } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } } ``` #### 4.5.6 使用ApiPost(PostMan) 使用ApiPost或PostMan完成对用户的注册 #### 4.5.7 生成代码时对Swagger的支持 ```java .globalConfig(builder -> { builder.author("Mr.abel(ShiJiaYi)") // 设置作者 .enableSwagger() // 开启 swagger 模式 .fileOverride() // 覆盖已生成文件 .commentDate("yyyy-MM-dd") .outputDir("C://Users//十一//Desktop//喜乐快够官网//src//main//java"); // 指定输出目录 }) ``` #### 4.5.8 Mybatis-plus基本使用 ##### 4.5.8.1逻辑删除 ```yaml mybatis-plus: global-config: db-config: logic-delete-field: valid # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) ``` 逻辑删除在于保留数据,以方便后续可能会出现的相关性错误。 flag 已删除 true 1 false 0 delete-value应该为1 valid 有效列 true 1 false 0 delete-value应该为0 ##### 4.5.8.2 通用枚举类型 在数据库创建年级字段grade 创建枚举类 ```java package com.xxgc.springboot.entity.enums; import com.baomidou.mybatisplus.annotation.EnumValue; import lombok.Getter; /** * @program: springboot * @description: 枚举 * @author: Mr.abel(ShiJiaYi) **/ @Getter public enum GradeEnum { PRIMARY(1, "小学"), SECONDORY(2, "中学"), HIGH(3, "高中"); GradeEnum(int grade, String descp) { this.grade = grade; this.descp = descp; } @EnumValue//标记数据库存的值是code private final int grade; private final String descp; } ``` ```java @ApiModelProperty("年级") private GradeEnum grade; ``` 在实体类使用枚举 ```yaml # 支持统配符 * 或者 ; 分割 typeEnumsPackage: com.xxgc.springboot.entity.enums ``` 配置枚举扫描 ##### 4.5.8.3 自动填充处理器 添加更新字段 update_time ```java @Slf4j @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用) } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐) } } ``` 实体类添加 ```java @TableField(fill = FieldFill.INSERT_UPDATE) @ApiModelProperty("最后一次更新") private LocalDateTime updateTime; ``` ##### 4.5.8.4 数据安全保护 ```yml // 加密配置 mpw: 开头紧接加密内容( 非数据库配置专用 YML 中其它配置也是可以使用的 ) spring: datasource: url: mpw:qRhvCwF4GOqjessEB3G+a5okP+uXXr96wcucn2Pev6Bf1oEMZ1gVpPPhdDmjQqoM password: mpw:Hzy5iliJbwDHhjLs1L0j6w== username: mpw:Xb+EgsyuYRXw7U7sBJjBpA== ``` ```java // 生成 16 位随机 AES 密钥 String randomKey = AES.generateRandomKey(); // 随机密钥加密 String result = AES.encrypt(data, randomKey); ``` 使用 ```java // Jar 启动参数( idea 设置 Program arguments , 服务器可以设置为启动环境变量 ) --mpw.key=d1104d7c3b616f0b ``` ##### 4.5.8.5 自定义主键策略 ```sql CREATE TABLE `orders` ( `id` varchar(200) NOT NULL COMMENT 'id', `name` varchar(200) NOT NULL COMMENT '订单名', `quantity` int(3) NOT NULL DEFAULT '1' COMMENT '订单数量', `flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '有效字段', `create_time` datetime NOT NULL COMMENT '创建时间', `update_time` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单' ``` 为什么使用主键策略? 答:如订单数据,在数据传输中如果使用自增长会泄露真实订单数量。如果是用户表,那会泄露真实的用户数量 解决方案: | 值 | 描述 | | :---------: | :----------------------------------------------------------: | | AUTO | 数据库 ID自增,这种情况下将表中主键设置为自增,否则,没有设置主动设置id值进行插入时会报错 | | NONE | 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里默认 ASSIGN_ID),注意这里官网文档有误 | | INPUT | insert 前自行 set 主键值,在采用IKeyGenerator类型的ID生成器时必须为INPUT | | ASSIGN_ID | 分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法) | | ASSIGN_UUID | 分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认 default 方法) | ###### 4.5.8.5.1 AUTO`自动增长策略 这个配合数据库使用,`Mysql`可以,但是`Oracle`不行。不配合会报错,这里就不细展开了,有兴趣的同学可以去试一试。 在主键字段上加上 `@TableId(type = IdType.AUTO)` ```java //指定主键使用数据库ID自增策略 @TableId(type = IdType.AUTO) private Integer id; ``` ###### 4.5.8.5.2 Input(自定义输入策略) 自定义输入策略:如果不想使用数据库的自增主键,也可以使用INPUT进行自己传递主键即可,进行插入工作,但在插入之前一定要检查数据库是否已经存在了该主键。 Mybatis-Plus 内置了如下数据库主键序列(如果内置支持不满足你的需求,可实现 IKeyGenerator 接口来进行扩展): ```tex DB2KeyGenerator H2KeyGenerator KingbaseKeyGenerator OracleKeyGenerator PostgreKeyGenerator ``` **使用方法如下所示:** 首先添加@Bean ```java @Bean public OracleKeyGenerator oracleKeyGenerator(){ return new OracleKeyGenerator(); } ``` 然后实体类配置主键 `Sequence`,指定主键策略为`IdType.INPUT`即可 ```java @Data @KeySequence(value = "SEQ_ACL_ROLE" , clazz = Integer.class) public class AclUser implements Serializable { private static final long serialVersionUID = 780903014942735924L; @TableId(value = "ID",type = IdType.INPUT) private Integer id; ``` ###### 4.5.8.5.3`ASSIGN_ID`(雪花算法) ![img](README.assets/5ba368daed80d653109e77940df2d255.webp) 如果不设置type值,默认则使用IdType.ASSIGN_ID策略(自 3.3.0 起)。该策略会使用雪花算法自动生成主键 ID,主键类型为 Long 或 String。 雪花算法(SnowFlake)是 Twitter 开源的分布式 id 生成算法。其核心思想就是:使用一个 64 bit 的 long 型的数字作为全局唯一 id。在分布式系统中的应用十分广泛,且 ID 引入了时间戳,基本上保持自增的。 使用方法 指定主键生成策略使用雪花算法(默认策略) ```java @ApiModelProperty("id") //指定主键生成策略使用雪花算法(默认策略) @TableId(type = IdType.ASSIGN_ID) private String id; ``` ###### 4.5.8.5.4 ASSIGN_UUID ASSIGN_UUID(不含中划线的`UUID`) 如果使用 `IdType.ASSIGN_UUID` 策略,则会自动生成不含中划线的 `UUID` 作为主键,主键类型为 `String`。 **使用方法** ```java @Data public class UserInfo { //指定主键生成策略为不含中划线的UUID @TableId(type = IdType.ASSIGN_UUID) private String id; private String userName; private String passWord; } ``` ###### 4.5.8.5.5 NONE(无状态) 如果使用 `IdType.NONE` 策略,表示未设置主键类型(注解里等于跟随全局,全局里约等于` INPUT`) 假设我们希望默认全部都使用 `AUTO `策略(数据库`ID`自增),那么可以在 `application.properties `中添加如下配置进行修改: ```properties mybatis-plus.global-config.db-config.id-type=auto ``` ##### 4.5.8.6 多表联合查询 ### 4.6 整合Redis ### 4.7 整合MongoDB ### 4.8 整合IOC和AOP #### 4.8.1 IOC 控制反转(IOC)和依赖注入(DI)是同一个概念,目的在于降低系统耦合,将类的实例化工作交给Spring代理,主要用到的设计模式为工厂模式,通过Java反射机制实现类的自动注入。 IoC(Inverse of Control:控制反转)是⼀种设计思想,就是 将原本在程序中⼿动创建对象的控制 权,交由Spring框架来管理。 IoC 在其他语⾔中也有应⽤,并⾮ Spring 特有。 IoC 容器是 Spring ⽤来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各 种对象。 将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注⼊。这样可以很⼤ 程度上简化应⽤的开发,把应⽤从复杂的依赖关系中解放出来。 IoC 容器就像是⼀个⼯⼚⼀ 样,当我们需要创建⼀个对象的时候,只需要配置好配置⽂件/注解即可,完全不⽤考虑对象是如 何被创建出来的。 #### 4.8.2 AOP 面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。 利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。 能够将那些与业务⽆关,却为业务模块所共 同调⽤的逻辑或责任(例如事务处理、⽇志管理、权限控制等)封装起来,便于减少系统的重复 代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。 ##### 4.8.2.1概念 ![img](README.assets/a1ec08fa513d2697251aa39dbe5c9ff24116d852.png) 1. 方面(Aspect):一个关注点的模块化,这个关注点实现可能另外横切多个对象。事务管理是J2EE应用中一个很好的横切关注点例子。方面用Spring的Advisor或拦截器实现。将那些影响了 多个类的公共行为封装到一个可重用模块。简单地说,就是将那些与业务无关,却为业务模块所共同调用的 逻辑或责任封装起来,比如日志记录,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。 2. 切入点(Pointcut):指定一个通知将被引发的一系列连接点的集合。 3. 连接点(Joinpoint):程序执行过程中明确的点,如方法的调用或特定的异常被抛出。 4. 通知(Advice):在特定的连接点,AOP框架执行的动作。 5. 目标对象(Target Object):包含连接点的对象,也被称作被通知或被代理对象。 6. AOP代理(AOP Proxy):AOP框架创建的对象,包含通知。在Spring中,AOP代理可以是JDK动态代理或CGLIB代理。 7. 引入(Introduction):添加方法或字段到被通知的类。Spring允许引入新的接口到任何被通知的对象。 8. 编织(Weaving):组装方面来创建一个被通知对象。 ##### 4.8.2.2 使用场景 - 权限检查 - 缓存 - 内容传递 - 错误处理 - 日志记录,跟踪,优化,校准 - 性能优化,效率检查 ##### 4.8.2.3 使用 Spring Boot使用AOP需要添加spring-boot-starter-aop依赖 ```xml org.springframework.boot spring-boot-starter-aop ``` 定义通知 通知有五种类型,分别是: 1. 前置通知(@Before):在目标方法调用之前调用通知 2. 后置通知(@After):在目标方法完成之后调用通知 3. 环绕通知(@Around):在被通知的方法调用之前和调用之后执行自定义的方法 4. 返回通知(@AfterReturning):在目标方法成功执行之后调用通知 5. 异常通知(@AfterThrowing):在目标方法抛出异常之后调用通知 代码中定义了三种类型的通知,使用@Before注解标识前置通知,打印“beforeAdvice...”,使用@After注解标识后置通知,打印“AfterAdvice...”,使用@Around注解标识环绕通知,在方法执行前和执行之后分别打印“before”和“after”。这样一个切面就定义好了,代码如下: ```java @Aspect @Component public class AopAdvice { @Pointcut("execution (* com.xxgc.springboot.controller.*.*(..))") public void test() { } @Before("test()") public void beforeAdvice() { System.out.println("beforeAdvice..."); } @After() public void afterAdvice() { System.out.println("afterAdvice..."); } @Around() public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) { System.out.println("before"); try { proceedingJoinPoint.proceed(); } catch (Throwable t) { t.printStackTrace(); } System.out.println("after"); } } ``` ### 4.9 整合Quartz定时框架 #### 补充讲解 Timer ```java public class TimerTest { private static int i = 3; //Timer Java自带的定时任务类 public static void main(String[] args) { //任务1 TimerTask timerTask = new TimerTask(){ public void run(){ int j = 1 / i; System.out.println("我被执行了"); i--; } }; //任务2 TimerTask timerTask2 = new TimerTask(){ public void run(){ System.out.println("22222222222222222"); } }; //计时器 Timer timer = new Timer(); //开始执行任务 1秒后执行 每3秒执行一次 timer.schedule(timerTask,1000, 3000); //开始执行任务 1秒后执行 每3秒执行一次 timer.schedule(timerTask2,1000, 4000); } } ``` timer有一个问题,在创建schedule,如果某一个日程报错,其他日志将不会继续执行。 #### 补充讲解:ScheduledExecutorService 官方发现后提供了ScheduledExecutorService类来解决Timer所有的问题。 #### 补充讲解 Spring Task Spring Framework自带的定时任务,核心使用了Timer和ScheduledExecutorService两种方式混合实现 SpringBoot项目使用Spring Task分为两步 1、开启定时任务 ```java //开启定时任务 @EnableScheduling public class SpringbootApplication { public static void main(String[] args) { SpringApplication.run(SpringbootApplication.class, args); } } ``` 2、添加定时任务 ```java @Component public class SpringTaskTest { //cron 表达式 //星期1-星期5 每天 19点35分0秒执行 @Scheduled(cron = "0 35 19 * * 1-5") public void doZhxStudy(){ System.out.println("周焕熙起来学习"); } } ``` #### 4.9.1 Quartz介绍导入 Quartz 石英 》手表 》 时间 》 任务调度框架 Timer ```xml org.springframework.boot spring-boot-starter-quartz ``` - Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目 - Quartz 是一个完全由 Java 编写的开源作业调度框架,为在 Java 应用程序中进行作业调度提供了简单却强大的机制。 - Quartz 可以与 J2EE 与 J2SE 应用程序相结合也可以单独使用。 - Quartz 允许程序开发人员根据时间的间隔来调度作业。 - Quartz 实现了作业和触发器的多对多的关系,还能把多个作业与不同的触发器关联。 #### 4.9.2 名词解析 1. **Job** 表示一个工作,要执行的具体内容。此接口中只有一个方法 ```java void execute(JobExecutionContext context) throws JobExecutionException; ``` 1. **JobDetail** (工作细节)表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务**调度的方案和策略**。 2. **Trigger** 触发器,代表一个调度参数的配置,**什么时候去调**。 3. **Scheduler** (日程)调度器,代表一个**调度容器**,一个调度容器中可以注册多个 JobDetail 和 Trigger。当 Trigger 与 JobDetail 组合,就可以被 Scheduler 容器调度了。 **Scheduler** 调度器 》 **Trigger** 触发器 ​ 》 **JobDetail** 工作策略 》**Job** 工作 #### 4.9.3 常用API 1. Scheduler 用于与调度程序交互的主程序接口 调度程序-任务执行计划表,只有安排进执行计划的任务Job(通过Schedule.scheduleJob方法安排进任务计划),任务触发trigger,任务才会执行 2. Job 任务类,如果自定义任务的话,需要实现Job接口,实现**void execute(JobExecutionContext context) throws** **JobExecutionException;**方法 3. JobDetail 定义任务Job的实例,通过JobBuilder类创建 4. JobDataMap Map接口实现类,存储Job实例执行过程中的数据 5. Trigger触发器,用来执行Job任务 6. JobBuilder 用于声明一个任务实例,可定义该任务的任务名,组名等 7. TriggerBuilder 触发器创建类,用于创建触发器trigger的实例 8. JobListener、TriggerListener、SchedulerListener监听器,用于对组件的监听 #### 4.9.4 初体验 ```java public class HelloJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { String string = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); System.out.println("数据库备份时间:"+string); } } ``` 调用 ```java public class TestQuartz { public static void main(String[] args) throws SchedulerException { // 1. 创建调度器Scheduler,从工厂中获取默认调度器 Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); // 2. 创建JobDetail实例 // HelloJob.class 实现Job接口的实例类 // "JobDetailName" JobDetail的名称,用来标识任务 // "JobDetailGroup" 任务组名称,可省略,默认为DEFAULT // build() 创建JobDetail实例 JobDetail jobDetail = JobBuilder.newJob(HelloJob.class) .withIdentity("JobDetailName", "JobDetailGroup") .build(); // 3. 触发器 Trigger // "TriggerName" 触发器名称 用来标识触发器 // "TriggerGroup" 触发器组名称,可省略,默认为DEFAULT // startNow() 开始时间 立刻开始 // withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5)) 每5秒执行一次 // 创建触发器 Trigger实例 Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("TriggerName", "TriggerGroup") .startNow() .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5)) .build(); // 将任务与触发器相关联 scheduler.scheduleJob(jobDetail, trigger); // 启动调度器 scheduler.start(); } } ``` #### 4.9.5 Job和JobDetail ##### 4.9.5.1 Job - Job:工作任务调度的接口,任务类需要实现该接口,该接口中的execute方法,类似JDK提供的TimeTask中的run方法,在里面编写任务执行的业务逻辑代码 ##### 4.9.5.2 Job生命周期 - Job实例在Quartz中的生命周期:每次调度器执行Job时,它在调用execute方法钱会创建一个新的Job实例,当调用结束后,关联的Job对象就会被释放,被垃圾回收机制回收 - 修改HelloJob类,添加无参构造方法 - ```java public class HelloJob implements Job { public HelloJob() { System.out.println("HelloJob任务被创建"); } @Override public void execute(JobExecutionContext context) throws JobExecutionException { String string = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); System.out.println("数据库备份时间:"+string); } } ``` - 每执行一次任务,HelloJo类就会被创建一次 ##### 4.9.5.3 JobDetail 1. jobDetail为Job实例提供了许多设置属性,以及JobDetalMap成员变量属性,用来存储特定Job实例的状态信息,调度器需要借助JobDatail对象添加Job实例 2. 重要属性:name、group、jobClass、jobDataMap 3. 修改TestQuartz测试类 ```java public class TestQuartz { public static void main(String[] args) throws SchedulerException { // 1. 创建调度器Scheduler,从工厂中获取默认调度器 Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); // 2. 创建JobDetail实例 // HelloJob.class 实现Job接口的实例类 // "JobDetailName" JobDetail的名称,用来标识任务 // "JobDetailGroup" 任务组名称,可省略,默认为DEFAULT // build() 创建JobDetail实例 JobDetail jobDetail = JobBuilder.newJob(HelloJob.class) .withIdentity("JobDetailName", "JobDetailGroup") .build(); // 获取jobDetail设置的名称 String jobDetailName = jobDetail.getKey().getName(); System.out.println("设置的任务名称:"+jobDetailName); // 获取设置的任务组名称 String jobDetailGroup = jobDetail.getKey().getGroup(); System.out.println("设置的任务组名称:"+jobDetailGroup); // 获取Job实现类的名称 String simpleName = jobDetail.getJobClass().getSimpleName(); System.out.println("任务实例类名称:"+simpleName); // 3. 触发器 Trigger // "TriggerName" 触发器名称 用来标识触发器 // "TriggerGroup" 触发器组名称,可省略,默认为DEFAULT // startNow() 开始时间 立刻开始 // withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5)) 每5秒执行一次 // 创建触发器 Trigger实例 Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("TriggerName", "TriggerGroup") .startNow() .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5)) .build(); // 将任务与触发器相关联 scheduler.scheduleJob(jobDetail, trigger); // 启动调度器 scheduler.start(); } } ``` - 打印JobDetail信息 ```tex 设置的任务名称:JobDetailName 设置的任务组名称:JobDetailGroup 任务实例类名称:HelloJob HelloJob任务被创建 数据库备份时间:2021-08-27 18:28:24 HelloJob任务被创建 数据库备份时间:2021-08-27 18:28:29 ······ ``` #### 4.9.6 JobExecutionContext - 当Scheduler调用一个ob,就会将JobExecutionContext传递给Job的execute()方法; - Job能通过JobExecutionContext对象访问到Quartz运行时候的环境以及Job本身的明细数据。 - 修改HelloJob类: - ```java public class HelloJob implements Job { public HelloJob() { System.out.println("HelloJob任务被创建"); } @Override public void execute(JobExecutionContext context) throws JobExecutionException { JobDetail jobDetail = context.getJobDetail(); String jobDetailName = jobDetail.getKey().getName(); System.out.println("任务名称:"+jobDetailName); String jobDetailGroup = jobDetail.getKey().getGroup(); System.out.println("任务组名称:"+jobDetailGroup); String jobSimpleName = jobDetail.getJobClass().getSimpleName(); System.out.println("任务实现类名称:"+jobSimpleName); Trigger trigger = context.getTrigger(); String triggerName = trigger.getKey().getName(); System.out.println("触发器名称:"+triggerName); String triggerGroup = trigger.getKey().getGroup(); System.out.println("触发器组名称:"+triggerGroup); // 获取其他信息 Date date = context.getFireTime(); String format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); System.out.println("任务执行的时间"+format); Date nextFireTime = context.getNextFireTime(); String format1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(nextFireTime); System.out.println("下一次任务执行的时间"+format1); String string = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); System.out.println("数据库备份时间:"+string); } } ``` - ```tex HelloJob任务被创建 任务名称:JobDetailName 任务组名称:JobDetailGroup 任务实现类名称:HelloJob 触发器名称:TriggerName 触发器组名称:TriggerGroup 任务执行的时间2021-08-30 11:00:02 下一次任务执行的时间2021-08-30 11:02:30 数据库备份时间:2021-08-30 11:00:02 ``` #### 4.9.7 JobDataMap - 在进行任务调度时,JobDataMap存储在JobExecutionContext中 - JobDataMap可以用来装载任何可序列化的数据对象,当job实例对象被执行时这些参数对象会传递给它 - JobDataMap实现了JDK的Map接口 ![524d8ab210794a66b621bee45712274b](README.assets/524d8ab210794a66b621bee45712274b.png) ##### 4.9.7.1 修改TestQuartz类 ```java public class TestQuartz { public static void main(String[] args) throws SchedulerException { // 1. 创建调度器Scheduler,从工厂中获取默认调度器 Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); // 2. 创建JobDetail实例 // HelloJob.class 实现Job接口的实例类 // "JobDetailName" JobDetail的名称,用来标识任务 // "JobDetailGroup" 任务组名称,可省略,默认为DEFAULT // build() 创建JobDetail实例 JobDataMap jobDataMap = new JobDataMap(); jobDataMap.put("message","打印日志"); JobDetail jobDetail = JobBuilder.newJob(HelloJob.class) .withIdentity("JobDetailName", "JobDetailGroup") .usingJobData(jobDataMap) .build(); /*// 获取jobDetail设置的名称 String jobDetailName = jobDetail.getKey().getName(); System.out.println("设置的任务名称:"+jobDetailName); // 获取设置的任务组名称 String jobDetailGroup = jobDetail.getKey().getGroup(); System.out.println("设置的任务组名称:"+jobDetailGroup); // 获取Job实现类的名称 String simpleName = jobDetail.getJobClass().getSimpleName(); System.out.println("任务实例类名称:"+simpleName);*/ // 3. 触发器 Trigger // "TriggerName" 触发器名称 用来标识触发器 // "TriggerGroup" 触发器组名称,可省略,默认为DEFAULT // startNow() 开始时间 立刻开始 // withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5)) 每5秒执行一次 // 创建触发器 Trigger实例 Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("TriggerName", "TriggerGroup") .usingJobData(jobDataMap) .startNow() .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5)) .build(); // 将任务与触发器相关联 scheduler.scheduleJob(jobDetail, trigger); // 启动调度器 scheduler.start(); } } ``` ##### 4.9.7.2 修改HelloJob类 ```java public class HelloJob implements Job { public HelloJob() { System.out.println("HelloJob任务被创建"); } @Override public void execute(JobExecutionContext context) throws JobExecutionException { /*JobDetail jobDetail = context.getJobDetail(); String jobDetailName = jobDetail.getKey().getName(); System.out.println("任务名称:"+jobDetailName); String jobDetailGroup = jobDetail.getKey().getGroup(); System.out.println("任务组名称:"+jobDetailGroup); String jobSimpleName = jobDetail.getJobClass().getSimpleName(); System.out.println("任务实现类名称:"+jobSimpleName); Trigger trigger = context.getTrigger(); String triggerName = trigger.getKey().getName(); System.out.println("触发器名称:"+triggerName); String triggerGroup = trigger.getKey().getGroup(); System.out.println("触发器组名称:"+triggerGroup);*/ // 获取jobDataMap JobDataMap jobDetailDataMap = context.getJobDetail().getJobDataMap(); String JobDetailMessage = jobDetailDataMap.getString("message"); System.out.println("任务信息:"+JobDetailMessage); JobDataMap triggerJobDataMap = context.getTrigger().getJobDataMap(); String triggerMessage = triggerJobDataMap.getString("message"); System.out.println("触发器信息:"+triggerMessage); String string = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); System.out.println("数据库备份时间:"+string); } } ``` 结果 ```tex HelloJob任务被创建 任务信息:打印日志 触发器信息:打印日志 数据库备份时间:2021-08-30 10:44:24 ······ ``` - Job实现类中添加setter方法对应JobDataMap的键值,Quartz[框架](https://so.csdn.net/so/search?q=框架&spm=1001.2101.3001.7020)默认的JobFactory实现类在初始化job实例对象时会自动地调用这些setter方法。 ```java public class TestQuartz { public static void main(String[] args) throws SchedulerException { // 1. 创建调度器Scheduler,从工厂中获取默认调度器 Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); // 2. 创建JobDetail实例 // HelloJob.class 实现Job接口的实例类 // "JobDetailName" JobDetail的名称,用来标识任务 // "JobDetailGroup" 任务组名称,可省略,默认为DEFAULT // build() 创建JobDetail实例 JobDetail jobDetail = JobBuilder.newJob(HelloJob.class) .withIdentity("JobDetailName", "JobDetailGroup") .usingJobData("message","任务信息") .build(); /*// 获取jobDetail设置的名称 String jobDetailName = jobDetail.getKey().getName(); System.out.println("设置的任务名称:"+jobDetailName); // 获取设置的任务组名称 String jobDetailGroup = jobDetail.getKey().getGroup(); System.out.println("设置的任务组名称:"+jobDetailGroup); // 获取Job实现类的名称 String simpleName = jobDetail.getJobClass().getSimpleName(); System.out.println("任务实例类名称:"+simpleName);*/ // 3. 触发器 Trigger // "TriggerName" 触发器名称 用来标识触发器 // "TriggerGroup" 触发器组名称,可省略,默认为DEFAULT // startNow() 开始时间 立刻开始 // withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5)) 每5秒执行一次 // 创建触发器 Trigger实例 Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("TriggerName", "TriggerGroup") .usingJobData("message","触发器信息") .startNow() .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5)) .build(); // 将任务与触发器相关联 scheduler.scheduleJob(jobDetail, trigger); // 启动调度器 scheduler.start(); } } ``` ```java public class HelloJob implements Job { private String message; public HelloJob() { System.out.println("HelloJob任务被创建"); } public void setMessage(String message) { this.message = message; } @Override public void execute(JobExecutionContext context) throws JobExecutionException { /*JobDetail jobDetail = context.getJobDetail(); String jobDetailName = jobDetail.getKey().getName(); System.out.println("任务名称:"+jobDetailName); String jobDetailGroup = jobDetail.getKey().getGroup(); System.out.println("任务组名称:"+jobDetailGroup); String jobSimpleName = jobDetail.getJobClass().getSimpleName(); System.out.println("任务实现类名称:"+jobSimpleName); Trigger trigger = context.getTrigger(); String triggerName = trigger.getKey().getName(); System.out.println("触发器名称:"+triggerName); String triggerGroup = trigger.getKey().getGroup(); System.out.println("触发器组名称:"+triggerGroup);*/ /*// 获取jobDataMap JobDataMap jobDetailDataMap = context.getJobDetail().getJobDataMap(); String JobDetailMessage = jobDetailDataMap.getString("message"); System.out.println("任务信息:"+JobDetailMessage); JobDataMap triggerJobDataMap = context.getTrigger().getJobDataMap(); String triggerMessage = triggerJobDataMap.getString("message"); System.out.println("触发器信息:"+triggerMessage);*/ /*// 获取其他信息 Date date = context.getFireTime(); String format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); System.out.println("任务执行的时间"+format); Date nextFireTime = context.getNextFireTime(); String format1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(nextFireTime); System.out.println("下一次任务执行的时间"+format1);*/ System.out.println(message); String string = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); System.out.println("数据库备份时间:"+string); } } ``` - 成员的类型和名称需要和在TestQuartz中设置的对应 - jobDetail和trigger的jobDataMap的key值相同,trigger的jobDataMap会覆盖jobDetail的 #### 4.9.8 有状态的Job和无状态的Job - @PersistJobDataAfterExecution注解的使用 - 有状态的Job可以理解为多次job调用期间可以持有一些状态信息,这些状态信息存储在JobDataMap中 - 默认的无状态job每次调用时都会创建一个新的JobDataMap。 - ```java public class TestQuartz01 { public static void main(String[] args) throws SchedulerException { // 1. 获取调度器 Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); // 2. 获取JobDetail JobDetail jobDetail = JobBuilder.newJob(TestJob.class) .withIdentity("JobDetail", "JobDetailGroup") .usingJobData("count",0) .build(); // 3. 获取trigger Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("Trigger", "TriggerGroup") .startNow() .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5)) .build(); // 4. 通过调度器绑定任务和触发器 scheduler.scheduleJob(jobDetail,trigger); //5. 启动 scheduler.start(); } } ``` - ```java public class TestJob implements Job { private Integer count; public void setCount(Integer count) { this.count = count; } @Override public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println("TestJob"); count++; JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); jobDataMap.put("count",count); System.out.println("TestJob中的count值:"+count); System.out.println("jobDataMap的count值:"+jobDataMap.getInt("count")); } } ``` - ```tex TestJob TestJob中的count值:1 jobDataMap的count值:1 TestJob TestJob中的count值:1 jobDataMap的count值:1 ······ ``` - 添加@PersistJobDataAfterExecution注解 - ```java @PersistJobDataAfterExecution public class TestJob implements Job { private Integer count; public void setCount(Integer count) { this.count = count; } @Override public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println("TestJob"); count++; JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); jobDataMap.put("count",count); System.out.println("TestJob中的count值:"+count); System.out.println("jobDataMap的count值:"+jobDataMap.getInt("count")); } } ``` - ```java TestJob TestJob中的count值:1 jobDataMap的count值:1 TestJob TestJob中的count值:2 jobDataMap的count值:2 TestJob TestJob中的count值:3 jobDataMap的count值:3 ······ ``` #### 4.9.9 cron表达式 **Spring Task框架只能填前6位** 1 2 3 4 5 6 7 **一共七个位置,分别代表:** 1、秒 2、分钟 3、小时 4、日 5、月 6、星期 7、年 **4个常用符号,分别代表 :** 1、/ 从什么开始每过多久执行一次 2、- 从什么时候到什么时候 3、* 通配符 4、 '?'只能在日和星期(周)中指定使用,其作用为不指定 ~~**如果激活时间过了没有激活,那后续添加不会运行!!!**~~ #### 4.9.10 整合数据库 ![image-20220919172826553](README.assets/image-20220919172826553.png) ![image-20220919172933138](README.assets/image-20220919172933138.png) ### 4.10 整合Kafka消息队列 ### 4.11 整合RocketMQ ### 4.12 整合日志框架 #### 4.12.1为什么需要日志框架 首先我们需要明白,日志的作用是什么--即用来在程序运行过程中,将我们需要的信息打印出来,便于我们在调试中查找和观察。在JAVA中存在很多常见的日志框架,如JUL、JCL、Jboss-logging、log4j、logback、slf4j等,这么多日志框架,我们该如何选择? 在日志框架选型之前,我们先了解一个概念,什么是**日志门面**?日志门面,不是**具体的日志解决方案**,它只服务于**各种各样的日志系统**,允许最终用户在部署其应用时使用**其所希望的日志**实现来使用日志功能。而日志实现则是基于对应的日志门面的规范来实现的具体日志功能的框架,常见的日志门面与日志实现关系如下: 日志门面: JCL 、SL4FJ、JBoss-logging 日志实现 Log4j、JUL(Java.util.logging)、log4j2、logback 每一种日志框架输出信息的效率也不尽相同,而我们日常开发使用的框架中往往都会引入一个日志框架来辅助输出框架信息,然而框架之间由于历史迭代原因及框架性能等问题,选择的日志框架也不一样,常见的**框架**与**默认选择的日志**系统关系如下: ![img](README.assets/v2-c95761d36fdff548fcbbdcd582716a1f_720w.jpg) 由于历史迭代原因,**JCL和jboss-logging**日志框架,基本已经很久没有更新了,不太适合作为现在框架的主流选择,那么剩下的选择中**log4j、slf4j**是使用最多的,然而由于**log4j**的输出性能问题,log4j的作者选择重新编写了一个日志门面--**Slf4j**,并且编写了基于Slf4j的日志实现--**logback**,其输出信息的效率远超log4j,解决了log4j遗留下的性能问题,所以在**SpringBoot**框架中,默认也选择了**Slf4j**来作为默认日志框架 #### 4.12.2使用 ```java Logger logger = LoggerFactory.getLogger(HelloWorld.class); logger.trace("这是trace日志。。。"); system.out.print logger.debug("这是debug日志。。。"); //Springboot默认只到info及以下 logger.info("这是info日志。。。"); logger.warn("这是warn日志。。。"); logger.error("这是error日志。。。"); ``` ![这里写图片描述](README.assets/20171219163536712) 每个日志的实现框架都有自己的配置文件。slf4j门面的**配置文件还是做成日志实现框架自身的配置文件** #### 4.12.3配置 ```yaml logging: file: name: D://log//xxgc.log #文件名 path和name二选一 level: #日志输出包范围 日志输出级别 com.xxgc.springboot.controller: debug ``` 需要做日志大数据分析 ```yaml #yml配置 logging: config: classpath:log/logback-spring.xml path: D:/log level: root: info ``` 在资源文件夹下创建logback-spring.xml ```xml logback info ${CONSOLE_LOG_PATTERN} UTF-8 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n UTF-8 ${logging.file.path}/debug/log-debug-%d{yyyy-MM-dd}.%i.log ${maxFileSize} ${maxHistory} debug ACCEPT DENY %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n UTF-8 ${logging.file.path}/info/log-info-%d{yyyy-MM-dd}.%i.log ${maxFileSize} ${maxHistory} info ACCEPT DENY %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n UTF-8 ${logging.file.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log ${maxFileSize} ${maxHistory} warn ACCEPT DENY %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n UTF-8 ${logging.file.path}/error/log-error-%d{yyyy-MM-dd}.%i.log ${maxFileSize} ${maxHistory} ERROR ACCEPT DENY ``` ### 4.13 整合RabbitMQ ### 4.14 整合ElasticSearch ### 4.15 整合Solr