1. **重点掌握**Knif4j 配置及使用
2. 了解AOP设计思想及原理
3. **重点掌握**AOP相关术语及使用方式
## 1整合 Knif4j 应用完成 API 操作实践
### 1.1 概述
knife4j 是国人开发的一个基于 Swagger2 技术,为 Java MVC 框架生成 Api 文档的解决方案 , 前身是swagger-bootstrap-ui , 取名 knif4j 是希望她能像一把匕首一样小巧 , 轻量 , 并且功能强悍!
### 1.2 设计目标
目前的项目基本都是前后端分离,后端为前端提供接口的同时,还需同时提供接口的说明文档。但我们的代码总是会根据实际情况来实时更新,如果对这个方法做了修改 , 例如添加了一个新的参数 , 则需要修改这个接口文档,这个时候有可能会忘记更新接口的说明文档,造成一些不必要的问题。基于这个缺点 , 诞生了一个技术 , 接口文档自动生成工具 -- knife4j 。
换句话讲,Knif4j 就是帮你写接口说明文档的。
<img src="day04-1/image-20230213155144252.png" alt="image-20230213155144252" style="zoom:67%;" />
这里的接口不是 Java 中的 interface , 而是SpringMVC 的控制层 Controller 类的方法 , 都是通过 @ResponseBody注解返回 JSON 格式的数据。
接口文档,顾名思义就是接口的说明文档,它是我们调用接口的依据。好的接口文档包含了对接口URL,参数以及输出内容的说明,我们参照接口文档就能方便的知道接口的作用,以及接口如何进行调用。
### 1.3 初始准备工作
第一步:添加项目依赖,例如:
```xml
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
```
第二步:在application.yml配置文件中添加如下配置:
```properties
knife4j:
enable: true
```
第三步:创建配置类,例如:
```java
/**
* Knife4j(Swagger2)的配置类
*/
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfiguration {
//此方法创建API应用
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
// 创建接口文档的具体信息
.apiInfo(apiInfo())
// 创建选择器,控制哪些接口被加入文档
.select()
// 指定此包下的接口被加入文档
.apis(RequestHandlerSelectors.basePackage("cn.tedu.controller"))//重点
// 允许匹配所有的路径
.paths(PathSelectors.any())
.build();
}
//此方法创建该API的基本信息(这些基本信息会展示在文档页面中)
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
// 文档标题
.title("接口文档标题")
// 文档描述
.description("文档内容描述")
// 服务条款URL
.termsOfServiceUrl("http://www.xxx.com")
// 联系人信息
.contact(new Contact("baobao", "http://baobao.com", "baobao@qq.com"))
// 版本
.version("1.0")
.build();
}
}
```
如上代码所示,其中 :
- **@Configuration注解** : 表示此类为配置类 , 让Spring来加载该类配置。
- **@EnableSwagger2WebMvc** : 表示此类为 Knife4j 类。
- **DocumentationType.SWAGGER_2** : 告诉Docketbean我们正在使用Swagger规范的版本2。
- **apiInfo()** : 用来创建该Api的基本信息(这些基本信息会展现在文档页面中)。
- **select()创建一个构建器** : 用于定义哪些控制器及其生成的文档中应包含哪些方法。
- **apis()定义要包含的类(控制器和实体类)** : 这里包括当前包及子包中的类。
- **paths() **: 允许根据路径映射定义应包含哪个控制器的方法。
第四步:启动项目测试
完成后,启动项目,通过 http://localhost/doc.html 即可访问在线 API 文档。
### 1.4 常用注解应用分析
在 API 文档中,菜单中的名称默认是根据控制器类名、方法名转换得到的,通常会通过注解对类、方法、参数配置描述,常用注解说明如下:
- **@Api:**是添加在控制器类上的注解,通过此注解的tags属性可以修改原本显示控制器类名称的位置的文本,通常建议在配置的tags值上添加序号,例如:“1. 管理员模块”、“2. 商品模块”,则框架会根据值进行排序。
- **@ApiOperation:**是添加在控制器类中方法上的注解,用于配置此方法处理的请求在API文档中显示的文本。
- **@ApiOperationSupport:**是添加在控制器类中方法上的注解,通过配置其order属性可以指定各方法在API文档中的显示顺序。
- **@ApiModel: ** 用于类,表示对类进行说明,用于参数用实体类接收。
- **@ApiModelProperty:**是添加在POJO类的属性上的注解,用于对请求参数或响应结果中的某个属性进行说明,主要通过其value属性配置描述文本,并可通过example属性配置示例值,还可在响应结果时通过position属性指定顺序。
- **@ApiImplicitParam:**是添加在控制器类中方法上的注解,也可以作为@ApiImplicitParams注解的参数值,主要用于配置非封装的参数。
- **@ApiImplicitParams:**是添加在控制器类中方法上的注解,当方法有多个非封装的参数时,在方法上添加此注解,并在注解内部通过@ApiImplicitParam数组配置多个参数。
- **@ApiParam:**用于单个参数描述,与 @ApiImplicitParam不同的是,此注解是写在参数左侧的。
- **@ApiSupport(author = "xxx"):**整个Controller中的所有接口方法将被指定为该作者。
### 1.5 应用案例解析
#### 1.5.1 实体类Product
```java
package cn.tedu.pojo;
@Data
@ApiModel(value = "商品类", description = "用于描述商品信息")
public class Product {
@ApiModelProperty(value = "商品id",position = 1)
private Integer id;
/**商品标题*/
@ApiModelProperty(value = "商品标题",position = 2)
private String title;
/**商品url*/
@ApiModelProperty(value = "商品图片",position = 3)
private String url;
/**商品价格*/
@ApiModelProperty(value = "商品价格",position = 4)
private Double price;
/**商品原价*/
@ApiModelProperty(value = "商品原价",position = 5)
private Double oldPrice;
/**商品浏览量*/
@ApiModelProperty(value = "商品浏览量",position = 6)
private Integer viewCount; //浏览量
/**商品销量*/
@ApiModelProperty(value = "商品销量",position = 7)
private Integer saleCount;
/**商品发布时间*/
@ApiModelProperty(value = "商品发布时间",position = 8)
@JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss",timezone = "GMT+8")
private Date created; //发布时间 导包java.util
/**商品分类*/
@ApiModelProperty(value = "商品分类",position = 9)
private Integer categoryId; //商品分类id
}
```
#### 1.5.2 ProductController类
```java
package cn.tedu.controller;
@Api(tags = "3.商品处理类")
@ApiSupport(author = "tianhaohao")
@RestController
public class ProductController {
@Autowired
private ProductMapper productMapper;
/**查询前端系统首页商品列表信息*/
@ApiOperation("查询所有商品")
@ApiOperationSupport(order = 1)
@GetMapping("/product/list/index")
public List<Product> doSelectIndex(){
return productMapper.selectIndex();
}
/**查询商品销量并在前端系统中显示*/
@ApiOperation("查询排行榜商品")
@ApiOperationSupport(order = 2)
@GetMapping("/product/list/top")
public List<Product> doSelectTop(){
List<Product> products = productMapper.selectTop();
for(Product pro:products){
String title=pro.getTitle().substring(0,3)+"...";
pro.setTitle(title);
}
return products;
}
//@Transactional(readOnly = true)
/**基于id查询某个商品*/
@ApiOperation("基于id查询商品")
@ApiOperationSupport(order = 3)
@RequiredLog(operation = "浏览具体商品")
@GetMapping("/product/select/{id}")
public Product doSelectById(@PathVariable("id") Integer id){
//让浏览量+1
productMapper.updateViewCount(id);
return productMapper.selectById(id);
}
/**基于关键字查询某个商品*/
@ApiOperation("基于关键字查询商品")
@ApiOperationSupport(order = 4)
@GetMapping("/product/selectByWd/{keyWord}")
public List<Product> doSelectByWd(@PathVariable("keyWord") String keyWord){
return productMapper.selectByWd(keyWord);
}
/**
* 基于商品分类id查询商品信息
*/
@ApiOperation("基于分类查询商品")
@ApiOperationSupport(order = 5)
@GetMapping("/product/selectByCid/{categoryId}")
public List<Product> doSelectByCid(@PathVariable("categoryId") Integer categoryId){
return productMapper.selectByCid(categoryId);
}
/**发布新商品*/
@ApiOperation("发布商品")
@ApiOperationSupport(order = 7)
@PostMapping("/product/insert")
public void insert(@RequestBody @ApiParam("商品") Product product){
product.setCreated(new Date());
productMapper.insert(product);
}
/**查询后台系统商品列表信息*/
@ApiOperation("查询后台商品")
@ApiOperationSupport(order = 6)
@GetMapping("/product/list/admin")
public List<Product> doSelectAdmin() {
return productMapper.select();
}
/**删除商品信息*/
@ApiOperation("删除商品")
@ApiOperationSupport(order = 8)
@ApiImplicitParam(name = "id", value = "商品id", example = "1",
required = true, dataType = "int")
@Transactional(timeout = 30,
readOnly = true,
isolation = Isolation.READ_COMMITTED,
rollbackFor = Throwable.class,
propagation = Propagation.REQUIRED)
@DeleteMapping("/product/delete/{id}")
public void doDeleteById(@PathVariable("id") Integer id){
//通过id查询到商品的图片名字
String url = productMapper.selectUrlById(id);
//路径
if (url != null) {
String filePath = "d:/file/"+url;
new File(filePath).delete();
}
//删除数据库中的数据
productMapper.deleteById(id);
}
}
```
启动项目测试即可。
## 2 Spring AOP应用
### 2.1 AOP简介
AOP(Aspect Orient Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程(OOP)的一种补充和完善。它以通过预编译方式和运行期动态代理方式,实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。如图所示:
<img src="day04-1/image-20230129201142773.png" alt="image-20230129201142773" style="zoom:33%;" />
AOP与OOP字面意思相近,但其实两者完全是面向不同领域的设计思想。实际项目中我们通常将面向对象理解为一个静态过程(例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面的运行期代理方式,理解为一个动态过程,可以在对象运行时动态织入一些扩展功能或控制对象执行。
### 2.2 AOP原理分析
AOP可以在系统启动时为目标类型创建子类或兄弟类型对象 , 这样的对象我们通常会称之为动态代理对象。
- 第一种方式 : 借助JDK官方API为目标对象类型创建其兄弟类型对象 , 但是目标对象类型需要**实现接口**
<img src="day04-1/image-20230129201815403.png" alt="image-20230129201815403" style="zoom:67%;" />
- 第二种方式 : 借助CGLIB库为目标对象类型创建其子类类型对象 , 但是目标对象类型不能使用final修饰
<img src="day04-1/image-20230129201834802.png" style="zoom:67%;" />
### 2.3 AOP相关术语分析
- 切面(Aspect) : 横切面对象 , 一般为一个具体类对象(可以借助@Aspect声明)。
- 通知(Advice) : 在切面的某个特定连接点上执行的动作(扩展功能),例如Around , Before , After等。
- 连接点(JoinPoint) : 程序执行过程中某个特定的点 ,一般指被拦截到的的方法。
- 切入点(Pointcut) : 对多个连接点(JoinPoint)一种定义 , 一般可以理解为多个连接点的集合。
说明:我们可以简单的将机场来做比喻 ,切入点理解为多个安检口组成的安检链 , 连接点理解为其中的一个安检口 , 通知理解为安全检查的过程。总之,概念很晦涩难懂,多做例子,做完就会清晰。
### 2.4 AOP业务分析及入门实现
#### 2.4.1 添加AOP依赖
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
```
说明 : 基于此依赖Spring可以整合AspectJ框架快速完成AOP的基本实现。AspectJ 是一个面向切面的框架,他定义了AOP的一些语法,有一个专门的字节码生成器来生成遵守java规范的class文件。
#### 2.4.2 创建日志切面类对象
将此日志切面类作为核心业务增强(一个横切面对象)类,用于输出业务执行时长,其关键代码如下:
```java
package cn.tedu.aspect;
/**
* @Aspect 注解描述的类型为一个切面类型(AOP 中的横切面类型),这样的切面类型中通常会定义两部分内容:
* 1)切入点:切入扩展功能的点(例如业务对象中的一个或多个方法)
* 2)通知:在切点对应的方法执行时,要织入的扩展功能。
*/
@Aspect
@Slf4j
@Component
public class SysLogAspect {
/**
* @Pointcut 注解一般用于描述方法,在方法上定义切入点。
* bean(productController) 为一个切入点表达式
*/
@Pointcut("bean(productController)")
public void logPointCut() {}//方法内部不需要写任何内容
/**
* 通知,在通知中写额外增强功能的代码
*/
@Around("logPointCut()")
public Object around(ProceedingJoinPoint jp)throws Throwable{
//记录方法执行时的开始时间
long t1=System.currentTimeMillis();
try {
//调用目标方法
Object result=jp.proceed();//调用本切面中其它通知或下一个切面的通知或目标方法
//记录方法执行的结束时间以及总时长。
long t2=System.currentTimeMillis();
log.info("method execute time {}",(t2-t1));
return result;
}catch(Throwable e) {
//出现异常时还要输出错误日志。
log.error("error is {} ",e.getMessage());
throw e;
}
}
}
```
说明:
- @Aspect 注解用于标识或者描述AOP中的切面类型,基于切面类型构建的对象用于为目标对象进行功能扩展或控制目标对象的执行。
- @Pointcut注解用于描述切面中的方法,并定义切面中的切入点(基于特定表达式的方式进行描述),在本案例中切入点表达式用的是bean表达式,这个表达式以bean开头,bean括号中的内容为一个Spring管理的某个bean对象的名字。
- @Around注解用于描述切面中方法,这样的方法会被认为是一个环绕通知(核心业务方法执行之前和之后要执行的一个动作),@Aournd注解内部value属性的值为一个切入点表达式或者是切入点表达式的一个引用(这个引用为一个@PointCut注解描述的方法的方法名)。
- ProceedingJoinPoint类为一个连接点类型,此类型的对象用于封装要执行的目标方法相关的一些信息。用于@Around注解描述的方法参数。
#### 2.4.3 运行原理
如图所示:
<img src="day04-1/image-20230129210255572.png" alt="image-20230129210255572" style="zoom: 50%;" />
### 总结 :
#### 重难点分析 :
1. Knif4j 各个注解的作用
2. 为什么使用AOP? AOP的好处是什么?
3. AOP是如何添加额外功能的?