# 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 示例项目

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

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数据转换的原理

- 当一个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 extends AjaxResult> 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
```
#### 隐藏版本号
**添加依赖**
```xml
org.webjars webjars-locator 0.30
```
**修改页面**
```html
首页 This Is Index Page
```
访问 : http://localhost:8080 项目根目录,会直接映射到 index.html
> classpath: /public 下的 index.html 是网站默认的首页
>
> classpath: /static 下的 favicon.ico 是网站默认的icon 图标
## 模板引擎选型
- JSP
- Thymeleaf
```html
| | | | |
```
- Apache Freemarker
```html
<#list users as item> | {{item.userId}} | {{item.username}} | {{item.password}} | {{item.email}} | {{item.mobile}} |
#list>
```
- Mustache
- Groovy Templates
- vue 、ReactJS、AngularJs
## 整合Jsp
### 添加依赖
```xml
org.apache.tomcat.embed tomcat-embed-jasper javax.servlet jstl
```
### 配置
```yaml
spring: mvc: view: suffix: .jsp prefix: /WEB-INF/jsp/
```
1. `main`目录下创建 `webapp`目录,webapp 下再创建 WEB-INFO/jsp
2. Project Structure 面板 Module 中 选择 项目的web 模块, 将 webapp 目录 添加到 `Web Resource Directory`
### 创建 jsp
`/WEB-INF/jsp/jsptemp.jsp`
```jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> 首页 | 作者 | 教程名称 | 教程内容 |
| ${article.author} | ${article.title} | ${article.content} |
```
### 创建 controller
```java
@Controller@RequestMapping(value="/template")public class IndexController { @Autowired private IArticleService articleService; @GetMapping("/jsp") public String index(String name, Model model) { List articles = articleService.selectAll(); model.addAttribute("articles", articles); return "jsptemp"; }}
```
访问测试: http://localhost:8080/template/jsp
## 整合freemarker
### 添加依赖
```xml
org.springframework.boot spring-boot-starter-freemarker
```
### 增加配置
> 从springboot2.0开始,使用的freemarker版本采用的默认文件后缀不再是ftl,而是ftlh
```yaml
spring: freemarker: # 缓存配置,开发阶段配置为false,生产配置成true cache: false # 模板文件后缀 # 从springboot2.0开始,使用的freemarker版本采用的默认文件后缀不再是ftl,而是ftlh suffix: .ftlh # 模板文件编码 charset: UTF-8 template-loader-path: classpath:/templates/
```
### 创建页面
classpath:/templates/freemarkertemp.ftlh
```html
首页 | 作者 | 教程名称 | 教程内容 |
<#list articles as article> ${article.author} | ${article.title} | ${article.content} | #list>
```
### 测试 controller
```java
@Controller@RequestMapping(value="/template")public class IndexController { @Autowired private IArticleService articleService; @GetMapping("/freemarker") public String index(Model model) { List articles = articleService.selectAll(); model.addAttribute("articles", articles); return "freemarkertemp"; }}
```
访问测试: http://localhost:8080/template/freemarker
### 学习手册
http://freemarker.foofun.cn/
## 整合 thymeleaf
### 添加依赖
```xml
org.springframework.boot spring-boot-starter-thymeleaf
```
### 新增配置
```YAML
srping: thymeleaf: # 启用缓存:建议生产环境启用 cache: false # 检查模板是否存在 check-template: true # 是否启用 enabled: true # 模板编码 encoding: UTF-8 # 应该从解析中排除的视图名称列表(用逗号分开) excluded-view-names: # 模板模式 mode: HTML5 # 模板存放路径 prefix: classpath:/templates/ # 模板后缀 suffix: .html
```
### 创建页面
classpath:/templates/thymeleaftemp.html
```html
首页
```
### 创建测试 controller
```java
@Controller@RequestMapping(value="/template")public class IndexController { @Autowired private IArticleService articleService; @GetMapping("/thymeleaf") public String index(Model model) { List articles = articleService.selectAll(); model.addAttribute("articles", articles); return "thymeleaftemp"; }}
```
访问测试: http://localhost:8080/template/thymeleaf
### 学习手册
#### 基本语法
> 基本表达式
- 变量表达式 `${}`
- 选择变量表达式 `*{}`
- 链接表达式 `@{}`
- 字符串连接、数学运算,布尔逻辑和三目运算符
```html
```
> 迭代状态变量
- index 当前迭代索引,从0开始。这是索引属性
- count 当前迭代序号,从1开始
- size 元素的总量迭代变量
- current 变量为每个迭代,当前正迭代的元素
- even/odd 是否当前迭代时奇数还是偶数,为布尔值
- first 是否为第一个当前迭代的元素,布尔值
- last 是否最后一个迭代,布尔值
```html
| | | | |
```
> 条件判断
条件判断表达式可以是以下类型:
- boolean 类似
- 数字类型 值不是0,返回true
- 字符类型 值不是0,返回true
- String 类型 值不是"false"/"off"/"no",返回true
- boolean、数值、字符、String 之外的其他类型不为null,返回true
- 对象是null,返回false
```html
文章存在
文章不存在
文章不存在
```
#### 内置对象与工具类
> 内置对象
- `${#ctx}` 上下文对象,可用来获取其他内置对象
- `${#param}` 上下文参数变量
- `${#locale}` 上下文区域语言设置对象
- `${#request}` HttpServletRequest 对象
- `${#response}` HttpServletResponse 对象
- `${#session}` HttpSession 对象
- `${#servletContext}` ServletContext 对象
```html
Thymeleaf内置对象
语言国家:
param:
request:
session:
application:
session是否包含name3,不包含显示 zoo:
session包含属性数量:
session是否为空:
```
www.thymeleaf.org
#### 公共代码片段及内联JS
> 引用代码片段的语法
- `~{viewName}` 表示引入完整页面
- `~{viewName ::selector}` 表示在指定页面找片段
- 其中sector可作为片段名,css选择器等
- 即可以在一个HTML页面内定义多个片段
- `~{::selector}` 表示在当前HTML页面查找代码片段
**定义片段 `common/head.html` **
```html
```
**引用**
```html
```
> 片段组合方式
- `th:replace` : 不要自己的主标签,保留 th:fragment 的主标签
- `th:insert` : 保留自己的主标签,保留 th:fragment 的主标签
- `th:include` 保留自己的主标签,不要 th:fragment 的主标签
> 内联JS
```js
```
# 其他组件
## 监听器
**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";
}
```