项目实战一-SpringBoot+FreeMarker

本项目讲解前后端不分离,采用SpringBoot+FreeMarker技术

企业开发中常见的后台管理系统

  1. CRM(Customer Relationship Management): 客户关系管理,管理客户信息(个人信息、订单信息等)
  2. OA(Office Automation): 办公自动化,基于工作流概念,使企业内部人员方便快捷地共享信息,高效协同工作
  3. ERP(Enterprise Resource Planning): 企业资源计划,物资资源管理、人力资源管理、财务资源管理、信息资源管理等集成一体化

项目构建步骤

  1. 新建数据库,初始化表结构
  2. 构建po层
  3. 构建Mapper层
  4. 构建service层
  5. 构建controller层

初始化项目

  1. IDEA新建一个Module,命名为JiaKao
    1. 新建入口类

       //com.zh.jk.JiaKaoApplication
       @SpringBootApplication
       public class JiaKaoApplication {
           public static void main(String[] args) {
               SpringApplication.run(JiaKaoApplication.class, args);
           }
       }
      
    2. 添加项目常见依赖pom.xml

       <parent>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-parent</artifactId>
           <version>2.3.4.RELEASE</version>
       </parent>
          
       <properties>
           <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
           <druid.version>1.2.1</druid.version>
           <mybatis.verson>2.1.3</mybatis.verson>
       </properties>
          
       <dependencies>
          
           <dependency>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-starter-web</artifactId>
           </dependency>
           <!-- 数据库相关 -->
           <!-- mysql-->
           <dependency>
               <groupId>mysql</groupId>
               <artifactId>mysql-connector-java</artifactId>
           </dependency>
           <!-- 连接池-->
           <dependency>
               <groupId>com.alibaba</groupId>
               <artifactId>druid-spring-boot-starter</artifactId>
               <version>${druid.version}</version>
           </dependency>
           <!-- mybatis-->
           <dependency>
               <groupId>org.mybatis.spring.boot</groupId>
               <artifactId>mybatis-spring-boot-starter</artifactId>
               <version>${mybatis.verson}</version>
           </dependency>
          
           <!-- debug阶段使用,使用provided -->
           <dependency>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-configuration-processor</artifactId>
               <scope>provided</scope>
           </dependency>
           <dependency>
               <groupId>org.projectlombok</groupId>
               <artifactId>lombok</artifactId>
               <scope>provided</scope>
           </dependency>
           <!-- 热部署-->
           <dependency>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-devtools</artifactId>
               <scope>provided</scope>
           </dependency>
       </dependencies>
      
  2. 新建数据库,初始化表结构
    1. 将准备好的数据库表结构、测试数据文件夹bk_jaikao.sql、data.sql放入到项目目录sql中
    2. IDEA点击右侧database,展开,点击顶部的+ ->Schema->Name为jiakaox,priview第一句修改为create database点击OK
    3. 右击jiakaox->Run SQL Script…->选择项目中的bk_jaikao.sql语句那个文件夹即可->点击OK就会执行文件中的sql语句
    4. 然后同样的步骤,选择data.sql,会自动生成一些测试数据
    5. 在resources下新建application.yml

       spring:
         datasource:
           type: com.alibaba.druid.pool.DruidDataSource
           username: root
           password: root
           url: jdbc:mysql://localhost:3306/jiakaox?servieTimerzone=GMT%2B8
           driver-class-name: com.mysql.cj.jdbc.Driver
      
  3. 构建po层
    1. 新建com.zh.jk.pojo.po包名,具体原因详见【项目中的各种Object】
    2. 新建po的模型类:如果有n张表,那么就要手动建n个object,比较麻烦,可以使用MyBatis Generator,自动生成,详情见【MyBatis Generator】讲解
  4. 构建Mapper层
    1. 新建com.zh.jk.mapper包名,依次新建com.zh.jk.service、com.zh.jk.controller
    2. 在mapper下新建DictTypeMapper类

       //com.zh.jk.mapper
       public interface DictTypeMapper {
           @Select("SELECT id, name, value, intro FROM dict_type")
           List<DictType> list();
       }
      
  5. 构建service层

     //com.zh.jk.service
     public interface DictTypeService {
         List<DictType> list();
     }
        
     //com.zh.jk.service.impl
     @Service
     //开启事务
     @Transactional
     public class DictTypeServiceImpl implements DictTypeService {
         @Autowired
         private DictTypeMapper mapper;
        
         @Override
         @Transactional(readOnly = true)
         public List<DictType> list() {
             return mapper.list();
         }
     }
    
  6. 构建controller层

     //com.zh.jk.controller
     @Controller
     @RequestMapping("/dictTypes")
     public class DictTypeController {
         @Autowired
         private DictTypeService service;
        
         @GetMapping("/list")
         @ResponseBody
         public List <DictType> list() {
             return service.list();
         }
     }
    
  7. 运行项目,在浏览器输入:http://localhost:8080/dictTypes/list,能查询到数据

