# boot-launch **Repository Path**: yanglf_admin/boot-launch ## Basic Information - **Project Name**: boot-launch - **Description**: No description available - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-10-01 - **Last Updated**: 2022-02-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: SpringBoot ## README # 基础及概念入门 ## 产生的背景及其优势 ### 目标 - 使配置变简单 - 使监控变简单 - 使部署变简单 - 使开发变简单 ### 主要特点 - 遵循约定优于配置的原则,简化配置 - 可以完全脱离xml配置文件,采用注解配置和Java config - 内嵌servlet容器,应用可以使用jar包执行,`java -jar xx.jar` - 快速完成项目搭建,整合第三方内库,方便易用 - 提供了 starter pom,能够方便的进行包管理,简化包管理配置 - 与springcloud天然融合,spring boot是目前体系内实现微服务的最佳方案 ### 集成第三方库步骤 1. 通过maven引入springboot-XXXX-starter 2. 修改ymal或properties全局统一配置文件 3. 加入一个Java Config。这个属于个性化配置,如果使用通用配置,这一步不需要。 默认内库: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot-starter ## 项目结构 ### Hello World 示例项目 ![img](https://i.loli.net/2021/09/26/o7ShcjIVsLw614Q.png) yml文件和properties配置文件具有同样的功能。二者的区别在于: - yml文件的层级更加清晰直观,但是书写时需要注意格式缩进对齐。yml格式配置文件更有利于表达复杂数据结构的配置。比如:列表,对象(后面章节会详细说明)。 - properties阅读上不如yml直观,好处在于书写时不用特别注意格式缩进对齐。 ```yaml server: port: 8888 # web应用服务端口 ``` 引入spring-boot-starter-web依赖(不需要加版本号,版本号由parent应用统一管理) ```xml org.springframework.boot spring-boot-starter-web ``` 编写第一个 controller ```java @RestController public class HelloController { @RequestMapping("/hello") public String hello(String name) { return "hello world, " +name; } } ``` ### 项目结构 项目结构目录整体上符合maven规范要求: | 目录位置 | 功能 | | :---------------------------------------- | :----------------------------------------------------------- | | src/main/java | 项目java文件存放位置,初始化包含主程序入口 XxxApplication,可以通过直接运行该类来 启动 Spring Boot应用 | | src/main/resources | 存放静态资源,图片、CSS、JavaScript、web页面模板文件等 | | src/test | 单元测试代码目录 | | .gitignore | git版本管理排除文件 | | target文件夹 | 项目代码构建打包结果文件存放位置,不需要人为维护 | | pom.xml | maven项目配置文件 | | application.properties(application.yml) | 用于存放程序的各种依赖模块的配置信息,比如服务端口,数据库连接配置等 | - src/main/resources/static主要用来存放css、图片、js等开发用静态文件 - src/main/resources/public用来存放可以直接用于访问的html文件 - src/main/resources/templates用来存放web开发模板文件 ## 核心概念 ### Spring Boot 、 Spring MVC 、Spring对比 #### Spring Spring框架最核心的特性就是依赖注入DI(Dependency Injecttion)和控制反转IOC(Inversion Of Control)。如果你能够合理的使用DI和IOC,可以开发出松耦合、扩展性好的的应用程序。 #### Spring MVC Spring MVC提供了一种友好的方式来开发Web应用程序。 通过使用诸如Dispatcher Servlet,ModelAndView和View Resolver,可以轻松开发Web应用程序。 #### Spring Boot Spring 和 Spring MVC最大的弊病在于存在大量的配置,并且这些配置在不同的项目中具有很高的相似性。从而导致重复配置,繁琐而且杂乱! Spring Boot期望通过结合自动配置和starters来解决了这个问题。 另外,Spring Boot还提供了一些功能,可以更快地构建可用于生产环境的应用程序。 ### Spring Boot 自动配置 Spring和Spring MVC应用程序里面有大量的XML或Java Bean配置。Spring Boot为解决这个问题,提供一种新的解决方案,新的思维方式 ### Spring Boot Starter Spring Boot Starter是一组被依赖第三方类库的集合。 如果你要开发一个web应用程序,就通过包管理工具(如maven)引入spring-boot-starter-web就可以了,而不用分别引入下面这么多依赖类库,spring-boot-starter-web一次性帮你引入下面的这些常用类库。 - Spring — spring 核心, beans, context上下文, AOP面向切面 - Web MVC — Spring MVC - Jackson — JSON数据的序列化与反序列化 - Validation — Hibernate参数校验及校验API - 嵌入式 Servlet Container — Tomcat - 日志框架Logging — logback, slf4j ### Spring Boot Starter Parent 所有的Spring Boot项目默认使用spring-boot-starter-parent作为应用程序的父项目。 ```xml org.springframework.boot spring-boot-starter-parent 2.0.0.RELEASE ``` 继承父项目的好处在于: 统一java版本配置和其他的一些依赖类库的版本。也就是说,你引入的第三方类库不要加版本号,父项目替你管理版本,而且是经过兼容性测试的。比你自己随便引入一个版本兼容性更好。 ### 嵌入式web容器 Spring boot打成jar包,默认包含嵌入式的web容器:tomcat。你可以简单的使用如下命令启动一个web服务: ```sh java -jar springboot-demo.jar ``` 这更有利于微服务的部署及微服务的构建、启动、扩容。Spring Boot还支持Jetty和Undertow作为web容器。 ### Spring Data ![img](https://i.loli.net/2021/09/26/ekfHFEig4byZ9dm.png) Spring Data的目标是提供一种更友好的方式或者是API来存取数据。包括对于关系型数据库和NOSQL数据的支持。比如: - Spring Data JPA — 关系型数据库操作的API,友好且易于使用 - Spring Data MongoDB -MongoDB的操作API - Spring Data REST — 从持久层Repositories自动生成服务层API,暴露 REST APIs 接口。超级好用! ### spring boot2.x新特性 #### 基础环境升级 - 最低 JDK 8,支持 JDK 9,不再支持 Java 6 和 7。Spring Boot 2.0 要求 Java 8 作为最低版本,许多现有的 API 已更新,以利用 Java 8 的特性。 例如,接口上的默认方法,函数回调以及新的 API,如 javax.time。 - 如果你正在使用 Java 7 或更早版本,则在开发 Spring Boot 2.0 应用程序之前,需要升级你的 JDK。 #### 依赖组件升级 - Jetty 9.4,Jetty 是一个开源的 Servlet 容器,它为基于 Java 的 Web 内容,例如 JSP 和 Servlet 提供运行环境。Jetty 是使用 Java 语言编写的,它的 API 以一组 JAR 包的形式发布。 - Tomcat 8.5,Apache Tomcat 8.5.x 旨在取代 8.0.x,完全支持 Java 9。 - Flyway 5,Flyway 是独立于数据库的应用、管理并跟踪数据库变更的数据库版本管理工具。用通俗的话讲,Flyway 可以像 SVN 管理不同人的代码那样,管理不同人的 SQL 脚本,从而做到数据库同步。 - Hibernate 5.2,Hibernate 是一款非常流行的 ORM 框架。 - Gradle 3.4,Spring Boot 的 Gradle 插件在很大程度上已被重写,有了重大的改进。 - Thymeleaf 3.0,Thymeleaf 3 相对于 Thymeleaf 2 有非常大的性能提升 #### 默认软件替换 - 默认数据库连接池已从 Tomcat 切换到 HikariCP,HikariCP 是一个高性能的 JDBC 连接池,Hikari 是日语“光”的意思。 - redis客户端默认使用 Lettuce,替换掉Jedis.Lettuce 是一个可伸缩的线程安全的 Redis 客户端,用于同步、异步和反应使用。多个线程可以共享同一个 RedisConnection,它利用优秀 Netty NIO 框架来高效地管理多个连接,支持先进的 Redis 功能,如 Sentinel、集群、流水线、自动重新连接和 Redis 数据模型。 #### 新技术的引入 - 响应式编程WebFlux,重要的变革,后续章节会详细展示 - 支持 Quartz,Spring Boot 1.0 并没有提供对 Quartz 的支持,之前出现了各种集成方案,Spring Boot 2.0 给出了最简单的集成方式。 - 对Kotlin 的支持 - JOOQ 的支持,JOOQ 是基于 Java 访问关系型数据库的工具包。JOOQ 既吸取了传统 ORM 操作数据的简单性和安全性,又保留了原生 SQL 的灵活性,它更像是介于 ORMS 和 JDBC 的中间层。 #### 彩蛋 在 Spring Boot 1.0 项目中 src/main/resources 路径下新建一个 banner.txt 文件,文件中写入一些字符,启动项目时就会发现默认的 Banner 被替换了,到了 Spring Boot 2.0 现在可以支持 Gif 文件的打印,Spring Boot 2.0 在项目启动的时候,会将 Gif 图片的每一个画面,按照顺序打印在日志中,所有的画面打印完毕后,才会启动 Spring Boot 项目。 ## lombok ### 优点 - 根据成员变量生成get和set方法 - 根据成员变量生成类的构造函数 - 重写toString()和hashCode方法 - 引入日志框架logFactory,用来打印日志 ### 使用步骤 #### 安装lombok插件 打开 IDEA 的 File->Settings 面板,并选择 Plugins 选项,然后点击 “Browse repositories”。在搜索框输入”lombok”,结果中找到lombok点击install,然后重启 IDEA。 #### 添加依赖 ```xml org.projectlombok lombok ``` > 在Spring Boot项目里面不需要加入版本号,spring Boot父项目会代为管理。如果是其他项目,请自行添加版本号! #### 常用注解 ```java /** * @author yanglf * @date 2021年09月25日 19:53 */ @Data @Builder @Slf4j @AllArgsConstructor @NoArgsConstructor public class User { private String name; private Integer age; } ``` ## 热部署 ### Jrebel插件 这是最简单的一种方式,但是有一定的个局限性,Jrebel插件是收费的 ### devtools 添加依赖 ```xml org.springframework.boot spring-boot-devtools ``` 修改完代码点击 build `锤子菜单` 其他配置 ```properties # 热加载是否生效spring.devtools.restart.enabled=false# 额外新增的热加载目录spring.devtools.restart.additional-paths=# 热加载排除的目录spring.devtools.restart.exclude= ``` ### Live Reload 插件 Spring devtools默认会启动一个 Live Reload Server实例,监听文件的变化。并实时的与浏览器插件通信,更新浏览器展示界面 ## IDEA 常用插件 1. `Codota`: 极其强大的代码自动补全 2. `Auto filling Java call arguments`: 调用一个函数,使用 Alt+Enter 组合键,调出 "Auto fill call parameters" 自动使用该函数定义的参数名填充。 3. `GsonFormat`: 可以快速的将JSON转换为实体类 4. Rainbow Brackets 5. Maven Helper 6. Key promoter X 7. 换个美女图: Ctrl+Shift+A(或者help -> find action)调用弹窗后输入Set Background Image # RESTful接口实现与测试 ## RESTful接口与http协议状态表述 ### 优点 1. 看Url就知道要什么资源 2. 看http method就知道针对资源干什么 3. 看http status code就知道结果如何 ### RESTful API的设计风格 #### RESTful是面向资源的(名词) | 不符合Restfull风格的接口url | 符合Restfull风格的接口url | 功能 | | --------------------------- | ------------------------- | -------------------- | | GET /api/getDogs/{id} | GET /api/dogs/{id} | 获取一个小狗 => (R) | | GET /api/getDogs | GET /api/dogs | 获取所有小狗 => (R) | | GET /api/addDogs | POST /api/dogs | 新增一个小狗 => (C) | | GET /api/editDogs/{id} | PUT /api/dogs/{id} | 修改一个小狗 => (U) | | GET /api/deleteDogs/{id} | DELETE /api/dogs/{id} | 删除一个小狗 => (D) | #### 用HTTP方法体现对资源的操作(动词) - GET : 获取、读取资源 - POST : 添加资源 - PUT : 修改资源 - DELETE : 删除资源 #### HTTP状态码 200: OK 400: Bad Request 500: Internal Server Error #### 复杂资源关系的表述 - GET /cars/711/drivers/ 返回使用过编号是711的汽车的所有司机 - GET /cars/711/drivers/4 返回使用过编号是711的汽车的4号司机 #### 高级用法:HATEOAS **返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么** ```json {"link": { "rel": "collection https://www.example.com/zoos", "href": "https://api.example.com/zoos", "title": "List of zoos", "type": "application/vnd.yourformat+json"}} ``` #### 资源过滤、排序、选择和分页的表述 - 资源过滤 - GET /cars?color=red 获取红色的汽车 - GET /cars?seats<=4 获取小于四座的汽车 - 资源数据排序 - GET /cars?sort=-manufactorer,+model 获取汽车资源数据,先按照生产者降序排列,在按照车架模型升序排序 - 资源数据字段选择 - GET /cars?fields=manufactorer,model,color 只获取其中一些字段,给api消费者一个选择字段的能力,降低网络流量 - 资源数据分页 - GET /cars?offset=10&limit=5 使用offset和limit实现分页 #### 版本化API 强制性增加api的版本声明,不要发布无版本的api `/api/v1/blogs` ## Spring常用注解及基础讲解 ### HTTP协议的四种传参方式 | HTTP 协议组成 | HTTP协议内容示例 | 对应Spring注解 | | --------------------- | ---------------------------------------------- | -------------- | | path info 传参 | /articles/12 查询id是12的文章,12是参数 | @PathVariable | | URL Query String 传参 | /articles?id=12 | @RequestParam | | Body 传参 | Content-Type:multipart/form-data | @RequestParam | | Body 传参 | Content-Type:application/json 或其他自定义格式 | @RequestBody | | Headers 传参 | | @RequestHeader | ### 常用注解 #### @RequestBody与@ResponseBody ```java //注意并不要求@RequestBody与@ResponseBody成对使用。public @ResponseBody AjaxResponse saveArticle(@RequestBody ArticleVO article) ``` - @RequestBody修饰请求参数,注解用于接收HTTP的body,默认是使用JSON的格式 - @ResponseBody修饰返回值,注解用于在HTTP的body中携带响应数据,默认是使用JSON的格式。如果不加该注解,spring响应字符串类型,是跳转到模板页面或jsp页面的开发模式。说白了:加上这个注解你开发的是一个数据接口,不加这个注解你开发的是一个页面跳转控制器。 #### @RequestMapping注解 @RequestMapping注解是所有常用注解中,最有看点的一个注解,用于标注HTTP服务端点。它的很多属性对于丰富我们的应用开发方式方法,都有很重要的作用。如: - value: 应用请求端点,最核心的属性,用于标志请求处理方法的唯一性; - method: HTTP协议的method类型, 如:GET、POST、PUT、DELETE等; - consumes: HTTP协议请求内容的数据类型(Content-Type),例如application/json, text/html; - produces: HTTP协议响应内容的数据类型。下文会详细讲解。 - params: HTTP请求中必须包含某些参数值的时候,才允许被注解标注的方法处理请求。 - headers: HTTP请求中必须包含某些指定的header值,才允许被注解标注的方法处理请求 ```java @RequestMapping(value = "/article", method = POST)@PostMapping(value = "/article") ``` 上面代码中两种写法起到的是一样的效果,也就是PostMapping等同于@RequestMapping的method等于POST。同理:@GetMapping、@PutMapping、@DeleteMapping也都是简写的方式。 #### @RestController与@Controller @Controller注解是开发中最常使用的注解,它的作用有两层含义: - 一是告诉Spring,被该注解标注的类是一个Spring的Bean,需要被注入到Spring的上下文环境中。 - 二是该类里面所有被RequestMapping标注的注解都是HTTP服务端点。 @RestController相当于 @Controller和@ResponseBody结合。它有两层含义: - 一是作为Controller的作用,将控制器类注入到Spring上下文环境,该类RequestMapping标注方法为HTTP服务端点。 - 二是作为ResponseBody的作用,请求响应默认使用的序列化方式是JSON,而不是跳转到jsp或模板页面。 #### @PathVariable 与@RequestParam PathVariable用于URI上的{参数},如下方法用于删除一篇文章,其中id为文章id。如:我们的请求URL为“/article/1”,那么将匹配DeleteMapping并且PathVariable接收参数id=1。而RequestParam用于接收普通表单方式或者ajax模拟表单提交的参数数据。 ```java @DeleteMapping("/article/{id}")public @ResponseBody AjaxResponse deleteArticle(@PathVariable Long id) {@PostMapping("/article")public @ResponseBody AjaxResponse deleteArticle(@RequestParam Long id) { ``` ### Http数据转换的原理 ![img](https://i.loli.net/2021/09/26/93gYWSL7A2Ma8nO.png) - 当一个HTTP请求到达时是一个InputStream,通过HttpMessageConverter转换为java对象,从而进行参数接收。 - 当对一个HTTP请求进行响应时,我们首先输出的是一个java对象,然后由HttpMessageConverter转换为OutputStream输出。 当我们在Spring Boot应用中集成了jackson的类库之后,如下的一些HttpMessageConverter将会被加载。 | 实现类 | 功能说明 | | :----------------------------------- | :----------------------------------------------------------- | | StringHttpMessageConverter | 将请求信息转为字符串 | | FormHttpMessageConverter | 将表单数据读取到MultiValueMap中 | | XmlAwareFormHttpMessageConverter | 扩展与FormHttpMessageConverter,如果部分表单属性是XML数据,可用该转换器进行读取 | | ResourceHttpMessageConverter | 读写org.springframework.core.io.Resource对象 | | BufferedImageHttpMessageConverter | 读写BufferedImage对象 | | ByteArrayHttpMessageConverter | 读写二进制数据 | | SourceHttpMessageConverter | 读写java.xml.transform.Source类型的对象 | | MarshallingHttpMessageConverter | 通过Spring的org.springframework,xml.Marshaller和Unmarshaller读写XML消息 | | Jaxb2RootElementHttpMessageConverter | 通过JAXB2读写XML消息,将请求消息转换为标注的XmlRootElement和XmlType连接的类中 | | MappingJacksonHttpMessageConverter | 利用Jackson开源包的ObjectMapper读写JSON数据 | | RssChannelHttpMessageConverter | 读写RSS种子消息 | | AtomFeedHttpMessageConverter | 和RssChannelHttpMessageConverter能够读写RSS种子消息 | 根据HTTP协议的Accept和Content-Type属性,以及参数数据类型来判别使用哪一种HttpMessageConverter。当使用RequestBody或ResponseBody时,再结合前端发送的Accept数据类型,会自动判定优先使用MappingJacksonHttpMessageConverter作为数据转换器。但是,不仅JSON可以表达对象数据类型,XML也可以。如果我们希望使用XML格式该怎么告知Spring呢,那就要使用到produces属性了。 ```java @GetMapping(value ="/demo",produces = MediaType.APPLICATION_XML_VALUE) ``` 这里我们明确的告知了返回的数据类型是xml,就会使用Jaxb2RootElementHttpMessageConverter作为默认的数据转换器。当然实现XML数据响应比JSON还会更复杂一些,还需要结合@XmlRootElement、@XmlElement等注解实体类来使用。同理consumes属性你是不是也会用了呢。 ### 自定义HttpMessageConverter #### 添加依赖 ```xml org.apache.poi poi-ooxml 4.1.2 ``` #### 编写转换类 ````java @Servicepublic class ResponseToXlsConverter extends AbstractHttpMessageConverter { private static final MediaType EXCEL_TYPE = MediaType.valueOf("application/vnd.ms-excel"); ResponseToXlsConverter() { super(EXCEL_TYPE); } @Override protected boolean supports(Class clazz) { // 只要是 返回 AjaxResult 的结果都需要序列 return (AjaxResult.class == clazz); } @Override protected AjaxResult readInternal(Class clazz, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException { // 序列化请求参数 return null; } @Override protected void writeInternal(AjaxResult ajaxResult, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException { // 序列化响应参数 try (Workbook workbook = new HSSFWorkbook()) { Sheet sheet = workbook.createSheet(); Row row = sheet.createRow(0); row.createCell(0).setCellValue(ajaxResult.toString()); workbook.write(httpOutputMessage.getBody()); } }} ```` - 实现AbstractHttpMessageConverter接口 - 指定该转换器是针对哪种数据格式的?如上文代码中的"application/vnd.ms-excel" - 指定该转换器针对那些对象数据类型?如上文代码中的supports函数 - 使用writeInternal对数据进行输出处理,上例中是输出为Excel格式 ## 常用注解开发一个RESTful接口 ### 开发REST接口 #### 定义资源(对象) ```java @Data@Builderpublic class Article { private Long id; private String author; private String title; private String content; private Date createTime; private List reader;} ``` ```java @Datapublic class Reader { private String name; private Integer age;} ``` Data、Builder都是lombok提供给我们的注解,有利于我们简化代码。可以参考本专栏之前章节对lombok进行学习。 - @Builder为我们提供了通过对象属性的链式赋值构建对象的方法,下文中代码会有详细介绍。 - @Data注解帮我们定义了一系列常用方法,如:getters、setters、hashcode、equals等 #### HTTP方法与Controller(动作) - 增加一篇Acticle,使用 POST 方法 - 删除一篇Acticle,使用 DELETE 方法,参数是id - 更新一篇Article,使用 PUT 方法,以id为主键进行更新 - 获取一篇Article,使用 GET 方法 ```java @Slf4j@RestController@RequestMapping("/api/v1")public class ArticleController { //获取一篇Article,使用GET方法,根据id查询一篇文章 //@RequestMapping(value = "/articles/{id}",method = RequestMethod.GET) @GetMapping("/articles/{id}") public AjaxResponse getArticle(@PathVariable("id") Long id){ //使用lombok提供的builder构建对象 Article article = Article.builder() .id(id) .author("zimug") .content("spring boot 从青铜到王者") .createTime(new Date()) .title("t1").build(); log.info("article:" + article); return AjaxResponse.success(article); } //增加一篇Article ,使用POST方法(RequestBody方式接收参数) //@RequestMapping(value = "/articles",method = RequestMethod.POST) @PostMapping("/articles") public AjaxResponse saveArticle(@RequestBody Article article, @RequestHeader String aaa){ //因为使用了lombok的Slf4j注解,这里可以直接使用log变量打印日志 log.info("saveArticle:" + article); return AjaxResponse.success(); } //增加一篇Article ,使用POST方法(RequestParam方式接收参数) /*@PostMapping("/articles") public AjaxResponse saveArticle(@RequestParam String author, @RequestParam String title, @RequestParam String content, @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") @RequestParam Date createTime){ log.info("saveArticle:" + createTime); return AjaxResponse.success(); }*/ //更新一篇Article,使用PUT方法,以id为主键进行更新 //@RequestMapping(value = "/articles",method = RequestMethod.PUT) @PutMapping("/articles") public AjaxResponse updateArticle(@RequestBody Article article){ if(article.getId() == null){ //article.id是必传参数,因为通常根据id去修改数据 //TODO 抛出一个自定义的异常 } log.info("updateArticle:" + article); return AjaxResponse.success(); } //删除一篇Article,使用DELETE方法,参数是id //@RequestMapping(value = "/articles/{id}",method = RequestMethod.DELETE) @DeleteMapping("/articles/{id}") public AjaxResponse deleteArticle(@PathVariable("id") Long id){ log.info("deleteArticle:" + id); return AjaxResponse.success(); }} ``` ### 统一规范接口响应的数据格式 统一所有开发人员响应前端请求的返回结果格式,减少前后端开发人员沟通成本,是一种RESTful接口标准化的开发约定 ```java @Datapublic class AjaxResponse { private boolean isok; //请求是否处理成功 private int code; //请求响应状态码(200、400、500) private String message; //请求结果描述信息 private Object data; //请求结果数据(通常用于查询操作) private AjaxResponse(){} //请求成功的响应,不带查询数据(用于删除、修改、新增接口) public static AjaxResponse success(){ AjaxResponse ajaxResponse = new AjaxResponse(); ajaxResponse.setIsok(true); ajaxResponse.setCode(200); ajaxResponse.setMessage("请求响应成功!"); return ajaxResponse; } //请求成功的响应,带有查询数据(用于数据查询接口) public static AjaxResponse success(Object obj){ AjaxResponse ajaxResponse = new AjaxResponse(); ajaxResponse.setIsok(true); ajaxResponse.setCode(200); ajaxResponse.setMessage("请求响应成功!"); ajaxResponse.setData(obj); return ajaxResponse; } //请求成功的响应,带有查询数据(用于数据查询接口) public static AjaxResponse success(Object obj,String message){ AjaxResponse ajaxResponse = new AjaxResponse(); ajaxResponse.setIsok(true); ajaxResponse.setCode(200); ajaxResponse.setMessage(message); ajaxResponse.setData(obj); return ajaxResponse; }} ``` ## JSON数据处理与接口测试 ### 序列化和反序列化 HttpMessageConverter #### 常用的json库 - 开源的 Jackson (默认) - Google Gson - 阿里巴巴 FastJson 序列化性能: FastJson > Jackson > Gson 反序列化性能:Gson 略好一些 常用的注解 ```java // 字段排序@JsonPropertyOrder(value = {"totalAmount","body"})// 不给前端返回这个字段@JsonIgnore// 使用注解定义的参数名返回给前端@JsonProperty("orderId")// 如果变量不是空 才返回 ,空值不返回@JsonInclude(JsonInclude.Include.NON_NULL)// 格式化字符串@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") ``` #### 序列化和反序列化 ```java // 序列化String jsonStr = objectMapper.writeValueAsString(user);log.info("jsonStr:{}",jsonStr);// 反序列化User user1 = objectMapper.readValue(jsonStr, User.class);log.info("user1:{}",user1); ``` ### 接口单元测试 - Junit(JUnit5) - Mockito - spring-boot-starter-test #### 添加依赖 ```xml org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine ``` #### 编写用例 ```java @Slf4jpublic class ArticleTestControllerTest { private static MockMvc mockMvc; @BeforeAll static void senUp() { mockMvc = MockMvcBuilders.standaloneSetup(new ArticleController()).build(); } @Test public void saveArticle() throws Exception { String article = "{\"id\":1,\"author\": \"yanglf\"," + "\"content\": \"spring boot 从青铜到王者\"," + " \"title\": \"t1\"}"; MvcResult mvcResult = mockMvc.perform( MockMvcRequestBuilders.request(HttpMethod.POST, "/api/v1/articles") .contentType("application/json") .content(article) ).andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.data.author").value("yanglf")) .andDo(print()) .andReturn(); mvcResult.getResponse().setCharacterEncoding("UTF-8"); log.info(mvcResult.getResponse().getContentAsString()); }} ``` #### 打包测试 ```sh # 打包的时候执行测试用例mvn clean && mvn package ``` ### Servlet 环境测试 上边的测试,没有真正启用容器,无法测试依赖注入,是否有问题 ```java @Slf4j@SpringBootTest// 自动构建 MockMvc@AutoConfigureMockMvc// 添加依赖注入@ExtendWith(SpringExtension.class)public class ArticleTestControllerTest2 { @Autowired private MockMvc mockMvc; @Test public void saveArticle() throws Exception { String article = "{\"id\":1,\"author\": \"yanglf\"," + "\"content\": \"spring boot 从青铜到王者\"," + " \"title\": \"t1\"}"; MvcResult mvcResult = mockMvc.perform( MockMvcRequestBuilders.request(HttpMethod.POST, "/api/v1/articles") .contentType("application/json") .content(article) ).andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.data.author").value("yanglf")) .andDo(print()) .andReturn(); mvcResult.getResponse().setCharacterEncoding("UTF-8"); log.info(mvcResult.getResponse().getContentAsString()); }} ``` 当代码里出现依赖注入才需要用这个方式,运行速度慢 ### Mockito测试 需要模拟多种情况 #### 未完成的接口 ```java public interface IArticleService { String saveArticle(Article article);}@Servicepublic class ArticleService implements IArticleService{ @Override public String saveArticle(Article article) { return null; }} ``` #### 修改 controller ```java @PostMapping("/articles") public AjaxResponse saveArticle(@RequestBody Article article, @RequestHeader(required = false) String aaa) { //因为使用了lombok的Slf4j注解,这里可以直接使用log变量打印日志 log.info("saveArticle:" + article); return AjaxResponse.success(articleService.saveArticle(article)); } ``` #### 测试未完成的接口 ```java @Slf4j@WebMvcTest(ArticleController.class)//@SpringBootTest// 自动构建 MockMvc@AutoConfigureMockMvc// 添加依赖注入@ExtendWith(SpringExtension.class)public class ArticleTestControllerTest3 { @Autowired private MockMvc mockMvc; @MockBean private IArticleService articleService; @Test public void saveArticle() throws Exception { String article = "{\"id\":1,\"author\": \"yanglf\"," + "\"content\": \"spring boot 从青铜到王者\"," + " \"title\": \"t1\"}"; ObjectMapper objectMapper=new ObjectMapper(); Article articleObj = objectMapper.readValue(article, Article.class); // saveArticle 还没有开发完 // 当执行 saveArticle 方法的时候,不真正执行,直接返回成功 when(articleService.saveArticle(articleObj)).thenReturn("ok"); MvcResult mvcResult = mockMvc.perform( MockMvcRequestBuilders.request(HttpMethod.POST, "/api/v1/articles") .contentType("application/json") .content(article) ).andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.data").value("ok")) .andDo(print()) .andReturn(); mvcResult.getResponse().setCharacterEncoding("UTF-8"); log.info(mvcResult.getResponse().getContentAsString()); }} ``` 使用 `@WebMvcTest`代替 `@SpringBootTest` ,加载需要测试的bean,可以提高执行速度 #### 补充 ```java // 模拟 get 请求mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}",userId));// 模拟 post 请求mockMvc.perform(MockMvcRequestBuilders.post("/users",user));// 模拟文件上传 mockMvc.perform(MockMvcRequestBuilders.multipart("/users").file("fileName","file".getBytes("UTF-8")));// 模拟 session 和 cookiemockMvc.perform(MockMvcRequestBuilders.get("uri").sessionAttr("name","value"));mockMvc.perform(MockMvcRequestBuilders.get("uri").cookie(new Cookie("name","value")));mockMvc.perform(MockMvcRequestBuilders.get("uri") .contentType("application/x-www-form-urlencoded") .accept("application/json") .header("", "")); ``` ## API 文档构建 ### Swagger构建 - 代码变,文档变 - 跨语言,支持40多种语言 - Swagger UI 呈现出来的是一份可交互的API文档 - 将文档规范导入相关的SoapUI,自动的创建对应的测试 #### 引入依赖 ```xml io.springfox springfox-swagger2 2.6.1 io.springfox springfox-swagger-ui 2.6.1 ``` #### 编写配置类 ```java @Configuration@EnableSwagger2public class SwaggerConfig { @Bean public Docket createRestApi(){ return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.yanglf.demo1")) .paths(PathSelectors.regex("/api/.*")) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("boot launch 项目API文档") .description("简单的rest风格接口") .termsOfServiceUrl("http://yanglf.xyz") .version("1.0") .build(); }} ``` 访问项目地址: http://localhost:8080/swagger-ui.html #### 接口中文注解 ```java @ApiOperation(value = "添加文章", notes = "添加新文章", tags = "Article", httpMethod = "POST") @ApiImplicitParams({ @ApiImplicitParam(name = "title", value = "文章标题", required = true, dataType = "String"), @ApiImplicitParam(name = "author", value = "文章作者", required = true, dataType = "String"), @ApiImplicitParam(name = "content", value = "文章内容", required = true, dataType = "String") }) @ApiResponses({ @ApiResponse(code = 200, message = "成功", response = AjaxResponse.class) }) @PostMapping("/articles2") public AjaxResponse saveArticle(@RequestParam String author, @RequestParam String title, @RequestParam String content, @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") @RequestParam Date createTime) { log.info("saveArticle:" + createTime); return AjaxResponse.success(); } ``` #### 实体类注解 ```java @Data@ApiModel(value = "通用响应数据结构类")public class AjaxResponse { @ApiModelProperty(value = "请求是否成功") private boolean isok; //请求是否处理成功 @ApiModelProperty(value = "请求响应状态码",example = "200、400、500") private int code; //请求响应状态码(200、400、500) @ApiModelProperty(value = "请求结果描述信息") private String message; //请求结果描述信息 @ApiModelProperty(value = "请求结果数据(通常用于查询操作)") private Object data; //请求结果数据(通常用于查询操作) ``` ### Swagger离线文档 #### 添加依赖 ```xml io.github.swagger2markup swagger2markup 1.3.1 io.swagger swagger-core 1.5.16 io.swagger swagger-models 1.5.16 ``` #### 编写导出用例 ```java @Slf4j@ExtendWith(SpringExtension.class)@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)public class SwaggerExportTest { @Test public void generateAsciiDocs() throws Exception { // 输出 Ascii 格式 Swagger2MarkupConfig config=new Swagger2MarkupConfigBuilder() // 设置生成格式 .withMarkupLanguage(MarkupLanguage.MARKDOWN) // 设置生成语言 .withOutputLanguage(Language.ZH) .withPathsGroupedBy(GroupBy.TAGS) .withGeneratedExamples() .withoutInlineSchema() .build(); Swagger2MarkupConverter.from(new URI("http://localhost:8888/v2/api-docs")) .withConfig(config) .build() .toFile(Paths.get("src/main/resources/docs/asciidoc")); }} ``` #### maven 插件生成 ```xml org.springframework.boot spring-boot-maven-plugin org.asciidoctor asciidoctor-maven-plugin 1.5.6 src/main/resources/docs src/main/resources/html html coderay left true ``` 点击maven面板,插件 `asciidoctor` 下的 `asciidoctor:process:asciidoc` ### OpenApi构建文档 需要移除之前的swagger依赖 #### 添加依赖 ```xml org.springdoc springdoc-openapi-ui 1.4.0 ``` 访问: http://localhost:8080/swagger-ui.html #### 接口分组配置 ```java @Configurationpublic class OpenAPIConfig { @Bean public GroupedOpenApi restApi(){ return GroupedOpenApi.builder() .group("rest-api") .pathsToMatch("/rest/**") .build(); } @Bean public GroupedOpenApi helloApi(){ return GroupedOpenApi.builder() .group("hello-api") .pathsToMatch("/hello/**") .build(); }} ``` #### OpenApi和swagger注解 | Swagger2注解 | OpenApi3(swagger3)注解 | | ------------------ | ------------------------------------------------------------ | | @ApiParam | @Parameter | | @ApiOperation | @Operation | | @Api | @Tag | | @ApilmplicitParams | @Parameters | | @ApilmplicitParam | @Parameter | | @Apilgnore | @Parameter(hidden = true) or @Operation(hidden = true ) or @Hidden | | @ApiModel | @Schema | | @ApiModelProperty | @Schema | # 配置管理 ## YML 文件配置 ### YML 语法 ```yaml family: # 一般不需要加引号,除非有转义符 family-name: 'hello family' father: name: yanglf # 随机整数 age: ${random.int} mother: alias: - lovely - ailice child: # 指定默认值 name: ${family.father.name:ylf} age: 5 friends: - hobby: football sex: male - hobby: baskeyball sex: female ``` **占位符** - `${random.ivalue}` : 类似uuid的随机数,没有"-"连接符 - `${random.int}` : 随机取整型范围内的一个值 - `${random.long}` : 随机取长整型范围内的一个值 - `${random.long(100,200)}` : 随机生成长整型100-200范围内的一个值 - `${random.uuid}` : 生成一个uuid,有短杠连接 - `${random.int(10)}` : 随机生成10以内的数 - `${random.int(100,200)}` : 随机生成100-200范围内的数 ## 绑定变量的方式 - @Value - @ConfigurationProperties ### @Value ```java @Data@Componentpublic class Family { @Value(value = "${family.family-name}") private String familyName;} ``` 编写测试类 ```java @Slf4j@ExtendWith(SpringExtension.class)@SpringBootTestpublic class ValueBindTest { @Autowired private Family family; @Test public void valueBindTest () throws Exception{ log.info("family:{}",family); }} ``` ### @ConfigurationProperties ```java @Data@Component@ConfigurationProperties("family")public class Family {// @Value(value = "${family.family-name}") private String familyName; private Father father; private Mother mother; private Child child;} ``` ## 配置绑定值校验 - JSR303 - Hibernate-validator ### 添加依赖 ```xml org.hibernate hibernate-validator 5.2.4.Final ``` ### 校验类 ```java @Datapublic class Father { private String name; @Min(21) private Integer age;}@Data@Component@ConfigurationProperties("family")@Validatedpublic class Family {// @Value(value = "${family.family-name}") private String familyName; private Father father; private Mother mother; private Child child;} ``` 需要添加 `@Validated` ,启动的时候,如果配置的参数不符合要求,会报错 ### 常用校验方法 ```java @Size(min = 6,max = 20,message = "密码长度只能在6-20位")@Pattern(regexp = "/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$/",message = "请输入正确的邮箱")@Length(min = 5,max = 20,message = "用户名长度必须在5到10之间")@Email(message = "请输入正确的邮箱")@NotNull(message = "用户名不能为空")@Max(value = 100,message = "年龄不能大于100岁")@Min(value = 18,message = "必须年满18岁")@AssertTrue(message = "bln4 must is ture")@AssertFalse(message = "bln4 must is false")@DecimalMin(value = "100",message = "decimal值最小100")@DecimalMax(value = "100",message = "decimal值最大100") ``` ## 加载旧项目的方式 - 使用 `@PropertySource` 加载自定义的yml或者properties文件 - 使用 `@ImportResource` 加载 Spring 的 xml配置文件 ### @PropertySource 默认只支持 properties 文件加载,如果需要支持 yml 需要添加以下类 ```java public class MixPropertySourceFactory extends DefaultPropertySourceFactory { @Override public PropertySource createPropertySource(@Nullable String name, EncodedResource resource) throws IOException { String sourceName = name != null ? name : resource.getResource().getFilename(); if (sourceName != null && (sourceName.endsWith(".yml") || sourceName.endsWith(".yaml"))) { // yaml 转换 properties Properties propertiesFromYml = loadYml(resource); return new PropertiesPropertySource(sourceName, propertiesFromYml); } else { return super.createPropertySource(name, resource); } } private Properties loadYml(EncodedResource resource) { YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); factory.setResources(resource.getResource()); factory.afterPropertiesSet(); return factory.getObject(); }} ``` 使用 ```java @Data@Component@ConfigurationProperties("family")@Validated@PropertySource(value = "classpath:family.yml",factory = MixPropertySourceFactory.class)public class Family {// @Value(value = "${family.family-name}") private String familyName; private Father father; private Mother mother; private Child child;} ``` ### @ImportResource 编写测试的 `bean.xml` ```xml ``` 使用 @ImportResource 导入配置文件 ```java @SpringBootApplication@ImportResource(locations = "classpath:bean.xml")public class Demo2Application { public static void main(String[] args) { SpringApplication.run(Demo2Application.class, args); }} ``` 测试 bean 是否注入进来 ```java @Slf4j@ExtendWith(SpringExtension.class)@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)public class ImportResourceTest { @Autowired private ConfigurableApplicationContext ioc; @Test public void testImport() throws Exception{ boolean isImport = ioc.containsBean("user"); log.info("isImport:{}",isImport); }} ``` ## SpEl 表达式绑定配置 ### 编写测试的配置文件 ```properties employee.names:james|curry|yanglf|赵丽颖employee.type=球员,教练,经理,演员employee.age:{one:'27',two:'36',three:'18',four:'26'} ``` ### 测试类 ```java @Data@Configuration@PropertySource(value = "classpath:employee.properties", encoding = "utf-8")public class Employee { @Value("#{'${employee.names}'.split('\\|')}") private List employeeNames; // 读取指定元素 @Value("#{'${employee.names}'.split('\\|')[0]}") private String firstEmployeeName; @Value("#{${employee.age}}") private Map employeeAge; // 读取指定元素 @Value("#{${employee.age}.two}") private Integer employeeAgeTwo; @Value("#{${employee.age}['four']}") private Integer employeeAgeFour; // 设置默认值 @Value("#{${employee.age}['five'] ?: 30 }") private Integer employeeAgeFive; @Value("#{systemProperties['java.home']}") private String javaHome; @Value("#{systemProperties['user.dir']}") private String userDir;} ``` 如果有乱码,需要修改 `设置 -> File Encodings` 下边的编码格式为 utf-8 ## 不同环境不同配置 ### 编写环境配置文件 - application-dev.yml ```yml server: port: 8899 ``` - application-test.yml ```yml server: port: 9090 ``` - application.prod ```yml server: port: 8080 ``` ### 启动脚本 - application.yml ```yml spring: profiles: active: dev ``` - idea 中配置 ```sh VM options: -Dspring.profiles.active=prod或者Program arguments: --spring.profiles.active=prod ``` - 命令 ```sh java -jar ./demo.jar --spring.profiles.active=prod ``` ## 配置优先级 ### 配置文件加载位置 - file: ./config/ 当前项目路径config目录下 优先级:1 - file: ./ 当前项目路径下 优先级:2 - classpath: /config/ 类路径config目录下 优先级: 3 - classpath: / 类路径下 优先级: 4 优先级越小越高 自定义加载路径 优先级最高 ```sh java -jar demo.jar --spring.config.location=D:/application.yml ``` ## 配置文件敏感字段加密 官网: www.jasypt.org 下载 jasypt-1.9.2.jar jar包 ### 编写加密脚本 `jasypt.bat` ```bash @echo offset/p input=待加密的明文字符串:set/p password=加密密钥(盐值):echo 加密中...java -cp jasypy-1.9.2.jar org.jasypt.inft.cli.JasyptPBEStringEncryptionCIL ^input=%input% password=%password% ^algorithm=PBEWithMD5AndDECpause ``` ### 替换加密的配置 ```yml # 配置加密字符串family: familyName: ENC(加密后的字符串)# 配置密钥 生产环境通过 环境变量、命令行 形式配置jasypt: encryptor: password: 123456 ``` 使用命令行配置密钥 ```sh java -jar demo.jar --jasypt.encryptor.password=123456 ``` ### 解密 引入依赖 spring boot 自动解密 ```xml com.github.ulisesbocchio jasypt-spring-boot-starter 1.18 ``` # 数据库操作与分布式事务 ## Spring JDBC ### 之前 ```java try{ // 加载数据库驱动 Class.forName(driver); // 获取数据库连接 conn = DriverManager.getConnection(url,username,password); // 获取数据库操作对象 stmt = conn.getStatement(); // 定义操作的 sql 语句 String sql = " select * from user where id=6 "; // 执行数据库操作 rs = stmt.executeQuery(sql); // 获取并操作结果集 while(rs.next){ // 解析结果集 }}catch{ // 错误信息}finally{ // 关闭资源} ``` ### 集成 springboot #### 创建表 ```sql CREATE TABLE `article` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `author` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '作者', `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '文章标题', `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '文章内容', `create_time` datetime DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; ``` #### 引入依赖 ```xml org.springframework.boot spring-boot-starter-jdbc mysql mysql-connector-java ``` #### 配置文件 ```yml server: port: 8080spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 datasource: url: jdbc:mysql://localhost:3306/bootdb?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: admin123 driver-class-name: com.mysql.cj.jdbc.Driver ``` #### 编写操作类 ```java @Repositorypublic class ArticleJDBCDAO { @Autowired private JdbcTemplate jdbcTemplate; // 新增一篇文章 public void saveArticle(Article article) { jdbcTemplate.update("INSERT INTO article(author,title,content,create_time) VALUES (?,?,?,?)", article.getAuthor(), article.getTitle(), article.getContent(), article.getContent()); } // 删除一篇文章 public void deleteById(Long id) { jdbcTemplate.update("DELETE FROM article WHERE id=?", id); } // 修改一篇文章 public void updateById(Article article) { jdbcTemplate.update("UPDATE article SET author=?,title=?,content=?,create_time=?", article.getAuthor(), article.getTitle(), article.getContent(), article.getContent()); } // 查询一篇文章 public Article findById(Long id) { return jdbcTemplate.queryForObject("SELECT * FROM article WHERE id=?", new Object[]{id}, new BeanPropertyRowMapper<>(Article.class)); } // 查询所有文章 public List
findAll() { return jdbcTemplate.query("SELECT * FROM article", new BeanPropertyRowMapper<>(Article.class)); }} ``` #### 服务后端分层开发和事务 服务接口 ```java public interface IArticleService { void saveArticle(Article article); void updateById(Article article); void deleteById(Long id); Article findById(Long id); List
findAll();} ``` 接口实现 ```java @Servicepublic class ArticleServiceImpl implements IArticleService { @Autowired private ArticleJDBCDAO articleJDBCDAO; @Override public void saveArticle(Article article) { articleJDBCDAO.saveArticle(article); } @Override @Transactional public void updateById(Article article) { if (article.getId() == null) { //article.id是必传参数,因为通常根据id去修改数据 //TODO 抛出一个自定义的异常 } // articleJDBCDAO.updateById(article); articleJDBCDAO.deleteById(article.getId()); articleJDBCDAO.saveArticle(article); int i=10/0; } @Override public void deleteById(Long id) { articleJDBCDAO.deleteById(id); } @Override public Article findById(Long id) { return articleJDBCDAO.findById(id); } @Override public List
findAll() { return articleJDBCDAO.findAll(); }} ``` 修改 controller ```java @Slf4j@RestController@RequestMapping("/api/v1")public class ArticleController { @Autowired private IArticleService articleService; //获取一篇Article,使用GET方法,根据id查询一篇文章 @GetMapping("/articles/{id}") public AjaxResponse getArticle(@PathVariable("id") Long id) { Article article = articleService.findById(id); log.info("article:" + article); return AjaxResponse.success(article); } // 查询所有文章 @GetMapping("/articles") public AjaxResponse getArticle() { List
articles = articleService.findAll(); log.info("articles:" + articles); return AjaxResponse.success(articles); } //增加一篇Article ,使用POST方法(RequestBody方式接收参数) @PostMapping("/articles") public AjaxResponse saveArticle(@RequestBody Article article, @RequestHeader(required = false) String aaa) { //因为使用了lombok的Slf4j注解,这里可以直接使用log变量打印日志 articleService.saveArticle(article); log.info("saveArticle:" + article); return AjaxResponse.success(article); } //更新一篇Article,使用PUT方法,以id为主键进行更新 @PutMapping("/articles") public AjaxResponse updateArticle(@RequestBody Article article) { articleService.updateById(article); log.info("updateArticle:" + article); return AjaxResponse.success(); } //删除一篇Article,使用DELETE方法,参数是id @DeleteMapping("/articles/{id}") public AjaxResponse deleteArticle(@PathVariable("id") Long id) { articleService.deleteById(id); log.info("deleteArticle:" + id); return AjaxResponse.success(); }} ``` ### Spring JDBC多数据源 #### 修改数据源 ```yaml spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 datasource: primary: jdbc-url: jdbc:mysql://localhost:3306/bootdb?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: admin123 driver-class-name: com.mysql.cj.jdbc.Driver secondary: jdbc-url: jdbc:mysql://localhost:3306/bootdb2?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: admin123 driver-class-name: com.mysql.cj.jdbc.Driver ``` #### 增加配置类 ```java @Configurationpublic class DataSourceConfig { @Primary @Bean("primaryDataSource") @ConfigurationProperties("spring.datasource.primary") public DataSource primaryDataSource(){ return DataSourceBuilder.create().build(); } @Bean("secondaryDataSource") @ConfigurationProperties("spring.datasource.secondary") public DataSource secondaryDataSource(){ return DataSourceBuilder.create().build(); } @Bean("primaryJdbcTemplate") public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource") DataSource primaryDataSource){ return new JdbcTemplate(primaryDataSource); } @Bean("secondaryJdbcTemplate") public JdbcTemplate secondaryDataSource(@Qualifier("secondaryDataSource") DataSource secondaryDataSource){ return new JdbcTemplate(secondaryDataSource); }} ``` #### 修改测试类 ```java @Repositorypublic class ArticleJDBCDAO { @Autowired private JdbcTemplate primaryJdbcTemplate; // 新增一篇文章 public void saveArticle(Article article, JdbcTemplate jdbcTemplate) { if (jdbcTemplate == null) { jdbcTemplate = primaryJdbcTemplate; } jdbcTemplate.update("INSERT INTO article(author,title,content,create_time) VALUES (?,?,?,?)", article.getAuthor(), article.getTitle(), article.getContent(), article.getCreateTime()); } // 删除一篇文章 public void deleteById(Long id,JdbcTemplate jdbcTemplate) { if (jdbcTemplate == null) { jdbcTemplate = primaryJdbcTemplate; } jdbcTemplate.update("DELETE FROM article WHERE id=?", id); } // 修改一篇文章 public void updateById(Article article,JdbcTemplate jdbcTemplate) { if (jdbcTemplate == null) { jdbcTemplate = primaryJdbcTemplate; } jdbcTemplate.update("UPDATE article SET author=?,title=?,content=?,create_time=? WHERE id=?", article.getAuthor(), article.getTitle(), article.getContent(), article.getCreateTime(), article.getId()); } // 查询一篇文章 public Article findById(Long id,JdbcTemplate jdbcTemplate) { if (jdbcTemplate == null) { jdbcTemplate = primaryJdbcTemplate; } return jdbcTemplate.queryForObject("SELECT * FROM article WHERE id=?", new Object[]{id}, new BeanPropertyRowMapper<>(Article.class)); } // 查询所有文章 public List
findAll(JdbcTemplate jdbcTemplate) { if (jdbcTemplate == null) { jdbcTemplate = primaryJdbcTemplate; } return jdbcTemplate.query("SELECT * FROM article", new BeanPropertyRowMapper<>(Article.class)); }} ``` 修改 service ```java @Autowiredprivate JdbcTemplate secondaryJdbcTemplate;@Overridepublic void saveArticle(Article article) { articleJDBCDAO.saveArticle(article, null); articleJDBCDAO.saveArticle(article,secondaryJdbcTemplate); } ``` ### 分布式事务 - XA规范-两阶段提交 - JTA规范 - Atomikos #### 新增依赖 ```xml org.springframework.boot spring-boot-starter-jta-atomikos ``` #### 修改数据源配置 ```yml primarydb: uniqueResourceName: primary xaDataSourceClassName: com.mysql.cj.jdbc.MysqlXADataSource xaProperties: url: jdbc:mysql://localhost:3306/bootdb?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai user: root password: admin123 exclusiveConnectionMode: true minPoolSize: 3 maxPoolSize: 10 testQuery: SELECT 1 FROM dualsecondarydb: uniqueResourceName: secondary xaDataSourceClassName: com.mysql.cj.jdbc.MysqlXADataSource xaProperties: url: jdbc:mysql://localhost:3306/bootdb2?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai user: root password: admin123 exclusiveConnectionMode: true minPoolSize: 3 maxPoolSize: 10 testQuery: SELECT 1 FROM dual ``` #### 修改配置类 ```java @Configurationpublic class DataSourceConfig { @Primary @Bean(name = "primarydb",initMethod = "init",destroyMethod = "close") @ConfigurationProperties("primarydb") public DataSource primaryDataSource() { return new AtomikosDataSourceBean(); } @Bean(name = "secondarydb",initMethod = "init",destroyMethod = "close") @ConfigurationProperties("secondarydb") public DataSource secondaryDataSource() { return new AtomikosDataSourceBean(); } @Bean("primaryJdbcTemplate") public JdbcTemplate primaryJdbcTemplate(@Qualifier("primarydb") DataSource primaryDataSource) { return new JdbcTemplate(primaryDataSource); } @Bean("secondaryJdbcTemplate") public JdbcTemplate secondaryDataSource(@Qualifier("secondarydb") DataSource secondaryDataSource) { return new JdbcTemplate(secondaryDataSource); }} ``` #### 新增事务管理器 ```java @Configurationpublic class TransactionManagerConfig { @Bean public UserTransaction userTransaction() throws SystemException { UserTransaction userTransaction = new UserTransactionImp(); userTransaction.setTransactionTimeout(10000); return userTransaction; } @Bean(name = "atomikostransactionManager", initMethod = "init", destroyMethod = "close") public TransactionManager atomikostransactionManager() throws Throwable { UserTransactionManager userTransactionManager = new UserTransactionManager(); userTransactionManager.setForceShutdown(false); return userTransactionManager; } @Bean @DependsOn({"userTransaction", "atomikostransactionManager"}) public PlatformTransactionManager transactionManager() throws Throwable { UserTransaction userTransaction = userTransaction(); JtaTransactionManager manager = new JtaTransactionManager(userTransaction, atomikostransactionManager()); return manager; }} ``` #### 测试事务 ```java @Override @Transactional public void saveArticle(Article article) { articleJDBCDAO.saveArticle(article, null); articleJDBCDAO.saveArticle(article, secondaryJdbcTemplate); int i = 1 / 0; } ``` `atomikos`: 优点: 支持分布式事务 缺点: 性能开销大,不适合高并发场景 ## Bean 赋值转换 - `PO`: 持久对象,类定义成员变量和数据库字段一致 - `BO`: 通常是多个PO的组合体 - `VO`: 与前端展示的数据结构对应 ### BeanUtils 性能比 Dozer 好,不支持不同类型数据转换 ```java ArticleVo articleVo = new ArticleVo();BeanUtils.copyProperties(article,articleVo); ``` ### Dozer ```java Mapper mapper = DozerBeanMapperBuilder.buildDefault();ArticleVo articleVo = mapper.map(article,ArticleVo.class); ``` 工具类 ```java @Componentpublic class GeneralConvertor { @Resource Mapper mapper; /** * List 实体类 转换器 * * @param source 原数据 * @param clz 转换类型 * @param * @param * @return */ public List convertor(List source, Class clz) { if (source == null) { return null; } List map = new ArrayList<>(); for (S s : source) { map.add(mapper.map(s, clz)); } return map; } /** * Set 实体类 深度转换器 * * @param source 原数据 * @param clz 目标对象 * @param * @param * @return */ public Set convertor(Set source, Class clz) { if (source == null) { return null; } Set set = new TreeSet<>(); for (S s : source) { set.add(mapper.map(s, clz)); } return set; } /** * 实体类 深度转换器 * * @param source * @param clz * @param * @param * @return */ public T convertor(S source, Class clz) { if (source == null) { return null; } return mapper.map(source, clz); } public void convertor(Object source, Object object) { mapper.map(source, object); } public void copyConvertor(T source, Object object) { mapper.map(source, object); }} ``` ## Spring Data Jpa ### 环境搭建 #### 新增依赖 ```xml org.springframework.boot spring-boot-starter-data-jpa mysql mysql-connector-java com.github.dozermapper dozer-spring-boot-starter 6.2.0 ``` #### 新增配置 ```yml spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 datasource: url: jdbc:mysql://localhost:3306/bootdb?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: admin123 driver-class-name: com.mysql.cj.jdbc.Driver jpa: # 使用 innodb 数据库 支持事务 database-platform: org.hibernate.dialect.MySql5InnoDBDialect hibernate: # create 每次都会删除上一次的表,根据定义的 model 重新创建 # create-drop 每次 session 关闭 删除表,创建 session 创建表 # update 数据库没有创建,数据库存在,更新表结构,不会删除以前的数据 # validate 每次加载时候,验证数据库结构和定义的model是否一致,不一致报错,比较安全 ddl-auto: validate database: mysql show-sql: true ``` #### PO创建 ```java @Data@Builder@NoArgsConstructor@AllArgsConstructor@Entity@Table(name = "article")public class Article { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false,length = 32) private String author; @Column(nullable = false,length = 32,unique = true) private String title; @Column(length = 512) private String content; private Date createTime;} ``` #### Dao 创建 ```java public interface ArticleRepository extends JpaRepository { // 关键词查询 List
findArticleByAuthorOrderByCreateTimeDesc(String author);} ``` #### 测试 ```java @Override@Transactionalpublic void saveArticle(ArticleVo articleVo) { Mapper mapper = DozerBeanMapperBuilder.buildDefault(); Article article = mapper.map(articleVo,Article.class); article.setCreateTime(new Date()); articleRepository.save(article);} ``` ### 多数据源 - JdbcTemplate 实现 - 分包实现 > 分包实现 #### 修改数据源 ```yml spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 datasource: primary: jdbc-url: jdbc:mysql://localhost:3306/bootdb?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: admin123 driver-class-name: com.mysql.cj.jdbc.Driver secondary: jdbc-url: jdbc:mysql://localhost:3306/bootdb2?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: admin123 driver-class-name: com.mysql.cj.jdbc.Driver jpa: # 使用 innodb 数据库 支持事务 database-platform: org.hibernate.dialect.MySQL5InnoDBDialect hibernate: # create 每次都会删除上一次的表,根据定义的 model 重新创建 # create-drop 每次 session 关闭 删除表,创建 session 创建表 # update 数据库没有创建,数据库存在,更新表结构,不会删除以前的数据 # validate 每次加载时候,验证数据库结构和定义的model是否一致,不一致报错,比较安全 ddl-auto: create database: mysql show-sql: true ``` #### 新增数据源配置类 JPAPrimaryConfig 只需要修改扫描的包,其他的不动 ```java @Configuration@EnableTransactionManagement@EnableJpaRepositories( entityManagerFactoryRef = "entityManagerFactoryPrimary", transactionManagerRef = "transactionManagerPrimary", basePackages = "com.yanglf.demo4.dao.test1")public class JPAPrimaryConfig { @Autowired private JpaProperties jpaProperties; @Autowired private HibernateProperties hibernateProperties; @Primary @Bean("primaryDataSource") @ConfigurationProperties(prefix = "spring.datasource.primary") public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } @Primary @Bean("primaryEntityManager") public EntityManager entityManager(EntityManagerFactoryBuilder builder) { return entityManagerFactoryPrimary(builder).getObject().createEntityManager(); } @Primary @Bean(name = "entityManagerFactoryPrimary") public LocalContainerEntityManagerFactoryBean entityManagerFactoryPrimary(EntityManagerFactoryBuilder builder) { Map properties = hibernateProperties.determineHibernateProperties( jpaProperties.getProperties(), new HibernateSettings() ); return builder.dataSource(primaryDataSource()) .properties(properties) .packages("com.yanglf.demo4.dao.test1") .persistenceUnit("primaryPersistenceUnit") .build(); } @Primary @Bean(name = "transactionManagerPrimary") public PlatformTransactionManager transactionManagerPrimary(EntityManagerFactoryBuilder builder) { return new JpaTransactionManager(entityManagerFactoryPrimary(builder).getObject()); }} ``` JPASecondaryConfig ```java @Configuration@EnableTransactionManagement@EnableJpaRepositories( entityManagerFactoryRef = "entityManagerFactorySecondary", transactionManagerRef = "transactionManagerSecondary", basePackages = "com.yanglf.demo4.dao.test2")public class JPASecondaryConfig { @Autowired private JpaProperties jpaProperties; @Autowired private HibernateProperties hibernateProperties; @Bean("secondaryDataSource") @ConfigurationProperties(prefix = "spring.datasource.secondary") public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } @Bean("secondaryEntityManager") public EntityManager entityManager(EntityManagerFactoryBuilder builder) { return entityManagerFactorySecondary(builder).getObject().createEntityManager(); } @Bean(name = "entityManagerFactorySecondary") public LocalContainerEntityManagerFactoryBean entityManagerFactorySecondary(EntityManagerFactoryBuilder builder) { Map properties = hibernateProperties.determineHibernateProperties( jpaProperties.getProperties(), new HibernateSettings() ); return builder.dataSource(secondaryDataSource()) .properties(properties) .packages("com.yanglf.demo4.dao.test2") .persistenceUnit("primaryPersistenceUnit") .build(); } @Bean(name = "transactionManagerSecondary") public PlatformTransactionManager transactionManagerSecondary(EntityManagerFactoryBuilder builder) { return new JpaTransactionManager(entityManagerFactorySecondary(builder).getObject()); }} ``` #### 创建分包实体类和DAO com.yanglf.demo4.dao.test1 ```java @Data@Builder@NoArgsConstructor@AllArgsConstructor@Entity@Table(name = "article")public class Article { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column(nullable = false,length = 32) private String author; @Column(nullable = false,length = 32,unique = true) private String title; @Column(length = 512) private String content; private Date createTime;}public interface ArticleRepository extends JpaRepository { /** * @param author * @return */ List
findArticleByAuthorOrderByCreateTimeDesc(String author);} ``` com.yanglf.demo4.dao.test2 ```java @Data@Builder@NoArgsConstructor@AllArgsConstructor@Entity@Table(name = "message")public class Message { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column(nullable = false,length = 32,unique = true) private String title; @Column(length = 512) private String content; private Date createTime;}public interface MessageRepository extends JpaRepository { /** * @param title * @return */ List findMessageByTitleOrderByCreateTimeDesc(String title);} ``` #### 测试 ```java @Autowired private ArticleRepository articleRepository; @Autowired private MessageRepository messageRepository;@Override @Transactional public void saveArticle(ArticleVo articleVo) { Article article = generalConvertor.convertor(articleVo, Article.class); article.setCreateTime(new Date()); articleRepository.save(article); Message message=new Message(); message.setTitle("保存文章成功"); message.setContent("保存:【"+articleVo.getAuthor()+"】的文章:{"+articleVo.getTitle()+"}"); message.setCreateTime(new Date()); messageRepository.save(message); } ``` ### Atomikos 分布式事务 #### 新增依赖 ```xml org.springframework.boot spring-boot-starter-jta-atomikos ``` #### 新增配置 ```yaml spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 datasource: primary: # 使用 url 而不是 jdbc-url url: jdbc:mysql://localhost:3306/bootdb?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: admin123 driver-class-name: com.mysql.cj.jdbc.Driver secondary: url: jdbc:mysql://localhost:3306/bootdb2?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: admin123 driver-class-name: com.mysql.cj.jdbc.Driver jpa: # 使用 innodb 数据库 支持事务 database-platform: org.hibernate.dialect.MySQL5InnoDBDialect hibernate: # create 每次都会删除上一次的表,根据定义的 model 重新创建 # create-drop 每次 session 关闭 删除表,创建 session 创建表 # update 数据库没有创建,数据库存在,更新表结构,不会删除以前的数据 # validate 每次加载时候,验证数据库结构和定义的model是否一致,不一致报错,比较安全 ddl-auto: create database: mysql show-sql: true jta: atomikos: datasource: max-pool-size: 20 borrow-connection-timeout: 60 connectionfactory: borrow-connection-timeout: 60 max-pool-size: 20 ``` #### 事务管理器 ```java public class AtomikosJtaPlatform extends AbstractJtaPlatform { static TransactionManager transactionManager; static UserTransaction userTransaction; @Override protected TransactionManager locateTransactionManager() { return transactionManager; } @Override protected UserTransaction locateUserTransaction() { return userTransaction; }} ``` #### 配置数据源 删除多数据源配置 数据源: `primary` ```java @Configuration@EnableTransactionManagement@EnableJpaRepositories( entityManagerFactoryRef = "primaryEntityManager", transactionManagerRef = "transactionManager", basePackages = "com.yanglf.demo4.dao.test1")public class JPAPrimaryConfig { @Autowired private JpaVendorAdapter jpaVendorAdapter; @Primary @Bean("primaryDataSourceProperties") @ConfigurationProperties(prefix = "spring.datasource.primary") public DataSourceProperties primaryDataSourceProperties() { return new DataSourceProperties(); } @Primary @Bean(value = "primaryDataSource", initMethod = "init", destroyMethod = "close") public DataSource primaryDataSource() throws SQLException { MysqlXADataSource mysqlXADataSource = new MysqlXADataSource(); mysqlXADataSource.setURL(primaryDataSourceProperties().getUrl()); mysqlXADataSource.setPinGlobalTxToPhysicalConnection(true); mysqlXADataSource.setPassword(primaryDataSourceProperties().getPassword()); mysqlXADataSource.setUser(primaryDataSourceProperties().getUsername()); AtomikosDataSourceBean xaDataSourceBean = new AtomikosDataSourceBean(); xaDataSourceBean.setXaDataSource(mysqlXADataSource); xaDataSourceBean.setUniqueResourceName("primary"); xaDataSourceBean.setBorrowConnectionTimeout(60); xaDataSourceBean.setMaxPoolSize(20); return xaDataSourceBean; } @Primary @Bean(name = "primaryEntityManager") @DependsOn("transactionManager") public LocalContainerEntityManagerFactoryBean primaryEntityManager() throws Throwable { Map properties = new HashMap(); properties.put("hibernate.transaction.jta.platform", AtomikosJtaPlatform.class.getName()); properties.put("javax.persistence.transactionType", "JTA"); LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean(); entityManager.setJtaDataSource(primaryDataSource()); entityManager.setJpaVendorAdapter(jpaVendorAdapter); entityManager.setPackagesToScan("com.yanglf.demo4.dao.test1"); entityManager.setPersistenceUnitName("primaryPersistenceUnit"); entityManager.setJpaPropertyMap(properties); return entityManager; }} ``` 数据源: `secondary` ```java @Configuration@EnableTransactionManagement@EnableJpaRepositories( entityManagerFactoryRef = "secondaryEntityManager", transactionManagerRef = "transactionManager", basePackages = "com.yanglf.demo4.dao.test2")public class JPASecondaryConfig { @Autowired private JpaVendorAdapter jpaVendorAdapter; @Bean("secondaryDataSourceProperties") @ConfigurationProperties(prefix = "spring.datasource.secondary") public DataSourceProperties secondaryDataSourceProperties() { return new DataSourceProperties(); } @Bean(value = "secondaryDataSource", initMethod = "init", destroyMethod = "close") public DataSource secondaryDataSource() throws SQLException { MysqlXADataSource mysqlXADataSource = new MysqlXADataSource(); mysqlXADataSource.setURL(secondaryDataSourceProperties().getUrl()); mysqlXADataSource.setPinGlobalTxToPhysicalConnection(true); mysqlXADataSource.setPassword(secondaryDataSourceProperties().getPassword()); mysqlXADataSource.setUser(secondaryDataSourceProperties().getUsername()); AtomikosDataSourceBean xaDataSourceBean = new AtomikosDataSourceBean(); xaDataSourceBean.setXaDataSource(mysqlXADataSource); xaDataSourceBean.setUniqueResourceName("secondary"); xaDataSourceBean.setBorrowConnectionTimeout(60); xaDataSourceBean.setMaxPoolSize(20); return xaDataSourceBean; } @Bean(name = "secondaryEntityManager") @DependsOn("transactionManager") public LocalContainerEntityManagerFactoryBean secondaryEntityManager() throws Throwable { Map properties = new HashMap(); properties.put("hibernate.transaction.jta.platform", AtomikosJtaPlatform.class.getName()); properties.put("javax.persistence.transactionType", "JTA"); LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean(); entityManager.setJtaDataSource(secondaryDataSource()); entityManager.setJpaVendorAdapter(jpaVendorAdapter); entityManager.setPackagesToScan("com.yanglf.demo4.dao.test2"); entityManager.setPersistenceUnitName("secondaryPersistenceUnit"); entityManager.setJpaPropertyMap(properties); return entityManager; }} ``` #### 测试 ```java @Autowired private ArticleRepository articleRepository; @Autowired private MessageRepository messageRepository; @Override @Transactional public void saveArticle(ArticleVo articleVo) { Article article = generalConvertor.convertor(articleVo, Article.class); article.setCreateTime(new Date()); articleRepository.save(article); Message message = new Message(); message.setTitle("保存文章成功"); message.setContent("保存:【" + articleVo.getAuthor() + "】的文章:{" + articleVo.getTitle() + "}"); message.setCreateTime(new Date()); int i = 1 / 0; messageRepository.save(message); } ``` ## MyBatis ### MyBatis Generator - MyBatis Generator - MyBatis Plus #### 新增依赖 ```xml org.mybatis.spring.boot mybatis-spring-boot-starter 2.1.3 mysql mysql-connector-java ``` #### 数据库配置 ```yml spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 datasource: url: jdbc:mysql://localhost:3306/bootdb?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: admin123 driver-class-name: com.mysql.cj.jdbc.Drivermybatis: mapper-locations: classpath:generate/*.xmllogging: level: com.yanglf.demo5: debug ``` #### 启动类加入扫描包 ```java @SpringBootApplication@MapperScan(basePackages = "com.yanglf.demo5.generate")public class Demo5Application { public static void main(String[] args) { SpringApplication.run(Demo5Application.class, args); }} ``` #### 代码自动生成 - XML配置文件实现 `MyBatis Generator` 代码生成配置 - 编写实现 `MyBatis Generator` 代码生成配置 - 通过IDEA插件实现 `MyBatis Generator` 代码生成配置 `better-mybatis-generator` 插件,勾选 example 生成条件查询 ### MyBatis Plus #### 添加依赖 ```xml com.baomidou mybatis-plus-boot-starter 3.3.2 mysql mysql-connector-java ``` #### 数据库配置 mybatis plus 完全兼容 mybatis ,所以配置文件基本相同 ```yml spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 datasource: url: jdbc:mysql://localhost:3306/bootdb?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: admin123 driver-class-name: com.mysql.cj.jdbc.Drivermybatis-plus: mapper-locations: classpath:mapper/*Mapper.xmllogging: level: com.yanglf.demo6: debug ``` #### 启动类添加扫描包 ```java @SpringBootApplication@MapperScan(basePackages = "com.yanglf.demo6.mapper")public class Demo6Application { public static void main(String[] args) { SpringApplication.run(Demo6Application.class, args); }} ``` #### 定义实体类 继承 `Model` 可以直接 CURD 操作数据库 ```java @Datapublic class Article extends Model
{ private Long id; private String author; private String title; private String content; private Date createTime;} ``` #### 接口服务和实现 ```java public interface ArticleMapper extends BaseMapper
{ } ``` 接口实现 ```java @Servicepublic class ArticleServiceImpl implements IArticleService { @Autowired private ArticleMapper articleMapper; @Override @Transactional public void saveArticle(Article article) { articleMapper.insert(article); } @Override @Transactional public void updateById(Article article) { if (article.getId() == null) { //article.id是必传参数,因为通常根据id去修改数据 //TODO 抛出一个自定义的异常 } articleMapper.updateById(article); } @Override public void deleteById(Long id) { articleMapper.deleteById(id); } @Override public Article findById(Long id) { return articleMapper.selectById(id); } @Override public List
findAll() { return articleMapper.selectList(null); }} ``` ### 最佳实践 - 使用 mybatis generator 或者 mybatis plus `默认方法` - 使用 xml 方式实现 `动态sql` ```xml ``` - 使用注解方式实现 `只适用简单sql` ```java @Select("select * from article where title like CONCAT('%',#{title},'%')") @Results({ @Result(property = "author", column = "author"), @Result(property = "content", column = "content") }) List
selectByLikeTitle(String title); ``` - 驼峰映射 ```yml mybatis: configuration: map-underscore-to-camel-case: true ``` - 使用 `@MapperScan` 注解 ### 多数据源支持 - 分包依赖注入数据源 `不适用 mybatis plus` #### 修改数据源 ```yml spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 datasource: primary: jdbc-url: jdbc:mysql://localhost:3306/bootdb?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: admin123 driver-class-name: com.mysql.cj.jdbc.Driver secondary: jdbc-url: jdbc:mysql://localhost:3306/bootdb2?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: admin123 driver-class-name: com.mysql.cj.jdbc.Drivermybatis: mapper-locations: classpath:generate/*.xmllogging: level: com.yanglf.demo5: debug ``` #### 新增配置类 com.yanglf.demo7.test1 ```java @Configuration@MapperScan( basePackages = "com.yanglf.demo7.test1", sqlSessionTemplateRef = "primarySqlSessionTemplate")public class PrimaryDataSourceConfig { @Bean(name = "primaryDataSource") @ConfigurationProperties("spring.datasource.primary") @Primary public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "primarySqlSessionFactory") @Primary public SqlSessionFactory primarySqlSessionFactory( @Qualifier("primaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:test1/*Mapper.xml")); return sessionFactoryBean.getObject(); } @Bean(name = "primaryTransactionManager") @Primary public DataSourceTransactionManager primaryTransactionManager( @Qualifier("primaryDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = "primarySqlSessionTemplate") @Primary public SqlSessionTemplate primarySqlSessionTemplate( @Qualifier("primarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); }} ``` com.yanglf.demo7.test2 ```java @Configuration@MapperScan( basePackages = "com.yanglf.demo7.test2", sqlSessionTemplateRef = "secondarySqlSessionTemplate")public class SecondaryDataSourceConfig { @Bean(name = "secondaryDataSource") @ConfigurationProperties("spring.datasource.secondary") public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "secondarySqlSessionFactory") public SqlSessionFactory secondarySqlSessionFactory( @Qualifier("secondaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:test2/*Mapper.xml")); return sessionFactoryBean.getObject(); } @Bean(name = "secondaryTransactionManager") public DataSourceTransactionManager secondaryTransactionManager( @Qualifier("secondaryDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = "secondarySqlSessionTemplate") public SqlSessionTemplate secondarySqlSessionTemplate( @Qualifier("secondarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); }} ``` #### 分包创建 Dao 和 xml文件 `classpath:test1`和 `classpath:test2` 分别创建xml 文件 ```java com.yanglf.demo7.test1 @Data@Builder@NoArgsConstructor@AllArgsConstructorpublic class Article { private Long id; private String author; private String title; private String content; private Date createTime;}public interface ArticleMapper{ /** * @param article * @return */ int insert(Article article);} ``` ```java com.yanglf.demo7.test2 @Data@Builder@NoArgsConstructor@AllArgsConstructorpublic class Message { private Long id; private String title; private String content; private Date createTime;}public interface MessageMapper { /** * @param message * @return */ int insert(Message message);} ``` #### 测试 ```java @Autowired private ArticleMapper articleMapper; @Autowired private MessageMapper messageMapper; @Override @Transactional public void saveArticle(Article article) { articleMapper.insert(article); Message message = new Message(); message.setTitle("保存文章成功"); message.setContent("保存:【" + article.getAuthor() + "】的文章:{" + article.getTitle() + "}"); message.setCreateTime(new Date()); messageMapper.insert(message); }} ``` ### Atomikos 分布式事务 #### 新增依赖 ```xml org.springframework.boot spring-boot-starter-jta-atomikos ``` #### 修改数据源配置 ```yml primarydb: uniqueResourceName: primary xaDataSourceClassName: com.mysql.cj.jdbc.MysqlXADataSource xaProperties: url: jdbc:mysql://localhost:3306/bootdb?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai user: root password: admin123 exclusiveConnectionMode: true minPoolSize: 3 maxPoolSize: 10 testQuery: SELECT 1 FROM dualsecondarydb: uniqueResourceName: secondary xaDataSourceClassName: com.mysql.cj.jdbc.MysqlXADataSource xaProperties: url: jdbc:mysql://localhost:3306/bootdb2?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai user: root password: admin123 exclusiveConnectionMode: true minPoolSize: 3 maxPoolSize: 10 testQuery: SELECT 1 FROM dual ``` #### 新增事务管理器 ```java @Configuration@EnableTransactionManagementpublic class XATransactionManagerConfig { @Bean("userTransaction") public UserTransaction userTransaction() throws Throwable { UserTransactionImp userTransactionImp = new UserTransactionImp(); userTransactionImp.setTransactionTimeout(10000); return userTransactionImp; } @Bean(value = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close") public TransactionManager atomikosTransactionManager() throws Throwable { UserTransactionManager userTransactionManager = new UserTransactionManager(); userTransactionManager.setForceShutdown(false); return userTransactionManager; } @Bean(value = "transactionManager") @DependsOn({"userTransaction","atomikosTransactionManager"}) public PlatformTransactionManager transactionManager() throws Throwable { return new JtaTransactionManager(userTransaction(), atomikosTransactionManager()); }} ``` #### 修改数据源配置类 primaryDataSource ```java @Configuration@MapperScan( basePackages = "com.yanglf.demo8.test1", sqlSessionTemplateRef = "primarySqlSessionTemplate")public class PrimaryDataSourceConfig { @Bean(name = "primaryDataSource") @ConfigurationProperties("primarydb") @Primary public DataSource primaryDataSource() { return new AtomikosDataSourceBean(); } @Bean(name = "primarySqlSessionFactory") @Primary public SqlSessionFactory primarySqlSessionFactory( @Qualifier("primaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:test1/*Mapper.xml")); return sessionFactoryBean.getObject(); } @Bean(name = "primarySqlSessionTemplate") @Primary public SqlSessionTemplate primarySqlSessionTemplate( @Qualifier("primarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); }} ``` secondaryDataSource ```java @Configuration@MapperScan( basePackages = "com.yanglf.demo8.test2", sqlSessionTemplateRef = "secondarySqlSessionTemplate")public class SecondaryDataSourceConfig { @Bean(name = "secondaryDataSource") @ConfigurationProperties("secondarydb") public DataSource secondaryDataSource() { return new AtomikosDataSourceBean(); } @Bean(name = "secondarySqlSessionFactory") public SqlSessionFactory secondarySqlSessionFactory( @Qualifier("secondaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:test2/*Mapper.xml")); return sessionFactoryBean.getObject(); } @Bean(name = "secondarySqlSessionTemplate") public SqlSessionTemplate secondarySqlSessionTemplate( @Qualifier("secondarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); }} ``` #### 测试 ```java @Autowired private ArticleMapper articleMapper; @Autowired private MessageMapper messageMapper; @Override @Transactional public void saveArticle(Article article) { articleMapper.insert(article); Message message = new Message(); message.setTitle("保存文章成功"); message.setContent("保存:【" + article.getAuthor() + "】的文章:{" + article.getTitle() + "}"); message.setCreateTime(new Date()); int i = 1 / 0; messageMapper.insert(message); } ``` ## Spring事务与分布式事务 ### ACID - 原子性(Atomicity) - 一致性(Consistency) - 隔离性(Isolation) - 持久性(Durability) ### 数据库事务隔离界别 | 隔离级别 | 出现更新丢失问题 | 出现脏读问题 | 出现虚读问题 | 出现幻读问题 | 实现方式 | | -------- | ---------------- | ------------ | ------------ | ------------ | ---------------------- | | 读未提交 | No | Yes | Yes | Yes | 排他写锁 | | 读提交 | No | No | Yes | Yes | 瞬间共享读锁和排他写锁 | | 重复读 | No | No | No | Yes | 共享读锁和排他写锁 | | 序列化 | No | No | No | No | | ### 事务传递行为 | 事务传递行为类型 | 说明 | | ------------------------- | ------------------------------------------------------------ | | Propagation.REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务,加入到这个事务中。这是最常见的选项。 | | Propagation.SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 | | Propagation.MANDATORY | 使用当前事务,如果当前没有事务,就抛出异常。 | | Propagation.REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 | | Propagation.NOT_SUPPORTED | 以非事务方式执行,如果当前存在事务,就把当前事务挂起。 | | Propagation.NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 | | Propagation.NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 Popagation.REQUIRED 类似的操作。 | `@Transactional(propagation = Propagation.REQUIRED)` ### 跨库分布式事务和跨服务分布式事务 ## 一行代码实现 RESTfull接口 Spring Data REST 基于Spring Data - Spring Data JPA - Spring Data MongoDB - Spring Data Neo4j - Spring Data GemFire - Spring Data Cassandra ### 新增依赖 > 与 swagger2.0 兼容不好,`不要加 swagger 依赖` > > 与OpenApi3(Swagger3) 兼容性好 ```xml org.springframework.boot spring-boot-starter-data-jpa mysql mysql-connector-java org.springframework.boot spring-boot-starter-data-rest ``` ### 增加 JPA 配置 ```yml spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 datasource: url: jdbc:mysql://localhost:3306/bootdb?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: admin123 driver-class-name: com.mysql.cj.jdbc.Driver jpa: # 使用 innodb 数据库 支持事务 database-platform: org.hibernate.dialect.MySQL5InnoDBDialect hibernate: # create 每次都会删除上一次的表,根据定义的 model 重新创建 # create-drop 每次 session 关闭 删除表,创建 session 创建表 # update 数据库没有创建,数据库存在,更新表结构,不会删除以前的数据 # validate 每次加载时候,验证数据库结构和定义的model是否一致,不一致报错,比较安全 ddl-auto: create database: mysql show-sql: true # 配置rest请求方式的基路径# spring.data.rest.base-path=/api ``` ### DAO 映射成接口 ````java @RepositoryRestResource(path = "articles")public interface ArticleRepository extends JpaRepository { @RestResource(path = "findByAuthor",rel = "nameStartsWith") Article findByAuthor(@Param(value = "author") String author);} ```` 访问: http://localhost:8080/articles 查询数据 所有接口以 restfull 风格 访问,没有的接口自己开发 ### 整合 OpenApi文档 加入依赖 ```xml org.springframework.boot spring-boot-starter-data-rest org.springdoc springdoc-openapi-ui 1.4.0 ``` 添加分组配置 ```java @Configurationpublic class OpenAPIConfig { @Bean public GroupedOpenApi restApi(){ return GroupedOpenApi.builder() .group("rest-api") .pathsToMatch("/rest/**") .build(); } @Bean public GroupedOpenApi helloApi(){ return GroupedOpenApi.builder() .group("hello-api") .pathsToMatch("/hello/**") .build(); } @Bean public GroupedOpenApi articlesApi(){ return GroupedOpenApi.builder() .group("articles-api") .pathsToMatch("/articles/**") .build(); }} ``` 访问: http://localhost:8080/swagger-ui.html # 模板引擎 ## warjars与静态资源 WebJars能使Maven的依赖管理支持OSS的JavaScript库/CSS库,比如jQuery、Bootstrap等 WebJars是将Web前端Javascript和CSS等资源打包成Java的Jar包,这样在Java Web开发中我们可以借助Maven这些依赖库的管理,保证这些Web资源版本唯一性。 ### 静态资源目录 - classpath: /static/ - classpath: /public - classpath: /resources - classpath: /META-INF/resources ### 自定义静态资源目录 ```yml spring: resources: static-locations: classpath:/mystatic/ ``` ### 集成到项目 #### 添加依赖 ```xml org.webjars jquery 3.5.1 org.webjars bootstrap 4.5.0 ``` #### 页面引用 classpath: /public 目录下新建 `index.html` ```html 首页

This Is Index Page

成功! bootstrap webjar 完美
``` # 其他组件 ## 监听器 **Servlet Listener**: - `ServletContext Listener`: 监听Servlet 容器的创建和销毁 - `HttpSession Listener`: 监听用户浏览器 session 会话的创建和销毁 - `ServletRequest Listener`: 监听用户请求 **监听域对象内属性的变化**: - HttpSessionAttributeListener - ServletContextAttributeListener - ServletRequestAttributeListener ### 定义监听器 ```java @Slf4j@WebListenerpublic class CustomListener implements ServletContextListener, ServletRequestListener, HttpSessionListener, ServletRequestAttributeListener { @Override public void contextInitialized(ServletContextEvent sce) { log.info("====================context 创建:{}",sce); } @Override public void contextDestroyed(ServletContextEvent sce) { log.info("====================context 销毁:{}",sce); } @Override public void requestInitialized(ServletRequestEvent sre) { log.info("====================request监听器的 创建:{}",sre); } @Override public void requestDestroyed(ServletRequestEvent sre) { log.info("====================request监听器的 销毁:{}",sre); } @Override public void sessionCreated(HttpSessionEvent se) { log.info("====================session 创建:{}",se); } @Override public void sessionDestroyed(HttpSessionEvent se) { log.info("====================session 销毁:{}",se); } @Override public void attributeAdded(ServletRequestAttributeEvent srae) { log.info("====================attributeAdded:{}",srae); } @Override public void attributeReplaced(ServletRequestAttributeEvent srae) { log.info("====================attributeReplaced:{}",srae); } @Override public void attributeRemoved(ServletRequestAttributeEvent srae) { log.info("====================attributeRemoved:{}",srae); }} ``` ### 注册到启动类 使用 `@ServletComponentScan` 注册 ```java @SpringBootApplication@ServletComponentScanpublic class Demo12Application { public static void main(String[] args) { SpringApplication.run(Demo12Application.class, args); }} ``` ### 测试 controller ```java @RestControllerpublic class IndexController { @GetMapping("/hello") public String hello(HttpServletRequest request, HttpSession session) { // 操作 attribute request.setAttribute("a", "a"); request.setAttribute("b", "b"); request.getAttribute("a"); request.removeAttribute("a"); // session 操作 session.setAttribute("a", "a"); session.getAttribute("a"); session.removeAttribute("a"); session.invalidate(); return "hello"; }} ``` ## 过滤器 - 在客户端的请求访问后端之前,拦截请求 - 在服务器响应发送回客户端之前,拦截请求 ### 编写过滤器 ```java @Slf4jpublic class CustomFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { log.info("customFilter 初始化------------"); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { log.info("customFilter 请求处理之前------------"); servletRequest.setCharacterEncoding("UTF-8"); filterChain.doFilter(servletRequest, servletResponse); log.info("customFilter 请求响应之前------------"); servletResponse.setCharacterEncoding("UTF-8"); } @Override public void destroy() { log.info("customFilter 销毁------------"); }} ``` ### Filter 注册方式 - 利用 WebFilter 注解配置 无法设置优先级,默认优先级按类名首字母排序执行 - FilterRegistrationBean 方式 可以设置优先级,推荐使用 - XML配置方式,放弃 #### WebFilter 注解 ```java @Slf4j@WebFilter(filterName = "customFilter",urlPatterns = "/*")public class CustomFilter implements Filter { ....}// 启动类也需要加 @ServletComponentScan 注解@SpringBootApplication@ServletComponentScanpublic class Demo12Application { public static void main(String[] args) { SpringApplication.run(Demo12Application.class, args); }} ``` #### FilterRegistrationBean 注册 ```java @Configurationpublic class FilterRegistration { @Bean public FilterRegistrationBean filterRegistrationBean1() { FilterRegistrationBean registrationBean = new FilterRegistrationBean(); registrationBean.setFilter(new CustomFilter1()); registrationBean.setName("customFilter1"); registrationBean.addUrlPatterns("/*"); registrationBean.setOrder(10); return registrationBean; } @Bean public FilterRegistrationBean filterRegistrationBean2() { FilterRegistrationBean registrationBean = new FilterRegistrationBean(); registrationBean.setFilter(new CustomFilter2()); registrationBean.setName("customFilter2"); registrationBean.addUrlPatterns("/*"); registrationBean.setOrder(9); return registrationBean; }} ``` ## 拦截器 ### 拦截器和过滤器主要区别 - 规范不同 过滤器: Servlet 中定义的组件,在Servlet 容器中生效 拦截器: Spring 框架定义的组件,在Spring的上下文生效 - 作用范围不同 过滤器的范围比拦截器范围大 - 使用资源不同 拦截器可以获取到Spring ioc 容器中注入的bean,过滤器无法获取,也就是无法实现依赖注入 - 粒度不同 过滤器的范围大,在拦截器的外层,适合整个api的过滤,拦截器适合模块过滤 ### 编写拦截器 ```java @Slf4j@Componentpublic class CustomHandlerInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("preHandle: 请求前调用-----"); // 返回 false 请求中断 return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { log.info("postHandle: 请求后调用-----"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info("afterCompletion: 请求调用完成后回调方法,即在视图渲染完成后调用-----"); }} ``` ### 注册 ```java @Configurationpublic class MyWebMvcConfigurer implements WebMvcConfigurer { @Autowired private CustomHandlerInterceptor customHandlerInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { // 注册拦截器 拦截规则 // 多个拦截器时,按顺序依次添加 registry.addInterceptor(customHandlerInterceptor) .addPathPatterns("/*"); }} ``` ## 自定义事件发布与监听 ### 常用的事件监听实现 - 使用消息队列中间件的发布订阅模式 - Redis的发布订阅模式 `redisson` - JDK自带的 `java.util.EventListener` - Spring 环境下的实现事件发布监听 ### 定义事件源 ```java public class MyEvent extends ApplicationEvent { public MyEvent(Object source) { super(source); }} ``` ### spring 4 种实现监听方式 - 写代码向 `ApplicationContext` 中添加监听器 - 使用 `Component` 注解将监听器载入spring容器 (推荐) - 在 `application.properties` 中配置监听器 - 通过 `@EventListener` 注解实现事件监听 (推荐) > 写代码向 `ApplicationContext` 中添加监听器 ```java @Slf4jpublic class MyListener1 implements ApplicationListener { @Override public void onApplicationEvent(MyEvent myEvent) { log.info(String.format("%s监听到事件源:%s",MyListener1.class.getName(),myEvent.getSource())); }}@SpringBootApplication//@ServletComponentScanpublic class Demo12Application { public static void main(String[] args) { ConfigurableApplicationContext applicationContext = SpringApplication.run(Demo12Application.class, args); applicationContext.addApplicationListener(new MyListener1()); }} ``` >使用 `Component` 注解将监听器载入spring容器 (推荐) ```java @Slf4j@Componentpublic class MyListener2 implements ApplicationListener { @Override public void onApplicationEvent(MyEvent myEvent) { log.info(String.format("%s监听到事件源:%s", MyListener2.class.getName(),myEvent.getSource())); }} ``` > 在 `application.properties` 中配置监听器 ```java @Slf4jpublic class MyListener3 implements ApplicationListener { @Override public void onApplicationEvent(MyEvent myEvent) { log.info(String.format("%s监听到事件源:%s", MyListener3.class.getName(),myEvent.getSource())); }} ``` application.yml ```yaml context: listener: classes: com.yanglf.demo12.event.MyListener3 ``` > 通过 `@EventListener` 注解实现事件监听 (推荐) ```java @Slf4j@Componentpublic class MyListener4 { @EventListener public void listener(MyEvent myEvent) { log.info(String.format("%s监听到事件源:%s", MyListener4.class.getName(),myEvent.getSource())); }} ``` ### 发送消息 ```java @RestControllerpublic class IndexController { @Autowired private ApplicationContext applicationContext; @GetMapping("/msg") public String msg() { applicationContext.publishEvent(new MyEvent("事件发布...")); return "hello"; }} ``` 接受到消息优先级: MyListener3 > MyListener4 > MyListener2 > MyListener1 ## 应用程序启动监听 ### 实现方式 - CommandLineRunner - ApplicationRunner > 1. 实现这两个接口任意一个 > 2. 通过 `@Component` 注解 或者 `@Bean` 定义方式实现,结合 `@Order` 设置执行顺序 ### 适用场景 - 将系统常用数据加载到内存 - 应用上一次运行的垃圾数据清理 - 系统启动成功后的通知发送 ### 具体实现 > 通过 `@Component` 注解实现 CommandLineRunner ```java @Slf4j @Component public class CommandLineStartupRunner implements CommandLineRunner { @Override public void run(String... args) throws Exception { // 加载数据库配置信息到本地缓存 log.info("CommandLineStartupRunner 传入的参数:{}", Arrays.toString(args)); } } ``` ApplicationRunner ```java @Slf4j @Component public class AppStartupRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { log.info("AppStartupRunner 参数名:{}",args.getOptionNames()); log.info("AppStartupRunner 参数值:{}",args.getOptionValues("age")); log.info("AppStartupRunner 参数:{}", Arrays.toString(args.getSourceArgs())); } } ``` > 通过`@Bean` 定义方式实现 ```java @Configuration @Slf4j public class BeanRunner { @Bean @Order(1) public CommandLineRunner runner1() { return args -> { log.info("BeanCommandLineRunner runner1:{}", Arrays.toString(args)); }; } @Bean @Order(2) public CommandLineRunner runner2() { return args -> { log.info("BeanCommandLineRunner runner2:{}", Arrays.toString(args)); }; } @Bean @Order(3) public ApplicationRunner runner3() { return args -> { log.info("BeanApplicationRunner runner3:{}", Arrays.toString(args.getSourceArgs())); }; } @Bean @Order(4) public ApplicationRunner runner4() { return args -> { log.info("BeanApplicationRunner runner4:{}", Arrays.toString(args.getSourceArgs())); }; } @Bean @Order(5) public ApplicationRunner runner5() { return args -> { log.info("BeanApplicationRunner runner5:{}", Arrays.toString(args.getSourceArgs())); }; } } ``` ### 测试 启动参数 `Program arguments` 中添加 `--name=张三 --age=22` 启动测试 - ApplicationRunner 执行优先级高于CommandLineRunner - 以 @Bean 的形式运行的 Runner 优先级要低于 @Component 注解的方式 - Order 注解只能保证同类型的 Runner 的执行顺序 ## Web Server ### 嵌入容器运行参数 #### 嵌入容器类型 - Tomcat (默认) - Jetty - Undertow #### 调整运行参数方式 - 修改配置文件 简单 - 自定义配置类 (专业调优) > server-properties - `server.xx` 开头的是所有servlet容器通用的配置 - `server.tomcat.xx` 开头的是Tomcat容器持有的配置参数 - `server.jetty.xx` 开头的是Jettyr容器持有的配置参数 - `server.undertow.xx` 开头的是Jetty容器持有的配置参数 > 常用基础配置参数 | 参数 | 默认值 | 说明 | | ------------------------------ | ------ | ------------------------------------------------------------ | | server.port | 8080 | 配置web容器端口 | | server.servlet.session.timeout | 30m | session失效时间,如果不写单位,默认是秒 | | server.servlet.context-path | / | url访问路径的基础路径 | | server.tomcat.uri-encoding | UTF-8 | 配置tomcat的请求编码 | | server.tomcat.basedir | | 配置tomcat运行日志和临时文件的目录。若不配置,则默认使用系统的临时目录。 | > Tomcat 调优核心参数 | 参数 | 默认值 | 说明 | | ------------------------------- | ------ | -------------------------------------------------------- | | server.tomcat.max-connections | 8192 | 接收的最大连接数 | | server.tomcat.accept-count | 100 | 当所有线程被占用,将放入请求队列等待的最大的请求连接数量 | | server.tomcat.threas.max | 200 | 最大的工作线程池数量 | | server.tomcat.threads.min-spare | 10 | 最小的工作线程池数量 | > 自定义配置类配置参数 ```java @Configuration public class TomcatCustomizer { @Bean public ConfigurableServletWebServerFactory configurableServletWebServerFactory() { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); factory.addConnectorCustomizers(new MyTomcatConnectorCustomizer()); return factory; } static class MyTomcatConnectorCustomizer implements TomcatConnectorCustomizer { MyTomcatConnectorCustomizer() { } @Override public void customize(Connector connector) { connector.setPort(Integer.parseInt("8888")); connector.setProperty("maxConnections", "8192"); connector.setProperty("acceptorThreadCount", "100"); connector.setProperty("minSpareThreads", "10"); connector.setProperty("maxThreads", "200"); } } } ``` ### 配置 HTTPS #### 生成证书 生成自签名证书 keytool - `-genkey` 表示创建一个新的密钥 - `-alias` 表示keytool的别名 - `-keyalg` 表示使用的加密算法是RSA (一个非对称解密算法) - `-keysize` 表示密钥长度 - `-keystore` 表示生成的密钥存放的位置 - `-validity` 表示密钥的有效期 (单位时天) ```sh keytool -genkeypair -alias selfsigned_localhost_sslserver -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore yanglf-ssl-key.p12 -validity 3650 ``` #### 配置证书 将生成的证书复制到项目的 classpath: /cert 目录下 ```yaml server: port: 8080 ssl: key-store: classpath:cert/yanglf-ssl-key.p12 key-store-password: 111111 key-store-type: PKCS12 ``` #### HTTP 重定向 HTTPS ```yaml server: # https 访问端口 port: 8888 # http 访问端口 httpPort: 80 ssl: key-store: classpath:cert/yanglf-ssl-key.p12 key-store-password: 111111 key-store-type: PKCS12 ``` 配置tomcat ```java @Configuration public class TomcatCustomizer { @Value("${server.port}") private Integer httpsPort; @Value("${server.httpPort}") private Integer httpPort; @Bean public ConfigurableServletWebServerFactory configurableServletWebServerFactory() { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory() { @Override protected void postProcessContext(Context context) { SecurityConstraint constraint = new SecurityConstraint(); // 表示需要认证 constraint.setUserConstraint("CONFIDENTIAl"); SecurityCollection collection = new SecurityCollection(); collection.addPattern("/*"); constraint.addCollection(collection); context.addConstraint(constraint); } }; factory.addAdditionalTomcatConnectors(connector()); return factory; } public Connector connector() { Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); connector.setScheme("http"); // Connector 监听的 HTTP 端口 connector.setPort(httpPort); connector.setSecure(false); // 监听到 HTTP 的端口号后转到 HTTPS 端口 connector.setRedirectPort(httpsPort); return connector; } } ``` 访问测试: http://localhost/index 会重定向到 https://localhost:8888/index ### 切换内置的Tomcat 容器 #### 修改配置文件 `排除web依赖中的tomcat容器,添加 undertow 或者 jetty 的依赖` ```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-tomcat org.springframework.boot spring-boot-starter-undertow org.springframework.boot spring-boot-starter-jetty ``` #### Jetty Server 配置参数 | 参数 | 默认值 | 说明 | | ------------------------------ | ------ | ------------------------------------------------------------ | | server.jetty.threads.acceptors | -1.0 | acceptor的线程数量,当设置成-1,会根据CPU的逻辑核数/8来决定,最大不能超过4 | | server.jetty.threads.selectors | -1.0 | selectors线程数量,当设置为-1 时,根据CPU逻辑核数/2决定,至少1个 | | server.jetty.threads.min | 8 | 工作线程最小线程数 | | server.jetty.threads.max | 200 | 工作线程最大线程数 | #### Undertow 配置参数 ```yaml server: port: 8088 undertow: # 设置IO 线程数,它主要负责非阻塞任务, io-threads: 4 # 工作任务线程,默认为 io-threads 的8倍 worker-threads: 32 ``` ### War 包部署到外置Tomcat > 修改 pom.xml 配置 ```xml war org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-tomcat org.springframework.boot spring-boot-starter-tomcat provided ``` > 新增配置类 ```java public class ServletInitializer extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { // Demo14Application 是 spring boot 的启动类 return builder.sources(Demo11Application.class); } } ``` > pom.xml 添加 打包配置 ```xml demo11 org.springframework.boot spring-boot-maven-plugin ``` 1. 打包: `mvn clean && mvn package` 2. 将 war 包 复制到 tomcat webapps 目录 3. 启动 tomcat,访问测试,如果 war 包名称不是 ROOT,访问的时候需要加上文件名前缀,静态资源会出现404,因为没有前缀 # 异常处理 ## 设计规范 - 自定义异常 - 全局异常 - 异常页面或者统一响应结构数据 > 1. 拦截异常转换为自定义异常抛出,不允许将异常私自截留 > 2. 统一数据响应代码,使用HTTP状态码(400,500),不要自定义 > 3. 自定义异常message属性,以用户友好的语言描述异常信息 > 4. 不允许对父类 Exception 统一 catch,要分小类 catch ## 自定义异常和相关数据结构 - `CustomException` 自定义异常 - `ExceptionTypeEnum` 枚举异常分类,将异常分类固化下来 - `AjaxResponse` 用于响应HTTP接口请求的统一数据结构 ### 异常枚举 ```java public enum CustomExceptionType { USER_INPUT_ERROR(400,"您输入的数据错误或您没有权限访问资源!"), SYSTEM_ERROR(500,"系统出现异常,请您稍后重试或者联系管理员!"), OTHER_ERROR(999,"系统出现未知错误,请您联系管理员!"); // code private int code; // 异常类型中文描述 private String desc; public CustomExceptionType(int code, String desc) { this.code = code; this.desc = desc; } public int getCode() { return code; } public String getDesc() { return desc; } } ``` ### 自定义异常 ```java public class CustomException extends RuntimeException { // 异常编码 private int code; // 异常信息 private String message; public CustomException(CustomExceptionType customExceptionType) { this.code = customExceptionType.getCode(); this.message = customExceptionType.getDesc(); } public CustomException(CustomExceptionType customExceptionType,String message) { this.code = customExceptionType.getCode(); this.message = message; } public int getCode() { return code; } @Override public String getMessage() { return message; } } ``` ### 统一返回数据结构 ```java @Data public class AjaxResponse { private boolean isok; //请求是否处理成功 private int code; //请求响应状态码(200、400、500) private String message; //请求结果描述信息 private Object data; //请求结果数据(通常用于查询操作) private AjaxResponse() { } //请求出现异常时响应的数据封装 public static AjaxResponse error(CustomException e) { AjaxResponse ajaxResponse = new AjaxResponse(); ajaxResponse.setIsok(false); ajaxResponse.setCode(e.getCode()); ajaxResponse.setMessage(e.getMessage()); return ajaxResponse; } //请求出现异常时响应的数据封装 public static AjaxResponse error(CustomExceptionType customExceptionType, String errorMessage) { AjaxResponse ajaxResponse = new AjaxResponse(); ajaxResponse.setIsok(false); ajaxResponse.setCode(customExceptionType.getCode()); ajaxResponse.setMessage(errorMessage); return ajaxResponse; } //请求成功的响应,不带查询数据(用于删除、修改、新增接口) public static AjaxResponse success() { AjaxResponse ajaxResponse = new AjaxResponse(); ajaxResponse.setIsok(true); ajaxResponse.setCode(200); ajaxResponse.setMessage("请求响应成功!"); return ajaxResponse; } //请求成功的响应,带有查询数据(用于数据查询接口) public static AjaxResponse success(Object obj) { AjaxResponse ajaxResponse = new AjaxResponse(); ajaxResponse.setIsok(true); ajaxResponse.setCode(200); ajaxResponse.setMessage("请求响应成功!"); ajaxResponse.setData(obj); return ajaxResponse; } //请求成功的响应,带有查询数据(用于数据查询接口) public static AjaxResponse success(Object obj, String message) { AjaxResponse ajaxResponse = new AjaxResponse(); ajaxResponse.setIsok(true); ajaxResponse.setCode(200); ajaxResponse.setMessage(message); ajaxResponse.setData(obj); return ajaxResponse; } } ``` ### 通用全局异常 > json 异常处理 ```java @RestControllerAdvice public class WebExceptionHandler { @ExceptionHandler(CustomException.class) public AjaxResponse customException(CustomException e) { if (e.getCode() == CustomExceptionType.SYSTEM_ERROR.getCode()) { //TODO 将 500 异常 持久化,方便运维人员查看 } return AjaxResponse.error(e); } @ExceptionHandler(Exception.class) public AjaxResponse exception(Exception e) { //TODO 将 500 异常 持久化,方便运维人员查看 return AjaxResponse.error(new CustomException( CustomExceptionType.OTHER_ERROR )); } } ``` > 处理 web 异常 和 json 异常 ```java @Component @ControllerAdvice public class GlobalResponseAdvice implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter methodParameter, Class aClass) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { if (mediaType.equalsTypeAndSubtype(MediaType.APPLICATION_JSON)) { if (body instanceof AjaxResponse) { serverHttpResponse.setStatusCode( HttpStatus.valueOf(((AjaxResponse) body).getCode())); return body; } else { serverHttpResponse.setStatusCode(HttpStatus.OK); return AjaxResponse.success(body); } } return body; } } ``` ### 测试 > 测试服务 ```java @Service public class ExceptionService { public void systemBizError() { try { Class.forName("com.mysql.xxx.Driver"); } catch (ClassNotFoundException e) { throw new CustomException(CustomExceptionType.SYSTEM_ERROR, "在XXX业务,myBiz()方法内,出现CustomException,请联系管理员"); } } public void userBizError(int input) { if (input < 0) { throw new CustomException(CustomExceptionType.USER_INPUT_ERROR, "您输入的数据不符合业务,请确认后重新输入!!"); } } } ``` > 测试 controller ```java @RestController public class IndexController { @Autowired private ExceptionService exceptionService; // 测试异常 @GetMapping("/exception/{type}") public AjaxResponse exception(@PathVariable Integer type) { if (type == 1) { exceptionService.systemBizError(); } else { exceptionService.userBizError(-1); } return AjaxResponse.success(); } // 测试没有封装统一对象的数据,GlobalResponseAdvice 拦截器 是否会封装 @GetMapping("/getArticle") public Article test() { Article article = Article.builder() .id(1L) .author("yanglf") .content("1P") .title("Spring Boot 青铜") .createTime(new Date()) .build(); return article; } } ``` ### 数据校验异常处理 > 完善全局异常 ```java @RestControllerAdvice public class WebExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) //@ResponseStatus(HttpStatus.OK) public AjaxResponse handleBindException(MethodArgumentNotValidException e) { FieldError fieldError = e.getBindingResult().getFieldError(); return AjaxResponse.error(new CustomException(CustomExceptionType.USER_INPUT_ERROR, fieldError.getDefaultMessage())); } @ExceptionHandler(BindException.class) //@ResponseStatus(HttpStatus.OK) public AjaxResponse handleBindException(BindException e) { FieldError fieldError = e.getBindingResult().getFieldError(); return AjaxResponse.error(new CustomException(CustomExceptionType.USER_INPUT_ERROR, fieldError.getDefaultMessage())); } @ExceptionHandler(IllegalArgumentException.class) public AjaxResponse handleIllegalArgumentException(IllegalArgumentException e) { return AjaxResponse.error(new CustomException(CustomExceptionType.USER_INPUT_ERROR, e.getMessage())); } @ExceptionHandler(CustomException.class) public AjaxResponse customException(CustomException e) { if (e.getCode() == CustomExceptionType.SYSTEM_ERROR.getCode()) { //TODO 将 500 异常 持久化,方便运维人员查看 } return AjaxResponse.error(e); } @ExceptionHandler(Exception.class) public AjaxResponse exception(Exception e) { //TODO 将 500 异常 持久化,方便运维人员查看 return AjaxResponse.error(new CustomException( CustomExceptionType.OTHER_ERROR )); } } ``` > 修改测试服务 ```java @Service public class ExceptionService { public void systemBizError() { try { Class.forName("com.mysql.xxx.Driver"); } catch (ClassNotFoundException e) { throw new CustomException(CustomExceptionType.SYSTEM_ERROR, "在XXX业务,myBiz()方法内,出现CustomException,请联系管理员"); } } public void userBizError(int input) { if (input < 0) { throw new CustomException(CustomExceptionType.USER_INPUT_ERROR, "您输入的数据不符合业务,请确认后重新输入!!"); } // 触发 IllegalArgumentException Assert.isTrue(input>18,"年龄不满18,请重新输入"); } } ``` ### 页面跳转异常 `不要使用这种写法`: ```java @GetMapping("/freemarker") public String index(Model model) { try { List
articles = articleService.findAll(); model.addAttribute("articles", articles); } catch (Exception e) { return "error"; } return "fremarkertemp"; } ``` **使用这个方式 aop +自定义注解** ```java @ModelView @GetMapping("/freemarker") public String index(Model model) { List
articles = articleService.findAll(); model.addAttribute("articles", articles); return "fremarkertemp"; } ``` > 添加 aop 依赖 ```xml org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-thymeleaf ``` > 定义 @ModelView 注解 ```java @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ModelView { } ``` > 新增 aop 类 ```java @Aspect @Component @Slf4j public class ModelViewAspect { @Pointcut("@annotation(com.yanglf.demo14.exception.ModelView)") public void pointcut() { } @AfterThrowing(pointcut = "pointcut()", throwing = "e") public void afterThrowing(Throwable e) { throw ModelViewException.transfer(e); } } ``` > 定义 `ModelViewException` 异常类 ```java public class ModelViewException extends RuntimeException { // Exception 转换 为 ModelViewException public static ModelViewException transfer(Throwable cause) { return new ModelViewException(cause); } private ModelViewException(Throwable cause) { super(cause); } } ``` > 修改全局异常 `ModelViewException` 异常处理方法 ```java @ControllerAdvice public class WebExceptionHandler { @ExceptionHandler(ModelViewException.class) public ModelAndView modelViewException(HttpServletRequest request, ModelViewException e) { ModelAndView modelAndView = new ModelAndView(); // 将异常信息设置到 modelAndView modelAndView.addObject("exception", e); modelAndView.addObject("url", request.getRequestURI()); modelAndView.setViewName("error"); return modelAndView; } @ResponseBody @ExceptionHandler(MethodArgumentNotValidException.class) //@ResponseStatus(HttpStatus.OK) public AjaxResponse handleBindException(MethodArgumentNotValidException e) { FieldError fieldError = e.getBindingResult().getFieldError(); return AjaxResponse.error(new CustomException(CustomExceptionType.USER_INPUT_ERROR, fieldError.getDefaultMessage())); } @ResponseBody @ExceptionHandler(BindException.class) //@ResponseStatus(HttpStatus.OK) public AjaxResponse handleBindException(BindException e) { FieldError fieldError = e.getBindingResult().getFieldError(); return AjaxResponse.error(new CustomException(CustomExceptionType.USER_INPUT_ERROR, fieldError.getDefaultMessage())); } @ResponseBody @ExceptionHandler(IllegalArgumentException.class) public AjaxResponse handleIllegalArgumentException(IllegalArgumentException e) { return AjaxResponse.error(new CustomException(CustomExceptionType.USER_INPUT_ERROR, e.getMessage())); } @ResponseBody @ExceptionHandler(CustomException.class) public AjaxResponse customException(CustomException e) { if (e.getCode() == CustomExceptionType.SYSTEM_ERROR.getCode()) { //TODO 将 500 异常 持久化,方便运维人员查看 } return AjaxResponse.error(e); } @ResponseBody @ExceptionHandler(Exception.class) public AjaxResponse exception(Exception e) { //TODO 将 500 异常 持久化,方便运维人员查看 return AjaxResponse.error(new CustomException( CustomExceptionType.OTHER_ERROR )); } } ``` > 编写 `error.html` ```html Error Page

exception.toString()

exception.message

url

``` > 测试 controller ```java @ModelView @GetMapping("/test") public String test(@RequestParam(required = false) Integer a) { if (a < 0) { throw new CustomException(CustomExceptionType.USER_INPUT_ERROR); } return "test"; } ```