简介

Gradle是一个基于Apache Ant和Apache Maven概念的项目自动化构建开源工具。它使用一种基于Groovy的特定领域语言(DSL)来声明项目设置,也增加了基于Kotlin语言的kotlin-based DSL,抛弃了基于XML的各种繁琐配置。

参考资料

环境简介:

  • 之前用的 SpringBoot 2.3.12 + JDK11 + Maven 来构建的,这里做了一次升级,主要记录升级中遇到的问题和解决方法后期好参考
  • JDK 17.02
  • Gradle 7.4.1
  • SpingBoot 2.6.4
  • IDEA 2021.3

flyway配置参考

快速上手

简介

  • 注意注意如果你的磁盘是ExFAT 格式是无法创建 gradle 工程的,这是一个官方一致存在的问题也是之前困扰了我很久,因此你需要在其它磁盘上新建
  • Gradle 是一个比 maven 更加先进的构建工具,经过体验觉得配置方面比 maven 的 pom.xml 要简洁很多,构建速度也有明显的提升
  • 本次笔记主要用于记录将我刚写的测试平台由原来的 maven 换成 gradle 的一些流程
  • 本次还将原来的 jdk11 换成了 jdk17
  • 前提需要在本地安装 jdk17 和 gradle,本次使用的都是当时最新版本 jdk17.0.2 gradle7.4.1

快速搭建

  • 这里都是用命令操作,因为 IDEA 如果新建一个 gradle 工程默认会使用它当前的版本,我觉得不是最新的,我想用自己的(当然也可以通过配置指定自己的,但是这样如果拉取了别人的项目,别使用的版本和我们指定的版本不一致所以就可能会有一些冲突)
  • 本地新建一个文件夹如 automation,然后用 IDEA 打开
  • 在当前 automation 下新建一个 common 文件夹 mkdir common
  • 新建文件 touch settings.gradle.kts (gradle 有两种DSL,领域特定语言去定义它的脚本,一种是早期的 Groovy,在后期新增了对Kotlin 语言的支持定义脚本,也就是这里的kts
  • 执行初始化 gradle wrapper gradle wrapper 命令会帮我们把 geadle 重新包一层,用 gradlew 命令去替代 gradle 命令,如果我们后期需要更新我们项目的 gradle 版本只需要更新 wrapper 里面的gradle-wrapper.properties,这样我们就可以在不同的版本里面随意切换,并且我们的 wrapper 被提交到代码仓库以后别人也会只用这个 wrapper 可以防止版本不同造成的一些问题
  • 注意事项:导入的时候应该选第二个从外部模型导入 Import module from external model 如果选第一个会导致你用 Gradle 管理 module 的方式和 IDEA管理 module 方式之间的一些冲突

快速搭建

基础配置

  • settings.gradle.kts
1
rootProject.name = "common"
  • build.gradle.kts (先创建这个文件,该文件主要构建的配置相关内容)
1
2
3
plugins{
java
}

模块依赖

  • 参考:多模块依赖
  • 描述:我当前有个项目叫做 automation 项目下目前有两个模块,common manage 后期还会有其它的模块
  • 需求:将 automation 当做一个顶层项目管理公共的依赖配置项,common 做为子模块的基础服务提供基础的项目配置和一些工具类,manage 需要依赖于 common
  • 实现重点:包名前缀和 group 保持一致 automation 中用 include 包含 common 需要用添加tasks.bootJar 和 tasks.jar manage中 implementation(project(":common"))

模块依赖

配置参考项

automation

  • 父项目的配置
1
2
3
rootProject.name="automation"
include("common")
include("manage")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
java
id("org.springframework.boot") version "2.6.4"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.6.10"
kotlin("plugin.spring") version "1.6.10"
}

group = "com.adalucky"
version = "1.0.0"
java.sourceCompatibility = JavaVersion.VERSION_17

configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}

repositories {
maven {
setUrl("https://maven.aliyun.com/repository/public")
}
mavenLocal()
mavenCentral()
}