数据字典页面开发

  1. Freemarker集成,在【项目实战-扩展】中的【集成到SpringBoot】中已经讲解
  2. 接下来要做分页查询,因此需要条件:搜索关键字、每页数量、当前页码
  3. 在pojo下新建一个query文件夹,po专门跟表对应,query专门跟查询对应
    1. 新建一个基础类PageQuery,查询页面的必要条件page(当前页码)、size(每页大小)
    2. 再建一个KeyWordQuery,需要关键字
    3. 新建DictTypeQuery继承自KeyWordQuery

       //@Data
       public class PageQuery {
           private long page;
           private long size;
           //设置默认值
           public long getPage() {
               return Math.max(1,page);
           }
              
           public long getSize() {
               return (size < 1) ? DEFAULT_SIZE : size;
           }
       }
       //KeyWordQuery
       @EqualsAndHashCode(callSuper = true)
       @Data
       public class KeyWordQuery extends PageQuery{
           private String keyword;
       }
              
       //DictTypeQuery
       @EqualsAndHashCode(callSuper = true)
       @Data
       public class DictTypeQuery extends KeyWordQuery{
              
       }
      
    4. 改造控制器DictTypeController

       //查询新增了一个query
       @GetMapping("/list")
       public String list(DictTypeQuery query, Model model) {
           model.addAttribute("data",service.list(query));
           //默认情况下回自动添加前缀:classpath:/templates/,默认后置:.ftlh
           return "page/dictType";
       }
      
    5. service层也要新增query参数

       //DictTypeService
       public interface DictTypeService {
           List<DictType> list(DictTypeQuery query);
       }
              
       //DictTypeServiceImpl
       @Service
       //开启事务
       @Transactional
       public class DictTypeServiceImpl implements DictTypeService {
           @Autowired
           private DictTypeMapper mapper;
              
           @Override
           @Transactional(readOnly = true)
           public List<DictType> list(DictTypeQuery query) {
               return mapper.list();
           }
       }
      
    6. 注意: 在这里就需要改造了,使用功能mapper查询时,条件是动态的,因为keyword可能有也可能没有,可以用之前讲解的mybatis中动态sql来解决,但是仍然比较麻烦,此时就出现了My-Batis-Plus的作用

My-Batis-Plus实现条件查询

  1. 在【项目实战-扩展】-》【MyBatis-Plus简单使用】前提下
  2. service改造
    1. DictTypeService新增方法

       public interface DictTypeService extends IService<DictType> {
           List<DictType> list(DictTypeQuery query);
              
       }
      
    2. DictTypeServiceImpl改造

       //com.zh.jk.service.impl
       @Service
       //开启事务
       @Transactional
       public class DictTypeServiceImpl extends ServiceImpl<DictTypeMapper,DictType> implements DictTypeService {
           @Autowired
           private DictTypeMapper mapper;
              
           @Override
           @Transactional(readOnly = true)
           public List<DictType> list(DictTypeQuery query) {
               //专门用来做查询条件的
               //SELECT id,name,value,intro FROM dict_type WHERE (name LIKE ? OR value LIKE ? OR intro LIKE ?) ORDER BY id DESC
               LambdaQueryWrapper<DictType> wrapper = new LambdaQueryWrapper<>();
               String keyword = query.getKeyword();
               if(!StringUtils.isEmpty(keyword)){
                   wrapper.like(DictType::getName, keyword).or()
                           .like(DictType::getValue,keyword).or()
                           .like(DictType::getIntro,keyword);
               }
               //按照id降序排列
               //wrapper.orderByDesc(DictType::getId);
               //只查name,SELECT name FROM dict_type WHERE (name LIKE ? OR value LIKE ? OR intro LIKE ?) ORDER BY id DESC
               // wrapper.select(DictType::getName);
              
       //        return mapper.selectList(wrapper);
              
               //分页查询
               Page<DictType> page = new Page<>(query.getPage(),query.getSize());
               return mapper.selectPage(page,wrapper).getRecords();
           }
       }
      
  3. 新增配置类MyBatisPlusCfg(分页查询才会用)

     //com.zh.jk.cfg
     @Configuration
     @MapperScan("com.zh.jk.mapper")
     public class MyBatisPlusCfg {
         @Bean
         public MybatisPlusInterceptor mybatisPlusInterceptor(){
             MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
             PaginationInnerInterceptor innerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
             //当页数超过总页数时,自动跳回到第1页
             innerInterceptor.setOverflow(true);
             interceptor.addInnerInterceptor(innerInterceptor);
             return interceptor;
         }
        
         @Bean
         public ConfigurationCustomizer configurationCustomizer(){
             return configuration -> configuration.setUseDeprecatedExecutor(false);
         }
     }
    
  4. 浏览器输入:http://localhost:8080/dictTypes/list?keyword=32&page=2&size=5
    1. 实现关键字模糊搜索,分页查询,浏览器的参数会直接赋值到控制器DictTypeQuery类型的query参数对象的属性中

分页搜索实现

  1. 效果如下图

    图1

    1. 由上图分析可知,每次搜索完之后,需要将关键字、总页数、总条数、每页大小都要返回给客户端

       //分页查询
       Page<DictType> page = new Page<>(query.getPage(),query.getSize());
       //注意:page与iPage完全相等
       IPage<DictType> iPage = mapper.selectPage(page,wrapper);
       //符合条件的总共多少条
       iPage.getTotal();
       //总共多少页
       iPage.getPages();
       //当前页码
       iPage.getCurrent();
       //每页展示多少条
       iPage.getSize();
       //满足条件的当前页的数据
       iPage.getRecords();
       return iPage.getRecords;
      
    2. 如果将iPage返回给客户端,很显然不够,还需要将关键字返回给客户端,以后甚至还有其他更多条件,那么怎么办呢?

  2. 解决办法:不设置返回值,直接将查询结果存放在query对象中

    1. query中类改造成可存放查询结果

       //PageQuery
       @Data
       public class PageQuery<T> {
           private static final int DEFAULT_SIZE = 10;
           private long page;
           private long size;
              
           /***查询结果存放***/
           //总数
           private long count;
           //总页数
           private long pages;
           //符合条件的当前页数据
           private List<T> data;
              
              
           //设置默认值
           public long getPage() {
               return Math.max(1,page);
           }
              
           public long getSize() {
               return (size < 1) ? DEFAULT_SIZE : size;
           }
       }
              
       //KeyWordQuery
       @EqualsAndHashCode(callSuper = true)
       @Data
       public class KeyWordQuery<T> extends PageQuery<T>{
           private String keyword;
       }
       //DictTypeQuery
       @EqualsAndHashCode(callSuper = true)
       @Data
       public class DictTypeQuery extends KeyWordQuery<DictType> {}
      
    2. DictTypeServiceImpl查询方法改造如下:

       @Override
       @Transactional(readOnly = true)
       public void list(DictTypeQuery query) {
           LambdaQueryWrapper<DictType> wrapper = new LambdaQueryWrapper<>();
           String keyword = query.getKeyword();
           if(!StringUtils.isEmpty(keyword)){
               wrapper.like(DictType::getName, keyword).or()
                       .like(DictType::getValue,keyword).or()
                       .like(DictType::getIntro,keyword);
           }
           //分页查询
           Page<DictType> page = new Page<>(query.getPage(),query.getSize());
           //注意:page与iPage完全相等
           IPage<DictType> iPage = mapper.selectPage(page,wrapper);
           query.setData(iPage.getRecords());
           query.setCount(iPage.getTotal());
           query.setPages(iPage.getPages());
       }
      
    3. 控制器DictTypeController

       @GetMapping("/list")
       public String list(DictTypeQuery query, Model model) {
           service.list(query);
           model.addAttribute("query",query);
           //默认情况下回自动添加前缀:classpath:/templates/,默认后置:.ftlh
           return "page/dictType";
       }
      
    4. dictType.ftlh中使用

       <#list query.data as item>
       ...
       </#list>
              
       <input type="text" name="keyword" value="${query.keyword!}" class="form-control" placeholder="名称、值、简介">
       ...
        ,共${query.pages!}页,共${query.count}条
      

项目实战知识积累

如何打开别人项目?

  1. 首先看这个项目连接的数据库,将相应的数据库创建,或者能够连接上相应的数据库
    1. 一般项目中会有个项目用到的表的sql语句文件夹
    2. 点击右边菜单database=>+->DataSource->MySQL->先登录上MySQL
    3. 右击schemas->new->Schema->Name:数据库名称zhtest; preview中,第一句修改为create database,点击OK即可
    4. schemas下面会多出一个数据库zhtest
    5. 右击zhtest->Run SQL Script…->选择项目中的sql语句那个文件夹即可->点击OK就会执行文件中的sql语句
  2. 如果是springboot项目,直接运行入口类,如果是非springboot项目
    1. 如果是使用本地的Tomcat,则IDEA配置好,然后使用IDEA运行
    2. 如果是直接使用tomcat插件,tomcat7-maven-plugin,则直接使用Maven的生命周期运行项目tomcat7:run,

如何创建一个项目

  1. 底层建设(数据库):使用PowerDesigner来设计数据库,更加方便,具体使用看项目第一节视频:34分钟开始
  2. 后台管理端
  3. 客户端

如何设计项目中的字典?

  1. 何为字典?一个项目中有n个地方可以选择枚举值,比如:性别、职业等等,前端的情况是点击下拉可以选择各种枚举,这些枚举值在数据库中的存储就是字典
  2. 如何设计数据字典?
    1. 表中肯定会有枚举的名称、对应值,这两个字段,比如:nam为男,value为1
    2. 通常不会将n个字典建立n张表,通常的做法就是全部放一张表,或者2张表
  3. 数据字典统一放一张表

     表的主键    代表哪个类型  枚举名称    枚举值
     id          type        name      value
     1           sex         男         0
     2           sex         女         1
     3           job         教师       0
     ...   
    
  4. 数据字典统一放2张表
    1. 第一张表专门放字典的类型信息
    2. 第二张表专门放枚举的信息
    3. 第二张表外键引用第一张表
     //类型表1
     主键      名称   值         简介
     id      name    value     intro
     0       性别      sex      性别类型
     1       职业      job      职业类型
        
     //枚举表2
     主键  名称      值       序号(控制展示的顺序)   类型表1的主键    启用
     id    name     value    sn                      type_id     enabled     
    

项目中的各种Object

理论上一张表对应一个Object模型对象,但是通常这个对象需要封装改造,才能返回给客户端显示,因此针对这个流程,定制了一系列规范 在阿里巴巴的开发手册, 定义了各种Object,比如:PO、BO、D TO、VO、POJO、Query

PO (Persistent Object):持久化对象

  1. 属性跟数据库表的字段对应,一个PO对象对应数据库表的一条记录

BO (Business Object):业务对象

  1. 一类业务就会对应一个BO, 除了get\set方法以外,它会有很多业务操作,比如针对自身数据进行计算的方法
  2. 例子1
    1. P01是教育经历,PO2是工作经历,PO3是项目经验。 BO是简历,包含PO1~P03
  3. 例子2
    1. PO1是交易记录,P02是登录记录,PO3是商品浏览记录,P04是添加购物车记录,P05是搜索索记录
    2. BO是个人网站行为, 包含P01~P05

DTO(Data Transfer Object):数据传输对象

  1. 很多时候并不需要PO的所有属性(数据库表的所有字段)传输给客户端
  2. 所以,可以根据客户端的业务需要对PO、BO的属性进行删减或增加,重新组装成DTO对象

VO(View Object):视图对象

  1. 传输给客户端的数据展示对象,通常对应一个页面
  2. 在有些时候,业务字段的值和最终展示的值是不一样的,比如:在DTO中,sex的值是0、1
    1. 页面A的VO中, sex的值是男生、女生
    2. 页面B的VO中, sex的值是帅哥、美女

POJO(Plain Ordinary Java Object):简单的Java对象

  1. 是PO、BO、DTO、VO的统称

Query:数据查询对象

图1

浏览器自动格式化json

  1. 在谷歌插件中搜索JSON-handle,然后安装https://chrome.google.com/webstore/search/
  2. 用谷歌请求的json数据,会自动格式化展示

MySQL数据类型的选择

  1. 优先选择符合需求的最小数据类型(mysql的数据类型),比如:非负数就使用UNSIGNED、使用INT UNSIGNED存储IP地址

     //mysql的函数,将ip地址转化为整数值
     INET_ATON(‘255.255.255.255') = 4294967295
     INET_NTOA(4294967295)= '255.255.255.255'
    
  2. 固定长度的字符串使用CHAR,不使用VARCHAR(可变字符比较耗性能):比如手机号、身份证、MD5密码
  3. 谨慎使用TEXT、ENUM类型
    1. TEXT(大文本)在很多时候会降低数据库的性能
    2. 当需要修改ENUM类型的待选值时,需要使用ALTER语句(需要锁表)
  4. 金钱相关的数据,必须使用DECIMAL类型
  5. 需要注意的是,Java中的整数类型都是有符号的,所以与MYSQL类型的正确映射关系如下表所示

    图1

    1. MYSQL中存储的各种类型的数据,最终都会转化成Java中的某种类型
    2. 举例:mysql中的TINYINT转化为java中的 Byte类型,TINYINT UNSIGNED 类型转化为java中的Short类型
    3. 上图表达的就是mysql中的符号数据类型(TINYINT)与无符号数据类型(TINYINT UNSIGNED)分别对应java中的数据类型是什么
    4. 即MYSQL中的数据类型与Java中的数据类型对应关系
  6. 表的定义
    1. 主键优先采用整型AUTO_INCREMENT
    2. 每张表,每一列都要使用COMMENT增加注释

       CREATE TABLE XX(...) COMMENT 'XX'
      
    3. 每个字段都NOT NULL DEFAULT XX COMMENT ‘XX’:非空是为了方便以后建立索引(索引只能建立在非空列上)
    4. 谨慎外键使用:会影响数据库的性能,因此企业中基本不使用外键
  7. 使用IDEA中的Database新建表,详见【视频46-1:05分钟】开始
Table of Contents