手把手撸一个wiki系统(1):后台架构

2021年6月6日19:18:44
评论
104 8872字

大家好,这里是凡蜕博客。这次准备写一个wiki系统,使用常用流行的技术栈完成,也是对自己学习的一个总结,这里做一个记录。
代码已经上传,地址:https://github.com/Bronya0/my-wiki
可以重点关注每次的commit提交。

创建springboot工程

将spring初始化地址换成https://start.aliyun.com/
spring.io容易出问题。

勾选web、lombok,完成。

关联git仓库

在github上创建仓库,然后按照github的提示将git代码在idea的终端里跑一遍,这样本地仓库就与github关联了。

上方vcs选择启动git,左侧打开提交,多选要上传的git的文件,右键点击添加到vcs。

  • 绿色:已经加入控制暂未提交;
  • 红色:未加入版本控制;
  • 蓝色,加入,已提交,有改动;
  • 白色:加入,已提交,无改动;
  • 灰色:版本控制已忽略文件。

使用logback日志

resources下创建logback-spring.xml,内容:https://blog.ysboke.cn/archives/476.html 中的配置1.

application.properties 中添加spring.profiles.active=dev。

HTTP client测试

根目录下创建http目录,创建test.http文件,写测试路径:

例如:GET http://localhost:8080/demo

集成热部署

pom.xml引入devtools依赖,idea设置——构建、执行、部署——编译器里勾选自动构建项目项目未运行时自动编译)

再按ctrl+shift+alt+/ ,点击注册表,勾选Compiler autoMake allow when app running。(项目运行时自动编译)

改动代码后点击构建工具即可。

创建数据库

名字:mywiki,utf8mb4(支持表情,完整的),utf8mb4_general_ci

sql脚本内容(字段用``包裹, 字符串用''包裹, 用()创建表):

drop table if exists `ebook`;
create table `ebook`
(
    `id`           bigint      not null comment 'id',
    `name`         varchar(50) not null comment '名称',
    `category1_id` bigint comment '分类1',
    `category2_id` bigint comment '分类2',
    `description`  varchar(200) comment '描述',
    `cover`        varchar(200) comment '封面',
    `doc_count`    int comment '文档数',
    `view_count`   int comment '阅读数',
    `vote_count`   int comment '点赞数',
    primary key (`id`)
)   engine=innodb default charset=utf8mb4 comment='电子书';

insert into `ebook` (id,name,description) values (1,'SpringBoot教程','一个约定大于配置的方便好用的框架');
insert into `ebook` (id,name,description) values (2,'vue教程','前后端分离时代流行的前端框架');
insert into `ebook` (id,name,description) values (3,'python教程','大众化而应用广泛的编程语言');
insert into `ebook` (id,name,description) values (4,'mysql教程','最流行的开源数据库管理系统');
insert into `ebook` (id,name,description) values (5,'IDEA教程','Java开发的首选集成开发环境');

集成持久层框架MyBatis

引入依赖:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.22</version>
</dependency>

配置数据源:

spring.datasource.url=jdbc:mysql://localhost:3306/mywiki?characterEncoding=UTF8&&autoReconnect=true&&serverTimezone=Asia/Shanghai
spring.datasource.username=用户名
spring.datasource.password=密码
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

controller同级创建dao层,用于映射数据库表的字段,类名与表名一致

创建mapper层,编写接口。

resource下创建mapping层,用于编写sql的xml文件,文件名与mapper接口一致。

启动类里增加注解@MapperScan(""),填写mapper(接口层)路径。

在spring配置文件里指明xml脚本的位置:mybatis.mapper-locations=classpath:/mapping/**/*.xml

创建service层,写逻辑代码。类文件应标识注解@service,将类作为bean交给spring容器。

注入关系:mapping - > mapper -> service -> controller

集成mybatis-generator

添加依赖,修改底部配置文件的路径(mysql链接版本要与上面的一致, 路径修改):

<plugin>
    <groupId>org.mybatis.generator</groupId>
    <artifactId>mybatis-generator-maven-plugin</artifactId>
    <version>1.3.7</version>
    <dependencies>
        <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>1.3.7</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.22</version>
        </dependency>
    </dependencies>
    <executions>
        <execution>
            <id>mybatis-generator</id>
            <phase>package</phase>
            <goals>
                <goal>generate</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <!--允许移动生成的文件-->
        <verbose>true</verbose>
        <!--允许自动覆盖文件,开发环境不能加,会覆盖别人写的-->
    <!--    <overwrite>true</overwrite>  -->
        <!--mybatis-generator配置文件路径-->
        <configurationFile>
            src/main/resources/config/mybatis-generator.xml
        </configurationFile>
    </configuration>
</plugin>

config目录下创建mybatis-generator.xml,内容为(修改路径和表, 驱动名, 时区都要对):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <!--
    context:生成一组对象的环境
    id:必选,上下文id,用于在生成错误时提示
    defaultModelType:指定生成对象的样式
        1,conditional:类似hierarchical;
        2,flat:所有内容(主键,blob)等全部生成在一个对象中;
        3,hierarchical:主键生成一个XXKey对象(key class),Blob等单独生成一个对象,其他简单属性在一个对象中(record class)
    targetRuntime:
        1,MyBatis3:默认的值,生成基于MyBatis3.x以上版本的内容,包括XXXBySample;
        2,MyBatis3Simple:类似MyBatis3,只是不生成XXXBySample;
    introspectedColumnImpl:类全限定名,用于扩展MBG
    -->
    <context id="my" targetRuntime="MyBatis3">
        <!--1. value="true"去掉注释,如果不去掉注释的时候,
                第二次生成的时候不会再后面追加内容,但是去掉注释的时候,
                在第二次以后需要生成文件的时候先把它删掉,再生成,
                否则就会在原来的文件后面追加内容
        -->

        <!--2.加载连接数据库信息  -->
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://127.0.0.1:3306/mywiki?serverTimezone=Asia/Shanghai"
                        userId="root"
                        password="密码" />

        <!--3.生成Model类的包名和位置 -->
        <!--targetProject=  指定包所在的位置,只需要到src/main/java  -->
        <javaModelGenerator targetPackage="com.example.mywiki.domain"
                            targetProject="src/main/java">
            <!--如果包不存在,创建一个  -->
            <property name="enableSubPackages" value="true"/>
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>

        <!--4.生成映射文件包名和位置 -->
        <!-- 指定包的位置,只需到上一级 -->
        <sqlMapGenerator targetPackage="mapping"
                         targetProject="src/main/resources">
            <!--如果包不存在创建一个  -->
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>

        <!--5.生成dao层(mapper层)的包和位置  -->
        <javaClientGenerator targetPackage="com.example.mywiki.mapper" type="XMLMAPPER"
                             targetProject="src/main/java">
            <!--如果包不存在创建一个  -->
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>

        <!--6. 我要生成的表, 可以一次性生成多张表.
        tableName是表名
        domainObjectName是实体名, 默认是表名的首字母大写
        -->
        <table tableName="ebook" domainObjectName="Ebook"
               enableCountByExample="false"
               enableUpdateByExample="false"
               enableDeleteByExample="false"
               enableSelectByExample="false"
               selectByExampleQueryId="false">
        </table>

    </context>
</generatorConfiguration>

右侧maven选项卡--插件--mybatis-generator里选择mybatis-generator:generate即可

封装统一的后端返回对象

将后端内容填入content,增加success、message属性,方便前端处理。

package com.example.mywiki.response;

/**
 * 通用的返回对象
 * Created by tangssst@qq.com on 2021/06/04
 */
public class CommonResp<T> {

    /**
     * 业务上的成功或失败
     */
    private boolean success = true;

    /**
     * 返回信息
     */
    private String message;

    /**
     * 返回泛型数据,自定义类型
     */
    private T content;

    public boolean getSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getContent() {
        return content;
    }

    public void setContent(T content) {
        this.content = content;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("ResponseDto{");
        sb.append("success=").append(success);
        sb.append(", message='").append(message).append('\'');
        sb.append(", content=").append(content);
        sb.append('}');
        return sb.toString();
    }
}

封装请求参数

见commit 1-0 ,此时controller里编写了search方法,service、mapper、mapping里增加了对应的代码,完成的基本的功能。但是随着业务的复杂,请求参数增多,不能陈列式的一个一个写上去,因此要进行封装。

创建request包,复制domain中的实体类ebook.java,改名为ebookReq.java,删除不需要的属性及对应的方法(例如仅保留id和name)

然后将controller层和service的方法参数改为EbookReq,service层再用.getId ,.getName的方式获取需要的属性。

请求参数无需完整的ebookReq对象,spring会自动映射对应的属性(例如传入name会自动映射到ebookReq的name属性)。

封装返回参数

这里有个问题,每次查询的结果都是完整的json,包含了所有的字段,但是有些字段是不应该返回给前端的,例如密码之类的,所以要对返回参数进行封装。

将ebook复制到response包下,改名为ebookResp,对其中的属性进行处理,如果不想返回就删掉对应的属性、方法。

service层将List<Ebook>转化为List<EbookResp>,使用迭代器将每个List里的Ebook的属性复制到EbookResp中,再放到一个新的ArrayList里。

public List<EbookResp> search(EbookReq req){
    List<Ebook> ebookList  = ebookMapper.selectByName("%" + req.getName() + "%");
    //将List<Ebook>转换为List<EbookResp>
    List<EbookResp> respList = new ArrayList<>();
    for (Ebook ebook : ebookList) {
        EbookResp ebookResp = new EbookResp();
        BeanUtils.copyProperties(ebook,ebookResp);
        respList.add(ebookResp);
    }
    return respList;
}

controller层将commResp的泛型改为List,以及别的改造。

@GetMapping("/search")
public CommonResp search(EbookReq req){
    CommonResp<List<EbookResp>> commonResp = new CommonResp<>();
    List<EbookResp> ebook = ebookService.search(req);
    commonResp.setContent(ebook);
    return commonResp;
}

代码见commit 1-2

封装CopyUtils

大家看到上面代码中的BeanUtils,每次复制都要写很多代码比较麻烦,所以这里对此进行提取封装。

创建utils包,创建CopyUtil类,代码如下:

/**
 * 属性复制工具类
 * Created by tangssst@qq.com on 2021/06/06
 */
public class CopyUtil {
    /**
     * 单体复制,参数为源对象和目标class,通过newInstance方法生成对象,并将源对象的属性复制到到新对象里,返回新对象。
     */
    public static <T> T copy(Object source, Class<T> clazz) {
        if (source == null) {
            return null;
        }
        T obj = null;
        try {
            obj = clazz.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        BeanUtils.copyProperties(source, obj);
        return obj;
    }

    /**
     * 列表复制,参数为源List和目标class,通过上面的单体复制将List里的每个对象复制到新对象里,都加进一个ArrayList里,并返回。
     */
    public static <T> List<T> copyList(List source, Class<T> clazz) {
        List<T> target = new ArrayList<>();
        if (!CollectionUtils.isEmpty(source)){
            for (Object c: source) {
                T obj = copy(c, clazz);
                target.add(obj);
            }
        }
        return target;
    }
}

单体复制就用copy,List复制就用copyList。

那么service就变成了了:

public List<EbookResp> search(EbookReq req){
    List<Ebook> ebookList  = ebookMapper.selectByName("%" + req.getName() + "%");
    //将List<Ebook>转换为List<EbookResp>
    List<EbookResp> respList = CopyUtil.copyList(ebookList, EbookResp.class);
    return respList;
}

对比一下原来的:

public List<EbookResp> search(EbookReq req){
    List<Ebook> ebookList  = ebookMapper.selectByName("%" + req.getName() + "%");
    //将List<Ebook>转换为List<EbookResp>
    List<EbookResp> respList = new ArrayList<>();
    for (Ebook ebook : ebookList) {
        EbookResp ebookResp = new EbookResp();
        BeanUtils.copyProperties(ebook,ebookResp);
        respList.add(ebookResp);
    }
    return respList;
}

见commit 1-3.

您可能感兴趣的文章

匿名

发表评论

匿名网友