我正在参加「启航方案」

1. 关于Spring Boot

Spring Boot是Spring官方的一个产品,其本质上是一个依据Maven的、以Spring结构作为根底的进阶结构,很好的支撑了干流的其它结构,并默许完成了许多的装备,其间心思想是“约定大于装备”。

2. 创立Spring Boot工程

在IntelliJ IDEA中,在创立导游中选择Spring Initializer即可开端创立Spring Boot工程,在创立导游的界面中,需求重视的部分有:

  • Group Id
  • Artifact Id

以上2个值会共同构成一个Package name,假如Artifact Id的名字中有减号,在Package name中会去除,引荐手动增加小数点进行分隔。

因为Spring Boot官方更新版别的频率十分高,在创立项目时,随意选取某个版别均可,当项目创立成功后,引荐翻开pom.xml,将<parent>中的<version>(即Spring Boot父项目的版别)改成熟悉的版别,例如:2.5.9

在创立进程中,还能够在创立导游的界面中勾选所需求依靠项,假如创立时没有勾选,也能够在创立工程之后手动在pom.xml中增加。

3. Spring Boot工程的结构

因为Spring Boot工程本质上便是一个Maven工程,所以,目录结构根本上没有差异。

与一般Maven工程最大的不同在于:Spring Boot工程在src\main\javasrc\test\java下默许现已存在Package,是创立项目时指定的Package,**需求留意:此Package现已被装备为Spring履行组件扫描的根包,所以,在编写代码时,一切的组件类都有必要放在此包或这今后代包中!**一般,引荐将一切的类(及接口)都创立在此包及这今后代包下。

src\main\java下的根包下,默许就现已存在某个类,其类名是创立项目时指定的Artifact与Application单词的组合,例如BootDemoApplication,此类中有main()办法,履行此类的main()就会发动整个项目,假如当时项目是Web项目,还会主动将项目布置到Web服务器并发动服务器,所以,此类一般也称之为“发动类”。

在发动类上,默许增加了@SpringBootApplication注解,此注解的元注解中包括@SpringBootConfiguration,而@SpringBootConfiguration的元注解中包括@Configuration,所以,发动类自身也是装备类!所以,答应将@Bean办法写在此类中,或者某些与装备相关的注解也能够增加在此类上!

src\test\java下的根包下,默许就现已存在某个类,其类名是在发动类的称号根底上增加了Tests单词的组合,例如BootDemoApplicationTests,此类默许没有增加public权限,乃至其内部的默许的测验办法也是默许权限的,此测验类上增加了@SpringBootTest注解,其元注解中包括@ExtendWith(SpringExtension.class),与运用spring-test时的@SpringJUnitTest注解中的元注解相同,所以,@SpringBootTest注解也会使得当时测验类在履行测验办法之前是加载了Spring环境的,在实践编写测验时,能够经过主动安装得到任何已存在于Spring容器中的方针,在各测验办法中只需求重视被测验的方针即可。

pom.xml中,默许现已增加了spring-boot-starterspring-boot-starter-test依靠,分别是Spring Boot的根底依靠依据Spring Boot的测验的依靠

别的,假如在创立工程时,勾选依靠项时选中了Web项,在src\main\resources下默许就现已创立了statictemplates文件夹,假如没有勾选Web则没有这2个文件夹,能够后续自行弥补创立。

src\main\resources文件夹下,默许就现已存在application.properties文件,用于编写装备,Spring Boot会主动读取此文件(运用@PropertySource注解)。

小结:

  • 创立项目后默许的Package不要修正,防止犯错
  • 在编码进程中,自行创立的一切类、接口均放在默许的Package或这今后代包中
  • src\main\java下默许已存在XxxApplication是发动类,履行此类中的main()办法就会发动整个项目
  • 发动类自身也是装备类
  • 装备都应该编写到src\main\resources下的application.properties中,Spring Boot会主动读取
  • 测验类也有必要放在src\test\java下的默许Package或这今后代包中
  • 在测验类上增加@SpringBootTest注解,则其间的测验办法履行之前会主动加载Spring环境及当时项目的装备,能够在测验类中运用主动安装

4. 在Spring Boot工程中运用Mybatis

需求增加相关依靠项:

  • mysql-connector-java
  • mybatis-spring-boot-starter

其依靠的代码为:

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

阐明:在Spring Boot工程,许多依靠项都是不需求显式的指定版别号的,因为在父项目中现已对这些依靠项的版别进行了办理(装备版别号),假如必定需求运用特定的版别,也能够自行增加<version>节点进行装备

阐明:在依靠项的源代码中,当<scope>的值为runtime时,表明此依靠项是运转进程中需求的,可是,在编译时并不需求参与编译

需求留意:当增加了以上数据库编程的依靠后,假如发动项目,将失利!

因为增加了数据库编程的依靠项后,Spring Boot就会测验主动安装数据源(DataSource)等方针,安装时所需的衔接数据库的装备信息(例如URL、登录数据库的用户名和暗码)应该是装备在application.properties中的,可是,假如尚未装备,就会导致失利!

关于衔接数据库的装备信息,Spring Boot要求对应的特点名是:

# 衔接数据库的URL
spring.datasource.url=???
# 登录数据库的用户名
spring.datasource.username=???
# 登录数据库的暗码
spring.datasource.password=???

在装备时,也有必要运用以上特点名进行装备,则Spring Boot会主动读取这些特点对应的值,用于创立数据源方针!

例如,装备为:

# 衔接数据库的URL
spring.datasource.url=jdbc:mysql://localhost:3306/mall_ams?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
# 登录数据库的用户名
spring.datasource.username=root
# 登录数据库的暗码
spring.datasource.password=1234

因为Spring Boot在发动时仅仅加载以上装备,并不会实践的衔接到数据库,所以,当以上装备存在时,发动就不会报错,可是,无法查验以上装备的值是否正确!

能够在测验类中增加测验办法,测验衔接数据库,以查验以上装备值是否正确:

@SpringBootTest
class BootDemoApplicationTests {
    @Autowired
    DataSource dataSource;
    @Test
    void testGetConnection() throws Exception {
        System.out.println(dataSource.getConnection());
    }
}

假如以上测验经过,则表明装备值无误,能够正确衔接到数据库,假如测验失利,则表明装备值过错,需查看装备值及本地环境(例如MySQL是否发动、是否已创立对应的数据库等)。

5. 关于Profile装备

在Spring Boot中,对Profile装备有很好的支撑,开发人员能够在src\main\resources下创立更多的装备文件,这些装备文件的称号应该是application-???.properties(其间的???是某个称号,是自界说的)。

例如:

  • 仅在开发环境中运用的装备值能够写在application-dev.properties
  • 仅在测验环境中运用的装备值能够写在application-test.properties
  • 仅在出产环境(项目上线的环境)中运用的装备值能够写在application-prod.properties

当把装备写在以上这类文件后,Spring Boot默许并不会运用以上这些文件中的装备,当需求运用某个装备时,需求在application.properties中激活某个Profile装备,例如:

# 激活Profile装备
spring.profiles.active=dev

提示:以上装备值中的dev是需求激活的装备文件的文件名后缀,当装备为dev时,就会激活application-dev.properties,同理,假如以上装备值为test,就会激活application-test.properties

6. 关于YAML装备

Spring Boot也支撑运用YAML装备,在开发实践中,YAML的装备也运用得比较多。

YAML装备便是把原有的.properties装备的扩展改为yml

YAML装备本来并不是Spring系列结构内置的装备语法,假如在项目中需求运用这种语法进行装备,解析这类文件需求增加相关依靠,在Spring Boot中默许已增加此依靠。

在YAML装备中,本来在.properties的装备体现为运用多个小数点分隔的装备将改为换行运用2个空格缩进的语法,换行前的部分运用冒号表明完毕,最终的特点名与值之间运用冒号和1个空格进行分隔,假如有多条特点在.properties文件中特点名有重复的前缀,在yml中不用也不能重复写。

例如,本来在.properties中装备为:

spring.datasource.username=root
spring.datasource.password=123456

则在yml文件中装备为:

spring:
  datasource:
    username: root
    password: 123456

提示:在IntelliJ IDEA中编写yml时,当需求缩进2个空格时,仍能够运用键盘上的TAB键进行缩进,IntelliJ IDEA会主动将其转换为2个空格。

无论是.properties仍是yml,仅仅装备文件的扩展名和文件内部的装备语法有差异,关于Spring Boot终究的履行其实没有任何体现上的不同。

7. 运用Druid数据库衔接池

Druid数据库衔接是阿里巴巴团队研制的,在Spring Boot项目中,假如需求显式的指定运用此衔接池,首要,需求在项目中增加依靠:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.20</version>
</dependency>

当增加了此依靠,在项目中需求运用时,需求在装备文件中指定spring.datasource.type特点,取值为以上依靠项的jar包中的DruidDataSource类型的全限定名。

例如,在yml中装备为:

# Spring系列结构的装备
spring:
  # 衔接数据库的相关装备
  datasource:
    # 运用的数据库衔接池类型
    type: com.alibaba.druid.pool.DruidDataSource

8. 编写耐久层(数据拜访层)代码

数据耐久化:在开发领域中,评论数据时,一般指定是正在履行或处理的数据,这些数据都是在内存中的,而内存(RAM)的特征包括”一旦断电,数据将全部丢失“,为了让数据永久保存下来,一般会将数据存储到能够永久存储数据的介质中,一般是核算机的硬盘,硬盘上的数据都是以文件的办法存在的,所以,当需求永久保存数据时,能够将数据存储到文本文件中,或存储到XML文件中,或存储到数据库中,这些保存的做法便是数据耐久化,而文本文件、XML文件都不利于完成增修正查中的一切数据拜访操作,而数据库是完成增修正查这4种操作都比较便利的,所以,一般在评论数据耐久化时,默许指的都是运用数据库存储数据。

在项目中,会将代码(各类、接口)区分一些层次,各层用于处理不同的问题,其间,耐久层便是用于处理数据耐久化问题的,乃至,简略来说,耐久层对应的便是数据库编程的相关文件或代码。

现在,运用Mybatis技术完成耐久层编程,需求:

  • 编写一次性的根底装备
    • 运用@MapperScan指定接口地点的Base Package
    • 指定装备SQL句子的XML文件的方位
  • 编写每个数据拜访功用的代码
    • 在接口中增加有必要的笼统办法
      • 可能需求创立相关的POJO类
    • 在XML文件中装备笼统办法映射的SQL句子

关于一次性的装备,@MapperScan注解需求增加在装备类上,有2种做法:

  • 直接将此注解增加在发动类上,因为发动类自身也是装备类
  • 自行创立装备类,在此装备类上增加@MapperScan

假如采用以上的第2种做法,则应该在src\main\java的根包下,创立config.MybatisConfig类,并在此类运用@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 MybatisConfig {
}

别的,关于指定装备SQL句子的XML文件的方位,需求在application.yml(或application.properties)中装备mybatis.mapper-locations特点,例如:

# Mybatis相关装备
mybatis:
  # 用于装备SQL句子的XML文件的方位
  mapper-locations: classpath:mapper/*.xml

依据以上的装备值,还应该在src/main/resources下自行创立名为mapper的文件夹。

至此,关于运用Mybatis完成数据库编程的一次性装备完毕!

接下来,能够运用任何你已知的Mybatis运用办法完成所需的数据拜访。

现在,设定方针为:终究完成”增加办理员账号“的功用。则在数据拜访层需求做到:

  • 刺进办理员数据
    • 创立cn.tedu.boot.demo.entity.Admin
    • cn.tedu.boot.demo.mapper包(不存在,则创立)下创立AdminMapper接口,并在接口中声明int insert(Admin admin);办法
    • src/main/resources/mapper文件夹下经过粘贴得到AdminMapper.xml文件,在此文件中装备与以上笼统办法映射的SQL句子
    • 编写完成后,应该及时测验,测验时,引荐在src/test/java的根包下创立mapper.AdminMapperTests测验类,并在此类中编写测验办法
  • 依据用户名查询办理员数据
    • 后续,在每次刺进数据之前,会调用此功用进行查询,以此保证”重复的用户名不会被增加到数据库中“
      • 即便在数据表中用户名现已增加了unique,可是,不应该让程序履行到此处
    • AdminMapper接口中增加Admin getByUsername(String username);办法
    • AdminMapper.xml文件中增加与以上笼统办法映射的SQL句子
    • 编写完成后,应该及时测验
  • 其它问题暂不考虑,例如在ams_admin中,其实phoneemail也是设置了unique的,假如完好的完成,则还需求增加依据phone查询办理员的功用,和依据email查询办理员的功用,在不完成这2个功用的情况下,后续进行测验和运用时,应该不运用重复的phoneemail值来测验或履行

9. 关于事务逻辑层(Service层)

事务逻辑层是被Controller直接调用的层(Controller不答应直接调用耐久层),一般,在事务逻辑层中编写的代码是为了保证数据的完好性和安全性,使得数据是随着我们设定的规矩而发生或发生变化。

一般,在事务逻辑层的代码会由接口和完成类组件,其间,接口被视为是有必要的

  • 引荐运用依据接口的编程办法
  • 部分结构在处理某些功用时,会运用依据接口的代理办法,例如Spring JDBC结构在处理事务时

在接口中,声明笼统办法时,仅以操作成功为前提来规划回来值类型(不考虑失利),假如事务在履行进程可能呈现某些失利(不符合所设定的规矩),能够经过抛出反常来表明!

关于抛出的反常,一般是自界说的反常,而且,自界说反常一般是RuntimeException的子类,主要原因:

  • 不用显式的抛出或捕获,因为事务逻辑层的反常永久是抛出的,而控制器层会调用事务逻辑层,在控制器层的Controller中其实也是永久抛出反常的,这些反常会经过Spring MVC一致处理反常的机制进行处理,关于反常的整个进程都是固定流程,所以,没有必要显式抛出或捕获
  • 部分结构在处理某些工作时,默许只对RuntimeException的后代类进行识别并处理,例如Spring JDBC结构在处理事务时

所以,在实践编写事务逻辑层之前,应该先规划反常,例如先创立ServiceException类:

package cn.tedu.boot.demo.ex;
public class ServiceException extends RuntimeException {
}

接下来,再创立详细的对应某种“失利”的反常,例如,在增加办理员时,可能因为“用户名现已存在”而失利,则创立对应的UsernameDuplicateException反常:

package cn.tedu.boot.demo.ex;
public class UsernameDuplicateException extends ServiceException {
}

别的,当刺进数据时,假如回来的受影响行数不是1时,必定是某种过错,则创立对应的刺进数据反常:

package cn.tedu.boot.demo.ex;
public class InsertException extends ServiceException {
}

关于笼统办法的参数,应该规划为客户端提交的数据类型或对应的封装类型,不能够是数据表对应的实体类型!假如运用封装的类型,这品种型在类名上应该增加某种后缀,例如DTO或其它后缀,例如:

package cn.tedu.boot.demo.pojo.dto;
public class AdminAddNewDTO implements Serializable {
    private String username;
    private String password;
    private String nickname;
    private String avatar;
    private String phone;
    private String email;
    private String description;
    // Setters & Getters
    // hashCode(), equals()
    // toString()
}

然后,在cn.tedu.boot.demo.service包下声明接口及笼统办法:

package cn.tedu.boot.demo.service;
public interface IAdminService {
    void addNew(AdminAddNewDTO adminAddNewDTO);
}

并在以上service包下创立impl子包,再创立AdminServiceImpl类:

package cn.tedu.boot.demo.service.impl;
@Service // @Component, @Controller, @Repository
public class AdminServiceImpl implements IAdminService {
    @Autowired
    private AdminMapper adminMapper;
    @Override
    public void addNew(AdminAddNewDTO adminAddNewDTO) {
        // 经过参数获取用户名
        // 调用adminMapper的Admin getByUsername(String username)办法履行查询
        // 判别查询成果是否不为null
        // -- 是:表明用户名现已被占用,则抛出UsernameDuplicateException
        // 经过参数获取原暗码
        // 经过加密办法,得到加密后的暗码encodedPassword
        // 暂时不加密,写为String encodedPassword = adminAddNewDTO.getPassword();
        // 创立当时时刻方针now > LocalDateTime.now()
        // 创立Admin方针
        // 补全Admin方针的特点值:经过参数获取username,nickname……
        // 补全Admin方针的特点值:password > encodedPassword
        // 补全Admin方针的特点值:isEnable > 1
        // 补全Admin方针的特点值:lastLoginIp > null
        // 补全Admin方针的特点值:loginCount > 0
        // 补全Admin方针的特点值:gmtLastLogin > null
        // 补全Admin方针的特点值:gmtCreate > now
        // 补全Admin方针的特点值:gmtModified > now
        // 调用adminMapper的insert(Admin admin)办法刺进办理员数据,获取回来值
        // 判别以上回来的成果是否不为1,抛出InsertException反常
    }
}

以上事务代码的完成为:

package cn.tedu.boot.demo.service.impl;
import cn.tedu.boot.demo.entity.Admin;
import cn.tedu.boot.demo.ex.InsertException;
import cn.tedu.boot.demo.ex.UsernameDuplicateException;
import cn.tedu.boot.demo.mapper.AdminMapper;
import cn.tedu.boot.demo.pojo.dto.AdminAddNewDTO;
import cn.tedu.boot.demo.service.IAdminService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@Service
public class AdminServiceImpl implements IAdminService {
    @Autowired
    private AdminMapper adminMapper;
    @Override
    public void addNew(AdminAddNewDTO adminAddNewDTO) {
        // 经过参数获取用户名
        String username = adminAddNewDTO.getUsername();
        // 调用adminMapper的Admin getByUsername(String username)办法履行查询
        Admin queryResult = adminMapper.getByUsername(username);
        // 判别查询成果是否不为null
        if (queryResult != null) {
            // 是:表明用户名现已被占用,则抛出UsernameDuplicateException
            throw new UsernameDuplicateException();
        }
        // 经过参数获取原暗码
        String password = adminAddNewDTO.getPassword();
        // 经过加密办法,得到加密后的暗码encodedPassword
        String encodedPassword = password;
        // 创立当时时刻方针now > LocalDateTime.now()
        LocalDateTime now = LocalDateTime.now();
        // 创立Admin方针
        Admin admin = new Admin();
        // 补全Admin方针的特点值:经过参数获取username,nickname……
        admin.setUsername(username);
        admin.setNickname(adminAddNewDTO.getNickname());
        admin.setAvatar(adminAddNewDTO.getAvatar());
        admin.setPhone(adminAddNewDTO.getPhone());
        admin.setEmail(adminAddNewDTO.getEmail());
        admin.setDescription(adminAddNewDTO.getDescription());
        // 以上这些从一个方针中把特点赋到另一个方针中,还能够运用:
        // BeanUtils.copyProperties(adminAddNewDTO, admin);
        // 补全Admin方针的特点值:password > encodedPassword
        admin.setPassword(encodedPassword);
        // 补全Admin方针的特点值:isEnable > 1
        admin.setIsEnable(1);
        // 补全Admin方针的特点值:lastLoginIp > null
        // 补全Admin方针的特点值:loginCount > 0
        admin.setLoginCount(0);
        // 补全Admin方针的特点值:gmtLastLogin > null
        // 补全Admin方针的特点值:gmtCreate > now
        admin.setGmtCreate(now);
        // 补全Admin方针的特点值:gmtModified > now
        admin.setGmtModified(now);
        // 调用adminMapper的insert(Admin admin)办法刺进办理员数据,获取回来值
        int rows = adminMapper.insert(admin);
        // 判别以上回来的成果是否不为1,抛出InsertException反常
        if (rows != 1) {
            throw new InsertException();
        }
    }
}

以上代码未完成对暗码的加密处理!关于暗码加密,相关的代码应该界说在别的某个类中,不应该直接将加密进程编写在以上代码中,因为加密的代码需求在多处运用(增加用户、用户登录、修正暗码等),而且,从分工的角度上来看,也不应该是事务逻辑层的使命!所以,在cn.tedu.boot.demo.util(包不存在,则创立)下创立PasswordEncoder类,用于处理暗码加密:

package cn.tedu.boot.demo.util;
@Component
public class PasswordEncoder {
    public String encode(String rawPassword) {
        return "aaa" + rawPassword + "aaa";
    }
}

完成后,需求在AdminServiceImpl中主动安装以上PasswordEncoder,并在需求加密时调用PasswordEncoder方针的encode()办法。

10. 运用Lombok结构

在编写POJO类型(包括实体类、VO、DTO等)时,都有一致的编码标准,例如:

  • 特点都是私有的
  • 一切特点都有对应的Setter & Getter办法
  • 应该重写equals()hashCode()办法,以保证:假如2个方针的字面值完全相同,则equals()对比成果为true,且hashCode()回来值相同,假如2个方针的字面值不相同,则equals()对比成果为false,且hashCode()回来值不同
  • 完成Serializable接口

别的,为了便于观察方针的各特点值,一般还会重写toString()办法。

因为以上操作办法十分固定,且触及的代码量尽管不难,可是篇幅较长,而且,当类中的特点需求修正时(包括修正原有特点、或增加新特点、删去原有特点),对应的其它办法都需求修正(或从头生成),办理起来比较费事。

运用Lombok结构能够极大的简化这些操作,此结构能够经过注解的办法,在编译期来生成Setters & Getters、equals()hashCode()toString(),乃至生成结构办法等,所以,一旦运用此结构,开发人员就只需求在类中声明各特点、完成Serializable、增加Lombok指定的注解即可。

在Spring Boot中,增加Lombok依靠,能够在创立项目时勾选,也能够后期自行增加,依靠项的代码为:

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

完成后,在各POJO类型中,将不再需求在源代码增加Setters & Getters、equals()hashCode()toString()这些办法,只需求在POJO类上增加@Data注解即可!

当增加@Data注解,且删去相关办法后,因为源代码中没有相关办法,则调用了相关代码的办法可能会报错,可是,并不影响程序运转!

为了防止IntelliJ IDEA判别失误而提示了正告和过错,引荐安装Lombok插件,可参阅:

  • doc.canglaoshi.org/doc/idea_lo…

再次提示:无论是否安装插件,都不影响代码的编写和运转!

11. Slf4j日志结构

在开发实践中,不答应运用System.out.println()或相似的输出句子来输出显现要害数据(中心数据、敏感数据等),因为,假如是这样运用,无论是在开发环境,仍是测验环境,仍是出产环境中,这些输出句子都将输出相关信息,而删去或增加这些输出句子的操作本钱比较高,操作可行性低。

引荐的做法是运用日志结构来输出相关信息!

当增加了Lombok依靠后,能够在需求运用日志的类上增加@Slf4j注解,然后,在类的恣意中,均可运用名为log变量,且调用其办法来输出日志(名为log的变量也是Lombok结构在编译期主动弥补的声明并创立方针)!

在Slf4j日志结构中,将日志的可显现等级依据其重要程度(严重程度)由低到高分为:

  • trace:盯梢信息
  • debug:调试信息
  • info:一般信息,一般不触及要害流程和敏感数据
  • warn:正告信息,一般代码能够运转,但不行完美,或不标准
  • error:过错信息

在装备文件中,能够经过logging.level.包名.类名来设置当时类的日志显现等级,例如:

logging.level.cn.tedu.boot.demo.service.impl.AdminServiceImpl: info

当设置了显现的日志等级后,仅显现设置等级和更重要的等级的日志,例如,设置为info时,只显现infowarnerror,不会显现debugtrace等级的日志!

当输出日志时,经过log变量调用trace()办法输出的日志便是trace等级的,调用debug()办法输出的日志便是debug()等级的,以此类推,可调用的办法还有info()warn()error()

在开发实践中,要害数据和敏感数据都应该经过trace()debug()进行输出,在开发环境中,能够将日志的显现等级设置为trace,则会显现一切日志,当需求交付到出产环境中时,只需求将日志的显现等级调整为info即可!

默许情况下,日志的显现等级是info,所以,即便没有在装备文件中进行正确的装备,一切info、warn、error等级的日志都会输出显现。

在装备时,特点称号中的logging.level部分是有必要的,在这今后,有必要写至少1级包名,例如:

logging.level.cn: trace

以上装备表明cn包及这今后代包下的一切类中的日志都依照trace等级进行显现!

在开发实践中,特点称号一般装备为logging.level.项目根包,例如:

logging.level.cn.tedu.boot.demo: trace

在运用Slf4j时,经过log调用的每种等级的办法都被重载了多次(各等级对应除了办法称号不同,重载的次数和参数列表均相同),引荐运用的办法是参数列表为(String format, Object... arguments)的,例如:

public void trace(String format, Object... arguments);
public void debug(String format, Object... arguments);
public void info(String format, Object... arguments);
public void warn(String format, Object... arguments);
public void error(String format, Object... arguments);

以上办法中,第1个参数是行将输出的字符串的办法(模版),在此字符串中,假如需求包括某个变量值,则运用{}表明,假如有多个变量值,均是如此,然后,再经过第2个参数(是可变参数)顺次表明各{}对应的值,例如:

log.debug("加密前的暗码:{},加密后的暗码:{}", password, encodedPassword);

运用这种做法,能够防止多变量时频频的拼接字符串,别的,日志结构会将第1个参数进行缓存,以此进步后续每一次的履行效率。

在开发实践中,应该对程序履行要害方位增加日志的输出,一般包括:

  • 每个办法的第1行有用句子,表明代码现已履行到此办法内,或此办法现已被成功调用
    • 假如办法是有参数的,还应该输出参数的值
  • 要害数据或中心数据在改变之前和之后
    • 例如对暗码加密时,应该经过日志输出加密前和加密后的暗码
  • 重要的操作履行之前
    • 例如测验刺进数据之前、修正数据之前,应该经过日志输出相关值
  • 程序走到某些重要的分支时
    • 例如经过判别,走向抛出反常之前

其实,Slf4j日志结构仅仅日志的一种标准,并不是详细的完成(感觉上与Java中的接口有点相似),常见有详细完成了日志功用的结构有log4j、logback等,为了一致标准,所以才呈现了Slf4j,一同,因为log4j、logback等结构完成功用并不一致,所以,Slf4j提供了对干流日志结构的兼容,在Spring Boot工程中,spring-boot-starter就现已依靠了spring-boot-starter-logging,而在此依靠下,一般包括Slf4j、详细的日志结构、Slf4j对详细日志结构的兼容。

12. 暗码加密

【这并不是Spring Boot结构的知识点】

对暗码进行加密,能够有用的保障暗码安全,即便呈现数据库泄密,暗码安全也不会受到影响!为了完成此方针,需求在对暗码进行加密时,运用不可逆的算法进行处理!

一般,不能够运用加密算法对暗码进行加暗码处理,从严厉界说上来看,一切的加密算法都是能够逆向运算的,即一同存在加密和解密这2种操作,加密算法只能用于保证传输进程的安全,并不应该用于保证需求存储下来的暗码的安全!

哈希算法都是不可逆的,一般,用于处理暗码加密的算法中,典型的是一些音讯摘要算法,例如MD5、SHA256或以上位数的算法。

音讯摘要算法的主要特征有:

  • 音讯相一同,摘要必定相同
  • 某种算法,无论音讯长度多少,摘要的长度是固定的
  • 音讯不一同,摘要几乎不会相同

在音讯摘要算法中,以MD5为例,其运算成果是一个128位长度的二进制数,一般会转换成十六进制数显现,所所以32位长度的十六进制数,MD5也被称之为128位算法。理论上,会存在2的128次方品种的摘要成果,且对应2的128次方种不同的音讯,假如在未超过2的128次方种音讯中,存在2个或多个不同的音讯对应了相同的摘要,则称之为:发生了磕碰。一个音讯摘要算法是否安全,取决其实践的磕碰概率,关于音讯摘要算法的破解,也是研究其磕碰概率。

存在穷举音讯和摘要的对应联系,并运用摘要在此对应联系进行查询,从而得知音讯的做法,可是,因为MD5是128位算法,全部穷举是不可能完成的,所以,只需原始暗码(音讯)满足杂乱,就不会被收录到所记录的对应联系中去!

为了进一步进步暗码的安全性,在运用音讯摘要算法进行处理时,一般还会加盐!盐值能够是恣意的字符串,用于与暗码一同作为被音讯摘要算法运算的数据即可,例如:

@Test
public void md5Test() {
    String rawPassword = "123456";
    String salt = "kjfcsddkjfdsajfdiusf8743urf";
    String encodedPassword = DigestUtils.md5DigestAsHex(
            (salt + salt + rawPassword + salt + salt).getBytes());
    System.out.println("原暗码:" + rawPassword);
    System.out.println("加密后的暗码:" + encodedPassword);
}

加盐的目的是使得被运算数据变得愈加杂乱,盐值自身和用法并没有明确要求!

乃至,在某些用法或算法中,还会运用随机的盐值,则能够运用完全相同的原音讯对应的摘要却不同!

引荐了解:预核算的哈希链、彩虹表、雪花算法。

为了进一步保证暗码安全,还能够运用多重加密,即反复调用音讯摘要算法。

除此以外,还能够运用安全系数更高的算法,例如SHA-256是256位算法,SHA-384是384位算法,SHA-512是512位算法。

一般的运用办法能够是:

public class PasswordEncoder {
    public String encode(String rawPassword) {
        // 加密进程
        // 1. 运用MD5算法
        // 2. 运用随机的盐值
        // 3. 循环5次
        // 4. 盐的处理办法为:盐 + 原暗码 + 盐 + 原暗码 + 盐
        // 留意:因为运用了随机盐,盐值有必要被记录下来,本次的回来成果运用$分隔盐与密文
        String salt = UUID.randomUUID().toString().replace("-", "");
        String encodedPassword = rawPassword;
        for (int i = 0; i < 5; i++) {
            encodedPassword = DigestUtils.md5DigestAsHex(
                    (salt + encodedPassword + salt + encodedPassword + salt).getBytes());
        }
        return salt + encodedPassword;
    }
    public boolean matches(String rawPassword, String encodedPassword) {
        String salt = encodedPassword.substring(0, 32);
        String newPassword = rawPassword;
            for (int i = 0; i < 5; i++) {
                newPassword = DigestUtils.md5DigestAsHex(
                        (salt + newPassword + salt + newPassword + salt).getBytes());
        }
        newPassword = salt + newPassword;
        return newPassword.equals(encodedPassword);
    }
}

13. 控制器层开发

Spring MVC是用于处理控制器层开发的,在运用Spring Boot时,在pom.xml中增加spring-boot-starter-web即可整合Spring MVC结构及相关的常用依靠项(包括jackson-databind),能够将已存在的spring-boot-starter直接改为spring-boot-starter-web,因为在spring-boot-starter-web中现已包括了spring-boot-starter

先在项目的根包下创立controller子包,并在此子包下创立AdminController,此类应该增加@RestController@RequestMapping(value = "/admins", produces = "application/json; charset=utf-8")注解,例如:

@RestController
@RequestMapping(values = "/admins", produces = "application/json; charset=utf-8")
public class AdminController {
}

因为现已决议了服务器端呼应时,将呼应JSON格局的字符串,为保证能够呼应JSON格局的成果,处理恳求的办法回来值应该是自界说的数据类型,则从此前学习的spring-mvc项目中找到JsonResult类及相关类型,复制到当时项目中来。

接下来,即可在AdminController中增加处理“增加办理员”的恳求:

@Autowired
private IAdminService adminService;
// 留意:暂时运用@RequestMapping,不要运用@PostMapping,以便于直接在浏览器中测验
// http://localhost:8080/admins/add-new?username=root&password=1234
@RequestMapping("/add-new") 
public JsonResult<Void> addNew(AdminAddNewDTO adminAddNewDTO) {
    adminService.addNew(adminAddNewDTO);
    return JsonResult.ok();
}

完成后,运转发动类,即可发动整个项目,在spring-boot-starter-web中,包括了Tomcat的依靠项,在发动时,会主动将当时项目打包并布置到此Tomcat上,所以,履行发动类时,会履行此Tomcat,一同,因为是内置的Tomcat,只为当时项目服务,所以,在将项目布置到Tomcat时,默许现已将Context Path(例如spring_mvc_war_exploded)装备为空字符串,所以,在发动项目后,拜访的URL中并没有此前遇到的Context Path值。

当项目发动成功后,即可在浏览器的地址栏中输入网址进行测验拜访!

留意:假如是未增加的办理员账号,能够成功履行完毕,假如办理员账号现已存在,因为尚未处理反常,会提示500过错。

关于处理反常,应该先在State中保证有每种反常对应的枚举值,例如本次需求弥补InsertException对应的枚举值:

public enum State {
    OK(200),
    ERR_USERNAME(201),
    ERR_PASSWORD(202),
    ERR_INSERT(500); // 新增的枚举值
    // 原有其它代码
}

然后,在cn.tedu.boot.demo.controller下创立handler.GlobalExceptionHandler类,用于一致处理反常,例如:

package cn.tedu.boot.demo.controller.handler;
import cn.tedu.boot.demo.ex.ServiceException;
import cn.tedu.boot.demo.ex.UsernameDuplicateException;
import cn.tedu.boot.demo.web.JsonResult;
import cn.tedu.boot.demo.web.State;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(ServiceException.class)
    public JsonResult<Void> handleServiceException(ServiceException e) {
        if (e instanceof UsernameDuplicateException) {
            return JsonResult.fail(State.ERR_USERNAME, "用户名过错!");
        } else {
            return JsonResult.fail(State.ERR_INSERT, "刺进数据失利!");
        }
    }
}

完成后,从头发动项目,当增加办理员时的用户名没有被占用时,将正常增加,当用户名现已被占用时,会依据处理反常的成果进行呼应!

因为在一致处理反常的机制下,同一种反常,无论是在哪种事务中呈现,处理反常时的描绘信息都是完全相同的,也无法精准的表达过错信息,这是不合适的!别的,依据面向方针的“分工”思想,关于过错信息(反常对应的描绘信息),应该是由Service来描绘,即“谁抛出谁描绘”,因为抛出反常的代码片段是最了解、最明确呈现反常的原因的!

为了更好的描绘反常的原因,应该在自界说的ServiceException和这今后代类反常中增加依据父类的全部结构办法(5个),然后,在AdminServiceImpl中,当抛出反常时,能够在反常的结构办法中增加String类型的参数,对反常发生的原因进行描绘,例如:

@Override
public void addNew(AdminAddNewDTO adminAddNewDTO) {
    // ===== 原有其它代码 =====
    // 判别查询成果是否不为null
    if (queryResult != null) {
        // 是:表明用户名现已被占用,则抛出UsernameDuplicateException
        log.error("此账号现已被占用,将抛出反常");
        throw new UsernameDuplicateException("增加办理员失利,用户名(" + username + ")现已被占用!");
    }
    // ===== 原有其它代码 =====
    // 判别以上回来的成果是否不为1,抛出InsertException反常
    if (rows != 1) {
        throw new InsertException("增加办理员失利,服务器忙,请稍后再次测验!");
    }
}

最终,在处理反常时,能够调用反常方针的getMessage()办法获取抛出时封装的描绘信息,例如:

@ExceptionHandler(ServiceException.class)
public JsonResult<Void> handleServiceException(ServiceException e) {
    if (e instanceof UsernameDuplicateException) {
        return JsonResult.fail(State.ERR_USERNAME, e.getMessage());
    } else {
        return JsonResult.fail(State.ERR_INSERT, e.getMessage());
    }
}

完成后,再次重启项目,当用户名现已存在时,能够显现在Service中描绘的过错信息!

最终,当增加成功时,呼应的JSON数据例如:

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

当用户名抵触,增加失利时,呼应的JSON数据例如:

{
    "state":201,
    "message":"增加办理员失利,用户名(liuguobin)现已被占用!",
    "data":null
}

能够看到,无论是成功仍是失利,呼应的JSON中都包括了不用要的数据(为null的数据),这些数据特点是没有必要呼应到客户端的,假如需求去除这些不用要的值,能够在对应的特点上运用注解进行装备,例如:

@Data
public class JsonResult<T> implements Serializable {
    // 状况码,例如:200
    private Integer state;
    // 音讯,例如:"登录失利,用户名不存在"
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String message;
    // 数据
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private T data;
    // ===== 原有其它代码 =====
}

则呼应的JSON中只会包括不为null的部分。

此注解还能够增加在类上,则作用于当时类中一切的特点,例如:

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class JsonResult<T> implements Serializable {
    // ===== 原有其它代码 =====
}

即便增加在类上,也只对当时类的3个特点有用,后续,当呼应某些数据时,data特点可能是用户、产品、订单等类型,这些类型的数据中为null的部分仍然会被呼应到客户端去,所以,还需求对这些类型也增加相同的注解装备!

以上做法相对比较繁琐,能够在application.properties / application.yml中增加大局装备,则作用于当时项目中一切呼应时触及的类,例如在properties中装备为:

spring.jackson.default-property-inclusion=non_null

yml中装备为:

spring:
  jackson:
    default-property-inclusion: non_null

留意:当你需求在yml中增加以上装备时,前缀特点名可能现已存在,则不答应呈现重复的前缀特点名,例如以下装备便是过错的:

spring:
  profiles:
    active: dev
spring: # 此处就呈现了相同的前缀特点名,是过错的
  jackson:
    default-property-inclusion: non_null

正确的装备例如:

spring:
  profiles:
    active: dev
  jackson:
    default-property-inclusion: non_null

最终,以上装备仅仅“默许”装备,假如在某些类型中还有不同的装备需求,仍能够在类或特点上经过@JsonInclude进行装备。

14. Validation结构

当客户端向服务器提交恳求时,假如恳求数据呈现显着的问题(例如要害数据为null、字符串的长度不在可接受范围内、其它格局过错),应该直接呼应过错,而不是将显着过错的恳求参数传递到Service!

关于判别过错,只要触及数据库中的数据才能判别出成果的,都由Service进行判别,而根本的格局判别,都由Controller进行判别。

Validation结构是专门用于处理查看数据根本格局有用性的,最早并不是Spring系列的结构,现在,Spring Boot提供了更好的支撑,所以,一般结合在一同运用。

在Spring Boot项目中,需求增加spring-boot-starter-validation依靠项,例如:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

在控制器中,首要,对需求查看数据格局的恳求参数增加@Valid@Validated注解(这2个注解没有差异),例如:

@RequestMapping("/add-new")
public JsonResult<Void> addNew(@Validated AdminAddNewDTO adminAddNewDTO) {
    adminService.addNew(adminAddNewDTO);
    return JsonResult.ok();
}

真正需求查看的是AdminAddNewDTO中各特点的值,所以,接下来需求在此类的各特点上经过注解来装备查看的规矩,例如:

@Data
public class AdminAddNewDTO implements Serializable {
    @NotNull // 验证规矩为:不答应为null
    private String username;
    // ===== 原有其它代码 =====
}

重启项目,经过不提交用户名的URL(例如:http://localhost:8080/admins/add-new)进行拜访,在浏览器上会呈现400过错页面,而且,在IntelliJ IDEA的控制台会呈现以下正告:

2022-06-07 11:37:53.424  WARN 6404 --- [nio-8080-exec-8] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [
org.springframework.validation.BindException: 
org.springframework.validation.BeanPropertyBindingResult: 1 errors<EOL>Field error in object 'adminAddNewDTO' on field 'username': rejected value [null]; codes [NotNull.adminAddNewDTO.username,NotNull.username,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [adminAddNewDTO.username,username]; arguments []; default message [username]]; default message [不能为null]]

从正告信息中能够看到,当验证失利时(不符合所运用的注解对应的规矩时),会呈现org.springframework.validation.BindException反常,则自行处理此反常即可!

假如有多个特点需求验证,则多个特点都需求增加注解,例如:

@Data
public class AdminAddNewDTO implements Serializable {
    @NotNull
    private String username;
    @NotNull
    private String password;
    // ===== 原有其它代码 =====
}

首要,在State中增加新的枚举:

public enum State {
    OK(200),
    ERR_USERNAME(201),
    ERR_PASSWORD(202),
    ERR_BAD_REQUEST(400), // 新增
    ERR_INSERT(500);
    // ===== 原有其它代码 =====
}

然后,在GlobalExceptionHandler中增加新的处理反常的办法:

@ExceptionHandler(BindException.class)
public JsonResult<Void> handleBindException(BindException e) {
    return JsonResult.fail(State.ERR_BAD_REQUEST, e.getMessage());
}

完成后,再次重启项目,继续运用为null的用户名提交恳求时,能够看到反常现已被处理,此时,呼应的JSON数据例如:

{
    "state":400,
    "message":"org.springframework.validation.BeanPropertyBindingResult: 2 errors\nField error in object 'adminAddNewDTO' on field 'username': rejected value [null]; codes [NotNull.adminAddNewDTO.username,NotNull.username,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [adminAddNewDTO.username,username]; arguments []; default message [username]]; default message [不能为null]\nField error in object 'adminAddNewDTO' on field 'password': rejected value [null]; codes [NotNull.adminAddNewDTO.password,NotNull.password,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [adminAddNewDTO.password,password]; arguments []; default message [password]]; default message [不能为null]"
}

关于过错提示信息,以上内容中呈现了不能为null的字样,是默许的提示文本,能够经过@NotNull注解的message特点进行装备,例如:

@Data
public class AdminAddNewDTO implements Serializable {
    @NotNull(message = "增加办理员失利,请提交用户名!")
    private String username;
    @NotNull(message = "增加办理员失利,请提交暗码!")
    private String password;
    // ===== 原有其它代码 =====
}

然后,在处理反常时,经过反常信息获取自界说的提示文本:

@ExceptionHandler(BindException.class)
public JsonResult<Void> handleBindException(BindException e) {
    BindingResult bindingResult = e.getBindingResult();
    String defaultMessage = bindingResult.getFieldError().getDefaultMessage();
    return JsonResult.fail(State.ERR_BAD_REQUEST, defaultMessage);
}

再次运转,在不提交用户名和暗码的情况下,会随机的提示用户名或暗码验证失利的提示文本中的某1条。

在Validation结构中,还有其它许多注解,用于进行不同格局的验证,例如:

  • @NotEmpty:只能增加在String类型上,不许为空字符串,例如""即视为空字符串
  • @NotBlank:只能增加在String类型上,不答应为空白,例如一般的空格可视为空白,运用TAB键输入的内容也是空白,(尽管不太可能在此处呈现)换行发生的空白区域也是空白
  • @Size:约束巨细
  • @Min:约束最小值
  • @Max:约束最大值
  • @Range:能够装备minmax特点,一同约束最小值和最大值
  • @Pattern:只能增加在String类型上,自行指定正则表达式进行验证
  • 其它

以上注解,包括@NotNull是答应叠加运用的,即答应在同一个参数特点上增加多个注解!

以上注解均能够装备message特点,用于指定验证失利的提示文本。

一般:

  • 关于有必要提交的特点,都会增加@NotNull
  • 关于数值类型的,需求考虑是否增加@Range(则不需求运用@Min@Max
  • 关于字符串类型,都增加@Pattern注解进行验证

15. 处理跨域问题

在运用前后端别离的开发办法下,前端项目和后端项目可能是2个完全不同的项目,而且,各自己独立开发,独立布置,在这种做法中,假如前端直接向后端发送异步恳求,默许情况下,在前端会呈现相似以下过错:

Access to XMLHttpRequest at 'http://localhost:8080/admins/add-new' from origin 'http://localhost:8081' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

以上过错信息的要害字是CORS,一般称之为“跨域问题”。

在依据Spring MVC结构的项目中,当需求处理跨域问题时,需求一个Spring MVC的装备类(完成了WebMvcConfigurer接口的类),并重写其间的办法,以答应指定条件的跨域拜访,例如:

package cn.tedu.boot.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("*")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

16. 关于客户端提交恳求参数的格局

一般,客户端向服务器端发送恳求时,恳求参数能够有2种办法,第1种是直接经过&拼接各参数与值,例如:

// FormData
// username=root&password=123456&nickname=jackson&phone=13800138001&email=jackson@baidu.com&description=none
let data = 'username=' + this.ruleForm.username
              + '&password=' + this.ruleForm.password
              + '&nickname=' + this.ruleForm.nickname
              + '&phone=' + this.ruleForm.phone
              + '&email=' + this.ruleForm.email
              + '&description=' + this.ruleForm.description;

第2种办法是运用JSON语法来安排各参数与值,例如:

let data = {
    'username': this.ruleForm.username, // 'root'
    'password': this.ruleForm.password, // '123456'
    'nickname': this.ruleForm.nickname, // 'jackson'
    'phone': this.ruleForm.phone, // '13800138001'
    'email': this.ruleForm.email, // 'jackson@baidu.com'
    'description': this.ruleForm.description // 'none'
};

详细运用哪种做法,取决于服务器端的规划:

  • 假如服务器端处理恳求的办法中,在参数前增加了@RequestBody,则答应运用以上第2种做法(JSON数据)提交恳求参数,不答应运用以上第1种做法(运用&拼接)
  • 假如没有运用@RequestBody,则只能运用以上第1种做法

17. 处理登录

17.1. 开发流程

正常的项目开发流程大致是:

  • 先整理出当时项目触及的数据的类型
    • 例如:电商类包括用户、产品、购物车、订单等
  • 再列举各种数据类型触及的数据操作
    • 例如:用户类型触及注册、登录等
  • 再选择相对简略的数据类型先处理
    • 简略的易于完成,且能够堆集经历
  • 在各数据类型触及的数据操作中,大致遵循增、查、删、改的开发顺序
    • 只要先增,还可能查、删、改
    • 只要查了今后,才能明确有哪些数据,才便于完成删、改
    • 删和改相比,删一般愈加简略,所以先开发删,再开发改
  • 在开发详细的数据操作时,应该大致遵循耐久层 >> 事务逻辑层 >> 控制器层 >> 前端页面的开发顺序

17.2. 办理员登录-耐久层

17.2.1. 创立或装备

假如是整个项目第1次开发耐久层,在Spring Boot项目中,需求装备:

  • 运用@MapperScan装备接口地点的根包
  • 在装备文件中经过mybatis.mapper-locations装备XML文件的方位

假如第1次处理某品种型数据的耐久层拜访,需求:

  • 创立接口
  • 创立XML文件

本次需求开发的“办理员登录”并不需求再做以上操作

17.2.2. 规划需求履行的SQL句子

需求履行的SQL句子大致是:

select * from ams_admin where username=?

因为在ams_admin表中有很多字段,一同,不答应运用星号表明字段列表,则以上SQL句子应该细化为:

select id, username, password, nickname, avatar, is_enable from ams_admin where username=?

提示:理论上,还应该查出login_count,当登录成功后,还应该更新login_countgmt_last_login等数据,此次暂不考虑。

17.2.3. 在接口中增加笼统办法(含创立必要的VO类)

提示:一切的查询成果,都应该运用VO类,而不要运用实体类,依据阿里的开发标准,每张数据表中都应该有idgmt_creategmt_modified这3个字段,而gmt_creategmt_modified这2个字段都是用于特殊情况下排查问题的,一般情况下均不会运用,所以,假如运用实体类,必定存在剩余的特点,一同,因为不运用星号作为字段列表,则一般也不会查询这2个字段的值,会导致实体类方针中永久至少存在2个特点为null

依据以上提示,以前现已写好的getByUsername()是不标准的,应该调整已存在此办法,本次并不需求增加新的笼统办法。

则先创立cn.tedu.boot.demo.pojo.vo.AdminSimpleVO类,增加此次查询时需求的特点:

package cn.tedu.boot.demo.pojo.vo;
@Data
public class AdminSimpleVO implements Serializable {
    private Long id;
    private String username;
    private String password; 
    private String nickname; 
    private String avatar;
    private Integer isEnable;
}

然后,在AdminMapper接口文件中,将原有的Admin getByUsername(String username);改为:

AdminSimpleVO getByUsername(String username);

留意:一旦修正了原有代码,则调用了原办法的代码都会呈现过错,包括:

  • 测验
  • 事务逻辑层的完成类

应该及时修正过错的代码,可是,因为此时还未完成SQL装备,所以,相关代码暂时并不能运转。

17.2.4. 在XML中装备SQL

AdminMapper.xml中,需求调整:

  • 删去<sql>中不用查询的字段,留意:此处的字段列表最终不要有剩余的逗号
  • 修正<resultMap>节点的type特点值
  • <resultMap>节点下,删去不用要的装备
<select id="getByUsername" resultMap="BaseResultMap">
    select
        <include refid="BaseQueryFields" />
    from
         ams_admin
    where
         username=#{username}
</select>
<sql id="BaseQueryFields">
    <if test="true">
        id,
        username,
        password,
        nickname,
        avatar,
        is_enable
    </if>
</sql>
<resultMap id="BaseResultMap" type="cn.tedu.boot.demo.pojo.vo.AdminSimpleVO">
    <id column="id" property="id" />
    <result column="username" property="username" />
    <result column="password" property="password" />
    <result column="nickname" property="nickname" />
    <result column="avatar" property="avatar" />
    <result column="is_enable" property="isEnable" />
</resultMap>

17.2.5. 编写并履行测验

此次并不需求编写新的测验,运用原有的测验即可!

留意:因为本次是修正了原“增加办理员”就现已运用的功用,应该查看原功用是否能够正常运转。

17.3. 办理员登录-事务逻辑层

17.3.1. 创立

假如第1次处理某品种型数据的事务逻辑层拜访,需求:

  • 创立接口
  • 创立类,完成接口,并在类上增加@Service注解

本次需求开发的“办理员登录”并不需求再做以上操作

17.3.2. 在接口中增加笼统办法(含创立必要的DTO类)

在规划笼统办法时,假如参数的数量超过1个,且多个参数具有相关性(是否都是客户端提交的,或是否都是控制器传递过来的等),就应该封装!

在处理登录时,需求客户端提交用户名和暗码,则能够将用户名、暗码封装起来:

package cn.tedu.boot.demo.pojo.dto;
@Data
public class AdminLoginDTO implements Serializable {
    private String username;
    private String password;
}

IAdminService中增加笼统办法:

AdminSimpleVO login(AdminLoginDTO adminLoginDTO);

17.3.3. 在完成类中规划(打草稿)事务流程与事务逻辑(含创立必要的反常类)

此次事务履行进程中,可能会呈现:

  • 用户名不存在,导致无法登录
  • 用户状况为【禁用】,导致无法登录
  • 暗码过错,导致无法登录

关于用户名不存在的问题,能够自行创立新的反常类,例如,在cn.tedu.boot.demo.ex包下创立UserNotFoundException类表明用户数据不存在的反常,继承自ServiceException,且增加5款依据父类的结构办法:

package cn.tedu.boot.demo.ex;
public class UserNotFoundException extends ServiceException {
    // 主动生成5个结构办法
}

再创立UserStateException表明用户状况反常:

package cn.tedu.boot.demo.ex;
public class UserStateException extends ServiceException {
    // 主动生成5个结构办法
}

再创立PasswordNotMatchException表明暗码过错反常:

package cn.tedu.boot.demo.ex;
public class PasswordNotMatchException extends ServiceException {
    // 主动生成5个结构办法
}

登录进程大致是:

public AdminSimpleVO login(AdminLoginDTO adminLoginDTO) {
    // 经过参数得到测验登录的用户名
    // 调用adminMapper.getByUsername()办法查询
    // 判别查询成果是否为null
    // 是:表明用户名不存在,则抛出UserNotFoundException反常
    // 【假如程序能够履行到此步,则能够确定未抛出反常,即查询成果不为null】
    // 【以下可视为:存在与用户名匹配的办理员数据】
    // 判别查询成果中的isEnable特点值是否不为1
    // 是:表明此用户状况是【禁用】的,则抛出UserStateException反常
    // 【假如程序能够履行到此步,表明此用户状况是【启用】的】
    // 从参数中取出此次登录时客户端提交的暗码
    // 调用PasswordEncoder方针的matches()办法,对客户端提交的暗码和查询成果中的暗码进行验证
    // 判别以上验证成果
    // true:暗码正确,视为登录成功
    // -- 将查询成果中的password、isEnable设置为null,防止呼应到客户端
    // -- 回来查询成果
    // false:暗码过错,视为登录失利,则抛出PasswordNotMatchException反常
}

17.3.4. 在完成类中完成事务

AdminServiceImpl中重写接口中新增的笼统办法:

@Override
public AdminSimpleVO login(AdminLoginDTO adminLoginDTO) {
    // 日志
    log.debug("行将处理办理员登录的事务,测验登录的办理员信息:{}", adminLoginDTO);
    // 经过参数得到测验登录的用户名
    String username = adminLoginDTO.getUsername();
    // 调用adminMapper.getByUsername()办法查询
    AdminSimpleVO queryResult = adminMapper.getByUsername(username);
    // 判别查询成果是否为null
    if (queryResult == null) {
        // 是:表明用户名不存在,则抛出UserNotFoundException反常
        log.warn("登录失利,用户名不存在!");
        throw new UserNotFoundException("登录失利,用户名不存在!");
    }
    // 【假如程序能够履行到此步,则能够确定未抛出反常,即查询成果不为null】
    // 【以下可视为:存在与用户名匹配的办理员数据】
    // 判别查询成果中的isEnable特点值是否不为1
    if (queryResult.getIsEnable() != 1) {
        // 是:表明此用户状况是【禁用】的,则抛出UserStateException反常
        log.warn("登录失利,此账号现已被禁用!");
        throw new UserNotFoundException("登录失利,此账号现已被禁用!");
    }
    // 【假如程序能够履行到此步,表明此用户状况是【启用】的】
    // 从参数中取出此次登录时客户端提交的暗码
    String rawPassword = adminLoginDTO.getPassword();
    // 调用PasswordEncoder方针的matches()办法,对客户端提交的暗码和查询成果中的暗码进行验证
    boolean matchResult = passwordEncoder.matches(rawPassword, queryResult.getPassword());
    // 判别以上验证成果
    if (!matchResult) {
        // false:暗码过错,视为登录失利,则抛出PasswordNotMatchException反常
        log.warn("登录失利,暗码过错!");
        throw new PasswordNotMatchException("登录失利,暗码过错!");
    }
    // 暗码正确,视为登录成功
    // 将查询成果中的password、isEnable设置为null,防止呼应到客户端
    queryResult.setPassword(null);
    queryResult.setIsEnable(null);
    // 回来查询成果
    log.debug("登录成功,行将回来:{}", queryResult);
    return queryResult;
}

17.3.5. 编写并履行测验

AdminServiceTests中增加测验:

@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
@Test
public void testLoginSuccessfully() {
    // 测验数据
    String username = "admin001";
    String password = "123456";
    AdminLoginDTO adminLoginDTO = new AdminLoginDTO();
    adminLoginDTO.setUsername(username);
    adminLoginDTO.setPassword(password);
    // 断语不会抛出反常
    assertDoesNotThrow(() -> {
        // 履行测验
        AdminSimpleVO adminSimpleVO = service.login(adminLoginDTO);
        log.debug("登录成功:{}", adminSimpleVO);
        // 断语测验成果
        assertEquals(1L, adminSimpleVO.getId());
        assertNull(adminSimpleVO.getPassword());
        assertNull(adminSimpleVO.getIsEnable());
    });
}
@Sql({"classpath:truncate.sql"})
@Test
public void testLoginFailBecauseUserNotFound() {
    // 测验数据
    String username = "admin001";
    String password = "123456";
    AdminLoginDTO adminLoginDTO = new AdminLoginDTO();
    adminLoginDTO.setUsername(username);
    adminLoginDTO.setPassword(password);
    // 断语会抛出UserNotFoundException
    assertThrows(UserNotFoundException.class, () -> {
        // 履行测验
        service.login(adminLoginDTO);
    });
}
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
@Test
public void testLoginFailBecauseUserDisabled() {
    // 测验数据
    String username = "admin005"; // 经过SQL脚本刺进的此数据,is_enable为0
    String password = "123456";
    AdminLoginDTO adminLoginDTO = new AdminLoginDTO();
    adminLoginDTO.setUsername(username);
    adminLoginDTO.setPassword(password);
    // 断语会抛出UserStateException
    assertThrows(UserStateException.class, () -> {
        // 履行测验
        service.login(adminLoginDTO);
    });
}
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
@Test
public void testLoginFailBecausePasswordNotMatch() {
    // 测验数据
    String username = "admin001";
    String password = "000000000000000000";
    AdminLoginDTO adminLoginDTO = new AdminLoginDTO();
    adminLoginDTO.setUsername(username);
    adminLoginDTO.setPassword(password);
    // 断语会抛出PasswordNotMatchException
    assertThrows(PasswordNotMatchException.class, () -> {
        // 履行测验
        service.login(adminLoginDTO);
    });
}

17.4. 办理员登录-控制器层

17.4.1. 创立

假如是整个项目第1次开发控制器层,需求:

  • 创立一致处理反常的类
    • 增加@RestControllerAdvice
  • 创立一致的呼应成果类型及相关类型
    • 例如:JsonResultState

假如第1次处理某品种型数据的控制器层拜访,需求:

  • 创立控制器类
    • 增加@RestController
    • 增加@RequestMapping

本次需求开发的“办理员登录”并不需求再做以上操作

17.4.2. 增加处理恳求的办法,验证恳求参数的根本有用性

AdminLoginDTO的各特点上增加验证根本有用性的注解,例如:

package cn.tedu.boot.demo.pojo.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
@Data
public class AdminLoginDTO implements Serializable {
    @NotNull(message = "登录失利,请提交用户名!") // 新增
    private String username;
    @NotNull(message = "登录失利,请提交暗码!") // 新增
    private String password;
}

AdminController中增加处理恳求的办法:

@RequestMapping("/login") // 暂时运用@RequestMapping,后续改成@PostMapping
public JsonResult<AdminSimpleVO> login(@Validated AdminLoginDTO adminLoginDTO) {
    AdminSimpleVO adminSimpleVO = adminService.login(adminLoginDTO);
    return JsonResult.ok(adminSimpleVO);
}

17.4.3. 处理反常(按需)

先在State中增加新创立的反常对应枚举:

public enum State {
    OK(200),
    ERR_USERNAME(201),
    ERR_PASSWORD(202),
    ERR_STATE(203), // 新增
    ERR_BAD_REQUEST(400),
    ERR_INSERT(500);
    // ===== 原有其它代码 =====
}

GlobalExceptionHandlerhandleServiceException()办法中增加更多分支,针对各反常进行判别,并呼应不同成果:

@ExceptionHandler(ServiceException.class)
public JsonResult<Void> handleServiceException(ServiceException e) {
    if (e instanceof UsernameDuplicateException) {
        return JsonResult.fail(State.ERR_USERNAME, e.getMessage());
    } else if (e instanceof UserNotFoundException) {				// 从此行起,是新增的
        return JsonResult.fail(State.ERR_USERNAME, e.getMessage());
    } else if (e instanceof UserStateException) {
        return JsonResult.fail(State.ERR_STATE, e.getMessage());
    } else if (e instanceof PasswordNotMatchException) {
        return JsonResult.fail(State.ERR_PASSWORD, e.getMessage());	// 新增完毕标记
    } else {
        return JsonResult.fail(State.ERR_INSERT, e.getMessage());
    }
}

17.4.4. 测验

发动项目,暂时经过 http://localhost:8080/admins/login?username=admin001&password=123456 相似的URL测验拜访。留意:在测验拜访之前,有必要保证数据表中的数据状况是符合预期的。

17.5. 办理员登录-前端页面

18. 控制器层的测验

关于控制器层,也能够写测验办法进行测验,在Spring Boot项目中,能够运用MockMvc进行模仿测验,例如:

package cn.tedu.boot.demo.controller;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
@SpringBootTest
@AutoConfigureMockMvc // 主动装备MockMvc
public class AdminControllerTests {
    @Autowired
    MockMvc mockMvc; // Mock:模仿
    @Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
    @Test
    public void testLoginSuccessfully() throws Exception {
        // 预备测验数据,不需求封装
        String username = "admin001";
        String password = "123456";
        // 恳求途径,不需求写协议、服务器主机和端口号
        String url = "/admins/login";
        // 履行测验
        // 以下代码相对比较固定
        mockMvc.perform( // 履行发出恳求
                MockMvcRequestBuilders.post(url) // 依据恳求办法决议调用的办法
                .contentType(MediaType.APPLICATION_FORM_URLENCODED) // 恳求数据的文档类型,例如:application/json; charset=utf-8
                .param("username", username) // 恳求参数,有多个时,多次调用param()办法
                .param("password", password)
                .accept(MediaType.APPLICATION_JSON)) // 接纳的呼应成果的文档类型,留意:perform()办法到此完毕
                .andExpect( // 预判成果,相似断语
                        MockMvcResultMatchers
                                .jsonPath("state") // 预判呼应的JSON成果中将有名为state的特点
                                .value(200)) // 预判呼应的JSON成果中名为state的特点的值,留意:andExpect()办法到此完毕
                .andDo( // 需求履行某使命
                        MockMvcResultHandlers.print()); // 打印日志
    }
}

履行以上测验时,并不需求发动当时项目即可测验。

在履行以上测验时,呼应的JSON中假如包括中文,可能会呈现乱码,需求在装备文件(application.propertiesapplication.yml这类文件)中增加装备。

.properties文件中:

server.servlet.encoding.force=true
server.servlet.encoding.charset=utf-8

.yml文件中:

server:
  servlet:
    encoding:
      force: true
      charset: utf-8