Java后端系统学习路线--白卷项目优化(一)(代码可运行)

133

白卷项目优化(一),码云仓库地址:gitee.com/qinstudy/wj

1、引入Swagger在线文档

大榜:前面我们讨论了白卷项目的复盘以及后续的10个优化事项,接下来我们对前3个优化事项(Swagger在线文档、MyBatis做持久层、分页查询),进行讨论和分析。

小汪:好啊,期待已久,我们一起讨论,共同学习进步!

大榜:哈哈哈,好滴了。第1个优化点是引入Swagger在线文档,Swagger可以帮助我们生成web层的API接口,方便前端、后端进行联调测试。

小汪:你的意思是有了Swagger后,前端、后端开发联调的时候,前端就可以对着在线文档,就可以知道后端有哪些请求接口以及入参是什么,感觉很香啊!我之前和前端进行联调时,一般是编写一个Word文档,然后把请求接口和入参 复制粘贴到Word文档中,然后拷贝给前端人员,还得给前端小子解释清楚入参的每个参数表示什么意思,很费时费力啊。

大榜:是啊,没有Swagger在线文档时,我也是这么搞的,不仅效率低,还容易出错。你看,Swagger在线文档,是这样的:

image.png

小汪:Swagger可以把所有Controller层的接口,都标记出来,然后生成在线文档,是一个提高前后端开发联调的好工具啊。榜哥,项目如何引入Swagger怎呢?

大榜:如何引入Swagger,其实百度上有不少教程,不过要踩一些坑。说实在,搭建好Swagger后,刚开始可能不习惯,但我们要多使用,熟能生巧嘛。

小汪:别逼逼了,你快说嘛。

大榜:现在大家开发都是使用Spring Boot作为脚手架,所以我们讨论如何在Spring Boot项目中引入Swagger?一般分为3步:引入pom依赖;创建Swagger配置类;在拦截器中放行Swagger相关的资源。具体如下:

1)引入pom依赖;

<!--        引入swagger,用来自动生成同步的API在线文档-->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.7.0</version>
</dependency><dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.7.0</version>
</dependency>

2)创建Swagger配置类,并创建在线文档相关的bean对象;

package com.bang.wj.config;
​
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
​
/**
 * swagger文档的访问地址:http://localhost:8443/swagger-ui.html
 *                    http://localhost:18443/swagger-ui.html
 *
 * 后续,如果有需求,将swagger API在线文档,导出为doc等格式,可以使用第三方的工具进行导出
 *
 * @author qinxubang
 * @Date 2022/5/27 11:53
 */
@Configuration
@EnableSwagger2
@ConditionalOnProperty(prefix = "swagger", name = "base-package")
public class SwaggerConfig {
    @Value("${swagger.base-package}")
    private String basePackage;
​
    @Value("${swagger.title}")
    private String title;
​
    @Value("${swagger.version}")
    private String version;
​
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage(basePackage))
                .paths(PathSelectors.any())
                .build();
    }
​
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title(title)
                .version(version)
                .build();
    }
​
}

配置文件中,添加Swagger相关的配置项:

# swagger配置,用来自动生成API在线文档
swagger:
  base-package: com.bang.wj.controller
  title: 白卷-图书和博客管理系统
  version: 1.0

3)如果项目中没有拦截器,则无需配置此项;若项目中有拦截器,则必须在拦截器中放行Swagger相关的资源,如"/swagger-resources/", "/webjars/", "/swagger-ui.html"等资源,否则前端访问Swagger在线文档时,会被拦截器拦截,导致无法访问Swagger文档。

 // 注入登录的拦截器到Bean
    @Bean
    public LoginInterceptor getLoginInterceptor() {
        return new LoginInterceptor();
    } 
​
@Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 若没有取消对“/index.html”的拦截,则重定向到“/index.html”会再次触发LoginInterceptor拦截器,
        // 从而再次重定向到“/login”,引发重定向次数过多的问题。
//        registry.addInterceptor(getLoginInterceptor()).addPathPatterns("/**");
​
        registry.addInterceptor(getLoginInterceptor()).addPathPatterns("/**").
                excludePathPatterns("/index.html", "/swagger-resources/**", "/webjars/**", "/swagger-ui.html",
                        "/error/**", "/error", "/api/login", "/api/logout", "/api/register");
    }

小汪:我懂了,如果项目中有自定义的拦截器,我们需要对Swagger的资源进行放行嘛,这个不难。那后端引入Swagger后,重启项目,浏览器如何访问到Swagger在线文档呢?

大榜:当我们后端成功引入Swagger后,只需要在项目的访问地址后面加上”/swagger-ui.html“,就可以访问Swagger在线文档了。你看,我的项目使用本机的IP地址localhost,端口号是8443,所以我在浏览器中输入的url如下:

http://localhost:8443/swagger-ui.html

小汪:感觉很好用的样子啊。Swagger在线文档中既然可以展示请求url,那请求入参中各个参数都表示什么意思,应该也可以在Swagger中展示出来把?我可不想每次都给前端解释每个参数表示什么意思。

大榜:请求入参中每个参数表示什么意思,就需要使用Swagger为我们提供的注解了。比如,用户注册接口,代码是下面这样:

@ApiOperation(value = "用户注册的请求")
@PostMapping("api/register")
@ResponseBody
public Result register(@RequestBody UserDto userDto) {...}

你看,用户注册接口中的UserDto类,代码是下面这样的:

package com.bang.wj.entity.dto;
​
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.ToString;
​
/**
 * @author qinxubang
 * @Date 2021/4/30 14:59
 */
@ApiModel("用户信息的传输对象")
@Data
@ToString
public class UserDto {
    @ApiModelProperty("主键标识")
    private int id;
​
    @ApiModelProperty(value = "用户名")
    private String username;
​
    @ApiModelProperty("密码")
    private String password;
}

UserDto类中,我们使用@ApiModel注解来标记UserDto类是用来干什么的,使用@ApiModelProperty来标记实例变量表示什么意思,也就是你刚刚说的入参中的每个参数表示什么意思。 使用Swagger注解来标记请求接口后,在线文档中,用户注册接口的展示情况如下图所示:

image.png

小汪:哦哦,我懂了,Swagger在线文档中,可以清楚的看到,“/api/register”请求接口是表示用户注册,专门用来处理用户的注册;请求入参为UserDto,UserDto中有3个参数,分别是id、password、username,以及这3个参数表示的中文意思,我都可以看到。如果我是前端开发人员,拿到这个接口的Swagger在线文档,就可以传入接口请求url和请求入参,开发前端的注册功能了,效率提升了好几倍啊!

大榜:是啊,而且后端修改接口时,只需要同步修改接口上面的Swagger注解信息,就可以生成更新之后的Swagger在线文档了。

小汪:榜哥,你的意思是 如果后端更新了Swagger在线文档,给前端说一下,这样前端人员,就可以按照更新好的Swagger文档,直接修改前端功能就好了,再也不用更新修改Word文档,来回沟通扯皮了,太香了把!

2、引入MyBatis做数据库持久化层

大榜:讨论完了Swagger在线文档,接下来我们说下第二个优化事项:使用MyBatis做数据库持久化层。

小汪:白卷项目中是使用Spring Data JPA做持久化层,基本不用写SQL语句,它不香吗,为什么要修改为MyBatis呢?

大榜:为什么修改为MyBatis嘛,主要还是因为国内一般用MyBatis,写复杂的SQL语句很方便,如果使用JPA的话,需要遵循一定的约束,而且打印出来的SQL语句也不友好。所以我这边选择用MyBatis,感觉自己的理由有点牵强了啊。

小汪:没事儿,榜哥。多学门技术,没准以后多一个工作机会嘛。我记得MyBatis和Spring Boot整合,也很方便,开发起来应该也很香把。

大榜:Spring Boot项目中引入MyBatis,其实很简单,主要是下面3步:

1)引入pom依赖

<!--        引入mybatis场景启动器-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.4</version>
</dependency>

2)新增mybatis的配置项,配置sql脚本的xml文件的位置;

# MyBatis的配置项
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.bang.wj.entity.po
  configuration:
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

# 设置为debug模式,才会打印SQL语句
logging:
  level:
    com.bang.wj.mapper: debug

3)启动类中添加@MapperScan注解

package com.bang.wj;
​
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
​
@SpringBootApplication
@ServletComponentScan // 扫描Servlet相关的组件,加入到IOC容器
@MapperScan("com.bang.wj.mapper")
public class WjApplication {
    public static void main(String[] args) {
        SpringApplication.run(WjApplication.class, args);
    }
}

小汪:引入MyBatis后,我们如何使用它实现增删改查呢?

大榜:这个也不难,我实现MyBatis的查询功能,另外的增删改功能,你看完后,应该也能很快实现。MyBatis实现增删改查,主要分为2步:

1)首先创建BookMapper类,定义getBooks方法,查询书籍信息,代码如下:

package com.bang.wj.mapper;
​
import com.bang.wj.entity.PageParameter;
import com.bang.wj.entity.po.BookPo;
import org.apache.ibatis.annotations.Param;
​
import java.util.List;
​
/**
 * @author qinxubang
 * @Date 2022/5/27 11:31
 */public interface BookMapper {
    List<BookPo> getBooks(@Param("pageParameter") PageParameter pageParameter);
}

2)创建BookMapper对应的xml文件,在xml文件中编写查询SQL语句来查询书籍信息,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.bang.wj.mapper.BookMapper"><select id="getBooks" resultType="BookPo">
        select
            mb.id  AS bookId,
            mb.cover AS cover,
            mb.title AS title,
            mb.author AS author,
            mb.date AS date,
            mb.press AS press,
            mb.abs AS abs,
            ca.id AS categoryId,
            ca.name AS name
        from
            my_book mb
            LEFT JOIN category ca ON mb.cid = ca.id
        <where>
            <if test="pageParameter.startDate != null">
                AND date &gt;= #{pageParameter.startDate}
            </if>
            <if test="pageParameter.endDate != null">
                AND date &lt;= #{pageParameter.endDate}
            </if>
        </where>
        <if test="pageParameter.field != null and pageParameter.sort != null">
            ORDER BY ${pageParameter.field} ${pageParameter.sort}
        </if>
    </select></mapper>

这样,我们就实现了通过MyBatis,查询数据库中的书籍信息。

小汪:那如何查看自己编写的SQL是不是对的呢?

大榜:这个也很简单,引入MyBatis时,我们在配置文件中新增了打印SQL文件的配置项,如下:

# 设置为debug模式,才会打印SQL语句
logging:
  level:
    com.bang.wj.mapper: debug

有了上面的配置项后,当我们使用Swagger在线文档,发送查询书籍信息的请求时,控制台中会打印SQL语句,如下所示:

Preparing: SELECT count(0) FROM my_book mb LEFT JOIN category ca ON mb.cid = ca.id WHERE date >= ? AND date <= ?
Parameters: 2018-05-15 15:00:00(String), 2022-12-30 16:00:00(String)

小汪:哇哇,打印出SQL语句后,我就可以验证自己编写的SQL是不是对的,比JPA打印的SQL好看多了。

大榜:其实,MyBatis除了select查询标签,还有insert标签,用来实现插入功能;还有foreach标签,用来实现批量插入,也就是多条SQL语句,拼接成一条SQL语句,然后插入到数据库中。

小汪:批量插入,我知道啊,可以减少与数据库交互的次数,降低资源的消耗。榜哥,要不讲讲批量插入把?

大榜:MyBatis实现批量插入,现在没有需求场景,放到以后再讲也不迟。小伙子,要不我们一起看看你最关心的分页查询把。

3、后端分页查询

小汪:好啊。我感觉自己只会使用别人写好的分页组件,来完成分页查询,自己并没有深入研究过分页查询的本质,我们一起讨论下分页查询。

大榜:分页查询的意思,就是将数据库中的记录查询出来,然后分成一页一页的返回给前端,最后前端进行展示。

小汪:你这么一说,我觉得分页查询的使用场景,应该是记录数特别多的情况,比如数据库中的书籍有几百万册,分页查询就排上用场了。反过来说,如果数据库中的书籍就十来本,就没必要使用分页查询了,我们直接从数据库中做全量查询,也能够满足需求。

大榜:从数据库中全量查询,是通过如下的SQL语句实现:

SELECT * from book;

那使用分页查询,SQL语句是什么样的呢?

小汪:如果要分页,应该要使用limit关键字,我猜SQL语句如下:

SELECT * from book LIMIT 0,10;

上面的语句,是查询10本数据。

大榜:是的,本质上就是limit关键字,来实现分页查询。你看,如果我们设定每页记录数pageSize为10条,查询第1页page,SQL语句就是你上面写的。

那如果我们设定pageSize为10,查询第2页,SQL语句就是下面这样:

SELECT * from book LIMIT 10,20;

小汪:我懂了,后端可以把每页记录数pageSize、查询页码page,作为变量,由前端传入,然后 后端根据前端的分页参数,来查询到底是第几页的结果。

大榜:是的。不过每页记录数pageSize、查询页码page仅仅是分页参数的一部分,有些需求场景,需要按照哪个字段来排列,而且按照降序排列,面向这样的需求,我们就需要使用field字段、sort字段作为分页参数了。其中,field字段是按照哪个字段来排序,sort字段是表示升序还是降序。

小汪:field字段是按照哪个字段来排序,也是由前端传入的把,前端可以自由选择使用某个字段来实现排序,这样的话,即使老板给我说,按照 图书出版日期 来排序,后端分页查询也不需要修改,那我就可以坐着喝茶了,等着前端开发完成后联调。

大榜:哈哈哈,是啊。后端接口接收前端的分页参数,然后根据分页参数(第几页,每页记录数、按哪个字段排序、升序还是降序),查询指定的书籍记录,最后将指定的书籍记录 返回给前端就可以了。

小汪:只返回指定的书籍记录给前端,应该远远不够把,我记得还需要返回总记录数、总页数的啊。

大榜:你说得对,还好你提醒了我,后端还需要返回总记录数和总页数,返回给前端的实体对象PageData,代码是这样的:

package com.bang.wj.entity;
​
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
​
import java.util.List;
​
/**
 * 分页查询返回的封装对象
 * @param <T>
 */
@Data
@ApiModel("分页信息+数据对象")
public class PageData<T> {
​
    @ApiModelProperty(value="页码,第几页")
    private int page;
    
    @ApiModelProperty(value="总页数")
    private int totalPage;
    
    @ApiModelProperty(value="页长")
    private int pageSize;
​
    @ApiModelProperty(value="排序字段名称")
    private String field;
​
    @ApiModelProperty(value="排序类型")
    private String sort;
​
    @ApiModelProperty(value="总记录数")
    private Long totalNum;
​
    @ApiModelProperty(value="数据对象")
    private List<T> dataList;
​
    public PageData(PageParameter pageParameter){
        this.page = pageParameter.getPage();
        this.pageSize = pageParameter.getPageSize();
        this.field = pageParameter.getField();
        this.sort = pageParameter.getSort();
    }
}

小汪:那后端接口如何接收前端的分页参数呢?

大榜:这个也不难,主要分为3步。

1)引入分页组件的pom依赖;

<!--        引入分页查询的插件-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.3.1</version>
</dependency>

2)编写后端接口,接收前端的分页参数,调用分页组件进行分页查询。

其中,后端接收前端的分页参数,实体类PageParameter是下面这样的:

package com.bang.wj.entity;
​
​
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
​
import javax.servlet.http.HttpServletRequest;
​
/**
 * 分页参数
 * */
@Data
public class PageParameter {
​
    private int page;       // 第几页
    private int pageSize;   // 每页的最大数量
    private int type;
    private String name;
    private String startDate;
    private String endDate;
    private String field;
    private String sort;
​
    public PageParameter(){}
​
    public PageParameter(HttpServletRequest req) {
        // 当前端传递的page、pageSize、type为空时,使用如下的默认值
        page = Integer.parseInt(req.getParameter("page") != null ? req.getParameter("page") : "1");
        pageSize = Integer.parseInt(req.getParameter("pageSize") != null ? req.getParameter("pageSize") : "128");
        type = Integer.parseInt(req.getParameter("type") != null ? req.getParameter("type") : "-1");
        name = StringUtils.isEmpty(req.getParameter("name")) ? null : req.getParameter("name");
        startDate = StringUtils.isEmpty(req.getParameter("startDate")) ? null : req.getParameter("startDate");
        endDate = StringUtils.isEmpty(req.getParameter("endDate")) ? null : req.getParameter("endDate");
        field = StringUtils.isEmpty(req.getParameter("field")) ? null : req.getParameter("field");
        sort = StringUtils.isEmpty(req.getParameter("sort")) ? null : req.getParameter("sort");
    }
​
}

然后,后端调用分页组件进行分页查询,代码是下面这样的:

@ApiOperation(value = "分页查询书籍信息")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "page",value = "页数(第几页)",defaultValue = "1",paramType = "query",dataType = "int"),
            @ApiImplicitParam(name = "pageSize",value = "页长",defaultValue = "5",paramType = "query",dataType = "int"),
            @ApiImplicitParam(name = "startDate",value = "开始时间",defaultValue = "2018-05-15 15:00:00",paramType = "query",dataType = "String"),
            @ApiImplicitParam(name = "endDate",value = "结束时间",defaultValue = "2022-12-30 16:00:00",paramType = "query",dataType = "String"),
            @ApiImplicitParam(name = "field",value = "排序字段名",defaultValue = "bookId",paramType = "query",dataType = "String"),
            @ApiImplicitParam(name = "sort",value = "排序方式",defaultValue = "DESC",paramType = "query",dataType = "String")
    })
    @GetMapping(value = "api/books/my", produces={"application/json;charset=UTF-8"})
    public PageData<Object> getBooks(HttpServletRequest request) {
        List<Object> list = null;
        PageParameter pageParameter = new PageParameter(request);
        Page<Object> page= PageHelper.startPage(pageParameter.getPage(), pageParameter.getPageSize());
​
        if (pageParameter.getPageSize() == 5) {
            throw new IllegalStateException("每页记录数为5,太小!");
        } else if (pageParameter.getPageSize() == 20) {
            throw new NullPointerException("每页记录数为20,太大!");
        }
        list = new ArrayList<>(bookService.myBatisList(pageParameter));
​
        PageInfo<Object> pageInfo = new PageInfo<>(page.getResult());
        PageData<Object> pageData = new PageData<>(pageParameter);
        pageData.setDataList(list);
        pageData.setTotalPage(pageInfo.getPages());
        pageData.setTotalNum(pageInfo.getTotal());
        return pageData;
    }

你看,getBooks方法中使用了Swagger注解,设置了默认的分页参数;接着我们调用了分页组件的PageHelper.startPage方法,来得到分页查询结果,查询结果中包含了查询的指定记录、总记录数、总页码;然后将分页查询结果封装成PageData对象,返回给前端。后端分页查询的结果如下所示:

{
  "code": "1",
  "msg": "成功888",
  "data": {
    "page": 1,
    "totalPage": 10,
    "pageSize": 2,
    "field": "bookId",
    "sort": "DESC",
    "totalNum": 20,
    "dataList": [
      {
        "bookId": 70,
        "cover": "http://localhost:8443/api/file/lq2oz6.png",
        "title": "像艺术家一样思考",
        "author": "[英] 威尔·贡培兹",
        "date": "2019-4",
        "press": "湖南美术出版社",
        "abs": "归纳成就艺术大师的 10 个关键词\n\n揭示大师们的创作秘辛\n\n凝聚 BBC 艺术频道主编威尔·贡培兹职业生涯的所见、所知、所想\n\n·\n\n威尔·贡培兹是你能遇到的最好的老师\n\n——《卫报》",
        "categoryId": 3,
        "name": "文化"
      },
      {
        "bookId": 69,
        "cover": "http://localhost:8443/api/file/t7m3cd.png",
        "title": "谋杀狄更斯",
        "author": "[美] 丹·西蒙斯 ",
        "date": "2019-4",
        "press": "上海文艺出版社",
        "abs": "“狄更斯的那场意外灾难发生在1865年6月9日,那列搭载他的成功、平静、理智、手稿与情妇的火车一路飞驰,迎向铁道上的裂隙,突然触目惊心地坠落了。”",
        "categoryId": 1,
        "name": "文学"
      }
    ]
  }
}

小汪:当数据库的书籍数量有几百万条时,这个分页查询就会很香啊。

而且,后端对分页参数中的field、sort字段进行了适配,SQL语句是下面这样的:

<if test="pageParameter.field != null and pageParameter.sort != null">
    ORDER BY ${pageParameter.field} ${pageParameter.sort}
</if>

有了上面的SQL语句,当前端传入的分页参数中field字段的值为“bookId”时,sort字段的值为“desc”时,后端会根据bookId进行排序,并按照降序排列。这样的话,即使老板给我说,按照 图书出版日期 排序一下,后端都不用修改代码,就可以适配按照图书出版日期排列,我就可以坐着喝茶了,哈哈哈,美滋滋!

大榜:小伙子,看来你已经对白卷项目的3个优化事项:Swagger在线文档、MyBatis做持久层、分页查询有了一定的理解,以后遇到类似的问题,应该就可以照葫芦画瓢了。

这3个优化事项对应的代码,我放在了码云仓库,你为团队引入Swagger在线文档、MyBatis和分页查询时,可以直接作为脚手架拿来使用,码云仓库地址:gitee.com/qinstudy/wj

4、总结

承接上篇博客中的白卷项目复盘,小汪和大榜讨论了前3个优化事项:Swagger在线文档、MyBatis做持久层、分页查询,并针对优化事项,设想了需求场景进行了优化,然后做了代码实战演示。