SSM、Spring Boot、Spring Validation、Spring Security

175 阅读38分钟

第四阶段课程

主要学习的框架技术:

  • SSM(Spring、Spring MVC、Mybatis)
  • Spring Boot
  • Spring Validation
  • Spring Security
  • 其它……

项目介绍:酷鲨商城运营管理平台(商品管理平台、管理员管理平台、前端)

gitee.com/chengheng20…

创建项目

1679016251579.png

如果默认的创建项目的URL(start.spring.io)暂时不可用,可尝试切换为https://start.spri…

1679016659848.png

在以上界面中,版本可以随意选择,后续当项目创建出来后,通过源代码再调整为习惯使用的版本。

并且,在以上界面中,可以不必勾选任何依赖项,都可以在创建创建出来之后再通过编辑源代码来添加依赖项。

调整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;

例如:

1679021160468.png

然后,在IntelliJ IDEA中配置Database面板:

1679021277800.png

然后,打开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 / intInteger
bigintLong
varchar / char / text系列String
date_timeLocalDateTime
decimalBigDecimal

关于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>标签上,必须配置resultTyperesultMap这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框架默认使用tracedebug级别输出了一些日志,所以,当把日志的显示级别调为较“低”的级别时,可以在控制台看到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配置文件,是必须激活才生效的!

例如:

1679281885845.png

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.propertiesapplication.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...catchthrows之间二选一,例如:

public void b() {
    try {
        a();
    } catch (IOException e) {
    }
}

或者:

public void b() throws IOException {
    a();
}

在开发实践中,异常必须被处理,如果没有处理,会向客户端响应500错误,并且,在控制台可以看到异常信息。

处理异常的本质是:明确的告之客户端“出错了,错误的原因是什么,如何调整以避免再次出现同样的错误”。

注意:当程序执行过程中,如果出现某个异常,并且,被try..catch捕获并处理了,则这个异常就已经相当于不存在了!

注意:对于出现的RuntimeException及其子孙类异常而言,只要没有try...catch,就相当于throws,但是,不需要在语法上表现出来!

关于try...catchthrows的选取,在项目中,Controller才是负责响应的组件,所以,Controller才可以处理异常,并且,其它组件(例如Service等)都不允许处理异常,否则,如果其它组件处理了异常,对于Controller而言,并不知道异常的存在,将向客户端响应“成功”,但是,由于确实出现过异常,数据处理过程中并不是真正的“成功”了,则这样的处理是有问题的!

Spring MVC框架统一处理异常

在Spring MVC框架中,提供了“统一处理异常”的机制,具体表现为:每个处理请求的方法都不必使用try...catch对异常进行捕获和处理,只需要继续将异常抛出即可,这些抛出的异常都会由专门处理异常的方法进行处理!

关于处理异常的方法:

  • 返回值类型:参考处理请求的方法
  • 方法名称:自定义
  • 参数列表:至少包含被处理的异常,还可以按需添加HttpServletRequestHttpServletResponse等少量特定类型的参数,不可以随意添加其它参数(例如在此方法的参数列表中添加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及以上版本
  • 添加配置类
    • 此配置类是固定的代码,请注意检查是否需要修改用于配置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: 需要在1099之间

处理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:配置整型数值的取值区间,通过此注解的minmax属性可以配置最小值与最大值,最小值的默认值为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": "添加相册成功!"
}
100200OK,成功
201CREATED,成功,且服务器端创建了某个数据
204NO_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种做法允许共存,当配置值不同时,遵循“范围越小越优先”的原则。