第四阶段课程
主要学习的框架技术:
- SSM(Spring、Spring MVC、Mybatis)
- Spring Boot
- Spring Validation
- Spring Security
- 其它……
项目介绍:酷鲨商城运营管理平台(商品管理平台、管理员管理平台、前端)
创建项目
如果默认的创建项目的URL(start.spring.io)暂时不可用,可尝试切换为https://start.spri…
在以上界面中,版本可以随意选择,后续当项目创建出来后,通过源代码再调整为习惯使用的版本。
并且,在以上界面中,可以不必勾选任何依赖项,都可以在创建创建出来之后再通过编辑源代码来添加依赖项。
调整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>
<!-- 父级项目的版本,注意使用的版本即可 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- 当前项目的信息 -->
<groupId>cn.tedu</groupId>
<artifactId>csmall-product</artifactId>
<version>0.0.1</version>
<!-- 属性配置 -->
<properties>
<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>
<!-- 构建项目的配置 -->
<build>
<!-- 若干个插件 -->
<plugins>
<!-- Spring Boot对Maven的支持的插件 -->
<!-- 用于将项目打包,打包时会将当前项目的各依赖包也全部打进同一个jar包文件 -->
<!-- 如果此代码报错,可以选择: -->
<!-- 1. 将以下代码删除,后续将无法打完整的包,但对开发和在IntelliJ IDEA中运行没有影响 -->
<!-- 2. 显式的配置version标签,指定和Spring Boot父项目相同的版本号 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
创建数据库与数据库
创建名为mall_pms的数据库:
CREATE DATABASE mall_pms;
例如:
然后,在IntelliJ IDEA中配置Database面板:
然后,打开Database面板中的Console,将创建表的SQL语句粘贴到此处,全选并执行,即可创建当前库中所有数据库,同时还插入了一些测试数据。
**附:**配置Database面板的视频教程:doc.vrd.net.cn/doc/idea_da…
添加数据库编程的依赖
在pom.xml中添加依赖项:
<!-- Mybatis整合Spring Boot的依赖项 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- MySQL的依赖项 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
在Spring Boot项目中,添加以上依赖项(mybatis-spring-boot-starter)后,默认启动项目时或执行任何基于Spring Boot的测试时会报错!因为Spring Boot项目会在添加数据库编程的依赖项后,自动配置DataSource,需要读取配置文件中连接数据库的信息,目前,还没有进行相关配置,所以读取不到信息,就会报错!
则在src/main/resources/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时就不会报错了!
**注意:**以上各属性的值是区分大小写的!
为了检验以上配置值是否正确,在测试类中添加:
@Autowired // 禁止随意添加required=false
DataSource dataSource;
@Test
void getConnection() throws Throwable {
dataSource.getConnection(); // 调用此方法,会连接数据库
}
通过以上测试方法,可以检验配置值是否正确!
当没有读取到配置的密码时,报错信息如下:
java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: NO)
当配置的密码错误时,报错信息如下:
java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)
当配置的用户名错误时,报错信息如下:
java.sql.SQLException: Access denied for user 'rootfdsfdsafads'@'localhost' (using password: YES)
当配置的主机名错误时,报错信息如下:
com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
Caused by: java.net.UnknownHostException: localhast
当配置的端口号错误时,报错信息如下:
Caused by: java.net.ConnectException: Connection refused: connect
当配置的数据库名称错误时,报错信息如下:
java.sql.SQLSyntaxErrorException: Unknown database 'mall_pms1'
当配置的服务器时区错误时,报错信息如下:
java.sql.SQLNonTransientConnectionException: Could not create connection to database server.
Caused by: java.time.zone.ZoneRulesException: Unknown time-zone ID: Asia/ShangHai
MySQL中的字段类型与Java属性类型
| MySQL中的字段类型 | Java属性类型 |
|---|---|
tinyint / smallint / int | Integer |
bigint | Long |
varchar / char / text系列 | String |
date_time | LocalDateTime |
decimal | BigDecimal |
关于POJO类型
POJO类型应该:
-
【规范】类应该实现
Serializable接口- 某些框架在实现某些功能时,会自动将类型转换为
Serializable
- 某些框架在实现某些功能时,会自动将类型转换为
-
【规范】所有的属性都应该是私有的(
private) -
【规范】所有的属性都应该有对应的、规范的Setters & Getters
- 某些框架在实现某些功能时,会自动调用这些Setters & Getters
-
【规范】存在基于所有属性值的
hashCode()与equals()- 通常用于
Set等
- 通常用于
-
【建议】添加基于所有属性的
toString()
关于Lombok
Lombok框架可以通过特定的注解,在代码的编译期自动生成某些代码,例如自动生成各属性的Setters & Getters、hashCode()、equals()、toString()等。
Lombok框架的依赖项:
<!-- Lombok的依赖项,主要用于简化POJO类的编写 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
例如,在类上添加@Data注解,则Lombok会在编译器自动生成各属性的Setters & Getters、hashCode()、equals()、toString()等!
例如:
@Data
public class Album implements Serializable {
private Long id;
private String name;
private String description;
private Integer sort;
private LocalDateTime gmtCreate;
private LocalDateTime gmtModified;
}
**注意:**你应该在IntelliJ IDEA中安装Lombok插件,否则,你的IntelliJ IDEA会认为当前项目中没有那些生成的方法,并报错!
在IntelliJ IDEA中安装Lombok插件的视频教程:doc.vrd.net.cn/doc/idea_lo…
另外,关于Lombok的常用注解:
@Getter:添加在属性上,或添加在类上,用于生成Getters方法@Setter:与@Getter类似@ToString:添加在类上,用于生成基于类中所有属性的toString()方法@EqualsAndHashCode:添加在类上,用于生成基于类中所有属性的hashCode()与equals()方法@AllArgsConstructor:添加在类上,用于生成基于类中所有属性的全部参数的构造方法@NoArgsConstructor:添加在类上,用于生成无参数构造方法@Slf4j:再议
使用Mybatis的一次性配置
使用Mybatis时,需要配置:
- Mapper接口的位置
- 【推荐】在配置类上通过
@MapperScan注解的参数来配置Mapper接口所在的包 - 【不推荐】在各Mapper接口上添加
@Mapper注解
- 【推荐】在配置类上通过
- XML文件的位置
- 在配置文件中通过
mybatis.mapper-locations属性来配置XML文件的位置
- 在配置文件中通过
则在项目的根包(项目创建成功时就已经存在的包)下创建config.MybatisConfiguration类,用于配置Mapper接口的位置:
@Configuration
@MapperScan("cn.tedu.csmall.product.mapper")
public class MybatisConfiguration {
}
在src/main/resources/application.properties中添加配置:
mybatis.mapper-locations=classpath:mapper/*.xml
并且,在src/main/resources下创建名为mapper的文件夹!
相册数据的增删改查
关于抽象方法的声明
-
返回值类型:如果要执行的SQL是增、删、改类型的,统一使用
int作为返回值类型,Mybatis会返回“受影响的行数”,不推荐使用void;如果要执行的SQL是查询类型的,只需要保证能“装得下”返回结果即可 -
方法名称:不建议重载,取名可根据阿里巴巴的开发手册
【参考】各层命名规约: Service/DAO 层方法命名规约 1) 获取单个对象的方法用 get 做前缀。 2) 获取多个对象的方法用 list 做前缀。 3) 获取统计值的方法用 count 做前缀。 4) 插入的方法用 save/insert 做前缀。 5) 删除的方法用 remove/delete 做前缀。 6) 修改的方法用 update 做前缀。 -
参数列表:根据需要执行的SQL语句的参数来决定,如果参数数量较多,可以封装
关于XML文件的配置
- 根标签必须是
<mapper> - 必须配置
<mapper>的namespace属性,取值为Mapper接口的全限定名 - 在
<mapper>的子级,通过<insert>等标签配置SQL语句,这些标签都必须配置id属性,取值为抽象方法的名称 - 当插入数据时,如果表中的主键是自动编号的,应该在
<insert>标签上配置useGeneratedKeys="true"和keyProperty="主键对应的属性名",以获取自动编号的主键值 - 在
<select>标签上,必须配置resultType或resultMap这2个属性中的某1个
插入相册数据
在项目的根包下创建AlbumMapper接口,并添加抽象方法:
public interface AlbumMapper {
int insert(Album album);
}
**提示:**可以在此接口上添加@Repository注解,避免IntelliJ IDEA在自动装配此接口对象时报错!其实这个注解并没有发挥任何实质的作用,只是“骗”过一IntelliJ IDEA而已。
通过复制粘贴的做法,在src/main/resources/mapper下得到AlbumMapper.xml文件,在此文件中配置以上抽象方法映射的SQL语句:
<?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" useGeneratedKeys="true" keyProperty="id">
INSERT INTO pms_album (
name, description, sort
) VALUES (
#{name}, #{description}, #{sort}
)
</insert>
</mapper>
完成后,在src/test/java下的根包下创建mapper.AlbumMapperTests类,在这个类中编写并执行测试:
@SpringBootTest
public class AlbumMapperTests {
@Autowired
AlbumMapper mapper;
@Test
void insert() {
Album album = new Album();
album.setName("测试相册2211");
album.setDescription("测试相册2211的简介");
album.setSort(95);
int rows = mapper.insert(album);
System.out.println("插入数据完成,受影响的行数:" + rows);
}
}
**注意:**测试类与被测试的类型不允许使用相同的名称!
如果Mybatis找不到Mapper接口(可能是@MapperScan的参数配置错误,或Mapper接口没有创建在指定的包下),会报错:
Caused by: 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)}
如果存在以下问题,会报错:
- 在配置文件中的
mybatis.mapper-locations属性的值配置错误 - 在XML中的
<mapper>标签的namespace值配置错误 - 在XML中的
<insert>这类标签的id值配置错误
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): cn.tedu.csmall.product.mapper.AlbumMapper.insert
SLF4j日志
在开发实践中,应该禁止使用System.out.println()输出内容(除非你使用这段代码的位置是在测试类中),主要原因有:
- 此语句输出内容的效率非常低
- 不便于控制在不同环境下是否输出显示
日志是区分显示级别的,根据信息的重要程度,从低到高依次为:
trace:跟踪信息,通常是项目的执行过程的信息debug:调试信息,通常包含程序运行过程中的(某些敏感的)变量值info:一般信息,通常是不介意被其它人看到信息warn:警告信息,通常是需要引起关注的,但不一定会造成明显不良后果的问题的信息error:错误信息,通常表示某些错误,是必须明确关注的,且保存历史记录的
日志是可以配置当前的显示级别的,当配置后,仅显示配置的级别及更加重要的级别的日志!
在添加了Lombok框架的项目中,可以在任何类上添加@Slf4j注解,则Lombok会在编译期在此类中添加名为log的变量,此变量就是用于调用日志相关方法的!
在使用SLF4j时,调用log变量不同的方法,就可以输出不同显示级别的日志,此变量可以调用以上5个显示级别名称对应的方法!例如:
@Slf4j
public class Slf4jTests {
@Test
void test() {
log.trace("这是一条【TRACE】日志");
log.debug("这是一条【DEBUG】日志");
log.info("这是一条【INFO】日志");
log.warn("这是一条【WARN】日志");
log.error("这是一条【ERROR】日志");
}
}
SLF4j日志的默认显示级别是debug,所以,在默认情况下,只会显示debug和更加重要的日志,而不会显示trace级别的日志!
在Spring Boot项目中,SLF4j日志的默认显示级别是info,可以在配置文件中通过logging.level.根包名[.类名]属性来配置日志的显示级别。
例如,在application.properties中添加配置:
logging.level.cn.tedu.csmall=trace
另外,Mybatis框架默认使用trace和debug级别输出了一些日志,所以,当把日志的显示级别调为较“低”的级别时,可以在控制台看到SQL语句的相关日志!
在日志的API中,每个级别的日志输出都有String message, Object... args作为参数列表的方法,更加便于输出变量的值,且执行效率非常高!例如:
int x = 1;
int y = 2;
System.out.println("x = " + x + ", y = " + y + ", x + y = " + (x + y));
log.debug("x = {}, y = {}, x + y = {}", x, y, x + y);
其实,SLF4j日志框架只是一个日志框架的标准,并没有具体的实现日志的处理,具体的实现都是通过log4j、logback等其它日志框架来实现的!
关于Profile配置
Spring框架支持使用Profile配置,使得某些配置必须要激活才可以生效,否则,是无效的配置!在开发实践中,通常会为不同的环境(例如:开发环境、测试环境、生产环境等)准备多套配置文件,并且,按需切换使用配置文件!
Spring Boot框架默认的配置文件是application.properties,它还允许使用application-自定义名称.properties配置文件,这些添加了“自定义名称”部分的配置文件都是Profile配置文件,是必须激活才生效的!
例如:
application-dev.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
logging.level.cn.tedu.csmall=trace
application-test.properties
spring.datasource.url=jdbc:mysql://192.168.1.188:3306/mall_pms?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=test
spring.datasource.password=test123
logging.level.cn.tedu.csmall=debug
application-prod.properties
spring.datasource.url=jdbc:mysql://aliyun.com:3306/mall_pms?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=csmall-admin
spring.datasource.password=P@ssw0rd2211
logging.level.cn.tedu.csmall=info
区分了各Profile配置后,需要在application.properties(主配置文件)中激活Profile配置:
spring.profiles.active=test
以上spring.profiles.active属性的值就是Profile配置文件的文件名中自定义的部分!
当存在多个配置时,主配置文件是始终加载的,并且,如果application.properties和其它Profile配置文件中存在相同的属性,以Profile配置为准!
并且,在spring.profiles.active的值可以是多个Profile配置,取值时各配置之间使用逗号分隔,例如spring.profiles.active=dev, test,如果激活的多个配置中存在相同的属性,以后面的Profile中的配置为准!
关于YAML配置
YAML是一种编写配置文件的语法,如果存在.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
logging.level.cn.tedu.csmall=trace
改成YAML语法后,配置内容如下:
spring:
datasource:
url: jdbc:mysql://localhost:3306/mall_pms?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
logging:
level:
cn.tedu.csmall: trace
关于YAML语法:
- 在
.properties中属性名中间的小数点,在YAML语法中改为冒号且换行,每次换行,缩进2个空格 - 属性名与属性值之间使用冒号和1个空格进行分隔
- 多个属性中相同的部分不用重复配置,只需要保持相同的缩进即可
- 属性名中,并不是所有的小数点都必须换成冒号加空格,即使完全没有替换,也是完全可用的
注意:YAML对语法的要求是严格的!例如,换行后缩进2个空格,这2个空格不可以使用TAB符号替换,但是,在IntelliJ IDEA中编辑YAML文件时,按下的TAB键会自动生成为2个空格。
YAML语法的文件的扩展名可以是.yml,也可以是.yaml,两者没有区别。
在Spring框架中,并不直接识别YAML语法的配置,需要添加snakeyml工具包才可以解析这类文件,在Spring Boot框架中,基础依赖项(spring-boot-starter)中已经包含此工具包,所以,在Spring Boot项目中可以直接识别并解析此类文件!
如果同时存在application.properties和application.yml,将以application.properties的配置为准!
项目的开发过程
首先,应该先确定各种数据的处理先后顺序,通常,应该先处理更加基础的数据,后处理相对更加复杂的数据,例如:你先应该处理品牌数据,再处理商品数据,因为品牌是商品数据的一部分,也可以理解为是商品的基础数据;在此基础上,如果各数据并不是彼此的基础数据,通常,应该先处理相对简单的数据,再处理相对复杂的数据,例如:品牌与类别并不直接相关,即使没有处理类别,也可以处理品牌,反之,没有处理品牌的情况下,也可以处理类别,相比之下,品牌数据更加简单,所以,建议先处理品牌数据,再处理类别数据。
所以,在当前项目中,各数据的处理顺序可以是:相册 ? 属性模板 ? 品牌 ? 类别 > 属性 ? 图片 > SPU > SKU
然后,挑出需要处理的数据类型,分析此数据涉及哪些业务功能,以相册为例,其涉及功能至少包括:添加相册、根据ID删除相册、修改相册、查询相册列表,各业务功能通常根据增、查、删、改的顺序进行开发,其中,删、改相关的业务功能可能有多个,如果有多个,则先开发简单的,再开发较复杂的!
所以,在当前项目,相册管理的业务功能的开发顺序应该是:添加相册 > 查询相册列表 > 根据ID删除相册 > 修改相册。
在实际编写各业务功能时,编写代码的步骤通常是:Mapper > Service > Controller > 页面。
关于Service
Service是项目中用于处理业务的组件,主要职责是:组织业务流程与业务逻辑,以保障数据的完整性、有效性、安全性(数据会随着我们设定的规则而产生,或发生变化)。
在开发Service的代码时,通常会先定义一个接口,再编写其实现类。
【强制】对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部 的实现类用 Impl 的后缀与接口区别。 正例:CacheServiceImpl 实现 CacheService 接口。
关于Service中的业务方法的声明原则:
- 返回值类型:仅以操作成功为前提来设计返回值类型
- 如果视为操作失败,将通过抛出异常来表示
- 方法名称:自定义
- 参数列表:通常按照客户端或控制器(Controller)会传递过来的参数来设计
关于返回值类型和抛出的异常,以“用户登录”为例:
User login(String username, String password) throws
UserNotFoundException,
PasswordNotMatchException,
AccountDisabledException;
后续,此方法的调用者可能会:
try {
User user = service.login("root", "1234");
log.debug("登录成功,用户信息:{}", user);
} catch (UserNotFoundException e) {
log.debug("登录失败,用户数据不存在!")
} catch (PasswordNotMatchException e) {
log.debug("登录失败,密码错误!")
} catch (AccountDisabledException e) {
log.debug("登录失败,此账号已经被禁用!")
}
添加相册--Service
在“添加相册”时,将设置规则:相册的名称不允许重复(或:相册的名称必须唯一),要实现这个规则,在执行添加之前必须检查,检查时需要执行的SQL语句大致是:
SELECT count(*) FROM pms_album WHERE name=?
在AlbumMapper.java中添加新的抽象方法:
int countByName(String name);
在AlbumMapper.xml中配置以上抽象方法映射的SQL:
<!-- int countByName(String name); -->
<select id="countByName" resultType="int">
SELECT count(*) FROM pms_album WHERE name=#{name}
</select>
在AlbumMapperTests中编写并执行测试:
@Test
void countByName() {
String name = "华为Mate3000的相册";
int count = mapper.countByName(name);
System.out.println("根据名称【" + name + "】统计完成,数据的数量:" + count);
}
在项目的根包下创建pojo.dto.AlbumAddNewDTO类:
@Data
public class AlbumAddNewDTO implements Serializable {
private String name;
private String description;
private Integer sort;
}
在项目的根包下创建service.IAlbumService接口:
public interface IAlbumService {
void addNew(AlbumAddNewDTO albumAddNewDTO);
}
在项目的根包下创建service.impl.AlbumServiceImpl类,实现以上接口,并重写抽象方法:
@Service
public class AlbumServiceImpl implements IAlbumService {
@Autowired
private AlbumMapper albumMapper;
@Override
public void addNew(AlbumAddNewDTO albumAddNewDTO) {
// 从参数对象中获取相册名称
// 调用albumMapper的int countByName(String name)根据名称执行统计
// 判断统计结果是否大于0
// 是:抛出异常throw new RuntimeException()
// 创建Album对象
// 调用BeanUtils.copyProperties()将参数对象的属性值复制到Album对象中
// 调用albumMapper的int insert(Album album)方法将相册数据插入到数据库
}
}
具体实现为:
package cn.tedu.csmall.product.service.impl;
import cn.tedu.csmall.product.mapper.AlbumMapper;
import cn.tedu.csmall.product.pojo.dto.AlbumAddNewDTO;
import cn.tedu.csmall.product.pojo.entity.Album;
import cn.tedu.csmall.product.service.IAlbumService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AlbumServiceImpl implements IAlbumService {
@Autowired
private AlbumMapper albumMapper;
@Override
public void addNew(AlbumAddNewDTO albumAddNewDTO) {
// 从参数对象中获取相册名称
String name = albumAddNewDTO.getName();
// 调用albumMapper的int countByName(String name)根据名称执行统计
int countByName = albumMapper.countByName(name);
// 判断统计结果是否大于0
if (countByName > 0) {
// 是:抛出异常throw new RuntimeException()
throw new RuntimeException();
}
// 创建Album对象
Album album = new Album();
// 调用BeanUtils.copyProperties()将参数对象的属性值复制到Album对象中
BeanUtils.copyProperties(albumAddNewDTO, album);
// 调用albumMapper的int insert(Album album)方法将相册数据插入到数据库
albumMapper.insert(album);
}
}
完成后,在src/test/java下的根包下创建service.AlbumServiceTests测试类,编写并执行测试:
package cn.tedu.csmall.product.service;
@SpringBootTest
public class AlbumServiceTests {
@Autowired
IAlbumService service;
@Test
void addNew() {
AlbumAddNewDTO albumAddNewDTO = new AlbumAddNewDTO();
albumAddNewDTO.setName("测试相册001");
albumAddNewDTO.setDescription("测试相册的简介001");
albumAddNewDTO.setSort(99);
try {
service.addNew(albumAddNewDTO);
System.out.println("添加相册成功!");
} catch (RuntimeException e) {
System.out.println("添加相册失败,相册名称已经被占用!");
}
}
}
关于异常
在Service中使用抛出异常的方式来表示某种“失败”,在调用Service中的方法时,也会捕获对应的异常,来发现并处理这些“失败”!
如果抛出的异常是某种已经存在的异常类型,例如以上使用RuntimeException,在实际执行时,如果因为其它原因导致了RuntimeException,对于方法的调用者而言,将无法正确的区分,最终,捕获并处理时可能不准确!
为了解决此问题,在Service中抛出的异常必须是自定义异常!
在项目的根包下创建ex.ServiceException,继承自RuntimeException:
package cn.tedu.csmall.product.ex;
public class ServiceException extends RuntimeException {
}
然后,在AlbumServiceImpl中的addNew()方法,如果相册名称已经存在,则改为抛出此类型的异常:
if (countByName > 0) {
throw new ServiceException(); // 原本抛出的是RuntimeException
}
并且,在测试时,改为捕获自定义类型的异常,即在AlbumServiceTests中:
@Test
void addNew() {
AlbumAddNewDTO albumAddNewDTO = new AlbumAddNewDTO();
albumAddNewDTO.setName("测试相册002");
albumAddNewDTO.setDescription("测试相册的简介002");
albumAddNewDTO.setSort(99);
try {
service.addNew(albumAddNewDTO);
System.out.println("添加相册成功!");
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 捕获自定义类型的异常
} catch (ServiceException e) {
System.out.println("添加相册失败,相册名称已经被占用!");
} catch (RuntimeException e) {
System.out.println("出现了某种RuntimeException:"
+ e.getClass().getName());
}
}
至于自定义的异常需要继承自RuntimeException,主要原因有:
- 第1条:再议
- 第2条:再议
课堂练习
完成“属性模板”的“新增”的业务,业务规则:属性模板的名称必须唯一。
添加Web开发的依赖
Spring MVC框架是用于处理控制器开发的框架,其基础依赖项是spring-webmvc。
在Spring Boot项目中,当需要添加Web开发的依赖项时,应该添加spring-boot-starter-web,并且,此依赖项中包含Spring Boot的基础依赖项(spring-boot-starter),所以,在当前项目中,只需要将原有的spring-boot-starter改为spring-boot-starter-web即可。
<!-- Spring Boot支持Spring MVC的依赖项 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
当添加以上依赖项后,会发生以下直接变化:
- 启动项目时,会启动Tomcat,默认占用
8080端口- 在配置文件中,可以通过
server.port属性修改端口号
- 在配置文件中,可以通过
- 在
src/main/resources下可以创建static文件夹,是默认的静态资源文件夹
添加相册--Controller
在项目的根包下创建controller.AlbumController类,在类上添加@RestController,在类中装配IAlbumService属性,并在类中添加处理请求的方法,在类和方法上均使用@RequestMapping配置请求路径,在处理请求的过程中,调用IAlbumService进行处理,例如:
@RestController
@RequestMapping("/album")
public class AlbumController {
@Autowired
private IAlbumService albumService;
// http://localhost:9080/album/add-new?name=TestName001&description=TestDescription001&sort=199
@RequestMapping("/add-new")
public String addNew(AlbumAddNewDTO albumAddNewDTO) {
try {
albumService.addNew(albumAddNewDTO);
return "添加相册成功!";
} catch (ServiceException e) {
return "添加相册失败,相册名称已经被占用!";
}
}
}
完成后,启动项目,可以在浏览器中通过 http://localhost:9080/album/add-new?name=TestName001&description=TestDescription001&sort=199 测试访问。
F&Q
为什么要有Service?
分工明确!
其实,Service中的代码也可以直接写在Controller中,但是,这样做就存在分工不明确的问题,项目的扩展性不够好!
并且,Service中的代码应该与框架无关(除了最基础的框架以外,例如Spring),无论你的项目使用什么技术框架,这个项目中的数据处理规则应该是不变的!
如果把Service中的代码写在Controller中,则数据处理规则是编写在Spring MVC框架的基础之上的类中的,如果后续使用别的技术框架替换Spring MVC框架,项目中负责数据处理规则的代码也要跟随调整。
为什么Service要有接口?
对于Service的调用者(比如Controller,或测试类),只需要关心Service的特征,并不需要关心它的具体实现!
接口是用于描述行为能力、行为特征的!具体表现为:接口就是用于定义抽象方法的!
另外,因为Controller的调整者是框架,所以,Controller不需要接口!
异常
在Java语言中,异常的继承体系结构是:
Throwable
-- Error
-- -- OutOfMemoryError(OOM)
-- Exception
-- -- IOException
-- -- -- FileNotFoundException
-- -- RuntimeException
-- -- -- NullPointerException(NPE)
-- -- -- ClassCastException
-- -- -- NumberFormatException
-- -- -- IndexOutOfBoundsException
-- -- -- -- ArrayIndexOutOfBoundsException
-- -- -- -- StringIndexOutOfBoundsException
在异常体系中,RuntimeException及其子孙类异常是比较特殊的,不受到使用异常的语法约束!对于非RuntimeException类型的异常,一旦出现,则当前方法必须声明抛出,例如:
// 在方法上声明抛出
public void a() throws IOException {
// 在方法内部抛出了异常
throw new IOException();
}
并且,这类方法的调用者,必须在try...catch和throws之间二选一,例如:
public void b() {
try {
a();
} catch (IOException e) {
}
}
或者:
public void b() throws IOException {
a();
}
在开发实践中,异常必须被处理,如果没有处理,会向客户端响应500错误,并且,在控制台可以看到异常信息。
处理异常的本质是:明确的告之客户端“出错了,错误的原因是什么,如何调整以避免再次出现同样的错误”。
注意:当程序执行过程中,如果出现某个异常,并且,被try..catch捕获并处理了,则这个异常就已经相当于不存在了!
注意:对于出现的RuntimeException及其子孙类异常而言,只要没有try...catch,就相当于throws,但是,不需要在语法上表现出来!
关于try...catch和throws的选取,在项目中,Controller才是负责响应的组件,所以,Controller才可以处理异常,并且,其它组件(例如Service等)都不允许处理异常,否则,如果其它组件处理了异常,对于Controller而言,并不知道异常的存在,将向客户端响应“成功”,但是,由于确实出现过异常,数据处理过程中并不是真正的“成功”了,则这样的处理是有问题的!
Spring MVC框架统一处理异常
在Spring MVC框架中,提供了“统一处理异常”的机制,具体表现为:每个处理请求的方法都不必使用try...catch对异常进行捕获和处理,只需要继续将异常抛出即可,这些抛出的异常都会由专门处理异常的方法进行处理!
关于处理异常的方法:
- 返回值类型:参考处理请求的方法
- 方法名称:自定义
- 参数列表:至少包含被处理的异常,还可以按需添加
HttpServletRequest、HttpServletResponse等少量特定类型的参数,不可以随意添加其它参数(例如在此方法的参数列表中添加HttpSession是错误的),各参数不区分先后顺序 - 注解:必须添加
@ExceptionHandler
例如,在AlbumController中添加方法:
@ExceptionHandler
public String handleServiceException(ServiceException e) {
return "程序运行过程中出现了ServiceException";
}
对于Spring MVC框架而言,大概是:
try {
albumController.addNew(); // 调用其它方法也是如此处理
} catch (ServiceException e) {
albumController.handleServiceException(e);
}
所以,在当前控制器类中,无论是哪个方法(必须是由Spring MVC调用的方法)的执行过程中,只要出现了ServiceException,都将由以上handleServiceException()进行处理!
Spring MVC框架允许你定义若干个处理不同异常的方法,用于处理不同的异常!多个处理异常的方法,所处理的异常允许存在父子级继承关系,如果程序执行过程中出现的是子级的异常,将由处理子级异常的方法进行处理!
在开发实践中,通常会添加一个处理Throwable的方法,它将可以处理所有类型的异常,则不会再出现500错误!
**注意:**在控制器类中,处理异常的方法仅能作用于当前类中处理请求时出现的异常!也就是无法作用于其它控制器类中处理请求时出现的异常!
Spring MVC建议将所有处理异常的方法定义在专门的类中,并且,在此类上添加@RestControllerAdvice,当添加此注解后,此类中特定的方法(例如处理异常的方法)将作用于整个项目中每次处理请求的过程中!
关于Knife4j框架
Knife4j是一款基于Swagger 2的在线API文档框架。
使用Knife4j的步骤:
- 添加依赖
- 目前使用的Knife4j版本是
2.0系列的,不可以用于Spring Boot 2.6及以上版本
- 目前使用的Knife4j版本是
- 添加配置类
- 此配置类是固定的代码,请注意检查是否需要修改用于配置Controller所在的包的属性
- 在主配置文件中添加配置
在pom.xml中添加依赖:
<!-- Knife4j Spring Boot:在线API -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.9</version>
</dependency>
然后,在项目的根包创建config.Knife4jConfiguration类,配置文件内容如下:
package cn.tedu.csmall.product.config;
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";
@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.yml中添加配置:
knife4j:
enable: true
完成后,重启项目,可以通过 http://localhost:9080/doc.html 访问。
关于@RequestMapping
在Spring MVC框架中,@RequestMapping注解主要用于配置“请求路径”与“处理请求的方法”的映射关系!
在此注解中,可以通过method属性来限制请求方式,例如:
@RequestMapping(value = "/add-new", method = RequestMethod.POST)
按照以上配置,/add-new这个路径只能通过POST方式发起请求,如果不是POST,服务器端将响应405错误(如果使用全局异常处理器,可能会捕获并处理这种错误)。
在Spring MVC框架中,提供了更加简洁的注解,例如@PostMapping,它是已经将请求方式限制为POST的@RequestMapping,并且,此注解只能添加在方法上,与之类似的注解还有:
@GetMapping@DeleteMapping@PutMapping@PatchMapping
小结:在类上使用@RequestMapping,在方法上使用限制了请求方式的注解。
配置在线API文档
-
@Api:添加在控制器类上,通过此注解的tags属性,可以配置模块名称 -
@ApiOpeartion:添加在处理请求的方法上,通过此注解的value属性,可以配置业务名称 -
@ApiOperationSupport:添加在处理请求的方法,通过此注解的order属性(数值类型),可配置业务的排序序号,在显示多个业务时,将按照此值升序排列 -
@ApiModelProperty:添加在POJO类型的属性上,对请求参数或响应的数据属性进行描述配置,通过此注解的value属性,可以配置参数名称,通过此注解的required属性,可以配置“是否必须提交此请求参数”,通过此注解的example属性,可以配置参数的“示例值”- **注意:**此处如果配置了
required=true,只是一种显示效果,Knife4j框架并不具备检查功能 - **注意:**如果需要配置
example,必须保证配置值是可以被转换成实际所需的值!例如sort的值必须是数值类型的,配置此属性的example值必须保证是纯数字的!
- **注意:**此处如果配置了
-
@ApiImplicitParam:添加在处理请求的方法上,用于配置处理请求的方法的参数(通常是简单数据类型的)的描述,使用此注解配置时,必须先使用name属性指定参数名称(参数的变量名),然后,再通过value属性配置参数的描述,并按需使用dataType指定数据类型(一旦使用此注解,各参数的数据类型默认都会显示为string),另外,也可以通过此注解的required属性,可以配置“是否必须提交此请求参数”,通过此注解的example属性,可以配置参数的“示例值” -
@ApiImplicitParams:添加在处理请求的方法上,如果此方法需要使用多个@ApiImplicitParam注解对多个参数进行描述说明,则多个@ApiImplicitParam必须作为当前注解的参数值 -
@ApiIgnore:添加在处理请求的方法的参数上,用于表示API文档框架应该“忽略”此参数
Spring Validation
关于Spring Validation
Spring Validation框架的主要作用是:检查参数的基本有效性。
在Spring Boot项目中,使用此框架需要添加依赖:
<!-- Spring Boot支持Spring Validation的依赖项,用于检查参数的基本有效性 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
检查POJO类型的参数
首先,在处理请求的方法的参数列表中,在POJO类型的参数上添加@Valid或@Validated注解,表示需要通过Spring Validation框架检查此参数,例如:
@PostMapping("/add-new")
// ↓↓↓↓↓↓ 标记,需要检查此参数
public String addNew(@Valid AlbumAddNewDTO albumAddNewDTO) {
albumService.addNew(albumAddNewDTO);
return "添加相册成功!";
}
然后,在此POJO类中的属性上,添加对应的检查注解,以配置检查规则,例如,添加@NotNull注解,就表示“不允许为null”的规则!
例如,在AlbumAddNewDTO类中:
@Data
public class AlbumAddNewDTO implements Serializable {
// 添加检查注解
@NotNull
private String name;
// 暂不关心其它代码
}
完成后,重启项目,当提交请求时,如果没有提交以上name参数,服务器端将响应400错误。
处理BindException
在使用Spring Validation框架执行参数的检查时,如果检查不通过,除了响应400错误以外,在控制台还会提示错误:
2023-03-22 16:44:29.228 WARN 36332 --- [nio-9080-exec-8] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errorsField error in object 'albumAddNewDTO' on field 'name': rejected value [null]; codes [NotNull.albumAddNewDTO.name,NotNull.name,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [albumAddNewDTO.name,name]; arguments []; default message [name]]; default message [不能为null]]
可以看到,如果检查不通过,Spring Validation框架会抛出BindException!
所以,可以在全局异常处理器(GlobalExceptionHandler)中添加处理以上异常的方法,例如:
@ExceptionHandler
public String handleBindException(BindException e) {
return "请求参数格式错误!";
}
重启项目,当未提交name时,可以发现异常已经被以上代码处理!但是,处理结果并不合适,因为,客户端得到此结果后,仍无法明确出现了什么错误!
所有的检查注解都可以配置message参数,用于对错误进行描述,例如:
@NotNull(message = "添加相册失败,必须提交相册名称!")
private String name;
在处理异常时,需要调用BindException对象的getFieldError().getDefaultMessage()获取以上配置的描述文本,例如:
@ExceptionHandler
public String handleBindException(BindException e) {
String message = e.getFieldError().getDefaultMessage();
return message;
}
Spring Validation(续)
快速失败
在项目的根包下创建config.ValidationConfiguration类,配置快速失败:
package cn.tedu.csmall.product.config;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.validation.Validation;
/**
* Validation配置类
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Slf4j
@Configuration
public class ValidationConfiguration {
public ValidationConfiguration() {
log.debug("创建配置类对象:ValidationConfiguration");
}
@Bean
public javax.validation.Validator validator() {
return Validation.byProvider(HibernateValidator.class)
.configure() // 开始配置
.failFast(true) // 配置快速失败
.buildValidatorFactory() // 构建Validator工厂
.getValidator(); // 从Validator工厂中获取Validator对象
}
}
当Spring Validation框架执行检查时,即使存在多个错误,当检查到第1个错误时,就会停止检查,并反馈错误。
检查简单类型的参数
当需要检查简单类型的参数时,需要先在当前类上添加@Validated注解,例如:
@RestController
@RequestMapping("/album")
@Validated // 添加此注解
public class AlbumController {
// 需要检查此类中的方法的简单类型参数
}
然后,直接在方法上添加检查注解,例如:
@PostMapping("/delete")
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 配置检查注解
public String delete(@RequestParam @Range(min = 10, max = 99) Long id) {
return "模拟删除相册成功!";
}
提示:以上配置的最小值和最大值只是为了便于演示而设置的,并不是合理的值!
当检查参数时发现错误,会出现以下异常:
javax.validation.ConstraintViolationException: delete.id: 需要在10和99之间
处理ConstraintViolationException
在全局异常处理器(GlobalExceptionHandler)中补充处理以上异常的方法:
@ExceptionHandler
public String handleConstraintViolationException(ConstraintViolationException e) {
String message = null;
Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
for (ConstraintViolation<?> constraintViolation : constraintViolations) {
message = constraintViolation.getMessage();
}
return message;
}
关于更多检查注解
找到@NotNull和@Range的导包语句(import语句),按下Ctrl键,点击注解所在的包,即可展开此包,显示包中的各检查注解!
常用的注解有:
@NotNull:不允许为null- 可以用于任何类型的参数
@NotEmpty:不允许为空字符串,即不允许是长度为0的字符串- 仅能用于字符串类型的参数
@NotBlank:不允许为空白,即不允许是纯由空格、TAB制表位、换行符组成的字符串- 仅能用于字符串类型的参数
@Pattern:通过此注解的regexp属性配置正则表达式,要求参数必须匹配此正则表达式- 仅能用于字符串类型的参数
@Range:配置整型数值的取值区间,通过此注解的min和max属性可以配置最小值与最大值,最小值的默认值为0,最大值的默认值为long的上限值
需要注意:以上@Range注解并不检查参数为null的情况,也就是说,如果参数值为null,是可以通过@Range注解的检查的(本质上是无视了@Range),所以,可能需要结合@NotNull一起使用,以上@Pattern也可能需要结合@NotNull一起使用,但是,@NotEmpty和@NotBlank本身也会检查是否为null,如果参数为null,检查将不通过,所以,这2个注解不需要和@NotNull一起使用。
关于RESTful
**百科资料:**RESTFUL是一种网络应用程序的设计风格和开发方式,基于HTTP,可以使用XML格式定义或JSON格式定义。RESTFUL适用于移动互联网厂商作为业务接口的场景,实现第三方OTT调用移动网络资源的功能,动作类型为新增、变更、删除所调用资源。
**重点:**RESTful是一种风格。
RESTful的典型表现有:
- 必须是前后端分离的
- 服务器端响应的结果必须是XML格式或JSON格式的
- 通常会将具有“唯一性”的参数值设计为URL的一部分
- 根据访问数据的目的不同,使用不同的请求方式:
- 新增数据:
POST - 删除数据:
DELETE - 修改数据:
PUT - 获取数据:
GET - 在绝大部分业务系统中,并不使用这样的做法
- 新增数据:
在开发实践中,关于URL的设计,可以参考:
- 查询数据列表:
/数据类型的复数- 例如:
/albums
- 例如:
- 根据ID查询某个数据:
/数据类型的复数/ID值- 例如:
/albums/{id}
- 例如:
- 根据ID操作某个数据:
/数据类型的复数/ID值/命令- 例如:
/albums/{id}/delete
- 例如:
使用@PathVariable
Spring MVC框架很好的支持了RESTful风格。
当需要将某参数值定义在URL中时,在使用@RequestMapping系列注解(包括@PostMapping等)配置URL时,可以使用{自定义名称}表示占位符,例如:
// http://localhost:9081/album/delete/9527
@PostMapping("/delete/{id}")
public String delete() {}
并且,当需要接收占位符对应的参数时,只需要在处理请求的方法的参数列表中添加匹配名称的参数,并且,在参数上添加@PathVariable注解即可,例如:
// http://localhost:9081/album/delete/9527
@PostMapping("/delete/{id}")
public String delete(@PathVariable Long id) {
return "模拟删除相册成功!ID=" + id;
}
如果占位符中的名称,与参数的名称并不相同,可以配置@PathVariable注解参数,例如:
// http://localhost:9081/album/delete/9527
@PostMapping("/delete/{albumId}")
public String delete(@PathVariable("albumId") Long i) {
return "模拟删除相册成功!ID=" + i;
}
并且,在配置占位符时,可以在自定义名称的右侧添加冒号,再添加正则表达式,例如:
@PostMapping("/delete/{id:[0-9]+}")
当添加以上正则表达式后,如果参数值不匹配此正则表达式,将响应404错误!相比原本的400错误(要想看到400错误,需要先停用全局异常处理器),404是更早拒绝错误的请求的,是更合适的做法!
另外,不冲突的多个使用了正则表达式的配置是允许共存的,例如:
@PostMapping("/delete/{id:[0-9]+}")
public String delete(@PathVariable Long id) {
return "模拟删除相册成功!Long ID=" + id;
}
@PostMapping("/delete/{name:[a-z]+}")
public String delete(@PathVariable String id) {
return "模拟删除相册成功!String ID=" + id;
}
并且,使用了占位符与不使用占位符的设计也是允许共存的,例如:
@GetMapping("/delete/{id:[a-z]+}")
public String delete(@PathVariable String id) {
return "模拟删除相册成功!String ID=" + id;
}
@GetMapping("/delete/test")
public String deleteTest() {
return "模拟删除相册成功!这只是一个测试!";
}
当接收到 /delete/test 请求时,会由以上第2个方法deleteTest()进行处理!
关于响应结果类型
当服务器端向客户端响应时,不能只响应字符串结果!以“添加相册”为例,仅响应字符串时,响应结果可能是:
添加相册成功!
添加相册失败,相册名称已经被占用!
如果响应这样的结果,客户端的程序可能是:
if (message == '添加相册成功!') {
// 在界面上显示成功,或跳转页面
} else if (message == '添加相册失败,相册名称已经被占用!') {
// 在界面上显示错误信息
}
使用字符串值作为判断条件显然是不合适的!
通常,会使用某个数值来表示某种状态,例如使用1表示成功,使用2表示某种失败,使用3表示另一种失败等等,具体使用哪个数字表示哪个状态,可以由服务器端的开发人员和客户端的开发人员共同协商。
同时,为了保证客户端在显示错误信息时,显示的内容是准确的,服务器端还应该将错误的描述文本也响应到客户端去!
所以,服务器端向客户端的响应结果,应该包含1或者2这类的“状态码”,还应该包含错误的描述文本!
则需要在项目的根包下创建web.JsonResult类,并在类中声明需要响应到客户端的数据属性:
@Data
public class JsonResult {
private Integer state;
private String message;
}
然后,将控制器中处理请求的方法的返回值类型改为以上类型,并且返回以上类型的对象:
@PostMapping("/add-new")
public JsonResult addNew(@Valid AlbumAddNewDTO albumAddNewDTO) {
albumService.addNew(albumAddNewDTO);
JsonResult jsonResult = new JsonResult();
jsonResult.setState(1);
jsonResult.setMessage("添加相册成功!");
return jsonResult;
}
完成后,重启项目,当添加相册成功时,服务器端的响应结果为:
{
"state": 1,
"message": "添加相册成功!"
}
100:
200:OK,成功
201:CREATED,成功,且服务器端创建了某个数据
204:NO_CONTENT,成功,且服务器端不额外响应数据
206:成功,且这是一个区间段的响应
400:请求错误,参数格式错误
404:请求错误,资源不存在
405:请求错误,方式错误
500:服务器内部错误,存在没有被处理的异常
关于响应
在使用Spring MVC框架处理请求时,控制器类中处理请求的方法的返回值就表示此次处理请求的响应。
在默认的情况下,方法的返回值表示的是“视图组件的名称”,这不是“前后端分离”的作法!
在处理请求的方法上,添加@ResponseBody注解,表示此方法会“响应正文”,也就是“方法的返回值会响应到客户端去”,这是“前后端分离”的作法!
当使用了@ResponseBody,并且,方法的返回值是String时,返回结果默认表示响应到客户端的HTML源代码!
也可以将@ResponseBody添加在控制器类上,则当前类中所有处理请求的方法都是“响应正文”的!
或者,也可以在控制器类上改为使用@RestController,它包含了@Controller和@ResponseBody。
其实,在全局异常器上,需要添加的注解是
@ControllerAdvice,在没有使用@ResponseBody的情况下,处理异常后的响应结果也不是响应正文的,当改为@RestControllerAdvice后,就会是响应正文的!
在响应正文时,Spring MVC会自动使用HttpMessageConverter(消息转换器)将方法的返回值转换为响应到客户端的结果,Spring MVC内置了多款不同的HttpMessageConverter,根据方法的返回值类型不同,自动选取对应的HttpMessageConverter,例如,当方法的返回值类型是String时,会自动使用StringHttpMessageConverter,但是,Spring MVC也不可能穷举所有可能的返回值类型,当你的项目添加了jackson-databind依赖项后,如果方法的返回值类型并不是Spring MVC匹配了HttpMessageConverter的类型,Spring MVC会自动使用jackson-databind提供的HttpMessageConverter,而jackson-databind会自动将返回的结果转换为JSON格式再响应!
提示:在spring-boot-starter-web中包含了spring-boot-starter-json,而spring-boot-starter-json中包含了jackson-databind。
提示:当需要启用其它HttpMessageConverter时,需要在配置类上添加@EnableWebMvc注解,在Spring Boot项目中,已经自动配置了,不需要显式添加此注解。
去除响应结果中的null值
当需要去除响应结果(JSON结果)中为null的属性的部分时,可以:
-
在对应的属性上使用
@JsonInclude进行配置@JsonInclude(JsonInclude.Include.NON_NULL) private String message; -
在对应的属性所归属的类上使用
@JsonInclude进行配置,将使得类中所有属性都应用此配置@Data @JsonInclude(JsonInclude.Include.NON_NULL) public class JsonResult implements Serializable { } -
在配置文件中通过
spring.jackson.default-property-inclusion属性进行配置,将使得当前项目中所有类型都应用此配置spring: jackson: default-property-inclusion: non_null
**提示:**以上3种做法允许共存,当配置值不同时,遵循“范围越小越优先”的原则。