文章目录
-
- 一、背景说明
- 二、集成过程
-
- [2.1 引入 maven 依赖](#2.1 引入 maven 依赖)
- [2.2 增加属性配置](#2.2 增加属性配置)
- [2.3 自动配置类](#2.3 自动配置类)
- 三、验证集成
-
- [3.1 控制器](#3.1 控制器)
- [3.2 服务类](#3.2 服务类)
- [3.3 Mapper接口类](#3.3 Mapper接口类)
- [3.4 实体类](#3.4 实体类)
- [3.4 不要忘记XML文件](#3.4 不要忘记XML文件)
- [3.5 发起请求](#3.5 发起请求)
- 四、技巧拓展
-
- [4.1 如何打印sql语句?](#4.1 如何打印sql语句?)
- [4.2 如何对参数增加非空验证?](#4.2 如何对参数增加非空验证?)
- [4.3 如何查看 Maven 依赖?](#4.3 如何查看 Maven 依赖?)
- [4.3 如何解决时间格式问题?](#4.3 如何解决时间格式问题?)
- [4.4 如何快速增加 `serialVersionUID` 属性?](#4.4 如何快速增加
serialVersionUID
属性?)
- [五、总 结](#五、总 结)
一、背景说明
大部分的项目都需要进行数据的持久化,所以必然会使用到数据库。而关系型数据库是其中比较常见的数据库类型。目前使用比较多的关系型数据库有:MySQL、PostgreSQL和Oracle等。
在古早的应用开发中,需要开发人员写许多的DAL(数据访问层)代码,需要自己管理数据库的连接与关闭,还需要自己从ResultSet中获取数据,然后再将其组装为对象。在其中会编写大量非业务代码,并且这些代码往往充满了重复。
ORM的出现解决了前面提到的一系列问题,ORM的全称是 Object Relational Mapping
,翻译为对象关系模型
。这里所说的对象
是指业务领域的对象,关系
则指的是关系数据库(具体而言就是数据表和字段)。
ORM可以实现自动将数据库中的表字段映射为对象的属性,其采用的方式是:使用映射元数据
来描述对象关系的映射。ORM充当了应用程序的业务逻辑层和数据库之间的桥梁。常见的ORM中间件有:Hibernate和ibatis(目前已更名为 MyBatis
)。
本篇所提及的 MyBatis-Plus
对 MyBatis
的功能进行了增强。
二、集成过程
在Spring Boot项目中集成 MyBatis-Plus 过程比较简单,大概分为三个步骤:
- 引入maven依赖
- 增加属性配置
- 增加相关的自动配置类
如果不知道如何创建Spring Boot项目,可以参考以往的文章:
2.1 引入 maven 依赖
为了使用 MyBatis-Plus,需要添加相关依赖:
xml
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybaits-plus.version}</version>
</dependency>
同时因为需要数据库进行数据的存储,所以还需要添加下面的依赖(我们选择的MySQL数据库):
xml
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
如果仅限于集成 MyBatis-Plus,这两个依赖就已经足够了。
2.2 增加属性配置
相关的属性配置到 application.yml
文件中:
yaml
spring:
application:
name: "demo-api"
datasource:
# MySql 8.0以上版本
driver-class-name: com.mysql.cj.jdbc.Driver
# 兼容以前的配置
jdbc-url: jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&useTimezone=true&serverTimezone=GMT%2B8&allowMultiQueries=true
url: ${spring.datasource.jdbc-url}
username: your_username
password: your_password
2.3 自动配置类
前面我们已经提过:ORM使用映射元数据
来描述对象关系的映射。所以我们需要对相关信息进行配置,比如:
- 使用的是什么数据库
- 数据库连接是什么
- Mapper 接口文件在哪里
- MyBatis 所依赖的XML文件到哪里去找
- MyBatis 的 SqlSessionFactory 如何创建(依赖数据源、MyBatis插件)
Spring Boot 中的配置一般以 @Configuration 进行标识。相关代码如下:
java
@Configuration
@MapperScan(basePackages = "com.xhm.demo.api.mapper")
public class DataSourceConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
//注册乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource, MybatisPlusInterceptor interceptor) throws Exception {
MybatisSqlSessionFactoryBean ssfb = new MybatisSqlSessionFactoryBean();
ssfb.setDataSource(dataSource);
ssfb.setPlugins(interceptor);
//到哪里找xml文件
ssfb.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:/mapper/*Mapper.xml"));
return ssfb.getObject();
}
}
配置类的 @MapperScan(basePackages = "com.xhm.demo.api.mapper")
指定了 Mapper 接口的存放位置。
第一个 Bean 用于创建数据源,会根据相关依赖自动判断数据库类型。@ConfigurationProperties(prefix = "spring.datasource")
会将以 spring.datasource
开头的属性配置赋值给数据源对象的对应属性。
第二个 Bean 用于注册 MyBatis-Plus 的相关插件(这里暂时只注册了 分页插件和 乐观锁插件)
第三个 Bean 用于创建 SqlSessionFactory
的实例。 SqlSessionFactory
是一个接口,负责数据库Session(会话)的管理,数据库会话通俗点讲就是客户端与数据库的单次连接。其依赖下面的信息:
- 数据源
- MyBatis-Plus 插件
- xml文件的存放位置(资源路径)
三、验证集成
下面是常见的三层架构的目录结构:
shell
api
├── controller
├── dto
├── entity
├── mapper
└── service
其中最主要的目录自上而下依次是:
- controller :控制器目录(表示层)
- service:服务类目录(业务逻辑层)
- mapper:DAO目录(数据访问层)
另外两个目录的作用:
- entity:实体类目录
- dto:数据传输对象(DTO)目录
创建对应的表:
sql
create table t_card_puncher
(
id int(10) not null auto_increment,
name varchar(15) comment '打卡人姓名',
nick varchar(50) comment '打卡人昵称',
avatar varchar(100) comment '头像',
status smallint(2) default 10 comment '打卡人状态(10-已保存;20-已启用;0-已作废)',
account_id int(10) comment '对应账号id',
remark varchar(500) comment '备注',
added_by int(10) comment '新增人id',
added_by_name varchar(20) comment '新增人姓名',
added_time datetime default CURRENT_TIMESTAMP comment '新增时间',
last_modified_by int(10) comment '最后修改人id',
last_modified_by_name varchar(20) comment '最后修改人姓名',
last_modified_time datetime default CURRENT_TIMESTAMP comment '最后修改时间',
last_modified_ip varchar(50) comment '最后修改IP',
valid smallint(1) default 1 comment '是否有效(1-有效;0-无效)',
primary key (id)
);
alter table t_card_puncher comment '习惯打卡人表';
依次在上面的目录中创建相关类,实现一个简单的接口。我们将按照从上而下的顺序创建相关的类。
3.1 控制器
代码清单3.1-1:
java
// CardPuncherController.java
@RestController
@RequestMapping("/cardPuncher")
public class CardPuncherController {
@Resource
private CardPuncherService service;
@PostMapping("/queryById")
public BaseResponse<CardPuncher> queryById(@RequestBody @Valid IdRequest request){
CardPuncher dto = service.queryCardPuncherById(request.getId());
return BaseResponse.ok(dto);
}
}
控制器类依赖下面三个类:
- IdRequest (DTO)
- CardPuncher (实体类)
- CardPuncherService (服务类--接口)
代码清单3.1-2:
java
// IdRequest.java
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.io.Serializable;
import javax.validation.constraints.NotNull;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class IdRequest implements Serializable {
private static final long serialVersionUID = 4111263664475615283L;
/**
* id
*/
@NotNull(message = "ID不能为空!")
private Integer id;
}
3.2 服务类
控制器一般不含任何的业务逻辑,它只是将请求委托给相关的服务类。并且为了保持依赖的松散,控制器是不直接依赖于具体类的,而是依赖于接口。
代码清单3.2-1:
java
//打卡人服务类 :CardPuncherService.java
import com.baomidou.mybatisplus.extension.service.IService;
import com.xhm.demo.api.entity.CardPuncher;
public interface CardPuncherService extends IService<CardPuncher> {
/**
* 根据id查询打卡人
*
* @param id 打卡人id
* @return 打卡人DTO
*/
CardPuncher queryCardPuncherById(Integer id);
}
代码清单3.2-2,就是接口的对应实现:
java
// 打卡人服务实现类:CardPuncherServiceImpl.java
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xhm.demo.api.entity.CardPuncher;
import com.xhm.demo.api.enums.ValidEnum;
import com.xhm.demo.api.mapper.CardPuncherMapper;
import com.xhm.demo.api.service.CardPuncherService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class CardPuncherServiceImpl extends ServiceImpl<CardPuncherMapper, CardPuncher> implements CardPuncherService {
@Override
public CardPuncher queryCardPuncherById(Integer id) {
LambdaQueryWrapper<CardPuncher> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.eq(CardPuncher::getValid, ValidEnum.VALID.getCode());
queryWrapper.eq(CardPuncher::getId, id);
return this.getOne(queryWrapper);
}
}
服务实现类依赖下面的类:
- ServiceImpl(MyBatis-Plus内置的服务实现类)
- CardPuncherService (服务接口类)
- CardPuncherMapper(Mapper接口)
- CardPuncher
- Wrappers (条件构造器)
3.3 Mapper接口类
Mapper接口类比较简单,因为我们没有自定义脚本,而是调用了 ServiceImpl.getOne
方法。所以 Mapper 接口类中是没有自己的方法的。
代码清单3.3-1:
java
// 打卡人 Mapper 接口类:CardPuncherMapper.java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface CardPuncherMapper extends BaseMapper<CardPuncher> {
}
3.4 实体类
实体类和数据表的字段是一一对应的,所以其是一个比较重要的类。相关代码如下:
java
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("t_card_puncher")
public class CardPuncher extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 打卡人姓名
*/
private String name;
}
其中需要重点强调的是:
当数据库中table的名称和实体类名称不一致时,一定需要使用注解
@TableName
进行显示声明表的名称。否则会提示相关的表不存在(例子中默认认为表名是
card_puncher
,而我们的表是有前缀t_
的)
其中的注解 @Accessors(chain = true)
的作用如下:
被该注解修饰的类,setters方法返回的该类的实例(即this),而不是void。所以可以链式地调用setters方法。
链式调用代码如下:
java
CardPuncher entity = new CardPuncher().setName("老书生");
上述类之间的关系如下图所示:
3.4 不要忘记XML文件
在讲自动配置类时,我们提到过需要指定xml文件的存放位置:
"classpath:/mapper/*Mapper.xml"
那么我们必须在mapper中创建对应的xml文件,否则就会报如下错误:
shell
org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'cardPuncherController'
: Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException
: Error creating bean with name 'cardPuncherServiceImpl'
: Unsatisfied dependency expressed through field 'baseMapper'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException
: Error creating bean with name 'cardPuncherMapper' defined in file
...
nested exception is java.io.FileNotFoundException: class path resource [mapper/] cannot be resolved to URL because it does not exist
主要错误信息:
java.io.FileNotFoundException: class path resource [mapper/] cannot be resolved to URL because it does not exist
XML文件(CardPuncherMapper.xml)内容如下:
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xhm.demo.api.mapper.CardPuncherMapper">
</mapper>
启动Spring Boot 项目,观察控制台,发现多了MyBatis的banner。并且没有任何报错信息。
移步到端点,可以发现多一个db项:
表明项目中使用了数据库,并且数据库的类型是MySQL。
3.5 发起请求
使用 Postman 发起接口请求。
因为此时表中还没有数据,所以结果如下:
插入1条数据:
sql
insert into my_database.t_card_puncher(`name`,nick,added_by,added_by_name,last_modified_by,last_modified_by_name)
values('老书生','leon',-1,'系统',-1,'系统');
再次请求,结果如下:
可以成功查出数据。到这里 MyBatis 的集成已经初步成功了。
之所以说初步成功,是因为还有一些问题需要解决。如上图中时间格式的问题,2024-06-21T09:31:00
这种时间格式有自己的专有名词:ISO日期时间格式
/ ISO 8601
。
还有一个问题:当Id为空时,没有返回期望的错误信息,而是报400错误:
四、技巧拓展
4.1 如何打印sql语句?
发送请求后,在项目的控制台没有出现期望的sql语句,那么该如何处理才能成功打印sql语句呢?
其实方法也很简单,在 application.yml
中添加如下配置:
yaml
# 查看sql
logging:
level:
com.xhm.demo.api.mapper: debug
其中的 com.xhm.demo.api.mapper
是 Mapper 接口所在的包名。设置其日志级别为debug
打印的sql脚本如下:
shell
2024-06-23 00:26:49.147 DEBUG 40300 --- [nio-8080-exec-6] c.x.d.a.m.CardPuncherMapper.selectOne : ==> Preparing: SELECT id,name,added_by,added_by_name,added_time,last_modified_by,last_modified_by_name,last_modified_time,last_modified_ip,valid FROM t_card_puncher WHERE (valid = ? AND id = ?)
2024-06-23 00:26:49.188 DEBUG 40300 --- [nio-8080-exec-6] c.x.d.a.m.CardPuncherMapper.selectOne : ==> Parameters: 1(Integer), 1(Integer)
2024-06-23 00:26:49.226 DEBUG 40300 --- [nio-8080-exec-6] c.x.d.a.m.CardPuncherMapper.selectOne : <== Total: 1
脚本是出现了,但是似乎不那么完美。如果查询条件比较多,拼接起来还是比较麻烦的。有无其他办法能够查看拼接好的脚本呢?
可以试试一款IDEA插件:MyBatis Log Free
借助这款插件,可以查看完整的sql语句:
如果是第一次使用该插件,可以通过如下方式打开:
【Tools】-> 【MyBatis Log Plugin】
4.2 如何对参数增加非空验证?
非空校验不生效,有两种情况:
- 空条件直接传到sql中
- 接口返回400错误,控制台有
WARN
日志
第一种情况
如果 @NotNull注解在请求类的属性 (如IdRequest类)上。检查Controller的方法的入参是否遗漏 @Valid
注解。
如果已经增加了注解,非空校验依然没有生效,则有可能是maven依赖不完整,可能 pom 中只增加了如下依赖:
xml
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
有两种解决方法:
- 增加
hibernate-validator
依赖
validation-api
中只定义了相关注解,具体的校验逻辑在hibernate-validator
中。所以需要添加如下依赖:
xml
<!-- hibernate-validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
- 去除
validation-api
依赖,以下面的依赖进行替换:
xml
<!--参数校验-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
第二种情况
因为没有对 MethodArgumentNotValidException
异常进行处理而导致校验信息没有返回。具体的现象是响应报文的结果如下:
所以需要增加 统一异常处理类 。相关代码如下:
java
// exception/CommonExceptionHandler.java
@Slf4j
@ControllerAdvice
public class CommonExceptionHandler {
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseBody
public BaseResponse<?> bindMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
String traceId = MDC.get("traceId");
StringBuilder errorMessage = new StringBuilder();
List<FieldError> errors = ex.getBindingResult().getFieldErrors();
for (FieldError error : errors) {
//只取错误信息
errorMessage.append(error.getDefaultMessage()).append(";");
}
errorMessage.deleteCharAt(errorMessage.length() - 1);
log.error(String.format("traceId: %s, Exception: [%s] %s, request error, parameters invalid:%s", traceId, ex.getParameter().getParameterName(), ex, errorMessage));
return BaseResponse.error(BaseResultCodeEnum.ERROR.getCode(), errorMessage.toString());
}
}
BaseResponse.java代码如下:
java
@Data
public class BaseResponse<T> implements Serializable {
private static final long serialVersionUID = -5330932746124338859L;
/**
* 响应状态码
*/
private String code;
/**
* 响应消息
*/
private String message;
/**
* 响应数据
*/
private T data;
public static <K> BaseResponse<K> ok() {
return BaseResponse.result(BaseResultCodeEnum.SUCCESS.getCode(), BaseResultCodeEnum.SUCCESS.getMessage());
}
public static <K> BaseResponse<K> ok(K data) {
BaseResponse<K> response = BaseResponse.ok();
response.setData(data);
return response;
}
public static <K> BaseResponse<K> error(String message) {
return BaseResponse.result(BaseResultCodeEnum.ERROR.getCode(), message);
}
public static <K> BaseResponse<K> error(String code, String message) {
return BaseResponse.result(code, message);
}
public static <K> BaseResponse<K> result(String code, String message) {
BaseResponse<K> response = new BaseResponse<>();
response.setCode(code);
response.setMessage(message);
return response;
}
}
4.3 如何查看 Maven 依赖?
有两种查看 Maven 依赖的方式:
- mvn dependency:tree命令
- 使用Idea的
Dependency Analyzer
其中第二种方式依赖 IDEA的插件: Maven Helper
。
第一种方式的演示:
在终端输入如下命令:
shell
mvn dependency:tree
输出结果如下:
shell
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building demo-api 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:3.1.2:tree (default-cli) @ demo-api ---
[INFO] com.xhm:demo-api:jar:0.0.1-SNAPSHOT
...
[INFO] +- org.projectlombok:lombok:jar:1.18.20:compile (optional)
...
[INFO] +- mysql:mysql-connector-java:jar:8.0.23:runtime
[INFO] \- org.springframework.boot:spring-boot-starter-validation:jar:2.3.10.RELEASE:compile
[INFO] +- org.glassfish:jakarta.el:jar:3.0.3:compile
[INFO] \- org.hibernate.validator:hibernate-validator:jar:6.1.7.Final:compile
[INFO] +- jakarta.validation:jakarta.validation-api:jar:2.0.2:compile
[INFO] +- org.jboss.logging:jboss-logging:jar:3.4.1.Final:compile
[INFO] \- com.fasterxml:classmate:jar:1.5.1:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.018 s
[INFO] Finished at: 2024-06-25T16:12:36+08:00
[INFO] Final Memory: 26M/304M
[INFO] ------------------------------------------------------------------------
第二种方式的演示:
打开pom文件,从 Text
模式切换到 Dependency Analyzer
模式:
- 选择
All Dependencies as Tree
,点击 "Refresh UI" 按钮- 收缩树形结构(全部)
- 展开
spring-boot-starter-validation
,可以看到其依赖项。如图所示,其依赖于
hibernate-validator
,而hibernate-validator
又依赖于jakarta.validation-api
(替换validation-api
包含注解javax.validation.constraints.NotNull
)
4.3 如何解决时间格式问题?
前面我提到过接口返回的日期时间格式默认是:ISO日期时间格式
。而很多时候我们需要指定的日期时间格式。如:
- 当提到出生日期是,时间精度至少需要精确到天,在中国一般的日期格式是:
2024-06-20
- 但是当我们需要获知订单的创建时间时,可能需要这样的时间格式:
2024-06-21 08:30:50
要解决这个问题,其实也比较简单,可以使用 jackson-annotation
包中的注解 @JsonFormat
对日期时间格式进行自定义。
下面使用该注解对 BaseEntity
进行功能增强:
java
@Data
public class BaseEntity implements Serializable {
...
/**
* 新增时间
*/
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDateTime addedTime;
...
/**
* 最后修改时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime lastModifiedTime;
}
这里只是为了演示的方便,生产代码不建议这样写:
直接将实体类暴露给最终用户 不是一个好的实践,因为一旦字段发生变动,就需要客户代码进行相应的调整。
普遍的做法是:引入DTO作为接口的数据载体。所以
@JsonFormat
注解也是打在DTO类上。
4.4 如何快速增加 serialVersionUID
属性?
对于可序列化的类,一般强烈建议显式声明 serialVersionUID 值。因为默认计算得到的 serialVersionUID 值可能会引发 InvalidClassException
。那么有没有什么快速的方法去完成这件事情呢?
如果你使用的是 IDEA ,可以参考下面的方法:
- 打开 Settings 窗口,输入搜索词:
serial
。- 在【Editor】-【Inspections】中找到:【Java】- 【Serializatioin issues】。
- 然后勾选其中的
Serializable class without serialVersionUID
。
上面设置的作用是:如果一个类实现了Serializable接口,但是没有声明 serialVersionUID 值,就会告警。
F2 定位到告警信息后,再组合按键 Alt + Enter ,弹出上图的信息后,再按一次 Enter 后,就可以快速插入 serialVersionUID
值了。使用该方式生成的 serialVersionUID
能很好地避免重复的问题。
五、总 结
本文详细讲述了如何在Spring Boot 项目中集成 MyBatis-Plus,其中的设置和依赖都遵循最小功能集的原则。
增加集成相关的配置和依赖后,我们又创建了一个简单的接口,用于验证本次集成是否成功。其中谈到了SSM项目中基本的项目结构,相关代码都是从生产代码中抽象出来的,具有很强的参考意义。文中还以类图的形式给出了各个类之间的关系,可以帮助读者更好地理解代码结构。
接口测试时,我们使用了post工具,Postman是其中比较常见的一款。通过接口可以成功返回数据,但是数据的格式有一些问题。文章对这些问题的解决进行了细致地阐述。
最后还分享了实际开发中可能会用到的一些小技巧,希望可以启动抛砖引玉的作用。有交流意愿的小伙伴可以在评论区进行留言。
参考资料: