我不能停滞不前
前言
诸君,搭建个人博客系列,我终于开始更了。。没想到还有人催更,受宠若惊!
在阅读本文前,我假设您除了Spring Boot,还对以下技术有了解
- ORM框架:Mybatis-Plus
- 插件工具:Lomok
- 数据库连接池:Druid
- 模板引擎:Freemark,Thymeleaf
- API文档框架:Swagger
为了用户体验,正文部分是一气呵成的,搭建过程中所遇问题,可以看踩坑纪部分。
提醒一下:
若是您要参考正文部分来搭建环境,请务必看踩坑纪部分,否则只看正文部分,不便于理解且可能项目会跑不起来。
好吧。。其实这里本来还有一些话的,但是发现写的太长了。。也是为了不影响读者体验,放在了后记中;毕竟各位是来看技术文章的,不是看我瞎BB的,不能主次颠倒。
开始正文
正文
1. 数据库设计
数据库设计是最初的阶段,同时我也是卡在这阶段最久。至于为什么卡这么久请看踩坑纪部分
根据我上一篇的【Spring Boot】搭建个人博客 - 需求分析设计数据库表。
数据库设计规范主要参考阿里巴巴Java开发手册 1.4.0,以及一些网上资料;
表名前缀是模块名,分了8个模块。
一共18张表,其中16张实体表,2张多对多关系表;
sql文件会与项目一同上传至Github
2. idea创建Spring boot项目
具体创建过程参考这里:【Spring Boot】初体验
给出application.properties和pom.xml
application.properties
#上下文
server.servlet.context-path=/blog
#日志配置
logging.level.org.springframework.web=DEBUG
#Thymeleaf 配置
spring.thymeleaf.mode=LEGACYHTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.servlet.content-type=text/html
##缓存设置为false, 这样修改之后马上生效,便于调试;生产环境下可以设置为true
spring.thymeleaf.cache=false
#Druid配置
##JDBC配置
spring.datasource.druid.url=jdbc:mysql://localhost:3306/blog?useUnicode=true&useSSL=false&characterEncoding=utf8
spring.datasource.druid.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.druid.username=root
spring.datasource.druid.password=root
##连接池配置
spring.datasource.druid.max-active=20
#Freemarker配置
##不使用文件系统优先,而使用classpath下的资源文件优先,解决打jar包运行后,出现的异常问题
spring.freemarker.prefer-file-system-access=false
pom.xml
4.0.0
top.sinch
blog
0.0.1-SNAPSHOT
jar
blog
Blog project for Spring Boot
org.springframework.boot
spring-boot-starter-parent
2.0.4.RELEASE
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-devtools
true
com.baomidou
mybatis-plus-boot-starter
3.0.5
mysql
mysql-connector-java
runtime
com.alibaba
druid-spring-boot-starter
1.1.10
io.springfox
springfox-swagger2
2.9.2
io.springfox
springfox-swagger-ui
2.9.2
org.projectlombok
lombok
1.18.4
provided
com.google.code.gson
gson
2.8.5
org.springframework.boot
spring-boot-starter-freemarker
org.springframework.boot
spring-boot-starter-thymeleaf
Thymeleaf的非严格HTML格式包-->
net.sourceforge.nekohtml
nekohtml
1.9.22
org.springframework.boot
spring-boot-maven-plugin
3. Mybatis-Plus代码生成器生成模板代码
使用Mybatis-Plus官方提供的代码生成器Demo生成模板代码。
需要注意的是:
- 生成器根据数据库表名,一次生成的是同一模块下代码;若有几个模块则需要更改模块名及相对应表名;有几个模块,执行几次生成器生成对应模块下的代码
- 若只有一个模块或者不区分模块,则只用执行一次生成器
新建包路径
在src\main\java
下新建包路径com.baomidou.mybatisplus
,把代码生成器和项目代码区分开
代码生成器(把官方Demo根据需求改造了一下)
package com.baomidou.mybatisplus;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
/**
* mybatis-plus 代码生成器
*
* @Author sincH
* @Date 2018/11/10
*/
public class CodeGenerator {
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
//项目根目录
String projectPath = "D:/_Code/idea/blog";
// String projectPath = "C:/Users/sincH/Desktop/Temp/blog";
//Java源码输出目录
gc.setOutputDir(projectPath + "/src/main/java");
//作者
gc.setAuthor("sincH");
//是否打开输出目录,默认true
gc.setOpen(false);
//开启swagger2模式(将实体类默认的Javadoc文档注解转为Swagger文档注解),默认false
gc.setSwagger2(true);
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
//数据库连接URL
dsc.setUrl("jdbc:mysql://localhost:3306/blog?useUnicode=true&useSSL=false&characterEncoding=utf8");
// dsc.setSchemaName("public");//数据库模式;MySQL中库即模式
//数据库驱动
dsc.setDriverName("com.mysql.jdbc.Driver");
//数据库用户名
dsc.setUsername("root");
//数据库密码
dsc.setPassword("root");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
//模块名
pc.setModuleName("article");
//父包名;模块将在父包下生成
pc.setParent("top.sinch.blog");
mpg.setPackageInfo(pc);
// 自定义配置
// InjectionConfig cfg = new InjectionConfig() {
// @Override
// public void initMap() {
// // to do nothing
// }
// };
// List focList = new ArrayList<>();
// focList.add(new FileOutConfig("/templates/mapper.xml.ftl") {
// @Override
// public String outputFile(TableInfo tableInfo) {
// // 自定义输入文件名称
// return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
// + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
// }
// });
// cfg.setFileOutConfigList(focList);
// mpg.setCfg(cfg);
//setXml(null)代表不生成xml文件
mpg.setTemplate(new TemplateConfig().setXml(null));
// 策略配置
StrategyConfig strategy = new StrategyConfig();
//表名下划线转驼峰
strategy.setNaming(NamingStrategy.underline_to_camel);
//列名下划线转驼峰
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
//Boolean类型字段是否移除is前缀(默认 false)
strategy.setEntityBooleanColumnRemoveIsPrefix(true);
// strategy.setSuperEntityClass("com.baomidou.ant.common.BaseEntity");
//是否开启实体类Lombok模式(默认false)
strategy.setEntityLombokModel(true);
//是否开启RestController风格
strategy.setRestControllerStyle(true);
// strategy.setSuperControllerClass("com.baomidou.ant.common.BaseController");
//表名(数据库中存在的表);多表传数组
strategy.setInclude(new String[]{
"article_info","article_content","article_comment","article_comment_reply",
"article_archive","article_label","article_category",
"r_article_info_article_label","r_article_info_article_category"
});
// strategy.setSuperEntityColumns("id");
//驼峰转连字符(是否开启Controller映射连字符风格)
strategy.setControllerMappingHyphenStyle(true);
/**
*设置表名前缀;
*此表名前缀若与数据库表名前缀一致,则自动创建实体类时,实体类名匹配数据库表名下划线后面的名;
*栗子:数据库表名为 admin_info ,当strategy.setTablePrefix("admin_")时,则实体类名为Info;
*若不设置表名前缀,则实体类名匹配数据库表名
*/
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
//设置代码生成器的模板引擎
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
//执行代码生成器
mpg.execute();
}
}
需要注意的是:
把代码生成器生成的代码输出目录设置为项目地址,这样就可以不用在代码生成后还要手动复制到项目中。
生成器生成结果及其对应模块
这里是9个模块,多了一个core(核心)模块;是放项目的配置类,拦截器,过滤器等等,其实也可以不用建;不建的话,配置类那些比较散乱分布,我觉得还是集中比较好,就建了个core包
修改生成器生成的模板代码命名
有时候,模板代码的命名不太符合需求,比如我这里statistics(统计)模块下有个统计IP的子模块,模板代码用了驼峰命名把IP命名为Ip,我觉得还是都用大写字母表示IP好
4. 单元测试
基于以上,若是单模块项目或者多模块项目无同名Class的话,项目的后端环境搭建就基本到此完成了,写个单元测试;不会写单元测试的请参考嘟嘟独立博客的这篇文章:Spring Boot干货系列:(十二)Spring Boot使用单元测试。
新建包路径
在test
文件下新建要进行单元测试的类的相对应包路径;
这里举例,要进行单元测试的类是ContentMapper
ContentMapper单元测试
package top.sinch.blog.article.mapper;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import top.sinch.blog.BlogApplication;
import top.sinch.blog.article.entity.Content;
import javax.annotation.Resource;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.*;
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class ContentMapperTest {
// @Autowired(required = false)//允许依赖对象为null
@Resource
private ContentMapper contentMapper;
@Test
public void insert() {
Content content = new Content();
content.setDetail("23333333333333333333333333333333");
// content.setArticleInfoId(Integer.toUnsignedLong(1));
int result = contentMapper.insert(content);
Assert.assertThat(result, equalTo(1 ));
}
}
单元测试结果
test passed:测试通过
但是请注意,我这里强调的是单模块项目或者多模块项目无同名Class,而我的这个个人博客项目是多模块且不同模块下有同名Class的,所以您会发现单元测试会失败,报以下错误或者类似错误
org.springframework.beans.factory.BeanDefinitionStoreException: Failed to parse configuration class [top.sinch.blog.BlogApplication]; nested exception is org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'infoController' for bean class [top.sinch.blog.admin.controller.InfoController] conflicts with existing, non-compatible bean definition of same name and class [top.sinch.blog.about.controller.InfoController]
这个坑我就不放在踩坑纪部分说了,在这里解释;
出现这个错误的原因:
是因为本项目中不同模块下都有一个Info实体类,相对应的就都有InfoController,InfoServiceImpl,InfoMapper;若不为不同模块下的InfoController,InfoServiceImpl,InfoMapper取别名的话,这些InfoController等在注册进Spring容器时就会失败;因为Spring默认是用类名进行Bean注册的,而它们类名又都是InfoController等,自然会失败
这里举例about模块和admin模块,以便于理解
解决方法
为不同模块下的InfoController,InfoServiceImpl,InfoMapper取别名;
同样举例,about模块和admin模块。
然后再进行单元测试,完美通过!Perfect!!
5. 设计RESTful API
初次尝试在项目中设计RESTful API;这里鉴于篇幅原因,仅给出一个Demo,其余都是类似的;不过在Demo之前先讲讲RESTful API设计规范,规范主要参考阮一峰的这篇RESTful API 最佳实践
RESTful API 设计规范
阮一峰的RESTful API 最佳实践推荐使用复数URL,但是我根据项目情况并没有采用复数URL;
这里以article模块下的InfoController为例子
-
GET /article/info/{id}
获取单个文章信息 -
GET /article/info/all
获取所有文章信息 -
POST /article/info
新增一篇文章信息 -
PUT /article/info
更新一篇文章信息 -
DELETE /article/info/{id}
删除一篇文章信息
swagger2配置类
package top.sinch.blog.core.config;
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;
/**
* Swagger2配置类
*
* @Author sincH
* @Date 2018/11/14
*/
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("top.sinch.blog"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("sincH个人博客RESTful APIs")
.description("sincH个人博客API文档")
.contact(new Contact("sincH","https://www.jianshu.com/u/64e4e9db42c9","[email protected]"))
.version("1.0")
.build();
}
}
article模块下的InfoController
package top.sinch.blog.article.controller;
import com.google.gson.Gson;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import top.sinch.blog.article.entity.Info;
/**
*
* 前端控制器 文章信息
*
*
* @author sincH
* @since 2018-11-20
*/
@RestController("articleInfoController")
@RequestMapping("/article/info")
public class InfoController {
@ApiOperation("获取单个文章信息")
@GetMapping("/{id}")
public String get(@PathVariable("id") String id) {
Info articleInfo = new Info();
articleInfo.setAuthor("sincH");
articleInfo.setTitle("Test");
return new Gson().toJson(articleInfo);
}
@ApiOperation("获取所有文章信息")
@GetMapping("/all")
public String list(){
return "list successful";
}
@ApiOperation("新增一篇文章信息")
@PostMapping("")
public String save(){
return "save successful";
}
@ApiOperation("更新一篇文章信息")
@PutMapping("")
public String update(){
return "update successful";
}
@ApiOperation("删除一篇文章信息")
@DeleteMapping("/{id}")
public String remove(@PathVariable("id") String id){
return "remove successful";
}
}
除了以上步骤外,肯定要先导入swagger相关依赖包;
有认真看的看官,应该知道我已经先在pom.xml中导入了swagger相关依赖包
所以设计完RESTful API,直接在浏览器输入地址:
http://localhost:8080/blog/swagger-ui.html
若您更改了端口和项目名,请相对应更改地址;不然肯定访问不了(感觉说这话有点多余)
对某个RESTful API进行测试
踩坑纪
纪一 :命名规范问题
命名规范是我这么久都没更新文章的主要原因;我因为命名问题卡在了数据库设计阶段超级久;每天上班前的一小时,下班后的两小时都在想数据库表名,字段名,类名等怎么命名会对后面的开发比较友好;
没错!您也不用怀疑;就是命名问题卡了我许久,也不是什么技术大问题;因为我对命名有些许强迫症,命名不好就会推翻重来;不过倘若是别人一开始就设计好的命名,我其实也不会太纠结。关键这是我自己设计的就会十分纠结。
光是最初的数据库表名设计,我就有好几种命名;
以文章信息表为例,有以下命名
- article
- article_base
- article_article_info
- article_info
命名规范参考了阿里巴巴Java开发手册 1.4.0,以及一些网上资料;
接下来逐个分析为什么我使用这个命名,以及为什么抛弃这个命名;可能比较长,希望您有耐心看;可能有些人觉得没必要这么纠结命名,那么您可以跳过这一纪,看下一个坑的纪录。
-
article
- 为什么使用这个命名
article这命名其实不用过多解释,当你想设计文章表时,那么article肯定是你的首选命名,可能您看到过essay这个命名,当然也是可以的,但是推荐使用article - 为什么抛弃这个命名
根据阿里巴巴Java开发手册 1.4.0中的MySQL数据库(一)建表规约第8条
- 【强制】varchar 是可变长字符串,不预先分配存储空间,长度不要超过5000,如果存储长度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。
那么article(文章)表中肯定有个content(内容)字段,而content字段长度不确定,有可能会超过5000,所以我决定将content独立成表,命名为article_content;
那么article,article_content不太统一,要么都是“XXX_XXX”格式,要么就都不是比较好;而且根据阿里巴巴Java开发手册 1.4.0中的MySQL数据库(一)建表规约第10条- 【推荐】表的命名最好是加上“业务名称_表的作用”。 正例:alipay_task / force_project / trade_config
因此将article重命名为article_base
- 为什么使用这个命名
-
article_base
- 为什么使用这个命名
因为抛弃了article命名,所以就改成了article_base;可能有人会说为什么不用article_info呢,因为我觉得文章表直接命名article就行了,如果命名article_info有点多此一举,但是又要分表,所以就想到了article_base,直译为文章基础表,以后要是想扩展表,可以命名为article_extra之类的,妙啊 - 为什么抛弃这个命名
因为使用MyBatis-Plus的代码生成器时,倘若设置了表名前缀且表名前缀与数据库表名前缀一致,那么代码生成器生成的实体类名为表名前缀后的名。
举个栗子:
也就是说倘若代码生成器里设置了表名前缀为"article_",而数据库表名为"article_base",则实际由代码生成器生成的实体类名为Base。
那么实体类名为Base,不如Info来得更直观友好,因此抛弃article_base命名
- 为什么使用这个命名
//省略前面的配置
//模块名
pc.setModuleName("article");
//省略中间的配置
//表名(数据库中存在的表);多表传数组
strategy.setInclude(new String[]{
"article_base"
});
/**
*设置表名前缀;
*此表名前缀若与数据库表名前缀一致,则自动创建实体类时,实体类名匹配数据库表名前缀后面的名;
*栗子:数据库表名为 article_base ,当strategy.setTablePrefix("article_")时,则实体类名为Base;
*若不设置表名前缀,则实体类名匹配数据库表名
*/
strategy.setTablePrefix(pc.getModuleName() + "_");
- article_article_info
- 为什么使用这个命名
前面分析article_base这个命名时说到了,代码生成器的表名前缀若与数据库的表名前缀一致时,则生成的实体类名为表名前缀后的名;
也就是说代码生成器的表名前缀为"article_",数据库表名为"article_info",则实际由代码生成器生成的实体类名为Info。而单单Info这个名并不够直观,我希望它是ArticleInfo,因此article_info前面再加了article,这样命名就是article_article_info,然后代码生成器生成的实体类名就是ArticleInfo,不过这样看着就很重复了。 - 为什么抛弃这个命名
抛弃的原因很简单,因为article_article_info这样的命名看着很重复,很不舒服,心里难受,就抛弃了。。
- 为什么使用这个命名
- article_info
- 为什么最终使用这个命名
使用这个命名的原因也很简单,在抛弃了article,article_base,article_article_info这几个命名后,权衡利弊,综合考虑,还是觉得article_info这个命名好
- 为什么最终使用这个命名
命名规范这个坑我写得还挺长,我觉得能看完命名规范这个坑的都是勇者,我为你们鼓掌,毕竟看我在命名规范这里瞎BB也不容易。
纪二:数据库建立索引
在搞定命名规范问题后,又要面对索引问题。
在进行数次网上冲浪后,对怎么建立索引进行了简单总结。
- 【推荐】对出现在SQL语句的
WHERE
后面的字段建立索引 - 【推荐】对需要精确查询("=","IN"和"<=>"查询)的字段建立HASH索引
- 【推荐】对需要范围查询的字段建立BTREE索引
好!总结完毕!可能有些人觉得太简单了,的确,相对于其他关于索引的文章,我总结的是很简单,所以仅仅只能适用于简单的情况,至于什么是简单的情况,请各位自己思考;
关于我参考的网上冲浪资料在文末的参考文章给出
纪三:模型同步到数据库中出现问题
在Navicat Premuim中设计模型后同步到数据库中出现问题
分析
因为我在某些字段上加了 HASH(哈希)索引,而 HASH索引是不支持排序的;而从模型同步到数据库时,建立了索引的字段默认用了ASC排序(升序排列),就算你建立索引时不设置排序方式,同步时也会默认用ASC排序,这有点坑。。而HASH索引不支持排序,自然同步会失败。
-
模型中并没有设置排序方式
-
将模型导出为SQL文件,发现默认用了ASC排序
解决方法
将模型导出为SQL文件,再将SQL文件中的ASC去除,最后在Navicat Premium中运行SQL文件,完美解决!
后记
。。终于,我!写到了后记;实不相瞒,这篇文章,我也写了好久,期间历经修修改改,推翻重来之类的。下面就是我瞎BB的时间了;it's my show time!!
Q:别人的项目后端环境都搭建很快的,为什么你用了这么久!!
A:你也说是别人了,我就是我,是与众不同的我;好吧,开玩笑的,主要是两点原因;
其一:命名规范问题;没错,就是我在踩坑纪中也提到的命名规范问题。以前我搭建项目后端环境也是很快的,好吧。不过,那时我并没有考虑太多命名规范什么的,导致我后面再看自己的代码时,就觉得 这是谁写的代码,什么鬼啊!
其二:数据库索引;没错,同样是在踩坑纪中也提到的索引问题;呀,怎么去建立索引,我也思考了好久。
以上便是我后端环境搭建这么久的原因。其实并不是什么技术上的大问题Q:就算是那两点原因,那也不用久啊,从你项目需求分析开始,到现在两个多月了都,黄花菜都凉了。
A:行的吧,的确也是不用那么久。还有原因就是我在实习。。可能你想说这不是理由,好吧,不是就不是,但我也利用了每天上班前的一小时,下班后的两小时在做这个项目了。我在努力了,为什么还是这么慢呢,究其原因,我还是知道的,就是目前的我还是太弱鸡了,不够强啊。嗯,我不能停滞不前。
再有就是时不时懒癌发作。。就没有然后了
综上,弱鸡+间歇性懒癌发作=久Q:拖了这么久,对甲方爸爸有没有什么影响?
A:独秀同学,请你坐下,一看你就没认真看我的文章,我这是个人博客项目,甲方爸爸不是其他人,就是我自己,至于对我有没有影响,请看下一个QA部分。
如果硬要说甲方爸爸是其他人的话,那这个其他人就是关注我这个博客项目的人;我看到评论区有人催更的时候,真是有点受宠若惊。
当然要是有真.甲方爸爸或者是商业项目的话,我肯定不会拖这么久了。要是真拖这么久的话,你这是在玩火啊,少年Q:拖了这么久,对你有没有什么影响?
A:一开始我以为不会有什么影响,直到开始写这篇文章时,才意识到影响太大了。
消极影响:开始写这篇文章时,肯定要梳理一下思路,然后我就发现隔了太久了,有些思路就不知道是怎么回事了;有些bug,踩过的坑也因为太久了就忘记了;虽然平时在遇到bug时,会进行速记,但时间久了,就不知道这速记记的是什么鬼。所以影响真的是很大,我以后应该不会拖这么久了。
积极影响:说实话,我自己也没想到让我卡壳这么久的并不是什么技术大问题,而是命名之类的设计问题。
突然想起很久之前,一位已经在外工作的师兄说的一句话,你不能仅仅是Coder,还要是Designer;感觉自己已经朝着Designer往前了一步
终于。。快要写完了,这篇文章写得真是又长又久,能看到这里的,我真心觉得你牛逼
最后说一句,因为文章我写的又长又久,也不想回头校对,所以要是文章中有出错的地方或者您有其他意见,请在评论区中赐教
参考文章
因为隔了太久了,而且这次参考的很多,有些参考文章就忘记收藏了,下面给出我收藏记的:
阿里巴巴Java开发手册 1.4.0
MySQL命名、设计及使用规范
数据库设计中的命名规范
字段类型与合理的选择字段类型
MySQL索引类型 btree索引和hash索引的区别
Mybatis-Plus代码生成器
Spring Boot干货系列:(十二)Spring Boot使用单元测试
Spring Boot中使用Swagger2构建强大的RESTful API文档
RESTful API 最佳实践
项目地址
Github:h-blog
小结
最后做个小结吧。。本来不用做的。拖得太久了。。我也很绝望啊
希望自己能吸取这次教训吧,下次不要拖这么久了。
再再说一次,要是文中有错误或者有其他意见,请私信我或者评论区留言