//公共依赖的配置,apply 需要在 dependencies 前面
subprojects {
apply {
plugin("org.springframework.boot")
plugin("io.spring.dependency-management")
plugin("org.jetbrains.kotlin.jvm")
plugin("org.jetbrains.kotlin.plugin.spring")
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
runtimeOnly("mysql:mysql-connector-java")

implementation("com.baomidou:mybatis-plus-boot-starter:3.5.1")
implementation("com.github.xiaoymin:knife4j-spring-boot-starter:3.0.3")
implementation("p6spy:p6spy:3.9.1")
implementation("com.alibaba:easyexcel:2.2.6")

//easyExcel jdk17 报错 https://blog.csdn.net/weixin_42792301/article/details/121456156
implementation("org.burningwave:core:12.47.0")

compileOnly("org.projectlombok:lombok")
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("mysql:mysql-connector-java")
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}

tasks.withType<Test> {
useJUnitPlatform()
}
}

common

  • 基础模块,这里主要是 mybatis-plus 的代码生成需要的
  • 需要加入 tasks.bootJar tasks.jar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
repositories {
maven {
setUrl("https://maven.aliyun.com/repository/public")
}
mavenLocal()
mavenCentral()
}

dependencies {
annotationProcessor("org.projectlombok:lombok")
compileOnly("org.projectlombok:lombok")
implementation("com.baomidou:mybatis-plus-generator:3.5.1")
implementation("org.apache.velocity:velocity-engine-core:2.3")

}

tasks.bootJar {
enabled = false
}

tasks.jar {
enabled = true
}

manage

  • 后台管理模块,主要是需要用 implementation 关联依赖的项目
1
2
3
4
5
6
7
8
9
10
11
12
13
repositories {
maven {
setUrl("https://maven.aliyun.com/repository/public")
}
mavenLocal()
mavenCentral()
}

dependencies{
implementation(project(":common"))
annotationProcessor("org.projectlombok:lombok")
compileOnly("org.projectlombok:lombok")
}

优雅编程

自定义 git 命令

  • 当前环境为 Mac,在全局配置新增一个方法示例如下(配置后需要执行 source /etc/profile 最好是在 ~/.zshrc 中添加 source /etc/profile 大致就是这个意思,不过每个人的电脑用的 zsh 不一样)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function gacp() {
log=$1 files=$2
if [ ! -n "$log" ]; then
echo "必须输入提交说明"
return
fi
if [ ! -n "$files" ]; then
files="."
fi

git add .
git commit -a -m "$1"
git push
}

