# Spring Boot

滕祯
2023-12-01

Spring Boot

1. 关于Spring Boot

Spring Boot框架主要解决了创建工程后需要进行繁琐的配置的问题,是一个“开箱即用”的框架,其核心思想是“约定大于配置”。

2. 创建Spring Boot工程

使用IntelliJ IDEA的创建向导中的Spring Initializer即可创建Spring Boot工程。

在创建时,如果 https://start.spring.io 无响应,可尝试替换为 https://start.springboot.io。

在创建过程中,需要填写并关注的几项有:

  • Group Id:组Id,通常是公司的域名倒序排列的结果,例如cn.tedu
  • Artifact Id:坐标Id,应该是此工程的名称,如果名称中有多个单词,应该使用减号分隔,例如boot-demo
  • Java Version:使用到的Java版本,目前推荐选择8
  • Package:项目的根包,默认是由以上填写的Group IdArtifact Id组成

注意:如果Artifact Id中使用减号分隔了多个单词,在Package中默认并没有分开,通常建议手动添加小数点(.)进行分隔

注意:此处Package决定了默认的组件扫描,所以,在后续开发代码时,所有的组件类都必须放在此包或其子孙包下,在开发实践中,其实会把所有创建的类、接口都放在此包或其子孙包下,不是组件的类不添加组件即可

注意:当工程已经创建出来后,不要修改包的名称,除非你已经掌握了解决方案!

在添加依赖项时,首先需要注意的就是Spring Boot的版本号,通常非常不建议使用较新的版本号,建议使用的是半年或1年之内的版本即可!如果在创建向导的界面没有需要的版本号,可以随便选一下,当项目创建成功后,打开pom.xml,修改<parent>子级的<version>节点的值即可。

当项目创建成功后,在src/main/java下默认就存在一个包,是由创建项目时填写的Package决定的,就是当前项目组件扫描的包,相当于默认就有了@ComponentScan("cn.tedu.boot.demo")

项目中默认就存在BootDemoApplication类,此类的名称是由创建项目时填写的Artifact Id加上Application单词组成的,这个类名称是可以改的,这个类中有main()方法,执行此方法就会启动整个项目,将加载项目中所有依赖所需的环境。

src/main/resources下默认存在application.properties配置文件,它是项目默认会加载的配置文件。另外,Spring Boot的自动配置机制要求此处的许多配置是使用固定的属性名的!

3. 当前案例目标

客户端发出请求,最终增加管理员信息。

4. 开发数据访问层

4.1. 添加Mybatis相关依赖项

pom.xml中添加必要的依赖项:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

4.2. 配置连接数据库的信息

当添加以上依赖项,如果启动项目(执行BootDemoApplication类中的main()方法)会报告错误,因为Spring Boot允许自动配置,当添加以上依赖项后,就会自动读取连接数据库的相关信息,并自动配置数据源,甚至Mybatis所需要其它基础配置,而目前并没有配置连接数据库的相关信息,所以出现错误!

则在application.properties中添加配置:

spring.datasource.url=jdbc:mysql://localhost:3306/mall_ams?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root

完成后,在src/test/java下找到默认即存在的测试类,在此测试类中尝试获取数据库连接对象:

@SpringBootTest
class BootDemoApplicationTests {

    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() throws Exception {
        System.out.println(dataSource.getConnection());
    }

}

如果能顺利执行此测试,则表示以上配置是正确的!

4.3. 创建与数据表对应的实体类

为了简化编写POJO类,通常会在项目中添加Lombok依赖:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

提示:当使用了Lombok后,应该在开发工具中安装Lombok插件,否则,在编写代码时,所有相关的Setters & Getters都没有自动提示,也会报告语法错误,但是不影响运行。

在插入数据时,需要使用实体类封装即将插入到表中的多个数据,则在cn.tedu.boot.demo包下创建entity子包,并在其下创建Admin类:

@Data
public class Admin implements Serializable {
    
    private Long id;
    private String username;
    private String password;
    private String nickname;
    private String avatar;
    private String phone;
    private String email;
    private String description;
    private Integer isEnable;
    private String lastLoginIp;
    private Integer loginCount;
    private LocalDateTime gmtLastLogin;
    private LocalDateTime gmtCreate;
    private LocalDateTime gmtModified;
    
}

4.4. 插入管理员数据

要执行的SQL语句大致是:

insert into ams_admin (除了id以外的字段列表……) values (值列表)

