文章目录

  • 第一天
    • 关于此项目
    • 项目的开发流程
    • 关于数据库与数据表
    • 具体开发顺序
    • 实现数据访问层的开发
  • 第二天
    • LOMBOK
    • Mybatis框架
      • Mybatis框架的主要作用
      • 使用Mybatis的前期准备
      • 使用Mybatis插入数据
    • 插入数据时获取自动编号的id
      • 使用Mybatis删除数据
    • Spring Boot框架
      • 关于启动类
      • 关于配置文件
    • 课后作业
  • 第三天
    • MyBatis的动态SQL--foreach
    • 使用Mybaits修改数据
    • 使用Mybatis查询--统计
    • 使用Mybatis查询--根据id查询
    • 使用Mybatis查询数据列表
    • 作业
    • 1. 关于utf8mb4
    • 2. Mybatis中的`#{}`占位符
    • 3. 其它问题
  • 第四天
    • 关于Mybatis小结
    • 使用SLF4j日志
    • Spring MVC框架
      • Spring MVC框架的作用
      • 基础配置
      • 使用Spring MVC框架接收请求
      • 响应正文
      • @RequestMapping
      • 关于注解的源代码
      • 接收请求参数
    • RESTful风格
      • 作业
  • 第五天
    • 关于RESTful(续)
    • 关于MVC
    • 开发Service
    • 关于业务异常
    • Spring MVC统一处理异常
    • 使用Spring Validation检查请求参数
    • 周末作业
  • 第六天
    • Knife4j框架
    • 关于响应结果
  • 第七天
    • 前后端分离的跨域访问
    • 关于Spring MVC中的@RequestBody
    • 前端框架:qs
  • 第九天
    • 1. Mybatis的`#{}`与`${}`格式的占位符
    • 2. Mybatis的缓存机制
  • 第十四天
    • 单点登录
    • 在Product项目中实现授权访问
  • 作业
    • 显示类别列表
    • 添加类别
    • 删除类别
    • 启用与禁用类别
    • 创建属性模板
    • 显示属性模板
    • 删除属性模板
    • 关联类别与属性模板
    • 添加属性
    • 根据属性模板id查询属性列表
    • 删除属性
    • 显示品牌列表
    • 关联类别与品牌
    • 删除品牌
  • 第十六天
    • Redis
      • 关于Redis
      • Redis的简单操作
      • 在Spring Boot项目中读写Redis
  • 1. Spring MVC拦截器
    • 1.1. 关于拦截器
    • 1.2. 使用拦截器
    • 1.3. 配置拦截路径
    • 1.4. 拦截器与过滤器的区别
  • 第十八天
    • 在项目中应用Redis
    • 关于缓存预热
    • 关于更新缓存
    • Spring AOP
  • 第十九天

第一天

关于此项目

此项目是《酷鲨商城》的服务器端管理商品相关数据的项目(至于管理员、用户、订单等,并不在此项目中开发)。

此项目是使用Spring Boot作为基础框架的项目,在后续的使用过程中,将使用到主流的SSM(Spring / Spring MVC / Mybatis)、Spring Security、Spring Validation等框架。

早期流行的是SSH:Spring / Struts 2 / Hibernate

创建项目的参数:

  • Group:cn.tedu
  • Artifact:csmall-product
  • Package Name:cn.tedu.csmall.product
  • Java版本:1.8
  • Spring Boot父项目版本:2.5.9
  • 创建过程中勾选的依赖:无

创建项目完成后,pom.xml文件的内容为:

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion><!-- 父级项目版本,推荐暂时使用2.5.9 --><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.5.9</version><relativePath/> <!-- lookup parent from repository --></parent><!-- 当前项目信息 --><groupId>cn.tedu</groupId><artifactId>csmall-product</artifactId><version>0.0.1</version><name>jsd2204-csmall-product-teacher</name><description>这是酷鲨商城的商品管理服务的项目(学习中……)</description><!-- 属性 --><properties><!-- 使用的Java的版本 --><java.version>1.8</java.version></properties><!-- 当前项目使用的依赖项(框架、工具包) --><!-- scope > test:此依赖项的作用范围只是测试,仅能用于src/test下的代码,且不参与编译、打包 --><dependencies><!-- Spring Boot的基础依赖项 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><!-- Spring Boot的测试的依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></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_categoryleft join pms_brandon pms_brand_category.brand_id = pms_brand.idleft join pms_categoryon 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, '<div>iPhone 13的详情HTML</div>'),(2, '<div>小米11 Ultra的详情HTML</div>');-- 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种数据类型,应该先开发基础的、与其它数据不直接相关的数据类型,例如相册、名牌、类别等,其它类型的开发将后置。

对于相册、名牌、类型这些数据,基本的数据操作至少包括:

  • 插入数据
  • 根据id删除数据
  • 根据id查询数据详情
  • 根据id修改数据
  • 查询数据列表
  • 其它……

实现数据访问层的开发

数据访问层指的就是增删改查相关的数据操作,对于数据库中的数据操作,通常使用Mybatis框架来实现。

在实现Mybatis编程之前,首先,应该有各数据表对应的实体类,关于实体类的开发:

  • 实体类的名称应该与数据表名的关键字相对应,例如表名为pms_album,则实体类名为Album,表名为pms_attribute_template,则实体类名为AttributeTemplate

  • 实体类中的属性的类型应该与表设计保持一致,通常对应关系为:

    • 数据表字段类型 实体类属性类型
      bigint Long
      intsmallinttinyint Integer
      charvarchartext String
      date_time LocalDateTime
      decimal BigDecimal
  • 实体类中所有属性都应该是私有的

  • 实体类中所有属性都应该有对应的Setter & Getter方法【自动生成】

  • 实体类必须存在无参数构造方法

  • 实体类必须重写hashCode()equals(),且必须保证:hashCode()返回值相同时,equals()对比结果必须为truehashCode()返回值不同时,equals()对比结果必须为false【自动生成】

    • 提示:不同的开发工具、生成时使用的模板不同时,生成的代码可能不同,但不重要
  • 实体类都应该重写toString()方法,以输出所有字段的值,便于后续观察对象

  • 实体类都必须实现Serializable接口

    • 可以不定义序列化版本ID

建议将实体类放在项目根包下的entity包中(某些编程习惯中可能使用其它的包名,例如domain等)。

第二天

LOMBOK

LOMBOK是一款可以在编译期在类中自动生成某些代码的工具,通常用于自动生成:

  • Setters & Getters
  • hashCode() and equals()
  • toString()
  • 无参数构造方法
  • 全参数构造方法

在使用时,需要添加依赖项:

<!-- Lombok -->
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.20</version><scope>provided</scope>
</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框架

Mybatis框架的主要作用

Mybatis框架主要实现了简化持久层编程的问题。

持久层:实现数据持久化的一系列组件。

数据持久化:通常,在开发领域中,讨论的数据大多是在内存中的,而内存默认特指内存条(RAM:Random Access Memory),RAM的特性包含“一旦断电,数据将全部丢失”,且“正在执行的程序和数据都是在内存中的”,由程序处理的数据最终应该永久的保存下来,则不能将这些数据一直只存储在内存中,通常,会将数据存储到可以永久保存数据的存储介质中,典型的永久存储数据的存储介质有:硬盘、U盘、光盘等,所以,数据持久化就是将内存中的数据存储到硬盘等介质中,而硬盘中的数据是以文件的形式存在的,所以,通常可以将数据存储到文本文件中、XML文件、数据库中,这些存储方案中,只有数据库是便于实现增、删、改、查这4种操作的,所以,一般“数据持久化”默认指的就是将数据存储到数据库中。

在Java语言中,实现数据库编程需要先建立与数据库的连接,然后准备SQL语句,然后执行,然后获取执行结果并处理结果,最后,关闭或归还数据库连接,这是一套非常固定的流程,无论对哪个数据表执行哪种操作,其流程大致是固定的,所以,就产生了一些框架,用于简化这部分的编程。

在使用Mybatis时,只需要关注2点:

  • 在接口中定义抽象方法
  • 配置抽象方法映射的SQL语句

使用Mybatis的前期准备

在Spring Boot项目中,当需要使用Mybatis时,需要添加相关的依赖:

<!-- MySQL依赖项,仅运行时需要 -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope>
</dependency>
<!-- Mybatis整合Spring Boot的框架 -->
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.2</version>
</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 {@Testvoid contextLoads() {}@AutowiredDataSource dataSource;@Testvoid testConnection() throws Exception {dataSource.getConnection();}}

当配置的URL错误(含主机名错误、端口号错误),或MySQL未启动时,将出现以下错误:

com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failureThe 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插入数据

Mybatis要求抽象方法必须存在于接口中(因为其实现原理是基于接口的代理模式的),所以,在项目的根包下创建mapper.AlbumMapper接口。

提示:可以在接口上添加@Repository注解,避免在自动装配时IntelliJ IDEA误判而提示错误。

关于接口中的抽象方法:

  • 返回值类型:如果需要执行的SQL是增、删、改类型的,使用int作为返回值类型,表示“受影响的行数”,不建议使用void
  • 方法名称 :自定义,不要使用重载
    • 获取单个对象的方法用get做前缀
    • 获取多个对象的方法用list做前缀
    • 获取统计值的方法用count做前缀
    • 插入的方法用save/insert做前缀
    • 删除的方法用remove/delete做前缀
    • 修改的方法用update做前缀
  • 参数列表:如果需要执行的SQL语句有多个参数,应该将这些参数封装到自定义的数据类型中,并使用自定义的数据类型作为抽象方法的参数

则,插入相册时需要执行的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语句,但不推荐,因为:

  • 不便于配置较长的SQL语句
  • 不便于做一些复杂的配置,特别是查询时
  • 不便于实现与DBA(Database Administrator)分工合作

建议的做法是使用XML文件来配置SQL语句,可以从 http://doc.canglaoshi.org/config/Mapper.xml.zip 下载得到所需的文件,然后,在项目的src/main/resources下创建mapper文件夹,将得下载、解压得到的XML文件复制到此文件夹中。

关于配置SQL的XML文件:

  • 根节点必须是<mapper>
  • <mapper>上必须配置namespace属性,取值为对应的接口的全限定名
  • <mapper>的子级,根据需要执行的SQL语句,选择使用<insert><delete><update><select>中的某个节点,准备配置SQL语句,这些节点必须配置id属性,取值为抽象方法的名称(不包括括号和参数列表),并在这些节点内部配置SQL语句

提示:在本项目中,当插入数据时,不需要关注gmtCreategmtModified这2个字段的值的插入,后续将使用Xxx自动完成。

例如:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="cn.tedu.csmall.product.mapper.AlbumMapper"><!-- int insert(Album album); --><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 {@AutowiredAlbumMapper mapper;@Testvoid 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文件中,<mapper>namespace属性值配置有误,或者<insert>节点的id属性值配置有误,或者在application.properties中没有正确的配置mybatis.mapper-locations属性,都将出现以下错误:

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): cn.tedu.csmall.product.mapper.AlbumMapper.insert

【练习】

  • 实现:插入品牌(Brand)数据
  • 实现:插入类别(Category)数据
  • 练习提示:需要创建新的Mapper接口,需要使用新的XML文件,开发完成后,使用新的测试类进行测试,注意:在配置SQL时,在#{}中的名称是类中的属性名

插入数据时获取自动编号的id

在配置<insert>节点时,配置useGeneratedKeyskeyProperty这2个属性,就可以得到自动编号的id,例如:

<!-- int insert(Album album); -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
<!-- 省略后续代码 -->

其中,useGeneratedKeys="true"表示“需要获取自动编号的键的值”,keyProperty="id"表示将得到的自动编号的id值放回到参数对象的id属性中去!

开发规范上,对于自动编号的表进行插入数据时,都应该配置这2个属性!

使用Mybatis删除数据

需求:根据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);
}

【练习】

  • 实现:根据id删除品牌(Brand)数据
  • 实现:根据id删除类别(Category)数据

Spring Boot框架

关于启动类

每个创建好的Spring Boot项目的src/main/java下都有一个默认的包,且包下有一个带main()方法的类,此类就是整个项目的启动类,执行此类的main()方法将启动整个项目。

关于配置文件

在Spring Boot项目中,在src/main/resources下,默认就存在application.properties文件,此文件是Spring Boot会自动读取的配置文件。

Spring Boot使用了许多自动配置的机制,以至于我们只需要按照规定的名称去填写配置值,这些配置就会生效!

课后作业

实现以下数据表的“插入数据”和“根据id删除数据”的功能:

  • 属性表:pms_attribute
  • 属性模板表:pms_attribute_template
  • 品牌与类别的关联表:pms_brand_category
  • 图片表:pms_picture

第三天

MyBatis的动态SQL–foreach

动态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时,需要使用到<foreach>节点对参数进行遍历:

<!-- int deleteByIds(Long[] ids); -->
<delete id="deleteByIds">DELETE FROM pms_albumWHERE id IN (<foreach collection="array" item="id" separator=",">#{id}</foreach>)
</delete>

关于<foreach>节点的配置:

  • 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语句:

<!-- int insertBatch(List<Album> albums); -->
<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>

使用Mybaits修改数据

通常,修改数据时,也会使用到动态SQL的机制,当传入某个字段对应的值时,SQL中才会包含修改此字段的部分,反之,如果没有传入某个字段对应的值,则SQL语句不会包含修改此字段的部分!

这样的功能可以通过动态SQL的<if>标签来实现!

假设需要实现修改相册数据,传入的参数中包含哪些数据,就修改哪些数据,不包含的部分将不会被修改。

AlbumMapper接口中添加抽象方法:

int update(Album album);

AlbumMapper.xml中配置SQL语句:

<!-- int update(Album album); -->
<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>

使用Mybatis查询–统计

假设需要实现:统计相册表中的数据的数量

需要执行的SQL语句大致是:

select count(*) from pms_album

关于抽象方法:在查询时,方法的返回值类型只要求能够存入查询结果即可。

则在AlbumMapper中添加抽象方法:

int count();

然后,在AlbumMapper.xml中配置SQL语句,将使用<select>节点,此节点必须配置resultTyperesultMap这2个属性中的某1个,当使用resultType时,此属性的值取决于抽象方法的返回值类型,如果是基本数据类型(例如int等),则resultType属性的值就是类型名,如果是引用数据类型(例如StringAlbum等),则resultType属性的值就是类型的全限定名(在java.lang包下的可以省略包名)。

<!-- int count(); -->
<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.

使用Mybatis查询–根据id查询

假设需要实现:根据id查询相册详情

需要执行的SQL语句大致是:

select id, name, description, sort from pms_album where id=?

关于抽象方法的返回值类型,原则上,只需要能够“放得下”就行,所以,可以使用Album作为此次查询的返回值类型,但是,并不建议这样处理!通常,建议另创建类型,用于封装查询结果!另外创建的类型,通常并不会称之为实体类,并且,这种类型会添加一些后缀,关于后缀的使用,阿里的文档的参考

  • 数据对象:xxxDO,xxx 即为数据表名
  • 数据传输对象:xxxDTO,xxx 为业务领域相关的名称
  • 展示对象:xxxVO,xxx 一般为网页名称
  • POJO 是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO

关于以上后缀:

  • DO:Data Object
  • DTO:Data Transfer Object
  • VO:View Object / Value Object

对于本次查询,可以使用VO作为类型的后缀,完整的类名可以使用AlbumStandardVO,此类应该放在项目的根包的pojo.vo包下:

@Data
public class AlbumStandardVO implements Serializable {// 除了gmtCreate和gmtModified以外的所有属性
}

提示:此前涉及的“实体类编写规范”本质上是POJO的规范!

接下来,在AlbumMapper接口中添加抽象方法:

AlbumStandardVO getStandardById(Long id);

AlbumMapper.xml中配置SQL:

<!-- AlbumStandardVO getStandardById(Long id); -->
<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时,为查询的字段自定义列名,使得“查询结果中的列名”与“封装结果的类型中的属性名”是一致的,例如:

<!-- BrandStandardVO getStandardById(Long id); -->
<select id="getStandardById" resultType="cn.tedu.csmall.product.pojo.vo.BrandStandardVO">SELECTid, name, pinyin, logo, description,keywords, sort, sales, product_count AS productCount, comment_count AS commentCount,positive_comment_count AS positiveCommentCount, enableFROM pms_brandWHERE 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>

<resultMap>内部,使用<result>节点,配置其columnproperty属性,用于指定列名与属性名的对应关系,例如:

<resultMap id="自定义的ResultMap名称" type="封装查询结果的类型的全限定名"><result column="product_count" property="productCount" /><result column="comment_count" property="commentCount" /><result column="positive_comment_count" property="positiveCommentCount" />
</resultMap>

提示:在普通的单表查询中,列名与属性名本身就对应的部分,并不需要在<resultMap>中配置。

另外,在开发实践中,建议将查询的字段列表使用<sql>节点进行封装,然后,在配置的SQL语句中,使用<include>节点进行调用即可:

<!-- BrandStandardVO getStandardById(Long id); -->
<select id="getStandardById" resultMap="StandardResultMap">SELECT<include refid="StandardQueryFields"/>FROM pms_brandWHERE 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>

当在<select>上使用resultType属性,取值却是<resultMap>的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

当在<select>上使用了resultMap,取值错误时(例如取值为类型的全限定名),将出现以下错误:

java.lang.IllegalArgumentException: Result Maps collection does not contain value for cn.tedu.csmall.product.pojo.vo.BrandStandardVO

使用Mybatis查询数据列表

查询列表与查询某1个数据的开发过程相差不大,主要区别在于:

  • 查询列表时,需要查询的字段通常更少
  • Mybatis会自动使用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:

<!-- List<BrandListItemVO> list(); -->
<select id="list" resultMap="ListItemResultMap">SELECT<include refid="ListItemQueryFields"/>FROM pms_brandORDER 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,还是配置<resultMap>,关于数据类型,都只需要指定为List中的元素类型即可!

作业

完成以下数据表的功能:“根据id查询数据”(已完成的则跳过)、“根据名称统计数据数量”

  • 品牌表
  • 相册表
  • 类别表

1. 关于utf8mb4

utf8mb4是MySQL / MariaDB中的一种字符集。

在当前主流版本的MySQL / MariaDB中,使用utf8作为字符集时,默认表示的是utf8mb3

关于utf8mb3utf8mb4,其主要区别在于:most bytes 3most bytes 4,即最多使用3 / 4个字节来表示1个字符!所以,当使用utf8mb4时,可以表示更多字符,例如生僻汉字、冷门符号、emoji表情符号等。

UTF指的是:Unicode Transfer Format,即Unicode传输编码。

在使用MySQL / MariaDB时,所有SQL语句中涉及的字符集都明确的使用utf8mb4,而不要使用utf8

2. Mybatis中的#{}占位符

在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个,但不是基本值时,在#{}的大括号里,必须写参数的数据类型的属性名,例如:

<!-- int insert(Album album); -->
<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注解以配置参数的名称!

3. 其它问题

  • 在Mybatis中#{}格式的占位符与${}格式的占位符的区别
  • Mybatis的缓存机制

第四天

关于Mybatis小结

  • 【理解】Mybatis主要解决了:简化数据库编程
  • 【了解】使用Mybatis时需要添加依赖:mysql-connector-javamybatis-spring-boot-startermybatis + mybatis-spring + spring-context + spring-jdbc + 数据库连接池)
  • 【掌握】在配置类上使用@MapperScan配置接口所在的包,并在application.properties中配置XML文件的位置
  • 【了解】在application.properties配置连接数据库的参数
  • 【掌握】POJO的规范:属性私有化,实现Serializable,全属性的Setters & Getters,hashCode()equals()、存在无参数的构造方法
    • toString()并不是规范所要求的
  • 【掌握】抽象方法的设计原则
    • 返回值类型:增删改使用int,查询使用可以装得下结果的类型即可
    • 方法名称:自定义,不要重载,最好参考规范
    • 参数列表:取决于需要执行的SQL语句中的参数,当参数较多时,应该封装
  • 【掌握】在XML中配置SQL
    • 此XML文件不是凭空创建的,应该从别处复制粘贴得到(此XML顶部的声明不易于手动编写)
    • 每个XML文件都必须使用<mapper>作为根节点,且配置namespace属性,此属性的值是对应的接口的全限定名
    • 区分使用<insert><delete><update><select>节点
      • <delete><update>可以浑为一谈
      • 在不考虑获取自动编号的值的情况,<insert><delete><update>都可以浑为一谈
    • <insert><delete><update><select>这些节点都必须配置id属性,取值为对应的抽象方法的名称
    • <insert>可以配置useGeneratedKeyskeyProperty属性,用于获取自动编号的id
    • <select>必须配置resultTyperesultMap其中的某1个(二选一)
    • 使用<foreach>可以实现对参数的遍历,可以实现批量删除、批量插入、批量修改……
    • 使用<set>结合<if>实现按需更新数据
    • 使用<sql>封装SQL语句片段,并使用<include>调用
    • 使用<resultMap>指导Mybatis封装查询结果

使用SLF4j日志

日志可以用于在程序执行过程中,向控制台或文件或数据库等位置输出一些自定义的信息。

注意:在开发实践中,不要在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 {@Testvoid 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 MVC框架的作用

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下自动创建statictemplates文件夹。

一旦添加了spring-boot-starter-web,当启动Spring Boot项目时,会自动启动Tomcat,并将此项目部署到Tomcat上。

Spring Boot启动Tomcat时,默认占用8080端口,可以在application.properties中通过server.port属性来修改端口号,例如:

# 服务端口
server.port=9080

使用Spring MVC框架接收请求

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 java@tedu.cn* @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

@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中的valuepath是等效的,所以,配置方式也完全相同!另外,之所以有这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种做法可以共存。

甚至,可以按需添加某些特定的参数,例如:HttpServletRequestHttpServletReponseHttpSession、其它框架允许的且添加了特定的注解的参数。

RESTful风格

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类中的属性可以待定)。

第五天

关于RESTful(续)

在设计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操作)

通常,绝大部分应用中,在处理业务时(并不是直接操作某数据),并不会采纳以上建议!

最后,在开发实践中,更多的还是只使用GETPOST这2种请求方式,关于RESTful 风格的URL设计参考:

  • 查询列表:/数据类型的复数

    • 例如:/albums
  • 查询指定id的数据:/数据类型的复数/id值
    • 例如:/albums/{id}
  • 对指定id的数据进行某操作:/数据类型的复数/id值/操作
    • 例如:/albums/{id}/delete

关于MVC

MVC = Model + View + Controller

MVC为设计软件提供了基本的思想,它认为每个软件都应该至少包含这3大部分,且各部分分工明确,只负责整个数据处理流程中的一部分功能。

例如V通常表现为“软件的界面”,用于呈现数据、提供用户操作的控件。

C表示控制器,用于接收请求、响应结果,并不会处理实质业务。

M表示数据模型,通常由业务逻辑和数据访问这2部分组成,在开发实践中,数据访问通常指的就是数据库编程,而业务逻辑是由专门的类来实现的,这样的类通常使用Service作为类名的关键字。

在整个数据处理过程中,将会是:Controller调用Service,而Service调用Mapper。

业务逻辑的主要职责是:设计业务流程,处理业务逻辑,以保证数据的完整性和安全性。

开发Service

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);// 判断统计结果是否大于0if (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:nowalbum.setGmtCreate(now);// 补全Album对象中各属性的值:gmtModified:nowalbum.setGmtModified(now);// 调用AlbumMapper对象的int insert(Album album)方法插入相册数据log.debug("即将向数据库中插入数据:{}", album);albumMapper.insert(album);
}

关于业务异常

通常,建议自定义异常,用于表示在业务逻辑层中的“失败”(或错误),而不要使用已知的异常类型,避免捕获、处理不准确!

可以在项目的根包下创建ex.ServiceException类,继承自RuntimeException

public class ServiceException extends RuntimeException {// 生成5个构造方法
}

Spring MVC统一处理异常

Spring MVC框架提供了统一处理异常的机制,使得每种类型的异常在处理时,只需要编写1次相关代码即可。

通常,统一处理异常的代码会写在专门的类中,此类应该添加@ControllerAdvice,则类中相关的方法会在处理每个请求时生效!

由于目前采取前后端分离的模式,处理异常后的响应方式是响应正文,所以,还应该使用@ResponseBody,或者,使用@RestControllerAdvice,它同时具有@ControllerAdvice@ResponseBody的效果。

@RestControllerAdvice
public class GlobalExceptionHandler {
}

然后,在此类中添加处理异常的方法:

  • 注解:必须添加@ExceptionHandler注解,表示此方法是统一处理异常的方法
  • 访问权限:应该使用public
  • 返回值类型:参考处理请求的方法
  • 方法名称:自定义
  • 参数列表:至少包含1个异常类型的参数,表示需要处理的异常,或理解为Spring MVC框架在调用控制器的方法后捕获的异常,另外,可按需添加HttpServletRequestHttpServletResponse等少量特定类型的参数

所以,完整的处理异常的代码为:

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 {@ExceptionHandlerpublic 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 {@ExceptionHandlerpublic String handleServiceException(ServiceException e) {log.debug("处理ServiceException:{}", e.getMessage());return e.getMessage();}@ExceptionHandlerpublic String handleThrowable(Throwable e) {log.debug("处理Throwable");e.printStackTrace();return "程序运行过程中出现意外错误,请联系系统管理员!";}}

使用Spring Validation检查请求参数

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:通过此注解的minmax属性可以指定整型数据的最小值和最大值
    • 提示:此注解可以和@NotNull一起使用

周末作业

关于根据id删除数据,在处理业务时,应该先根据id查询数据,检查此数据是否存在,然后再删除。

完成各数据的添加和根据id删除,包含Mapper层、业务逻辑层、控制器层。

第六天

Knife4j框架

Knife4j框架是一款基于Swagger 2框架的、能够基于项目中的控制器的代码来生成在线API文档的框架,另外,此框架还有调试功能,可以向服务器端发送请求,并获取响应结果。

关于此框架,要使之能够使用,需要:

  • 添加依赖
  • 添加配置类
  • application.properties中添加1条配置

关于依赖的代码:

<!-- Knife4j Spring Boot:在线API -->
<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>2.0.9</version>
</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 java@tedu.cn* @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 = "java@tedu.cn";/*** 版本号*/private String version = "1.0.0";@Autowiredprivate OpenApiExtensionResolver openApiExtensionResolver;public Knife4jConfiguration() {log.debug("加载配置类:Knife4jConfiguration");}@Beanpublic 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

注意:

  • 当前项目的Spring Boot版本必须是2.6以下的版本(2.6不可用)

    • 如果要使用更高版本的Spring Boot,必须使用更高版本的Knife4j
  • 在配置类中的basePackage必须是控制器类所在的包,记得需要修改

完成后,启动项目,通过 /doc.html 即可访问在线API文档。

在开发实践中,还应该对在线API文档进行细化,需要在控制器及相关类中进行一些配置:

  • 在控制器类上添加@Api注解,配置tags属性,此属性是String类型的
  • 在控制器类中处理请求的方法上添加@ApiOperation注解,配置value属性,此属性是String类型的
  • 在控制器类中处理请求的方法上添加@ApiOperationSupport注解,配置order属性,此属性是int类型的
    • 此属性用于排序,数据越小越靠前,不建议使用1位的数字
  • 在控制器类中处理请求的方法上,不要再使用没有限制请求方式的@RequestMapping,建议使用@GetMapping@PostMapping
  • 如果处理请求的方法中,如果参数是封装的数据类型,应该在此类型的各属性上添加@ApiModelProperty注解,以配置对参数的说明
  • 如果处理请求的方法中,如果参数并没有封装,则需要使用@ApiImplicitParams@ApiImplicitParam这2个注解组合来配置
    • 注意:一旦配置了@ApiImplicitParam,原本的提示的值会被覆盖,应该完整的配置各属性

完整的配置示例–AlbumController

@Api(tags = "04. 相册管理模块")
@Slf4j
@RestController
@RequestMapping("/albums")
public class AlbumController {@Autowiredprivate 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;}

关于响应结果

当服务器端向客户端响应数据时,除了必要的提示文本以外,还应该响应“业务状态码”到客户端,以便于客户端程序能够便捷且准确的判断当前请求的执行结果!

另外,某些操作是需要向客户端响应数据的

所以,向客户端的响应数据至少需要包含以下部分:

  • 业务状态码:本质上是一个数值,由服务器端和客户端共同约定每个数值的意义
  • 业务执行出错时的描述文本
  • 数据:当处理请求成功时,可能需要响应某些数据到客户端(通常是客户端发起GET请求,当然,某些POST请求可能也需要响应数据)

以上做法应该是针对所有请求都是如此响应的,通常,会自定义某个类型,用于封装以上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,由于使用了统一处理异常的机制,会导致所有异常的业务状态码都是相同的!为了解决此问题,可以:

  • 创建多种异常类型,针对不同的错误,在Service层抛出不同的异常
  • 在Service层每次抛出异常时,向异常对象中封装业务状态码

如果采取以上第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 java@tedu.cn* @version 0.0.1*/
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOriginPatterns("*").allowedMethods("*").allowedHeaders("*").allowCredentials(true).maxAge(3600);}}

关于Spring MVC中的@RequestBody

当服务器端接收来自客户的请求参数时,客户端的请求参数可以是以下2种格式:

  • JSON格式,例如:

    • {"description": "小米80的相册的简介","name": "小米80的相册","sort": 88
      }
      
  • FormData格式,例如:

    • name=小米80的相册&description=小米80的相册的简介&sort=88
      

如果使用了@RequestBody,则客户端提交的请求参数必须是JSON格式的。

如果没有使用@RequestBody,则客户端提交的请求参数必须是FormData格式的。

提示:如果使用了@RequestBody,在Knife4j的调试界面,将没有各请求参数的输入框,而是需要自行填写JSON格式的请求参数。

前端框架:qs

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);

第九天

1. Mybatis的#{}${}格式的占位符

当使用#{}占位符时,SQL语句会进行预编译处理,所以,不存在SQL注入的问题,#{}格式的占位符只能表示某个值,不能表示SQL语句中的某个片段,不需要关注参数值的数据类型

预编译:不代入值的情况下,执行编译,后续执行时再将值代入。

当使用${}占位符时,会先将值拼接到SQL语句中,再执行编译相关流程,所以,存在SQL注入的风险,${}格式的占位符可以表示SQL语句中的任何片段,不仅仅只是某个值而已,只需要保证将值代入后拼接得到的SQL语句是合法的即可,但是,对于非数值类型(字符串、时间等)的值,需要使用一对单引号框住参数值

2. Mybatis的缓存机制

缓存(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文件中添加<cache/>节点,表示开启当前namespace的缓存。

如果同一个namespace执行了任何写操作,都会导致二级缓存数据被清空!

注意:使用二级缓存时,用于封装查询结果的类型必须实现Serializable接口,否则查询时将出现异常!

另外,一旦使用了<cache/>,则当前namespace中所有的查询都是开启了二级缓存的,如果部分查询功能并不需要开启二级缓存,还可以在<select>节点上配置useCache="false"

结论:无论是一级缓存,还是二级缓存,都会因为发生了写操作而自动清空,这种机制通常并不满足生产环境的需求,所以,一般不会使用Mybatis的缓存机制!

第十四天

单点登录

单点登录,即SSO(Single Sign On),表现在多个服务中,在其中某1个服务登录后,其它的服务均能识别登录的用户的身份。

单点登录的解决方案:

  • 共享Session

    • 通常,可以选择将所有Session存储在专门的Redis服务器中,其它需要验证、读取用户身份的服务,都将从此Redis服务器中访问用户的Session信息
  • Token

    • 直接记录用户身份的信息,各服务器端只需要使用共同的验证、解析机制,即可识别用户的身份

在Product项目中实现授权访问

需要处理:

  • 添加spring-boot-starter-security依赖
  • 添加jjwtfastjson依赖
  • 创建Security配置类,在此配置类中:
    • 将Knife4j相关的URL设计为“白名单”
    • 允许跨域访问
    • 添加JWT过滤器
  • 创建JWT过滤器
  • 创建LoginPrincipal表示登录的当事人

作业

显示类别列表

注意:具体实现的应该是“根据父级类别,查询其子级类别列表”。

添加类别

业务规则:

  • 如果选择了父级类别,且父级类别不存在,不允许添加
  • 深度(depth),由服务器端直接决定,值为:当无父级类别时,值为1,当有父级类别时,值为父级类别的深度+1
  • 是否父级(is_parent),新添加的类别,此值固定为0;如果有父级类别,且父级类别的is_parent为0时,需要更新为1

删除类别

业务规则:

  • 如果数据不存在,不允许删除
  • 如果仍存在子级类别,不允许删除
  • 如果被删除的类别的父级没有更多子级,则需要将父级的is_parent更新为0
  • 如果此类别关联了品牌,不允许删除
  • 如果此类别关联了属性模板,不允许删除

启用与禁用类别

业务规则:

  • 如果数据不存在,不允许启用/禁用
  • 如果数据的当前状态与目标状态相同,不允许设置
    • 例如当前为启用,目标也是启用,则不允许

创建属性模板

业务规则:

  • 每个属性模板的名称必须唯一

显示属性模板

删除属性模板

业务规则:

  • 如果数据不存在,则不执行删除
  • 如果有“属性”数据关联到此模板,则不允许删除
  • 如果此属性模板关联了类别,不允许删除

关联类别与属性模板

提示:

  • 关联这2个数据,本质上是向pms_category_attribute_template表中插入数据
  • 前端页面设计难度较大,可以暂时使用2个输入框来输入类别的id、属性模板的id

添加属性

业务规则:

  • 属性模板必须存在

根据属性模板id查询属性列表

删除属性

业务规则:

  • 如果数据不存在,则不执行删除

显示品牌列表

关联类别与品牌

提示:

  • 关联这2个数据,本质上是向pms_brand_category表中插入数据
  • 前端页面设计难度较大,可以暂时使用2个输入框来输入类别的id、品牌的id

删除品牌

业务规则:

  • 如果数据不存在,不允许删除
  • 如果此品牌已经关联了类别,不允许删除

第十六天

Redis

关于Redis

Redis是一款基于内存使用了类似K-V结构来实现缓存数据的NoSQL非关系型数据库。

提示:Redis本身也会做数据持久化处理。

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项目中读写Redis

首先,需要添加spring-boot-starter-data-redis依赖项:

<!-- Spring Data Redis:读写Redis -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</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 java@tedu.cn* @version 0.0.1*/
@Slf4j
@Configuration
public class RedisConfiguration {@Beanpublic 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;}}

1. Spring MVC拦截器

1.1. 关于拦截器

拦截器:Interceptor

Spring MVC拦截器是Spring MVC框架中的一种组件,它可以执行在若干个请求之前、之后,通常用于解决处理若干个请求都需要执行的任务,例如验证用户是否已经登录等。

1.2. 使用拦截器

使用Spring MVC拦截器,首先,需要自定义类,作为拦截器类,这个类必须实现HandlerInterceptor接口,例如:

@Slf4j
@Component
public class DemoInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.debug("DemoInterceptor.preHandle()");return false;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {log.debug("DemoInterceptor.postHandle()");}@Overridepublic 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.3. 配置拦截路径

在配置拦截路径时,可以使用星号(*)作为通配符,但是,只能匹配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集合。

1.4. 拦截器与过滤器的区别

以上讨论的拦截器(Interceptor)是Spring MVC框架中的组件,而过滤器(Filter)是Java EE中的组件。

过滤器是最早接收到客户端请求的组件,是执行在所有组件之前的,而拦截器是执行在Spring MVC的控制器(Controller)之前和之后的。

  • 基于以上特点,某些问题只能通过过滤器来解决,例如:

    • 设置字符编码
    • Spring Security的相关过滤器

过滤器只能配置“黑名单”,不可以配置“白名单”,所以,使用时并不是那么方便,而拦截器的配置更加灵活!

第十八天

在项目中应用Redis

Redis是用于处理“缓存”的,当客户端尝试查询某些数据时,服务器端的处理流程大致是:

  • 优先从Redis中获取数据

    • 如果Redis中没有所需的数据,则从数据库中查询,并将查询结果存入到Redis
  • 将Redis中的数据(或:刚刚从数据库中查询出来的数据)响应到客户端

使用Redis后,可以明显的提高查询效率(当数据表中的数据量大时,效果明显),同时,还能减轻数据库服务器的压力。

在使用之前,还应该确定需要将哪些数据使用Redis处理查询,通常,应该是查询频率可能较高的、允许数据不够准确的(即使数据有一些不准确,但是对整个项目没有严重后果的),甚至这些数据极少改变的。

在具体使用时,可以直接使用RedisTemplate去操作Redis,也可以对RedisTemplate的使用进行再次封装。

关于缓存预热

缓存预热:启动项目时,就将缓存数据加载到Redis中。

在Spring Boot项目中,当需要实现“启动项目时直接执行”的效果,需要自定义组件类,实现ApplicationRunner接口,重写其中的run()方法,此run()将在项目启动成功后自动执行。

提示:缓存预热的操作应该通过ApplicationRunner来实现,这样才可以保证在所有组件都已经正确的创建后再执行缓存预热,如果通过某些组件的构造方法来编写缓存预热的代码,此时某些组件可能还没有创建,则无法正确执行。

关于缓存预热的具体实现:

  • 删除所有相关的缓存数据

    • 删除列表数据:如果不删除,再次向缓存中写入列表,将是在原列表的基础上追加,则会产生重复的列表项
    • 删除数据项(每一个数据):如果不删除,则会导致原本已经缓存的数据一直存在,某些数据可能在数据库中已经删除,则缓存中的数据也应该被删除
  • 从数据库中查询列表,并写入到缓存
  • 基于查询到的列表,遍历,得到每个数据的id,再从数据库中查出各数据,并写入到缓存

关于更新缓存

更新缓存的策略有多种,通常使用的可以是:

  • 手动更新

    • 适用于数据变化频率非常低的应用场景,这些数据的缓存可以是长期存在,偶尔需要更新时,手动更新即可
  • 自动更新
    • 适用于数据频繁的变化,通过手动更新不太现实,将会是每间隔一段时间,或在特定的某个时间(例如每周一凌晨3点)自动更新

关于自动更新,需要使用到“计划任务”。

使用计划任务,需要自定义组件类,然后,在类中自定义方法(应该是public权限,返回值类型声明为void,参数列表为空),这个方法将作为计划任务执行的方法,在此方法上需要添加@Scheduled注解,并配置其执行频率或特定的执行时间,最后,还需要在配置类上使用@EnableScheduling注解,以开启当前项目的计划任务。

Spring AOP

AOPAspect 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框架管理
  • 配置切面类中的方法在特定的点执行

在项目中添加spring-boot-starter-aop依赖。

在项目的根包下创建aop.TimerAspect类,在类上添加@Component@Aspect注解

long start = System.currentTimeMillis();xxx;long end = System.currentTimeMillis();long t = end - start;

第十九天

【技能描述】

  1. 【了解/掌握/熟练掌握】开发工具的使用,包括:Eclipse、IntelliJ IDEA、Git、Maven;
  2. 【了解/掌握/熟练掌握】Java语法,【理解/深刻理解】面向对象编程思想,【了解/掌握/熟练掌握】Java SE API,包括:String、日期、IO、反射、线程、网络编程、集合、异常等;
  3. 【了解/掌握/熟练掌握】HTML、CSS、JavaScript前端技术,并【了解/掌握/熟练掌握】前端相关框架技术及常用工具组件,包括:jQuery、Bootstrap、Vue脚手架、Element UI、axios、qs、富文本编辑器等;
  4. 【了解/掌握/熟练掌握】MySQL的应用,【了解/掌握/熟练掌握】DDL、DML的规范使用;
  5. 【了解/掌握/熟练掌握】数据库编程技术,包括:JDBC、数据库连接池(commons-dbcpcommons-dbcp2Hikaridruid),及相关框架技术,例如:Mybatis Plus等;
  6. 【了解/掌握/熟练掌握】主流框架技术的规范使用,例如:SSM(Spring,Spring MVC, Mybatis)、Spring Boot、Spring Validation、Spring Security等;
  7. 【理解/深刻理解】Java开发规范(参考阿里巴巴的Java开发手册);
  8. 【了解/掌握/熟练掌握】基于RESTful的Web应用程序开发;
  9. 【了解/掌握/熟练掌握】基于Spring Security与JWT实现单点登录;

第四阶段product笔记相关推荐

  1. 视觉SLAM十四讲学习笔记-第七讲-视觉里程计-特征点法和特征提取和匹配实践

    专栏系列文章如下: 视觉SLAM十四讲学习笔记-第一讲_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习笔记-第二讲-初识SLAM_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习 ...

  2. Think in Java第四版 读书笔记9第15章 泛型

    Think in Java第四版 读书笔记9第15章 泛型 泛型:适用于很多很多的类型 与其他语言相比 Java的泛型可能有许多局限 但是它还是有很多优点的. 本章介绍java泛型的局限和优势以及ja ...

  3. python数据分析的四阶段以及电商数据描述性分析和探索性分析

    目录 数据分析的四阶段 1 需求 数据情况 需求 产出 2 数据规整(数据预处理,数据清洗,数据重构) 2.1 数据预处理 2.1.1 发现错误的对策 2.1.2 修正缺失值 2.2 修正错误数据 方 ...

  4. stm32阶段总结笔记一

    第一阶段总结笔记,主要对GPIO,串口,时钟,中断,定时器进行总结,方便后续写程序快速查阅,寄存器用来了解库函数具体怎么配置的 文章目录 一.GPIO 寄存器 库函数操作 二.中断 外部中断 寄存器 ...

  5. 视觉SLAM十四讲学习笔记-第七讲-视觉里程计-三角测量和实践

     专栏汇总 视觉SLAM十四讲学习笔记-第一讲_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习笔记-第二讲-初识SLAM_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习笔记-第 ...

  6. 视觉SLAM十四讲学习笔记-第七讲-视觉里程计-对极几何和对极约束、本质矩阵、基础矩阵

    专栏系列文章如下:  专栏汇总 视觉SLAM十四讲学习笔记-第一讲_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习笔记-第二讲-初识SLAM_goldqiu的博客-CSDN博客 视觉SLA ...

  7. 视觉SLAM十四讲学习笔记-第六讲学习笔记总结(1)---非线性优化原理

    第六讲学习笔记如下: 视觉SLAM十四讲学习笔记-第六讲-非线性优化的状态估计问题_goldqiu的博客-CSDN博客 ​​​​​​视觉SLAM十四讲学习笔记-第六讲-非线性优化的非线性最小二乘问题_ ...

  8. 视觉SLAM十四讲学习笔记-第四讲---第五讲学习笔记总结---李群和李代数、相机

    第四讲---第五讲学习笔记如下: 视觉SLAM十四讲学习笔记-第四讲-李群与李代数基础和定义.指数和对数映射_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习笔记-第四讲-李代数求导与扰动模 ...

  9. 视觉SLAM十四讲学习笔记---前三讲学习笔记总结之SLAM的作用、变换和位姿表示

    经过半年学习SLAM相关知识,对SLAM系统有了一些新的认识,故回看以前的学习记录,做总结和校正. 前三讲学习笔记如下: 视觉SLAM十四讲学习笔记-第一讲_goldqiu的博客-CSDN博客 视觉S ...

最新文章

  1. 基于Shodan Python库的批量攻击实践 撒旦网
  2. MySQL参数调优[转载]
  3. PHP生成各种验证码和Ajax验证
  4. 如何通过postman测试需要登录授权的接口
  5. 318. Maximum Product of Word Lengths
  6. Linux学习:第四章-vi编辑器
  7. vscode 修改(自定义)插件的快捷键
  8. python中判断对象是否可迭代
  9. IT转型之路(一) 迷茫、困惑
  10. 网页Flash实现图片轮播特效
  11. Telegram普通账号定时发布信息、签到
  12. ICCV 2021 Oral | Paint Transformer - 基于笔触预测的快速油画渲染算法
  13. 台式计算机如何双屏显示,一个电脑主机怎样接两个显示器_一个电脑连接两个显示器如何操作-win7之家...
  14. 虚拟机服务器警报设置,设置VMware vCenter警报器获得更好的管理
  15. JAVA中Action层, Service层 ,model层 和 Dao层的功能区分
  16. Delphi XE7中使用JSON
  17. Renesas R-Car sound 驱动说明
  18. 非法作坊死猪拌入剧毒农药腌制腊肉公开销卖
  19. php 高级函数,10条PHP高级技巧[修正版]
  20. [蓝桥杯] 刷题记录(2020.3.8)

热门文章

  1. Android 音乐播放器的通知栏
  2. 操作Doris数据库错误记录——insert has filtered data in strict mode
  3. 打开word文档时出现严重错误解决方法
  4. Flash CS3的ActionScript3入门
  5. 美国4大移动运营商手机网络制式及频段
  6. 人生第一次心理咨询逐字稿以及分析
  7. TPM模拟器和TPM2-TSS安装
  8. Godot Engine:用Shader实现旗帜飘飘的效果
  9. node 框架 LoopBack 教程
  10. 飘雪网吧专用XP系统 v7.0