单元测试

  • 流程:在 TDD 的模式下是测试驱动开发,先写单元测试,再写业务逻辑代码,
  • 添加注解 @SpringBootTest @AutoConfigureMockMvc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.adalucky.modules.system.controller;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class SysUserControllerTest {
@Autowired
MockMvc mockMvc;
@Test
void list() throws Exception {
mockMvc
.perform(get("/user/list"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("成功"))
.andExpect(jsonPath("$.code").value("200"))
.andExpect(jsonPath("$.success").value("true"))

;
}

}

MockMvc单元测试

健康检查

  • 我们需要对 SpringBoot 做一个心跳检查,开放一个 get 请求,保证服务是正常启动的
  • 引入依赖 implementation("org.springframework.boot:spring-boot-starter-actuator")
  • get请求 ip:port/actuator/health 如果正常的话会返回 {“status”:”UP”}
  • 代码示例如下:其实和上面的单元测试是一样的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.adalucky;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
* @author ada
* @Version JDK17
* @Description 心跳检查
* @since 2022/3/19 15:57
*/
@SpringBootTest
@AutoConfigureMockMvc
public class SmokeTest {
@Autowired
MockMvc mockMvc;

@Test
void checkHeartbeat() throws Exception {
mockMvc
.perform(get("/actuator/health"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("UP"))
;
}
}

githooks

  • 需求:为了保证我们每次提交的代码都是无误的,那么可以配置当我们代码通过单元测试才能提交到远程仓库,这样是一个比较好的规范
  • 实现:①项目根目录创建 githooks 文件夹 ②该文件夹下新建 pre-commit 文件,并编写脚本 ③给该脚本赋予执行权限 ④git config 中指定文件路径(建议通过指定路径的方式,有些人是 直接用自己编写的 pre-commit 替换 .git 下的文件,这里不太推荐,因为我们的脚本可能后面还要修改,这样提交到远程后协同工作下大家都能享用最新的脚本)
  • pre-commit 脚本示例
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env sh
# 根目录下新建 githooks 文件夹,下面再新建一个 pre-commit 文件
# q 静默执行不输出内容; k keep index; u 没有被跟踪的代码会被 stash 掉
git stash -qku
# 执行检查,这里是用的当前项目根目录下的通过快速搭建中说的 gradle wrapper 帮我们把 geadle 重新包一层,用 gradlew 命令去替代 gradle 命令
./gradlew clean check
# 存储上一条命令执行的结果
RESULT=$?
git stash pop -q
exit $RESULT
# 文件需要赋予 755 可执行权限 chmod 755 githooks/pre-commit
# 配置 githooks path 命令--> git config core.hooksPath githhooks

githooks

flyway

  • flyway 通过版本管理的方式,我们可以比较方便的去定义每一次数据 schema 的变更

  • 依赖:runtimeOnly("org.flywaydb:flyway-core") 我这里没有指定版本,因为在 SpringBoot 的 BOM 中已经被定义过了

  • 创建目录: src/main/resources/db/migration (这是 flyway 的默认路径)

  • 创建 sql 文件: 项目版本号_sql号+内容.sql,例 V1.0.0_1__ceract_sysuser_table.sql 当前的 tag 为 1.0.0 为他执行的第一个 sql,内容是创建用户表

  • application.yml配置(如果没配的话可能会报 flywayInitializer 相关的错误):

    1
    2
    3
    spring:
    flyway:
    baseline-on-migrate: true
  • 注意事项:①1 和 creact 之间是两个下划线,这是官方的命名规范要求,如果按照这个规范可能只会生成 flyway_schema_history表,而没有执行 sql 的内容 ②每一个迁移脚本被执行后是不可以去修改的,因为 flyway 会对整个文件做一个 MD5 然后去进行检查,第二次就不会执行了,因此有变更的话需要定义第二个版本

flyway

test-containers

前置条件

  • 本地需要提前安装好 docker ,确保守护进程已经启动,并已启动
  • 我尝试过通过 brew install docker 但是并没发启动它的守护进程,只好安装桌面应用

简介

  • 需求:在单元测试中我们需要连接到测试的数据库,当然可以再 application.yml 中指定另外的 dev.yml 但是不能解决测试库也有历史数据的问题,我更加希望是每次启动单元测试的时候就会启动一个新的数据库,并且表内是没有任何数据的
  • 实现:test-containers 能够帮助我们在启动单元测试的时候通过 java-docker 这样一个库来连接我们本地 docker socks 上,并且通过 docker 去启动对应的容器,在测试期间我们去连接这个容器,测试完毕后自动销毁
  • 依赖: testImplementation("org.testcontainers:testcontainers:1.16.3") testImplementation("org.testcontainers:mysql:1.16.3")

实现

创建配置

  • 在 test 下新建一个配置类例如 DatabaseTestConfiguration
  • 注意添加 Wait.forListeningPort() 这样确保启动成功后再配置 DataSource
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.adalucky.core;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.autoconfigure.flyway.FlywayDataSource;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.wait.strategy.Wait;

import javax.sql.DataSource;

/**
* @author ada
* @Version JDK17
* @Description testcontainers 测试环境本地启动数据库
* @since 2022/3/20 19:11
*/
public class DatabaseTestConfiguration {
/**
* 创建 docker 容器对象
* initMethod = "start",destroyMethod = "stop" 初始这个 bean 时候开启,停止时销毁容器
* 根据镜像设置不同的类型,如果是 PostgreSQL 就用 PostgreSQLContainer<?>
*/
@Bean(initMethod = "start", destroyMethod = "stop")
public MySQLContainer<?> mysqlContainer() {
// 通过 docker hub 找到对应的镜像和版本号,并等待端口完全初始化完毕,不然下面配置数据源 new HikariDataSource 时容器还没完全初始化完毕会有报错
return new MySQLContainer<>("mysql:8.0.27").waitingFor(Wait.forListeningPort());
}

/**
* 配置数据源信息
* FlywayDataSource flyway 也会使用这个 DataSource 进行初始化
* mysqlContainer 上面创建的容器对象
*/
@Bean
@FlywayDataSource
public DataSource dataSource(MySQLContainer<?> mysqlContainer) {
var hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl(mysqlContainer.getJdbcUrl());
hikariConfig.setUsername(mysqlContainer.getUsername());
hikariConfig.setPassword(mysqlContainer.getPassword());
return new HikariDataSource(hikariConfig);
}
}

编写用例

  • 编写单元测试用例,每次执行完后数据是自动销毁的,所以每次都会重新执行 flyway
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.adalucky.modules.system.mapper;

import com.adalucky.core.DatabaseTestConfiguration;
import com.adalucky.enums.mysql.SexEnums;
import com.adalucky.enums.mysql.StatusEnums;
import com.adalucky.modules.system.entity.SysUser;
import com.adalucky.modules.system.mapper.SysUserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

import static org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace.NONE;

/**
* @author ada
* @Version JDK17
* @Description TODO
* @since 2022/3/20 19:44
*/
@SpringBootTest
// 关闭 SpringBoot 默认的 H2 数据源
@AutoConfigureTestDatabase(replace = NONE)
@Import(DatabaseTestConfiguration.class)
public class DataSourceTest {
@Autowired
SysUserMapper mapper;
@Test
void add(){
SysUser sysUser = new SysUser();
sysUser.setLoginName("13599998888");
sysUser.setNikeName("测试save456");
sysUser.setSex(SexEnums.男);
sysUser.setStatus(StatusEnums.启用);
mapper.insert(sysUser);
mapper.selectList(null).forEach(System.out::println);

}
}

组合注解

  • 需求:在上面的 test-containers 中,我们每次都需要定义三个注解来完成,可以通过自定义注解实现包含上面三个注解后,来完成一个注解替代
  • 实现示例:每次使用的使用就是 @类名注入 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.adalucky.core;

import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

import static org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace.NONE;

/**
* @author ada
* @Version JDK17
* @Description 自定义的组合注解 通过 @MysqlContainerTest 完成注入(类名)
* @since 2022/3/20 23:15
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootTest
// 关闭 SpringBoot 默认的 H2 数据源
@AutoConfigureTestDatabase(replace = NONE)
@Import(DatabaseTestConfiguration.class)
public @interface MysqlContainerTest {
}

配置文件

  • 在我们编写一个项目的时候,我们的代码其实是和我们的环境是无关的,也就是我们提交到远程仓库的时候是不应该提交我们配置文件application.yml
  • 方案:①创建一个 application-local.yml 配置我们本地运行的环境,并把该文件添加到 .gitignore ②创建一个 application-tmeplate.yml 给出配置示例,该文件提交到远程仓库 ③创建一个 application.yml 文件用于拉取代码后参照 application-env.tmeplate.yml 进行配置 ④本地idea 指定运行环境为 local
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 指定端口
server:
port: 9090

# spring 相关集成配置
spring:
# flyway 配置
flyway:
baseline-on-migrate: true
# swagger 报错配置
mvc:
pathmatch:
matching-strategy: ant_path_matcher
# 数据源配置
datasource:
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
url: jdbc:p6spy:mysql://192.168.1.115:3306/data_auto_endpoint?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
username: root
password: 123456

# Mybatis-Plus 配置
mybatis-plus:
type-enums-package: com.adalucky.enums.mysql
global-config:
db-config:
#全局配置,主键自动增长,就不需要在每个实体类上声明了 @TableId(type = IdType.AUTO)
id-type: auto
logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0版本以上就不需要在实体中对该字段添加@TableLogic注解)
logic-delete-value: 0 # 逻辑已删除值(默认为 1.我们改为 0)
logic-not-delete-value: 1 # 逻辑未删除值(默认为 0,我们改为 1)
mapper-locations: classpath*:/mapper/*.xml #不定义的话 springboot 不知道你的 xml 放在哪里的

# 日志输出配置
logging:
level:
root: info
com.adalucky.modules: debug

本地文件配置

代码规范检查

基础配置

  • 通过 checkstyle 进行代码的规范性检查
  • 引入依赖插件 plugins 中引入 checkstyle 并在全局中进行配置 apply 和 checkstyle 最大的警告数和版本号
  • 完整示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
java
checkstyle
id("org.springframework.boot") version "2.6.4"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.6.10"
kotlin("plugin.spring") version "1.6.10"
}

group = "com.adalucky"
version = "1.0.0"
java.sourceCompatibility = JavaVersion.VERSION_17

configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}

repositories {
maven {
setUrl("https://maven.aliyun.com/repository/public")
}
mavenLocal()
mavenCentral()
}


subprojects {
apply {
plugin("org.springframework.boot")
plugin("io.spring.dependency-management")
plugin("org.jetbrains.kotlin.jvm")
plugin("org.jetbrains.kotlin.plugin.spring")
plugin("checkstyle")
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
runtimeOnly("mysql:mysql-connector-java")

implementation("com.baomidou:mybatis-plus-boot-starter:3.5.1")
implementation("com.baomidou:mybatis-plus-boot-starter-test:3.5.1")
implementation("com.github.xiaoymin:knife4j-spring-boot-starter:3.0.3")
implementation("p6spy:p6spy:3.9.1")
implementation("com.alibaba:easyexcel:2.2.6")

//easyExcel jdk17 报错 https://blog.csdn.net/weixin_42792301/article/details/121456156
implementation("org.burningwave:core:12.47.0")

compileOnly("org.projectlombok:lombok")
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("mysql:mysql-connector-java")
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
checkstyle {
maxWarnings = 0
toolVersion = "10.0"
}

tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}

tasks.withType<Test> {
useJUnitPlatform()
}
}

规范配置

  • 在父项目根目录下新建一个 config/ckeckstyle/checkstyle.xml 文件,内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="warning"/>
<property name="fileExtensions" value="java, properties, xml"/>
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value="module\-info\.java$"/>
</module>
<module name="FileTabCharacter">
<property name="eachLine" value="true"/>
</module>
<module name="LineLength">
<property name="fileExtensions" value="java"/>
<property name="max" value="200"/>
<property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
</module>
<module name="TreeWalker">
<module name="UnusedImports"/>
<module name="OuterTypeFilename"/>
<module name="IllegalTokenText">
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
<property name="format"
value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
<property name="message"
value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
</module>
<module name="AvoidEscapedUnicodeCharacters">
<property name="allowEscapesForControlCharacters" value="true"/>
<property name="allowByTailComment" value="true"/>
<property name="allowNonPrintableEscapes" value="true"/>
</module>
<!-- 不应使用 '.*' 形式的导入 - org.springframework.web.bind.annotation.* 。 [AvoidStarImport] -->
<!--
<module name="AvoidStarImport"/>
-->
<module name="OneTopLevelClass"/>
<module name="NoLineWrap">
<property name="tokens" value="PACKAGE_DEF, IMPORT, STATIC_IMPORT"/>
</module>
<module name="EmptyBlock">
<property name="option" value="TEXT"/>
<property name="tokens" value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
</module>
<module name="LeftCurly">
<property name="tokens" value="ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF, INTERFACE_DEF, LAMBDA, LITERAL_CASE, LITERAL_CATCH,
LITERAL_DEFAULT, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY,
LITERAL_WHILE, METHOD_DEF, OBJBLOCK, STATIC_INIT, RECORD_DEF, COMPACT_CTOR_DEF"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlySame"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_DO"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlyAlone"/>
<property name="option" value="alone"/>
<property name="tokens"
value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT, INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, INTERFACE_DEF, RECORD_DEF, COMPACT_CTOR_DEF"/>
</module>
<module name="SuppressionXpathSingleFilter">
<property name="id" value="RightCurlyAlone"/>
<property name="query"
value="//RCURLY[parent::SLIST[count(./*)=1] or preceding-sibling::*[last()][self::LCURLY]]"/>
</module>
<module name="WhitespaceAfter">
<property name="tokens"
value="COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE, LITERAL_WHILE, LITERAL_DO, LITERAL_FOR, DO_WHILE"/>
</module>
<module name="WhitespaceAround">
<property name="allowEmptyConstructors" value="true"/>
<property name="allowEmptyLambdas" value="true"/>
<property name="allowEmptyMethods" value="true"/>
<property name="allowEmptyTypes" value="true"/>
<property name="allowEmptyLoops" value="true"/>
<property name="ignoreEnhancedForColon" value="false"/>
<property name="tokens" value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR, BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE,
EQUAL, GE, GT, LAMBDA, LAND, LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, LITERAL_RETURN,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN, NOT_EQUAL, PLUS, PLUS_ASSIGN,
QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR, SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND"/>
<message key="ws.notFollowed"
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
<message key="ws.notPreceded" value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
</module>
<module name="OneStatementPerLine"/>
<module name="MultipleVariableDeclarations"/>
<module name="ArrayTypeStyle"/>
<module name="MissingSwitchDefault"/>
<module name="FallThrough"/>
<module name="UpperEll"/>
<module name="ModifierOrder"/>
<module name="EmptyLineSeparator">
<property name="tokens"
value="PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF, STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF, RECORD_DEF, COMPACT_CTOR_DEF"/>
<property name="allowNoEmptyLineBetweenFields" value="true"/>
<property name="allowMultipleEmptyLinesInsideClassMembers" value="false"/>
<property name="allowMultipleEmptyLines" value="false"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapDot"/>
<property name="tokens" value="DOT"/>
<property name="option" value="nl"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapComma"/>
<property name="tokens" value="COMMA"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapEllipsis"/>
<property name="tokens" value="ELLIPSIS"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapArrayDeclarator"/>
<property name="tokens" value="ARRAY_DECLARATOR"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapMethodRef"/>
<property name="tokens" value="METHOD_REF"/>
<property name="option" value="nl"/>
</module>
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
<message key="name.invalidPattern" value="Package name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="TypeName">
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, RECORD_DEF"/>
<message key="name.invalidPattern" value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MemberName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern" value="Member name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern" value="Parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LambdaParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern" value="Lambda parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="CatchParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern" value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LocalVariableName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern" value="Local variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="PatternVariableName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern" value="Pattern variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ClassTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern" value="Class type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="RecordTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern" value="Record type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MethodTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern" value="Method type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="NoFinalizer"/>
<module name="GenericWhitespace">
<message key="ws.followed" value="GenericWhitespace ''{0}'' is followed by whitespace."/>
<message key="ws.preceded" value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
<message key="ws.illegalFollow" value="GenericWhitespace ''{0}'' should followed by whitespace."/>
<message key="ws.notPreceded" value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
</module>
<module name="Indentation">
<property name="basicOffset" value="4"/>
<property name="braceAdjustment" value="4"/>
<property name="caseIndent" value="4"/>
<property name="throwsIndent" value="4"/>
<property name="lineWrappingIndentation" value="4"/>
<property name="arrayInitIndent" value="4"/>
</module>
<module name="AbbreviationAsWordInName">
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="0"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF, PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF, PATTERN_VARIABLE_DEF, RECORD_DEF, RECORD_COMPONENT_DEF"/>
</module>
<module name="OverloadMethodsDeclarationOrder"/>
<module name="VariableDeclarationUsageDistance"/>
<module name="MethodParamPad">
<property name="tokens"
value="CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF, SUPER_CTOR_CALL, ENUM_CONSTANT_DEF, RECORD_DEF"/>
</module>
<module name="NoWhitespaceBefore">
<property name="tokens" value="COMMA, SEMI, POST_INC, POST_DEC, DOT, LABELED_STAT, METHOD_REF"/>
<property name="allowLineBreaks" value="true"/>
</module>
<module name="ParenPad">
<property name="tokens"
value="ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_CALL, CTOR_DEF, DOT, ENUM_CONSTANT_DEF, EXPR, LITERAL_CATCH, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW, LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_WHILE, METHOD_CALL, METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA, RECORD_DEF"/>
</module>
<module name="OperatorWrap">
<property name="option" value="NL"/>
<property name="tokens"
value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR, LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF, TYPE_EXTENSION_AND "/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationMostCases"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, RECORD_DEF, COMPACT_CTOR_DEF"/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationVariables"/>
<property name="tokens" value="VARIABLE_DEF"/>
<property name="allowSamelineMultipleAnnotations" value="true"/>
</module>
<module name="NonEmptyAtclauseDescription"/>
<module name="InvalidJavadocPosition"/>
<module name="JavadocTagContinuationIndentation"/>
<!-- Javadoc的第一句缺少一个结SummaryJavadoc -->
<!-- <module name="SummaryJavadoc">-->
<!-- <property name="forbiddenSummaryFragments" value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>-->
<!-- </module>-->
<module name="JavadocParagraph"/>
<module name="RequireEmptyLineBeforeBlockTagGroup"/>
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
<property name="target" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
</module>
<module name="MethodName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>
<message key="name.invalidPattern" value="Method name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="SingleLineJavadoc"/>
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="expected"/>
</module>
<module name="CommentsIndentation">
<property name="tokens" value="SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN"/>
</module>
<module name="SuppressionXpathFilter">
<property name="file" value="${org.checkstyle.google.suppressionxpathfilter.config}"
default="checkstyle-xpath-suppressions.xml"/>
<property name="optional" value="true"/>
</module>
</module>
</module>

checkstyle

接口入参校验

背景

  • 在前后端分离的项目中,很多程序的校验都是放在前端,例如 vue 的项目中可以自定义表单的校验规则 rule,但是这个只能控制前端用户通过界面的输入校验,并不能控制通过接口工具时用户不合法的输入造成的接口

目的

  • 不做参数校验带来的问题:①入库异常(数据库字段长度为50,实际数据长度为100)②NPE异常(未接收到参数就进行调用)③通过 if else 硬编码 方式会随着校验参数的个数增多而增多,不利于代码阅读(500行代码,250行都在用if…else做参数校验 ④需要重复校验,调用不同的接口,传入的参数比较类似,需要在不同的接口重新校验一遍
  • 做参数校验的好处:①减少已知可规避的风险 ②减少代码量,工作量

if-else进行参数校验

项目引用

背景

  • Java API规范 (JSR303) 定义了Bean校验的标准validation-api,但没有提供实现
  • hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length等
  • Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验

依赖引入

  • 如果 SpringBoot 版本小于2.3.x,spring-boot-starter-web 会自动传入hibernate-validator依赖
  • 如果 SpringBoot 版本大于2.3.x,则需要手动引入依赖
  • 当前项目为 SpringBoot 2.6.4 ,通过 gradle 构建
1
implementation("org.hibernate:hibernate-validator:8.0.0.CR2")

引入 hibernate-validator

快速上手

入参校验注解

  • 对入参 HospitalConfigDTO 对象的 hospitalCode 字段进行参数校验,限定为 3-9 位

参数校验示例

请求测试

  • 注意这里返回的状态码是 400 还有响应体格式很乱,这个后面需要统一优化
  • 响应的提示信息不同的注解会有对应的默认 message,也可自行在注解上添加 @Size(min = 3,max=9,message = "hospitalCode 字段的长度需要控制在 3-9 位")

请求测试

SpringBoot 依赖冲突

  • 当前所使用的 SpringBoot 为 2.6.4,所以其它 SpringBoot 的场景启动器都会是 2.6.4

swagger

跨域问题

  • 当我从 2.3.12 升级到 2.6.4 后有跨域问题,之前有配置跨域但是不起作用
  • 修改原来的跨域自动配置类和 yml 文件
  • yml 配置
1
2
3
4
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
  • 自动配置类示例如下(如果加了以后还有访问 404 的吧下面 knife4j与actuator依赖冲突 的 bean 加在 swagger 的自动配置类中)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.adalucky.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
* @author ada
* @Version JDK17
* @Description 跨域配置方案不然 SpringBoot2.6.x 访问会有一些报错
* @since 2022/3/19 19:34
*/
@Configuration
public class CrosConfig {
private CorsConfiguration corsConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOriginPattern("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setMaxAge(3600L);
return corsConfiguration;
}

@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig());
return new CorsFilter(source);
}
}

knife4j与actuator依赖冲突

  • 参考 knife4j与actuator依赖冲突
  • 这里采用的是 com.github.xiaoymin:knife4j-spring-boot-starter:3.0.3 集成 swagge 只需要引用这一个依赖,当我的项目引入了 org.springframework.boot:spring-boot-starter-actuator:2.6.4 后项目启动报错 org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
  • 在我们 swagger 的自动配置类中加入如下内容,注意依赖别导错了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.adalucky.config;

import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.web.*;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Configuration
//@EnableSwagger2
@EnableOpenApi
public class SwaggerConfiguration {
/**
* 解决springboot升级到2.6.x之后,knife4j报错
*
* @param webEndpointsSupplier the web endpoints supplier
* @param servletEndpointsSupplier the servlet endpoints supplier
* @param controllerEndpointsSupplier the controller endpoints supplier
* @param endpointMediaTypes the endpoint media types
* @param corsProperties the cors properties
* @param webEndpointProperties the web endpoint properties
* @param environment the environment
* @return the web mvc endpoint handler mapping
*/
@Bean
public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(
WebEndpointsSupplier webEndpointsSupplier, ServletEndpointsSupplier servletEndpointsSupplier,
ControllerEndpointsSupplier controllerEndpointsSupplier, EndpointMediaTypes endpointMediaTypes,
CorsEndpointProperties corsProperties, WebEndpointProperties webEndpointProperties,
Environment environment) {
List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
allEndpoints.addAll(webEndpoints);
allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
String basePath = webEndpointProperties.getBasePath();
EndpointMapping endpointMapping = new EndpointMapping(basePath);
boolean shouldRegisterLinksMapping = shouldRegisterLinksMapping(webEndpointProperties,
environment, basePath);
return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes,
corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath),
shouldRegisterLinksMapping, null);
}

/**
* shouldRegisterLinksMapping
*
* @param webEndpointProperties webEndpointProperties
* @param environment environment
* @param basePath /
* @return boolean
*/
private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties,
Environment environment, String basePath) {
return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath)
|| ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("测试平台API文档")
.description("自动化测试API文档")
.termsOfServiceUrl("www.adalucky.com")
.contact(new Contact("Ada", "www.adalucky.com", "958472019@qq.com"))
.version("1.0")
.build();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Bean
public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
return new BeanPostProcessor() {

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
}
return bean;
}

private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {
mappings.removeIf(mapping -> mapping.getPatternParser() != null);
}

@SuppressWarnings("unchecked")
private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
try {
Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
field.setAccessible(true);
return (List<RequestMappingInfoHandlerMapping>) field.get(bean);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
};
}

EasyExcel

  • EasyExcel 依赖版本:implementation(“com.alibaba:easyexcel:2.2.6”)
  • JDK 17

文件导出报错

  • 参考 JDK17 EasyExcel 导出报错 文章中用的是 9.5.2 我换成最新的 12.47.0 也是可以的
  • 问题描述,当我的在 JDK11 上运行时正常,升级到 17.0.2 后导出时报错 Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not “opens java.lang” to unnamed module @61832929
  • 看过一些网上的分析大多数都是说的 EasyExcel 依赖的 cglib 它又依赖于 asm4.2,与 SpringBoot2.6.x底层依赖的 asm3.x 有冲突,重新引入 cglib2.2 即可,不过我这里没有生效,后面百度说是 jdk17 和 EasyExcel 的一些问题
  • 解决方案:引入 implementation("org.burningwave:core:12.47.0") EasyExcel 工具类中添加 StaticComponentContainer.Modules.exportAllToAll();
  • 完整示例如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package com.adalucky;

import com.adalucky.config.CustomCellWriteHandler;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import org.burningwave.core.assembler.StaticComponentContainer;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.LinkedList;
import java.util.List;

/**
* @author ada
* @Version JDK11
* @since 2022/3/15 22:35
* @Description EasyExcel 工具类
*/
public class ExelUtil {
/**
* @param response 浏览器请求对象,必须在接口中定义一个 HttpServletResponse
* @param list 需要写入Excel的数据
* @param clazz 实体类(和 Excel 中的数据对应)
* @param fileName 写出的文件名
* @param <T> 泛型封装 List
* @return 返回 ExcelWriterBuilder 对象,通过调用 ExelUtil.writeExcel(args...).sheet("模板").doWrite(dataList);
* @throws IOException
*/
public static <T> ExcelWriterBuilder writeExcel(HttpServletResponse response, List<T> list, Class clazz, String fileName) throws IOException {
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
//设置输出流返回的格式为 json 和 utf-8编码
//设置成excel application/vnd.ms-excel 会报错 ,因为我们是自己封装的返回对象,No converter for [class com.adalucky.response.Result] with preset Content-Type 'application/vnd.ms-excel;charset=utf-8'
//response.setContentType("application/vnd.ms-excel");

StaticComponentContainer.Modules.exportAllToAll(); //jdk17 报错

response.setContentType("application/json;charset=utf-8");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系,自定义文件名
String name = URLEncoder.encode(fileName, "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + name + ".xlsx");
//写入的sheet 名称,应该可以用连缀的写法多个 sheet,然后 doWrite
return EasyExcel.write(response.getOutputStream(), clazz)
//自适应列宽
.registerWriteHandler(new CustomCellWriteHandler());
}

public static <T> List<T> readExcel(MultipartFile file, String sheet, Class<T> clazz) throws IOException {
List<T> readList = new LinkedList<>();
EasyExcel.read(file.getInputStream())
// 原本返回的LinkHashMap是所以这里需要用我们自己的实体类去加载就自动转成ExcelData类型
.head(clazz)
.sheet(sheet)
.registerReadListener(new AnalysisEventListener<T>() {
@Override
public void invoke(T execelData, AnalysisContext analysisContext) {
readList.add(execelData);
}

@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}).doRead();
return readList;
}

public static Object index(String sheetName) {
if (sheetName != null) {
return sheetName;
} else {
return 0;
}
}
}

JDK17新特性

  • 这里使用的是 JDK 17.0.2 LTS,下面将会记录从 JDK8 升级到 JDK17 用到的一些新语法,并不一定都是 17 的语法

String

isBlack

  • isEmpty 已经废弃了
  • isBlack方法和isEmpty 前者忽略空格,后者只要有长度就判为非空。通常对于形参校验的情况下,应使用isBlack

<ix:hidden;overflow-y:hidden; border:0xp none #fff; min-height:240px; width:100%;” frameborder=”0” scrolling=”no” allowtransparency=”true”>