则在cn.tedu.boot.demo包下创建mapper子包,并在其下创建AdminMapper接口,在接口中添加抽象方法:

package cn.tedu.boot.demo.mapper;

import cn.tedu.boot.demo.entity.Admin;
import org.springframework.stereotype.Repository;

@Repository
public interface AdminMapper {

    int insert(Admin admin);

}

还需要进行配置,使得Mybatis知道这些接口文件在哪里!则在cn.tedu.boot.demo下创建config包,并在此包下创建MybatisConfiguration类,通过@MapperScan配置接口文件所在的包:

package cn.tedu.boot.demo.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("cn.tedu.boot.demo.mapper")
public class MybatisConfiguration {
}

提示:关于@MapperScan注解,还可以配置在项目的启动类上(BootDemoApplication),因为启动类上有@SpringBootApplication注解,其元注解中有@SpringBootConfiguration,其元注解中有@Configuration,所以,启动类本身也是配置类!但是,如果项目中的配置较多,不建议全部写在启动类中,所以,可以分为多个配置类,独立配置。

接下来,在src/main/resources下创建mapper文件夹,并从前序项目中复制粘贴得到AdminMapper.xml文件(删除原文件中已经配置的SQL等代码),然后,在此文件中配置抽象方法映射的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.boot.demo.mapper.AdminMapper">

    <!-- int insert(Admin admin); -->
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into ams_admin (
            username, password, nickname, avatar,
            phone, email, description, is_enable,
            last_login_ip, login_count, gmt_last_login, gmt_create,
            gmt_modified
        ) values (
            #{username}, #{password}, #{nickname}, #{avatar},
            #{phone}, #{email}, #{description}, #{isEnable},
            #{lastLoginIp}, #{loginCount}, #{gmtLastLogin}, #{gmtCreate},
            #{gmtModified}
        )
    </insert>

</mapper>

完成后,还是应该配置这些XML文件的位置,需要在application.properties中添加配置:

mybatis.mapper-locations=classpath:mapper/*.xml

接下来,应该通过测试检验以上代码是否可以正确运行,为了保证测试时可以正确的断言,应该在src/test下创建resources文件夹,并从前序项目中复制脚本文件,至少包含清空并还原数据表、插入测试数据这2个脚本文件。

然后,在src/test/java下的cn.tedu.boot.demo包下创建mapper子包,并在其下创建AdminMapperTests测试类,在类上添加@SpringBootTest注解,在类中自动装配AdminMapper类型的对象,并编写、执行测试:

package cn.tedu.boot.demo.mapper;

import cn.tedu.boot.demo.entity.Admin;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.jdbc.Sql;

@SpringBootTest
public class AdminMapperTests {

    @Autowired
    AdminMapper mapper;

    // 测试插入数据是成功的
    @Test
    @Sql(scripts = {"classpath:truncate.sql"})
    @Sql(scripts = {"classpath:truncate.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    public void testInsertSuccessfully() {
        // 准备测试数据
        String username = "admin001";
        String password = "000000";
        Admin admin = new Admin();
        admin.setUsername(username);
        admin.setPassword(password);
        // 断言测试过程中不会抛出异常
        Assertions.assertDoesNotThrow(() -> {
            // 执行测试
            int rows = mapper.insert(admin);
            // 断言结果
            Assertions.assertEquals(1, rows);
            Assertions.assertEquals(1L, admin.getId());
        });
    }

}

4.5. 根据用户名查询用户数据

基于当前数据表的设计,每个管理员的“用户名”必须是唯一的,在提交增加管理员(或注册)时,必须先检查用户名是否被占用,如果被占用,将不允许增加(或注册)。判断“是否被占用”可以通过“根据用户名查询用户数据”来分析!

此部分练习请自行完成!

5. 业务逻辑层

5.1. 关于业务逻辑层

业务逻辑层是制定数据访问规则的层,此前的数据访问层只有功能,没有规则,例如执行“插入管理员数据”,则对应的方法就一定会执行,并不考虑是否合理,有关“合理”、“是否允许”等这样规则都是通过业务逻辑层来实现的。

业务逻辑层是数据访问层的调用者,通过调用相关的功能来保证规则的合理性、完整性、有效性!例如,在业务逻辑层中,可以先调用“根据用户名查询管理员信息”,再根据调用的返回值来决定是否执行“插入管理员数据”,就可以保证“每个管理员的用户名都是唯一的”这样的规则。

另外,业务逻辑层还需要考虑数据的完整性,因为在执行数据访问时,并不是所有必须的数据都会由客户端提交过来,在这样的过程中,业务逻辑层就需要补全一些数据,例如在“增加管理员”时,“是否启用”可能不会设计为客户端提交的数据,则业务逻辑层就可以补全此属性再调用数据访问进行插入数据操作。

对于一些特殊的数据,可能还需要在业务逻辑层中进行特殊的处理,以保证数据的合理性或有效性,典型的例如各用户的密码,由客户端提交过来的密码通常是明文,在业务逻辑层就应该对密码进行加密处理,并得到密文,然后再向数据库中写入。

在实际编写代码时,业务逻辑层的关键字是Service,通常业务逻辑层的类或接口名中都有此关键字。

业务逻辑层通常有2个部分,一个是接口,另一个是此接口的实现类。

注意:强烈建议在业务逻辑层先定义接口,再编写实现类!这样做是一种基于接口编程的做法,是提倡的,并且,在后续使用基于Spring JDBC的事务管理中,也要求业务逻辑层必须有接口!

在编写业务逻辑层,所有视为“失败”的情况都应该将异常抛出,而不要处理!

5.2. 自定义异常

为了更好的在业务逻辑层表现“错误”(操作失败,例如增加管理员时,用户名已存在,即视为错误),应该自定义一些异常类型,并在处理业务逻辑的过程中,当出现错误时抛出异常!

则在cn.tedu.boot.demo下创建ex子包,并在其下创建ServiceException异常类,继承自RuntimeException,并且,至少添加带String参数的构造方法,便于抛出异常时可以快捷封装错误的描述文本。

package cn.tedu.boot.demo.ex;

public class ServiceException extends RuntimeException {
    
    public ServiceException() {
    }

    public ServiceException(String message) {
        super(message);
    }
    
}

提示:自定义的业务异常应该继承自RuntimeException,因为当抛出RuntimeException对象时,不需要在方法的声明上使用throws声明抛出,并且,此方法的调用者还必须通过try...catchthrows解决语法问题,同时,由于业务逻辑层不适合处理异常,应该始终抛出,并且,业务逻辑层的调用者是控制器层,在Spring MVC中有统一处理异常的机制,所以在控制器中也应该是始终抛出即可,那么,对于异常的语法使用是固定的,而使用RuntimeException就可以避免受到语法的约束!另外,在后续基于Spring JDBC的事务管理中,默认也是根据RuntimeException进行失败的处理的!

5.3. 业务接口与抽象方法

需要自定义类型将“增加管理员”的各数据封装起来,则在cn.tedu.boot.demo下创建dto子包,并在其下创建AdminAddNewDTO类,并在这个类中声明各属性:

@Data
public class AdminAddNewDTO implements Serializable {
    private String username;
    private String password;
    private String nickname;
}

cn.tedu.boot.demo包下创建service子包,并在其下创建IAdminService接口,并在接口中声明“增加管理员”的抽象方法:

public interface IAdminService {
    
    void addNew(AdminAddNewDTO adminAddNewDTO);
    
}

提示:在业务逻辑层的抽象方法中,设计返回值时,仅以操作成功为前提来设计即可,因为所有的失败都会通过抛出异常的方式来表现。

提示:关于抽象方法的参数,如果参数的数量较少,直接声明即可,如果参数数量较多,则应该封装,在封装时,应该注意“将客户端会提交的数据封装在一起,如果某些数据不是客户端提交过来的,则不要封装在一起”。

5.4. 关于SLF4j

SLF4j是一款主流的日志框架,用于在代码中添加一些输出日志的语句,最终这些日志可以输出到控制台,或文件,甚至数据库中。

在SLF4j日志框架中,会将日志的重要程度分为几个级别,常用级别中,从不重要到非常重要,依次是:

  • trace:跟踪
  • debug:调试
  • info:一般信息(默认)
  • warn:警告
  • error:错误

在使用时,可以控制日志的显示级别,较低级别的将不会被显示,例如:

  • 当显示级别为info时,只会显示infowarnerror
  • 当显示级别为debug时,只会显示debuginfowarnerror
  • 当显示级别为trace时,会显示所有级别的日志

在Spring Boot项目中,在spring-boot-starter中已经集成了日志的依赖项,是可以直接使用的!在application.properties中添加配置,可以控制日志的显示级别,例如:

logging.level.cn.tedu.boot.demo.service.impl=info

在以上属性名中,配置的包是“根包”,例如配置为cn.tedu时,其子孙包中都会应用此配置。

当项目中已经添加了Lombok依赖后,可以在需要输出日志的类上添加@Slf4j注解,然后,在类中就可以使用名为log的变量来输出日志!

输出日志的示例代码:

log.trace("输出trace级别的日志");
log.debug("输出debug级别的日志");
log.info("输出info级别的日志");
log.warn("输出warn级别的日志");
log.error("输出error级别的日志");

在开发实践中,应该根据要输出的内容的敏感程度、重要性来选择调用某个方法,以输出对应级别的日志,例如涉及关键数据的应该使用tracedebug级别,这样的话,当交付项目时,将设置日志显示级别的配置删除,或显式的配置为info级别,则tracedebug级别的日志将不会被输出。

另外,warnerror级别的日志不受显示级别的限制。

关于输出日志的方法,都是被重载了多次的!如果输出的内容只是1个字符串,应该使用例如:

public void debug(String msg);

如果这个字符串中需要拼接多个变量的值,则应该使用:

public void debug(String format, Object... arguments);

使用示例如下:

log.debug("已经对密码进行加密处理,原文={},密文={}", rawPassword, encodedPassword);

以上这种做法会缓存、预编译字符串,再将值代入去执行,所以执行效率还远高于System.out.println()的输出语句!

另外,需要注意的是,SLF4j只是一个日志框架,它提供了使用日志的标准,并没有实现输出日志的具体功能,在现行版本的Spring Boot中,还依赖了SLF4j的具体实现,默认是logback框架。

5.5. 业务实现

通常,会在service包下再创建impl子包,用于存放业务接口的实现类,并且,实现类的名称通常是“接口名(不包含首字母I) + Impl”。

业务实现类应该实现业务接口,并且,还应该添加@Service注解。

所以,在cn.tedu.boot.demo.service.impl中创建AdminServiceImpl类,实现IAdminService接口,在类上添加@Service注解,并重写接口中的抽象方法:

package cn.tedu.boot.demo.service.impl;

import cn.tedu.boot.demo.dto.AdminAddNewDTO;
import cn.tedu.boot.demo.service.IAdminService;
import org.springframework.stereotype.Service;

@Service
public class AdminServiceImpl implements IAdminService {

    @Override
    public void addNew(AdminAddNewDTO adminAddNewDTO) {

    }
    
}

接下来,在编写业务方法(实现接口中的抽象方法)之前,应该整理此业务的编写思路:

package cn.tedu.boot.demo.service.impl;

import cn.tedu.boot.demo.dto.AdminAddNewDTO;
import cn.tedu.boot.demo.entity.Admin;
import cn.tedu.boot.demo.ex.ServiceException;
import cn.tedu.boot.demo.mapper.AdminMapper;
import cn.tedu.boot.demo.service.IAdminService;
import cn.tedu.boot.demo.util.GlobalPasswordEncoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

@Slf4j
@Service
public class AdminServiceImpl implements IAdminService {

    // 自动装配AdminMapper
    @Autowired
    private AdminMapper adminMapper;

    @Override
    public void addNew(AdminAddNewDTO adminAddNewDTO) {
        // 通过参数adminAddNewDTO中的username,调用AdminMapper的Admin getByUsername(String username)执行查询,并获取查询结果
        log.debug("即将增加管理员:{}", adminAddNewDTO);
        String username = adminAddNewDTO.getUsername();
        Admin queryResult = adminMapper.getByUsername(username);
        // 判断查询结果是否【不为null】
        if (queryResult != null) {
            // 是:表示用户名已经被占用,抛出ServiceException:增加管理员失败,用户名已经被占用
            log.warn("增加管理员失败,用户名({})已经被占用!", username);
            throw new ServiceException("增加管理员失败,用户名已经被占用!");
        }

        // 以参数adminAddNewDTO中的password作为明文,执行加密,得到密文密码
        String rawPassword = adminAddNewDTO.getPassword();
        String encodedPassword = GlobalPasswordEncoder.encode(rawPassword);
        log.debug("已经对密码进行加密处理,原文={},密文={}", rawPassword, encodedPassword);

        // 创建新的Admin对象
        Admin admin = new Admin();
        // 为Admin对象的属性赋值:username,nickname来自参数adminAddNewDTO
        admin.setUsername(username);
        admin.setNickname(adminAddNewDTO.getNickname());
        // 为Admin对象的属性赋值:password > 密文密码
        admin.setPassword(encodedPassword);
        // 为Admin对象的属性赋值:avatar, phone, email, description保持为null
        // 为Admin对象的属性赋值:isEnable > 1
        admin.setIsEnable(1);
        // 为Admin对象的属性赋值:lastLoginIp > null
        // 为Admin对象的属性赋值:loginCount > 0
        admin.setLoginCount(0);
        // 为Admin对象的属性赋值:gmtLastLogin > null
        // 为Admin对象的属性赋值:gmtCreate, gmtModified > LocalDateTime.now()
        LocalDateTime now = LocalDateTime.now();
        admin.setGmtCreate(now);
        admin.setGmtModified(now);
        // 调用AdminMapper对象的int insert(Admin admin)方法插入管理员数据,并获取返回值
        log.debug("即将执行插入管理员数据:{}", admin);
        int rows = adminMapper.insert(admin);
        // 判断返回值是否不为1
        if (rows != 1) {
            // 抛出ServiceException:服务器忙,请稍后再次尝试
            log.warn("服务器忙,请稍后再次尝试!");
            throw new ServiceException("服务器忙,请稍后再次尝试!");
        }
    }

}

完成后,在src/test/java下的cn.tedu.boot.demo下创建service子包,并在其下创建AdminServiceTests测试类,编写并执行测试:

package cn.tedu.boot.demo.service;

import cn.tedu.boot.demo.dto.AdminAddNewDTO;
import cn.tedu.boot.demo.ex.ServiceException;
import cn.tedu.boot.demo.service.impl.AdminServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.jdbc.Sql;

@SpringBootTest
public class AdminServiceTests {

    @Autowired
    IAdminService service;

    @Test
    @Sql(scripts = {"classpath:truncate.sql"})
    @Sql(scripts = {"classpath:truncate.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    public void testAddNewSuccessfully() {
        // 测试数据
        String username = "admin001";
        String password = "123456";
        String nickname = "管理员";
        AdminAddNewDTO adminAddNewDTO = new AdminAddNewDTO()
                .setUsername(username)
                .setPassword(password)
                .setNickname(nickname);
        // 断言不会抛出异常
        Assertions.assertDoesNotThrow(() -> {
            // 执行测试
            service.addNew(adminAddNewDTO);
        });
    }

    @Test
    @Sql(scripts = {"classpath:truncate.sql", "classpath:insert_data.sql"})
    @Sql(scripts = {"classpath:truncate.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    public void testAddNewFailBecauseUsernameConflict() {
        // 测试数据
        String username = "admin001";
        String password = "123456";
        String nickname = "管理员";
        AdminAddNewDTO adminAddNewDTO = new AdminAddNewDTO()
                .setUsername(username)
                .setPassword(password)
                .setNickname(nickname);
        // 断言不会抛出异常
        Assertions.assertThrows(ServiceException.class, () -> {
            // 执行测试
            service.addNew(adminAddNewDTO);
        });
    }

}

6. 控制器

6.1. 处理依赖项

当需要开发控制器时,需要在项目中存在spring-boot-starter-web的依赖项,此依赖项将包含此前学习时涉及的spring-webmvcjackson-databind等依赖项。

在具体操作方面,并不需要追加添加这个依赖项,只需要将spring-boot-starter改为spring-boot-starter-web即可,并且,在spring-boot-starter-web中也包含了spring-boot-starter,所以,对此项目原本的依赖也不产生影响。

6.2. 简单开发

cn.tedu.boot.demo下创建controller子包,并在其下创建AdminController类,作为处理“管理员”数据相关请求的控制器类,并在这个类中处理“增加管理员”的请求:

package cn.tedu.boot.demo.controller;

import cn.tedu.boot.demo.dto.AdminAddNewDTO;
import cn.tedu.boot.demo.service.IAdminService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/admin")
public class AdminController {

    @Autowired
    private IAdminService adminService;
    
    // http://localhost:8080/admin/add-new?username=admin001&password=1234&nickname=a001
    @RequestMapping("/add-new")
    public String addNew(AdminAddNewDTO adminAddNewDTO) {
        adminService.addNew(adminAddNewDTO);
        return "OK";
    }
    
}

因为spring-boot-starter-web中依赖了Tomcat,相当于每个Spring Boot工程都有一个内置的Tomcat,并且将Context Path配置为空字符串,所以在URL上并不需要添加其它路径,最后,启动项目时,就会自动打包部署此项目到内置的Tomcat上。

所以,执行BootDemoApplication,打开浏览器,通过 http://localhost:8080/admin/add-new?username=admin001&password=1234&nickname=a001 即可增加管理员。

以上只是简单的实现了数据访问,还需要解决的问题有:

  • 响应的结果不是JSON格式
  • 没有处理异常
  • 需要提供在线API文档
  • 没有对参数的基本格式进行检查

6.3. 响应JSON格式的数据

将此前学习Spring MVC时设计的JsonResult复制到此项目的cn.tedu.boot.demo.web包中,并且将处理请求的方法的返回值类型改为JsonResult类型:

// http://localhost:8080/admin/add-new?username=admin001&password=1234&nickname=a001
@RequestMapping("/add-new")
public JsonResult<Void> addNew(AdminAddNewDTO adminAddNewDTO) {
    adminService.addNew(adminAddNewDTO);
	return JsonResult.ok();
}

完成后,重启项目,通过正确的参数即可成功增加管理员,并且可以看到响应的结果是JSON格式的数据,例如:

{"state":20000,"message":null,"data":null}

以上数据中,messagedata都没有数据,是多余的!可以在application.properties中添加配置,以去除JSON数据中为null的部分:

spring.jackson.default-property-inclusion=non_null

重启服务后,响应的JSON数据中将不再包含为null的部分!

6.4. 处理异常

目前,在业务逻辑层抛出了2种不同原因导致的异常,异常的类型是完全相同的,会导致处理异常时,无法判断是哪种情况导致的异常,所以,应该先改造异常类,在类中添加State属性,并要求通过构造方法传入,则每个异常对象中都会包含异常的状态码和错误时的文本描述:

package cn.tedu.boot.demo.ex;

import cn.tedu.boot.demo.web.JsonResult;

public class ServiceException extends RuntimeException {

    private JsonResult.State state;

    public ServiceException() {
    }

    public ServiceException(JsonResult.State state, String message) {
        super(message);
        this.state = state;
    }

    public JsonResult.State getState() {
        return state;
    }

}

由于抛出异常时既包含了状态码,又包含了错误的描述文本,在JsonResult中还可以添加一个更加便捷的静态方法:

public static JsonResult<Void> fail(ServiceException e) {
    return fail(e.getState(), e.getMessage());
}

为了保证能够对当前已分析的2种错误进行区分,应该在State枚举中添加对应的状态码:

public enum State {
   OK(20000),
   ERR_CONFLICT(40900),
   ERR_INTERNAL_ERROR(50000);

   Integer value;

   State(Integer value) {
       this.value = value;
   }

   public Integer getValue() {
       return value;
   }
}

经过以上调整,原本的业务逻辑层的实现类将会报告错误,需要在创建并抛出异常时,除了传入错误的描述文本,还需要传入状态码

@Override
public void addNew(AdminAddNewDTO adminAddNewDTO) {
    // 忽略此次不需要调整的代码... ...
    // 判断查询结果是否【不为null】
    if (queryResult != null) {
        // 是:表示用户名已经被占用,抛出ServiceException:增加管理员失败,用户名已经被占用
        log.warn("增加管理员失败,用户名({})已经被占用!", username);
        throw new ServiceException(JsonResult.State.ERR_CONFLICT, "增加管理员失败,用户名已经被占用!");
    }

    // 忽略此次不需要调整的代码... ...
    // 判断返回值是否不为1
    if (rows != 1) {
        // 抛出ServiceException:服务器忙,请稍后再次尝试
        log.warn("服务器忙,请稍后再次尝试!");
        throw new ServiceException(JsonResult.State.ERR_INTERNAL_ERROR, "服务器忙,请稍后再次尝试!");
    }
}

cn.tedu.boot.demo.controller包下创建handler子包,并在其下创建GlobalExceptionHandler统一处理异常的类,在类上添加@RestControllerAdvice注解,并在类中处理异常。

@RestControllerAdvicd
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ServiceException.class)
    public JsonResult<Void> handleServiceException(ServiceException e) {
        return JsonResult.fail(state, e.getMessage());
    }
    
}
 类似资料: