项目实战一-SpringBoot+FreeMarker
本项目讲解前后端不分离,采用SpringBoot+FreeMarker技术
企业开发中常见的后台管理系统
- CRM(Customer Relationship Management): 客户关系管理,管理客户信息(个人信息、订单信息等)
- OA(Office Automation): 办公自动化,基于工作流概念,使企业内部人员方便快捷地共享信息,高效协同工作
- ERP(Enterprise Resource Planning): 企业资源计划,物资资源管理、人力资源管理、财务资源管理、信息资源管理等集成一体化
项目构建步骤
- 新建数据库,初始化表结构
- 构建po层
- 构建Mapper层
- 构建service层
- 构建controller层
初始化项目
- IDEA新建一个Module,命名为JiaKao
-
新建入口类
//com.zh.jk.JiaKaoApplication @SpringBootApplication public class JiaKaoApplication { public static void main(String[] args) { SpringApplication.run(JiaKaoApplication.class, args); } }
-
添加项目常见依赖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>
-
- 新建数据库,初始化表结构
- 将准备好的数据库表结构、测试数据文件夹bk_jaikao.sql、data.sql放入到项目目录sql中
- IDEA点击右侧database,展开,点击顶部的+ ->Schema->Name为jiakaox,priview第一句修改为
create database
点击OK - 右击jiakaox->Run SQL Script…->选择项目中的bk_jaikao.sql语句那个文件夹即可->点击OK就会执行文件中的sql语句
- 然后同样的步骤,选择data.sql,会自动生成一些测试数据
-
在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
- 构建po层
- 新建com.zh.jk.pojo.po包名,具体原因详见【项目中的各种Object】
- 新建po的模型类:如果有n张表,那么就要手动建n个object,比较麻烦,可以使用MyBatis Generator,自动生成,详情见【MyBatis Generator】讲解
- 构建Mapper层
- 新建com.zh.jk.mapper包名,依次新建com.zh.jk.service、com.zh.jk.controller
-
在mapper下新建DictTypeMapper类
//com.zh.jk.mapper public interface DictTypeMapper { @Select("SELECT id, name, value, intro FROM dict_type") List<DictType> list(); }
-
构建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(); } }
-
构建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(); } }
- 运行项目,在浏览器输入:
http://localhost:8080/dictTypes/list
,能查询到数据
数据字典页面开发
- Freemarker集成,在【项目实战-扩展】中的【集成到SpringBoot】中已经讲解
- 接下来要做分页查询,因此需要条件:搜索关键字、每页数量、当前页码
- 在pojo下新建一个query文件夹,po专门跟表对应,query专门跟查询对应
- 新建一个基础类PageQuery,查询页面的必要条件page(当前页码)、size(每页大小)
- 再建一个KeyWordQuery,需要关键字
-
新建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{ }
-
改造控制器DictTypeController
//查询新增了一个query @GetMapping("/list") public String list(DictTypeQuery query, Model model) { model.addAttribute("data",service.list(query)); //默认情况下回自动添加前缀:classpath:/templates/,默认后置:.ftlh return "page/dictType"; }
-
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(); } }
- 注意: 在这里就需要改造了,使用功能mapper查询时,条件是动态的,因为keyword可能有也可能没有,可以用之前讲解的mybatis中动态sql来解决,但是仍然比较麻烦,此时就出现了My-Batis-Plus的作用
My-Batis-Plus实现条件查询
- 在【项目实战-扩展】-》【MyBatis-Plus简单使用】前提下
- service改造
-
DictTypeService新增方法
public interface DictTypeService extends IService<DictType> { List<DictType> list(DictTypeQuery query); }
-
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(); } }
-
-
新增配置类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); } }
- 浏览器输入:
http://localhost:8080/dictTypes/list?keyword=32&page=2&size=5
- 实现关键字模糊搜索,分页查询,浏览器的参数会直接赋值到控制器DictTypeQuery类型的query参数对象的属性中
分页搜索实现
-
效果如下图
-
由上图分析可知,每次搜索完之后,需要将关键字、总页数、总条数、每页大小都要返回给客户端
//分页查询 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;
-
如果将iPage返回给客户端,很显然不够,还需要将关键字返回给客户端,以后甚至还有其他更多条件,那么怎么办呢?
-
-
解决办法:不设置返回值,直接将查询结果存放在query对象中
-
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> {}
-
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()); }
-
控制器DictTypeController
@GetMapping("/list") public String list(DictTypeQuery query, Model model) { service.list(query); model.addAttribute("query",query); //默认情况下回自动添加前缀:classpath:/templates/,默认后置:.ftlh return "page/dictType"; }
-
dictType.ftlh中使用
<#list query.data as item> ... </#list> <input type="text" name="keyword" value="${query.keyword!}" class="form-control" placeholder="名称、值、简介"> ... ,共${query.pages!}页,共${query.count}条
-
项目实战知识积累
如何打开别人项目?
- 首先看这个项目连接的数据库,将相应的数据库创建,或者能够连接上相应的数据库
- 一般项目中会有个项目用到的表的sql语句文件夹
- 点击右边菜单database=>+->DataSource->MySQL->先登录上MySQL
- 右击schemas->new->Schema->Name:数据库名称zhtest; preview中,第一句修改为
create database
,点击OK即可 - schemas下面会多出一个数据库zhtest
- 右击zhtest->Run SQL Script…->选择项目中的sql语句那个文件夹即可->点击OK就会执行文件中的sql语句
- 如果是springboot项目,直接运行入口类,如果是非springboot项目
- 如果是使用本地的Tomcat,则IDEA配置好,然后使用IDEA运行
- 如果是直接使用tomcat插件,
tomcat7-maven-plugin
,则直接使用Maven的生命周期运行项目tomcat7:run,
如何创建一个项目
- 底层建设(数据库):使用PowerDesigner来设计数据库,更加方便,具体使用看项目第一节视频:34分钟开始
- 后台管理端
- 客户端
如何设计项目中的字典?
- 何为字典?一个项目中有n个地方可以选择枚举值,比如:性别、职业等等,前端的情况是点击下拉可以选择各种枚举,这些枚举值在数据库中的存储就是字典
- 如何设计数据字典?
- 表中肯定会有枚举的名称、对应值,这两个字段,比如:nam为男,value为1
- 通常不会将n个字典建立n张表,通常的做法就是全部放一张表,或者2张表
-
数据字典统一放一张表
表的主键 代表哪个类型 枚举名称 枚举值 id type name value 1 sex 男 0 2 sex 女 1 3 job 教师 0 ...
- 数据字典统一放2张表
- 第一张表专门放字典的类型信息
- 第二张表专门放枚举的信息
- 第二张表外键引用第一张表
//类型表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):持久化对象
- 属性跟数据库表的字段对应,一个PO对象对应数据库表的一条记录
BO (Business Object):业务对象
- 一类业务就会对应一个BO, 除了get\set方法以外,它会有很多业务操作,比如针对自身数据进行计算的方法
- 例子1
- P01是教育经历,PO2是工作经历,PO3是项目经验。 BO是简历,包含PO1~P03
- 例子2
- PO1是交易记录,P02是登录记录,PO3是商品浏览记录,P04是添加购物车记录,P05是搜索索记录
- BO是个人网站行为, 包含P01~P05
DTO(Data Transfer Object):数据传输对象
- 很多时候并不需要PO的所有属性(数据库表的所有字段)传输给客户端
- 所以,可以根据客户端的业务需要对PO、BO的属性进行删减或增加,重新组装成DTO对象
VO(View Object):视图对象
- 传输给客户端的数据展示对象,通常对应一个页面
- 在有些时候,业务字段的值和最终展示的值是不一样的,比如:在DTO中,sex的值是0、1
- 页面A的VO中, sex的值是男生、女生
- 页面B的VO中, sex的值是帅哥、美女
POJO(Plain Ordinary Java Object):简单的Java对象
- 是PO、BO、DTO、VO的统称
Query:数据查询对象
浏览器自动格式化json
- 在谷歌插件中搜索JSON-handle,然后安装https://chrome.google.com/webstore/search/
- 用谷歌请求的json数据,会自动格式化展示
MySQL数据类型的选择
-
优先选择符合需求的最小数据类型(mysql的数据类型),比如:非负数就使用UNSIGNED、使用INT UNSIGNED存储IP地址
//mysql的函数,将ip地址转化为整数值 INET_ATON(‘255.255.255.255') = 4294967295 INET_NTOA(4294967295)= '255.255.255.255'
- 固定长度的字符串使用CHAR,不使用VARCHAR(可变字符比较耗性能):比如手机号、身份证、MD5密码
- 谨慎使用TEXT、ENUM类型
- TEXT(大文本)在很多时候会降低数据库的性能
- 当需要修改ENUM类型的待选值时,需要使用ALTER语句(需要锁表)
- 金钱相关的数据,必须使用DECIMAL类型
-
需要注意的是,Java中的整数类型都是有符号的,所以与MYSQL类型的正确映射关系如下表所示
- MYSQL中存储的各种类型的数据,最终都会转化成Java中的某种类型
- 举例:mysql中的TINYINT转化为java中的 Byte类型,TINYINT UNSIGNED 类型转化为java中的Short类型
- 上图表达的就是mysql中的符号数据类型(TINYINT)与无符号数据类型(TINYINT UNSIGNED)分别对应java中的数据类型是什么
- 即MYSQL中的数据类型与Java中的数据类型对应关系
- 表的定义
- 主键优先采用整型AUTO_INCREMENT
-
每张表,每一列都要使用COMMENT增加注释
CREATE TABLE XX(...) COMMENT 'XX'
- 每个字段都NOT NULL DEFAULT XX COMMENT ‘XX’:非空是为了方便以后建立索引(索引只能建立在非空列上)
- 谨慎外键使用:会影响数据库的性能,因此企业中基本不使用外键
- 使用IDEA中的Database新建表,详见【视频46-1:05分钟】开始