1 项目配置
1.1 版本:Version
01.项目结构
a.父pom
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
<relativePath/>
</parent>
<groupId>cn.myslayers</groupId>
<artifactId>libra-product-onboard</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>libra-cloud-common-core</module>
<module>libra-product-onboard-api</module>
<module>libra-product-onboard-entity</module>
<module>libra-product-onboard-service</module>
</modules>
<properties>
<!-- 公共版本配置 -->
<pinyin4j.version>2.5.1</pinyin4j.version>
<swagger2.version>3.0.0</swagger2.version>
<commons-io.version>2.11.0</commons-io.version>
<httpclient.version>4.5.13</httpclient.version>
</properties>
<profiles>
<!-- 方案一:Spring Boot 2.5.x + MyBatis-Plus 3.4.x(最保守稳定) -->
<profile>
<id>stable</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.5.14</spring-boot.version>
<spring-cloud.version>2020.0.4</spring-cloud.version>
</properties>
</profile>
<profiles>
<dependencyManagement>
<dependencies>
<!--======================================= 内部模块版本管理 =======================================-->
<!--======================================= Nacos 系列 ===============================================-->
<!--======================================= Spring 系列 ===============================================-->
<!--======================================= Starter 系列 ==============================================-->
<!--======================================= MyBatis 系列 ==============================================-->
<!--======================================= JSON 序列化系列 ============================================-->
<!--======================================= MapStruct & Lombok =======================================-->
<!--======================================= 工具库系列 ================================================-->
</dependencies>
</dependencyManagement>
<pluginRepositories></pluginRepositories>
<distributionManagement></distributionManagement>
<repositories></repositories>
<build></build>
</project>
b.子pom
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.myslayers</groupId>
<artifactId>libra-product-onboard</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>libra-product-onboard-entity</artifactId>
<dependencies>
<!--======================================= 内部模块版本管理 =======================================-->
<!--======================================= Nacos 系列 ===============================================-->
<!--======================================= Spring 系列 ===============================================-->
<!--======================================= Starter 系列 ==============================================-->
<!--======================================= MyBatis 系列 ==============================================-->
<!--======================================= JSON 序列化系列 ============================================-->
<!--======================================= MapStruct & Lombok =======================================-->
<!--======================================= 工具库系列 ================================================-->
</dependencies>
<build></build>
</project>
02.项目版本
a.方案一:Spring Boot 2.5.x + MyBatis-Plus 3.4.x(最保守稳定)
<java.version>1.8</java.version>
<spring-boot.version>2.5.14</spring-boot.version>
<spring-cloud.version>2020.0.4</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
<spring-cloud-bootstrap.version>3.0.3</spring-cloud-bootstrap.version>
<jackson.version>2.12.7</jackson.version>
<fastjson.version>1.2.83</fastjson.version>
<mybatis.version>3.5.6</mybatis.version>
<mybatis-plus.version>3.4.3.4</mybatis-plus.version>
<mybatis-plus.p6spy>1.8.1</mybatis-plus.p6spy>
<dynamic-datasource.version>3.4.1</dynamic-datasource.version>
<druid.version>1.2.15</druid.version>
<mapstruct.version>1.5.2.Final</mapstruct.version>
<lombok.version>1.18.24</lombok.version>
<knife4j.version>3.0.3</knife4j.version>
<easyexcel.version>3.0.5</easyexcel.version>
<joda-time.version>2.10.14</joda-time.version>
<hutool.version>5.7.22</hutool.version>
<guava.version>31.1-jre</guava.version>
<feign.version>11.10</feign.version>
<oshi.version>6.2.2</oshi.version>
<cxf.version>3.5.5</cxf.version>
b.方案二:Spring Boot 2.7.x + MyBatis-Plus 3.5.x(推荐平衡方案)
<java.version>17</java.version>
<spring-boot.version>2.7.18</spring-boot.version>
<spring-cloud.version>2021.0.9</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
<spring-cloud-bootstrap.version>3.0.3</spring-cloud-bootstrap.version>
<jackson.version>2.13.5</jackson.version>
<fastjson.version>1.2.83</fastjson.version>
<mybatis.version>3.5.10</mybatis.version>
<mybatis-plus.version>3.5.2</mybatis-plus.version>
<mybatis-plus.p6spy>1.8.1</mybatis-plus.p6spy>
<dynamic-datasource.version>3.5.2</dynamic-datasource.version>
<druid.version>1.2.16</druid.version>
<mapstruct.version>1.5.3.Final</mapstruct.version>
<lombok.version>1.18.26</lombok.version>
<knife4j.version>4.1.0</knife4j.version>
<easyexcel.version>3.2.1</easyexcel.version>
<joda-time.version>2.12.2</joda-time.version>
<hutool.version>5.8.15</hutool.version>
<guava.version>31.1-jre</guava.version>
<feign.version>12.1</feign.version>
<oshi.version>6.4.0</oshi.version>
<cxf.version>3.5.5</cxf.version>
<httpclient.version>4.5.14</httpclient.version>
c.方案三:Spring Boot 3.0.x + MyBatis-Plus 3.5.x(现代化方案)
<java.version>17</java.version>
<spring-boot.version>3.0.13</spring-boot.version>
<spring-cloud.version>2022.0.0</spring-cloud.version>
<spring-cloud-alibaba.version>2022.0.0.0</spring-cloud-alibaba.version>
<spring-cloud-bootstrap.version>3.1.0</spring-cloud-bootstrap.version>
<jackson.version>2.14.2</jackson.version>
<fastjson.version>2.0.25</fastjson.version>
<mybatis.version>3.5.13</mybatis.version>
<mybatis-plus.version>3.5.4</mybatis-plus.version>
<mybatis-plus.p6spy>1.9.1</mybatis-plus.p6spy>
<dynamic-datasource.version>4.1.3</dynamic-datasource.version>
<druid.version>1.2.20</druid.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
<lombok.version>1.18.26</lombok.version>
<knife4j.version>4.3.0</knife4j.version>
<easyexcel.version>3.3.2</easyexcel.version>
<joda-time.version>2.12.5</joda-time.version>
<hutool.version>5.8.22</hutool.version>
<guava.version>32.1.2-jre</guava.version>
<feign.version>12.4</feign.version>
<oshi.version>6.4.5</oshi.version>
<cxf.version>4.0.3</cxf.version>
<httpclient.version>4.5.14</httpclient.version>
d.方案四:Spring Boot 3.2.x + MyBatis-Plus 3.5.x(最新稳定)
<java.version>17</java.version>
<spring-boot.version>3.2.5</spring-boot.version>
<spring-cloud.version>2023.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version>
<spring-cloud-bootstrap.version>3.2.0</spring-cloud-bootstrap.version>
<jackson.version>2.15.2</jackson.version>
<fastjson.version>2.0.33</fastjson.version>
<mybatis.version>3.5.16</mybatis.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<mybatis-plus.p6spy>1.11.0</mybatis-plus.p6spy>
<dynamic-datasource.version>4.2.0</dynamic-datasource.version>
<druid.version>1.2.23</druid.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
<lombok.version>1.18.30</lombok.version>
<knife4j.version>4.4.0</knife4j.version>
<easyexcel.version>3.3.4</easyexcel.version>
<joda-time.version>2.12.7</joda-time.version>
<hutool.version>5.8.25</hutool.version>
<guava.version>33.0.0-jre</guava.version>
<feign.version>13.1</feign.version>
<oshi.version>6.4.10</oshi.version>
<cxf.version>4.0.4</cxf.version>
<httpclient.version>4.5.14</httpclient.version>
1.2 依赖:Knife4j
01.依赖
a.坐标
<dependencies>
<!-- 引入Knife4j的官方starter -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
</dependencies>
b.版本
Spring Boot 2.6.6 您指定的项目基础框架版本
Knife4j 3.0.3 这是与 springfox-boot-starter 兼容的稳定版本,广泛应用于Spring Boot 2.x项目中。它在UI和功能上都非常成熟
02.yml配置
server:
port: 8080 # 自定义端口
# 关键配置!解决Spring Boot 2.6.x以上版本与Swagger的兼容性问题
spring:
# Spring MVC 相关配置
mvc:
pathmatch:
# 设置URL路径匹配策略。ant_path_matcher是Spring的传统策略,支持 `*`, `**`, `?` 等通配符。
matching-strategy: ant_path_matcher
# ===============================================================
# Swagger/Knife4j 配置
# ===============================================================
knife4j:
enable: true # 是否启用 Knife4j
setting:
language: zh-CN # 文档语言
enable-swagger-models: true # 显示模型列表
swagger-model-name: 实体类列表 # 模型分组名称
03.配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2 // 开启Swagger2
// @EnableKnife4j // Knife4j 3.x 版本后,此注解可以省略,但加上也无妨
public class Knife4jConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
// 1. API信息配置
.apiInfo(apiInfo())
// 2. 函数选择器,用于配置哪些接口会生成到文档中
.select()
// 指定扫描的Controller包路径
.apis(RequestHandlerSelectors.basePackage("cn.myslayers.product.onboard.controller"))
// 指定路径处理,any()表示处理所有路径
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("XX项目在线API文档")
.description("本文档详细描述了XX项目的后端API接口。")
.contact(new Contact("你的名字", "http://your.website.com", "[email protected]"))
.version("1.0.0")
.build();
}
}
1.3 依赖:Jackson
01.依赖
a.坐标
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
b.说明
在标准的 Spring Boot Web项目中,我们通常会引入 spring-boot-starter-web。
这个 starter 已经包含了 spring-boot-starter-json,
而后者已经自动引入了 Jackson 所需的核心依赖库 (jackson-databind, jackson-datatype-jsr310 等
02.yml配置
# ===============================================================
# Spring Boot 核心配置 (Spring Core Configuration)
# ===============================================================
spring:
# ===============================================================
# Jackson JSON 序列化/反序列化配置 (Jackson JSON Serialization/Deserialization Configuration)
# ===============================================================
jackson:
# 全局设置日期类型(如 Date, LocalDateTime)序列化为JSON字符串时的格式
date-format: yyyy-MM-dd HH:mm:ss
# 设置时区,保证日期时间处理的一致性,GMT+8是中国标准时间
time-zone: GMT+8
serialization:
# 设置为false,表示将日期序列化为字符串格式(如 "2025-08-20 20:20:20"),而不是时间戳(如 1755778820000)
write-dates-as-timestamps: false
# 设置为false,当遇到空的Java对象时不抛出异常,允许序列化空对象(如只有getter没有属性的对象)
fail-on-empty-beans: false
deserialization:
# 设置为false,表示当JSON字符串中包含Java对象没有的属性时,不抛出异常,而是直接忽略
fail-on-unknown-properties: false
# 支持BigDecimal(已有,正确)- 设置为true时,JSON中的浮点数将被反序列化为BigDecimal而不是Double,提供更高的精度
use-big-decimal-for-floats: true
# 设置为true,允许将单个值作为数组处理。例如:JSON中 "items": "single" 可以反序列化到 List<String> items
accept-single-value-as-array: true
# 设置为true,将空字符串("")视为null对象,防止空字符串导致的反序列化异常
accept-empty-string-as-null-object: true
# 解析器配置,提高容错性
parser:
# 设置为true,允许JSON字符串中包含注释(// 或 /* */),提高配置文件的可读性
allow-comments: true
# 设置为true,允许JSON字符串使用单引号包围字段名和字符串值,而不仅仅是双引号
allow-single-quotes: true
# 设置为true,允许JSON字段名不使用引号包围(如 {name: "value"} 而不是 {"name": "value"})
allow-unquoted-field-names: true
# 全局属性包含策略 - NON_NULL表示序列化时忽略值为null的字段,减少JSON体积
default-property-inclusion: NON_NULL
# 映射器级别配置,用于控制Jackson ObjectMapper的行为
mapper:
# 设置为true,启用大小写不敏感的属性映射。JSON中的 "UserName" 可以映射到 Java 的 "username" 字段
accept-case-insensitive-properties: true
03.配置类
a.总结
依赖:通常无需额外添加,spring-boot-starter-web 已足够。
配置:优先使用 application.yml 进行配置,因为它简洁明了,能满足90%以上的需求。
配置类:仅在需要进行 yml 无法完成的复杂定制(如添加自定义序列化器/反序列化器、注册特定模块等)时,才创建 JacksonConfig 类,并使用 Jackson2ObjectMapperBuilderCustomizer 以追加的方式进行配置。
b.核心思想
Spring Boot 的自动配置机制会读取您在 application.yml 中的所有 spring.jackson.* 配置,并自动应用到一个全局的 ObjectMapper Bean 中。
因此,对于您在 yml 中已经声明的配置,通常不再需要一个Java配置类来重复设置。
c.高级配置
只有当您需要进行一些 yml 文件无法支持的高级定制时,才需要提供一个配置类。
以下是一个推荐的、非侵入式的配置方式,它会在 Spring Boot 自动配置的基础上进行追加配置,而不是完全覆盖它。
d.高级配置-示例:如何为 Java 8 的 LocalDateTime, LocalDate, LocalTime 提供更精细的格式化
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
/**
* Jackson 全局配置
* <p>
* 推荐使用 Jackson2ObjectMapperBuilderCustomizer 来定制由 Spring Boot 自动配置的 ObjectMapper。
* 这样做的好处是不会完全覆盖 Spring Boot 的默认配置,而是在其基础上进行补充和修改。
* 你的 yml 配置依然会生效。
*/
@Configuration
public class JacksonConfig {
// 定义各种日期时间格式
private static final String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
private static final String DATE_PATTERN = "yyyy-MM-dd";
private static final String TIME_PATTERN = "HH:mm:ss";
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
// 注意:yml中的 spring.jackson.date-format 优先级更高,这里可以作为备用或更精细化的控制。
// 当 yml 中未配置 date-format 时,这里的配置会生效。
// 如果希望代码配置覆盖yml配置,可以采用其他方式(如直接创建ObjectMapper Bean),但通常不推荐。
// 1. 创建 JavaTimeModule 用于处理 Java 8 的日期时间类型
JavaTimeModule javaTimeModule = new JavaTimeModule();
// 2. 添加针对 LocalDateTime 的序列化和反序列化器
javaTimeModule.addSerializer(LocalDateTime.class,
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATETIME_PATTERN)));
javaTimeModule.addDeserializer(LocalDateTime.class,
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATETIME_PATTERN)));
// 3. 添加针对 LocalDate 的序列化和反序列化器
javaTimeModule.addSerializer(LocalDate.class,
new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_PATTERN)));
javaTimeModule.addDeserializer(LocalDate.class,
new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_PATTERN)));
// 4. 添加针对 LocalTime 的序列化和反序列化器
javaTimeModule.addSerializer(LocalTime.class,
new LocalTimeSerializer(DateTimeFormatter.ofPattern(TIME_PATTERN)));
javaTimeModule.addDeserializer(LocalTime.class,
new LocalTimeDeserializer(DateTimeFormatter.ofPattern(TIME_PATTERN)));
// 5. 将模块注册到 builder 中
builder.modules(javaTimeModule);
// 示例:如果你想在代码中设置 yml 中的某些属性,可以这样做:
// builder.failOnUnknownProperties(false); // 对应 spring.jackson.deserialization.fail-on-unknown-properties
// builder.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); // 设置命名策略为下划线
};
}
}
1.4 配置:MapStruct
01.错误原因分析
a.根本原因
a.注解处理器执行顺序问题
问题:MapStruct 处理器在 Lombok 处理器之前执行
结果:MapStruct 无法识别 Lombok 生成的 getter/setter 方法
b.依赖缺失
问题:缺少 lombok-mapstruct-binding 依赖
结果:两个框架无法协同工作
c.属性访问权限问题
问题:属性为 private,且 Lombok 未生成访问方法
结果:MapStruct 无法访问属性
b.具体错误分析
a.错误1:Unknown property "id"
问题代码:
@Mapping(target = "id", expression = "java(UUIDUtil.getUUID())")
OnbInterviewEvaluation createEvaluation(String applicantId, String positionId);
-------------------------------------------------------------------------------------------------
错误原因:MapStruct 无法识别 OnbInterviewEvaluation 的 id 属性
b.错误2:No property named "applicantId"
问题代码:
@Mapping(source = "applicantId", target = "applicantId")
PageAllocateDTOs.AuditStatusUpdateDTO createAuditUpdateParams(String applicantId, OnbInterviewAuditStatusEnum auditStatus);
-------------------------------------------------------------------------------------------------
错误原因:MapStruct 无法识别源参数中的 applicantId 属性
02.解决方案
a.解决方案A:修复注解处理器顺序(推荐)
a.步骤1:添加 lombok-mapstruct-binding 依赖
<!-- 在父 POM 的 dependencyManagement 中添加 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</dependency>
b.步骤2:配置 Maven 编译器插件
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<annotationProcessorPaths>
<!-- 1. 先执行 Lombok -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<!-- 2. 再执行 MapStruct -->
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<!-- 3. 最后执行绑定器 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
b.解决方案B:修复 MapStruct 表达式引用
a.问题:表达式中的类引用不完整
// 错误:使用简单类名
@Mapping(target = "auditStatus", expression = "java(OnbInterviewAuditStatusEnum.getByKey(1))")
// 正确:使用全限定类名
@Mapping(target = "auditStatus", expression = "java(cn.myslayers.product.onboard.api.enums.OnbInterviewAuditStatusEnum.getByKey(1))")
b.修复后的完整映射器
@Mapper
public interface AllocateUpdateMapper {
AllocateUpdateMapper INSTANCE = Mappers.getMapper(AllocateUpdateMapper.class);
@Mapping(target = "id", expression = "java(cn.myslayers.common.core.util.UUIDUtil.getUUID())")
@Mapping(source = "applicantId", target = "applicantId")
@Mapping(source = "applyPosition", target = "applyPosition")
@Mapping(target = "auditStatus", expression = "java(cn.myslayers.product.onboard.api.enums.OnbInterviewAuditStatusEnum.getByKey(1))")
@Mapping(target = "createTime", expression = "java(new java.util.Date())")
@Mapping(target = "createUser", constant = "currentUser")
OnbInterviewPosition createPositionFromDTO(PageAllocateDTOs.AllocateUpdateDTO dto);
}
c.解决方案C:验证实体类属性定义
a.检查 Lombok 注解
// 正确:确保 Lombok 注解正确
@Data
@Accessors(chain = true)
@TableName("onb_interview_evaluation")
public class OnbInterviewEvaluation implements Serializable {
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
@TableField("applicant_id")
private String applicantId;
// ... 其他属性
}
b.检查 getter/setter 方法生成
# 编译后检查生成的类
find target/generated-sources -name "*.java" -exec grep -l "getApplicantId\|setApplicantId" {} \;
1.5 配置:DataSource
01.baomidou
a.依赖
<!-- MyBatis-Plus 动态数据源:支持多数据源自动切换 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${dynamic-datasource.version}</version>
</dependency>
b.说明
它的职责是整合和管理多个数据源。它本身并不提供数据库连接池,而是作为一个“管理器”,去集成像 Druid, HikariCP 这样的具体连接池
c.yml配置
spring:
datasource:
# druid 的通用配置需要保留,dynamic-datasource 会读取它
druid:
keepAlive: true
initial-size: 5
# ... 其他 druid 配置 ...
# dynamic 的配置是核心,必须保留
dynamic:
primary: master
strict: true
datasource:
master:
# ... master 配置 ...
onboard:
# ... onboard 配置 ...
02.alibaba
a.依赖
<!-- Druid 数据源:阿里巴巴数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
b.说明
整合单个 Druid 数据源**到 Spring Boot 中。它会自动配置一个 `DataSource` Bean,并让你可以在 `spring.datasource.*` 下进行配置
c.yml配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://...
username: root
password: 123456
1.6 配置:MybatisConfig
01.依赖
a.MyBatis 系列
<!-- MySQL JDBC:MySQL 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis-Plus Starter:Spring Boot 集成 MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- Dynamic Datasource:多数据源自动切换(降级到稳定版本) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
</dependency>
<!-- MyBatis-Plus p6spy打印:记录所有的SQL语句及其执行时间 -->
<dependency>
<groupId>com.github.gavlyukovskiy</groupId>
<artifactId>p6spy-spring-boot-starter</artifactId>
</dependency>
<!-- Druid:数据库连接池,用于 DatabaseUtil.java -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
b.JSON 序列化系列
<!-- Jackson Databind:核心 JSON 读写功能 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Jackson Core:底层 JSON 处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<!-- Jackson Annotations:JSON 注解支持 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<!-- FastJSON:阿里巴巴 JSON 序列化工具 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
c.MapStruct & Lombok
<!-- MapStruct:编译期 Java Bean 转换 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<!-- MapStruct Processor:MapStruct 注解处理器 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
</dependency>
<!-- Lombok:简化 Java Bean 编写 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
d.Maven Compiler:指定 Java 版本与注解处理器
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<annotationProcessorPaths>
<!-- 1. 先执行 Lombok -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<!-- 2. 再执行 MapStruct -->
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<!-- 3. 最后执行绑定器 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
02.yml配置
a.Dynamic Datasource:多数据源自动切换
# ===============================================================
# Spring Boot 核心配置 (Spring Core Configuration)
# ===============================================================
spring:
# ===============================================================
# p6spy配置 (p6spy Configuration)
# ===============================================================
p6spy:
# 启用日志记录
enable-logging: true
# 多行模式
multiline: true
# 使用SLF4J日志记录器
logging: slf4j
# 日志格式
log-format: |-
time:%(executionTime)ms || sql:%(sql)
# ===============================================================
# 数据源配置 (Datasource Configuration)
# ===============================================================
datasource:
# ---------------------------------------------------------------
# Druid 连接池通用配置 (Applies to all datasources below)
# ---------------------------------------------------------------
druid:
# 建议配置为true,防止MySQL等数据库8小时无交互自动断开连接的问题
keepAlive: true
# 初始化时建立物理连接的个数
initial-size: 5
# 连接池中保持的最小空闲连接数
min-idle: 5
# 连接池中允许的最大活动连接数
maxActive: 20
# 获取连接时最大等待时间,单位毫秒。如果超过该时间仍未获取到连接,则抛出异常
maxWait: 60000
# 检测并关闭空闲连接的时间间隔,单位毫秒
timeBetweenEvictionRunsMillis: 60000
# 每隔多久将监控数据记录到日志中,单位毫秒。设置为0表示关闭
timeBetweenLogStatsMillis: 60000
# 连接在池中保持空闲而不被驱逐的最长时间,单位毫秒
maxEvictableIdleTimeMillis: 360000
# 连接在池中最小生存的时间,单位毫秒。只有空闲时间超过该值的连接才可能被驱逐
minEvictableIdleTimeMillis: 300000
# 用来检测连接是否有效的SQL查询。DUAL是Oracle和MySQL中的虚拟表
validationQuery: SELECT 'x' FROM DUAL
# 建议配置为true,表示在连接空闲时检测其有效性,不影响性能
testWhileIdle: true
# 获取连接时是否检测其有效性。设置为false可提高性能
testOnBorrow: false
# 归还连接时是否检测其有效性。设置为false可提高性能
testOnReturn: false
# 是否打印被丢弃的连接的日志
logAbandoned: true
# 是否移除长时间未关闭的连接(超时连接)
removeAbandoned: true
# 超时时间,单位秒。当一个连接超过这个时间未使用,则被认为是超时连接并被回收
removeAbandonedTimeout: 1800
# 是否开启 PreparedStatement 缓存(PSCache)
# Oracle下建议开启,MySQL下由于驱动自身有优化,建议关闭
poolPreparedStatements: false
# PreparedStatement 缓存的大小
maxPoolPreparedStatementPerConnectionSize: -1
# 配置Druid的过滤器,用于监控、日志、防火墙等功能。
# stat: 性能统计, log4j2: 日志记录, wall: SQL防火墙
filters: stat,log4j2
# 连接属性配置
# druid.stat.mergeSql=true: 合并相似的SQL,方便统计
# druid.stat.slowSqlMillis=5000: 记录执行时间超过5秒的SQL为慢SQL
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 配置Druid的Web监控过滤器(DruidStatFilter)
web-stat-filter:
# 启用Web监控
enabled: true
# 拦截所有URL请求
url-pattern: "/*"
# 指定不进行统计的静态资源URL
exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
# ---------------------------------------------------------------
# 动态数据源配置 (Dynamic Datasource - The REAL configuration)
# ---------------------------------------------------------------
dynamic:
# 设置主数据源(默认数据源)。当代码中未指定使用哪个数据源时,会自动使用名为 'master' 的数据源
primary: master
# 严格模式。设置为true时,如果程序中指定了一个不存在的数据源,会立即报错
strict: true
# 是否启用 p6spy 来监控和分析SQL。p6spy是一个SQL日志工具,可以详细记录SQL执行情况
p6spy: true
# 定义所有的数据源
datasource:
# --- 数据源1 ---
master: # 数据源的逻辑名称
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3307/libra-product-onboard?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
# --- 数据源2 ---
onboard: # 数据源的逻辑名称
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3307/libra-product-onboard?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
b.MyBatis-Plus Starter:Spring Boot 集成 MyBatis-Plus
# ===============================================================
# MyBatis-Plus ORM 框架配置
# ===============================================================
mybatis-plus:
# 指定 Mapper XML 文件的位置。classpath: 表示从类路径下查找
mapper-locations: classpath:/mybatis/*Mapper.xml
# 实体类(Entity)所在的包路径。配置后可在 Mapper XML 中直接使用类名作为别名
type-aliases-package: cn.myslayers.product.onboard.api.entity
# 枚举类所在的包路径。MyBatis-Plus 会自动扫描并处理这些枚举类型与数据库字段的映射
type-enums-package: cn.myslayers.product.onboard.api.enums
# 自定义 TypeHandler 所在包。MyBatis-Plus 启动时会扫描此包下所有继承 BaseTypeHandler,并标注 @MappedJdbcTypes 的类,自动注册到全局配置,用于复杂类型(如 JSON 与对象)映射
type-handlers-package: cn.myslayers.product.onboard.api.hander
# 全局策略配置
global-config:
# ---------- 数据库相关配置 ----------
db-config:
# 主键生成策略:
# AUTO(0): 数据库自增
# INPUT(1): 用户输入
# ASSIGN_ID(2): 雪花算法
# ASSIGN_UUID(3): UUID 字符串
id-type: ASSIGN_UUID
# 字段插入和更新策略:
# IGNORED(0): 忽略判断
# NOT_NULL(1): 只对非 NULL 字段操作
# NOT_EMPTY(2): 只对非空(非 NULL 且非空串/空集合)字段操作
field-strategy: 2
# ❌ 已废弃:3.4.x版本后不再生效,功能已迁移至 configuration.map-underscore-to-camel-case
# 原因:为了与 MyBatis 原生配置保持一致,避免配置重复和混乱
# db-column-underline: true
# ❌ 已废弃:3.4.x版本后功能已内置,无需手动配置
# 原因:MyBatis-Plus 会自动监听 XML 文件变化,该配置项已无实际作用
# refresh-mapper: true
# ❌ 已废弃:3.4.x版本后配置方式发生变化
# 原因:该配置项在新版本中被重新设计,改为更细粒度的控制
# capital-mode: true
# 逻辑删除功能配置:
# 实体类属性名,对应数据库逻辑删除字段
logic-delete-field: deletedFlag
# 逻辑删除时字段值
logic-delete-value: 1
# 未删除时字段值
logic-not-delete-value: 0
# ---------- 自动填充策略(MetaObjectHandler) ----------
# 注册实现了 MetaObjectHandler 接口的 Bean 名称或类,可在插入/更新时自动填充字段
# meta-object-handler: cn.myslayers.product.onboard.config.MyMetaObjectHandler
# MyBatis 原生配置
configuration:
# 开启驼峰命名自动映射(替代已废弃的 db-column-underline)
map-underscore-to-camel-case: true
# 是否开启 MyBatis 二级缓存(默认关闭)
cache-enabled: false
# 查询结果为 NULL 时,是否调用实体 setter(可触发默认值)
call-setters-on-nulls: true
# 指定 MyBatis 使用的日志实现,将 SQL 输出到控制台
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 一级缓存作用域(SESSION 或 STATEMENT)
# local-cache-scope: STATEMENT
# 默认枚举类型处理器(修正为3.4.3.4版本正确路径)
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
03.配置类
a.MybatisPlusConfig
/**
* MyBatis-Plus 统一增强插件配置(3.4.3.4/2.5.14专用版)
* 功能涵盖:分页、乐观锁、多租户、数据安全、自动填充等
*/
@Configuration
public class MybatisPlusConfig {
/**
* MyBatis-Plus 插件主拦截器
* - 分页
* - 乐观锁
* - 多租户
* - 防全表操作
* - 数据变动记录
* - 非法SQL拦截等(如有需要可逐步添加)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(/*@Autowired(required = false) TenantLineHandler tenantLineHandler*/) {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// ========== 1. 多租户插件 ==========(如有多租户场景,取消下面注释并自定义TenantLineHandler实现)
/*TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor();
tenantInterceptor.setTenantLineHandler(tenantLineHandler);
interceptor.addInnerInterceptor(tenantInterceptor);*/
// ========== 2. 动态表名(如有需求) ==========
// interceptor.addInnerInterceptor(new DynamicTableNameInnerInterceptor());
// ========== 3. 分页插件 ==========
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInnerInterceptor.setMaxLimit(1000L); // 单页最大1000,可自定义
interceptor.addInnerInterceptor(paginationInnerInterceptor);
// ========== 4. 乐观锁插件 ==========
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// ========== 5. 阻断全表更新与删除插件 ==========
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
// ========== 6. 数据变动记录插件(如需数据审计等) ==========
// DataChangeRecorderInnerInterceptor dataChangeRecorder = new DataChangeRecorderInnerInterceptor();
// dataChangeRecorder.setBatchUpdateLimit(1000).openBatchUpdateLimitation();
// interceptor.addInnerInterceptor(dataChangeRecorder);
// ========== 7. 非法SQL拦截插件等(如有需求) ==========
// interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());
return interceptor;
}
}
b.MyMetaObjectHandler
/**
* MyBatis-Plus 通用字段自动填充处理器【3.4.x标准范式】
*/
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
Date now = new Date();
this.strictInsertFill(metaObject, "createTime", Date.class, now);
this.strictInsertFill(metaObject, "updateTime", Date.class, now);
this.strictInsertFill(metaObject, "createUser", String.class, getCurrentUser());
this.strictInsertFill(metaObject, "updateUser", String.class, getCurrentUser());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
this.strictUpdateFill(metaObject, "updateUser", String.class, getCurrentUser());
}
private String getCurrentUser() {
return "system"; // 可集成安全上下文,如 SecurityUtil.getCurrentUserName()
}
}
2 数据管理
2.1 坑点:2类,6种
01.分类1
a.OGNL 表达式相关 Bug
a.字符串比较错误
a.问题描述
在 `<if>` 标签中,OGNL 表达式会将单引号 `'1'` 解析为 `char` 类型,导致与 `String` 类型的变量比较时失败。
b.错误示例
<if test="name != null and name == '1'">
AND id = #{id}
</if>
c.解决方案
a.方案1:使用双引号将字符包装成字符串。
<if test='name != null and name == "1"'>
AND id = #{id}
</if>
b.方案2:调用 `toString()` 方法进行类型转换。
<if test="name != null and name == '1'.toString()">
AND id = #{id}
</if>
b.对于任何非字符串(String)的对象类型,如 Date、Integer、BigDecimal 等,条件判断中应该只使用 != null 来检查,而 != '' 的检查仅对 String 类型有效
a.影响版本
MyBatis 3.3.0
b.问题描述
在此特定版本中,将 `java.util.Date` 类型的参数与空字符串 `''` 进行比较会抛出类型转换异常。
c.错误示例
<if test="createTime != null and createTime != ''">
date(create_time) = date(#{createTime,jdbcType=TIMESTAMP})
</if>
d.解决方案与建议
a.方案1 (推荐): 移除多余的空字符串比较,仅保留 `null` 判断。
<if test="createTime != null">
date(create_time) = date(#{createTime,jdbcType=TIMESTAMP})
</if>
b.方案2 (备选): 避免使用 3.3.0 版本,可降级至 3.2.8 或升级至更高版本。
c.新日期类型(LocalDateTime)映射失效
a.影响版本
MyBatis-Plus 3.1.1+ 配合旧版 Druid
b.问题描述
使用 `LocalDateTime` 等 Java 8 新日期类型时,抛出 `java.sql.SQLFeatureNotSupportedException` 异常。
c.根本原因
MyBatis-Plus 3.1.1 内部依赖的 MyBatis 升级到了 3.5.1,该版本开始要求 JDBC 驱动支持 JDBC 4.2 规范才能正确处理新日期类型。而旧版 Druid (< 1.1.21) 不支持此规范。
d.解决方案
推荐方案:将 Druid 依赖升级至 `1.1.21` 或更高版本。
b.事务管理 Bug
a.saveBatch() 隐式事务问题
a.问题描述
MyBatis-Plus 的 `saveBatch()` 方法内部已经声明了 `@Transactional`。如果外部业务方法也声明了事务,会导致事务嵌套和传播行为异常,可能使外部事务的回滚失效。
b.错误场景示例
@Transactional(rollbackFor = Exception.class)
public Boolean batchInsert(List<Entity> list) {
// 此处 saveBatch 的内部事务可能先于外部事务提交,导致外部异常时无法回滚
entityService.saveBatch(list);
return Boolean.TRUE;
}
c.解决方案
a.方案1:绕过 Service 层,直接调用 Mapper 的批量插入方法,让整个操作处于同一个事务中。
@Transactional(rollbackFor = Exception.class)
public Boolean batchInsert(List<Entity> list) {
return entityMapper.insertBatch(list) > 0;
}
b.方案2:使用手动事务管理,完全控制事务的生命周期。
@Autowired
private MybatisBatch mybatisBatch;
public Boolean batchInsert(List<Entity> list) {
return mybatisBatch.execute(sqlSession -> {
EntityMapper mapper = sqlSession.getMapper(EntityMapper.class);
list.forEach(mapper::insert);
return true;
});
}
b.多数据源下事务管理器冲突
a.问题描述
当应用中配置了多个数据源时,如果存在多个 `PlatformTransactionManager` 类型的 Bean,Spring 将无法确定使用哪一个,导致事务功能失效。
b.解决方案
在使用 `@Transactional` 注解时,通过 `transactionManager` 属性明确指定要使用的事务管理器 Bean 的名称。
@Transactional(transactionManager = "primaryTransactionManager")
public void primaryDbOperation() {
// 主数据库操作
}
@Transactional(transactionManager = "secondaryTransactionManager")
public void secondaryDbOperation() {
// 从数据库操作
}
c.批量更新语法错误
a.错误现象
批量操作提示 `multi-statement not allow`
b.解决方案
连接串添加 `allowMultiQueries=true` 参数
c.参考链接
[MySQL批量操作](https://stackoverflow.com/questions/22829539/mybatis-batch-update-exception)
[连接参数文档](https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-security.html)
c.多租户插件 Bug
a.JSQLParser解析失败
a.影响版本
3.4.x - 3.5.x
b.错误现象
`JSQLParserException: Encountered unexpected token`
c.解决方案
方案1:使用 `@InterceptorIgnore(tenantLine="true")` 跳过解析
方案2:升级JSQLParser版本排除冲突依赖
方案3:简化SQL语法避开不支持函数
d.参考链接
[JSQLParser升级指南](https://blog.csdn.net/qq_24615389/article/details/132046825)
[Gitee Issue](https://gitee.com/baomidou/mybatis-plus/issues/I7MJ5G)
b.多语句执行限制
a.影响版本
全版本
b.错误现象
`multi-statement not allow`
c.解决方案
方案1:数据源URL添加 `allowMultiQueries=true`
方案2:Druid配置 `WallConfig.setMultiStatementAllow(true)`
d.参考链接
[Druid配置](https://blog.csdn.net/weixin_41716049/article/details/130953393)
[多语句配置](https://github.com/alibaba/druid/wiki/FAQ)
02.分类2
a.分页插件 Bug
a.分页total为0
a.问题描述
分页查询能够返回数据列表,但分页结果对象中的 `total` (总记录数) 和 `pages` (总页数) 字段恒为 0。
b.常见原因
未在 Spring 配置中注册分页拦截器 Bean。
注册了分页拦截器,但忘记在其配置方法上添加 `@Bean` 注解。
使用了与当前 MyBatis-Plus 版本不兼容的旧版分页拦截器。
c.正确配置示例 (MyBatis-Plus 3.4.x+)
@Configuration
@MapperScan("com.example.dao")
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
b.分页插件不生效
a.影响版本
全版本
b.错误现象
分页参数传入但SQL未添加LIMIT子句
c.解决方案
MyBatis-Plus < 3.4.0: 使用旧的 `PaginationInterceptor`。
MyBatis-Plus >= 3.4.0: 必须使用新的 `MybatisPlusInterceptor` 并添加 `PaginationInnerInterceptor`。
正确配置分页拦截器 `interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL))`
d.参考链接
[配置示例](https://blog.csdn.net/qq_39084776/article/details/124922593)
[调试指南](https://www.cnblogs.com/lcxz8686TL/p/17288561.html)
b.字段策略 Bug
a.updateById更新失效
a.问题描述
调用 MyBatis-Plus 提供的 `updateById()` 方法后,发现数据库中某些字段并未更新,但方法没有报错。
b.根本原因
MyBatis-Plus 的默认字段更新策略是 `NOT_NULL`,这意味着当实体类中某个字段的值为 `null` 时,该字段将不会被包含在最终生成的 UPDATE SQL 语句中。
c.字段策略说明
`IGNORED`: 忽略所有判断,任何字段都会被更新,即使值为 `null`。
`NOT_NULL`: (默认值) 仅更新值不为 `null` 的字段。
`NOT_EMPTY`: 仅更新值不为 `null` 且不为空字符串的字段。
d.解决方案与代码示例
a.方案1:全局修改更新策略。
mybatis-plus:
global-config:
db-config:
field-strategy: IGNORED
b.方案2:在实体类字段上使用注解,进行局部修改。
public class User {
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String nickName; // 允许将 nickName 更新为 null
}
c.方案3:使用 `UpdateWrapper` 精确控制更新行为,此方法会忽略所有策略。
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.eq("id", userId)
.set("nick_name", null); // 强制将 nick_name 设置为 null
userMapper.update(null, wrapper);
b.@TableField注解失效
a.影响版本
全版本
b.错误现象
手写SQL中@TableField映射不生效
c.解决方案
手写SQL场景使用原生MyBatis的resultMap配置
d.参考链接:
[注解失效说明](https://www.cnblogs.com/kuangdaoyizhimei/p/15965206.html)
[ResultMap配置](https://mybatis.org/mybatis-3/sqlmap-xml.html#Result_Maps)
c.Oracle NULL更新异常
a.问题描述
在 Oracle 数据库环境下,当尝试将字段更新为 `null` 时,如果 XML 映射文件中没有明确指定 `jdbcType`,可能会因无法推断类型而抛出异常。
b.解决方案
在 XML 的 `UPDATE` 语句中,为所有可能为 `null` 的字段显式添加 `jdbcType` 属性。
<update id="updateUser">
UPDATE user SET
name = #{name, jdbcType=VARCHAR},
age = #{age, jdbcType=INTEGER}
WHERE id = #{id}
</update>
c.结果映射与类型处理
a.构造器映射依赖字段顺序
a.问题描述
当实体类仅有全参构造器时,MyBatis 会尝试按查询结果的字段顺序与构造器参数顺序进行匹配。一旦 SQL 查询的字段顺序发生改变,就会导致属性映射错乱。
b.解决方案
a.方案1 (推荐): 为实体类提供一个无参构造器。
@NoArgsConstructor // Lombok 注解
public class User { ... }
b.方案2: 在 `<resultMap>` 中使用 `<constructor>` 标签,明确指定结果集列与构造器参数的映射关系。
<resultMap id="userResultMap" type="User">
<constructor>
<idArg column="id" javaType="Long"/>
<arg column="name" javaType="String"/>
</constructor>
</resultMap>
b.Map 结果集处理问题
a.问题一:下划线无法转驼峰
当 `resultType` 设置为 `java.util.Map` 时,MyBatis 默认不会将数据库列的下划线命名(e.g., `user_name`)自动转换为 Map 中 key 的驼峰命名(`userName`)。
b.问题一解决方案
开启全局的 `mapUnderscoreToCamelCase` 配置。
@Configuration
public class MyBatisConfig {
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> {
configuration.setMapUnderscoreToCamelCase(true);
};
}
}
c.问题二:null 值的 key 丢失
默认情况下,如果查询结果的某个字段值为 `null`,那么在最终返回的 Map 中,将不会包含这个字段对应的 key。
d.问题二解决方案
开启 `call-setters-on-nulls` 配置,强制为 `null` 值的字段也在 Map 中创建 key。
mybatis:
configuration:
call-setters-on-nulls: true
2.2 注解:Annotation
01.MyBaits
a.映射
@Arg: 用于指定构造函数参数的映射
@AutomapConstructor: 自动映射构造函数参数
@ConstructorArgs: 指定构造函数参数的映射
@MapKey: 指定返回的 Map 集合中使用的键
@Result: 指定查询结果的映射
@ResultMap: 引用预定义的结果映射
@Results: 定义一组结果映射
@ResultType: 指定结果类型
@TypeDiscriminator: 用于多态结果映射
b.缓存
@CacheNamespace: 配置命名空间的缓存
@CacheNamespaceRef: 引用另一个命名空间的缓存配置
c.SQL
@Delete: 用于执行删除操作的 SQL 语句
@DeleteProvider: 提供动态删除 SQL 语句
@Insert: 用于执行插入操作的 SQL 语句
@InsertProvider: 提供动态插入 SQL 语句
@Select: 用于执行查询操作的 SQL 语句
@SelectKey: 在插入操作前或后执行的 SQL 语句
@SelectProvider: 提供动态查询 SQL 语句
@Update: 用于执行更新操作的 SQL 语句
@UpdateProvider: 提供动态更新 SQL 语句
d.其他
@Flush: 用于刷新缓存
@Lang: 指定使用的语言驱动
@Many: 指定一对多关系的映射
@Mapper: 标记接口为 MyBatis 映射器
@One: 指定一对一关系的映射
@Options: 配置 SQL 语句的执行选项
@Param: 指定参数名称
@Property: 配置属性
02.MyBatisPlus
a.汇总
@TableName
@Tableld
@TableField
-----------------------------------------------------------------------------------------------------
@Version
@EnumValue
@TableLogic
@SqlParser
@KeySequence
@Interceptorlgnore
@OrderBy
b.@TableName
使用:用于指定实体类对应的数据库表名
示例:
@TableName("student")
public class Student {
// 类属性
}
说明:@TableName 注解用于将实体类与数据库表进行映射,指定表名为 student
c.@TableId
使用:用于指定实体类中的主键字段
示例:
@TableId(value = "id", type = IdType.AUTO)
private Long id;
说明:@TableId 注解指定 id 为主键,并使用自动增长策略
d.@TableField
使用:用于指定实体类字段与数据库表字段的映射关系
示例:
@TableField("name")
private String name;
说明:@TableField 注解将实体类的 name 属性映射到数据库表的 name 字段
e.@Version
使用:用于实现乐观锁
示例:
@Version
private Integer version;
说明:@Version 注解用于标识乐观锁版本字段,更新时自动增加版本号
f.@EnumValue
使用:用于枚举类型字段的映射
示例:
public enum Status {
@EnumValue
ACTIVE(1),
INACTIVE(0);
private final int value;
Status(int value) {
this.value = value;
}
}
说明:@EnumValue 注解用于指定枚举值在数据库中的存储值
g.@TableLogic
使用:用于实现逻辑删除
示例:
@TableLogic
private Integer deleted;
说明:@TableLogic 注解用于标识逻辑删除字段,删除操作时不会物理删除数据,而是更新该字段
h.@SqlParser
使用:用于控制 SQL 解析行为
示例:
@SqlParser(filter = true)
public void customMethod() {
// 自定义方法
}
说明:@SqlParser 注解用于指定方法是否应用 SQL 解析器
i.@KeySequence
使用:用于指定序列主键生成策略
示例:
@KeySequence("seq_user")
public class User {
// 类属性
}
说明:@KeySequence 注解用于指定使用数据库序列生成主键
j.@InterceptorIgnore
使用:用于忽略拦截器
示例:
@InterceptorIgnore(tenantLine = "true")
public void someMethod() {
// 方法体
}
说明:@InterceptorIgnore 注解用于指定方法忽略某些拦截器,如租户拦截器
k.@OrderBy
使用:用于指定排序字段
示例:
@OrderBy("age DESC")
private Integer age;
说明:@OrderBy 注解用于指定查询结果的排序规则
2.3 分页:PageHelper
01.手动分页
a.风格
很清晰地分三步:查数据列表 → 查总数 → 封装 Page;
总是返回 IPage,而不是裸露 List;
用 `Page<>` 来手动 set current、size、total、pages、records;
没有借助 MyBatis-Plus 的自动分页插件,而是手动 SQL + 计算 offset + count。
b.具体操作
controller 层负责接收请求,调用 service 并返回 ApiResult<IPage>。
service 层实现具体分页逻辑:手动查列表和 count,并组装 Page<IPage>。
mapper 层接口包含两个方法:列表查询和总数统计(带参数)。
mapper.xml 件分别写 select 和 count SQL(用 limit/offset 或只查 count)。
02.手动分页-代码
a.controller
@GetMapping("/assigned-to/{assignee}")
@ApiOperation("查询分配给指定人员的应聘者")
public ApiResult<IPage<ApplicantListVO>> getApplicantsByAssignee(
@PathVariable String assignee,
@ApiParam("当前页") @RequestParam(defaultValue = "1") Integer current,
@ApiParam("每页大小") @RequestParam(defaultValue = "10") Integer size) {
try {
IPage<ApplicantListVO> result = applicantService.getApplicantsByAssignee(current, size, assignee);
return ApiResult.success(result);
} catch (Exception e) {
log.error("查询分配给指定人员的应聘者失败", e);
return ApiResult.failure("查询分配给指定人员的应聘者失败: " + e.getMessage());
}
}
b.service
@Transactional(readOnly = true)
public IPage<SomeVO> findSomethingPage(Integer current, Integer size, String condition) {
try {
long offset = (long) (current - 1) * size;
// 1. 查询分页数据
List<SomeEntity> list = someMapper.selectPageByCondition(offset, size, condition);
// 2. 查询总数
Long total = someMapper.countByCondition(condition);
// 3. 转换为 VO
List<SomeVO> voList = new ArrayList<>();
for (SomeEntity entity : list) {
SomeVO vo = new SomeVO();
BeanUtils.copyProperties(entity, vo);
voList.add(vo);
}
// 4. 组装 Page
IPage<SomeVO> result = new Page<>();
result.setCurrent(current);
result.setSize(size);
result.setTotal(total);
result.setPages((long) Math.ceil((double) total / size));
result.setRecords(voList);
return result;
} catch (Exception e) {
log.error("分页查询失败", e);
throw e;
}
}
c.mapper
public interface ApprovalLogMapper {
List<SomeEntity> selectPageByCondition(@Param("offset") Long offset,
@Param("limit") Long limit,
@Param("condition") String condition);
Long countByCondition(@Param("condition") String condition);
}
d.mapper.xml
<select id="selectPageByCondition" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM some_table
WHERE 1=1
<if test="condition != null and condition != ''">
AND some_column = #{condition}
</if>
ORDER BY create_time DESC
LIMIT #{offset}, #{limit}
</select>
<select id="countByCondition" resultType="java.lang.Long">
SELECT COUNT(*)
FROM some_table
WHERE 1=1
<if test="condition != null and condition != ''">
AND some_column = #{condition}
</if>
</select>
2.4 主键:ASSIGN_UUID
01.优先级机制
a.层次结构
1.@TableId 注解指定 (最高优先级)
↓
2.实体类级别配置
↓
3.application.yml 全局配置
↓
4.MyBatis-Plus 框架默认值 (最低优先级)
b.第一优先级:@TableId 注解
a.代码
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
b.说明
作用范围:单个字段
优先级:最高,会覆盖所有其他配置
适用场景:需要特殊处理的主键字段
c.第二优先级:实体类级别配置
a.代码
@TableName(value = "table_name", autoResultMap = true)
public class Entity {
// 实体类级别的配置
}
b.说明
作用范围:整个实体类
优先级:中等,仅次于注解级别
d.第三优先级:application.yml 全局配置
a.代码
mybatis-plus:
# 全局策略配置
global-config:
# ---------- 数据库相关配置 ----------
db-config:
# 主键生成策略:
# AUTO(0): 数据库自增
# INPUT(1): 用户输入
# ASSIGN_ID(2): 雪花算法
# ASSIGN_UUID(3): UUID 字符串
id-type: ASSIGN_UUID
# 字段插入和更新策略:
# IGNORED(0): 忽略判断
# NOT_NULL(1): 只对非 NULL 字段操作
# NOT_EMPTY(2): 只对非空(非 NULL 且非空串/空集合)字段操作
field-strategy: 2
b.说明
作用范围:整个应用
优先级:较低,会被注解覆盖
e.第四优先级:框架默认值
a.MyBatis-Plus 3.3.0+ 之后
默认为 `IdType.ASSIGN_ID`
b.MyBatis-Plus 3.3.0 之前
:默认为 `IdType.NONE`
02.ID生成策略
a.汇总
策略类型 值 说明 适用场景
AUTO 0 数据库自增 单机数据库,性能要求高
NONE 1 无策略 手动指定ID
INPUT 2 用户输入 业务需要指定ID
ASSIGN_ID 3 雪花算法 分布式系统,Long类型
ASSIGN_UUID 4 UUID字符串 分布式系统,String类型
ASSIGN_UUID_32 5 32位UUID 兼容性要求高
b.ASSIGN_UUID
a.代码
// 生成示例:550e8400-e29b-41d4-a716-446655440000
@TableId(type = IdType.ASSIGN_UUID)
private String id;
b.说明
优点:全局唯一、无需协调、支持分布式
缺点:存储空间大、索引性能略低
适用:分布式系统、微服务架构
c.ASSIGN_ID
a.代码
// 生成示例:1234567890123456789
@TableId(type = IdType.ASSIGN_ID)
private Long id;
b.说明
优点:性能高、存储空间小、索引友好
缺点:依赖时钟、时钟回拨问题
适用:高性能要求、单机或小规模集群
03.MapStruct与MyBatis-Plus搭配
a.优先级
a.最高
MapStruct 表达式优先级更高!
b.实体类中的 MyBatis-Plus 注解
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
c.MapStruct 映射器中的表达式
@Mapping(target = "id", expression = "java(cn.myslayers.common.core.util.UUIDUtil.getUUID())")
b.执行顺序
a.步骤1:MapStruct 映射执行
// 当调用这个方法时
position = OnbAllocateMSTs.AllocateUpdateMapper.INSTANCE.createPositionFromDTO(dto);
// MapStruct 会:
// 1. 使用 expression 生成 UUID
// 2. 将生成的 UUID 赋值给 position.id
// 3. 此时 position.id 已经有值了
b.步骤2:MyBatis-Plus 插入执行
// 当执行插入时
positionMapper.insert(position);
// MyBatis-Plus 会:
// 1. 检查 position.id 是否已有值
// 2. 发现 position.id 已经有值(MapStruct 生成的)
// 3. 直接使用这个值,忽略 @TableId 的 IdType.ASSIGN_UUID 策略
c.使用方案
a.方案1(使用):当前配置的实际行为
// 1. MapStruct 生成 UUID
String generatedId = cn.myslayers.common.core.util.UUIDUtil.getUUID();
// 结果:550e8400-e29b-41d4-a716-446655440000
// 2. 赋值给实体
position.setId(generatedId);
// 3. MyBatis-Plus 插入时
// 由于 id 已有值,直接使用,不会重新生成
b.方案2(推荐)
// 让 MyBatis-Plus 处理 ID 生成,MapStruct 只负责业务字段映射
@Mapper
public interface EvaluationMapper {
// 不配置 id 字段,让 MyBatis-Plus 自动处理
@Mapping(source = "applicantId", target = "applicantId")
@Mapping(source = "positionId", target = "applicantPositionId")
// ... 其他业务字段映射
OnbInterviewEvaluation createEvaluation(String applicantId, String positionId);
}
2.5 处理器:JsonArrayTypeHandler
01.SQL数据库
a.核心JSON字段设计
-- 应聘渠道JSON数组字段
`application_channels` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '应聘途径JSON数组字符串'
b.数据库存储示例
// application_channels 字段存储内容
["网站", "微信", "朋友推荐", "社会招聘会"]
02.实体类设计
a.主实体类设计
@Data
@Accessors(chain = true)
@TableName("onb_applicant")
@ApiModel(value = "OnbApplicant对象", description = "应聘者信息表")
public class OnbApplicant implements Serializable {
// JSON数组字段 - 使用专门的数组TypeHandler
@ApiModelProperty("应聘途径(多选)")
@TableField(value = "application_channels", typeHandler = JsonArrayTypeHandler.class)
private ApplicationChannels applicationChannels;
}
b.内部类设计
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonSerialize(using = ApplicationChannelsHandler.ApplicationChannelsSerializer.class)
@JsonDeserialize(using = ApplicationChannelsHandler.ApplicationChannelsDeserializer.class)
public static class ApplicationChannels {
private List<String> channels; // 应聘渠道列表,["网站", "微信", "朋友推荐", "社会招聘会", "校园招聘会"]
/**
* 检查是否为空
* @return true if channels is null or empty
*/
public boolean isEmpty() {
return channels == null || channels.isEmpty();
}
/**
* 获取渠道数量
* @return 渠道数量
*/
public int size() {
return channels == null ? 0 : channels.size();
}
/**
* 检查是否包含指定渠道
* @param channel 渠道名称
* @return true if contains the channel
*/
public boolean contains(String channel) {
return channels != null && channels.contains(channel);
}
}
03.MyBatis-Plus配置层
a.YAML配置
# ===============================================================
# MyBatis-Plus ORM 框架配置
# ===============================================================
mybatis-plus:
# 自定义 TypeHandler 所在包。MyBatis-Plus 启动时会扫描此包下所有继承 BaseTypeHandler,并标注 @MappedJdbcTypes 的类,自动注册到全局配置,用于复杂类型(如 JSON 与对象)映射
type-handlers-package: cn.myslayers.product.onboard.api.hander
b.自定义TypeHandler设计层
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* 通用的JSON数组字符串与特定业务对象转换的MyBatis TypeHandler基类
*
* @param <T> 具体的业务对象类型,如 ApplicationChannels
*/
public abstract class JsonArrayTypeHandler<T> extends BaseTypeHandler<T> {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final TypeReference<List<String>> TYPE_REFERENCE = new TypeReference<List<String>>() {};
/**
* 子类必须实现此方法,用于从业务对象中提取出 List<String>
* @param parameter 业务对象
* @return 提取出的字符串列表
*/
protected abstract List<String> extractList(T parameter);
/**
* 子类必须实现此方法,用于将 List<String> 包装成业务对象
* @param list 字符串列表
* @return 包装后的业务对象
*/
protected abstract T wrapList(List<String> list);
@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
try {
List<String> list = extractList(parameter);
if (list == null) {
ps.setString(i, null);
} else {
ps.setString(i, OBJECT_MAPPER.writeValueAsString(list));
}
} catch (JsonProcessingException e) {
throw new SQLException("Failed to serialize object to JSON array", e);
}
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
return parseJson(rs.getString(columnName));
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return parseJson(rs.getString(columnIndex));
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return parseJson(cs.getString(columnIndex));
}
private T parseJson(String json) throws SQLException {
if (json == null || json.trim().isEmpty() || "null".equalsIgnoreCase(json.trim())) {
return null;
}
try {
List<String> list = OBJECT_MAPPER.readValue(json, TYPE_REFERENCE);
return wrapList(list);
} catch (Exception e) {
throw new SQLException("Failed to parse JSON array to object", e);
}
}
}
c.具体类型处理器实现
/**
* 针对 ApplicationChannels 类型的具体 TypeHandler
*
* 功能说明:
* 1. 继承 BaseJsonArrayTypeHandler,复用 MyBatis 数据库处理逻辑
* 2. 提供自定义 Jackson 序列化器,确保 JSON 格式为纯数组:["网站", "微信"]
* 3. 而不是对象格式:{"channels": ["网站", "微信"]}
*/
@MappedJdbcTypes(JdbcType.VARCHAR)
public class ApplicationChannelsHandler extends JsonArrayTypeHandler<ApplicationChannels> {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final TypeReference<List<String>> TYPE_REFERENCE = new TypeReference<List<String>>() {};
// ==================== BaseJsonArrayTypeHandler 实现 ====================
/**
* 从 ApplicationChannels 对象中提取 List<String>
* 用于 MyBatis 存储到数据库
*/
@Override
protected List<String> extractList(ApplicationChannels parameter) {
return parameter != null ? parameter.getChannels() : null;
}
/**
* 将 List<String> 包装成 ApplicationChannels 对象
* 用于 MyBatis 从数据库读取
*/
@Override
protected ApplicationChannels wrapList(List<String> list) {
return new ApplicationChannels(list);
}
// ==================== Jackson 序列化器和反序列化器 ====================
/**
* 自定义 Jackson 序列化器
* 将 ApplicationChannels 对象序列化为 JSON 数组格式:["网站", "微信"]
* 而不是对象格式:{"channels": ["网站", "微信"]}
*/
public static class ApplicationChannelsSerializer extends JsonSerializer<ApplicationChannels> {
@Override
public void serialize(ApplicationChannels value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value == null || value.getChannels() == null) {
gen.writeNull();
} else {
// 直接写数组,不写对象
gen.writeStartArray();
for (String channel : value.getChannels()) {
gen.writeString(channel);
}
gen.writeEndArray();
}
}
}
/**
* 自定义 Jackson 反序列化器
* 将 JSON 数组格式:["网站", "微信"] 反序列化为 ApplicationChannels 对象
*/
public static class ApplicationChannelsDeserializer extends JsonDeserializer<ApplicationChannels> {
@Override
public ApplicationChannels deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// 直接读取数组为 List<String>
List<String> channels = p.readValueAs(TYPE_REFERENCE);
return new ApplicationChannels(channels);
}
}
// ==================== 便捷工具方法 ====================
/**
* 将 JSON 数组字符串转换为 ApplicationChannels 对象
* 用于控制器层手动转换
*/
public static ApplicationChannels fromJsonArray(String jsonArray) throws JsonProcessingException {
if (jsonArray == null || jsonArray.trim().isEmpty() || "null".equalsIgnoreCase(jsonArray.trim())) {
return null;
}
List<String> channels = OBJECT_MAPPER.readValue(jsonArray, TYPE_REFERENCE);
return new ApplicationChannels(channels);
}
/**
* 将 ApplicationChannels 对象转换为 JSON 数组字符串
* 用于控制器层手动转换
*/
public static String toJsonArray(ApplicationChannels applicationChannels) throws JsonProcessingException {
if (applicationChannels == null || applicationChannels.getChannels() == null) {
return null;
}
return OBJECT_MAPPER.writeValueAsString(applicationChannels.getChannels());
}
}
04.DTO设计层
a.数据传输对象设计
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public static class ApplicantUpdateDTO implements Serializable {
// DTO中也需要声明TypeHandler,确保参数传递时正确序列化
@TableField(value = "application_channels", typeHandler = ApplicationChannelsHandler.class)
private OnbApplicant.ApplicationChannels applicationChannels;
}
05.Mapper接口与XML配置
a.Mapper接口设计
@Mapper
@DS("onboard")
public interface OnbApplicantMapper {
List<ApplicantListVO> listByDto(@Param("params") ApplicantListDTO dto);
ApplicantDetailVO detailByDto(@Param("params") ApplicantDetailDTO dto);
int insertByDto(@Param("params") ApplicantInsertDTO dto);
int updateByDto(@Param("params") ApplicantUpdateDTO dto);
int deleteByDto(@Param("params") ApplicantDeleteDTO dto);
}
b.XML配置要点
<!-- 更新操作中,MyBatis-Plus会自动调用对应的TypeHandler -->
<update id="updateByDto">
UPDATE onb_applicant
<set>
<if test="params.homeAddress != null">
home_address = #{params.homeAddress},
</if>
<if test="params.pastHistory != null">
past_history = #{params.pastHistory},
</if>
<if test="params.applicationChannels != null">
application_channels = #{params.applicationChannels},
</if>
</set>
WHERE id = #{params.id}
</update>
06.前端数据处理
a.前端接收数据格式
// 从后端接收的数据格式
"applicationChannels": ["网站", "微信", "朋友推荐"]
b.前端表单处理
// Vue.js 组件中的数据处理
data() {
return {
formData: {
applicationChannels: []
}
}
},
methods: {
// 提交数据到后端
submitForm() {
// 前端数据格式与后端DTO完全匹配,无需额外转换
this.$http.post('/api/applicant/update', this.formData)
}
}
07.完整数据流转过程
a.数据写入流程
a.流程
前端表单数据
↓ (HTTP POST)
Controller接收DTO
↓ (参数绑定)
Service业务处理
↓ (调用Mapper)
MyBatis执行SQL
↓ (TypeHandler序列化)
JSON字符串存入数据库
b.说明
1.前端提交:`{homeAddress: {province: "黑龙江省", city: "哈尔滨市"}}`
2.Jackson反序列化:转换为`Address`对象
3.MyBatis参数设置:调用`AddressTypeHandler.setNonNullParameter()`
4.JSON序列化:`{"province":"黑龙江省","city":"哈尔滨市"}`
5.数据库存储:VARCHAR字段存储JSON字符串
b.数据读取流程
a.流程
数据库JSON字符串
↓ (ResultSet获取)
TypeHandler反序列化
↓ (转换为Java对象)
实体类映射
↓ (Service返回VO)
Controller返回前端
↓ (Jackson序列化)
前端接收JSON数据
b.说明
1.数据库读取:`{"province":"黑龙江省","city":"哈尔滨市"}`
2.TypeHandler反序列化:调用`AddressTypeHandler.getNullableResult()`
3.对象转换:创建`Address`对象实例
4.VO映射:Service层转换为前端VO对象
5.JSON响应:Jackson序列化返回给前端
2.6 处理器:JsonObjectTypeHandler
01.SQL数据库
a.核心JSON字段设计
-- 地址相关JSON对象字段
`home_address` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '现家庭住址JSON对象字符串',
`household_register` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '户口所在地JSON对象字符串',
`native_place` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '籍贯JSON对象字符串',
-- 医疗史相关JSON对象字段
`past_history` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '过往史JSON对象字符串',
`current_history` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '现在史JSON对象字符串',
`family_history` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '家族史JSON对象字符串',
`occupational_history` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '职业史JSON对象字符串',
b.数据库存储示例
// home_address 字段存储内容
{
"province": "黑龙江省",
"city": "哈尔滨市",
"district": "道里区",
"detail": "中山东路123号",
"police": "中山派出所",
"village": "中山社区",
"street": "中山东路",
"number": "123号"
}
// past_history 字段存储内容
{
"infectious": true,
"allergy": false,
"surgery": null,
"other": true
}
02.实体类设计
a.主实体类设计
@Data
@Accessors(chain = true)
@TableName("onb_applicant")
@ApiModel(value = "OnbApplicant对象", description = "应聘者信息表")
public class OnbApplicant implements Serializable {
// JSON对象字段 - 使用自定义TypeHandler
@ApiModelProperty("现家庭住址")
@TableField(value = "home_address", typeHandler = AddressTypeHandler.class)
private Address homeAddress;
@ApiModelProperty("户口所在地")
@TableField(value = "household_register", typeHandler = AddressTypeHandler.class)
private Address householdRegister;
@ApiModelProperty("籍贯")
@TableField(value = "native_place", typeHandler = AddressTypeHandler.class)
private Address nativePlace;
@ApiModelProperty("过往史")
@TableField(value = "past_history", typeHandler = PastHistoryTypeHandler.class)
private PastHistory pastHistory;
@ApiModelProperty("现在史")
@TableField(value = "current_history", typeHandler = CurrentHistoryTypeHandler.class)
private CurrentHistory currentHistory;
@ApiModelProperty("家族史")
@TableField(value = "family_history", typeHandler = FamilyHistoryTypeHandler.class)
private FamilyHistory familyHistory;
@ApiModelProperty("职业史")
@TableField(value = "occupational_history", typeHandler = OccupationalHistoryTypeHandler.class)
private OccupationalHistory occupationalHistory;
}
b.内部类设计
@Data
@NoArgsConstructor
public static class Address {
private String province; // 省份
private String city; // 城市
private String district; // 区/县
private String detail; // 详细地址
private String police; // 派出所名称
private String village; // 村或路
private String street; // 街道
private String number; // 门牌号
}
@Data
@NoArgsConstructor
public static class CurrentHistory {
private Boolean disability; // 残疾
private Boolean mental; // 精神疾病
private Boolean colorBlind; // 色盲/色弱
private Boolean other; // 其他
}
@Data
@NoArgsConstructor
public static class FamilyHistory {
private Boolean heart; // 心脏病
private Boolean hypertension; // 高血压
private Boolean diabetes; // 糖尿病
private Boolean other; // 其他
}
@Data
@NoArgsConstructor
public static class OccupationalHistory {
private Boolean ent; // 耳鼻喉职业病
private Boolean organ; // 脏器职业病
private Boolean other; // 其他
}
@Data
@NoArgsConstructor
public static class PastHistory {
private Boolean infectious; // 传染病史
private Boolean allergy; // 过敏史
private Boolean surgery; // 手术史
private Boolean other; // 其他
}
03.MyBatis-Plus配置层
a.YAML配置
# ===============================================================
# MyBatis-Plus ORM 框架配置
# ===============================================================
mybatis-plus:
# 自定义 TypeHandler 所在包。MyBatis-Plus 启动时会扫描此包下所有继承 BaseTypeHandler,并标注 @MappedJdbcTypes 的类,自动注册到全局配置,用于复杂类型(如 JSON 与对象)映射
type-handlers-package: cn.myslayers.product.onboard.api.hander
b.自定义TypeHandler设计层
/**
* 泛型 JSON 对象类型处理器
* 用于处理任意复杂对象与数据库 JSON 字符串之间的转换
*
* @param <T> 需要处理的对象类型,如 Address、PastHistory 等
*/
@MappedJdbcTypes(JdbcType.VARCHAR) // 指定处理的 JDBC 类型为 VARCHAR(JSON 字符串存储)
public class JsonObjectTypeHandler<T> extends BaseTypeHandler<T> {
// Jackson ObjectMapper 实例,用于 JSON 序列化和反序列化
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
// 静态初始化块:配置 ObjectMapper 忽略未知属性,避免反序列化时因字段不匹配而报错
static {
OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
// 存储具体的类型信息,用于泛型类型的实际反序列化
private Class<T> type;
/**
* 无参构造函数
* MyBatis 要求 TypeHandler 必须有无参构造函数,用于反射实例化
*/
public JsonObjectTypeHandler() {
// 必需的无参构造函数
}
/**
* 有参构造函数
* 用于具体子类(如 AddressTypeHandler)调用,传入具体的类型信息
*
* @param type 具体的类型 Class 对象,如 Address.class
*/
public JsonObjectTypeHandler(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.type = type;
}
/**
* 设置非空参数到 PreparedStatement(写入数据库)
* 将 Java 对象序列化为 JSON 字符串存储到数据库
*
* @param ps PreparedStatement 对象
* @param i 参数索引位置
* @param parameter 要序列化的 Java 对象
* @param jdbcType JDBC 类型
* @throws SQLException 如果序列化失败
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
try {
// 使用 Jackson 将 Java 对象转换为 JSON 字符串
ps.setString(i, OBJECT_MAPPER.writeValueAsString(parameter));
} catch (JsonProcessingException e) {
throw new SQLException("Failed to serialize object to JSON", e);
}
}
/**
* 从 ResultSet 中根据列名获取结果(从数据库读取)
*
* @param rs ResultSet 对象
* @param columnName 数据库列名
* @return 反序列化后的 Java 对象
* @throws SQLException 如果反序列化失败
*/
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
return parseJson(rs.getString(columnName));
}
/**
* 从 ResultSet 中根据列索引获取结果(从数据库读取)
*
* @param rs ResultSet 对象
* @param columnIndex 列索引
* @return 反序列化后的 Java 对象
* @throws SQLException 如果反序列化失败
*/
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return parseJson(rs.getString(columnIndex));
}
/**
* 从 CallableStatement 中获取结果(存储过程调用)
*
* @param cs CallableStatement 对象
* @param columnIndex 列索引
* @return 反序列化后的 Java 对象
* @throws SQLException 如果反序列化失败
*/
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return parseJson(cs.getString(columnIndex));
}
/**
* 解析 JSON 字符串为 Java 对象的核心方法
*
* @param json 从数据库读取的 JSON 字符串
* @return 反序列化后的 Java 对象,如果 JSON 为空则返回 null
* @throws SQLException 如果解析失败
*/
private T parseJson(String json) throws SQLException {
// 处理空值情况:null、空字符串、"null" 字符串
if (json == null || json.trim().isEmpty() || "null".equalsIgnoreCase(json.trim())) {
return null;
}
try {
// 检查类型是否已初始化(防御性编程)
if (this.type == null) {
// 在某些边缘情况下,如果type未被初始化,抛出异常
// 正常情况下,具体的子类(如AddressTypeHandler)会在构造函数中设置type
throw new SQLException("Type handler was not properly initialized with a target type.");
}
// 使用 Jackson 将 JSON 字符串反序列化为指定类型的 Java 对象
return OBJECT_MAPPER.readValue(json, type);
} catch (Exception e) {
throw new SQLException("Failed to parse JSON to object type: " + (type != null ? type.getName() : "unknown"), e);
}
}
}
c.具体类型处理器实现
/**
* 地址类型处理器:转换的类型为Address
*/
public class AddressTypeHandler extends JsonObjectTypeHandler<Address> {
public AddressTypeHandler() {
super(Address.class);
}
}
/**
* 当前历史类型处理器:转换的类型为CurrentHistory
*/
public class CurrentHistoryTypeHandler extends JsonObjectTypeHandler<CurrentHistory> {
public CurrentHistoryTypeHandler() {
super(CurrentHistory.class);
}
}
/**
* 家庭历史类型处理器:转换的类型为FamilyHistory
*/
public class FamilyHistoryTypeHandler extends JsonObjectTypeHandler<FamilyHistory> {
public FamilyHistoryTypeHandler() {
super(FamilyHistory.class);
}
}
/**
* 职业历史类型处理器:转换的类型为OccupationalHistory
*/
public class OccupationalHistoryTypeHandler extends JsonObjectTypeHandler<OccupationalHistory> {
public OccupationalHistoryTypeHandler() {
super(OccupationalHistory.class);
}
}
/**
* 过去历史类型处理器:转换的类型为PastHistory
*/
public class PastHistoryTypeHandler extends JsonObjectTypeHandler<PastHistory> {
public PastHistoryTypeHandler() {
super(PastHistory.class);
}
}
04.DTO设计层
a.数据传输对象设计
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public static class ApplicantUpdateDTO implements Serializable {
// DTO中也需要声明TypeHandler,确保参数传递时正确序列化
@TableField(value = "home_address", typeHandler = AddressTypeHandler.class)
private OnbApplicant.Address homeAddress;
@TableField(value = "household_register", typeHandler = AddressTypeHandler.class)
private OnbApplicant.Address householdRegister;
@TableField(value = "native_place", typeHandler = AddressTypeHandler.class)
private OnbApplicant.Address nativePlace;
@TableField(value = "past_history", typeHandler = PastHistoryTypeHandler.class)
private OnbApplicant.PastHistory pastHistory;
@TableField(value = "current_history", typeHandler = CurrentHistoryTypeHandler.class)
private OnbApplicant.CurrentHistory currentHistory;
@TableField(value = "family_history", typeHandler = FamilyHistoryTypeHandler.class)
private OnbApplicant.FamilyHistory familyHistory;
@TableField(value = "occupational_history", typeHandler = OccupationalHistoryTypeHandler.class)
private OnbApplicant.OccupationalHistory occupationalHistory;
}
05.Mapper接口与XML配置
a.Mapper接口设计
@Mapper
@DS("onboard")
public interface OnbApplicantMapper {
List<ApplicantListVO> listByDto(@Param("params") ApplicantListDTO dto);
ApplicantDetailVO detailByDto(@Param("params") ApplicantDetailDTO dto);
int insertByDto(@Param("params") ApplicantInsertDTO dto);
int updateByDto(@Param("params") ApplicantUpdateDTO dto);
int deleteByDto(@Param("params") ApplicantDeleteDTO dto);
}
b.XML配置要点
<!-- 更新操作中,MyBatis-Plus会自动调用对应的TypeHandler -->
<update id="updateByDto">
UPDATE onb_applicant
<set>
<if test="params.homeAddress != null">
home_address = #{params.homeAddress},
</if>
<if test="params.pastHistory != null">
past_history = #{params.pastHistory},
</if>
<if test="params.applicationChannels != null">
application_channels = #{params.applicationChannels},
</if>
</set>
WHERE id = #{params.id}
</update>
06.前端数据处理
a.前端接收数据格式
// 从后端接收的数据格式
{
"homeAddress": {
"province": "黑龙江省",
"city": "哈尔滨市",
"district": "道里区",
"detail": "中山东路123号"
},
"pastHistory": {
"infectious": true,
"allergy": false,
"surgery": null,
"other": true
},
}
b.前端表单处理
// Vue.js 组件中的数据处理
data() {
return {
formData: {
homeAddress: {
province: '',
city: '',
district: '',
detail: ''
},
pastHistory: {
infectious: null,
allergy: null,
surgery: null,
other: null
},
}
}
},
methods: {
// 提交数据到后端
submitForm() {
// 前端数据格式与后端DTO完全匹配,无需额外转换
this.$http.post('/api/applicant/update', this.formData)
}
}
07.完整数据流转过程
a.数据写入流程
a.流程
前端表单数据
↓ (HTTP POST)
Controller接收DTO
↓ (参数绑定)
Service业务处理
↓ (调用Mapper)
MyBatis执行SQL
↓ (TypeHandler序列化)
JSON字符串存入数据库
b.说明
1.前端提交:`{homeAddress: {province: "黑龙江省", city: "哈尔滨市"}}`
2.Jackson反序列化:转换为`Address`对象
3.MyBatis参数设置:调用`AddressTypeHandler.setNonNullParameter()`
4.JSON序列化:`{"province":"黑龙江省","city":"哈尔滨市"}`
5.数据库存储:VARCHAR字段存储JSON字符串
b.数据读取流程
a.流程
数据库JSON字符串
↓ (ResultSet获取)
TypeHandler反序列化
↓ (转换为Java对象)
实体类映射
↓ (Service返回VO)
Controller返回前端
↓ (Jackson序列化)
前端接收JSON数据
b.说明
1.数据库读取:`{"province":"黑龙江省","city":"哈尔滨市"}`
2.TypeHandler反序列化:调用`AddressTypeHandler.getNullableResult()`
3.对象转换:创建`Address`对象实例
4.VO映射:Service层转换为前端VO对象
5.JSON响应:Jackson序列化返回给前端
3 后端管理
3.1 实体:3类
00.汇总
a.切面切控制器
切点为所有控制器的所有方法,作用是打日志,判断有无预处理器,有预处理器则执行预处理逻辑,没有则进入控制器。
b.预处理层
对前端数据进行校验和预处理,因为前后分开,前端数据千变万化,所以这一层把数据进行校验和整理。
c.控制器层
调用service层,不做任何多余动作
d.service层
差不多是做事的,但独木难支,单独靠它自己还是需要写些冗余代码,
所以所有的service均继承一个convert接口和IBaseService接口,
IBaseService并包含两个方法,getDomainHelper和getDomainMapper方法,
它俩一个返回的对象用于实体类的创建,封装与校验,后者负责数据库交互,俩对象的类均为模板生成。
e.Mapper层
继承自basemapper,并以符合jpa命名规则的方式包含所有字段的各种单条件查询,负责与数据库交互,提供全方位无死角的crud
f.Helper层
每个实体都有一个helper,负责与实体类打交道
g.生成器
读取实体类,生成java的helper mapper,Typescript代码的实体接口定义,query查询辅助类
h.实体类
需要自行定义
i.过滤器
包含数据权限校验和Token校验
01.常见分类1
a.dto:数据传输对象
FlowApprovalDTOs.java # DTO 聚合模型
OnbApplicantDTOs.java # DTO 聚合模型
PageAllocateDTOs.java # DTO 聚合模型
PageEvaluateDTOs.java # DTO 聚合模型
PageOfferDTOs.java # DTO 聚合模型
b.entity:实体类
OnbApplicant.java # 应聘者实体
OnbApplicantStatus.java # 应聘状态实体
OnbWorkExperience.java # 工作经历实体
OnbTrainingExperience.java # 培训经历实体
OnbFamilyInfo.java # 家庭信息实体
OnbEducationExperience.java # 教育经历实体
c.enums:枚举类
AuditStatusEnum.java # 审核状态枚举
GenderEnum.java # 性别枚举
BloodTypeEnum.java # 血型枚举
EmploymentTypeEnum.java # 用工性质枚举
ProbationDurationEnum.java # 试用期时长枚举
AttachmentTypeEnum.java # 附件类型枚举
InterviewTypeEnum.java # 面试类型枚举
ApprovalStatusEnum.java # 审批状态枚举
SchoolNatureEnum.java # 学历性质枚举
d.mapstruct:映射结构
PageAllocateMSTs.java # MapStruct 聚合模型
PageEvaluateMSTs.java # MapStruct 聚合模型
PageOfferMSTs.java # MapStruct 聚合模型
e.vo:页面对象
FlowApprovalVOs.java # VO 聚合模型
OnbApplicantVOs.java # VO 聚合模型
PageAllocateVOs.java # VO 聚合模型
PageEvaluateVOs.java # VO 聚合模型
PageOfferVOs.java # VO 聚合模型
02.常见分类2
a.dto:数据传输对象
ApplicantDTO.java # 应聘者DTO
ApplicantStatusDTO.java # 应聘状态DTO
InterviewEvaluationDTO.java # 面试评估DTO
EmploymentInfoDTO.java # 录用信息DTO
ApprovalRecordDTO.java # 审批记录DTO
AssignmentDTO.java # 分配DTO
FeedbackDTO.java # 反馈DTO
b.entity:实体类
OnbApplicant.java # 应聘者实体
OnbApplicantStatus.java # 应聘状态实体
OnbWorkExperience.java # 工作经历实体
OnbTrainingExperience.java # 培训经历实体
OnbFamilyInfo.java # 家庭信息实体
OnbEducationExperience.java # 教育经历实体
c.enums:枚举类
AuditStatusEnum.java # 审核状态枚举
GenderEnum.java # 性别枚举
BloodTypeEnum.java # 血型枚举
EmploymentTypeEnum.java # 用工性质枚举
ProbationDurationEnum.java # 试用期时长枚举
AttachmentTypeEnum.java # 附件类型枚举
InterviewTypeEnum.java # 面试类型枚举
ApprovalStatusEnum.java # 审批状态枚举
SchoolNatureEnum.java # 学历性质枚举
d.mapstruct:映射结构
ApplicantMapper.java # 应聘者映射器
InterviewMapper.java # 面试映射器
EmploymentMapper.java # 录用映射器
e.vo:页面对象
ApplicantVO.java # 应聘者展示对象
ApplicantListVO.java # 应聘者列表展示对象
InterviewFeedbackVO.java # 面试反馈展示对象
EmploymentSupplementVO.java # 录用补录展示对象
ApprovalFlowVO.java # 审批流程展示对象
StatisticsVO.java # 统计展示对象
03.常见分类3
a.annotation:自定义注解
DictSn.java # 字典编号注解
ExcelExport.java # Excel导出注解
AuditLog.java # 审计日志注解
b.bo:业务对象
ApplicantAssignmentBo.java # 应聘者分配业务对象
InterviewFeedbackBo.java # 面试反馈业务对象
EmploymentSupplementBo.java # 录用补录业务对象
c.constants:常量类
OnboardConstants.java # 入职管理常量
AuditStatusConstants.java # 审核状态常量
ApprovalConstants.java # 审批常量
d.converter:转换器
ApplicantConverter.java # 应聘者转换器
StatusConverter.java # 状态转换器
InterviewConverter.java # 面试转换器
e.eo:导出对象
ApplicantExportEO.java # 应聘者导出对象
InterviewResultEO.java # 面试结果导出对象
EmploymentStatisticsEO.java # 录用统计导出对象
f.processor:处理器
StatusFlowProcessor.java # 状态流转处理器
ApprovalProcessor.java # 审批处理器
NotificationProcessor.java # 通知处理器
04.聚合模型
a.PageAllocateDTOs
public class PageAllocateDTOs {
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public static class AllocateDetailDTO implements Serializable {
private static final long serialVersionUID = 1L;
private String applicantId;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public static class AllocateUpdateDTO implements Serializable {
private static final long serialVersionUID = 1L;
private String applicantId;
private String applyPosition;
private String assignedDept;
private String assignedUser;
private Date assignedDate;
private String assignedReason;
}
}
b.PageAllocateVOs
public class PageAllocateVOs {
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public static class AllocateDetailVO implements Serializable {
private static final long serialVersionUID = 1L;
private String applicantId;
private String applyPosition;
private String assignedDept;
private String assignedUser;
private Date assignedDate;
private String assignedReason;
}
}
c.PageAllocateMSTs
public class PageAllocateMSTs {
/**
* 分配详情转换器
*/
@Mapper
public interface AllocateDetailMapper {
AllocateDetailMapper INSTANCE = Mappers.getMapper(AllocateDetailMapper.class);
/**
* OnbInterviewPosition 转 AllocateDetailVO
*/
@Mapping(source = "applicantId", target = "applicantId")
@Mapping(source = "applyPosition", target = "applyPosition")
@Mapping(source = "assignedDept", target = "assignedDept")
@Mapping(source = "assignedUser", target = "assignedUser")
@Mapping(source = "assignedDate", target = "assignedDate")
@Mapping(source = "assignedReason", target = "assignedReason")
PageAllocateVOs.AllocateDetailVO positionToDetailVO(OnbInterviewPosition position);
}
}
3.2 日志:log4j
01.使用方式
a.SpringBoot自带的实现 Slf4j + logback
a.日志级别(7级)
TRACE < DEBUG < INFO(默认,只打印INFO后的级别信息) < WARN < ERROR < FATAL < OFF
b.自定义日志级别
logging.level.org.myslayers.HelloWorld=warn
b.自定义:Slf4j + Log4j2
a.原理
应用代码 → SLF4J API → SLF4J 绑定 → Log4j2 实现
b.细节
SLF4J 只是一个接口层,它允许你编写与具体日志实现无关的代码。
SLF4J(Simple Logging Facade for Java)本身并不是一个具体的日志实现,而是一个日志门面。
这意味着你不能“使用 SLF4J 日志”来直接输出日志,而是要通过 SLF4J 提供的 API 来调用底层具体的日志实现
(比如 Logback、Log4j2、java.util.logging 等)来完成日志的输出。
c.简单比喻
Log4j 1.x -> Logback:像是从 Windows XP 升级到了 Windows 7,系统更流畅、界面更美观、功能更强。
Logback -> Log4j 2:像是从 Windows 系统切换到了 macOS,理念和底层架构都有了巨大的变化,旨在提供一种更极致的体验
02.SLF4J绑定架构
a.图示
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 应用代码 │───▶│ SLF4J API │───▶│ SLF4J 绑定 │
│ (@Slf4j/手动) │ │ (slf4j-api) │ │(log4j-slf4j-impl)│
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Log4j2 Core │
│ (log4j-core) │
└─────────────────┘
b.完整的Maven依赖配置
<dependencies>
<!-- Lombok 支持 @Slf4j -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- SLF4J API -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<!-- Log4j2 核心 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.20.0</version>
</dependency>
<!-- Log4j2 API -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.20.0</version>
</dependency>
<!-- SLF4J 到 Log4j2 的绑定 - 关键依赖 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.20.0</version>
</dependency>
</dependencies>
c.SLF4J绑定机制详解
1.类路径扫描: SLF4J 在启动时扫描类路径中的 `org/slf4j/impl/StaticLoggerBinder.class`
2.绑定选择: 找到 `log4j-slf4j-impl` 提供的绑定实现
3.Logger 工厂: 使用 `Log4jLoggerFactory` 创建 Logger 实例
4.日志委托: 将所有 SLF4J 调用委托给 Log4j2
d.启动日志示例
SLF4J: Found binding in [jar:file:/path/to/log4j-slf4j-impl-2.20.0.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]
03.混合使用,完全可行
a.代码
// 同一个类中混合使用三种方式
@Slf4j // Lombok 生成的 SLF4J Logger
public class MixedLoggingService {
// 手动声明的 SLF4J Logger
private static final Logger manualSlf4jLogger =
LoggerFactory.getLogger("MANUAL_SLF4J");
// 直接使用 Log4j2 Logger
private static final org.apache.logging.log4j.Logger log4j2Logger =
LogManager.getLogger("DIRECT_LOG4J2");
public void demonstrateLogging() {
// 方式1: @Slf4j 注解生成的 logger
log.info("使用 @Slf4j 注解的日志");
// 方式2: 手动声明的 SLF4J logger
manualSlf4jLogger.warn("手动声明的 SLF4J 日志");
// 方式3: 直接使用 Log4j2 logger
log4j2Logger.error("直接使用 Log4j2 的日志");
}
}
b.输出
2025-01-09 14:30:15.123 [main] INFO MixedLoggingService - 使用 @Slf4j 注解的日志
2025-01-09 14:30:15.124 [main] WARN MANUAL_SLF4J - 手动声明的 SLF4J 日志
2025-01-09 14:30:15.125 [main] ERROR DIRECT_LOG4J2 - 直接使用 Log4j2 的日志
3.3 断言:assert
01.两种写法
a.使用断言
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public void cancelOrder(Long orderId) {
// --- 前置条件检查 ---
ServiceAssert.isNotNull(orderId, "订单ID不能为空");
Order order = orderMapper.selectById(orderId);
AssertService.isNotNull(order, "订单不存在");
boolean canCancel = order.getStatus().equals(OrderStatus.PENDING_PAYMENT) ||
order.getStatus().equals(OrderStatus.PENDING_SHIPPING);
// 使用带日志记录的断言,方便排查问题
AssertService.isTrue(canCancel,
"当前订单状态无法取消", // 这个消息给前端
"取消订单失败:订单 " + orderId + " 状态为 " + order.getStatus() // 这个消息记录在后端日志
);
// --- 核心业务逻辑 ---
// 只有所有断言都通过,代码才会执行到这里
System.out.println("订单 " + orderId + " 已成功取消。");
// ... 执行真正的取消订单业务逻辑 ...
}
}
b.不使用断言
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public void cancelOrder(Long orderId) {
// 校验1: 订单ID
if (orderId == null) {
throw new ServiceException("订单ID不能为空");
}
// 查询订单
Order order = orderMapper.selectById(orderId);
// 校验2: 订单是否存在
if (order == null) {
throw new ServiceException("订单不存在");
}
// 校验3: 订单状态
boolean canCancel = order.getStatus().equals(OrderStatus.PENDING_PAYMENT) ||
order.getStatus().equals(OrderStatus.PENDING_SHIPPING);
if (!canCancel) {
throw new ServiceException("当前订单状态无法取消");
}
// ... 执行真正的取消订单业务逻辑 ...
System.out.println("订单 " + orderId + " 已成功取消。");
}
}
02.ServiceAssert类
a.实现
package cn.myslayers.common.core.util;
import com.jhict.common.core.exception.ServiceException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Collection;
import java.util.List;
/**
* 业务断言工具类。
* <p>
* 用于在 Service 层进行参数校验或状态检查。当断言失败时,会抛出 {@link ServiceException},
* 中断业务流程。这有助于保持业务方法的代码清洁,并实现快速失败。
*/
public class AssertService {
private static final Logger log = LogManager.getLogger(ServiceAssert.class);
/**
* 断言布尔表达式为 true。
* 如果表达式为 false,则抛出 ServiceException。
*
* @param expression 布尔表达式
* @param message 如果断言失败,抛出的异常消息
* @throws ServiceException 如果表达式为 false
*/
public static void isTrue(boolean expression, String message) throws ServiceException {
if (!expression) {
throw new ServiceException(message);
}
}
/**
* 断言对象为 null。
* 如果对象不为 null,则抛出 ServiceException。
*
* @param object 要检查的对象
* @param message 如果断言失败,抛出的异常消息
* @throws ServiceException 如果对象不为 null
*/
public static void isNull(Object object, String message) throws ServiceException {
if (object != null) {
throw new ServiceException(message);
}
}
/**
* 断言布尔表达式为 true,并在断言失败时记录一条额外的错误日志。
* 这对于需要记录更详细上下文信息的场景非常有用。
*
* @param expression 布尔表达式
* @param message 如果断言失败,抛出的异常消息(通常给用户看)
* @param logErrorInfo 如果断言失败,记录到错误日志中的详细信息(通常给开发人员看)
* @throws ServiceException 如果表达式为 false
*/
public static void isTrue(boolean expression, String message, String logerrorInfo) throws ServiceException {
if (!expression) {
log.error(logerrorInfo);
throw new ServiceException(message);
}
}
/**
* 断言对象不为 null。
* 这是最常用的断言之一,用于检查方法参数或查询结果。
*
* @param object 要检查的对象
* @param message 如果断言失败,抛出的异常消息
* @throws ServiceException 如果对象为 null
*/
public static void isNotNull(Object object, String message) throws ServiceException {
if (object == null) {
throw new ServiceException(message);
}
}
/**
* 断言 List 集合不为空。
*
* @deprecated 此方法已被 {@link #collectionHasValue(Collection, String)} 替代,
* 因为后者更通用(支持所有 Collection 类型)并且能处理 null 的情况。
* @param list 要检查的 List 集合
* @param message 如果断言失败,抛出的异常消息
* @throws ServiceException 如果 List 为空
*/
@Deprecated
public static void arrayHasValue(List<? extends Object> list, String message) throws ServiceException {
if (list.isEmpty()) {
throw new ServiceException(message);
}
}
/**
* 断言集合不为 null 且不为空。
*
* @param collection 要检查的集合 (例如 List, Set 等)
* @param message 如果断言失败,抛出的异常消息
* @throws ServiceException 如果集合为 null 或为空
*/
public static void collectionHasValue(Collection<? extends Object> collection, String message) throws ServiceException {
if (collection == null || collection.isEmpty()) {
throw new ServiceException(message);
}
}
/**
* 断言字符串有实际内容(非 null、非空字符串、且不只由空白字符组成)。
*
* @param text 要检查的字符串
* @param message 如果断言失败,抛出的异常消息
* @throws ServiceException 如果字符串没有实际内容
*/
public static void hasText(String text, String message) throws ServiceException {
if (!hasText(text)) {
throw new ServiceException(message);
}
}
/**
* 断言 List<String> 集合中包含目标字符串。
*
* @param target 目标字符串
* @param resource 字符串 List 集合
* @param message 如果断言失败,抛出的异常消息
* @throws ServiceException 如果集合不包含目标字符串
*/
public static void arrayContains(String target, List<String> resource, String message) throws ServiceException {
if (!resource.contains(target)) {
throw new ServiceException(message);
}
}
/**
* 检查字符串是否有文本内容的私有辅助方法。
*
* @param str 要检查的字符串
* @return 如果有文本内容则返回 true,否则返回 false
*/
private static boolean hasText(String str) {
return str != null && !str.isEmpty() && !str.trim().isEmpty();
}
}
3.4 链路:traceId
01.配置说明
a.图示
config/
└── TraceIdConfiguration.java # 统一配置类
├── AopConfig (内部类) # AOP 配置区域
├── ControllerAspect (内部类) # Controller 切面区域
├── MybatisInterceptor (内部类) # MyBatis 拦截器区域
├── 自动配置区域 # WebMvcConfigurer 实现
└── 工具类区域 # MdcTaskDecorator
b.请求链路
HTTP请求 → TraceInterceptor → Controller → Service → Mapper → Database
↓ ↓ ↓ ↓ ↓ ↓
生成traceId 设置MDC 记录日志 记录日志 SQL拦截 执行SQL
↓ ↓ ↓ ↓ ↓ ↓
返回响应 ← 清理MDC ← 返回结果 ← 返回结果 ← 返回结果 ← 查询结果
c.配置类结构
@Configuration
@ConditionalOnProperty(name = "libra.trace.enabled", havingValue = "true", matchIfMissing = true)
public class TraceIdConfiguration implements WebMvcConfigurer {
// ===============================================================
// AOP 配置区域 - 启用 AspectJ 自动代理
// ===============================================================
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
static class AopConfig { }
// ===============================================================
// Controller AOP 切面区域 - 彩色高亮日志监测
// ===============================================================
@Aspect
@Component
@ConditionalOnProperty(name = "libra.trace.controller.aop.enabled", havingValue = "true")
static class ControllerAspect { }
// ===============================================================
// MyBatis 拦截器区域 - 全面数据库操作拦截
// ===============================================================
@Intercepts({...}) // 拦截 Executor + StatementHandler
static class MybatisInterceptor implements Interceptor { }
// ===============================================================
// 自动配置区域 - WebMvcConfigurer + Bean 定义
// ===============================================================
// 拦截器注册、线程池配置、RestTemplate 配置等
}
02.核心组件说明
a.TraceId 生成器
TraceIdGenerator.java 生成唯一的 traceId
格式: `{IP16进制}{时间戳}{递增序号}_{进程ID}`
示例: `ffffffff17257461411001_12345`
b.MDC 工具类
MDCUtils.java 管理 MDC 上下文
功能: 自动生成或从请求头提取 traceId
c.链路追踪上下文
TracerSpanContext.java 管理服务调用链
TracerUtils.java 提供工具方法
TraceIdService.java traceId 服务管理
d.拦截器组件
TraceInterceptor.java HTTP 请求拦截器
TraceClientHttpRequestInterceptor.java HTTP 客户端拦截器
TraceIdMybatisInterceptor.java MyBatis 数据库操作拦截器
TraceControllerAspect.java Controller 层 AOP 监测切面
03.配置说明
a.yml
# ===============================================================
# TraceId 链路追踪系统配置 (TraceId Tracing System Configuration)
# ===============================================================
libra:
trace:
enabled: true # 是否启用 TraceId 链路追踪系统
controller:
aop:
enabled: true # 是否启用 Controller AOP 监测
colorful: true # 是否启用彩色高亮输出
logger:
print:
printLink: true # 是否打印链路信息
b.TraceIdConfiguration.java 侵入式统一配置
@Configuration
@ConditionalOnProperty(name = "libra.trace.enabled", havingValue = "true", matchIfMissing = true)
public class TraceIdConfiguration implements WebMvcConfigurer {
// 统一管理所有 TraceId 相关配置
}
c.自动注册的组件
HTTP 拦截器 (`TraceInterceptor`)
HTTP 客户端拦截器 (`TraceClientHttpRequestInterceptor`)
MyBatis 数据库拦截器 (`MybatisInterceptor`)
Controller AOP 切面 (`ControllerAspect`)
异步线程池 (`traceThreadPoolTaskExecutor`)
RestTemplate (`traceRestTemplate`)
MDC 任务装饰器 (`MdcTaskDecorator`)
3.5 枚举:MapEnum1
01.自定义接口-使用
a.OnbMapEnum
/**
* 审批流程撤回标志枚举
*/
@Getter
@AllArgsConstructor
public enum ApprovalToStartEnum implements OnbMapEnum {
NO(0, "否"),
YES(1, "是");
@EnumValue
private int key;
private String value;
public int getKey() {
return key;
}
public String getValue() {
return value;
}
public static Map<Integer, ApprovalToStartEnum> KEYMAPS = new HashMap<>();
public static Map<String, ApprovalToStartEnum> VALUEMAPS = new HashMap<>();
static {
for (ApprovalToStartEnum enum_ : ApprovalToStartEnum.values()) {
KEYMAPS.put(enum_.getKey(), enum_);
VALUEMAPS.put(enum_.getValue(), enum_);
}
}
public static ApprovalToStartEnum getByKey(Integer key) {
return KEYMAPS.get(key);
}
public static ApprovalToStartEnum getByValue(String value) {
return VALUEMAPS.get(value);
}
}
b.OnbStringKeyMapEnum
/**
* 应聘者状态枚举
*/
@Getter
@AllArgsConstructor
public enum ApplicantStatusEnum {
APPLIED("APPLIED", "已申请"),
INTERVIEW("INTERVIEW", "面试中"),
OFFERED("OFFERED", "已录用"),
REJECTED("REJECTED", "已拒绝");
@EnumValue // <-- MyBatis-Plus: 标记此字段为存入数据库的值
private final String key;
private final String value;
/**
* Jackson 序列化方法
* @JsonValue 告诉 Jackson,当序列化此枚举时,调用此方法,并将其返回值作为 JSON 的值。
* @return 枚举的 key,将作为纯字符串输出
*/
@JsonValue
public String getKey() {
return key;
}
public static final Map<String, ApplicantStatusEnum> KEYMAPS = new HashMap<>();
public static final Map<String, ApplicantStatusEnum> VALUEMAPS = new HashMap<>();
static {
for (ApplicantStatusEnum enum_ : ApplicantStatusEnum.values()) {
KEYMAPS.put(enum_.getKey(), enum_);
VALUEMAPS.put(enum_.getValue(), enum_);
}
}
/**
* 传统的静态查找方法:通过 key 获取枚举实例。
* 同时,此方法也作为 Jackson 的反序列化入口。
* @JsonCreator 告诉 Jackson,当从 JSON 反序列化时,调用此静态方法。
*
* @param key 从 JSON 字符串中读取到的值,或代码中传入的 key
* @return 匹配的枚举实例,或 null
*/
@JsonCreator
public static ApplicantStatusEnum getByKey(String key) {
return KEYMAPS.get(key);
}
/**
* 传统的静态查找方法:通过 value 获取枚举实例。
* @param value 描述文本
* @return 匹配的枚举实例,或 null
*/
public static ApplicantStatusEnum getByValue(String value) {
return VALUEMAPS.get(value);
}
}
02.自定义接口-取值
a.ApprovalToStartEnum (整数Key) 的取值方式
a.直接访问枚举实例
ApprovalToStartEnum isApproved = ApprovalToStartEnum.YES;
System.out.println(isApproved); // 输出: YES
b.从枚举实例中获取属性
ApprovalToStartEnum status = ApprovalToStartEnum.YES;
int key = status.getKey(); // 返回 1
String value = status.getValue(); // 返回 "是"
String name = status.name(); // 返回 "YES"
int ordinal = status.ordinal(); // 返回 1
c.通过 Key 或 Value 反向查找枚举实例
ApprovalToStartEnum statusFromKey = ApprovalToStartEnum.getByKey(1); // 返回 YES
ApprovalToStartEnum statusFromValue = ApprovalToStartEnum.getByValue("否"); // 返回 NO
d.遍历所有枚举实例
for (ApprovalToStartEnum status : ApprovalToStartEnum.values()) {
System.out.println("Key: " + status.getKey() + ", Value: " + status.getValue());
}
b.ApplicantStatusEnum (字符串Key) 的取值方式
a.直接访问枚举实例
ApplicantStatusEnum currentStatus = ApplicantStatusEnum.INTERVIEW;
System.out.println(currentStatus); // 输出: INTERVIEW
b.从枚举实例中获取属性
ApplicantStatusEnum status = ApplicantStatusEnum.OFFERED;
String key = status.getKey(); // 返回 "OFFERED"
String value = status.getValue(); // 返回 "已录用"
String name = status.name(); // 返回 "OFFERED"
int ordinal = status.ordinal(); // 返回 2
c.通过 Key 或 Value 反向查找枚举实例
ApplicantStatusEnum statusFromKey = ApplicantStatusEnum.getByKey("APPLIED"); // 返回 APPLIED
ApplicantStatusEnum statusFromValue = ApplicantStatusEnum.getByValue("已拒绝"); // 返回 REJECTED
d.遍历所有枚举实例
for (ApplicantStatusEnum status : ApplicantStatusEnum.values()) {
System.out.println("Key: " + status.getKey() + ", Value: " + status.getValue());
}
03.自定义接口-实例比较
a.使用 == 运算符 (最佳实践)
a.代码
ApprovalToStartEnum currentStatus = ApprovalToStartEnum.YES;
if (currentStatus == ApprovalToStartEnum.YES) {
System.out.println("审批已通过!");
}
b.优点
性能最高:直接进行内存地址比较。
编译期类型安全:不同类型枚举无法比较。
Null安全:`null == ApprovalToStartEnum.YES` 返回 `false`。
b.使用 .equals() 方法
a.代码
ApprovalToStartEnum currentStatus = ApprovalToStartEnum.YES;
if (currentStatus.equals(ApprovalToStartEnum.YES)) {
System.out.println("审批已通过!");
}
b.缺点
存在 `NullPointerException` 风险。
性能稍差:方法调用有微小开销。
失去编译期类型安全。
c.使用 switch 语句 (推荐)
ApplicantStatusEnum currentStatus = ApplicantStatusEnum.OFFERED;
switch (currentStatus) {
case APPLIED:
System.out.println("处理“已申请”状态的逻辑...");
break;
case INTERVIEW:
System.out.println("处理“面试中”状态的逻辑...");
break;
case OFFERED:
System.out.println("处理“已录用”状态的逻辑...");
break;
case REJECTED:
System.out.println("处理“已拒绝”状态的逻辑...");
break;
default:
System.out.println("未知状态");
break;
}
d.与枚举的属性进行比较
int statusCodeFromApi = 1;
String statusKeyFromApi = "APPLIED";
if (statusCodeFromApi == ApprovalToStartEnum.YES.getKey()) {
System.out.println("API状态码 1 代表“是”。");
}
if (statusKeyFromApi.equals(ApplicantStatusEnum.APPLIED.getKey())) {
System.out.println("API状态Key 'APPLIED' 代表“已申请”。");
}
04.自定义接口-MapEnum
a.注解
/**
* MapEnum 接口:约定枚举必须提供一个数值型键(getKey)和一个文本型值(getValue)。
* 通过 JsonSerialize 和 JsonDeserialize 注解,绑定自定义的序列化/反序列化逻辑。
*
* @param <I> 枚举键的类型,继承自 Number,通常为 Integer 或 Long。
*/
@JsonSerialize(using = MapEnumSerializer.class) // 序列化:枚举 -> JSON 对象
@JsonDeserialize(using = MapEnumDeserializer.class) // 反序列化:JSON -> 枚举
public interface MapEnum<I extends Number> {
/**
* 获取枚举的数字键,用于数据库存储及 JSON 传输中的 key 字段。
* 示例:1、2、3 等。
*
* @return 枚举对应的数字键
*/
int getKey();
/**
* 获取枚举的显示文本,用于前端展示或日志记录。
* 示例:"初中"、"大专" 等。
*
* @return 枚举对应的文本值
*/
String getValue();
}
b.序列化
/**
* MapEnumSerializer 实现:
* 1. 将入参对象强制转换为 MapEnum,确保可以调用 getKey() 和 getValue()。
* 2. 创建一个 Map,填充 "key"(数字键)、"value"(文本值)、"logicType"(固定值 "enums")。
* 3. 使用 JsonGenerator.writeObject 将整个 Map 写入 JSON 输出。
*
* 序列化示例输出:
* {
* "key": 4,
* "value": "大专",
* "logicType": "enums"
* }
*/
public class MapEnumSerializer extends JsonSerializer {
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
@SuppressWarnings("unchecked")
MapEnum<Number> mapEnum = (MapEnum<Number>) value;
Map<String, Object> out = new HashMap<>(3);
out.put("key", mapEnum.getKey());
out.put("value", mapEnum.getValue());
out.put("logicType", "enums");
gen.writeObject(out);
}
}
c.反序列化
/**
* MapEnumDeserializer 实现:
* 1. 读取当前 JSON 属性的节点:可能是 ObjectNode、IntNode、TextNode 或 ArrayNode。
* 2. 尝试从节点中提取名为 "key" 的值,并转换成 Integer。
* 3. 通过 BeanUtils.findPropertyType 动态获取当前属性对应的枚举类类型。
* 4. 遍历该枚举类的所有常量,匹配 getKey() 与提取的 key 值,返回对应枚举实例。
* 5. 如果未匹配,则返回 null(可根据业务需求改为抛异常或返回默认枚举)。
*/
public class MapEnumDeserializer extends JsonDeserializer<MapEnum<Number>> {
/**
* 构造方法:可用于初始化日志或调试信息。
*/
protected MapEnumDeserializer() {
// 仅示例打印,实际可移除或改为日志输出
System.out.println("初始化 MapEnumDeserializer");
}
@Override
public MapEnum<Number> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// 1. 将整个 JSON 内容读取为通用树节点
TreeNode treeNode = p.getCodec().readTree(p);
Integer key = null;
// 2. 根据节点类型,提取 key 值
if (treeNode instanceof ObjectNode) {
// 对象格式:{ "key": 4, "value": "大专", "logicType": "enums" }
ObjectNode obj = (ObjectNode) treeNode;
// 尝试以整数形式读取
key = obj.get("key").asInt();
if (key == null) {
// 当 asInt() 返回 null 时,再尝试以文本形式读取并转换
key = Integer.valueOf(obj.get("key").asText());
}
} else if (treeNode instanceof IntNode) {
// 纯数字格式:4
key = ((IntNode) treeNode).asInt();
} else if (treeNode instanceof TextNode) {
// 文本数字格式:"4"
key = Integer.valueOf(((TextNode) treeNode).asText());
} else if (treeNode instanceof ArrayNode) {
// 数组格式:[ { "key": 4 } ]
ArrayNode array = (ArrayNode) treeNode;
key = array.findValue("key").asInt();
}
// 3. 获取当前 JSON 属性名及其所属对象,用于反射获取字段类型
String propName = p.getCurrentName();
Class<?> parentClass = ctxt.getParser().getCurrentValue().getClass();
@SuppressWarnings("unchecked")
// 4. 通过 BeanUtils 查找父对象中名为 propName 的字段类型,转换为 MapEnum 子类型
Class<? extends MapEnum<Number>> enumClass =
(Class<? extends MapEnum<Number>>) BeanUtils.findPropertyType(propName, new Class<?>[]{parentClass});
// 5. 遍历枚举常量,匹配 key
for (MapEnum<Number> constant : enumClass.getEnumConstants()) {
if (constant.getKey() == key) {
return constant;
}
}
// 6. 未找到匹配时返回 null(可替换逻辑)
return null;
}
}
05.自定义接口-StringKeyMapEnum
a.注解
/**
* StringKeyMapEnum 接口:约定枚举必须提供一个字符串键(getKey)和一个文本值(getValue)。
*/
@JsonSerialize(using = StringKeyMapEnumSerializer.class) // 序列化:枚举 -> 字符串 key
@JsonDeserialize(using = StringKeyMapEnumDeserializer.class) // 反序列化:字符串/对象 -> 枚举
public interface StringKeyMapEnum {
/**
* 获取枚举在 JSON 中的字符串键。
* @return 枚举对应的字符串键
*/
String getKey();
/**
* 获取枚举的文本展示值。
* @return 枚举对应的显示文本
*/
String getValue();
}
b.序列化
/**
* StringKeyMapEnumSerializer 实现:
* 1. 强制转换枚举为 StringKeyMapEnum。
* 2. 调用 getKey() 输出字符串。
*
* 序列化示例输出:
* "MY_ENUM_KEY"
*/
public class StringKeyMapEnumSerializer extends JsonSerializer {
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
StringKeyMapEnum mapEnum = (StringKeyMapEnum) value;
gen.writeObject(mapEnum.getKey());
}
}
c.反序列化
/**
* StringKeyMapEnumDeserializer 实现:
* 1. 读取 JSON 为 TreeNode。
* 2. 若为 ObjectNode,则获取 "key" 属性文本;TextNode 则直接获取文本;ArrayNode 则查找子节点 "key"。
* 3. 获取当前属性名与父对象类型,通过 BeanUtils 查找字段实际类型(枚举类)。
* 4. 遍历枚举常量,采用 String.equals 比对 key,并返回匹配常量。
* 5. 未匹配时返回 null(或抛异常/默认值)。
*/
public class StringKeyMapEnumDeserializer extends JsonDeserializer<StringKeyMapEnum> {
protected StringKeyMapEnumDeserializer() {
// 空构造,框架反射调用
}
@Override
@SuppressWarnings("unchecked")
public StringKeyMapEnum deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
TreeNode treeNode = p.getCodec().readTree(p);
String key = null;
if (treeNode instanceof ObjectNode) {
key = ((ObjectNode) treeNode).get("key").asText();
} else if (treeNode instanceof TextNode) {
key = ((TextNode) treeNode).asText();
} else if (treeNode instanceof ArrayNode) {
key = ((ArrayNode) treeNode).findValue("key").asText();
}
String propName = p.getCurrentName();
Class<?> parent = ctxt.getParser().getCurrentValue().getClass();
Class<? extends StringKeyMapEnum> enumClass =
(Class<? extends StringKeyMapEnum>) BeanUtils.findPropertyType(propName, new Class<?>[]{parent});
for (StringKeyMapEnum constant : enumClass.getEnumConstants()) {
if (constant.getKey().equals(key)) {
return constant;
}
}
return null;
}
}
3.6 枚举:MapEnum2
01.MP接口-使用
a.MapEnum
/**
* 审批流程撤回标志枚举
*
* 1. 移除 implements MapEnum
* 2. @EnumValue: 标记 key 为数据库存储值
* 3. @JsonFormat: 标记序列化为 JSON 对象
* 4. @JsonCreator: 标记反序列化的工厂方法
*/
@Getter
@AllArgsConstructor
@JsonFormat(shape = JsonFormat.Shape.OBJECT) // <-- Jackson: 序列化为JSON对象
public enum ApprovalToStartEnum {
NO(0, "否"),
YES(1, "是");
@EnumValue // <-- MyBatis-Plus: 标记此字段为存入数据库的值
private final int key;
// @JsonProperty 注解确保在 @JsonFormat(shape=OBJECT) 模式下,此字段能被正确序列化
@JsonProperty("value")
private final String value;
// @JsonProperty 同样可用于 getter 方法
@JsonProperty("key")
public int getKey() {
return key;
}
/**
* Jackson 反序列化工厂方法
* @JsonCreator 告诉 Jackson 使用此方法从 JSON 创建枚举实例。
* 支持多种入参格式:
* 1. 纯数字: 1
* 2. 字符串: "1"
* 3. 对象: { "key": 1, "value": "是" } (只会用到 key)
* @param key 从 JSON 中提取的 key 值
* @return 匹配的枚举实例,或 null
*/
@JsonCreator
public static ApprovalToStartEnum getByKey(Object key) {
if (key == null) {
return null;
}
// 兼容不同数字类型
int intKey;
if (key instanceof Number) {
intKey = ((Number) key).intValue();
} else {
try {
intKey = Integer.parseInt(key.toString());
} catch (NumberFormatException e) {
return null; // 或者抛出异常
}
}
return Stream.of(ApprovalToStartEnum.values())
.filter(e -> e.getKey() == intKey)
.findFirst()
.orElse(null);
}
}
b.StringKeyMapEnum
/**
* 流程节点枚举
*
* 1. @EnumValue: 标记 key (字符串) 为数据库存储值
* 2. @JsonValue: 标记 getKey() 的返回值为 JSON 序列化的值
* 3. @JsonCreator: 标记反序列化的工厂方法
*/
@Getter
@AllArgsConstructor
public enum ProcessNodeEnum {
APPLY_SUBMIT("APPLY_SUBMIT", "发起申请"),
DEPT_APPROVE("DEPT_APPROVE", "部门审批"),
HR_APPROVE("HR_APPROVE", "人事审批"),
FINAL_CONFIRM("FINAL_CONFIRM", "最终确认");
@EnumValue // <-- MyBatis-Plus: 标记此字段为存入数据库的值
private final String key;
private final String value;
/**
* Jackson 序列化方法
* @JsonValue 告诉 Jackson,当序列化此枚举时,调用此方法,并将其返回值作为 JSON 的值。
* @return 枚举的 key,将作为纯字符串输出
*/
@JsonValue
public String getKey() {
return key;
}
/**
* Jackson 反序列化工厂方法
* @param key 从 JSON 字符串中读取到的值
* @return 匹配的枚举实例,或 null
*/
@JsonCreator
public static ProcessNodeEnum getByKey(String key) {
return Stream.of(ProcessNodeEnum.values())
.filter(e -> e.getKey().equals(key))
.findFirst()
.orElse(null);
}
}
02.MP接口-取值
a.ApprovalToStartEnum (整数Key,序列化为JSON对象)
a.直接访问枚举实例
ApprovalToStartEnum yesEnum = ApprovalToStartEnum.YES;
ApprovalToStartEnum noEnum = ApprovalToStartEnum.NO;
b.从枚举实例中获取属性
ApprovalToStartEnum currentApproval = ApprovalToStartEnum.YES;
int key = currentApproval.getKey(); // --> 1
String value = currentApproval.getValue(); // --> "是"
String name = currentApproval.name(); // --> "YES"
int ordinal = currentApproval.ordinal(); // --> 1
c.通过 Key 反向查找枚举实例
ApprovalToStartEnum fromInt = ApprovalToStartEnum.getByKey(1); // --> YES
ApprovalToStartEnum fromString = ApprovalToStartEnum.getByKey("0"); // --> NO
ApprovalToStartEnum fromNull = ApprovalToStartEnum.getByKey(null); // --> null
d.遍历所有枚举实例
for (ApprovalToStartEnum status : ApprovalToStartEnum.values()) {
System.out.println(String.format("常量名: %s, Key: %d, Value: %s",
status.name(), status.getKey(), status.getValue()));
}
b.ProcessNodeEnum (字符串Key,序列化为JSON字符串)
a.直接访问枚举实例
ProcessNodeEnum node = ProcessNodeEnum.DEPT_APPROVE;
b.从枚举实例中获取属性
ProcessNodeEnum currentNode = ProcessNodeEnum.APPLY_SUBMIT;
String key = currentNode.getKey(); // --> "APPLY_SUBMIT"
String value = currentNode.getValue(); // --> "发起申请"
String name = currentNode.name(); // --> "APPLY_SUBMIT"
int ordinal = currentNode.ordinal(); // --> 0
c.通过 Key 反向查找枚举实例
ProcessNodeEnum fromKey = ProcessNodeEnum.getByKey("HR_APPROVE"); // --> HR_APPROVE
ProcessNodeEnum fromInvalidKey = ProcessNodeEnum.getByKey("INVALID_KEY"); // --> null
d.遍历所有枚举实例
for (ProcessNodeEnum node : ProcessNodeEnum.values()) {
System.out.println(String.format("Key: %s, Value: %s", node.getKey(), node.getValue()));
}
03.MP接口-实例比较
a.使用 == 运算符 (最佳实践)
a.代码
ProcessNodeEnum currentNode = ProcessNodeEnum.DEPT_APPROVE;
if (currentNode == ProcessNodeEnum.DEPT_APPROVE) {
System.out.println("当前节点是部门审批。");
}
b.优点
性能最佳:直接进行内存地址比较。
编译期类型安全:不同类型枚举无法比较。
Null安全:`null == ProcessNodeEnum.DEPT_APPROVE` 返回 `false`。
b.使用 .equals() 方法
a.代码
ProcessNodeEnum currentNode = ProcessNodeEnum.DEPT_APPROVE;
if (currentNode.equals(ProcessNodeEnum.DEPT_APPROVE)) {
System.out.println("当前节点是部门审批。");
}
b.缺点
存在 `NullPointerException` 风险。
性能稍差:方法调用有微小开销。
失去部分编译期类型安全。
c.使用 switch 语句 (推荐)
ProcessNodeEnum currentNode = ProcessNodeEnum.HR_APPROVE;
switch (currentNode) {
case APPLY_SUBMIT:
System.out.println("执行“发起申请”的后续操作...");
break;
case DEPT_APPROVE:
System.out.println("执行“部门审批”的后续操作...");
break;
case HR_APPROVE:
System.out.println("执行“人事审批”的后续操作...");
break;
case FINAL_CONFIRM:
System.out.println("流程结束!");
break;
default:
throw new IllegalStateException("未知的流程节点: " + currentNode);
}
d.与枚举的属性进行比较
int statusFromDB = 1;
String nodeKeyFromAPI = "APPLY_SUBMIT";
if (statusFromDB == ApprovalToStartEnum.YES.getKey()) {
System.out.println("数据库中的状态 1 代表“是”。");
}
if (nodeKeyFromAPI.equals(ProcessNodeEnum.APPLY_SUBMIT.getKey())) {
System.out.println("API传入的Key 'APPLY_SUBMIT' 代表“发起申请”。");
}
3.7 枚举:OnbMapEnum
01.汇总
a.区别
a.序列化:枚举 -> 变成JSON对象,放进去
OnbMapEnum 变成 {"key": 4, "value": "大专", "logicType": "enums"}
OnbStringKeyMapEnum 变成 {"key": "01", "value": "汉族", "logicType": "enums"}
b.反序列化:JSON -> 变成枚举,取出来
"key": 4 变成 OnbMapEnum
"key": "01" 变成 OnbStringKeyMapEnum
c.自定义处理器:2部分内容,放进去+取出来
1.设置非空参数时,将枚举的 key 作为整数写入 PreparedStatement。
2.从 JDBC 查询结果集 ResultSet 或 CallableStatement 中读取整数类型 key,并通过枚举常量中的 key 进行匹配返回对应枚举实例。
3.未匹配到对应枚举时返回 null
b.方案选型
a.方案A(推荐,稳妥):保留 Java 枚举常量集,DB 管理 value/排序/启用
优点:保留编译期类型安全;OnbStringKeyMapEnum 与现有 TypeHandler/Jackson 全兼容;getByKey 不变
限制:key 集合固定,不能在运行时新增新的 key
b.方案B(纯字典化):完全不用 Java enum(字段用 String 存 key;下拉清单/展示走“字典服务/缓存”)
优点:DB 可完全控制可选项的增删改
影响:实体/DTO/序列化/校验/持久化/前端/报表需联动改造,失去编译期类型安全
c.方案C(折中):保留少量强类型常量(如 OTHER),其余扩展走字典
优点:兼顾强类型分支与少量动态扩展
影响:需要清晰边界与治理
d.总结
若需要编译期类型安全与现有 TypeHandler/Jackson 行为,保留 Java 枚举更稳妥;
数据库仅管理显示文案(value)、排序、启用状态等。若需运行时新增/删减选项,应改为“纯字典化”方案(不再使用 Java enum)。
c.OnbApplicantNationEnum 的两种形态与用法
a.形态一(保留 enum,DB 覆盖 value)
代码保持当前结构与常量列表不变(key 集合固定)
启动时从 enum_config 读出 key -> value,通过反射覆盖各常量的 value 字段,并重建 VALUEMAPS
用法不变:OnbApplicantNationEnum.getByKey("01").getValue()(value 将是 DB 中维护的文本)
b.形态二(不再使用 enum,纯字典化)
删除枚举类,新增“字典访问器/服务”(如 DictService),统一提供 list(enumType)、get(enumType,key)、refresh(enumType)
实体字段改为 String key;序列化输出 {key,value} 或仅 key,由服务回填 value
MyBatis 不再需要 Enum TypeHandler;校验改为基于字典缓存的运行时校验
02.示例代码
a.ProcessNodeEnum枚举
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
/**
* -----------------------------------------------------------------------------------
* ProcessNodeEnum:流程节点枚举
* 说明:
* 1. 用于描述业务流转的多个节点,每个节点包含唯一 key、对应中文描述 value 以及 logicType。
* 2. 通过 @EnumValue 注解,将 key 作为 MyBatis-Plus 存储到数据库的值。
* 3. 通过 @JsonValue/@JsonCreator 支持 Jackson 的自定义序列化与反序列化为完整 JSON 对象。
* 4. 增加 logicType 字段,方便前端/后端业务统一识别为枚举类型。
* -----------------------------------------------------------------------------------
*/
@Getter
@AllArgsConstructor
public enum ProcessNodeEnum {
// 发起申请节点
APPLY_SUBMIT("APPLY_SUBMIT", "发起申请"),
// 部门审批节点
DEPT_APPROVE("DEPT_APPROVE", "部门审批"),
// 人事审批节点
HR_APPROVE("HR_APPROVE", "人事审批"),
// 最终确认节点
FINAL_CONFIRM("FINAL_CONFIRM", "最终确认");
/**
* key:数据库中的存储主键
* @EnumValue - MyBatis-Plus 注解,标记此字段为持久化到数据库的值
*/
@EnumValue
private final String key;
/**
* value:节点的中文描述
*/
private final String value;
/**
* logicType:逻辑标识,用于序列化到 JSON,前端、接口统一识别类型
* 该字段直接赋初值,不需参与构造
*/
private final String logicType = "enums";
/**
* Jackson 自定义序列化方法
* @JsonValue 指定序列化时使用该方法的返回值,序列化为完整对象
* 格式如:{ "key": "APPLY_SUBMIT", "value": "发起申请", "logicType": "enums" }
*/
@JsonValue
public Map<String, Object> toJson() {
Map<String, Object> map = new HashMap<>();
map.put("key", key);
map.put("value", value);
map.put("logicType", logicType);
return map;
}
/**
* Jackson 自定义反序列化工厂方法
* @JsonCreator 指定反序列化为枚举时用该静态方法
* 支持从完整 JSON 对象中(含 key/value/logicType)恢复枚举实例(主要根据 key 匹配)
* @param obj JSON 反序列化后的 Map 对象
* @return 匹配到的枚举实例;未匹配则返回 null(可根据业务需要调整为抛异常或默认值)
*/
@JsonCreator
public static ProcessNodeEnum fromJson(Map<String, Object> obj) {
if (obj == null || obj.get("key") == null) return null;
String k = obj.get("key").toString();
return Stream.of(ProcessNodeEnum.values())
.filter(e -> e.getKey().equals(k))
.findFirst()
.orElse(null);
}
}
b.ProcessNodeEnumTypeHandler自定义类型处理器
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.CallableStatement;
import java.sql.SQLException;
import java.util.stream.Stream;
/**
* -----------------------------------------------------------------------------------
* ProcessNodeEnumTypeHandler:用于数据库与 ProcessNodeEnum 枚举的双向映射
* 说明:
* 1. setNonNullParameter 用于向数据库写入(如插入、更新),仅写入枚举的 key(字符串)。
* 2. getNullableResult 用于从数据库读取 key,并将 key 转回对应的枚举实例。提供多种重载(By列名、By列索引、By存储过程)。
* 3. 当数据库中未查到匹配 key,返回 null。
* 4. 推荐在 MyBatis 配置或注解中为字段显式指定该 TypeHandler。
* -----------------------------------------------------------------------------------
*/
public class ProcessNodeEnumTypeHandler extends BaseTypeHandler<ProcessNodeEnum> {
/**
* 数据库写操作(插入和更新):将枚举类型字段的 key 写入 PreparedStatement。
* @param ps 编译好的 SQL 语句
* @param i 占位符索引
* @param parameter 当前参数(枚举实例,已保证非 null)
* @param jdbcType JDBC 类型(可选)
* @throws SQLException 写入过程异常由外抛出
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, ProcessNodeEnum parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter.getKey()); // 只写 key 字段
}
/**
* 数据库读操作:通过列名获取枚举实例
* @param rs 结果集 ResultSet
* @param columnName 列名
* @return 匹配到的枚举实例,未匹配返回 null
* @throws SQLException 数据库操作异常
*/
@Override
public ProcessNodeEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
String key = rs.getString(columnName);
return getEnumByKey(key);
}
/**
* 数据库读操作:通过列索引获取枚举实例
* @param rs 结果集 ResultSet
* @param columnIndex 列索引
* @return 匹配到的枚举实例,未匹配返回 null
* @throws SQLException 数据库操作异常
*/
@Override
public ProcessNodeEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String key = rs.getString(columnIndex);
return getEnumByKey(key);
}
/**
* 存储过程读操作:通过列索引获取枚举实例
* @param cs CallableStatement
* @param columnIndex 列索引
* @return 匹配到的枚举实例,未匹配返回 null
* @throws SQLException 数据库操作异常
*/
@Override
public ProcessNodeEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String key = cs.getString(columnIndex);
return getEnumByKey(key);
}
/**
* 根据 key 匹配枚举实例的内部工具方法
* @param key 数据库返回的 key
* @return 匹配到的枚举实例,未找到则返回 null
*/
private ProcessNodeEnum getEnumByKey(String key) {
return Stream.of(ProcessNodeEnum.values())
.filter(e -> e.getKey().equals(key))
.findFirst()
.orElse(null); // 如果没有匹配项,则返回 null(防止抛异常)
}
}
03.自定义接口-OnbMapEnum
a.注解
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
/**
* MapEnum 接口:约定枚举必须提供一个数值型键(getKey)和一个文本型值(getValue)。
* 通过 JsonSerialize 和 JsonDeserialize 注解,绑定自定义的序列化/反序列化逻辑。
*
* @param <I> 枚举键的类型,继承自 Number,通常为 Integer 或 Long。
*/
@JsonSerialize(using = OnbMapEnumSerializer.class) // 序列化:枚举 -> JSON 对象
@JsonDeserialize(using = OnbMapEnumDeserializer.class) // 反序列化:JSON -> 枚举
public interface OnbMapEnum<I extends Number> {
/**
* 获取枚举的数字键,用于数据库存储及 JSON 传输中的 key 字段。
* 示例:1、2、3 等。
*
* @return 枚举对应的数字键
*/
int getKey();
/**
* 获取枚举的显示文本,用于前端展示或日志记录。
* 示例:"初中"、"大专" 等。
*
* @return 枚举对应的文本值
*/
String getValue();
}
b.序列化
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* MapEnumSerializer 实现:
* 1. 将入参对象强制转换为 MapEnum,确保可以调用 getKey() 和 getValue()。
* 2. 创建一个 Map,填充 "key"(数字键)、"value"(文本值)、"logicType"(固定值 "enums")。
* 3. 使用 JsonGenerator.writeObject 将整个 Map 写入 JSON 输出。
*
* 序列化示例输出:
* {
* "key": 4,
* "value": "大专",
* "logicType": "enums"
* }
*/
public class OnbMapEnumSerializer extends JsonSerializer {
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
@SuppressWarnings("unchecked")
OnbMapEnum<Number> mapEnum = (OnbMapEnum<Number>) value;
Map<String, Object> out = new HashMap<>(3);
out.put("key", mapEnum.getKey());
out.put("value", mapEnum.getValue());
out.put("logicType", "enums");
gen.writeObject(out);
}
}
c.反序列化
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import java.io.IOException;
import org.springframework.beans.BeanUtils;
/**
* MapEnumDeserializer 实现:
* 1. 读取当前 JSON 属性的节点:可能是 ObjectNode、IntNode、TextNode 或 ArrayNode。
* 2. 尝试从节点中提取名为 "key" 的值,并转换成 Integer。
* 3. 通过 BeanUtils.findPropertyType 动态获取当前属性对应的枚举类类型。
* 4. 遍历该枚举类的所有常量,匹配 getKey() 与提取的 key 值,返回对应枚举实例。
* 5. 如果未匹配,则返回 null(可根据业务需求改为抛异常或返回默认枚举)。
*/
public class OnbMapEnumDeserializer extends JsonDeserializer<OnbMapEnum<Number>> {
/**
* 构造方法:可用于初始化日志或调试信息。
*/
protected OnbMapEnumDeserializer() {
// 仅示例打印,实际可移除或改为日志输出
System.out.println("初始化 MapEnumDeserializer");
}
@Override
public OnbMapEnum<Number> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// 1. 将整个 JSON 内容读取为通用树节点
TreeNode treeNode = p.getCodec().readTree(p);
Integer key = null;
// 2. 根据节点类型,提取 key 值
if (treeNode instanceof ObjectNode) {
// 对象格式:{ "key": 4, "value": "大专", "logicType": "enums" }
ObjectNode obj = (ObjectNode) treeNode;
// 尝试以整数形式读取
key = obj.get("key").asInt();
if (key == null) {
// 当 asInt() 返回 null 时,再尝试以文本形式读取并转换
key = Integer.valueOf(obj.get("key").asText());
}
} else if (treeNode instanceof IntNode) {
// 纯数字格式:4
key = ((IntNode) treeNode).asInt();
} else if (treeNode instanceof TextNode) {
// 文本数字格式:"4"
key = Integer.valueOf(((TextNode) treeNode).asText());
} else if (treeNode instanceof ArrayNode) {
// 数组格式:[ { "key": 4 } ]
ArrayNode array = (ArrayNode) treeNode;
key = array.findValue("key").asInt();
}
// 3. 获取当前 JSON 属性名及其所属对象,用于反射获取字段类型
String propName = p.getCurrentName();
Class<?> parentClass = ctxt.getParser().getCurrentValue().getClass();
@SuppressWarnings("unchecked")
// 4. 通过 BeanUtils 查找父对象中名为 propName 的字段类型,转换为 MapEnum 子类型
Class<? extends OnbMapEnum<Number>> enumClass =
(Class<? extends OnbMapEnum<Number>>) BeanUtils.findPropertyType(propName, new Class<?>[]{parentClass});
// 5. 遍历枚举常量,匹配 key
for (OnbMapEnum<Number> constant : enumClass.getEnumConstants()) {
if (constant.getKey() == key) {
return constant;
}
}
// 6. 未找到匹配时返回 null(可替换逻辑)
return null;
}
}
d.自定义类型处理器
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
/**
* OnbMapEnumHandler 是一个 MyBatis 通用的枚举类型处理器
* 适用于实现了 OnbMapEnum 接口,且用整型 key 作为映射值的枚举类型。
*
* 主要职责:
* 1. 设置非空参数时,将枚举的 key 作为整数写入 PreparedStatement。
* 2. 从 JDBC 查询结果集 ResultSet 或 CallableStatement 中读取整数类型 key,
* 并通过枚举常量中的 key 进行匹配返回对应枚举实例。
* 3. 未匹配到对应枚举时返回 null。
*
* @param <E> 继承自 Enum 且实现 OnbMapEnum 接口的枚举类型
*/
public class OnbMapEnumHandler<E extends Enum<E> & OnbMapEnum> extends BaseTypeHandler<E> {
/**
* 枚举类型的 Class 对象,用于获取所有枚举实例
*/
private final Class<E> type;
/**
* 构造器,传入枚举类型 Class
* @param type 枚举的Class类型,不能为空
* @throws IllegalArgumentException 当 type 为 null 时抛出
*/
public OnbMapEnumHandler(Class<E> type) {
if (type == null) throw new IllegalArgumentException("Type argument cannot be null!");
this.type = type;
}
/**
* 设置非空参数,将枚举的 key 值写入 PreparedStatement
* @param ps PreparedStatement
* @param i 参数索引
* @param parameter 枚举实例
* @param jdbcType JDBC类型
* @throws SQLException JDBC异常
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.getKey());
}
/**
* 根据列名从 ResultSet 获取整数 key,然后转换为对应枚举实例
* @param rs ResultSet
* @param columnName 列名
* @return 枚举实例或 null
* @throws SQLException JDBC异常
*/
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
int key = rs.getInt(columnName);
return enumByKey(key);
}
/**
* 根据列索引从 ResultSet 获取整数 key,然后转换为对应枚举实例
* @param rs ResultSet
* @param columnIndex 列索引
* @return 枚举实例或 null
* @throws SQLException JDBC异常
*/
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
int key = rs.getInt(columnIndex);
return enumByKey(key);
}
/**
* 根据列索引从 CallableStatement 获取整数 key,然后转换为对应枚举实例
* @param cs CallableStatement
* @param columnIndex 列索引
* @return 枚举实例或 null
* @throws SQLException JDBC异常
*/
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
int key = cs.getInt(columnIndex);
return enumByKey(key);
}
/**
* 辅助方法,通过整型 key 查找对应枚举实例
* @param key 枚举对应的整数 key
* @return 枚举实例或 null
*/
private E enumByKey(int key) {
for (E e : type.getEnumConstants()) {
if (e.getKey() == key) return e;
}
return null;
}
}
04.自定义接口-OnbStringKeyMapEnum
a.注解
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
/**
* StringKeyMapEnum 接口:约定枚举必须提供一个字符串键(getKey)和一个文本值(getValue)。
* 新版本序列化为完整对象格式:{"logicType": "enums", "value": "汉族", "key": "01"}
*/
@JsonSerialize(using = OnbStringKeyMapEnumSerializer.class) // 序列化:枚举 -> 完整JSON对象
@JsonDeserialize(using = OnbStringKeyMapEnumDeserializer.class) // 反序列化:多种格式 -> 枚举
public interface OnbStringKeyMapEnum {
/**
* 获取枚举在 JSON 中的字符串键。
* @return 枚举对应的字符串键
*/
String getKey();
/**
* 获取枚举的文本展示值。
* @return 枚举对应的显示文本
*/
String getValue();
}
b.序列化
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* StringKeyMapEnumSerializer 实现:
* 1. 将入参对象强制转换为 StringKeyMapEnum,确保可以调用 getKey() 和 getValue()。
* 2. 创建一个 Map,填充 "key"(字符串键)、"value"(文本值)、"logicType"(固定值 "enums")。
* 3. 使用 JsonGenerator.writeObject 将整个 Map 写入 JSON 输出。
*
* 序列化示例输出:
* {
* "key": "01",
* "value": "汉族",
* "logicType": "enums"
* }
*/
public class OnbStringKeyMapEnumSerializer extends JsonSerializer<OnbStringKeyMapEnum> {
@Override
public void serialize(OnbStringKeyMapEnum value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
Map<String, Object> result = new HashMap<>(3);
result.put("logicType", "enums");
result.put("value", value.getValue());
result.put("key", value.getKey());
gen.writeObject(result);
}
}
c.反序列化
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import java.io.IOException;
import org.springframework.beans.BeanUtils;
/**
* StringKeyMapEnumDeserializer 实现:
* 1. 读取 JSON 为 TreeNode。
* 2. 支持多种格式:
* - 完整对象:{"logicType": "enums", "value": "汉族", "key": "01"}
* - 纯文本: "01"
* - 数组: [{"key": "01"}]
* 3. 获取当前属性名与父对象类型,通过 BeanUtils 查找字段实际类型(枚举类)。
* 4. 遍历枚举常量,采用 String.equals 比对 key,并返回匹配常量。
* 5. 未匹配时返回 null(或抛异常/默认值)。
*/
public class OnbStringKeyMapEnumDeserializer extends JsonDeserializer<OnbStringKeyMapEnum> {
protected OnbStringKeyMapEnumDeserializer() {
// 空构造,框架反射调用
}
@Override
@SuppressWarnings("unchecked")
public OnbStringKeyMapEnum deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
TreeNode treeNode = p.getCodec().readTree(p);
String key = null;
if (treeNode instanceof ObjectNode) {
// 支持完整对象格式:{"logicType": "enums", "value": "汉族", "key": "01"}
ObjectNode objNode = (ObjectNode) treeNode;
if (objNode.has("key")) {
key = objNode.get("key").asText();
}
} else if (treeNode instanceof TextNode) {
// 支持纯文本格式:"01"
key = ((TextNode) treeNode).asText();
} else if (treeNode instanceof ArrayNode) {
// 支持数组格式:[{"key": "01"}]
key = ((ArrayNode) treeNode).findValue("key").asText();
}
// 如果无法提取key,返回null
if (key == null) {
return null;
}
String propName = p.getCurrentName();
Class<?> parent = ctxt.getParser().getCurrentValue().getClass();
Class<? extends OnbStringKeyMapEnum> enumClass =
(Class<? extends OnbStringKeyMapEnum>) BeanUtils.findPropertyType(propName, new Class<?>[]{parent});
// 遍历枚举常量,匹配key
for (OnbStringKeyMapEnum constant : enumClass.getEnumConstants()) {
if (constant.getKey().equals(key)) {
return constant;
}
}
return null;
}
}
d.自定义类型处理器
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
/**
* OnbStringKeyMapEnumHandler 是一个 MyBatis 通用的枚举类型处理器
* 适用于实现了 OnbStringKeyMapEnum 接口,且用字符串 key 作为映射值的枚举类型。
*
* 主要职责:
* 1. 设置非空参数时,将枚举的 key 作为字符串写入 PreparedStatement。
* 2. 从 JDBC 查询结果集 ResultSet 或 CallableStatement 中读取字符串类型 key,
* 并通过枚举常量中的 key 进行匹配返回对应枚举实例。
* 3. 未匹配到对应枚举时返回 null。
*
* @param <E> 继承自 Enum 且实现 OnbStringKeyMapEnum 接口的枚举类型
*/
public class OnbStringKeyMapEnumHandler<E extends Enum<E> & OnbStringKeyMapEnum> extends BaseTypeHandler<E> {
/**
* 枚举类型的 Class 对象,用于获取所有枚举实例
*/
private final Class<E> type;
/**
* 构造器,传入枚举类型 Class
* @param type 枚举的Class类型,不能为空
* @throws IllegalArgumentException 当 type 为 null 时抛出
*/
public OnbStringKeyMapEnumHandler(Class<E> type) {
if (type == null) throw new IllegalArgumentException("Type argument cannot be null!");
this.type = type;
}
/**
* 设置非空参数,将枚举的字符串 key 值写入 PreparedStatement
* @param ps PreparedStatement
* @param i 参数索引
* @param parameter 枚举实例
* @param jdbcType JDBC类型
* @throws SQLException JDBC异常
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter.getKey());
}
/**
* 根据列名从 ResultSet 获取字符串 key,然后转换为对应枚举实例
* @param rs ResultSet
* @param columnName 列名
* @return 枚举实例或 null
* @throws SQLException JDBC异常
*/
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
String key = rs.getString(columnName);
return enumByKey(key);
}
/**
* 根据列索引从 ResultSet 获取字符串 key,然后转换为对应枚举实例
* @param rs ResultSet
* @param columnIndex 列索引
* @return 枚举实例或 null
* @throws SQLException JDBC异常
*/
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String key = rs.getString(columnIndex);
return enumByKey(key);
}
/**
* 根据列索引从 CallableStatement 获取字符串 key,然后转换为对应枚举实例
* @param cs CallableStatement
* @param columnIndex 列索引
* @return 枚举实例或 null
* @throws SQLException JDBC异常
*/
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String key = cs.getString(columnIndex);
return enumByKey(key);
}
/**
* 辅助方法,通过字符串 key 查找对应枚举实例
* @param key 枚举对应的字符串 key
* @return 枚举实例或 null
*/
private E enumByKey(String key) {
for (E e : type.getEnumConstants()) {
if (e.getKey().equals(key)) return e;
}
return null;
}
}
05.全局自动注册实现-OnbMapEnum、OnbStringKeyMapEnum
a.说明
1.Spring 容器管理和初始化时机
该类被 @Configuration 注解标记为配置类,Spring 容器启动时会扫描并实例化它。
在实例化完成后,执行带有 @PostConstruct 注解的方法 registerEnumTypeHandlers(),确保注册逻辑在 Spring 和 MyBatis 配置都已初始化完成后运行。
-----------------------------------------------------------------------------------------------------
2.自动扫描枚举类
使用 Reflections 库对指定包(ENUM_SCAN_PACKAGE)进行扫描,找到所有实现了自定义接口 OnbMapEnum 和 OnbStringKeyMapEnum 的枚举类。
Reflections 通过反射机制遍历指定包下的所有类及其接口继承关系,无需手动维护枚举列表。
-----------------------------------------------------------------------------------------------------
3.判断并注册对应的 TypeHandler
对扫描到的每个实现接口的枚举类,先判断是否确实是枚举类型(enumClass.isEnum()),防止误注册非枚举类。
分别为两类枚举注册对应的自定义 TypeHandler 实例 OnbMapEnumHandler 和 OnbStringKeyMapEnumHandler。
注册时调用 MyBatis TypeHandlerRegistry 的 register() 方法,告知 MyBatis 对该枚举类数据库读写时应使用对应的 Handler。
-----------------------------------------------------------------------------------------------------
4.实现业务层的解耦和扩展便利
开发者只需让自定义枚举实现对应接口且放入指定包路径,无需额外配置 MyBatis xml 或代码。
程序启动时自动完成枚举与数据库类型之间的映射处理器注册,提升开发效率,避免遗漏和错误。
支持多个枚举接口,可灵活扩展更多枚举类型及对应处理器。
-----------------------------------------------------------------------------------------------------
5.简易的注册日志输出
注册完成后输出当前注册的处理器枚举数量,方便调试和确认功能生效。
b.代码
import java.util.Set;
import javax.annotation.PostConstruct;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.reflections.Reflections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
/**
* 全局自动注册实现 OnbMapEnum、OnbStringKeyMapEnum 的枚举 TypeHandler 配置
*/
@Configuration
public class OnbTypeHandlerAutoConfig {
// 你实际的包名(配置为你的根包即可,避免扫描不到自定义枚举)
private static final String ENUM_SCAN_PACKAGE = "com.jhict.product.onboard.api.enums";
@Autowired
private SqlSessionFactory sqlSessionFactory;
@PostConstruct
public void registerEnumTypeHandlers() {
TypeHandlerRegistry registry = sqlSessionFactory.getConfiguration().getTypeHandlerRegistry();
// 扫描所有 implements OnbMapEnum
Reflections reflections = new Reflections(ENUM_SCAN_PACKAGE);
Set<Class<? extends OnbMapEnum>> mapEnums = reflections.getSubTypesOf(OnbMapEnum.class);
for (Class<? extends OnbMapEnum> enumClass : mapEnums) {
// 检查是枚举类
if (enumClass.isEnum()) {
registry.register(enumClass, new OnbMapEnumHandler(enumClass));
}
}
// 扫描所有 implements OnbStringKeyMapEnum
Set<Class<? extends OnbStringKeyMapEnum>> stringEnums = reflections.getSubTypesOf(OnbStringKeyMapEnum.class);
for (Class<? extends OnbStringKeyMapEnum> enumClass : stringEnums) {
if (enumClass.isEnum()) {
registry.register(enumClass, new OnbStringKeyMapEnumHandler(enumClass));
}
}
System.out.println("=== 注册枚举TypeHandler ===");
System.out.println("注册 OnbMapEnumHandler 的枚举类型数量:" + mapEnums.size());
System.out.println("注册 OnbStringKeyMapEnumHandler 的枚举类型数量:" + stringEnums.size());
}
}
01.方式1:注解
a.import
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
b.Date
@DateTimeFormat(pattern = "yyyy-MM-dd") // 用于表单绑定
@JSONField(format = "yyyy-MM-dd") // Fastjson序列化,用于JSON处理
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") // Jackson序列化,用于JSON处理
private Date dischargeDate;
c.Date
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") // 用于表单绑定
@JSONField(format = "yyyy-MM-dd HH:mm:ss") // Fastjson序列化,用于JSON处理
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") // Jackson序列化,用于JSON处理
private Date updateTime;
02.方式2:配置DateFormatConfig
a.配置类:yml
# ===============================================================
# 自定义日期格式映射配置 (DateFormatConfig.class)
# ===============================================================
date-format:
patterns:
# 仅日期格式的字段(支持通配符)
date-only:
- birthDate
- dischargeDate
- availableDate
- fillDate
- "*Date" # 通配符:以Date结尾
- "*Birth*" # 通配符:包含Birth
- "expire*" # 通配符:以expire开头
# 日期时间格式的字段
date-time:
- createTime
- updateTime
- "*Time" # 通配符:以Time结尾
- "*DateTime" # 通配符:以DateTime结尾
- "start*" # 通配符:以start开头
- "end*" # 通配符:以end开头
formats:
date-only: "yyyy-MM-dd"
date-time: "yyyy-MM-dd HH:mm:ss"
b.配置类:注册
@SpringBootApplication
@ComponentScan("cn.myslayers.**")
@MapperScan("cn.myslayers.**.mapper")
@EnableConfigurationProperties(DateFormatConfig.class) // 注册 DateFormatConfig 配置类
public class LibraOnboardServerApplication {
public static void main(String[] args) {
SpringApplication.run(LibraOnboardServerApplication.class, args);
}
}
c.配置类:书写
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* DateFormatConfig 配置类
*
* <p>该配置类实现基于字段名匹配的日期格式动态序列化和反序列化。
* 通过 application.yml 中前缀为 "date-format" 的配置,载入字段名匹配规则 patterns
* 和格式映射 formats,实现在 JSON 序列化和反序列化过程中,根据字段名灵活切换日期格式。
*
* <p>序列化时:针对字段名匹配的日期格式,格式化 java.util.Date 为字符串。
*
* <p>反序列化时:实现 ContextualDeserializer,可以根据字段名上下文使用对应格式解析字符串为 Date;
* 解决默认全局统一格式带来的日期字符串解析失败问题。
*
* <p>该方案避免破坏 Spring Boot 的默认 Jackson 配置,将自定义序列化器和反序列化器包裹在自定义模块中,
* 由 Spring 容器自动发现并注入到全局 ObjectMapper 中,实现扩展而非替换。
*
* <p>示例配置(简化版):
* <pre>
* date-format:
* patterns:
* date-only:
* - "*Date"
* date-time:
* - "*Time"
* formats:
* date-only: "yyyy-MM-dd"
* date-time: "yyyy-MM-dd HH:mm:ss"
* </pre>
*
* <p>为什么需要在主启动类添加 {@code @EnableConfigurationProperties(DateFormatConfig.class)}
*
* <p>要让 Spring Boot 将 {@code DateFormatConfig} 中用 {@code @ConfigurationProperties(prefix = "date-format")}
* 标注的属性与 {@code application.yml}(或 {@code application.properties}) 中对应的配置绑定起来,
* 必须将该类注册为一个 Configuration Properties Bean。单纯在类上加上 {@code @Configuration}
* 并不足以触发属性绑定,其职责仅是将该类作为一个普通的配置类(含 {@code @Bean} 方法)加载到容器中。
*
* <p>1. {@code @ConfigurationProperties} 并不隐含组件扫描
* {@code @Configuration} 可以将带该注解的类纳入 Spring 容器进行管理,但不会对其中的
* {@code @ConfigurationProperties} 注解做属性值绑定处理。Spring Boot 在启动时,会扫描
* 被 {@code @EnableConfigurationProperties} 或 {@code @ConfigurationPropertiesScan}
* 显式标记的类,并将配置文件中的值注入到这些类中。
*
* <p>2. {@code @EnableConfigurationProperties} 的作用
* - 将指定的 {@code @ConfigurationProperties} 类(如 {@code DateFormatConfig})注册为 Bean
* - 触发对该 Bean 中字段的松散绑定(support for relaxed binding)
* - 自动验证(如配合 JSR-303 验证注解时生效)
*
* <p>3. 如果仅使用 {@code @Configuration} 会缺少绑定
* <pre>
* @Configuration
* @ConfigurationProperties(prefix = "date-format")
* public class DateFormatConfig { … }
* </pre>
* 以上写法会让 Spring 加载这个配置类并执行其 {@code @Bean} 方法(如 {@code customDateModule()}),
* 但对 {@code patterns}、{@code formats} 等字段不会进行值注入;这些字段仍保持默认的空 Map,
* 导致序列化/反序列化逻辑中无法拿到用户在 {@code application.yml} 中的配置。
*
* <p>4. 正确做法
* 在主启动类或任意 {@code @Configuration} 类上添加:
* <pre>
* @SpringBootApplication
* @EnableConfigurationProperties(DateFormatConfig.class)
* public class Application { … }
* </pre>
* 或者使用 Spring Boot 2.2+ 提供的批量扫描:
* <pre>
* @SpringBootApplication
* @ConfigurationPropertiesScan("cn.myslayers.product.onboard.config")
* public class Application { … }
* </pre>
*
* <p>关键结论:
* - {@code @Configuration} 负责注册 Bean;
* - {@code @EnableConfigurationProperties}(或 {@code @ConfigurationPropertiesScan})负责绑定配置。
* 二者缺一不可,才能让带 {@code @ConfigurationProperties} 的类既被托管,又能正确注入配置值。
*/
@Configuration
@ConfigurationProperties(prefix = "date-format")
@Data
public class DateFormatConfig {
/**
* 字段名匹配规则,key 为规则名称,value 为字段名或通配符列表
* 用于在序列化或反序列化时,根据字段名匹配对应格式
*/
private Map<String, List<String>> patterns = new HashMap<>();
/**
* 每个匹配规则对应的日期格式字符串,如 yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss
*/
private Map<String, String> formats = new HashMap<>();
/**
* 返回封装了自定义序列化器和反序列化器的 Jackson SimpleModule Bean。
* Spring Boot 自动检测并注册该模块到全局 ObjectMapper,实现增强功能。
*
* @return 包含自定义日期序列化和反序列化逻辑的 Jackson 模块
*/
@Bean
public SimpleModule customDateModule() {
SimpleModule module = new SimpleModule();
// 添加 Date 类型序列化器,负责根据字段名格式化日期输出
module.addSerializer(Date.class, new DateSerializer(this));
// 添加 Date 类型反序列化器,基于字段名动态匹配解析格式
module.addDeserializer(Date.class, new DateDeserializer(this));
return module;
}
/**
* 自定义日期序列化器
*
* <p>根据当前序列化字段名匹配对应日期格式,格式化日期为字符串输出。
* 使用线程安全的 SimpleDateFormat 缓存,保证多线程环境下安全。
*/
public static class DateSerializer extends JsonSerializer<Date> {
/** 配置对象,用于获取字段匹配规则及格式化字符串 */
private final DateFormatConfig config;
/** SimpleDateFormat 缓存,key 为格式字符串,value 为对应的格式化实例 */
private final Map<String, SimpleDateFormat> formatterCache = new ConcurrentHashMap<>();
/**
* 构造方法,注入配置实例
*
* @param config DateFormatConfig 实例,用于读取匹配配置
*/
public DateSerializer(DateFormatConfig config) {
this.config = config;
}
/**
* 序列化实现
*
* @param date java.util.Date 日期对象
* @param gen JsonGenerator,用于写出序列化内容
* @param serializers 序列化上下文(未使用)
* @throws IOException IO异常
*/
@Override
public void serialize(Date date, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (date == null) {
gen.writeNull();
return;
}
// 获取当前序列化的 JSON 字段名
String fieldName = gen.getOutputContext().getCurrentName();
// 根据字段名匹配合适的日期格式
String pattern = determinePattern(fieldName);
// 从缓存获取对应的 SimpleDateFormat 实例,若不存在则创建并缓存
SimpleDateFormat sdf = formatterCache.computeIfAbsent(pattern, p -> {
SimpleDateFormat format = new SimpleDateFormat(p);
format.setTimeZone(TimeZone.getTimeZone("GMT+8"));
return format;
});
// SimpleDateFormat 线程不安全,序列化时使用同步保证安全
synchronized (sdf) {
gen.writeString(sdf.format(date));
}
}
/**
* 根据字段名在配置的 patterns 中匹配对应格式
*
* @param fieldName JSON 字段名,可能为 null
* @return 匹配的日期格式字符串(若匹配失败,默认返回 date-time 格式)
*/
private String determinePattern(String fieldName) {
if (fieldName == null) {
return config.formats.getOrDefault("date-time", "yyyy-MM-dd HH:mm:ss");
}
// 优先寻找 date-only 模式匹配
for (String p : config.patterns.getOrDefault("date-only", Collections.emptyList())) {
if (matchPattern(fieldName, p)) {
return config.formats.getOrDefault("date-only", "yyyy-MM-dd");
}
}
// 再寻找 date-time 模式匹配
for (String p : config.patterns.getOrDefault("date-time", Collections.emptyList())) {
if (matchPattern(fieldName, p)) {
return config.formats.getOrDefault("date-time", "yyyy-MM-dd HH:mm:ss");
}
}
// 默认格式返回
return config.formats.getOrDefault("date-time", "yyyy-MM-dd HH:mm:ss");
}
/**
* 字段名与模式简单通配符匹配,支持 * 替代任意字符
*
* @param fieldName 字段名
* @param pattern 匹配模式,可包含 *
* @return 是否匹配
*/
private boolean matchPattern(String fieldName, String pattern) {
if (pattern.contains("*")) {
// 转换 * 为正则表达式 ".*"
String regex = pattern.replace("*", ".*");
return fieldName.matches(regex);
}
return fieldName.equals(pattern);
}
}
/**
* 自定义日期反序列化器,支持上下文感知,可根据 JSON 字段名动态选择解析日期格式。
*
* <p>通过实现 Jackson 的 ContextualDeserializer 接口:
* 反序列化过程中可获得当前字段信息,基于此选择对应的日期格式解析字符串。
*
* <p>线程安全的 SimpleDateFormat 缓存实现,确保多线程环境安全。
*/
public static class DateDeserializer extends JsonDeserializer<Date> implements ContextualDeserializer {
/** 配置对象,用于获取匹配规则 */
private final DateFormatConfig config;
/** 当前字段对应的日期格式字符串 */
private String pattern;
/** SimpleDateFormat 缓存 */
private final Map<String, SimpleDateFormat> formatterCache = new ConcurrentHashMap<>();
/** 无参构造,默认未绑定配置和格式 */
public DateDeserializer() {
this.config = null;
this.pattern = null;
}
/**
* 构造函数,绑定配置但未指定具体格式
*/
public DateDeserializer(DateFormatConfig config) {
this.config = config;
this.pattern = null;
}
/**
* 构造函数,绑定配置和具体格式,用于上下文创建返回具体格式的实例
*
* @param config 配置对象
* @param pattern 具体日期格式字符串
*/
public DateDeserializer(DateFormatConfig config, String pattern) {
this.config = config;
this.pattern = pattern;
}
/**
* 反序列化实现
*
* @param p JsonParser
* @param ctxt 反序列化上下文
* @return 解析后的 Date 对象
* @throws IOException 解析异常抛出
* @throws JsonProcessingException JSON 处理异常
*/
@Override
public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String text = p.getText();
if (text == null || text.trim().isEmpty()) {
return null;
}
// 使用上下文中匹配的格式,默认回退全局 date-time 格式
String parsePattern = pattern != null ? pattern : config.formats.getOrDefault("date-time", "yyyy-MM-dd HH:mm:ss");
SimpleDateFormat sdf = formatterCache.computeIfAbsent(parsePattern, ptn -> {
SimpleDateFormat format = new SimpleDateFormat(ptn);
format.setTimeZone(TimeZone.getTimeZone("GMT+8"));
return format;
});
try {
synchronized (sdf) {
return sdf.parse(text);
}
} catch (ParseException e) {
throw new IOException("Failed to parse Date value '" + text + "' with pattern '" + parsePattern + "'", e);
}
}
/**
* ContextualDeserializer 回调,基于 BeanProperty 获得当前 JSON 字段信息,
* 然后匹配规则,返回新的带有具体格式的 DateDeserializer 实例。
*
* @param ctxt 反序列化上下文
* @param property 当前字段属性信息
* @return 具体字段匹配的 DateDeserializer 实例
* @throws JsonMappingException 反序列化映射异常
*/
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
if (property != null && config != null) {
String fieldName = property.getName();
String matchedPattern = determinePattern(fieldName);
return new DateDeserializer(config, matchedPattern);
}
return this;
}
/**
* 同序列化器匹配逻辑,优先匹配 date-only,再匹配 date-time,未匹配使用默认。
*
* @param fieldName 当前字段名
* @return 日期格式字符串
*/
private String determinePattern(String fieldName) {
if (fieldName == null) {
return config.formats.getOrDefault("date-time", "yyyy-MM-dd HH:mm:ss");
}
for (String pattern : config.patterns.getOrDefault("date-only", Collections.emptyList())) {
if (matchPattern(fieldName, pattern)) {
return config.formats.getOrDefault("date-only", "yyyy-MM-dd");
}
}
for (String pattern : config.patterns.getOrDefault("date-time", Collections.emptyList())) {
if (matchPattern(fieldName, pattern)) {
return config.formats.getOrDefault("date-time", "yyyy-MM-dd HH:mm:ss");
}
}
return config.formats.getOrDefault("date-time", "yyyy-MM-dd HH:mm:ss");
}
/**
* 简单通配符匹配,支持 "*" 替代任意字符序列。
*
* @param fieldName 字段名
* @param pattern 模式,可能包含 *
* @return 是否匹配
*/
private boolean matchPattern(String fieldName, String pattern) {
if (pattern.contains("*")) {
String regex = pattern.replace("*", ".*");
return fieldName.matches(regex);
}
return fieldName.equals(pattern);
}
}
}
4 前端管理
4.1 ref
$
this 作用域
4.2 引入
引入方式
import tab from './tab';
import { VueInstanceAuth } from './auth';
import cache from './cache';
import { VueInstanceDownload } from './download';
import { App } from 'vue';
import { VueInstanceModal } from './modal';
4.3 mixins
src/mixins
https://zhuanlan.zhihu.com/p/482735975
BaseMixins.ts
drawMixin.js
ListMixins.js
ListModalMixins.js
OldBaseMixins.ts
Standard.js
TableMixins.js
TreeListMixins.js
TreeListModalMixins.js
4.4 plugins
src/plugins
// 页签操作
app.config.globalProperties.$tab = tab;
// 认证对象
app.config.globalProperties.$auth = new VueInstanceAuth();
// 缓存对象
app.config.globalProperties.$cache = cache;
// 模态框对象
const modal = new VueInstanceModal();
app.config.globalProperties.$modal = modal;
// 下载文件
app.config.globalProperties.$download = new VueInstanceDownload();
app.config.globalProperties.file_viewer_url = 'http://172.18.248.205:8012/onlinePreview';
app.config.globalProperties.msgSuccess = modal.msgSuccess;
app.config.globalProperties.msgWarning = modal.msgWarning;
app.config.globalProperties.msgError = modal.msgError;
// 新增常用的方法 全局变量
app.config.globalProperties.getAction = getAction;
app.config.globalProperties.postAction = postAction;
app.config.globalProperties.deleteAction = deleteAction;
app.config.globalProperties.putAction = putAction;
app.config.globalProperties.validateNull = validateNull;
4.5 字典配置
this.getDictListData(["meterRegion"]);
getDictListData (strSn) { src/mixins/ListModalMixins.js:24
ListModalMixins.js created () { },
ListMixins.js
this.$nextTick(() => {
if (!this.$refs.modalForm) {
// this.proxy.$modal.msgWarning('请添加modalForm表单!')
// return
}
});
if (this.autoLoadList && this.url && !this.isFangKeCar) {
this.loadData();
}
if (this.autoLoadDict) {
//初始化字典配置 在自己页面定义
this.initDictConfig();
}
4.6 父子组件
src/views/performance/meteringManagement
mixins: [ListModalMixins, ListMixins],
components: {
treeMultiple,
personSelect,
},