SpringBoot集成Mybatis项目实操
本文为《从零打造项目》系列第三篇文章,首发于个人网站。
《从零打造项目》系列文章
比MyBatis Generator更强大的代码生成器
SpringBoot项目基础设施搭建
SpringBoot集成Mybatis项目实操
前言
基于 orm-generate 项目可以实现项目模板代码,集成了三种 ORM 方式:Mybatis、Mybatis-Plus 和 Spring JPA,JPA 是刚集成进来的,该项目去年就已经发布过一版,也成功实现了想要的功能,关于功能介绍可以参考我之前的这篇文章。
如何搭建一个项目是每个开发人员会面临的问题。新人一般很难参与项目基础框架搭建,只会在项目基础框架布置完毕后才加入进来,接着进行功能模块代码开发,无法体验项目搭建的过程,其中包括一些通用代码。等到换个项目,可能还是重复之前的功能开发,而且每个项目基础框架可能不太一样,但我们能做的只是按照现有规定进行开发,对于一个项目的基础设施了解不多,也无法明白为何要选某某框架。为了突破,为了成长,新人有必要跟着参与一次项目基础框架搭建,切身体会各种框架的差异,参与一些通用代码的开发,而不仅仅是 CRUD。
上面提到的项目基础架构,比如说选择 SpringBoot 或者 SpringMVC,再比如流行的三种 ORM 框架:Mybatis、Mybatis-Plus 和 Spring JPA,这里我们暂时不关注 SpringCloud 框架,因为每个微服务还是基于 SpringBoot,至于其他各种中间件,暂时也不做考虑(我的视角暂时无法达到那样的高度)。
运行 orm-generate 项目可以从现有的数据库中获取模版代码,大致包含如下几部分内容:
- entity, 实体层,用于存放我们的实体类,与数据库中的属性值基本保持一致,实现set和get的方法 ;
- mapper/dao/repository,对数据库进行数据持久化操作,它的方法语句是直接针对数据库操作的,主要实现一些增删改查操作,在 mybatis 中方法主要与与 xxx.xml 内相互一一映射;
- service,业务 service 层,给 controller 层的类提供接口进行调用。一般就是自己写的方法封装起来,就是声明一下,具体实现在 serviceImpl 中;
- controller,控制层,负责具体模块的业务流程控制,需要调用 service 逻辑设计层的接口来控制业务流程。因为 service 中的方法是我们使用到的,controller 通过接收前端 H5 或者 App 传过来的参数进行业务操作,再将处理结果返回到前端。
- dto文件,用来分担实体类的功效,可以将查询条件单独封装一个类,以及前后端交互的实体类(有时候我们可能会传入 entity 实体类中不存在的字段);
- vo文件,后台返回给前台的数据结构,同样可以自定义字段。
- struct文件,dto、entity与vo文件相互转换。
有了这些模版代码,我们还需要构建项目基础架构,用来存放这些代码,并对其进行修改,最终实现功能开发。
本文将实现 SpringBoot+Mybatis 的项目搭建,除此之外,还有一些通用代码配置:
- 统一返回格式
- 全局异常处理
- Mybatis 操作工具类
- 字符串等工具类
- 请求日志记录
不说废话了,我们直接进入主题。
数据库
本项目采用的是 MySQL 数据库,版本为 8.x,建表语句如下:
CREATE TABLE `user` (`id` varchar(36) NOT NULL,`name` varchar(20) DEFAULT NULL,`age` int(11) DEFAULT NULL,`address` varchar(100) DEFAULT NULL,`created_date` timestamp NULL DEFAULT NULL,`last_modified_date` timestamp NULL DEFAULT NULL,`del_flag` tinyint(1) NOT NULL DEFAULT '0',`create_user_code` varchar(36) DEFAULT NULL,`create_user_name` varchar(50) DEFAULT NULL,`last_modified_code` varchar(36) DEFAULT NULL,`last_modified_name` varchar(50) DEFAULT NULL,`version` int(11) NOT NULL DEFAULT '1',PRIMARY KEY (`id`),UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户';CREATE TABLE `job` (`id` varchar(36) NOT NULL,`name` varchar(20) DEFAULT NULL,`user_id` varchar(36) NOT NULL,`address` varchar(100) DEFAULT NULL,`created_date` timestamp NULL DEFAULT NULL,`last_modified_date` timestamp NULL DEFAULT NULL,`del_flag` tinyint(1) NOT NULL DEFAULT '0',`create_user_code` varchar(36) DEFAULT NULL,`create_user_name` varchar(50) DEFAULT NULL,`last_modified_code` varchar(36) DEFAULT NULL,`last_modified_name` varchar(50) DEFAULT NULL,`version` int(11) NOT NULL DEFAULT '1',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='工作';
构建的数据
INSERT INTO mysql_db.`user` (id,name,age,address,created_date,last_modified_date,del_flag,create_user_code,create_user_name,last_modified_code,last_modified_name,version) VALUES('1f5ffce8eda44809b91af9857cde1870',NULL,25,'中国武汉','2022-09-20 23:15:43','2022-09-20 23:15:43',0,'1','hresh','1','hresh',0),('23dc1219d58c427884212127606fc830','clearLove',28,'中国上海','2022-09-19 21:14:56','2022-09-19 21:14:56',0,'1','hresh','1','hresh',0),('4f5f617e651a4126b2847f2f25537995','',25,'中国武汉','2022-09-20 22:53:29','2022-09-20 22:53:29',0,'1','hresh','1','hresh',0),('55dc89810e394306b66ab9567b568534','ascii0',21,'中国广东','2022-09-19 21:30:06','2022-09-19 21:30:06',0,'1','hresh','1','hresh',0),('8ac44600842b427c8ef12978c5e8c501',NULL,25,'中国武汉','2022-09-20 22:58:34','2022-09-20 22:58:34',0,'1','hresh','1','hresh',0),('93473b532494477b9d8d34b3165d216a','ascii1',21,'中国广东','2022-09-19 21:30:06','2022-09-19 21:30:06',0,'1','hresh','1','hresh',0),('a45908e8bc874940a6d682370a0ca8d7','clearLove3',28,'中国上海','2022-09-19 22:46:18','2022-09-19 22:46:18',0,'1','hresh','1','hresh',0),('cd613ce660264dc18b15b3333a6421da','ascii2',21,'中国广东','2022-09-19 21:30:06','2022-09-19 21:30:06',0,'1','hresh','1','hresh',0),('e30606f8ee7649499811e74fbc7df583',NULL,25,'中国武汉','2022-09-20 23:11:31','2022-09-20 23:11:31',0,'1','hresh','1','hresh',0),('f68073dc14be4be4a1042fcf78f8f7df','hresh',25,'中国武汉','2022-09-20 07:22:06','2022-09-20 07:22:06',0,'1','hresh','1','hresh',0);
INSERT INTO mysql_db.`user` (id,name,age,address,created_date,last_modified_date,del_flag,create_user_code,create_user_name,last_modified_code,last_modified_name,version) VALUES('fe4f62a77f75468e956fb285475ba3f3','clearLove2',28,'中国上海','2022-09-19 21:16:46','2022-09-19 21:16:46',0,'1','hresh','1','hresh',0);INSERT INTO mysql_db.job (id,name,user_id,address,created_date,last_modified_date,del_flag,create_user_code,create_user_name,last_modified_code,last_modified_name,version) VALUES('55dc89810e394306b66ab9567b568512','程序员','55dc89810e394306b66ab9567b568534','中国湖北',NULL,NULL,0,NULL,NULL,NULL,NULL,0),('55dc89810e394306b66ab9567b568513','外卖员','55dc89810e394306b66ab9567b568534','中国湖北',NULL,NULL,0,NULL,NULL,NULL,NULL,0),('55dc89810e394306b66ab9567b568514','外卖员','93473b532494477b9d8d34b3165d216a','中国湖北',NULL,NULL,0,NULL,NULL,NULL,NULL,0),('55dc89810e394306b66ab9567b568515','厨师','93473b532494477b9d8d34b3165d216a','中国湖北',NULL,NULL,0,NULL,NULL,NULL,NULL,0);
搭建SpringBoot项目
使用 IDEA 新建一个 Maven 项目,叫做 mybatis-springboot。
一些共用的基础代码可以参考上篇文章,这里不做重复介绍,会介绍一些 Mybatis 相关的代码。
引入依赖
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.3</version>
</parent><properties><java.version>1.8</java.version><fastjson.version>1.2.73</fastjson.version><hutool.version>5.5.1</hutool.version><mysql.version>8.0.19</mysql.version><mybatis.version>2.1.4</mybatis.version><mapper.version>4.1.5</mapper.version><org.mapstruct.version>1.4.2.Final</org.mapstruct.version><org.projectlombok.version>1.18.20</org.projectlombok.version>
</properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>${fastjson.version}</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>${hutool.version}</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${org.projectlombok.version}</version><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql.version}</version><scope>runtime</scope></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>${mybatis.version}</version></dependency><dependency><groupId>tk.mybatis</groupId><artifactId>mapper</artifactId><version>${mapper.version}</version></dependency><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1.4.3</version></dependency><dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-commons</artifactId><version>2.4.6</version></dependency><dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-ui</artifactId><version>1.6.9</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.18</version></dependency><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct</artifactId><version>${org.mapstruct.version}</version></dependency><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>${org.mapstruct.version}</version></dependency>
</dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins>
</build>
有些依赖不一定是最新版本,而且你看到这篇文章时,可能已经发布了新版本,到时候可以先模仿着将项目跑起来后,再根据自己的需求来升级各项依赖,有问题咱再解决问题。
分页处理
某些业务场景是需要分页查询和排序功能的,所以我们需要考虑前端如何传递参数给后端,后端如何进行分页查询或者是排序查询。我们使用的是 Mybatis,该框架有个配套的分页插件——PageHelper。
分页基础类
public class SimplePageInfo {private Integer pageNum = 1;private Integer pageSize = 10;public Integer getPageNum() {return pageNum;}public void setPageNum(Integer pageNum) {this.pageNum = pageNum;}public Integer getPageSize() {return pageSize;}public void setPageSize(Integer pageSize) {this.pageSize = pageSize;}
}
排序包装类
@Getter
@Setter
public class OrderInfo {private boolean asc = true;private String column;
}
分页且排序包装类
@Getter
@Setter
@EqualsAndHashCode(callSuper = true)
public class PageSortInfo extends SimplePageInfo {@Schema(name = "排序信息")private List<OrderInfo> orderInfos;public String parseSort() {if (CollectionUtils.isEmpty(orderInfos)) {return null;}StringBuilder sb = new StringBuilder();for (OrderInfo orderInfo : orderInfos) {sb.append(orderInfo.getColumn()).append(" ");sb.append(orderInfo.isAsc() ? " ASC," : " DESC,");}sb.deleteCharAt(sb.length() - 1);return sb.toString();}
}
前端分页查询的请求体对象
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserQueryPageDTO {private String name;@JsonUnwrappedprivate PageSortInfo pageSortInfo;
}
服务层分页查询
PageHelper.startPage(dto.getPageSortInfo().getPageNum(), dto.getPageSortInfo().getPageSize(),dto.getPageSortInfo().parseSort());
Page<User> userPage = (Page<User>) userMapper.select(user);
关于 PageHelper 的使用这里就不多做介绍了。
我们得到的分页查询结果是 Page 对象,可以直接使用,也可以根据需要进行修改,比如下面这个文件:
@Getter
@Setter
public class PageResult<T> {/*** 总条数*/private Long total;/*** 总页数*/private Integer pageCount;/*** 每页数量*/private Integer pageSize;/*** 当前页码*/private Integer pageNum;/*** 分页数据*/private List<T> data;/*** 处理Mybatis分页结果*/public static <T> PageResult<T> ok(Page<T> page) {PageResult<T> result = new PageResult<T>();result.setPageCount(page.getPages());result.setPageNum(page.getPageNum());result.setPageSize(page.getPageSize());result.setTotal(page.getTotal());result.setData(page.getResult());return result;}
}
Mybatis 分页结果除了 Page,还有 PageInfo,Page 继承 ArrayList,PageInfo 对象中的字段更多,这块可以结合项目实际情况进行选择。
Mybatis基础实体类
作为其他实体类的父类,封装了所有的公共字段,包括逻辑删除标志,版本号,创建人和修改人信息。到底是否需要那么多字段,结合实际情况,这里的示例代码比较全,其中@LogicDelete 和@Version 是 Mybatis 特有的注解,@CreatedBy、@CreatedDate 是Springframework 自带的注解,如果我们需要新建人和修改人姓名,则需要自定义注解。
@Data
@Schema(title = "核心基础实体类")
@NoArgsConstructor
@AllArgsConstructor
public class BaseEntity implements Serializable {private static final long serialVersionUID = 1L;@Schema(description = "删除标记")@LogicDelete@Column(name = "del_flag")private Boolean delFlag;@Schema(description = "创建人代码")@CreatedBy@Column(name = "create_user_code")private String createUserCode;@Schema(name = "创建人姓名")@CreatedName@Column(name = "create_user_name")private String createUserName;@Schema(name = "创建时间")@CreatedDate@Column(name = "created_date")private LocalDateTime createdDate;@Schema(name = "修改人代码")@LastModifiedBy@Column(name = "last_modified_code")private String lastModifiedCode;@Schema(name = "修改人姓名")@LastModifiedName@Column(name = "last_modified_name")private String lastModifiedName;@Schema(name = "修改时间")@LastModifiedDate@Column(name = "last_modified_date")private LocalDateTime lastModifiedDate;@Schema(name = "版本号")@Version@Column(name = "version")private Integer version;
}
自定义注解如下:
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CreatedName {}@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LastModifiedName {}
Mybatis拦截器
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。
通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
// ExamplePlugin.java
@Intercepts({@Signature(type= Executor.class,method = "update",args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {private Properties properties = new Properties();public Object intercept(Invocation invocation) throws Throwable {// implement pre processing if needObject returnObject = invocation.proceed();// implement post processing if needreturn returnObject;}public void setProperties(Properties properties) {this.properties = properties;}
}
全局xml配置:
<plugins><plugin interceptor="org.format.mybatis.cache.interceptor.ExamplePlugin"></plugin>
</plugins>
这个拦截器拦截 Executor 接口的 update 方法(其实也就是 SqlSession 的新增,删除,修改操作),所有执行executor 的 update 方法都会被该拦截器拦截到。
Mybatis 拦截器接口定义如下:
public interface Interceptor {Object intercept(Invocation var1) throws Throwable;default Object plugin(Object target) {return Plugin.wrap(target, this);}default void setProperties(Properties properties) {}
}
查看我们自定义的拦截器实现类
@Slf4j
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class,Object.class})})
public class AutoFillFieldInterceptor implements Interceptor {private static final Logger logger = LoggerFactory.getLogger(AutoFillFieldInterceptor.class);@Overridepublic Object intercept(Invocation invocation) throws Throwable {logger.info("执行intercept方法:{}", invocation.toString());Object[] args = invocation.getArgs();MappedStatement mappedStatement = (MappedStatement) args[0];SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();if (sqlCommandType != SqlCommandType.INSERT && sqlCommandType != SqlCommandType.UPDATE) {return invocation.proceed();}Object parameter = args[1];Class<?> clazz = parameter.getClass();// 批量SQL操作if (Map.class.isAssignableFrom(clazz)) {Map<String, Object> paramMap = (Map<String, Object>) parameter;if (paramMap.containsKey("recordList")) {processData(sqlCommandType, paramMap.get("recordList"));} else if (paramMap.containsKey("collection")) {processData(sqlCommandType, paramMap.get("collection"));} else if (paramMap.containsKey("list")) {processData(sqlCommandType, paramMap.get("list"));}} else {// 单个SQL操作processData(sqlCommandType, parameter);}return invocation.proceed();}private boolean isSkipInject(Class clazz) {return clazz.getAnnotation(Table.class) == null;}private void processData(SqlCommandType sqlCommandType, Object parameter) {Class<?> clazz = parameter.getClass();if (Collection.class.isAssignableFrom(clazz)) {Collection<?> collection = (Collection<?>) parameter;for (Object object : collection) {processData(sqlCommandType, object);}return;}if (isSkipInject(clazz)) {return;}List<EntityField> entityFieldList = getFields(clazz);MetaObject metaObject = MetaObjectUtil.forObject(parameter);for (EntityField field : entityFieldList) {if (sqlCommandType == SqlCommandType.INSERT) {if (field.isAnnotationPresent(CreatedDate.class)) {metaObject.setValue(field.getName(), LocalDateTime.now());}if (field.isAnnotationPresent(CreatedBy.class)) {String id = "1";metaObject.setValue(field.getName(), id);}if (field.isAnnotationPresent(CreatedName.class)) {metaObject.setValue(field.getName(), "hresh");}if (field.isAnnotationPresent(Version.class)) {metaObject.setValue(field.getName(), 0);}if (field.isAnnotationPresent(LogicDelete.class)) {metaObject.setValue(field.getName(), false);}}if (field.isAnnotationPresent(LastModifiedDate.class)) {metaObject.setValue(field.getName(), LocalDateTime.now());}if (field.isAnnotationPresent(LastModifiedBy.class)) {metaObject.setValue(field.getName(), "1");}if (field.isAnnotationPresent(LastModifiedName.class)) {metaObject.setValue(field.getName(), "hresh");}}}private List<EntityField> getFields(Class clazz) {return EntityHelper.getColumns(clazz).stream().map(EntityColumn::getEntityField).collect(Collectors.toList());}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {}
}
关于上述代码根据项目实际需求进行调整,来填充非关键数据。
那么该拦截器实现类是如何注册到 Spring 容器中的呢?还是全局搜索查看位置。
@Configuration
@ConditionalOnBean({SqlSessionFactory.class})
public class MybatisAutoConfiguration {@Autowiredprivate SqlSessionFactory sqlSessionFactory;@PostConstructpublic void init() {sqlSessionFactory.getConfiguration().addInterceptor(new AutoFillFieldInterceptor());}
}
最后在 resources 目录下创建META-INF目录下,在 META-INF 目录下创建 spring.factories 文件,文件内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.msdn.orm.hresh.common.mybatis.config.MybatisAutoConfiguration
批量操作功能
批量操作包括批量新增、修改、删除等功能。
虽然 Mybatis 提供了 IdListMapper<T, Long> InsertListMapper 这两个接口来实现批量操作,但功能有限,所以一般情况下我们会自定义批量操作类。
此时可以自定义一个通用 BaseMapper,如以下接口,再让编写的 mapper 继承这个 BaseMapper。
需要注意的是:自定义的通用 mapper,想要生效,必须要加上@RegisterMapper 注解。
@RegisterMapper
public interface ListMapper<T, PK> {/*** 批量插入,支持批量插入的数据库可以使用,** @param recordList* @return*/@InsertProvider(type = ListProvider.class, method = "dynamicSQL")int insertList(List<? extends T> recordList);/*** 批量更新** @return*/@UpdateProvider(type = ListProvider.class, method = "dynamicSQL")int updateBatchByPrimaryKeySelective(List<? extends T> recordList);/*** 根据主键字符串进行查询,类中只有存在一个带有@Id注解的字段** @param idList* @return*/@SelectProvider(type = ListProvider.class, method = "dynamicSQL")List<T> selectByIdList(@Param("idList") List<PK> idList);/*** 根据主键字符串进行删除,类中只有存在一个带有@Id注解的字段** @param idList* @return*/@DeleteProvider(type = ListProvider.class, method = "dynamicSQL")int deleteByIdList(@Param("idList") List<PK> idList);
}
关于批量操作的具体代码位于 ListProvider 文件中,因为代码比较多,这里只贴出批量新增的代码:
/*** 填充主键值** @param list*/public static void fillId(List<?> list, String fieldName) {for (Object object : list) {MetaObject metaObject = MetaObjectUtil.forObject(object);if (metaObject.getValue(fieldName) == null) {metaObject.setValue(fieldName, IdUtils.genId());}}}/*** 批量插入** @param ms*/public String insertList(MappedStatement ms) {final Class<?> entityClass = getEntityClass(ms);//开始拼sqlStringBuilder sql = new StringBuilder();List<EntityColumn> pkColumns = new ArrayList<>(EntityHelper.getPKColumns(entityClass));sql.append("<bind name=\"listNotEmptyCheck\" value=\"@tk.mybatis.mapper.util.OGNL@notEmptyCollectionCheck(list, '"+ ms.getId() + " 方法参数为空')\"/>");sql.append("<bind name=\"fillIdProcess\" value=\"@com.msdn.orm.hresh.common.mybatis.ListProvider@fillId(list, '"+ pkColumns.get(0).getProperty() + "')\"/>");sql.append(SqlHelper.insertIntoTable(entityClass, tableName(entityClass), "list[0]"));sql.append(SqlHelper.insertColumns(entityClass, false, false, false));sql.append(" VALUES ");sql.append("<foreach collection=\"list\" item=\"record\" separator=\",\" >");sql.append("<trim prefix=\"(\" suffix=\")\" suffixOverrides=\",\">");//获取全部列Set<EntityColumn> columnList = EntityHelper.getColumns(entityClass);//当某个列有主键策略时,不需要考虑它的属性是否为空,因为如果为空,一定会根据主键策略给他生成一个值for (EntityColumn column : columnList) {if (column.isInsertable()) {sql.append(column.getColumnHolder("record") + ",");}}sql.append("</trim>");sql.append("</foreach>");// 反射把MappedStatement中的设置主键名EntityHelper.setKeyProperties(EntityHelper.getPKColumns(entityClass), ms);return sql.toString();}
动态链式查询
虽然我们可以在 mapper.xml 文件中自定义 SQL 查询,但这样做有些麻烦,如果能够在代码中编写类似于 SQL 条件的 Code,对于开发人员来说,更加便捷,可读性也比较好。注意,特别麻烦的 SQL 语句还是要在 mapper.xml 文件中自定义。
这里借助 tk 的通用 mapper 实在 mybatis 中使用 Example 实现动态查询,大概有四种方式,可以参考本文。
其中方式三是:Example.builder 方式(其中 where 从句中内容可以拿出来进行动态 sql 拼接)
Example example = Example.builder(MybatisDemo.class).select("cabId","cabName").where(Sqls.custom().andEqualTo("count", 0).andLike("name", "%d%")).orderByDesc("count","name").build();
List<MybatisDemo> demos = mybatisDemoMapper.selectByExample(example);
可以看到上述实现方式,需要手动输入属性名,一旦数据库有变动或输入错误就会出错,不符合我们的期望。因此我们选择方式四。
//获得seekendsql
WeekendSqls<MybatisDemo> sqls = WeekendSqls.<MybatisDemo>custom();//可进行动态sql拼接
sqls = sqls.andEqualTo(MybatisDemo::getCount,0).andLike(MybatisDemo::getName,"%d%");//获得结果
List<MybatisDemo> demos = mybatisDemoMapper.selectByExample(Example.builder(MybatisDemo.class).where(sqls).orderByDesc("count","name").build());
在本项目中对于 Example 的使用有一个集成实现类,ExampleBuilder 类,该类有以下几个属性:
private Class<T> entityClass;//实体类private Class<? extends Mapper<T>> mapperClass;//实体类对应的mapper接口类private WeekendSqls<T> weekendSqls = WeekendSqls.custom();//用于拼接sqlprivate LinkedHashMap<String, Boolean> orderList = new LinkedHashMap<>();//用于存放排序的字段private Map<String, Object> setterList = new HashMap<>();//存放需要更新的字段以及新值
定义的方法过多,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ghDvF00F-1668951219082)(https://www.hreshhao.com/wp-content/uploads/2022/11/微信截图_20201010113049.png)]
在 service 服务类中的查询方法定义如下,不仅可以实现分页查询,还可以排序。
@Override
public Page<BankAreaVO> queryPage(BankAreaQueryPageDTO dto) {PageHelper.startPage(dto.getPageSortInfo().getPageNum(), dto.getPageSortInfo().getPageSize(), dto.getPageSortInfo().parseSort());List<BankArea> bankAreaList = ExampleBuilder.create(BankAreaMapper.class).andEqualTo(dto.getEnableFlag() != null, BankArea::getEnableFlag, dto.getEnableFlag()).andEqualTo(StringUtils.isNotEmpty(dto.getCountry()), BankArea::getCountry, dto.getCountry()).andEqualTo(StringUtils.isNotEmpty(dto.getProvince()), BankArea::getProvince, dto.getProvince()).andLike(StringUtils.isNotEmpty(dto.getBankAreaCode()), BankArea::getBankAreaCode, "%" + dto.getBankAreaCode() + "%").andLike(StringUtils.isNotEmpty(dto.getBankAreaName()), BankArea::getBankAreaName, "%" + dto.getBankAreaName() + "%").orderByAsc(BankArea::getBankAreaCode).select();Page<BankArea> bankAreaPage = (Page<BankArea>) bankAreaList;return BeanUtils.copyProperties(bankAreaPage, BankAreaVO.class);
}
我们重点关注经常使用到的方法,比如 create 方法等。
public static <T, M extends Mapper<T>> ExampleBuilder<T> create(Class<M> clazz) {ExampleBuilder<T> exampleBuilder = new ExampleBuilder<>();exampleBuilder.mapperClass = clazz;//获取 mapper对应的实体类exampleBuilder.entityClass = (Class<T>) ((ParameterizedType) clazz.getGenericInterfaces()[0]).getActualTypeArguments()[0];return exampleBuilder;
}
然后将查询条件拼接起来,andEqualTo()处理等值判断,andLike()处理模糊查询,orderByAsc()处理字段排序。
public ExampleBuilder<T> andEqualTo(boolean condition, Fn<T, Object> fn, Object value) {if (condition) {weekendSqls.andEqualTo(Reflections.fnToFieldName(fn), value);}return this;
}public ExampleBuilder<T> andLike(boolean condition, Fn<T, Object> fn, String value) {if (condition) {weekendSqls.andLike(Reflections.fnToFieldName(fn), value);}return this;
}public ExampleBuilder<T> orderByAsc(Fn<T, Object> fn) {String fieldName = Reflections.fnToFieldName(fn);this.orderList.put(fieldName, true);return this;
}
最后执行 select 方法
public List<T> select() {Example example = this.build();return SpringUtils.getBean(mapperClass).selectByExample(example);
}public Example build() {Example.Builder builder = Example.builder(entityClass);//拼接到where条件后if (weekendSqls.getCriteria().getCriterions().size() > 0) {builder.where(weekendSqls);}//遍历字段排序列表,获取指定排序的结果集for (Map.Entry<String, Boolean> entry : this.orderList.entrySet()) {if (entry.getValue()) {builder.orderByAsc(entry.getKey());} else {builder.orderByDesc(entry.getKey());}}return builder.build();
}
在 select()方法中的 SpringUtils.getBean(mapperClass)
是为了获取已经注册到 Spring 上下文中的实例,我们可以简单查看一下 SpringUtils 类。
@Service
public class SpringUtils implements ApplicationContextAware {private static ApplicationContext applicationContext;public void setApplicationContext(ApplicationContext applicationContext)throws BeansException {SpringUtils.applicationContext = applicationContext;}public static <T> T getBean(Class<T> clazz) {return (T) applicationContext.getBean(clazz);}public static Object getBean(String name) throws BeansException {return applicationContext.getBean(name);}
}
至此,关于本项目中有价值的内容已经讲述完毕,因篇幅有限,未能展示所有代码。基于上述核心代码,我们只需要往项目中添加相关业务代码即可,接下来我们就可以运行之前写的脚本工具,根据数据库表信息快速生成模板代码。
一键式生成模版代码
运行 orm-generate 项目,在 swagger 上调用 /build 接口,调用参数如下:
{"database": "mysql_db","flat": true,"type": "mybatis","group": "hresh","host": "127.0.0.1","module": "orm","password": "root","port": 3306,"table": ["user","job"],"username": "root","tableStartIndex":"0"
}
先将代码下载下来,解压出来目录如下:
代码文件直接移到项目中就行了,稍微修改一下引用就好了。
功能实现
请求日志输出
比如说我们访问 /users/queryPage 接口,看看控制台输出情况:
Request Info : {"classMethod":"com.msdn.orm.hresh.controller.UserController.queryPage","ip":"127.0.0.1","requestParams":{"dto":{"pageSortInfo":{"pageSize":5,"pageNum":1}}},"httpMethod":"GET","url":"http://localhost:8801/users/queryPage","result":{"code":"200","message":"操作成功","success":true},"methodDesc":"获取用户分页列表","timeCost":69}
可以看到,日志输出中包含前端传来的请求体,请求 API,返回结果,API 描述,API 耗时。
统一返回格式
比如说分页查询,返回结果如下:
{"data": {"total": 5,"pageCount": 1,"pageSize": 5,"pageNum": 1,"data": [{"id": null,"name": "clearLove","age": 28,"address": "中国上海"},{"id": null,"name": "ascii0","age": 21,"address": "中国广东"},{"id": null,"name": "ascii1","age": 21,"address": "中国广东"},{"id": null,"name": "ascii2","age": 21,"address": "中国广东"},{"id": null,"name": "clearLove2","age": 28,"address": "中国上海"}]},"code": "200","message": "操作成功","success": true
}
如果是新增请求,返回结果为:
{"data": null,"code": "200","message": "操作成功","success": true
}
此处 data 没有新增的数据,如果项目需要新增的实体信息,可以稍作修改。
异常处理
下面简单演示一下参数异常的情况,在 add user 时校验参数值是否为空。
public int add(UserDTO dto) {if (StringUtils.isBlank(dto.getName())) {BusinessException.validateFailed("userName不能为空");}User user = userStruct.dtoToModel(dto);return userMapper.insertSelective(user);}
如果传递的 name 值为空,则返回结果为:
{"data": null,"code": "400","message": "userName不能为空","success": false
}
补全操作者信息
此处依赖于 Mybatis 拦截器,重点在 AutoFillFieldInterceptor 文件。
for (EntityField field : entityFieldList) {if (sqlCommandType == SqlCommandType.INSERT) {if (field.isAnnotationPresent(CreatedDate.class)) {metaObject.setValue(field.getName(), LocalDateTime.now());}if (field.isAnnotationPresent(CreatedBy.class)) {String id = "1";metaObject.setValue(field.getName(), id);}if (field.isAnnotationPresent(CreatedName.class)) {metaObject.setValue(field.getName(), "hresh");}if (field.isAnnotationPresent(Version.class)) {metaObject.setValue(field.getName(), 0);}if (field.isAnnotationPresent(LogicDelete.class)) {metaObject.setValue(field.getName(), false);}}if (field.isAnnotationPresent(LastModifiedDate.class)) {metaObject.setValue(field.getName(), LocalDateTime.now());}if (field.isAnnotationPresent(LastModifiedBy.class)) {metaObject.setValue(field.getName(), "1");}if (field.isAnnotationPresent(LastModifiedName.class)) {metaObject.setValue(field.getName(), "hresh");}
}
上述代码中关于新增者信息和修改者信息,暂时是写死的状态,实际项目中,可以根据 token 信息进行解析,然后来填充新增者和修改者信息。
批量操作
这里简单演示一下关于批量新增的代码
@Overridepublic int batchAdd(UserDTO dto) {List<User> users = new ArrayList<>();for (int i = 0; i < 3; i++) {User user = User.builder().name("ascii" + i).age(21).address("中国广东").build();users.add(user);}return userMapper.insertList(users);}
注意:这里的 UserMapper 需要继承我们自定义的 ListMapper,如下所示:
public interface UserMapper extends Mapper<User>, ListMapper<User,Long> {}
执行效果如下:
分页查询
前端参数传递:
{"pageNum": 1,"pageSize": 5,"name": "ascii","orderInfos":[{"column": "name","asc": true}]
}
后端代码处理:
public Page<UserVO> queryPage(UserQueryPageDTO dto) {PageHelper.startPage(dto.getPageSortInfo().getPageNum(), dto.getPageSortInfo().getPageSize(),dto.getPageSortInfo().parseSort());List<User> users = ExampleBuilder.create(UserMapper.class).andLike(User::getName, dto.getName() + "%").orderByDesc(User::getName).select();Page<User> userPage = (Page<User>) users;return PageUtils.convert(userPage, UserVO.class);}
返回结果为:
{"data": {"total": 3,"pageCount": 1,"pageSize": 5,"pageNum": 1,"data": [{"name": "ascii0","age": 21,"address": "中国广东","jobVOS": null},{"name": "ascii1","age": 21,"address": "中国广东","jobVOS": null},{"name": "ascii2","age": 21,"address": "中国广东","jobVOS": null}]},"code": "200","message": "操作成功","success": true
}
动态查询
查询方法如下:
public List<UserVO> queryList(UserDTO dto) {List<User> users = ExampleBuilder.create(UserMapper.class).andLike(User::getName, dto.getName() + "%").orderByDesc(User::getName).select();return BeanUtils.copyProperties(users, UserVO.class);}
执行结果如下:
{"data": [{"name": "ascii2","age": 21,"address": "中国广东"},{"name": "ascii1","age": 21,"address": "中国广东"},{"name": "ascii0","age": 21,"address": "中国广东"}],"code": "200","message": "操作成功","success": true
}
如果是分页查询,可以这样处理:
public Page<UserVO> queryPage(UserQueryPageDTO dto) {PageHelper.startPage(dto.getPageSortInfo().getPageNum(), dto.getPageSortInfo().getPageSize(),dto.getPageSortInfo().parseSort());List<User> users = ExampleBuilder.create(UserMapper.class).andLike(User::getName, dto.getName() + "%").orderByDesc(User::getName).select();Page<User> userPage = (Page<User>) users;return PageUtils.convert(userPage, UserVO.class);}
查询结果为:
{"data": {"total": 3,"pageCount": 1,"pageSize": 5,"pageNum": 1,"data": [{"name": "ascii2","age": 21,"address": "中国广东"},{"name": "ascii1","age": 21,"address": "中国广东"},{"name": "ascii0","age": 21,"address": "中国广东"}]},"code": "200","message": "操作成功","success": true
}
一对多查询
比如说我们定义的 User 和 Job 类,存在着一对多的关系,所以查询 User 信息时,还需要返回关联的 Job 数据。
关于 Mybatis 一对多、多对一处理手段,可以参考我之前的文章。
本项目采用的是结果嵌套处理方式。
JobMapper.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.msdn.orm.hresh.mapper.JobMapper"><resultMap id="jobResultMap" type="com.msdn.orm.hresh.model.Job"><id column="id" property="id"/><result column="name" property="name"/><result column="user_id" property="userId"/><result column="address" property="address"/><result column="created_date" property="createdDate"/><result column="last_modified_date" property="lastModifiedDate"/><result column="del_flag" property="delFlag"/><result column="create_user_code" property="createUserCode"/><result column="create_user_name" property="createUserName"/><result column="last_modified_code" property="lastModifiedCode"/><result column="last_modified_name" property="lastModifiedName"/><result column="version" property="version"/></resultMap></mapper>
UserMapper.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.msdn.orm.hresh.mapper.UserMapper"><resultMap id="userResultMap" type="com.msdn.orm.hresh.model.User"><id column="id" property="id"/><result column="name" property="name"/><result column="age" property="age"/><result column="address" property="address"/><result column="created_date" property="createdDate"/><result column="last_modified_date" property="lastModifiedDate"/><result column="del_flag" property="delFlag"/><result column="create_user_code" property="createUserCode"/><result column="create_user_name" property="createUserName"/><result column="last_modified_code" property="lastModifiedCode"/><result column="last_modified_name" property="lastModifiedName"/><result column="version" property="version"/></resultMap><resultMap id="userVoResultMap" type="com.msdn.orm.hresh.model.User"extends="com.msdn.orm.hresh.mapper.UserMapper.userResultMap"><collection property="jobs" resultMap="com.msdn.orm.hresh.mapper.JobMapper.jobResultMap"columnPrefix="job_"/></resultMap><select id="queryList" resultMap="userVoResultMap">SELECT u.*,j.name job_name,j.address job_addressFROMuser uLEFT JOIN job j ON u.id = j.user_id<where><if test="name!=null and name!=''">and u.name like concat('%',#{name},'%')</if><if test="address != null and address !=''">and u.address like concat('%',#{address},'%')</if></where></select>
</mapper>
关于上述 xml 配置,我们最终获取的是关于 User 的返回结果,还需要再转换为 UserVO 才返回给前端。如果服务层获取到返回结果后,不需要其他业务操作,可以直接获取 UserVO,反之,我们需要查询得到 User,处理完其他操作后,再转换为 UserVO。需要修改的
<resultMap id="userVoResultMap" type="com.msdn.orm.hresh.vo.UserVO"extends="com.msdn.orm.hresh.mapper.UserMapper.userResultMap"><collection property="jobVOS" resultMap="com.msdn.orm.hresh.mapper.JobMapper.jobResultMap"columnPrefix="job_"/></resultMap><resultMap id="userResultMap2" type="com.msdn.orm.hresh.model.User"extends="com.msdn.orm.hresh.mapper.UserMapper.userResultMap"><collection property="jobs" resultMap="com.msdn.orm.hresh.mapper.JobMapper.jobResultMap"columnPrefix="job_"/></resultMap>
userVoResultMap 对应 UserVO 返回结果,userResultMap2 对应 User 结果。
对应的 UserMapper 文件
public interface UserMapper extends Mapper<User>, ListMapper<User,Long> {List<User> queryList(UserDTO userDTO);
}
我们修改 UserService 中的查询方法如下:
public List<UserVO> queryList(UserDTO dto) {// List<User> users = ExampleBuilder.create(UserMapper.class)
// .andLike(User::getName, dto.getName() + "%")
// .orderByDesc(User::getName)
// .select();List<User> users = userMapper.queryList(dto);return userStruct.modelToVO(users);}
同时在 application.yml 文件中打开 SQL 输出配置:
mybatis:mapper-locations: classpath:mapper/*Mapper.xmlconfiguration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
调用接口,可以发现控制台输出如下:
==> Preparing: SELECT u.*, j.name job_name, j.address job_address FROM user u LEFT JOIN job j ON u.id = j.user_id WHERE u.name like concat('%',?,'%')
==> Parameters: ascii(String)
<== Columns: id, name, age, address, created_date, last_modified_date, del_flag, create_user_code, create_user_name, last_modified_code, last_modified_name, version, job_name, job_address
<== Row: 55dc89810e394306b66ab9567b568534, ascii0, 21, 中国广东, 2022-09-19 21:30:06, 2022-09-19 21:30:06, 0, 1, hresh, 1, hresh, 0, 程序员, 中国湖北
<== Row: 55dc89810e394306b66ab9567b568534, ascii0, 21, 中国广东, 2022-09-19 21:30:06, 2022-09-19 21:30:06, 0, 1, hresh, 1, hresh, 0, 外卖员, 中国湖北
<== Row: 93473b532494477b9d8d34b3165d216a, ascii1, 21, 中国广东, 2022-09-19 21:30:06, 2022-09-19 21:30:06, 0, 1, hresh, 1, hresh, 0, 外卖员, 中国湖北
<== Row: 93473b532494477b9d8d34b3165d216a, ascii1, 21, 中国广东, 2022-09-19 21:30:06, 2022-09-19 21:30:06, 0, 1, hresh, 1, hresh, 0, 厨师, 中国湖北
<== Row: cd613ce660264dc18b15b3333a6421da, ascii2, 21, 中国广东, 2022-09-19 21:30:06, 2022-09-19 21:30:06, 0, 1, hresh, 1, hresh, 0, null, null
<== Total: 5
返回结果为:
{"data": [{"name": "ascii0","age": 21,"address": "中国广东","jobVOS": [{"name": "程序员","address": "中国湖北"},{"name": "外卖员","address": "中国湖北"}]},{"name": "ascii1","age": 21,"address": "中国广东","jobVOS": [{"name": "外卖员","address": "中国湖北"},{"name": "厨师","address": "中国湖北"}]},{"name": "ascii2","age": 21,"address": "中国广东","jobVOS": []}],"code": "200","message": "操作成功","success": true
}
看到这里,你可能发现了这样一个问题,如果使用我们自定义的工具类 ExampleBuilder,是无法完成连表查询的,也无法实现懒加载查询。尝试修改过 ExampleBuilder,但未能实现类似于 left join 的查询,不过我在构建 Spring JPA 动态查询工具类的时候,实现了连表查询,可以关注后续文章的发布。
Swagger
启动项目后,访问 swagger,页面展示如下:
总结
以上便是本项目所包含的内容,关于基础代码,可以结合实际需要继续深入挖掘,可能也有不足的地方,或者我所不了解的基础代码,望各位大佬多多指教。
身为过来人,我也体验过 CRUD 的工作,对项目搭建知之甚少,而在工作中很少遇到从零开发一个项目的机会,这也是我所苦恼的。希望这篇文章能够对大家有所帮助,尤其是那些还未毕业的同学们,如果你们想实操一个项目,可以先去 Github 上找一个感兴趣的项目,然后复用本文章中提到的基础设施,亲自动手去完成一个项目。
感兴趣的朋友可以去我的 Github 下载相关代码,如果对你有所帮助,不妨 Star 一下,谢谢大家支持!
参考文献
写个日志请求切面,前后端甩锅更方便
maven之自定义插件
liquibase的changelog详解
SpringBoot集成Mybatis项目实操相关推荐
- 一个项目了解 SpringBoot 集成 MyBatis
SpringBoot 集成 MyBatis 创建项目 1.引入依赖 mybatis-spring-boot-starter 依赖 完整的 pom.xml 2.配置文件 加入mybatis配置 完整的配 ...
- java整合mybatis,springboot集成mybatis
# springboot集成mybatis springboot基础mybatis还是很简单的,比之前springmvc集成mybatis要少很多配置,只要大家按照步骤一步一步来,几分钟就 能实现.具 ...
- springboot集成mybatis源码分析-mybatis的mapper执行查询时的流程(三)
springboot集成mybatis源码分析-mybatis的mapper执行查询时的流程(三) 例: package com.example.demo.service;import com.exa ...
- springboot集成mybatis源码分析-启动加载mybatis过程(二)
springboot集成mybatis源码分析-启动加载mybatis过程(二) 1.springboot项目最核心的就是自动加载配置,该功能则依赖的是一个注解@SpringBootApplicati ...
- springboot集成mybatis源码分析(一)
springboot集成mybatis源码分析(一) 本篇文章只是简单接受使用,具体源码解析请看后续文章 1.新建springboot项目,并导入mybatis的pom配置 配置数据库驱动和mybat ...
- SpringBoot集成Mybatis用法笔记
今天给大家整理SpringBoot集成Mybatis用法笔记.希望对大家能有所帮助! 搭建一个SpringBoot基础项目. 具体可以参考SpringBoot:搭建第一个Web程序 引入相关依赖 &l ...
- Java 捕获 mybatis异常_3 springboot集成mybatis和全局异常捕获
mybatis有两种方式,一种是基于XML,一种是基于注解 springboot集成mybatis 首先先创建表,这里都简化了 DROP TABLE IF EXISTS `user`; CREATE ...
- Springboot集成mybatis通用Mapper与分页插件PageHelper
Springboot集成mybatis通用Mapper与分页插件PageHelper 插件介绍 通用 Mapper 是一个可以实现任意 MyBatis 通用方法的框架,项目提供了常规的增删改查操作以及 ...
- SpringBoot教程(十一) | SpringBoot集成Mybatis
上一篇文章我们介绍了SpringBoot集成JdbcTemplate.简单体验了一下JdbcTemplate框架的用法,今天的内容比较重要,我们来介绍一下SpringBoot集成Mybatis的步骤. ...
最新文章
- 计算机技术大神,2017考研:计算机科学与技术学科大神给你的套路
- 历届试题 大臣的旅费(深搜 树的直径)
- Power BI连接MySQL 提示错误......未能加载文件或程序集......或它的某一个依赖项
- 入门RabbitMQ核心概念
- 再谈strncpy函数--值得一看的好文章
- 红橙Darren视频笔记 自定义RatingBar touch事件学习 dp转px listener监听
- “以毒攻毒”?阿里将上线“二哈”防骚扰电话应用程序
- PAT 乙级 1044. 火星数字(20) Java版
- Linux 命令(51)—— ipcs 命令
- UG NX 12 鼠标及快捷键的用法
- 打印流PrintWriter实现自动刷新和换行
- 人工智能基础入门——神经网络讲解
- 学习挖掘机和程序员哪个好
- 20211129编译RK3399的Android发生编译服务器的CPU看门狗软件死锁的问题
- 什么是数据库?什么是关系数据库?什么是非关系型数据库?
- Linux Shell 并行
- [转载]转录组测序分析中cufflinks的使用及问题
- 安装CentOS7虚拟机(超详细)
- 禁止Chrome浏览器自动更新 亲测可用
- 从程序员的尽头是业务说起
热门文章
- 网络营销方式有什么?
- 基于javaweb的仿天猫商城系统(java+jsp+springboot+ssm+mysql)
- 快递Api接口 微信公众号开发流程
- 上面一个星星下面一个r_《zhchshr》教案(人教版一年级上册)
- NOW直播——Flutter组件化开发方案
- 【Java刷题特辑第三期】——这些经典笔试题,你确定都做过吗?
- SpringBoot配置加载顺序
- Https基础以及如何配置Https
- FPGA中简单的握手协议
- 工业减压蒸馏工艺中大流量耐腐蚀快响应精密真空控制的解决方案