此项目是《酷鲨商城》的服务器端管理商品相关数据的项目(至于管理员、用户、订单等,并不在此项目中开发)。
此项目是使用Spring Boot作为基础框架的项目,在后续的使用过程中,将使用到主流的SSM(Spring / Spring MVC / Mybatis)、Spring Security、Spring Validation等框架。
早期流行的是SSH:Spring / Struts 2 / Hibernate。
创建项目的参数:
cn.tedu
csmall-product
cn.tedu.csmall.product
1.8
2.5.9
创建项目完成后,pom.xml
文件的内容为:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.5.9version>
<relativePath/>
parent>
<groupId>cn.tedugroupId>
<artifactId>csmall-productartifactId>
<version>0.0.1version>
<name>jsd2204-csmall-product-teachername>
<description>这是酷鲨商城的商品管理服务的项目(学习中……)description>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
project>
推荐学习《软件工程》。
大概的指导思想,开发项目的核心流程:需求分析、可行性分析、总体设计、详细设计等。
在开发实践中,每个用户感受到的功能(例如登录、注册等)都是由项目中的多个组件(例如Controller、Mapper等)来共同完成的,通常,在开发时,首先确定需要开发的数据类型有哪些,例如用户、类别、购物车、订单等,并且,从基础类型开始制定开发顺序,例如需要先开发用户类型的数据的相关功能,才能开发订单数据的相关功能,然后,对于互不直接相关的数据类型,一般先开发简单的,再开发难度略大的,接下来,就应该规划每种数据类型需要实现哪些业务(用户能感受到的功能),以用户数据为例,需要开发的业务可能有:登录、注册、修改密码、查看用户列表、禁用某用户、删除用户……并规划这些业务的开发先后顺序,通常,应该大致遵循增、查、删、改的顺序,例如需要先开发注册,再开发登录……然后,在每个业务的开发过程中,应该先开发数据访问功能(增删改查)、业务逻辑层、控制器、页面。
创建数据库mall_pms
(Product Management System):
CREATE DATABASE mall_pms;
然后,在IntelliJ IDEA中配置Database面板,并在Console中执行以下SQL:
-- 数据库:mall_pms
-- 相册表:创建数据表
drop table if exists pms_album;
create table pms_album
(
id bigint unsigned auto_increment comment '记录id',
name varchar(50) default null comment '相册名称',
description varchar(255) default null comment '相册简介',
sort tinyint unsigned default 0 comment '自定义排序序号',
gmt_create datetime default null comment '数据创建时间',
gmt_modified datetime default null comment '数据最后修改时间',
primary key (id)
) comment '相册' charset utf8mb4;
-- 相册表:为相册名称字段添加索引
create index idx_album_name on pms_album (name);
-- 图片表:创建数据表
drop table if exists pms_picture;
create table pms_picture
(
id bigint unsigned auto_increment comment '记录id',
album_id bigint unsigned default null comment '相册id',
url varchar(255) default null comment '图片url',
description varchar(255) default null comment '图片简介',
width smallint unsigned default null comment '图片宽度,单位:px',
height smallint unsigned default null comment '图片高度,单位:px',
is_cover tinyint unsigned default 0 comment '是否为封面图片,1=是,0=否',
sort tinyint unsigned default 0 comment '自定义排序序号',
gmt_create datetime default null comment '数据创建时间',
gmt_modified datetime default null comment '数据最后修改时间',
primary key (id)
) comment '图片' charset utf8mb4;
-- 品牌表:创建数据表
drop table if exists pms_brand;
create table pms_brand
(
id bigint unsigned auto_increment comment '记录id',
name varchar(50) default null comment '品牌名称',
pinyin varchar(50) default null comment '品牌名称的拼音',
logo varchar(255) default null comment '品牌logo的URL',
description varchar(255) default null comment '品牌简介',
keywords varchar(255) default null comment '关键词列表,各关键词使用英文的逗号分隔',
sort tinyint unsigned default 0 comment '自定义排序序号',
sales int unsigned default 0 comment '销量(冗余)',
product_count int unsigned default 0 comment '商品种类数量总和(冗余)',
comment_count int unsigned default 0 comment '买家评论数量总和(冗余)',
positive_comment_count int unsigned default 0 comment '买家好评数量总和(冗余)',
enable tinyint unsigned default 0 comment '是否启用,1=启用,0=未启用',
gmt_create datetime default null comment '数据创建时间',
gmt_modified datetime default null comment '数据最后修改时间',
primary key (id)
) comment '品牌' charset utf8mb4;
-- 品牌表:为品牌名称字段添加索引
create index idx_brand_name on pms_brand (name);
-- 类别表:创建数据表
drop table if exists pms_category;
create table pms_category
(
id bigint unsigned auto_increment comment '记录id',
name varchar(50) default null comment '类别名称',
parent_id bigint unsigned default 0 comment '父级类别id,如果无父级,则为0',
depth tinyint unsigned default 1 comment '深度,最顶级类别的深度为1,次级为2,以此类推',
keywords varchar(255) default null comment '关键词列表,各关键词使用英文的逗号分隔',
sort tinyint unsigned default 0 comment '自定义排序序号',
icon varchar(255) default null comment '图标图片的URL',
enable tinyint unsigned default 0 comment '是否启用,1=启用,0=未启用',
is_parent tinyint unsigned default 0 comment '是否为父级(是否包含子级),1=是父级,0=不是父级',
is_display tinyint unsigned default 0 comment '是否显示在导航栏中,1=启用,0=未启用',
gmt_create datetime default null comment '数据创建时间',
gmt_modified datetime default null comment '数据最后修改时间',
primary key (id)
) comment '类别' charset utf8mb4;
-- 类别表:为类别名称字段添加索引
create index idx_category_name on pms_category (name);
-- 品牌类别关联表:创建数据表
drop table if exists pms_brand_category;
create table pms_brand_category
(
id bigint unsigned auto_increment comment '记录id',
brand_id bigint unsigned default null comment '品牌id',
category_id bigint unsigned default null comment '类别id',
gmt_create datetime default null comment '数据创建时间',
gmt_modified datetime default null comment '数据最后修改时间',
primary key (id)
) comment '品牌与类别关联' charset utf8mb4;
-- 属性表:创建数据表
drop table if exists pms_attribute;
create table pms_attribute
(
id bigint unsigned auto_increment comment '记录id',
template_id bigint unsigned default null comment '所属属性模版id',
name varchar(50) default null comment '属性名称',
description varchar(255) default null comment '简介(某些属性名称可能相同,通过简介补充描述)',
type tinyint unsigned default 0 comment '属性类型,1=销售属性,0=非销售属性',
input_type tinyint unsigned default 0 comment '输入类型,0=手动录入,1=单选,2=多选,3=单选(下拉列表),4=多选(下拉列表)',
value_list varchar(255) default null comment '备选值列表',
unit varchar(50) default null comment '计量单位',
sort tinyint unsigned default 0 comment '自定义排序序号',
is_allow_customize tinyint unsigned default 0 comment '是否允许自定义,1=允许,0=禁止',
gmt_create datetime default null comment '数据创建时间',
gmt_modified datetime default null comment '数据最后修改时间',
primary key (id)
) comment '属性' charset utf8mb4;
-- 属性模版表:创建数据表
drop table if exists pms_attribute_template;
create table pms_attribute_template
(
id bigint unsigned auto_increment comment '记录id',
name varchar(50) default null comment '属性模版名称',
pinyin varchar(50) default null comment '属性模版名称的拼音',
keywords varchar(255) default null comment '关键词列表,各关键词使用英文的逗号分隔',
sort tinyint unsigned default 0 comment '自定义排序序号',
gmt_create datetime default null comment '数据创建时间',
gmt_modified datetime default null comment '数据最后修改时间',
primary key (id)
) comment '属性模版' charset utf8mb4;
-- 属性模版表:为属性模版名称字段添加索引
create index idx_attribute_template_name on pms_attribute_template (name);
-- 类别与属性模版关联表:创建数据表
drop table if exists pms_category_attribute_template;
create table pms_category_attribute_template
(
id bigint unsigned auto_increment comment '记录id',
category_id bigint unsigned default null comment '类别id',
attribute_template_id bigint unsigned default null comment '属性模版id',
gmt_create datetime default null comment '数据创建时间',
gmt_modified datetime default null comment '数据最后修改时间',
primary key (id)
) comment '类别与属性模版关联' charset utf8mb4;
-- SPU(Standard Product Unit)表:创建数据表
drop table if exists pms_spu;
create table pms_spu
(
id bigint unsigned not null comment '记录id',
name varchar(50) default null comment 'SPU名称',
type_number varchar(50) default null comment 'SPU编号',
title varchar(255) default null comment '标题',
description varchar(255) default null comment '简介',
list_price decimal(10, 2) default null comment '价格(显示在列表中)',
stock int unsigned default 0 comment '当前库存(冗余)',
stock_threshold int unsigned default 0 comment '库存预警阈值(冗余)',
unit varchar(50) default null comment '计件单位',
brand_id bigint unsigned default null comment '品牌id',
brand_name varchar(50) default null comment '品牌名称(冗余)',
category_id bigint unsigned default null comment '类别id',
category_name varchar(50) default null comment '类别名称(冗余)',
attribute_template_id bigint unsigned default null comment '属性模版id',
album_id bigint unsigned default null comment '相册id',
pictures varchar(500) default null comment '组图URLs,使用JSON数组表示',
keywords varchar(255) default null comment '关键词列表,各关键词使用英文的逗号分隔',
tags varchar(255) default null comment '标签列表,各标签使用英文的逗号分隔,原则上最多3个',
sales int unsigned default 0 comment '销量(冗余)',
comment_count int unsigned default 0 comment '买家评论数量总和(冗余)',
positive_comment_count int unsigned default 0 comment '买家好评数量总和(冗余)',
sort tinyint unsigned default 0 comment '自定义排序序号',
is_deleted tinyint unsigned default 0 comment '是否标记为删除,1=已删除,0=未删除',
is_published tinyint unsigned default 0 comment '是否上架(发布),1=已上架,0=未上架(下架)',
is_new_arrival tinyint unsigned default 0 comment '是否新品,1=新品,0=非新品',
is_recommend tinyint unsigned default 0 comment '是否推荐,1=推荐,0=不推荐',
is_checked tinyint unsigned default 0 comment '是否已审核,1=已审核,0=未审核',
check_user varchar(50) default null comment '审核人(冗余)',
gmt_check datetime default null comment '审核通过时间(冗余)',
gmt_create datetime default null comment '数据创建时间',
gmt_modified datetime default null comment '数据最后修改时间',
primary key (id)
) comment 'SPU(Standard Product Unit)' charset utf8mb4;
-- SPU详情表:创建数据表
drop table if exists pms_spu_detail;
create table pms_spu_detail
(
id bigint unsigned auto_increment comment '记录id',
spu_id bigint unsigned default null comment 'SPU id',
detail text default null comment 'SPU详情,应该使用HTML富文本,通常内容是若干张图片',
gmt_create datetime default null comment '数据创建时间',
gmt_modified datetime default null comment '数据最后修改时间',
primary key (id)
) comment 'SPU详情' charset utf8mb4;
-- SKU(Stock Keeping Unit)表:创建数据表
drop table if exists pms_sku;
create table pms_sku
(
id bigint unsigned not null comment '记录id',
spu_id bigint unsigned default null comment 'SPU id',
title varchar(255) default null comment '标题',
bar_code varchar(255) default null comment '条型码',
attribute_template_id bigint unsigned default null comment '属性模版id',
specifications varchar(2500) default null comment '全部属性,使用JSON格式表示(冗余)',
album_id bigint unsigned default null comment '相册id',
pictures varchar(500) default null comment '组图URLs,使用JSON格式表示',
price decimal(10, 2) default null comment '单价',
stock int unsigned default 0 comment '当前库存',
stock_threshold int unsigned default 0 comment '库存预警阈值',
sales int unsigned default 0 comment '销量(冗余)',
comment_count int unsigned default 0 comment '买家评论数量总和(冗余)',
positive_comment_count int unsigned default 0 comment '买家好评数量总和(冗余)',
sort tinyint unsigned default 0 comment '自定义排序序号',
gmt_create datetime default null comment '数据创建时间',
gmt_modified datetime default null comment '数据最后修改时间',
primary key (id)
) comment 'SKU(Stock Keeping Unit)' charset utf8mb4;
-- SKU规格参数表(存储各SKU的属性与值,即规格参数):创建数据表
drop table if exists pms_sku_specification;
create table pms_sku_specification
(
id bigint unsigned auto_increment comment '记录id',
sku_id bigint unsigned default null comment 'SKU id',
attribute_id bigint unsigned default null comment '属性id',
attribute_name varchar(50) default null comment '属性名称',
attribute_value varchar(50) default null comment '属性值',
unit varchar(10) default null comment '自动补充的计量单位',
sort tinyint unsigned default 0 comment '自定义排序序号',
gmt_create datetime default null comment '数据创建时间',
gmt_modified datetime default null comment '数据最后修改时间',
primary key (id)
) comment 'SKU数据' charset utf8mb4;
-- -------------------------- --
-- 以下是插入测试数据及一些测试访问 --
-- -------------------------- --
-- 品牌表:插入测试数据
insert into pms_brand (name, pinyin, description, keywords, enable)
values ('华为', 'huawei', '华为专注网络设备三十年', '华为,huawei,mate,magicbook', 1),
('小米', 'xiaomi', '小米,为发烧而生', '小米,xiaomi,发烧', 1),
('苹果', 'pingguo', '苹果,全球知名品牌', '苹果,apple,pingguo,iphone,mac', 1);
-- 类别表:插入测试数据
insert into pms_category (name, parent_id, depth, is_parent, keywords, enable, is_display)
values ('手机 / 运营商 / 数码', 0, 1, 1, null, 1, 1),
('手机通讯', 1, 2, 1, '手机,电话', 1, 1),
('智能手机', 2, 3, 0, null, 1, 1),
('非智能手机', 2, 3, 0, null, 1, 1),
('电脑 / 办公', 0, 1, 1, null, 1, 1),
('电脑整机', 5, 2, 1, '电脑,计算机,微机,服务器,工作站', 1, 1),
('电脑配件', 5, 2, 1, '配件,组装,CPU,内存,硬盘', 1, 1),
('笔记本', 6, 3, 0, '电脑,笔记本,微机,便携', 1, 1),
('台式机 / 一体机', 6, 3, 0, '台式机,一体机', 1, 1);
-- 品牌类别表:插入测试数据
insert into pms_brand_category (brand_id, category_id)
values (1, 3),
(2, 3),
(3, 3),
(1, 8),
(2, 8),
(3, 8),
(1, 9),
(3, 9);
-- 关联测试查询:各品牌有哪些类别的产品
select pms_brand_category.id, pms_brand.name, pms_category.name
from pms_brand_category
left join pms_brand
on pms_brand_category.brand_id = pms_brand.id
left join pms_category
on pms_brand_category.category_id = pms_category.id
order by pms_brand.pinyin;
-- 属性表:插入测试数据
insert into pms_attribute (name, description, type, input_type, value_list, unit, is_allow_customize)
values ('屏幕尺寸', '智能手机屏幕尺寸', 0, 1, '6.1,6.3', '英寸', 1),
('屏幕尺寸', '笔记本电脑屏幕尺寸', 0, 1, '14,15', '英寸', 1),
('颜色', '智能手机颜色', 0, 1, '黑色,金色,白色', null, 1),
('颜色', '衬衣颜色', 0, 1, '白色,蓝色,灰色,黑色', null, 1),
('运行内存', '智能手机运行内存', 0, 1, '4,8,16', 'GB', 1),
('CPU型号', '智能手机CPU型号', 0, 1, '骁龙870,骁龙880', null, 1),
('机身毛重', '智能手机机身毛重', 0, 0, null, 'g', 0),
('机身存储', '智能手机机身存储', 0, 1, '64,128,256,512', 'GB', 0),
('操作系统', '智能手机操作系统', 0, 1, 'Android,iOS', null, 0),
('操作系统', '电脑操作系统', 0, 1, '无,Windows 7,Windows 10,Ubuntu,Mac OS', null, 0);
-- 属性模版表:插入测试数据
insert into pms_attribute_template (name, pinyin, keywords)
values ('智能手机', 'zhinengshouji', '手机'),
('服装-上身', 'fuzhuang', '服装,上衣'),
('服装-裤子', 'fuzhuang', '服装,裤'),
('笔记本电脑', 'bijibendiannao', '电脑,笔记本'),
('台式电脑', 'taishidiannao', '电脑,台式电脑,台式机');
-- 相册表:插入测试数据
insert into pms_album (name, description)
values ('iPhone 13', null),
('Mi 11 Ultra', null);
-- 图片表:插入测试数据
insert into pms_picture (album_id, url, description, width, height)
values (1, '模拟数据:iPhone 13图片URL-1', null, 1024, 768),
(1, '模拟数据:iPhone 13图片URL-2', null, 1024, 768),
(1, '模拟数据:iPhone 13图片URL-3', null, 1024, 768),
(1, '模拟数据:iPhone 13图片URL-4', null, 1024, 768),
(1, '模拟数据:iPhone 13图片URL-5', null, 1024, 768),
(2, '模拟数据:Mi 11 Ultra图片URL-1', null, 1024, 768),
(2, '模拟数据:Mi 11 Ultra图片URL-2', null, 1024, 768),
(2, '模拟数据:Mi 11 Ultra图片URL-3', null, 1024, 768),
(2, '模拟数据:Mi 11 Ultra图片URL-4', null, 1024, 768),
(2, '模拟数据:Mi 11 Ultra图片URL-5', null, 1024, 768);
-- SPU表:插入测试数据
insert into pms_spu (id, name, type_number, title, description, list_price, stock, stock_threshold, unit, brand_id,
brand_name, category_id, category_name, keywords, tags)
values (202112010000001, 'iPhone 13', 'A2404', '苹果手机iPhone 13(A2404)', '2021年新款,全网首发',
5199.99, 5000, 20, '部', 3, '苹果', 3, '智能手机', 'ip13,iPhone13,苹果13', '20w快充,NFC,无线充电'),
(202112010000002, '小米11 Ultra', 'M112021', '小米11 Ultra(M112021)', '2021年最新旗舰机',
5899.99, 8000, 20, '部', 2, '小米', 3, '智能手机', 'mi11,xiaomi11,ultra', '67w快充,1亿像素,5000毫安电池');
-- SPU详情表:插入测试数据
insert into pms_spu_detail (spu_id, detail)
values (1, 'iPhone 13的详情HTML'),
(2, '小米11 Ultra的详情HTML');
-- SKU(Stock Keeping Unit)表:插入测试数据
insert into pms_sku (id, spu_id, title, attribute_template_id, specifications, price, stock, stock_threshold)
values (202112010000001, 2, '2021年新款,小米11 Ultra黑色512G,16G超大内存120Hz高刷67w快充', 1,
'{"attributes":[{"id":1,"name":"屏幕尺寸","value":"6.1寸"},{"id":3,"name":"颜色","value":"黑色"},{"id":5,"name":"运行内存","value":"16GB"}]}',
6999.99, 3000, 50),
(202112010000002, 2, '2021年新款,小米11 Ultra白色512G,8G超大内存120Hz高刷67w快充', 1,
'{"attributes":[{"id":1,"name":"屏幕尺寸","value":"6.1寸"},{"id":3,"name":"颜色","value":"白色"},{"id":5,"name":"运行内存","value":"8GB"}]}',
6499.99, 3000, 50);
-- SKU规格参数表(存储各SKU的属性与值,即规格参数):插入测试数据
insert into pms_sku_specification (sku_id, attribute_id, attribute_name, attribute_value, unit)
values (1, 1, '屏幕尺寸', '6.1', '寸'),
(1, 3, '颜色', '黑色', null),
(1, 5, '运行内存', '16', 'GB'),
(2, 1, '屏幕尺寸', '6.1', '寸'),
(2, 3, '颜色', '白色', null),
(2, 5, '运行内存', '8', 'GB');
目前涉及12张数据表,即12种数据类型,应该先开发基础的、与其它数据不直接相关的数据类型,例如相册、名牌、类别等,其它类型的开发将后置。
对于相册、名牌、类型这些数据,基本的数据操作至少包括:
数据访问层指的就是增删改查相关的数据操作,对于数据库中的数据操作,通常使用Mybatis框架来实现。
在实现Mybatis编程之前,首先,应该有各数据表对应的实体类,关于实体类的开发:
实体类的名称应该与数据表名的关键字相对应,例如表名为pms_album
,则实体类名为Album
,表名为pms_attribute_template
,则实体类名为AttributeTemplate
实体类中的属性的类型应该与表设计保持一致,通常对应关系为:
数据表字段类型 | 实体类属性类型 |
---|---|
bigint |
Long |
int 、smallint 、tinyint |
Integer |
char 、varchar 、text |
String |
date_time |
LocalDateTime |
decimal |
BigDecimal |
实体类中所有属性都应该是私有的
实体类中所有属性都应该有对应的Setter & Getter方法【自动生成】
实体类必须存在无参数构造方法
实体类必须重写hashCode()
和equals()
,且必须保证:hashCode()
返回值相同时,equals()
对比结果必须为true
,hashCode()
返回值不同时,equals()
对比结果必须为false
【自动生成】
实体类都应该重写toString()
方法,以输出所有字段的值,便于后续观察对象
实体类都必须实现Serializable
接口
建议将实体类放在项目根包下的entity
包中(某些编程习惯中可能使用其它的包名,例如domain
等)。
LOMBOK是一款可以在编译期在类中自动生成某些代码的工具,通常用于自动生成:
hashCode()
and equals()
toString()
在使用时,需要添加依赖项:
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.20version>
<scope>providedscope>
dependency>
在POJO类的声明上,添加@Data
注解,此注解可以帮助生成所有属性对应的Setters & Getters、规范的hashCode()
和equals()
、toString()
,并且,要求此类的父类中存在无参数构造方法。
package cn.tedu.csmall.product.pojo.entity;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
public class Album implements Serializable {
private Long id;
private String name;
private String description;
private Integer sort;
private LocalDateTime gmtCreate;
private LocalDateTime gmtModified;
}
注意:Lombok是在**编译期(将Java源代码文件.java编译成目标文件.class)**添加各方法,所以,在IntelliJ IDEA或其它开发工具中,默认情况下,直接调用以上各属性对应的Setter或Getter方法,在开发工具将无法提示这些方法,并且,已经写出来的调用这些方法的代码会报错,为了解决此问题,需要在开发工具中安装Lombok插件。
另外,Lombok还提供了以下注解:
@Data
@Setter
@Getter
@EqualsAndHashCode
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Accessors
@Accessors(chain = true)
时,将支持链式调用方法@Slf4j
Mybatis框架主要实现了简化持久层编程的问题。
持久层:实现数据持久化的一系列组件。
数据持久化:通常,在开发领域中,讨论的数据大多是在内存中的,而内存默认特指内存条(RAM:Random Access Memory),RAM的特性包含“一旦断电,数据将全部丢失”,且“正在执行的程序和数据都是在内存中的”,由程序处理的数据最终应该永久的保存下来,则不能将这些数据一直只存储在内存中,通常,会将数据存储到可以永久保存数据的存储介质中,典型的永久存储数据的存储介质有:硬盘、U盘、光盘等,所以,数据持久化就是将内存中的数据存储到硬盘等介质中,而硬盘中的数据是以文件的形式存在的,所以,通常可以将数据存储到文本文件中、XML文件、数据库中,这些存储方案中,只有数据库是便于实现增、删、改、查这4种操作的,所以,一般“数据持久化”默认指的就是将数据存储到数据库中。
在Java语言中,实现数据库编程需要先建立与数据库的连接,然后准备SQL语句,然后执行,然后获取执行结果并处理结果,最后,关闭或归还数据库连接,这是一套非常固定的流程,无论对哪个数据表执行哪种操作,其流程大致是固定的,所以,就产生了一些框架,用于简化这部分的编程。
在使用Mybatis时,只需要关注2点:
在Spring Boot项目中,当需要使用Mybatis时,需要添加相关的依赖:
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.2version>
dependency>
当添加以上依赖项后,如果启动项目,会提示以下错误:
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
Reason: Failed to determine a suitable driver class
因为Spring Boot启动时,如果检测到当前已经添加数据库编程的依赖项,会自动读取连接数据库的配置信息,由于目前尚未配置这些信息,所以,启动会报错!
所以,需要在application.properties
中添加配置:
# 连接数据库的参数
spring.datasource.url=jdbc:mysql://localhost:3306/mall_pms?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
由于Spring Boot在启动项目时只会读取以上配置并应用,并不会实际的连接数据库,所以,即使以上配置值是错误的,启动项目时并不会报告错误!
可以在src/test/java
的根包下的测试类中进行测试连接:
@SpringBootTest
class CsmallProductApplicationTests {
@Test
void contextLoads() {
}
@Autowired
DataSource dataSource;
@Test
void testConnection() throws Exception {
dataSource.getConnection();
}
}
当配置的URL错误(含主机名错误、端口号错误),或MySQL未启动时,将出现以下错误:
com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
如果数据库名称错误,或无此数据库,将出现以下错误:
java.sql.SQLSyntaxErrorException: Unknown database 'mall_pmsxzxxxxx'
如果用户或密码错误,将出现以下错误:
java.sql.SQLException: Access denied for user 'rootx'@'localhost' (using password: YES)
java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)
java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: NO)
Mybatis要求抽象方法必须存在于接口中(因为其实现原理是基于接口的代理模式的),所以,在项目的根包下创建mapper.AlbumMapper
接口。
提示:可以在接口上添加@Repository
注解,避免在自动装配时IntelliJ IDEA误判而提示错误。
关于接口中的抽象方法:
int
作为返回值类型,表示“受影响的行数”,不建议使用void
get
做前缀list
做前缀count
做前缀save/insert
做前缀remove/delete
做前缀update
做前缀则,插入相册时需要执行的SQL语句大致是:
insert into pms_album (name, description, sort, gmt_create, gmt_modified) values (?, ?, ?, ?, ?);
则抽象方法为:
int insert(Album album);
在首次使用时,需要让Mybatis知道哪些接口是Mapper接口,可以(二选一):
@Mapper
注解@MapperScan
并指定Mapper接口所在的包
@Configuration
注解,即是配置类config.MybatisConfiguration
类,同时添加@Configuration
和@MapperScan("cn.tedu.csmall.product.mapper")
即可另外,在使用Mybatis时,还需要为每个抽象方法配置其映射的SQL语句,可以使用@Insert
等注解来配置SQL语句,但不推荐,因为:
建议的做法是使用XML文件来配置SQL语句,可以从 http://doc.canglaoshi.org/config/Mapper.xml.zip 下载得到所需的文件,然后,在项目的src/main/resources
下创建mapper
文件夹,将得下载、解压得到的XML文件复制到此文件夹中。
关于配置SQL的XML文件:
上必须配置namespace
属性,取值为对应的接口的全限定名
的子级,根据需要执行的SQL语句,选择使用
、
、
、
中的某个节点,准备配置SQL语句,这些节点必须配置id
属性,取值为抽象方法的名称(不包括括号和参数列表),并在这些节点内部配置SQL语句提示:在本项目中,当插入数据时,不需要关注
gmtCreate
、gmtModified
这2个字段的值的插入,后续将使用Xxx自动完成。
例如:
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.tedu.csmall.product.mapper.AlbumMapper">
<insert id="insert">
INSERT INTO pms_album (
name, description, sort
) VALUES (
#{name}, #{description}, #{sort}
)
insert>
mapper>
在首次使用时,需要在application.properties
中配置以上XML文件的位置:
# Mybatis的配置SQL的XML文件的位置
mybatis.mapper-locations=classpath:mapper/*.xml
至此,“插入相册数据”的功能开发完成!
然后,应该及时测试以上功能是否正确,可以在src/test/java
下的根包下创建mapper.AlbumMapperTests
测试类,
package cn.tedu.csmall.product.mapper;
import cn.tedu.csmall.product.pojo.entity.Album;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class AlbumMapperTests {
@Autowired
AlbumMapper mapper;
@Test
void testInsert() {
Album album = new Album();
album.setName("某电视的相册");
album.setDescription("某电视的相册的描述");
album.setSort(63);
int rows = mapper.insert(album);
System.out.println("rows = " + rows);
}
}
在执行测试时,如果此前配置的@MapperScan
有误,会出现如下错误:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'cn.tedu.csmall.product.mapper.AlbumMapperTests': Unsatisfied dependency expressed through field 'mapper'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'cn.tedu.csmall.product.mapper.AlbumMapper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
如果在XML文件中,
的namespace
属性值配置有误,或者
节点的id
属性值配置有误,或者在application.properties
中没有正确的配置mybatis.mapper-locations
属性,都将出现以下错误:
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): cn.tedu.csmall.product.mapper.AlbumMapper.insert
【练习】
#{}
中的名称是类中的属性名在配置
节点时,配置useGeneratedKeys
和keyProperty
这2个属性,就可以得到自动编号的id,例如:
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
其中,useGeneratedKeys="true"
表示“需要获取自动编号的键的值”,keyProperty="id"
表示将得到的自动编号的id值放回到参数对象的id
属性中去!
开发规范上,对于自动编号的表进行插入数据时,都应该配置这2个属性!
需求:根据id删除相册数据
需要执行的SQL语句大致是:
delete from pms_album where id=?
在AlbumMapper
接口中添加抽象方法:
int deleteById(Long id);
在AlbumMapper.xml
中配置以上抽象方法映射的SQL:
<delete id="deleteById">
DELETE FROM pms_album WHERE id=#{id}
delete>
完成后,在AlbumMapperTests
中添加新的测试方法:
@Test
void testDeleteById() {
Long id = 1L;
int rows = mapper.deleteById(id);
System.out.println("rows = " + rows);
}
【练习】
每个创建好的Spring Boot项目的src/main/java
下都有一个默认的包,且包下有一个带main()
方法的类,此类就是整个项目的启动类,执行此类的main()
方法将启动整个项目。
在Spring Boot项目中,在src/main/resources
下,默认就存在application.properties
文件,此文件是Spring Boot会自动读取的配置文件。
Spring Boot使用了许多自动配置的机制,以至于我们只需要按照规定的名称去填写配置值,这些配置就会生效!
实现以下数据表的“插入数据”和“根据id删除数据”的功能:
动态SQL:根据参数值的不同,将生成不同的SQL语句。
假设存在需求:根据若干个id删除相册数据,即批量删除。
需要执行的SQL语句大致是:
delete from pms_album where id=? or id=? or id=? ...
或者:
delete from pms_album where id in (?, ?, ?, ... ?);
当实现以上功能时,关于抽象方法,可以设计为:
int deleteByIds(Long[] ids);
或者:
int deleteByIds(Long... ids);
或者:
int deleteByIds(List<Long> ids);
在配置SQL时,需要使用到
节点对参数进行遍历:
<delete id="deleteByIds">
DELETE FROM pms_album
WHERE id IN (
<foreach collection="array" item="id" separator=",">
#{id}
foreach>
)
delete>
关于
节点的配置:
collection
属性:当抽象方法的参数只有1个且没有添加@Param
注解时,当参数是数组类型时(包括类型为可变参数时),此属性取值为array
,当参数是List
集合类型时,此属性取值为list
item
属性:遍历过程中的每个元素的变量名,是自定义的名称separator
属性:遍历过程中各元素之间的分隔符号练习:批量插入相册数据
需要执行的SQL语句大致是:
insert into pms_album (name,description,sort) values (?,?,?), (?,?,?), (?,?,?)
在AlbumMapper
中添加抽象方法:
int insertBatch(List<Album> albums);
在AlbumMapper.xml
中配置SQL语句:
<insert id="insertBatch" useGeneratedKeys="true" keyProperty="id">
INSERT INTO pms_album (
name, description, sort
) values
<foreach collection="list" item="album" separator=",">
(#{album.name}, #{album.description}, #{album.sort})
foreach>
insert>
通常,修改数据时,也会使用到动态SQL的机制,当传入某个字段对应的值时,SQL中才会包含修改此字段的部分,反之,如果没有传入某个字段对应的值,则SQL语句不会包含修改此字段的部分!
这样的功能可以通过动态SQL的
标签来实现!
假设需要实现修改相册数据,传入的参数中包含哪些数据,就修改哪些数据,不包含的部分将不会被修改。
在AlbumMapper
接口中添加抽象方法:
int update(Album album);
在AlbumMapper.xml
中配置SQL语句:
<update id="update">
UPDATE pms_album
<set>
<if test="name != null">
name=#{name},
if>
<if test="description != null">
description=#{description},
if>
<if test="sort != null">
sort=#{sort},
if>
set>
WHERE id=#{id}
update>
假设需要实现:统计相册表中的数据的数量
需要执行的SQL语句大致是:
select count(*) from pms_album
关于抽象方法:在查询时,方法的返回值类型只要求能够存入查询结果即可。
则在AlbumMapper
中添加抽象方法:
int count();
然后,在AlbumMapper.xml
中配置SQL语句,将使用节点,此节点必须配置
resultType
或resultMap
这2个属性中的某1个,当使用resultType
时,此属性的值取决于抽象方法的返回值类型,如果是基本数据类型(例如int
等),则resultType
属性的值就是类型名,如果是引用数据类型(例如String
、Album
等),则resultType
属性的值就是类型的全限定名(在java.lang
包下的可以省略包名)。
<select id="count" resultType="int">
SELECT count(*) FROM pms_album
select>
如果既没有配置resultType
又没有配置resultMap
,将会出现以下错误:
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.executor.ExecutorException: A query was run and no Result Maps were found for the Mapped Statement 'cn.tedu.csmall.product.mapper.AlbumMapper.count'. It's likely that neither a Result Type nor a Result Map was specified.
假设需要实现:根据id查询相册详情
需要执行的SQL语句大致是:
select id, name, description, sort from pms_album where id=?
关于抽象方法的返回值类型,原则上,只需要能够“放得下”就行,所以,可以使用Album
作为此次查询的返回值类型,但是,并不建议这样处理!通常,建议另创建类型,用于封装查询结果!另外创建的类型,通常并不会称之为实体类,并且,这种类型会添加一些后缀,关于后缀的使用,阿里的文档的参考:
关于以上后缀:
对于本次查询,可以使用VO
作为类型的后缀,完整的类名可以使用AlbumStandardVO
,此类应该放在项目的根包的pojo.vo
包下:
@Data
public class AlbumStandardVO implements Serializable {
// 除了gmtCreate和gmtModified以外的所有属性
}
提示:此前涉及的“实体类编写规范”本质上是POJO的规范!
接下来,在AlbumMapper
接口中添加抽象方法:
AlbumStandardVO getStandardById(Long id);
在AlbumMapper.xml
中配置SQL:
<select id="getStandardById" resultType="xx.xx.xx.AlbumStandardVO">
select id, name, description, sort from pms_album where id=#{id}
select>
Mybatis在封装查询结果时,会自动的将**列名(Column)与属性名(Property)**匹配的结果进行封装,例如查询结果中的name
值将封装到返回值对象的name
属性中去,对于名称不匹配的,将放弃。
可以在配置SQL时,为查询的字段自定义列名,使得“查询结果中的列名”与“封装结果的类型中的属性名”是一致的,例如:
<select id="getStandardById" resultType="cn.tedu.csmall.product.pojo.vo.BrandStandardVO">
SELECT
id, name, pinyin, logo, description,
keywords, sort, sales,
product_count AS productCount,
comment_count AS commentCount,
positive_comment_count AS positiveCommentCount,
enable
FROM pms_brand
WHERE id=#{id}
select>
提示:在SQL语句中,自定义别名时,
AS
关键字并不是必须的,只需要有1个空格即可。
除了以上做法以外,还可以在application.properties
中添加配置,使得Mybatis能自动处理“全小写且使用下划线分隔的字段名对应的列名”与“驼峰命名法的属性名”之间的对应关系(例如此做法时,不必在查询时自定义别名):
mybatis.configuration.map-underscore-to-camel-case=true
或者,还可以选择自定义ResultMap,用于指导Mybatis如何封装查询结果,其基本方式是:
<resultMap id="自定义的ResultMap名称" type="封装查询结果的类型的全限定名">
resultMap>
<select id="xxx" resultMap="自定义的ResultMap名称">
select>
在
内部,使用
节点,配置其column
与property
属性,用于指定列名与属性名的对应关系,例如:
<resultMap id="自定义的ResultMap名称" type="封装查询结果的类型的全限定名">
<result column="product_count" property="productCount" />
<result column="comment_count" property="commentCount" />
<result column="positive_comment_count" property="positiveCommentCount" />
resultMap>
提示:在普通的单表查询中,列名与属性名本身就对应的部分,并不需要在
中配置。
另外,在开发实践中,建议将查询的字段列表使用
节点进行封装,然后,在配置的SQL语句中,使用
节点进行调用即可:
<select id="getStandardById" resultMap="StandardResultMap">
SELECT
<include refid="StandardQueryFields"/>
FROM pms_brand
WHERE id=#{id}
select>
<sql id="StandardQueryFields">
id, name, pinyin, logo, description,
keywords, sort, sales, product_count, comment_count,
positive_comment_count, enable
sql>
<resultMap id="StandardResultMap"
type="cn.tedu.csmall.product.pojo.vo.BrandStandardVO">
<result column="product_count" property="productCount" />
<result column="comment_count" property="commentCount" />
<result column="positive_comment_count" property="positiveCommentCount" />
resultMap>
当在上使用
resultType
属性,取值却是
的id时,将出现以下错误:
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sqlSessionFactory' defined in class path resource [org/mybatis/spring/boot/autoconfigure/MybatisAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.apache.ibatis.session.SqlSessionFactory]: Factory method 'sqlSessionFactory' threw exception; nested exception is org.springframework.core.NestedIOException: Failed to parse mapping resource: 'file [C:\Users\pc\IdeaProjects\jsd2204-csmall-product-teacher\target\classes\mapper\BrandMapper.xml]'; nested exception is org.apache.ibatis.builder.BuilderException: Error parsing Mapper XML. The XML location is 'file [C:\Users\pc\IdeaProjects\jsd2204-csmall-product-teacher\target\classes\mapper\BrandMapper.xml]'. Cause: org.apache.ibatis.builder.BuilderException: Error resolving class. Cause: org.apache.ibatis.type.TypeException: Could not resolve type alias 'StandardResultMap'. Cause: java.lang.ClassNotFoundException: Cannot find class: StandardResultMap
当在上使用了
resultMap
,取值错误时(例如取值为类型的全限定名),将出现以下错误:
java.lang.IllegalArgumentException: Result Maps collection does not contain value for cn.tedu.csmall.product.pojo.vo.BrandStandardVO
查询列表与查询某1个数据的开发过程相差不大,主要区别在于:
List
集合来封装查询到的多个数据,所以,抽象方法的返回值类型必须是List
类型的假设需要实现:查询品牌列表(不考虑分页问题)
需要执行的SQL语句大致是:
select * from pms_brand order by sort desc, pinyin, id desc
注意:如果执行的查询的结果可能超过1条(即2条或以上),必须显式的指定order by
进行排序!
在vo
包中创建BrandListItemVO
类:
@Data
public class BrandListItemVO implements Serializable {
// id, name, logo
}
然后,在BrandMapper
接口中添加抽象方法:
List<BrandListItemVO> list();
在BrandMapper.xml
中配置SQL:
<select id="list" resultMap="ListItemResultMap">
SELECT
<include refid="ListItemQueryFields"/>
FROM pms_brand
ORDER BY sort DESC, pinyin, id DESC
select>
<sql id="ListItemQueryFields">
id, name, logo
sql>
<resultMap id="ListItemResultMap" type="cn.tedu.csmall.product.pojo.vo.BrandListItemVO">
resultMap>
注意:即使是查询列表,无论使用resultType
,还是配置
,关于数据类型,都只需要指定为List
中的元素类型即可!
完成以下数据表的功能:“根据id查询数据”(已完成的则跳过)、“根据名称统计数据数量”
utf8mb4
是MySQL / MariaDB中的一种字符集。
在当前主流版本的MySQL / MariaDB中,使用utf8
作为字符集时,默认表示的是utf8mb3
。
关于utf8mb3
和utf8mb4
,其主要区别在于:most bytes 3
和most bytes 4
,即最多使用3 / 4个字节来表示1个字符!所以,当使用utf8mb4
时,可以表示更多字符,例如生僻汉字、冷门符号、emoji表情符号等。
UTF指的是:Unicode Transfer Format,即Unicode传输编码。
在使用MySQL / MariaDB时,所有SQL语句中涉及的字符集都明确的使用utf8mb4
,而不要使用utf8
。
#{}
占位符在Mybatis中配置SQL时,可以使用#{}
格式的占位符来表示SQL语句中的参数,在占位符的大括号中,当抽象方法只有1个基本值(基本数据类型对应的值,和String)参数时,占位符名称是完全无所谓的,例如:
select * from pms_brand where id=#{0}
select * from pms_brand where id=#{id}
select * from pms_brand where id=#{dxmkjsdoifds}
以上写法都是可以正确运行的!
注意:如果抽象方法的参数只有1个,但不是基本值时,在#{}
的大括号里,必须写参数的数据类型的属性名,例如:
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO pms_album (
name, description, sort
) VALUES (
#{name}, #{description}, #{sort}
)
insert>
如果抽象方法的参数有多个,在非Spring Boot的集成环境下,抽象方法的每个参数必须使用@Param
注解来配置参数名称,例如:
int updatePasswordByUserId(@Param("userId") Long userId,
@Param("password") String password);
然后,在配置SQL时,在#{}
格式的占位符的大括号,使用注解中配置的名称,例如:
<update id="updatePasswordByUserId">
update user set password=#{password} where id=#{userId}
update>
之所以需要使用@Param
注解来配置名称,是因为编译期会丢失局部的变量的名称(这是Java语言的特点),
在主流的Spring Boot的集成环境下,即使抽象方法有多个参数,也可以不使用@Param
注解来指定参数的名称,是因为在这样的集成环境下,Spring框架会对编译过程进行干预,从而保留抽象方法的参数名称,以至于在.class
文件中是存在参数的名称的,所以,可以不使用@Param
。(事实上,在Spring MVC的控制器中,Spring MVC框架也是做了这样的处理的)
在开发实践中,无论使用的是Spring Boot集成环境,还是没有Spring Boot的环境,都应该在多参数时使用@Param
注解以配置参数的名称!
#{}
格式的占位符与${}
格式的占位符的区别mysql-connector-java
、mybatis-spring-boot-starter
(mybatis
+ mybatis-spring
+ spring-context
+ spring-jdbc
+ 数据库连接池)@MapperScan
配置接口所在的包,并在application.properties
中配置XML文件的位置application.properties
配置连接数据库的参数Serializable
,全属性的Setters & Getters,hashCode()
、equals()
、存在无参数的构造方法
toString()
并不是规范所要求的int
,查询使用可以装得下结果的类型即可
作为根节点,且配置namespace
属性,此属性的值是对应的接口的全限定名
、
、
、
节点
和
可以浑为一谈
、
、
都可以浑为一谈
、
、
、
这些节点都必须配置id
属性,取值为对应的抽象方法的名称
可以配置useGeneratedKeys
和keyProperty
属性,用于获取自动编号的id
必须配置resultType
或resultMap
其中的某1个(二选一)
可以实现对参数的遍历,可以实现批量删除、批量插入、批量修改……
结合
实现按需更新数据
封装SQL语句片段,并使用
调用
指导Mybatis封装查询结果日志可以用于在程序执行过程中,向控制台或文件或数据库等位置输出一些自定义的信息。
注意:在开发实践中,不要在src/main/java
下的任何类中使用System.out.println()
的方式进行输出,除非你确定这些信息是一定要被任何人都可以看到的!
在spring-boot-starter
(此依赖项是几乎所有带有spring-boot-starter
依赖项的子级依赖)的依赖项中,默认已经集成了SLF4j日志框架。
日志是有显示级别的,根据日志信息的重要程度,从低到高分别是:
trace
:跟踪信息debug
:调试信息info
:一般信息warn
:警告信息error
:错误信息默认的显示级别是info
,则只会显示此级别及更重要的日志信息。
在Spring Boot项目的application.properties
中,可以添加配置,以指定日志的显示级别:
logging.level.包名=日志的显示级别
例如,可以配置为:
logging.level.cn.tedu.csmall.product=info
在添加了Lombok
框架的Spring Boot项目中,可以在任何类上添加@Slf4j
注解,则在当前类中就可以使用名为log
的变量来调用方法,实现日志的输出,例如:
@Slf4j
@SpringBootTest
public class Slf4jTests {
@Test
void testSlf4j() {
log.trace("这是一条【trace】级别的日志");
log.debug("这是一条【debug】级别的日志");
log.info("这是一条【info】级别的日志");
log.warn("这是一条【warn】级别的日志");
log.error("这是一条【error】级别的日志");
}
}
可以看到,log
变量可以调用trace()
、debug()
、info()
、warn()
、error()
方法,将输出对应级别的日志。
各级别的方法均重载了多次,通常使用的方法是:
trace(String message)
trace(String message, Object... args)
提示:其它各级别的方法也有以上方式的重载。
使用以上第2个方法时,可以在第1个参数的字符串中使用{}
作为占位符,表示某变量,然后,从第2个参数开始,依次表示各{}
占位符对应的值即可,例如:
@Test
void testSlf4j() {
int a = 1;
int b = 2;
log.debug("a=" + a + ", b=" + b + ", a+b=" + (a + b));
log.debug("a={}, b={}, a+b={}", a, b, a + b);
}
另外,使用trace(String message, Object... args)
这类方法来输出日志时,日志框架会对第1个参数String message
进行缓存,执行效率远高于使用System.out.println()
且拼接字符串的效果。
Spring MVC主要解决了接收请求、响应结果及相关的问题。
在Spring Boot项目中,添加spring-boot-starter-web
依赖项,即可添加Spring MVC框架所需的依赖!
提示:如果使用的不是Spring Boot项目,当需要使用Spring MVC框架时,需要添加的依赖项是
spring-webmvc
。
提示:只需要将原有的
spring-boot-starter
改为spring-boot-starter-web
即可。
提示:在创建工程时,如果勾选了Web,本质上也是添加
spring-boot-starter-web
依赖项,另外,还会在src/main/resources
下自动创建static
和templates
文件夹。
一旦添加了spring-boot-starter-web
,当启动Spring Boot项目时,会自动启动Tomcat,并将此项目部署到Tomcat上。
Spring Boot启动Tomcat时,默认占用8080
端口,可以在application.properties
中通过server.port
属性来修改端口号,例如:
# 服务端口
server.port=9080
Spring MVC需要自定义控制器类,在类中使用方法来接收请求。
关于控制器类:必须在项目的根包之下,且添加@Controller
注解。
关于处理请求的方法:
@RequestMapping
系列注解来配置路径public
简单的接收请求如下:
package cn.tedu.csmall.product.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 处理相册数据相关请求的控制器
*
* @author [email protected]
* @version 0.0.1
*/
@Slf4j
@Controller
public class AlbumController {
public AlbumController() {
log.info("创建控制器:AlbumController");
}
// http://localhost:9080/a
@RequestMapping("/a")
public String xx() {
log.info("开始处理请求……");
return null;
}
}
提示:启动项目后,可以在浏览器或任何可以发出请求的软件中访问 http://localhost:9080/a ,目前会提示500
错误,在服务器端的控制台可以看到日志的输出,且每提交一次以上路径的请求,日志都会输出一次,反映为:每次提交请求,以上方法都会自动运行。
在处理请求的方法上添加@ResponseBody
注解,则表示当前方法处理完请求后,将响应正文。
提示:如果没有使用响应正文的做法,则处理请求的方法的返回值表示某个视图组件。
当方法的返回值类型是String
时,响应正文就会将返回的字符串将作为HTML源代码直接响应到客户端去!
还可以把@ResponseBody
添加在控制器类上,则当前控制器类中所有处理请求的方法都将响应正文。
另外,还可以使用@RestController
取代@Controller
和@ResponseBody
!
关于@RestController
的源代码:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(
annotation = Controller.class
)
String value() default "";
}
可以看到,@RestController
使用@Controller
和@ResponseBody
作为其元注解,则同时具有这2个元注解的特点!
所以,@RestController
的效果为:
@RequestMapping
的主要作用是配置“请求路径”与“处理请求的方法”的映射关系。
在配置请求路径时,建议的做法是:在类和方法上分别添加此注解进行配置,在类上的配置值会和每个方法的配置值组合起来,形成完整的请求路径,所以,类上的配置值是一个“统一的URL前缀”,便于和其它控制器需要处理请求路径进行区分。
在配置请求路径时,如果配置值是有效值,则值两侧的/
并不是必须的,例如:
类上的配置值 | 方法上的配置值 |
---|---|
/album | /list |
/album | list |
album | /list |
album | list |
/album/ | /list |
/album/ | list |
album/ | /list |
album/ | list |
以上所有配置的组合都是等效的!建议使用第1种或第4种。
除了配置请求路径以外,@RequestMapping
注解还可以配置:
method
:用于限制请求方式produces
:用于配置响应头中的文档类型注解的源代码中,都使用了@Target
作为其元注解,此注解的作用是声明当前注解可以添加在哪些位置,以@RequestMapping
为例,上面就配置了:
@Target({ElementType.TYPE, ElementType.METHOD})
在注解的源代码内部,声明了此注解可以配置哪些属性,及属性的值,在@RequestMapping
为例,其代码中包括:
@AliasFor("path")
String[] value() default {};
以上代码中:
value()
:表示此注解可以配置名为value
的属性String[]
:表示此value
属性的值类型是String[]
default {}
:表示此value
属性的默认值是{}
(空数组)@AliasFor("path")
:表示此value
属性与当前注解中的path
属性是等效的基于以上声明,可以:
@RequestMapping(value = {"value1", "value2", "value3"})
在所有注解中,value
属性都是默认的属性,在配置此属性时,如果注解只配置这1个属性,则可以不必显式指定属性名,即:
@RequestMapping(value = {"value1", "value2", "value3"})
@RequestMapping({"value1", "value2", "value3"})
以上2种配置方式是完全等效的!
并且,在所有注解中,如果要配置的属性的值类型是数组,但是,只需要配置1个值时(数组中只有1个元素),可以不必使用{}
将值框住,例如:
@RequestMapping(value = {"value1"})
@RequestMapping(value = "value1")
所以,综合看来,
@RequestMapping(value = {"value1"})
@RequestMapping(value = "value1")
@RequestMapping({"value1"})
@RequestMapping("value1")
以上4种配置是完全等效的!
在@RequestMapping
中的value
和path
是等效的,所以,配置方式也完全相同!另外,之所以有这2个完全等效的属性,因为value
使用简便,但是,每个注解都可以有value
属性,所以,这个属性名称并不一定具有良好的可读性,所以,Spring MVC框架就另设计了path
属性,当开发者追求简便时,使用value
即可,追求代码的可读性时,则可以使用path
属性。
需要注意:如果注解中需要配置多个属性,则每个属性值都必须显式的指定属性名,例如:
@RequestMapping(name = "xxx", "/list")
以上做法是错误的!!!
正确的配置方式如下:
@RequestMapping(name = "xxx", value = "/list")
以上配置中,即使是value
属性,也必须显式的指定属性名!
在@RequestMapping
还定义了method
属性:
RequestMethod[] method() default {};
此属性的作用是限制为某种请求方式,例如配置为:
@RequestMapping(value = "/add-new", method = RequestMethod.POST)
以上代码表示/add-new
路径只能使用POST
方式来提交请求,如果使用其它请求方式,将响应405
错误。
如果没有配置method
属性,则所有请求方式都是支持的!
为了简化约束请求方式,Spring MVC还提供了以下注解:
@GetMapping
@PostMapping
在Spring MVC中,可以在处理请求的方法的参数列表中,按需添加所需的请求参数(需要客户端提交什么参数,就在方法的参数列表中写什么参数)。
当设计了请求参数后:
null
""
)在设计请求参数时,可以按需添加多个参数,且多个参数不区分先后顺序,各参数也可以按需设计为期望的数据类型,例如设计为:
@RequestMapping("/add-new")
public String xx(String name, Integer sort) {
}
在提交的请求中,sort
参数必须是可以被转换成Integer
的,否则,将出现400
错误。
关于400错误:客户端没有按照服务器端的要求来提交请求参数,例如参数类型不可以被成功转换为期望的类型,或服务器要求必须提交某参数却没有提交。
关于Integer
类型的请求参数:
400
错误null
null
当请求参数的数量较多时,还可以将这些请求参数封装到类中,并使用这种类型作为处理请求的方法的参数!通常,建议使用专门的数据类型,而不要使用实体类型!例如:
package cn.tedu.csmall.product.pojo.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class AlbumAddNewDTO implements Serializable {
private String name;
private String description;
private Integer sort;
}
// 添加相册
// http://localhost:9080/album/add-new?name=XiaoMi
@RequestMapping("/add-new")
public String addNew(AlbumAddNewDTO albumAddNewDTO) {
log.debug("开始处理请求:{}", albumAddNewDTO);
return "处理了/album/add-new的请求";
}
所以,在Spring MVC中,接收请求参数可以:
提示:以上2种做法可以共存。
甚至,可以按需添加某些特定的参数,例如:HttpServletRequest
、HttpServletReponse
、HttpSession
、其它框架允许的且添加了特定的注解的参数。
RESTful是一种设计URL的风格。
注意:RESTful既不是规定,也不是规范!
RESTful的典型表现是:将某些具有“唯一性”的参数值作为URL的一部分。
例如:
https://blog.csdn.net/wl_ss013/article/details/810691
https://blog.csdn.net/weixfd3811/article/details/11565346
以上URL,如果不采用RESTful风格,可能是:
https://blog.csdn.net/article/details?username=wl_ss013&id=810691
https://blog.csdn.net/article/details?username=weixfd3811&id=11565346
所以,如果需要设计“根据id删除相册”的URL,可以设计为:
http://localhost:8080/album/9527/delete
Spring MVC框架对RESTful提供了很好的支持,要实现以上效果,可以在方法上配置为:
@RequestMapping("/{id}/delete")
在处理请求的方法的参数列表中,可以声明与占位符的名称匹配的参数,并添加@PathVariable
注解,即可接收到URL中的参数值:
// http://localhost:9080/album/9527/delete
@RequestMapping("/{id}/delete")
public String delete(@PathVariable Long id) {
log.debug("开始处理删除id={}请求", id);
return "处理了/" + id + "/delete的请求";
}
提示:如果URL中占位符的名称与方法参数的名称不匹配,可以在@PathVariable
注解中配置参数,值为URL中占位符的名称即可,则方法参数的名称就不重要了!例如:
@RequestMapping("/{albumId}/delete")
public String delete(@PathVariable("albumId") Long id) {
log.debug("开始处理删除id={}请求", id);
return "处理了/" + id + "/delete的请求";
}
创建出所有12种类型的控制器类,每个类的构造方法中都添加日志来输出,并在这些控制器中都添加“添加数据”的处理(添加处理请求的方法,并要求有DTO类,DTO类中的属性可以待定)。
在设计URL时,使用{}
的占位符时,可以在名称右侧添加:
,并在其右侧配置正则表达式,以对URL中的参数的基本格式进行约束,例如:
// http://localhost:9080/album/9527/delete
@RequestMapping("/{id:[0-9]+}/delete")
public String delete(@PathVariable Long id) {
log.debug("开始处理删除id={}请求", id);
return "处理了/" + id + "/delete的请求";
}
通过使用以上正则表达式,纯数字的id可以匹配以上路径,可以正常访问,如果不是纯数字的id,则根本匹配不到以上路径,以上方法也不会执行,服务器端将直接响应404错误。
提示:404错误相比400错误,能更早的回绝客户端的错误请求。
在使用{}
占位符且使用了正则表达式时,不冲突的匹配(每个URL只会匹配到其中某1个正则表达式,不会同时匹配到多个正则表达式)是可以共存的,例如:
// http://localhost:9080/album/9527/delete
@RequestMapping("/{id:[0-9]+}/delete")
public String delete(@PathVariable Long id) {
log.debug("开始处理删除id={}请求", id);
return "处理了/" + id + "/delete的请求";
}
// http://localhost:9080/album/huawei/delete
@RequestMapping("/{name:[a-zA-Z]+}/delete")
public String delete(@PathVariable String name) {
log.debug("开始处理删除name={}请求", name);
return "处理了/" + name + "/delete的请求";
}
甚至,不使用正则表达式的,也可以与之共存,例如,在以上基础上,还可以添加:
// http://localhost:9080/album/test/delete
@RequestMapping("/test/delete")
public String delete() {
log.debug("开始处理测试删除请求");
return "处理了测试删除的请求";
}
Spring MVC在处理时,会优先匹配没有使用正则表达式的,所以,当提交 /album/test/delete
时,会成功匹配到以上delete()
方法,不会匹配到delete(String name)
方法。
在RESTful的建议中,对于不同的数据操作,应该使用不同的请求类型,例如:
GET >>> /albums/9
:对id值为9的相册数据执行查询(执行数据的select
操作)PUT >>> /albums/9
:对id值为9的相册数据执行编辑(执行数据的update
操作)DELETE >>> /albums/9
:对id值为9的相册数据执行删除(执行数据的delete
操作)POST >>> /albums
:新增相册数据(执行数据的insert
操作)通常,绝大部分应用中,在处理业务时(并不是直接操作某数据),并不会采纳以上建议!
最后,在开发实践中,更多的还是只使用GET
和POST
这2种请求方式,关于RESTful 风格的URL设计参考:
/数据类型的复数
/albums
/数据类型的复数/id值
/albums/{id}
/数据类型的复数/id值/操作
/albums/{id}/delete
MVC = Model + View + Controller
MVC为设计软件提供了基本的思想,它认为每个软件都应该至少包含这3大部分,且各部分分工明确,只负责整个数据处理流程中的一部分功能。
例如V通常表现为“软件的界面”,用于呈现数据、提供用户操作的控件。
而C表示控制器,用于接收请求、响应结果,并不会处理实质业务。
而M表示数据模型,通常由业务逻辑和数据访问这2部分组成,在开发实践中,数据访问通常指的就是数据库编程,而业务逻辑是由专门的类来实现的,这样的类通常使用Service
作为类名的关键字。
在整个数据处理过程中,将会是:Controller调用Service,而Service调用Mapper。
业务逻辑的主要职责是:设计业务流程,处理业务逻辑,以保证数据的完整性和安全性。
Service的开发规范是先写接口,再写实现类。
通常,会在项目的根包下创建service
子包,Service接口将存放在这个包中,并且,还会在service
包下创建impl
子包,Service实现类都将放在这个包中,实现类都会使用ServiceImpl
作为类名的后缀。
例如:在项目的根包下创建service.IAlbumService
接口,然后,再创建service.impl.AlbumServiceImpl
类,且此类将实现IAlbumService
接口。
为了保证项目启动时可以正确的创建此实现类,需要类上添加@Service
注解。
package cn.tedu.csmall.product.service;
public interface IAlbumService {
}
package cn.tedu.csmall.product.service.impl;
import cn.tedu.csmall.product.service.IAlbumService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class AlbumServiceImpl implements IAlbumService {
public AlbumServiceImpl() {
log.info("创建业务对象:AlbumServiceImpl");
}
}
假设需要实现:添加相册。
则在接口中添加抽象方法,关于抽象方法的设计:
void addNew(AlbumAddNewDTO albumAddNewDTO);
然后,在实现类中:
@Override
public void addNew(AlbumAddNewDTO albumAddNewDTO) {
log.debug("开始处理【添加相册】的业务,参数:{}", albumAddNewDTO);
// 调用AlbumMapper对象的int countByName(String name)方法统计此名称的相册的数量
String name = albumAddNewDTO.getName();
int countByName = albumMapper.countByName(name);
log.debug("尝试添加的相册名称是:{},在数据库中此名称的相册数量为:{}", name, countByName);
// 判断统计结果是否大于0
if (countByName > 0) {
// 是:相册名称已经存在,抛出RuntimeException异常
String message = "添加相册失败!相册名称【" + name + "】已存在!";
log.warn(message);
throw new RuntimeException(message);
}
// 获取当前时间:LocalDateTime now = LocalDateTime.now()
LocalDateTime now = LocalDateTime.now();
// 创建Album对象
Album album = new Album();
// 补全Album对象中各属性的值:name:来自参数
// 补全Album对象中各属性的值:description:来自参数
// 补全Album对象中各属性的值:sort:来自参数
BeanUtils.copyProperties(albumAddNewDTO, album);
// 补全Album对象中各属性的值:gmtCreate:now
album.setGmtCreate(now);
// 补全Album对象中各属性的值:gmtModified:now
album.setGmtModified(now);
// 调用AlbumMapper对象的int insert(Album album)方法插入相册数据
log.debug("即将向数据库中插入数据:{}", album);
albumMapper.insert(album);
}
通常,建议自定义异常,用于表示在业务逻辑层中的“失败”(或错误),而不要使用已知的异常类型,避免捕获、处理不准确!
可以在项目的根包下创建ex.ServiceException
类,继承自RuntimeException
:
public class ServiceException extends RuntimeException {
// 生成5个构造方法
}
Spring MVC框架提供了统一处理异常的机制,使得每种类型的异常在处理时,只需要编写1次相关代码即可。
通常,统一处理异常的代码会写在专门的类中,此类应该添加@ControllerAdvice
,则类中相关的方法会在处理每个请求时生效!
由于目前采取前后端分离的模式,处理异常后的响应方式是响应正文,所以,还应该使用@ResponseBody
,或者,使用@RestControllerAdvice
,它同时具有@ControllerAdvice
和@ResponseBody
的效果。
@RestControllerAdvice
public class GlobalExceptionHandler {
}
然后,在此类中添加处理异常的方法:
@ExceptionHandler
注解,表示此方法是统一处理异常的方法public
HttpServletRequest
、HttpServletResponse
等少量特定类型的参数所以,完整的处理异常的代码为:
package cn.tedu.csmall.product.ex.handler;
import cn.tedu.csmall.product.ex.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler
public String handleServiceException(ServiceException e) {
log.debug("处理ServiceException:{}", e.getMessage());
return e.getMessage();
}
}
并且,在任何控制器类中,都不必再处理ServiceException
了。
另外,在以上类中,可以同时存在多个处理不同异常的方法(允许多个处理的异常之间存在继承关系)!
建议在每个项目中,在统一处理异常的类中,都添加对Throwable
的处理,以保证所有异常都会被处理,粗糙的异常信息不会响应到客户端去!
package cn.tedu.csmall.product.ex.handler;
import cn.tedu.csmall.product.ex.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler
public String handleServiceException(ServiceException e) {
log.debug("处理ServiceException:{}", e.getMessage());
return e.getMessage();
}
@ExceptionHandler
public String handleThrowable(Throwable e) {
log.debug("处理Throwable");
e.printStackTrace();
return "程序运行过程中出现意外错误,请联系系统管理员!";
}
}
Spring Validation框架的主要作用:实现了简化检查请求参数的基本格式
在Spring Boot中,需要添加spring-boot-starter-validation
依赖项。
当需要检查请求参数时,需要在处理请求的方法的参数列表中,对需要检查的参数添加@Validated
注解,表示此参数是需要通过Spring Validation进行检查的:
@RequestMapping("/add-new")
public String addNew(@Validated AlbumAddNewDTO albumAddNewDTO) {
// 省略方法体的代码
}
然后,在类的属性上,添加相关检查注解,并在检查注解中配置message
属性以指定错误时的提示文本:
@Data
public class AlbumAddNewDTO implements Serializable {
@NotNull(message = "必须提交相册名称!")
private String name;
private String description;
private Integer sort;
}
当Spring Validation检查不通过时,将抛出BindException
,所以,可以在统一处理异常的类中对此类异常进行处理:
@ExceptionHandler
public String handleBindException(BindException e) {
log.debug("处理BindException:{}", e.getMessage());
StringBuilder stringBuilder = new StringBuilder();
List<FieldError> fieldErrors = e.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
String message = fieldError.getDefaultMessage();
stringBuilder.append(message);
}
return stringBuilder.toString();
}
除了@NotNull
以外,框架还提供了许多检查注解,
@Pattern
:通过此注解的regexp
属性配置正则表达式,并使用message
配置验证失败时的提示文本
null
”的情况,如果不允许为null
,则必须同时配置@NotNull
和@Pattern
@Range
:通过此注解的min
和max
属性可以指定整型数据的最小值和最大值
@NotNull
一起使用关于根据id删除数据,在处理业务时,应该先根据id查询数据,检查此数据是否存在,然后再删除。
完成各数据的添加和根据id删除,包含Mapper层、业务逻辑层、控制器层。
Knife4j框架是一款基于Swagger 2框架的、能够基于项目中的控制器的代码来生成在线API文档的框架,另外,此框架还有调试功能,可以向服务器端发送请求,并获取响应结果。
关于此框架,要使之能够使用,需要:
application.properties
中添加1条配置关于依赖的代码:
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>knife4j-spring-boot-starterartifactId>
<version>2.0.9version>
dependency>
关于配置类:
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.EnableSwagger2WebMvc;
/**
* Knife4j配置类
*
* @author [email protected]
* @version 0.0.1
*/
@Slf4j
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfiguration {
/**
* 【重要】指定Controller包路径
*/
private String basePackage = "cn.tedu.csmall.product.controller";
/**
* 分组名称
*/
private String groupName = "product";
/**
* 主机名
*/
private String host = "http://java.tedu.cn";
/**
* 标题
*/
private String title = "酷鲨商城在线API文档--商品管理";
/**
* 简介
*/
private String description = "酷鲨商城在线API文档--商品管理";
/**
* 服务条款URL
*/
private String termsOfServiceUrl = "http://www.apache.org/licenses/LICENSE-2.0";
/**
* 联系人
*/
private String contactName = "Java教学研发部";
/**
* 联系网址
*/
private String contactUrl = "http://java.tedu.cn";
/**
* 联系邮箱
*/
private String contactEmail = "[email protected]";
/**
* 版本号
*/
private String version = "1.0.0";
@Autowired
private OpenApiExtensionResolver openApiExtensionResolver;
public Knife4jConfiguration() {
log.debug("加载配置类:Knife4jConfiguration");
}
@Bean
public Docket docket() {
String groupName = "1.0.0";
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.host(host)
.apiInfo(apiInfo())
.groupName(groupName)
.select()
.apis(RequestHandlerSelectors.basePackage(basePackage))
.paths(PathSelectors.any())
.build()
.extensions(openApiExtensionResolver.buildExtensions(groupName));
return docket;
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title(title)
.description(description)
.termsOfServiceUrl(termsOfServiceUrl)
.contact(new Contact(contactName, contactUrl, contactEmail))
.version(version)
.build();
}
}
关于application.properties
中的配置:
# 开启Knife4j框架的增强模式
knife4j.enable=true
注意:
basePackage
必须是控制器类所在的包,记得需要修改完成后,启动项目,通过 /doc.html
即可访问在线API文档。
在开发实践中,还应该对在线API文档进行细化,需要在控制器及相关类中进行一些配置:
@Api
注解,配置tags
属性,此属性是String
类型的@ApiOperation
注解,配置value
属性,此属性是String
类型的@ApiOperationSupport
注解,配置order
属性,此属性是int
类型的
@RequestMapping
,建议使用@GetMapping
或@PostMapping
@ApiModelProperty
注解,以配置对参数的说明@ApiImplicitParams
和@ApiImplicitParam
这2个注解组合来配置
@ApiImplicitParam
,原本的提示的值会被覆盖,应该完整的配置各属性完整的配置示例–AlbumController
:
@Api(tags = "04. 相册管理模块")
@Slf4j
@RestController
@RequestMapping("/albums")
public class AlbumController {
@Autowired
private IAlbumService albumService;
public AlbumController() {
log.info("创建控制器:AlbumController");
}
// 添加相册
// http://localhost:9080/albums/add-new?name=XiaoMi&description=TestDescription&sort=69
@ApiOperation("添加相册")
@ApiOperationSupport(order = 100)
@PostMapping("/add-new")
public String addNew(@Validated AlbumAddNewDTO albumAddNewDTO) {
log.debug("开始处理【添加相册】的请求:{}", albumAddNewDTO);
albumService.addNew(albumAddNewDTO);
return "添加相册成功!";
}
// http://localhost:9080/albums/9527/delete
@ApiOperation("根据id删除相册")
@ApiOperationSupport(order = 200)
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "相册id", dataType = "long", required = true)
})
@PostMapping("/{id:[0-9]+}/delete")
public String delete(@PathVariable Long id) {
log.debug("开始处理【删除相册】的请求:id={}", id);
albumService.deleteById(id);
return "删除相册成功!";
}
}
完整的配置示例–AlbumAddNewDTO
:
@Data
public class AlbumAddNewDTO implements Serializable {
/**
* 相册名称
*/
@ApiModelProperty(value = "相册名称", example = "小米80的相册", required = true)
@NotNull(message = "必须提交相册名称!")
private String name;
/**
* 相册简介
*/
@ApiModelProperty(value = "相册简介", example = "小米80的相册的简介", required = true)
@NotNull(message = "必须提交相册简介!")
private String description;
/**
* 自定义排序序号
*/
@ApiModelProperty(value = "自定义排序序号", example = "88", required = true)
@NotNull(message = "必须提交自定义排序序号!")
@Range(max = 99, message = "自定义排序序号必须是0~99之间的值!")
private Integer sort;
}
当服务器端向客户端响应数据时,除了必要的提示文本以外,还应该响应“业务状态码”到客户端,以便于客户端程序能够便捷且准确的判断当前请求的执行结果!
另外,某些操作是需要向客户端响应数据的
所以,向客户端的响应数据至少需要包含以下部分:
以上做法应该是针对所有请求都是如此响应的,通常,会自定义某个类型,用于封装以上3种数据,作为处理请求的方法的返回值类型,当响应时,Spring MVC框架会将返回值转换成JSON格式的字符串!
提示:Spring MVC能够将处理请求的方法的返回值转换成JSON格式的字符串,需要:
- 此方法是响应正文的
- 此项目中需要添加
jackson-databind
依赖
- 在Spring Boot中,
spring-boot-starter-web
中包含了此依赖- 此方法的返回值类型在Spring MVC中没有默认的Converter(转换器),会自动调用
jackson-databind
中的Converter,而jackson-databind
的处理方法就是将返回值转换成JSON格式的字符串
- 只要是自定义的数据类型,在Spring MVC中都没有默认的Converter
例如,在项目的根包下创建web.JsonResult
类:
@Data
public class JsonResult implements Serializable {
private Integer state;
private String message;
private Object data;
}
以上类型,将作为项目中每个处理请求的方法、每个处理异常的方法的返回值类型!
如果在Service层始终抛出ServiceException
,由于使用了统一处理异常的机制,会导致所有异常的业务状态码都是相同的!为了解决此问题,可以:
如果采取以上第2种做法,则需要将ServiceException
调整为:
@Getter
public class ServiceException extends RuntimeException {
private Integer state;
public ServiceException(Integer state, String message) {
super(message);
this.state = state;
}
}
然后,另外自定义一个接口,用于声明各业务状态码的常量:
public interface ServiceCode {
Integer ERR_CONFLICT = 2;
Integer ERR_NOT_FOUND = 6;
Integer ERR_INSERT = 3;
Integer ERR_UPDATE = 4;
Integer ERR_DELETE = 5;
}
并且,在抛出异常时,向异常对象中封装以上业务状态码,例如:
String message = "添加相册失败!相册名称【" + name + "】已存在!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_CONFLICT, message);
this.axios.post(url, data).then(() => {
let data = response.data;
if (data.state == 1) {
// 成功
} else if (data.state == 2) {
// 显示 data.message
}
});
在前后端分离的开发模式下,如果前端(客户端)和后端(服务器端)并不是同一台主机或同一个IP地址,在默认情况下,是不允许跨域访问的(错误提示关键字为:CORS),需要在后端项目中添加以下配置类:
package cn.tedu.csmall.product.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Spring MVC的配置类
*
* @author [email protected]
* @version 0.0.1
*/
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
当服务器端接收来自客户的请求参数时,客户端的请求参数可以是以下2种格式:
JSON格式,例如:
{
"description": "小米80的相册的简介",
"name": "小米80的相册",
"sort": 88
}
FormData格式,例如:
name=小米80的相册&description=小米80的相册的简介&sort=88
如果使用了@RequestBody
,则客户端提交的请求参数必须是JSON格式的。
如果没有使用@RequestBody
,则客户端提交的请求参数必须是FormData格式的。
提示:如果使用了@RequestBody
,在Knife4j的调试界面,将没有各请求参数的输入框,而是需要自行填写JSON格式的请求参数。
qs是一个可以将JavaScript中的对象(与JSON格式相同)转换为FormData格式的框架!
在前端项目中,先安装qs:
npm i qs -S
然后,需要在main.js
中添加配置,以导入qs
并使用:
import qs from 'qs';
Vue.prototype.qs = qs;
接下来,在项目的任何视图组件中,都可以通过this.qs
来使用此对象。
在提交请求之前,可以使用qs
将JavaScript对象转换为FormData字符串,例如:
let formData = this.qs.stringify(this.ruleForm);
#{}
与${}
格式的占位符当使用#{}
占位符时,SQL语句会进行预编译处理,所以,不存在SQL注入的问题,#{}
格式的占位符只能表示某个值,不能表示SQL语句中的某个片段,不需要关注参数值的数据类型
预编译:不代入值的情况下,执行编译,后续执行时再将值代入。
当使用${}
占位符时,会先将值拼接到SQL语句中,再执行编译相关流程,所以,存在SQL注入的风险,${}
格式的占位符可以表示SQL语句中的任何片段,不仅仅只是某个值而已,只需要保证将值代入后拼接得到的SQL语句是合法的即可,但是,对于非数值类型(字符串、时间等)的值,需要使用一对单引号框住参数值
缓存(Cache):将原本需要查询的数据暂时存储到其它更易于读取的位置,并且,在后续查询数据时,从新的位置获取数据。
例如:通常查询数据是从数据库(例如MySQL等)位置进行查询,但是,MySQL的查询数据的效率其实很低!使用缓存的做法,可以是将前序的查询结果保存下来(不销毁),当下次再次查询同样的数据时,直接将此前保存下来的结果返回出去即可!
提示:关于将前序的查询结果保存下来,可以保存到应用服务器上,也可以保存在其它能够高效获取数据的位置。
Mybatis框架内置了缓存机制,分别是一级缓存(L1 Cache)和二级缓存(L2 Cache)。
**关于一级缓存:**通常也称之为Session缓存,或会话缓存,它是基于Mybatis的Session机制的,是默认开启的,人为不可控。
一级缓存的特点:必须是同一个会话(SqlSession)、同一个Mapper、执行同样的查询、查询的参数相同,则后续的查询会直接使用前序的查询结果,并不会反复执行查询!
一级缓存还有一些特点:如果更换SqlSession,则会重新查询,如果SqlSession关闭或调用了clearCache()
方法,则缓存数据会清空,或者,此表的数据发生了任何写(增删改)操作,缓存数据也会清空!
**关于二级缓存:**通常也称之为namespace缓存,在Spring Boot整合Mybatis的项目中,默认是全局开启,但各namespace默认未开启的!
二级缓存的特点:无论是否同一会话,只要是同一个namespace中的多次查询,均可应用二级缓存,Mybatis在查询数据时,会先检查二级缓存,如果命中,将直接返回结果,如果未命中,则检查一级缓存,如果命中则返回结果,如果仍未命中,则连接数据库执行查询。
二级缓存的使用:需要在配置SQL的XML文件中添加
节点,表示开启当前namespace的缓存。
如果同一个namespace执行了任何写操作,都会导致二级缓存数据被清空!
注意:使用二级缓存时,用于封装查询结果的类型必须实现Serializable
接口,否则查询时将出现异常!
另外,一旦使用了
,则当前namespace中所有的查询都是开启了二级缓存的,如果部分查询功能并不需要开启二级缓存,还可以在节点上配置
useCache="false"
。
结论:无论是一级缓存,还是二级缓存,都会因为发生了写操作而自动清空,这种机制通常并不满足生产环境的需求,所以,一般不会使用Mybatis的缓存机制!
单点登录,即SSO(Single Sign On),表现在多个服务中,在其中某1个服务登录后,其它的服务均能识别登录的用户的身份。
单点登录的解决方案:
共享Session
Token
需要处理:
spring-boot-starter-security
依赖jjwt
、fastjson
依赖LoginPrincipal
表示登录的当事人注意:具体实现的应该是“根据父级类别,查询其子级类别列表”。
业务规则:
is_parent
为0时,需要更新为1业务规则:
is_parent
更新为0
业务规则:
业务规则:
业务规则:
提示:
pms_category_attribute_template
表中插入数据业务规则:
业务规则:
提示:
pms_brand_category
表中插入数据业务规则:
Redis是一款基于内存使用了类似K-V结构来实现缓存数据的NoSQL非关系型数据库。
提示:Redis本身也会做数据持久化处理。
当已经安装Redis,并确保环境变量可用后,可以在命令提示符窗口(CMD)或终端(IDEA的Terminal,或MacOS/Linux的命令窗口)中执行相关命令。
在终端下,可以通过redis-cli
登录Redis客户端:
redis-cli
在Redis客户端中,可以通过ping
检测Redis是否正常工作,将得到PONG
的反馈:
ping
在Redis客户端中,可以通过set
命令向Redis中存入或修改简单类型的数据:
set name jack
在Redis客户端中,可以通过get
命令从Redis中取出简单类型的数据:
get name
如果使用的Key并不存在,使用
get
命令时,得到的结果将是(nil)
,等效于Java中的null
在Redis客户端中,可以通过keys
命令检索Key:
keys *
keys a*
注意:默认情况下,Redis是单线程的,keys
命令会执行整个Redis的检索,所以,执行时间可能较长,可能导致阻塞!
首先,需要添加spring-boot-starter-data-redis
依赖项:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
以上依赖项默认会连接localhost:6379
,并且无用户名、无密码,所以,当你的Redis符合此配置,则不需要在application.properties
/ application.yml
中添加任何配置就可以直接编程。如果需要显式的配置,各配置项的属性名分别为:
spring.redis.host
spring.redis.port
spring.redis.username
spring.redis.password
在使用以上依赖项实现Redis编程时,需要使用到的工具类型为RedisTemplate
,调用此类的对象的方法,即可实现读写Redis中的数据。
在使用之前,应该先在配置类中使用@Bean
方法创建RedisTemplate
,并实现对RedisTemplate
的基础配置,则在项目的根包下创建config.RedisConfiguration
类:
package cn.tedu.csmall.product.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.io.Serializable;
/**
* Redis的配置类
*
* @author [email protected]
* @version 0.0.1
*/
@Slf4j
@Configuration
public class RedisConfiguration {
@Bean
public RedisTemplate<String, Serializable> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Serializable> redisTemplate
= new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(RedisSerializer.json());
return redisTemplate;
}
}
拦截器:Interceptor
Spring MVC拦截器是Spring MVC框架中的一种组件,它可以执行在若干个请求之前、之后,通常用于解决处理若干个请求都需要执行的任务,例如验证用户是否已经登录等。
使用Spring MVC拦截器,首先,需要自定义类,作为拦截器类,这个类必须实现HandlerInterceptor
接口,例如:
@Slf4j
@Component
public class DemoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.debug("DemoInterceptor.preHandle()");
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.debug("DemoInterceptor.postHandle()");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.debug("DemoInterceptor.afterCompletion()");
}
}
每个拦截器都必须注册后才可以生效,在Spring MVC的配置类(实现了WebMvcConfigurer
接口的配置类)中重写addInterceptors()
方法即可实现注册,例如:
@Autowired
private DemoInterceptor demoInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(demoInterceptor)
.addPathPatterns("/brands", "/categories/list-by-parent");
}
通过测试运行,可以发现,拦截器的3个方法:
preHandle()
:在控制器(Controller
)之前执行,此方法的返回值是boolean
类型的,当返回true
时表示“放行”,当返回false
时表示“阻止”,当阻止时,程序不会向后继续运行,例如控制器将不会执行postHandle()
:在控制器(Controller
)之后执行afterCompletion()
:在处理完整个浏览,即将向客户端进行响应之前执行在配置拦截路径时,可以使用星号(*
)作为通配符,但是,只能匹配1层路径,例如:使用/brands/*
可以匹配/brands/add-new
、/brands/list
,却不可以匹配到/brands/1/delete
。
如果要匹配若干层路径,需要使用2个连续的星号(**
),例如:使用/brands/**
,可以匹配到/brands/add-new
、/brands/list
、/brands/1/delete
、/brands/1/status/disable
……
一旦使用通配符,可能导致匹配的范围过大,例如:配置为/admins/**
,将匹配到/admins/change-password
、/admins/upload-avatar
等,还会匹配到/admins/login
等,如果此拦截器是用于验证“是否登录”的,将/admins/login
也拦截是不合适的!
在配置拦截路径时,还可以调用excluedePathPatterns()
方法,在已有的拦截范围中添加“排除在外”的请求路径,例如:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(demoInterceptor)
.addPathPatterns("/admins/**")
.excludePathPatterns("/admins/login");
}
提示:以上addPathPatterns()
和excludePathPatterns()
这2个方法的参数都可以是可变参数,或List
集合。
以上讨论的拦截器(Interceptor
)是Spring MVC框架中的组件,而过滤器(Filter
)是Java EE中的组件。
过滤器是最早接收到客户端请求的组件,是执行在所有组件之前的,而拦截器是执行在Spring MVC的控制器(Controller
)之前和之后的。
过滤器只能配置“黑名单”,不可以配置“白名单”,所以,使用时并不是那么方便,而拦截器的配置更加灵活!
Redis是用于处理“缓存”的,当客户端尝试查询某些数据时,服务器端的处理流程大致是:
使用Redis后,可以明显的提高查询效率(当数据表中的数据量大时,效果明显),同时,还能减轻数据库服务器的压力。
在使用之前,还应该确定需要将哪些数据使用Redis处理查询,通常,应该是查询频率可能较高的、允许数据不够准确的(即使数据有一些不准确,但是对整个项目没有严重后果的),甚至这些数据极少改变的。
在具体使用时,可以直接使用RedisTemplate
去操作Redis,也可以对RedisTemplate
的使用进行再次封装。
缓存预热:启动项目时,就将缓存数据加载到Redis中。
在Spring Boot项目中,当需要实现“启动项目时直接执行”的效果,需要自定义组件类,实现ApplicationRunner
接口,重写其中的run()
方法,此run()
将在项目启动成功后自动执行。
提示:缓存预热的操作应该通过
ApplicationRunner
来实现,这样才可以保证在所有组件都已经正确的创建后再执行缓存预热,如果通过某些组件的构造方法来编写缓存预热的代码,此时某些组件可能还没有创建,则无法正确执行。
关于缓存预热的具体实现:
更新缓存的策略有多种,通常使用的可以是:
关于自动更新,需要使用到“计划任务”。
使用计划任务,需要自定义组件类,然后,在类中自定义方法(应该是public
权限,返回值类型声明为void
,参数列表为空),这个方法将作为计划任务执行的方法,在此方法上需要添加@Scheduled
注解,并配置其执行频率或特定的执行时间,最后,还需要在配置类上使用@EnableScheduling
注解,以开启当前项目的计划任务。
AOP:Aspect Oriented Programming,面向切面编程
注意:AOP并不是Spring框架独有的技术或特点,即使没有使用Spring框架,也可以实现AOP,但是,Spring框架很好的支持了AOP,所以,通常会使用Spring来实现AOP。
在开发实践中,数据的处理流程大致是:
注册:客户端 <---(请求)---> Controller <------> Service <------> Mapper
登录:客户端 <---(请求)---> Controller <------> Service <------> Mapper
下单:客户端 <---(请求)---> Controller <------> Service <------> Mapper
假设,现在添加一个需求:统计每个业务(Service中的方法)的执行耗时。
在没有AOP的情况下,只能编辑每个Service方法,添加几乎相同代码来实现以上需求,并且,当需求发生变化时,每个Service方法可能需要再次调整。
使用AOP实现以上需求,大致需要:
在项目中添加spring-boot-starter-aop
依赖。
在项目的根包下创建aop.TimerAspect
类,在类上添加@Component
和@Aspect
注解
long start = System.currentTimeMillis();
xxx;
long end = System.currentTimeMillis();
long t = end - start;
【技能描述】
commons-dbcp
、commons-dbcp2
、Hikari
、druid
),及相关框架技术,例如:Mybatis Plus等;