栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

第四阶段:05SpringBoot

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

第四阶段:05SpringBoot

5、SpringBoot 01.关于SpringBoot:重要!
Spring Boot是Spring官方的一个产品,
其本质上是一个基于Maven的、以Spring框架作为基础的进阶框架,
很好的支持了主流的其它框架,
并默认完成了许多的配置,
其核心思想是“约定大于配置”。
02.创建SpringBoot工程:
在IntelliJ IDEA中:
在创建向导中选择Spring Initializer即可开始创建Spring Boot工程:
在创建向导的界面中,需要关注的部分有:
- Group Id
- Artifact Id
以上2个值会共同构成一个Package name,
如果Artifact Id的名字中有减号,在Package name中会去除,推荐手动添加小数点进行分隔。

由于Spring Boot官方更新版本的频率非常高,
在创建项目时,
随便选取某个版本均可,
当项目创建成功后,
推荐打开pom.xml,将中的(即Spring Boot父项目的版本)
  改成熟悉的版本,例如:2.5.9

在创建过程中:
还可以在创建向导的界面中勾选所需要依赖项,
如果创建时没有勾选,也可以在创建工程之后手动在pom.xml中添加。

2.此处不勾选也可以,可以后面创建一个新的springboot项目,将相关的两个依赖复制到此工程的pom.xml中,
需要手动调节各依赖版本号相差不大!

3.打开 工程的pom.xml文件:将 parent节点中的版本由2.7.0改为2.5.9并刷新maven
注意:若点击刷新maven按钮后,下面多出【】节点报错,删掉即可!因为这个项目中我们用不到

03.Spring Boot工程的结构
(1)由于Spring Boot工程本质上就是一个Maven工程,所以,目录结构基本上没有区别。
—————————————————————————————————————————————————————————————
(2)
与普通Maven工程最大的不同在于:
Spring Boot工程在srcmainjava和srctestjava下默认已经存在Package,
是创建项目时指定的Package,
————>需要注意:此Package已经被配置为Spring执行组件扫描的根包,
			  所以,在编写代码时,所有的组件类都必须放在此包或其子孙包中!
			  通常,推荐将所有的类(及接口)都创建在此包及其子孙包下。
—————————————————————————————————————————————————————————————
(3)在srcmainjava下的根包下,
默认就已经存在某个类,
其类名是创建项目时指定的Artifact与Application单词的组合,
例如BootDemoApplication,
此类中有main()方法,执行此类的main()就会启动整个项目,
如果当前项目是Web项目,还会自动将项目部署到Web服务器并启动服务器,
所以,此类通常也称之为“启动类”。
—————————————————————————————————————————————————————————————
(4)启动类本身也是配置类:
在启动类上,
默认添加了@SpringBootApplication注解,此注解的元注解中包含@SpringBootConfiguration,
而@SpringBootConfiguration的元注解中包含@Configuration,
所以,启动类本身也是配置类!
所以,允许将@Bean方法写在此类中,或者某些与配置相关的注解也可以添加在此类上!
—————————————————————————————————————————————————————————————
(5)在srctestjava下的根包下,
默认就已经存在某个类,其类名是在启动类的名称基础上添加了Tests单词的组合,
例如BootDemoApplicationTests,
此类默认没有添加public权限,甚至其内部的默认的测试方法也是默认权限的,
此测试类上添加了@SpringBootTest注解,其元注解中包含@ExtendWith(SpringExtension.class),
与使用spring-test时的@SpringJUnitTest注解中的元注解相同,
所以,
@SpringBootTest注解也会使得当前测试类在执行测试方法之前是加载了Spring环境的,
在实际编写测试时,
可以通过自动装配得到任何已存在于Spring容器中的对象,
在各测试方法中只需要关注被测试的目标即可。
—————————————————————————————————————————————————————————————
在pom.xml中,
默认已经添加了spring-boot-starter和spring-boot-starter-test依赖,
分别是Spring Boot的基础依赖和基于Spring Boot的测试的依赖。
—————————————————————————————————————————————————————————————
另外,如果在创建工程时,勾选依赖项时选中了Web项,
在srcmainresources下默认就已经创建了static和templates文件夹,
如果没有勾选Web则没有这2个文件夹,可以后续自行补充创建。
—————————————————————————————————————————————————————————————
在srcmainresources文件夹下,
默认就已经存在application.properties文件,
用于编写配置,Spring Boot会自动读取此文件(利用@PropertySource注解)。
—————————————————————————————————————————————————————————————
————>小结:<————
(1)创建项目后默认的Package不要修改,避免出错
(2)在编码过程中,自行创建的所有类、接口均放在默认的Package或其子孙包中
(3)在srcmainjava下默认已存在XxxApplication是启动类,
   执行此类中的main()方法就会启动整个项目
(4)启动类本身也是配置类
(5)配置都应该编写到srcmainresources下的application.properties中,
   Spring Boot会自动读取
(6)测试类也必须放在srctestjava下的默认Package或其子孙包中
(7)在测试类上添加@SpringBootTest注解,
   则其中的测试方法执行之前会自动加载Spring环境及当前项目的配置,
   可以在测试类中使用自动装配
04.添加依赖:2个M的依赖
(1)创建工程时 spring中 打2个钩
   或者
   需要自行添加 2个 相关依赖项:
  【mysql-connector-java 、 mybatis-spring-boot-starter】

        
        
            mysql
            mysql-connector-java
            runtime
        
        
        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            2.2.2
        

(2)说明:
   在Spring Boot工程,许多依赖项都是不需要显式的指定版本号的,
   因为在父项目中已经对这些依赖项的版本进行了管理(配置版本号),
   如果一定需要使用特定的版本,也可以自行添加``节点进行配置

(3)说明:
   在依赖项的源代码中,当``的值为`runtime`时,
   表示此依赖项是运行过程中需要的,但是,在编译时并不需要参与编译
—————————————————————————————————————————————————————————————
(4)需要注意:
   当添加了以上《数据库编程的依赖》后,如果启动项目,将失败!
—————————————————————————————————————————————————————————————
(5)因为添加了数据库编程的依赖项后,
   Spring Boot就会尝试————>  自动装配数据源(`DataSource`)等对象,
   装配时所需的:
   连接数据库的配置信息(例如URL、登录数据库的用户名和密码)应该是配置在`application.properties`中的,
   但是,如果尚未配置,就会导致失败!
—————————————————————————————————————————————————————————————
(6)关于连接数据库的配置信息,   
   Spring Boot要求对应的属性名是:
   在【application.properties】文件中配置:

   # 连接数据库的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=root

   在配置时,也必须使用以上 属性名 进行配置,
   则Spring Boot会自动读取这些 属性对应的值 ,用于————>创建数据源对象!

   此时,
   即使将上边配置中的【password=root】改为任何密码,也可以启动,
   不过会自动停止服务!
   解决:加上Web相关才可以正式成为一个服务端,才可以正常启动!

   :	
   由于SpringBoot在启动时只是加载以上配置,并不会实际的连接到数据库。
   所以,当以上配置存在时,启动就不会报错,
   但是,无法检验以上配置的值是否正确!
—————————————————————————————————————————————————————————————
(7)可以在测试类中添加测试方法,尝试连接数据库,以检验以上配置值是否正确:

@SpringBootTest
class BootDemoApplicationTests {
    @Autowired
    DataSource dataSource;//数据源
    
    
    @Test
    void testGetConnection() throws SQLException {
        System.out.println(dataSource.getConnection());
    }
}    
        
   启动测试类 BootDemoApplicationTests,可以正常启动!  

   如果以上测试通过,则表示配置值无误,可以正确连接到数据库,
   如果测试失败,则表示配置值错误,
     需检查 配置值及本地环境(例如MySQL是否启动、是否已创建对应的数据库等) 。
—————————————————————————————————————————————————————————————
05.关于Profile配置
(1)在Spring Boot中,对Profile配置有很好的支持,
   开发人员可以在srcmainresources下创建更多的配置文件,
   这些配置文件的名称应该是:
   application-???.properties(其中的???是某个名称,是自定义的)。
例如:
/1.仅在开发环境中使用的配置值可写在————>application-dev.properties中
/2.仅在测试环境中使用的配置值可写在————>application-test.properties中
/3.仅在生产环境(项目上线的环境)中使用的配置值可写在——>application-prod.properties中
—————————————————————————————————————————————————————————————
(2)当把配置写在以上这类文件后,
   Spring Boot默认并不会应用以上这些文件中的配置,
   当需要应用某个配置时,
   【需要在application.properties中激活某个Profile配置】,
   例如:按上面所说,仅在开发环境中使用的配置值要写在application-dev.properties中
   
   # 激活Profile配置
   spring.profiles.active=dev

(3)提示:
   以上配置值中的 dev 是需要激活的配置文件的文件名后缀,
   当配置为 dev 时,就会激活 application-dev.properties,
   同理,如果以上配置值为test,就会激活 application-test.properties。
—————————————————————————————————————————————————————————————
06.关于YAML配置
(1)Spring Boot也支持使用YAML配置,在开发实践中,YAML的配置也使用得比较多。
—————————————————————————————————————————————————————————————
(2)YAML配置就是把原有的.properties配置的扩展改为yml。
—————————————————————————————————————————————————————————————
(3)YAML配置原本并不是Spring系列框架内置的配置语法,
   如果在项目中需要使用这种语法进行配置,解析这类文件需要添加相关依赖,
   在Spring Boot中默认已添加此依赖。
—————————————————————————————————————————————————————————————
(4)在YAML配置中,
   原本在.properties的配置表现为使用多个小数点分隔的配置,
   将改为换行并使用2个空格缩进的语法,换行前的部分使用冒号表示结束,
   最后的属性名与值之间使用冒号和1个空格进行分隔,
   如果有多条属性在.properties文件中属性名有重复的前缀,在yml中不必也不能重复写。
—————————————————————————————————————————————————————————————
(4)例如,原本在.properties中配置为:
   # 激活Profile配置
   spring.profiles.active=dev
 
   则将上边的application.properties 改为 application.yml文件:
   在该yml文件中配置为:
    # Spring系列框架的配置
    spring:
      # Profile配置
      profiles:
        # 激活Profile配置
        active: dev

   再将上边的application_dev.properties 改为 application-dev.yml文件:
   在该yml文件中配置为:
   # Spring系列框架的配置
   spring:
     # 连接数据库的配置
     datasource:
       # 连接数据库的URL
       url: jdbc:mysql://localhost:3306/mall_ams?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
       # 登录数据库的用户名
       username: root
       # 登录数据库的密码
       password: root

提示:在IntelliJ IDEA中编写yml时,当需要缩进2个空格时,
     仍可以使用键盘上的TAB键进行缩进,
     IntelliJ IDEA会自动将其转换为2个空格。
—————————————————————————————————————————————————————————————
(5)无论是 .properties 还是 .yml ,
   只是配置文件的扩展名和文件内部的配置语法有区别,
   对于Spring Boot最终的执行其实没有任何表现上的不同。
07.使用Druid数据库连接池
(1)Druid数据库连接是阿里巴巴团队研发的,
   在Spring Boot项目中,如果需要显式的指定使用此连接池,
   首先,需要在项目中添加依赖:
   在pom.xml文件里添加依赖:
        
        
            com.alibaba
            druid
            1.1.20
        
—————————————————————————————————————————————————————————————
(2)当添加了此依赖,
   在项目中需要应用时,
   需要在【配置文件中:src/main/resources下的:application.yml文件】中:
   指定【spring.datasource.type】属性,
   取值为以上依赖项的jar包中的【DruidDataSource】类型的全限定名。
   例如,在yml中配置为:
   
# application.yml文件中:激活Profile配置
spring:
  # Profile配置:
  profiles:
    # 激活Profile配置
    active: dev
  # 连接数据库的相关配置:
  datasource:
    # 使用的数据库连接池类型
    type: com.alibaba.druid.pool.DruidDataSource

—————————————————————————————————————————————————————————————
08.编写持久层(数据访问层)代码
(1)数据持久化:默认指的是使用数据库存储数据

数据持久化:
在开发领域中,讨论数据时,通常指定是正在执行或处理的数据,
这些数据都是在内存中的,而内存(RAM)的特征包含”一旦断电,数据将全部丢失“,
为了让数据永久保存下来,通常会将数据存储到能够永久存储数据的介质中,
通常是计算机的硬盘,硬盘上的数据都是以文件的形式存在的,
所以,当需要永久保存数据时,
可以将数据存储到文本文件中,或存储到XML文件中,或存储到数据库中,
这些保存的做法就是数据持久化,
而文本文件、XML文件都不利于实现增删改查中的所有数据访问操作,
而数据库是实现增删改查这4种操作都比较便利的,
所以,一般在讨论数据持久化时,默认指的都是使用数据库存储数据。
—————————————————————————————————————————————————————————————
(2)在项目中,会将代码(各类、接口)划分一些层次,各层用于解决不同的问题,
   其中,持久层就是用于解决数据持久化问题的,
   甚至,简单来说,持久层对应的就是数据库编程的相关文件或代码。


   目前,使用Mybatis技术实现持久层编程,需要:
   
   /1.编写一次性的基础配置:
		使用@MapperScan指定接口所在的Base Package
		指定配置SQL语句的XML文件的位
		
   /2.编写每个数据访问功能的代码:
		在接口中添加必须的抽象方法:
			可能需要创建相关的POJO类
		在XML文件中配置抽象方法映射的SQL语句
—————————————————————————————————————————————————————————————
(3)关于一次性的配置,@MapperScan注解需要添加在配置类上,有2种做法:
	/1.直接将此注解添加在启动类上,因为启动类本身也是配置类
	/2.自行创建配置类,在此配置类上添加@MapperScan
—————————————————————————————————————————————————————————————
(4)如果采用以上的第2种做法,
   则应该在srcmainjava包下:cn.tedu.boot.demo包下:创建包: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 {
   }
—————————————————————————————————————————————————————————————
(5)另外,关于指定配置SQL语句的XML文件的位置,
   需要在【application.yml文件】(或application.properties)中
   配置 [mybatis.mapper-locations] 属性,例如:

   #Mybatis相关配置:
   mybatis:
     #用于配置SQL语句的XML文件的位置
     mapper-localions: classpath:mapper]】
—————————————————————————————————————————————————————————————
(6)基于以上的配置值,还应该在src/main/resources下自行创建名为 mapper 的文件夹。
—————————————————————————————————————————————————————————————
(7)至此,关于使用Mybatis实现数据库编程的一次性配置结束!
   接下来,可以使用任何你已知的Mybatis使用方式实现所需的数据访问。
—————————————————————————————————————————————————————————————
—————————————————————————————————————————————————————————————

(7)目前,
   设定目标为:最终实现”添加管理员账号“的功能。则在数据访问层需要做到:
—————————————————————————————————————————————————————————————
(8.1)插入管理员数据:
   创建cn.tedu.boot.demo.entity.Admin类(复制mybatis里的Admin即可)
   
   在cn.tedu.boot.demo.mapper包(不存在,则创建)下创建AdminMapper接口,
   并在接口中声明int insert(Admin admin);方法
        @Repository
        public interface AdminMapper {
        	int insert(Admin admin);
		}
— —  —  —  —  —  —  —  —  —  —  —  —  —  — — —   —  —  —  — —
   在src/main/resources/mapper文件夹下:
   通过mybatis中的粘贴得到AdminMapper.xml文件,
   在此文件中配置与以上抽象方法映射的SQL语句:
   注意改动:【mapper节点的namespace属性的值:"cn.tedu.boot.demo.mapp.."】
	  	
   
— —  —  —  —  —  —  —  —  —  —  —  —  —  — — —   —  —  —  — —  	
  编写完成后,应该及时测试,测试时,   
  推荐在src/test/java的根包下创建mapper.AdminMapperTests测试类,	  	
  并在此类中编写AdminMapper接口中的抽象方法insert的测试方法:
	  	
package cn.tedu.boot.demo.mapper;
import cn.tedu.boot.demo.entity.Admin;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class AdminMapperTests {

    @Autowired
    AdminMapper mapper;

    @Test
    void testInsert() {
        Admin admin = new Admin();
        admin.setUsername("a");
        admin.setPassword("123456");
        int rows = mapper.insert(admin);
        System.out.println("rows = " + rows);
    }
}
—  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —
(8.2)根据用户名查询管理员数据
   后续,在每次插入数据之前,会调用此功能进行查询,以此保证”重复的用户名不会
   被添加到数据库中“: 即便在数据表中用户名已经添加了unique,
   但是,不应该让程序执行到此处
         				  
   在AdminMapper接口中添加【Admin getByUsername(String username);】方法;
   
—  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  — 	   
   
   在AdminMapper.xml文件中添加与以上抽象方法映射的SQL语句:
	   




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

    
        select
            
        from ams_admin
        where username=#{username}
    

    
        
            id,
            username,
            password,
            nickname,
            avatar,
            phone,
            email,
            description,
            is_enable,
            last_login_ip,
            login_count,
            gmt_last_login,
            gmt_create,
            gmt_modified
        
    

    
        
        
        
        
        
        
        
        
        
        
        
        
        
        
    

—  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —  —   
   编写完成后,应该及时测试,在测试类中添加【testGetByUsername】方法:
	@Test
    void testGetByUsername() {
        String username = "test-user-00888888";
        Admin admin = mapper.getByUsername(username);
        System.out.println("admin = " + admin);
    }
       然后进行测试


其它问题暂不考虑,
例如在ams_admin中,其实phone和email也是设置了unique的,
如果完整的实现,则还需要添加:根据phone查询管理员的功能 和 根据email查询管理员的功能,
在不实现这2个功能的情况下,
后续进行测试和使用时,
应该不使用重复的phone和email值来测试或执行
—————————————————————————————————————————————————————————————
09.关于业务逻辑层(Service层)
(1)业务逻辑层是被Controller直接调用的层(Controller不允许直接调用持久层),
   通常,
   在业务逻辑层中编写的代码是为了保证数据的完整性和安全性,
   使得数据是随着我们设定的规则而产生或发生变化。

   通常,在业务逻辑层的代码会由接口和实现类组件,其中,接口被视为是必须的
   :推荐使用基于接口的编程方式
   :部分框架在处理某些功能时,会使用基于接口的代理模式,例如Spring JDBC框架在处理事务时
   
   在接口中,声明抽象方法时,
   仅以操作成功为前提来设计返回值类型(不考虑失败),
   如果业务在执行过程可能出现某些失败(不符合所设定的规则),可以通过抛出异常来表示!


控制器Controller调用业务逻辑层Service的代码时,大致表现为: 
①   
try {
    Admin admin = service.login("root","1234");
    // 登录成功的后续处理
} catch (UserNotFoundException e) {
    // 针对用户名错误的处理
} catch (PasswordNotMatchException e) {
    // 针对密码错误的处理
}
②
public interface IAdminService {
    void addNew(Admin admin);
    
    Admin login(String username, String password) throws UserNotFoundException, PasswordNotMatchException;
}
③
//@Service
public class TomServiceImpl implements xxxService {
    public void addNew(Admin admin) {
        
    }
}

@Service
public class JackServiceImpl implements xxxService {
    public void addNew(Admin admin) {
        
    }
}
④
@Autowired
xxxService xxx;
———————————————————————————————————————————————————————————

(2)关于抛出的异常,通常是自定义的异常,
   并且,自定义异常通常是RuntimeException的子类,主要原因:两个:
   :不必显式的抛出或捕获,因为业务逻辑层的异常永远是抛出的,
    而控制器层会调用业务逻辑层,在控制器层的Controller中其实也是永远抛出异常的,
    这些异常会通过Spring MVC统一处理异常的机制进行处理,
    关于异常的整个过程都是固定流程,所以,没有必要显式抛出或捕获

  :部分框架在处理某些事情时,默认只对RuntimeException的子孙类进行识别并处理,
   例如Spring JDBC框架在处理事务时
———————————————————————————————————————————————————————————
———————————————————————————————————————————————————————————
———————————————————————————————————————————————————————————

代码实现:
(3)所以,在实际编写业务逻辑层之前,应该先规划异常,

   例如先创建包ex 再创建ServiceException类:
   并 extends RuntimeException:	
	
   package cn.tedu.boot.demo.ex;
   public class ServiceException extends RuntimeException {    
   }
———————————————————————————————————————————————————————————
(4)接下来,再创建具体的对应某种“失败”的异常,
   例如,在添加管理员时,可能因为“用户名已经存在”而失败,
        则在 ex 包下创建对应的UsernameDuplicateException异常:
        并extends ServiceException:
        
        package cn.tedu.boot.demo.ex;
        public class UsernameDuplicateException extends ServiceException {
	    }
———————————————————————————————————————————————————————————
(5)另外,当插入数据时,
   如果返回的受影响行数不是1时,必然是某种错误,则创建对应的插入数据异常:
   在 ex 包下创建InsertException类 并extends ServiceException:
  
  package cn.tedu.boot.demo.ex;
  public class InsertException extends ServiceException {   
  } 
———————————————————————————————————————————————————————————
(6)关于抽象方法的参数,
   应该设计为客户端提交的数据类型或对应的封装类型,
   不可以是数据表对应的实体类型!如果使用封装的类型,
   这种类型在类名上应该添加某种后缀,例如DTO或其它后缀,例如:
   创建包: pojo/dto 再创建类AdminAddNewDTO 
          并implements Serializable:
          并调用方法:

   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()
   }
———————————————————————————————————————————————————————————
(6)然后,在cn.tedu.boot.demo 下创建:service包 下创建接口及抽象方法:

   package cn.tedu.boot.demo.service;
   public interface IAdminService {
       void addNew(AdminAddNewDTO admin);
   }
———————————————————————————————————————————————————————————
(7.1)并在以上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异常
    }    
}



(7.2)以上未实现的代码为:
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 cn.tedu.boot.demo.util.PasswordEncoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;

//没有该注解,spring不会创建Admin对象
@Service // @Component, @Controller, @Repository
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();
        
        // TODO 通过加密方式,得到加密后的密码encodedPassword
        //暂时不加密 写为String encodedPassword = password;
        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(复制属性)
        // 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();
        }
    }

}
———————————————————————————————————————————————————————————
(8)各写各的测试:
   在 src/test/绿色java/cn.tedu.boot.demo下:
   创建包 service:创建测试类 AdminServiceTests:
   
package cn.tedu.boot.demo.service;
import cn.tedu.boot.demo.ex.UsernameDuplicateException;
import cn.tedu.boot.demo.pojo.dto.AdminAddNewDTO;
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;


@SpringBootTest
public class AdminServiceTests {

    @Autowired
    IAdminService service;

    
    @Test
    void testAddNewSuccessfully() {
        // 测试数据
        AdminAddNewDTO adminAddNewDTO = new AdminAddNewDTO();
        adminAddNewDTO.setUsername("service-admin-001");
        adminAddNewDTO.setPassword("123456");
        adminAddNewDTO.setNickname("业务管理员001");
        adminAddNewDTO.setAvatar("未知头像");
        adminAddNewDTO.setPhone("13800138008");
        adminAddNewDTO.setEmail("service-admin-001@tedu.cn");
        adminAddNewDTO.setDescription("懒得描述了");
        // 断言执行成功
        Assertions.assertDoesNotThrow(() -> {
            // 执行测试
            service.addNew(adminAddNewDTO);
        });
    }


    
    @Test
    void testAddNewFailBecauseUsernameDuplicate() {
        // 测试数据
        AdminAddNewDTO adminAddNewDTO = new AdminAddNewDTO();
        adminAddNewDTO.setUsername("service-admin-001");
        adminAddNewDTO.setPassword("123456");
        adminAddNewDTO.setNickname("业务管理员001");
        adminAddNewDTO.setAvatar("未知头像");
        adminAddNewDTO.setPhone("13800138008");
        adminAddNewDTO.setEmail("service-admin-001@tedu.cn");
        adminAddNewDTO.setDescription("懒得描述了");
        // 断言执行过程中将抛出UsernameDuplicateException异常
        Assertions.assertThrows(UsernameDuplicateException.class,() -> {
            // 执行测试
            service.addNew(adminAddNewDTO);
        });
    }
}
测试完第一个方法再测试第二个,都不会报错,可以正常运行!

———————————————————————————————————————————————————————————
(9)以上代码未实现对密码的加密处理!
   关于密码加密,相关的代码应该定义在别的某个类中,
   不应该直接将加密过程编写在以上代码中,
   因为加密的代码需要在多处应用(添加用户、用户登录、修改密码等),
   并且,从分工的角度上来看,也不应该是业务逻辑层的任务!
   所以在【cn.tedu.boot.demo.util(若包不存在,则创建)下创建PasswordEncoder类】,
   用于处理密码加密:
   
package cn.tedu.boot.demo.util;
import org.springframework.stereotype.Component;

//声明成一个组件,每次要用的时候就自动装配
@Component
public class PasswordEncoder {
    public String encode(String rawPassword) {
        // TODO 加密过程
        return "aaa" + rawPassword + "aaa";
    }
}
———————————————————————————————————————————————————————————
(10)完成后,需要在AdminServiceImpl中自动装配以上PasswordEncoder,
    并在需要加密时调用PasswordEncoder对象的encode()方法。

具体实现:
   在【AdminServiceImpl】类中:
   /1.添加自动装配的[passwordEncoder];
   /2.并且将下面的【暂时不加密】的等号右边由【password】改为:
      【passwordEncoder.encode(password);】:
    
  @Autowired
  private PasswordEncoder passwordEncoder;

  // 通过参数获取原密码
  String password = adminAddNewDTO.getPassword();
  
  // TODO 通过加密方式,得到加密后的密码encodedPassword
  //暂时不加密 写为String encodedPassword = password;
  String encodedPassword = passwordEncoder.encode(password);
  
———————————————————————————————————————————————————————————
(11)最后,加密完成再从测试类【AdminServiceTests】中测试:
   将【testAddNewSuccessfully】中的下面改掉:
   注意用户名、手机号和邮箱都要改为【002】
   
  adminAddNewDTO.setUsername("service-admin-002");
  adminAddNewDTO.setPhone("13800138002");
  adminAddNewDTO.setEmail("service-admin-002@tedu.cn");
   
  测试该方法,发现正常运行没有报错,此时我们来看一下数据库信息,
  发现在ams_admin表格中:
  刚添加的 用户名002的信息,密码已经变为:【aaa123456aaa】

至此,业务service层已经写完了
10.使用Lombok框架
(1)在编写POJO类型(包括实体类、VO、DTO等)时,都有统一的编码规范,例如:
   /1.属性都是私有的
   /2.所有属性都有对应的Setter & Getter方法
   /3.应该重写equals()和hashCode()方法,以保证:
      如果2个对象的字面值完全相同,则equals()对比结果为true,且hashCode()返回值相同,
      如果2个对象的字面值不相同,则equals()对比结果为false,且hashCode()返回值不同
   /4.实现Serializable 序列化接口

(2)另外,为了便于观察对象的各属性值,通常还会重写toString()方法。

(3)由于以上操作方式非常固定,且涉及的代码量虽然不难,但是篇幅较长,
   并且,
   当类中的属性需要修改时(包括修改原有属性、或增加新属性、删除原有属性),
   对应的其它方法都需要修改(或重新生成),管理起来比较麻烦。

(4)使用Lombok框架可以极大的简化这些操作,
   此框架可以通过注解的方式,
   在【编译期】来生成 Setters/Getters、equals()、hashCode()、toString(),甚至生成 构造方法 等,
   所以一旦使用此框架,
   开发人员就只需要:————>在类中声明各属性、实现Serializable序列化接口、添加Lombok指定的注解即可。
—————————————————————————————————————————————————————————————————————————————————————
(5)在Spring Boot工程:——————>【即boot-demo中】:
   添加Lombok依赖,可以在创建项目时勾选,也可以后期自行添加,依赖项的代码为:
   
——>
   
   	org.projectlombok
    lombok
    true
   
—————————————————————————————————————————————————————————————————————————————————————
(6)完成后,
   在各POJO类型中(包括实体类、VO、DTO等):
   将不再需要在源代码添加Setters & Getters、equals()、hashCode()、toString()这些方法,
   只需要在POJO类上添加@Data注解即可!
   
————>在Admin类中:添加【@Data】注解  注意是lombok包下的 ;并将下面所有调用方法删掉
————>在AdminAddNewDTO类中:
     也添加此注解:【@Data】,并将下面所有调用方法删掉,只留下属性和实现序列化接口即可
   
   当添加@Data注解,且删除相关方法后,由于源代码中没有相关方法,则调用了相关代码的方法可能会报错,
   但是,并不影响程序运行!
-	-	-	-	-	-	-	-	-	-	-	-	-	-	-	-	- - -	-	-	-	-	-	-	-      
   为了避免IntelliJ IDEA判断失误而提示了警告和错误(不影响代码的编写和运行):
   ——>在Admin类中的属性可能会变灰色,此时像AdminServiceTests类中调用set方法时会报错;
   ——>有的可能不会变灰色,因为当引入[@Data注解]时,idea自动下载了lombok插件
   推荐安装Lombok插件,可参考:
   ————> http://doc.canglaoshi.org/doc/idea_lombok/IDEA-5-PLUGINS-LOMBOK.html
   
   再次提示:无论是否安装插件,都不影响代码的编写和运行!
11.Slf4j(slf for java)日志框架
在Slf4j日志框架中,将日志的可显示级别【根据其重要程度(严重程度)[由低到高]】分为:
   trace:跟踪信息
   debug:调试信息
   info: 一般信息,通常不涉及关键流程和敏感数据 默认用info级别
   warn: 警告信息,通常代码可以运行,但不够完美,或不规范
   error:错误信息

工作流程:
1.AdminServiceImpl类上加注解:@Slf4j 
2.测试类中:【用log变量】【调用info等显示级别的方法】来输出你想在控制台打印的语句
3.配置文件中指定的级别:
  -若为最低级别 trace, 则在控制台会输出包括trace和更高级别的语句
  -若为最高级别 error, 则只会在控制台输出调用error的输出语句


Slf4j:没有实现具体功能  
log4j:实现了具体功能 
(1)在开发实践中,
   不允许使用System.out.println()或类似的输出语句来输出显示关键数据(核心数据、敏感数据等),
   因为,如果是这样使用,
   无论是在开发环境,还是测试环境,还是生产环境中,这些输出语句都将输出相关信息,
   而删除或添加这些输出语句的操作成本比较高,操作可行性低。

(2)推荐的做法是使用日志框架来输出相关信息!

(3)当添加了Lombok依赖后,可以在需要使用日志的类上添加【@Slf4j注解:slf for java】,
   然后,在类的任意方法中,均可使用名为log的变量,
   且调用其方法来输出日志(名为log的变量也是Lombok框架在【编译期】自动补充的声明并创建对象)!
—————————————————————————————————————————————————————————————————————————————————————
(4)在 AdminServiceImpl 类中:添加lombok.extern.slf4j包下的注解: @Slf4j 
   在【通过参数获取原密码】下添加:
     【log.info("原密码="+password);】
   在【通过加密方式,得到加密后的密码encodedPassword】下添加:
     【log.info("加密后的密码="+encodedPassword);】
   在AdminServiceTests类中的:
      testAddNewSuccessfully 方法中:改动用户名/电话号和email为063并测试该方法:
      改动的代码:
      ("service-admin-063")
      ("13800138063")
      ("service-admin-063@tedu.cn")   
      此时控制台输出:原密码和加密后的密码:
        控制台显示:
        : {dataSource-1} inited
        : 原密码=123456
        : 加密后的密码=aaa123456aaa           
—————————————————————————————————————————————————————————————————————————————————————.
(5)在Slf4j日志框架中,将日志的可显示级别【根据其重要程度(严重程度)[由低到高]】分为:
   trace:跟踪信息
   debug:调试信息
   info: 一般信息,通常不涉及关键流程和敏感数据
   warn: 警告信息,通常代码可以运行,但不够完美,或不规范
   error:错误信息
—————————————————————————————————————————————————————————————————————————————————————
(6)在配置文件(.yml文件)中,可以通过logging.level.包名.类名来设置当前类的日志显示级别,例如:
————>在application.yml文件中:写日志(注意靠着最左侧一个空格都没有的地方写):
     【logging.level.cn.tedu.boot.demo.service.impl.AdminServiceImpl: info】注意有个空格


     在AdminServiceTests类中的:
      testAddNewSuccessfully 方法中:改动用户名/电话号和email为064并测试该方法:
     此时控制台输出:原密码和加密后的密码:  和上边的测试一样
       控制台显示:
       : {dataSource-1} inited
       : 原密码=123456
       : 加密后的密码=aaa123456aaa 
—————————————————————————————————————————————————————————————————————————————————————
(7)在 AdminServiceImpl 类中:
   在【通过参数获取原密码】下的log调用方法改为:debug
     【log.debug("原密码="+password);】    
   在AdminServiceTests类中的:
      testAddNewSuccessfully 方法中:改动用户名/电话号和email为065并测试该方法:  
      ——————>我们发现此时在控制台该方法会运行成功,但不会显示调用 debug方法的输出语句:
      			 控制台显示:
             : {dataSource-1} inited
             : 加密后的密码=aaa123456aaa
   因为较低级别的不会显示,
   因为在配置文件中我们设置为了【info】,所以会显示【warm 和 error】
——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	—— 
——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——

   总结:
   当设置了显示的日志级别后,仅显示设置级别和更重要的级别的日志,
   例如,设置为info时,只显示info、warn、error,不会显示debug、trace级别的日志!
   
   当输出日志时,
   通过log变量:
   调用trace()方法输出的日志就是trace级别的,
   调用debug()方法输出的日志就是debug()级别的,
   以此类推,可调用的方法还有info()、warn()、error()。
——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——
   在开发实践中,关键数据和敏感数据都应该通过【trace() 或 debug()】进行输出,
   在开发环境中,可以将日志的显示级别设置为【trace】,则会显示所有日志,
   当需要交付到生产环境中时,只需要将日志的显示级别调整为【info】即可!
   
   例如:/1.当我们把【application.yml文件中】改为trace
            在【AdminServiceImpl 类中】:
              [通过参数获取原密码]下:换为调用trace()方法,
              [通过加密方式,得到加密后的密码encodedPassword]下:换为调用debug()方法
            在AdminServiceTests类中的:
              testAddNewSuccessfully 方法中:改动用户名/电话号和email为066并测试该方法: 
              此时————>则会显示所有日志(原密码和加密后的密码信息) 

		 /2.当我们把【application.yml文件中】改为 info,
		    再根据上边步骤对其相应调整,则不会输出任何日志信息(没有:原密码和加密后的密码信息) 
—————————————————————————————————————————————————————————————————————————————————————     
(8)在 AdminServiceImpl 类中:【addNew】方法中的首行添加:
        //日志
        log.trace("业务逻辑层: AdminAddNewDTO.addNew() >>> 开始添加管理员业务");
        .........
        .........

    根据你自己要输出的值来调用不同级别的方法,
    例如:
    纯文字的调用trace
    带数据的调用debug
    抛出异常调用error,因为日志级别比较重要
    配置文件中指定的级别:
    -若为最低级别 trace, 则在控制台会输出包括trace和更高级别的语句
    -若为最高级别 error, 则只会在控制台输出调用error的输出语句
————————————————————————————————————————————————————————————————————————————————————— 
(9)默认情况下,日志的显示级别是info,
   所以,即使没有在配置文件中进行正确的配置,所有info、warn、error级别的日志都会输出显示。

   在配置时,属性名称中的logging.level部分是必须的,在其后,必须写至少1级包名,例如:    
     [logging.level.cn: trace]
   以上配置表示cn包及其子孙包下的所有类中的日志都按照trace级别进行显示!

   在开发实践中,属性名称 通常配置为  logging.level.项目根包,
   例如:
   [logging.level.cn.tedu.boot.demo: trace]
————————————————————————————————————————————————————————————————————————————————————— 
(10)将【application.yml】配置文件中级别设为:error:
      ————>  logging.level.cn.tedu.boot.demo: error
    
    将【AdminServiceImpl类】的【addNew】下的这个需求改为warn:
      // 是:表示用户名已经被占用,则抛出UsernameDuplicateException
      log.warn("此账号已经被占用,将抛出异常");
    我们需要上边这条语句被触发,那用的用户名必须时存在的  ,
    运行方法时会报用户名重复异常,就不会在控制台显示此输出语句了
————————————————————————————————————————————————————————————————————————————————————— 
(11)在使用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);


例:
    在【AdminServiceImpl】类中,将    :
     log.debug("原密码="+password);
     log.debug("加密后的密码="+encodedPassword);
    这两句注释掉,下面加一句:
     log.debug("加密前的密码:{},加密后的密码:{}", password, encodedPassword);
    此时再次测试 AdminServiceTests 类的:testAddNewSuccessfully方法(添加一个不重复的进行测试):

会在控制台显示:(加密前后的密码)
: 业务逻辑层: AdminAddNewDTO.addNew() >>> 开始添加管理员业务
: 判断此账号是否已经被占用
: {dataSource-1} inited
: ==>  Preparing: select id, username, passwor
: ==> Parameters: service-admin-061(String)
: <==      Total: 0
: 此账号未被占用,准备向表中写入此次添加的管理员信息
: 加密前的密码:123456,加密后的密码:aaa123456aaa
: ==>  Preparing: insert into ams_admin (usern
: ==> Parameters: service-admin-061(String), a
: <==    Updates: 1
    

此时我们不用拼接字符串了: 
        //字符串拼接写法:
			 //log.debug("加密前的密码:" + password + ",加密后的密码:" + encodedPassword);
        //下面这句代码与上边等效:简化代码,提高拼接效率:
        log.debug("加密前的密码:{},加密后的密码:{}", password, encodedPassword);
     
使用这种做法,可以避免多变量时频繁的拼接字符串,
另外,
日志框架会将第1个参数进行缓存,以此提高后续每一次的执行效率。      
———————————————————————————————————————————————————————————————————————————————————
(12)在开发实践中,应该对程序执行关键位置添加日志的输出,通常包括:

    /1.每个方法的第1行有效语句,表示代码已经执行到此方法内,或此方法已经被成功调用
       ————>如果方法是有参数的,还应该输出参数的值
       
    /2.关键数据或核心数据在改变之前和之后
	   ————>例如对密码加密时,应该通过日志输出加密前和加密后的密码
	   
	/3.重要的操作执行之前
	   ————>例如尝试插入数据之前、修改数据之前,应该通过日志输出相关值
	        例:在AdminServiceImpl类中:添加【log.debug...这句】:
	            // 调用adminMapper的insert(Admin admin)方法插入管理员数据,获取返回值
        		log.debug("即将插入新的管理员信息: {}",admin);
        		int rows = adminMapper.insert(admin);
	        	   
	/4.程序走到某些重要的分支时
	   ————>例如经过判断,走向抛出异常之前    
———————————————————————————————————————————————————————————————————————————————————
(13)其实,
    Slf4j日志框架:只是日志的一种标准,并不是具体的实现(感觉上与Java中的接口有点相似),
    常见有具体实现了日志功能的框架有log4j、logback等,
    为了统一标准,所以才出现了Slf4j,
    同时,由于log4j、logback等框架实现功能并不统一,
    所以,Slf4j提供了对主流日志框架的兼容,
    在SpringBoot工程中:spring-boot-starter就已经依赖了spring-boot-starter-logging,
    而在此依赖下,通常包括Slf4j、具体的日志框架、Slf4j对具体日志框架的兼容。

    在application.yml 配置文件里,改动日志相关配置的书写方式:
    
    # 日志相关配置
	logging:
  		# 日志的显示级别
  		level:
    		# 根包的日志显示级别
    		cn.tedu.boot.demo: trace
		
	并将这段代码剪切到【application-dev.yml】配置文件里,因为放在这个文件里更合理
12.密码加密【这并不是Spring Boot框架的知识点】
  • ——————>防自己人的,即使被人知道结果是被加密算出来的,但你就是无法还原回去,大大保证用户的安全
(1)对密码进行加密,可以有效的保障密码安全,
   即使出现数据库泄密,密码安全也不会受到影响!
   为了实现此目标,需要在对密码进行加密时,使用不可逆的算法进行处理!

(2)通常,不可以使用加密算法对密码进行加密码处理,
   从严格定义上来看,所有的加密算法都是可以逆向运算的,
   即同时存在加密和解密这2种操作,
   加密算法只能用于保证传输过程的安全,并不应该用于保证需要存储下来的密码的安全!
———————————————————————————————————————————————————————————————————     
(3)哈希算法都是不可逆的,
   通常,用于处理密码加密的算法中,典型的是一些: 消息摘要算法,
   例如MD5、SHA256或以上位数的算法。
   

   在src/test/绿色的java下的:cn.tedu.boot.demo下创建测试类:DigestTests:  
   package cn.tedu.boot.demo;
   import org.junit.jupiter.api.Test;
   import org.springframework.util.DigestUtils;
   public class DigestTests { //DigestTests:文摘测试
      
       @Test
       public void md5Test(){
           String rawPassword = "123456";
           String encodePassword = DigestUtils.md5DigestAsHex(rawPassword.getBytes());
           System.out.println("原密码:"+rawPassword);
           System.out.println("加密后的密码:"+encodePassword);
           
       }
   }
———————————————————————————————————————————————————————————————————      
(4)消息摘要算法的主要特征有:
   /1.消息相同时,摘要一定相同
   /2.某种算法,无论消息长度多少,摘要的长度是固定的  
   /3.消息不同时,摘要几乎不会相同

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

(6)存在穷举消息和摘要的对应关系,并利用摘要在此对应关系进行查询,从而得知消息的做法,
   (以上的意思就是将每一个数值对应着消息摘要都记录下来存入一个库方便查询)
   但是,由于MD5是128位算法,全部穷举是不可能实现的,
   所以,只要原始密码(消息)足够复杂,就不会被收录到所记录的对应关系中去!
———————————————————————————————————————————————————————————————————  
(7)为了进一步提高密码的安全性,在使用消息摘要算法进行处理时,通常还会加盐!
   盐值可以是任意的字符串,用于与密码一起作为被消息摘要算法运算的数据即可,例如:

加自己定义的盐值:
package cn.tedu.boot.demo;
import org.junit.jupiter.api.Test;
import org.springframework.util.DigestUtils;
public class DigestTests { //DigestTests:文摘测试
    
    @Test
    public void md5Test(){
        // SHA-1 160, MD5 128
        // 123456 >> e10adc3949ba59abbe56e057f20f883e
        String rawPassword = "123456";
        String salt = "efegwgasgvetjhytjkshw";
        String encodePassword = DigestUtils.md5DigestAsHex((salt+salt+rawPassword+salt).getBytes());
        System.out.println("原密码:"+rawPassword);
        System.out.println("盐值:"+salt);
        System.out.println("加盐值并加密后的密码:"+encodePassword);
        
    }
}


加随机的盐值:
package cn.tedu.boot.demo;
import org.junit.jupiter.api.Test;
import org.springframework.util.DigestUtils;
import java.util.UUID;
public class DigestTests { //DigestTests:文摘测试
    
    @Test
    public void md5Test(){
        // SHA-1 160, MD5 128
        // 123456 >> e10adc3949ba59abbe56e057f20f883e
        String rawPassword = "123456";
//        String salt = "efegwgasgvetjhytjkshw";//自己定义的盐值
        String salt = UUID.randomUUID().toString();//随机盐
        String encodePassword = DigestUtils.md5DigestAsHex((salt+salt+rawPassword+salt).getBytes());
        System.out.println("原密码:"+rawPassword);
        System.out.println("随机盐值:"+salt);
        System.out.println("加随机盐值并加密后的密码:"+encodePassword);
        
    }
}

———————————————————————————————————————————————————————————————————  
(8)加盐的目的是使得被运算数据变得更加复杂,盐值本身和用法并没有明确要求!
   甚至,
   在某些用法或算法中,还会使用随机的盐值,则可以使用完全相同的原消息对应的摘要却不同!
   推荐了解:预计算的哈希链、彩虹表、雪花算法。
———————————————————————————————————————————————————————————————————  
(9)为了进一步保证密码安全,还可以使用多重加密,即反复调用消息摘要算法。
   例:
   写一个100000次的循环,将每次加密后的密码都作为原密码再去返回消息摘要:
   在DigestTests测试类中再写一个方法用来测试:
   
    @Test
    public void md5Test2(){
        String rawPassword = "123456";
        String encodePassword = rawPassword;
        for (int i = 0; i < 100000; i++) {
            encodePassword = DigestUtils.md5DigestAsHex(encodePassword.getBytes());
            System.out.println("加密后的密码:"+encodePassword);
        }
    }
   
———————————————————————————————————————————————————————————————————  
(10)除此以外,还可以使用安全系数更高的算法,
    例如SHA-256是256位算法,SHA-384是384位算法,SHA-512是512位算法。
    一般的应用方式可以是:

    我们设计一个加密过程:
        // 加密过程
        // 1. 使用MD5算法
        // 2. 使用随机的盐值
        // 3. 循环5次
        // 4. 盐的处理方式为:盐 + 原密码 + 盐 + 原密码 + 盐
        // 注意:因为使用了随机盐,盐值必须被记录下来,本次的返回结果使用$分隔盐与密文
        
    在【PasswordEncoder】类中完成以上的需求:
package cn.tedu.boot.demo.util;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import java.util.UUID;

//声明成一个组件,每次要用的时候就自动装配
@Component
public class PasswordEncoder {
    public String encode(String rawPassword) {
        // TODO 加密过程
//        return "aaa" + rawPassword + "aaa";

        // 加密过程
        // 1. 使用MD5算法
        // 2. 使用随机的盐值
        // 3. 循环5次
        // 4. 盐的处理方式为:盐 + 原密码 + 盐 + 原密码 + 盐
        // 注意:因为使用了随机盐,盐值必须被记录下来,本次的返回结果使用$分隔盐与密文
        String salt = UUID.randomUUID().toString();
        String encodePassword = rawPassword;
        for (int i = 0; i < 5; i++) {
            encodePassword = DigestUtils.md5DigestAsHex((salt+encodePassword+salt+encodePassword+salt).getBytes());
        }
        return salt+"$" +encodePassword;
    }
}


    在【AdminServiceTests类:testAddNewSuccessfully方法】中修改新用户信息为601再测试:
    会发现控制台报错,原因是我们设计的表的字段存不下这个长度,打开数据库客户端来查看一下:
    use mall_ams;
    desc ams_admin;
    可以发现密码为64位:【password  | char(64)】确实存不下
    那我们使用随机盐时用substring方法截取一部分:
    将上边的随机盐那一句代码改为:
    【String salt = UUID.randomUUID().toString().substring(4);】
    从测试类AdminServiceTests的testAddNewSuccessfully方法处再次测试:
    
    发现在控制台输出:
: <==      Total: 0
: 此账号未被占用,准备向表中写入此次添加的管理员信息
: 加密前的密码:123456,加密后的密码:cfaf-401d-43e4-ba54-41b7dbeb7598$d3b8d7c59d59a36f07540855755a9c87
: 即将插入新的管理员信息: Admin(id=null, username=service-admin-601, password=cfaf-401d-43e4-ba54-41b7d
: ==>  Preparing: insert into ams_admin (username, password, nickname, avatar, phone, email,
: ==> Parameters: service-admin-601(String), cfaf-401d-43e4-ba54-41b7dbeb7598$d3b8d7c59d59a3
    可以看到加密的密码已经非常难以被破解,就不会存在安全问题了

    当然我们也可以将截取的substring方法改为replace替换:
    :  【String salt = UUID.randomUUID().toString().replace("-", "");】
    再次进行新用户的测试方法,发现依然可以将密码改善的很安全
———————————————————————————————————————————————————————————————————  
(11)我们首先在PasswordEncoder类中添加一个matches方法:

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

  
   
    再从DigestTests测试类中添加2个方法:
    【testPasswordEncoderEncode、testPasswordEncoderMatches】
    :
    @Test
    public void testPasswordEncoderEncode() {
        String rawPassword = "123456";
        System.out.println(rawPassword);

        for (int i = 0; i < 10; i++) {
            String encodedPassword = new PasswordEncoder().encode(rawPassword);
            System.out.println(encodedPassword);
        }
        // 0c40272db9e34a1a97ee58ed5e4aef96497c366877ec91c06e288b7621908bd9
        // 46089874b4bf44f2b8ddc02754903b15e85c0c4e64270661e29930b34eb78484
        // 79bcb6da567e4e58a9a63275abf4e143f4dc9c33556ad68955e7778ca6f0c863
        // 122e41d862064e70b97155a40b0574b0be78b0b23bab5a12467313934cab7a3a
        // 4ba58f18150e45dfb3c806d80dc863190a54c55fd0312ec4da87b8d67486d51e
        // 3e126fe0fefb4b73b78c5cadc02db443c2a765003ab766c00d5604f5c00f7b5d
        // 212b69275c96458aab82e04014e23c61ec15ea373aaf325e3d86822f315cea10
        // e5cbd0762ff642feb4207e3b7699f3c489a13fb43e5cf3523df06870da8652dc
        // 789670fd48cf460788f28f50aafeec4eac066e5b5294c638e406822fd8d6386e
        // 1a120503d2434840b84b67b5b795deaf0b3757bcb485546263c505b5e4578475
    }

    
    @Test
    public void testPasswordEncoderMatches() {
        String rawPassword = "123456";
        String encodedPassword = "1a120503d2434840b84b67b5b795deaf0b3757bcb485546263c505b5e4578475";
        System.out.println(new PasswordEncoder().matches(rawPassword, encodedPassword));
    }

    测试上边第二个方法,来比较加密后和原密码的结果可以看到,控制台输出为true
    


最终PasswordEncoder类的代码:
package cn.tedu.boot.demo.util;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import java.util.UUID;


//声明成一个组件,每次要用的时候就自动装配
@Component
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.控制器层开发
(1)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。
   
   ————>在springboot工程(即bootdemo)的pom.xml文件里将:  
       【spring-boot-starter直接改为spring-boot-starter-web】
       
        
        
            org.springframework.boot
            spring-boot-starter-web
        
———————————————————————————————————————————————————————————————————  
(2)先在项目的根包(cn.tedu.boot.demo)下创建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 {
    
}
———————————————————————————————————————————————————————————————————  
(3)由于已经决定了服务器端响应时,将响应JSON格式的字符串,
   为保证能够响应JSON格式的结果,处理请求的方法返回值应该是自定义的数据类型,
   则从此前学习的spring-mvc项目中找到JsonResult类及相关类型,复制到当前项目中来。
———————————————————————————————————————————————————————————————————  
(4)接下来,即可在AdminController中添加处理“增加管理员”的请求:

@Autowired
private IAdminService adminService;
// 注意:暂时使用@RequestMapping,不要使用@PostMapping,以便于直接在浏览器中测试
// http://localhost:8080/admins/add-new?username=root&password=1234
@RequestMapping("/add-new") 
public JsonResult 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错误。
———————————————————————————————————————————————————————————————————  
写到最后,当我们运行启动类后,
访问http://localhost:8080/admins/add-new?username=root&password=1234 ;
会在页面显示500错误,
控制台也会报用户名重复异常:UsernameDuplicateException
———————————————————————————————————————————————————————————————————  
关于处理异常,应该先在State中确保有每种异常对应的枚举值,
例如本次需要补充InsertException对应的枚举值:
————>在State类中添加500的枚举类型:【  ERR_INSERT(500);  】

那我们在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 {//GlobalExceptionHandler:全局异常处理程序
    @ExceptionHandler(ServiceException.class)
    public JsonResult handleServiceException(ServiceException e) {
        if (e instanceof UsernameDuplicateException) {
            return JsonResult.fail(State.ERR_USERNAME, "用户名错误!");
        } else {
            return JsonResult.fail(State.ERR_INSERT, "插入数据失败!");
        }
    }
}

———————————————————————————————————————————————————————————————————  

完成后,重新启动项目,运行启动类,
测试网站http://localhost:8080/admins/add-new?username=root&password=1234
当添加管理员时的用户名没有被占用时,将正常添加,当用户名已经被占用时,会根据处理异常的结果进行响应!

由于在统一处理异常的机制下,同一种异常,无论是在哪种业务中出现,
处理异常时的描述信息都是完全相同的,也无法精准的表达错误信息,这是不合适的!
另外,基于面向对象的“分工”思想,关于错误信息(异常对应的描述信息),
应该是由Service来描述,即“谁抛出谁描述”,
因为抛出异常的代码片段是最了解、最明确出现异常的原因的!
———————————————————————————————————————————————————————————————————  
为了更好的描述异常的原因,
应该在自定义的ServiceException和其子孙类异常中添加基于父类的全部构造方法(5个):

ServiceException添加的构造:
    public ServiceException() {
    }

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

    public ServiceException(String message, Throwable cause) {
        super(message, cause);
    }

    public ServiceException(Throwable cause) {
        super(cause);
    }

    public ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }

InsertException添加的构造:
    public InsertException() {
    }

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

    public InsertException(String message, Throwable cause) {
        super(message, cause);
    }

    public InsertException(Throwable cause) {
        super(cause);
    }

    public InsertException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }


UsernameDuplicateException添加的构造:
    public UsernameDuplicateException() {
    }

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

    public UsernameDuplicateException(String message, Throwable cause) {
        super(message, cause);
    }

    public UsernameDuplicateException(Throwable cause) {
        super(cause);
    }

    public UsernameDuplicateException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }

———————————————————————————————————————————————————————————————————  
然后,【在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("添加管理员失败,服务器忙,请稍后再次尝试!");
    }
}
———————————————————————————————————————————————————————————————————  
最后,在处理异常时,
可以在【GlobalExceptionHandler类中】:
调用异常对象的getMessage()方法获取抛出时封装的描述信息,
例如:

    @ExceptionHandler(ServiceException.class)
    public JsonResult handleServiceException(ServiceException e) {
        if (e instanceof UsernameDuplicateException) {
            return JsonResult.fail(State.ERR_USERNAME, e.getMessage());
        } else {
            return JsonResult.fail(State.ERR_INSERT, e.getMessage());
        }
    }
———————————————————————————————————————————————————————————————————  
完成后,启动测试类,
访问http://localhost:8080/admins/add-new?username=root&password=1234
则在页面中显示:
{"state":201,"message":"添加管理员失败,用户名(root)已经被占用!","data":null}

发现处理异常的说明已经变为上边的描述:"添加管理员失败,用户名(roo..."

此时我们在页面中指定新的用户名fanchuanqi:
http://localhost:8080/admins/add-new?username=fanchuanqi&password=1234 回车后,
页面显示:{"state":200,"message":null,"data":null}
再次回车后,页面显示:
{"state":201,"message":"添加管理员失败,用户名(fanchuanqi)已经被占用!","data":null}
——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——
↑↑↑↑↑↑↑↑↑↑↑↑  
说明:
完成后,再次重启项目,当用户名已经存在时,
可以显示在Service中描述的错误信息!
最后,当添加成功时,响应的JSON数据
例如:
{
    "state":200,
    "message":null,
    "data":null
}

当用户名冲突,添加失败时,响应的JSON数据例如:
{
    "state":201,
    "message":"添加管理员失败,用户名(liuguobin)已经被占用!",
    "data":null
}

可以看到,无论是成功还是失败,
响应的JSON中都包含了不必要的数据(为null的数据),
这些数据属性是没有必要响应到客户端的,如果需要去除这些不必要的值,
可以在对应的属性上使用注解进行配置,
例如:在JsonResult类中——————>注意,此解决方式不是最终解决方法:
@Data
public class JsonResult 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 implements Serializable {
    // ===== 原有其它代码 =====    
}



我们可以通过添加注解@JsonInclude来解决页面中为null的属性不显示,就不占用多余的流量了,
————>但不建议,注解加在当前类只会当前的类有效


即使添加在类上,也只对当前类的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

正确的配置例如:
——————>在配置文件application.yml中添加关于Jackson的配置:
注意在【# Spring系列框架的配置】下对应层级写【# 关于Jackson的配置】:
# Spring系列框架的配置
spring:
  # Profile配置
  profiles:
    # 激活Profile配置
    active: dev
  # 连接数据库的相关配置
  datasource:
    # 使用的数据库连接池类型
    type: com.alibaba.druid.pool.DruidDataSource
  # 关于Jackson的配置
  jackson:
    default-property-inclusion: non_null

最后,以上配置只是“默认”配置,如果在某些类型中还有不同的配置需求,
仍可以在类或属性上通过@JsonInclude进行配置。
———————————————————————————————————————————————————————————————————  
14.Spring Validation框架
:检查数据基本格式有效性的
此时我们发现,启动运行类后,
在网址输入:http://localhost:8080/admins/add-new 进行测试,
即使什么属性也没给,页面也会显示状态state=200 添加成功,这是不合理的.

当客户端向服务器提交请求时,
如果请求出现明显的问题(例如关键数据为null、字符串的长度不在可接受范围内、其它格式错误),
应该直接响应错误,而不是将明显错误的请求参数传递到Service!
:
关于判断错误,只有涉及数据库中的数据才能判断出结果的,都由Service进行判断,而基本的格式判断,都由Controller进行判断。

Validation框架是专门用于解决检查数据基本格式有效性的,最早并不是Spring系列的框架,目前,Spring Boot提供了更好的支持,所以,通常结合在一起使用。

在SpringBoot项目中,需要添加spring-boot-starter-validation依赖项:
在bootdemo工程的pom.xml文件里添加依赖:
        
        
            org.springframework.boot
            spring-boot-starter-validation
        
———————————————————————————————————————————————————————————————————          
首先,对需要检查数据格式的请求参数添加@Valid 或 @Validated注解(这两个注解没有区别),
在AdminController:
//@Valid:表示需要检查
public JsonResult addNew(@Validated AdminAddNewDTO adminAddNewDTO)
——————————————————————————————————————————————————————————————————— 
真正需要检查的是AdminAddNewDTO中各属性的值,
所以,接下来需要在此类的各属性上通过注解来配置检查的规则,例如:

在AdminAddNewDTO:
属性上添加@NotNull注解,此时用户名会自动检查不为null的,:
    @NotNull //验证规则:不允许为null
    private String username;

注意,一个注解只能作用于当前属性
运行启动类,
让输入的用户名为 null,测试:http://localhost:8080/admins/add-new?password=1234
此时页面显示:400

若不在用户名属性上添加@NotNull此注解,
测试上面这个网址,页面则显示成功:【{"state":200}】
———————————————————————————————————————————————————————————————————  
重启项目,
通过不提交用户名的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 errorsField 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异常,
则自行处理此异常即可!
:

在state类里添加新的枚举:【   ERR_BAD_REQUEST(400),   】

找到统一处理异常的类GlobalExceptionHandler:
添加新的处理异常的方法:
@ExceptionHandler(BindException.class)
public JsonResult handleBindException(BindException e) {
    return JsonResult.fail(State.ERR_BAD_REQUEST, e.getMessage());
}

注意BindException是在:org.springframework.validation.BindException;下
———————————————————————————————————————————————————————————————————  
完成后,再次重启项目,
继续使用为null的用户名提交请求时(http://localhost:8080/admins/add-new),
可以看到异常已经被处理,
此时,页面中的显示为:响应的JSON数据例如:

{
    "state":400,
    "message":"org.springframework.validation.BeanPropertyBindingResult: 
               2 errorsnField 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属性进行配置,例如:

在AdminAddNewDTO 中:添加属性上的注解的参数:
    // 验证规则为:不允许为null
    @NotNull(message = "添加管理员失败,请提交用户名!")
    private String username;

    @NotNull(message = "添加管理员失败,请提交密码!")
    private String password;

运行启动类,当浏览器地址不输入用户名和密码时,
测试:http://localhost:8080/admins/add-new
页面显示:
{"state":400,"message":"org....message [添加管理员失败,请提交用户名!]
 .... default message [添加管理员失败,请提交密码!]"}
———————————————————————————————————————————————————————————————————  
然后,在处理异常时,通过异常信息获取自定义的提示文本:

在GlobalExceptionHandler:
改动 handleBindException 方法:
    @ExceptionHandler(BindException.class)
    public JsonResult handleBindException(BindException e) {
        BindingResult bindingResult = e.getBindingResult();
        String defaultMessage = bindingResult.getFieldError().getDefaultMessage();

//        return JsonResult.fail(State.ERR_BAD_REQUEST, e.getMessage());
        return JsonResult.fail(State.ERR_BAD_REQUEST,defaultMessage);
    }

运行启动类,当浏览器地址不输入用户名和密码时,
测试:http://localhost:8080/admins/add-new
页面显示:{"state":400,"message":"添加管理员失败,请提交用户名!"}
若重复刷新,则页面的内容会变为:{"state":400,"message":"添加管理员失败,请提交密码!"}
当点击刷新页面按钮时 上面两句显示的内容自动切换!

总结:
再次运行,在不提交用户名和密码的情况下,
会随机的提示用户名或密码验证失败的提示文本中的某1条。
———————————————————————————————————————————————————————————————————  
在Validation框架中,还有其它许多注解,用于进行不同格式的验证,例如:
  @NotEmpty:只能添加在String类型上,不许为空字符串,例如""即视为空字符串
  @NotBlank:只能添加在String类型上,不允许为空白,
             例如普通的空格可视为空白,使用TAB键输入的内容也是空白,
             (虽然不太可能在此处出现)换行产生的空白区域也是空白
  @Size:限制大小
  @Min:限制最小值
  @Max:限制最大值
  @Range:可以配置min和max属性,同时限制最小值和最大值
  @Pattern:只能添加在String类型上,自行指定正则表达式进行验证
  其它

以上注解,包括@NotNull是允许叠加使用的,即允许在同一个参数属性上添加多个注解!

以上注解均可以配置`message`属性,用于指定验证失败的提示文本。
———————————————————————————————————————————————————————————————————  
通常:
	对于必须提交的属性,都会添加@NotNull
	对于数值类型的,需要考虑是否添加@Range(则不需要使用@Min和@Max)
	对于字符串类型,都添加@Pattern注解进行验证
15.解决跨域问题
  • 接下来我们的需求是:客户端发出请求,服务端响应结果

0.
(1)找到工程:vue-project-05:
(2)在电脑中路径:
D:soft1Java0soft1projectchengHhengJavafiveJavadocnote1VueCli
中找到vue-project-server-0.0.1.jar 文件,并启动
(3)在idea界面下边的Terminal处 用命令npm run serve 启动05工程,
在浏览器输入网址:http://localhost:8080/ 跳转到用户登录页面
点击超链接:进入管理页面后
可以看到管理页面:

(1)跨域问题:
在使用前后端分离的开发模式下,前端项目和后端项目可能是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接口的类),
并重写其中的方法,以允许指定条件的跨域访问,
例如:

在boot-demo下的cn.tedu.boot.demo下的config包下创建:SpringMvcConfig配置类
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("
@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 = "/admin/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()); // 打印日志
    }
}

执行此方法,
可以看到控制台输出的下列代码:正对应着我们上边方法中写的【那些参数】
MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /admins/login
       Parameters = {username=[admin001], password=[123456]}
          Headers = [Content-Type:"application/x-www-form-urlencoded;charset=UTF-8", Accept:"application/json"]
             Body = null ..................
——————————————————————————————————————————————————————————————————————————
(2)再添加一个【测试登录失败,因为找不到用户】的方法:

    
    @Sql({"classpath:truncate.sql"})
    @Test
    public void testLoginFailBecauseUserNotFound() throws Exception {
        // 准备测试数据,不需要封装
        String username = "admin001";
        String password = "123456";
        // 请求路径,不需要写协议(http)、服务器主机和端口号
        String url = "/admins/login";
        // 执行测试
        // 以下代码相对比较固定:
        mockMvc.perform( // ①执行发出请求
                MockMvcRequestBuilders.post(url) // 根据请求方式(post)决定调用的方法
                        .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(201)) // 预判响应的JSON结果中名为state的属性的值,注意:andExpect()方法到此结束
                .andDo( // ③需要执行某任务
                        //MockMvcResultHandlers: 模拟Mvc结果处理程序
                        MockMvcResultHandlers.print()); // 打印日志
    }

此时运行AdminControllerTests 整个类,可以发现:
控制台显示:
失败的方法可以运行

成功的方法是黄色的×号,报No value at JSON path "state"
可以看到控制台上边有条信息:
【MockHttpServletResponse:
           Status = 404
】
可以判断是网址写错了
——————————————————————————————————————————————————————————————————————————
(3)执行以上测试时,并不需要启动当前项目即可测试。

在执行以上测试时,响应的JSON中如果包含中文,可能会出现乱码(但注意在正式的类中不会),
需要在配置文件(application.properties或application.yml这类文件)中添加配置。

/1.在.properties文件中添加:
  server.servlet.encoding.force=true
  server.servlet.encoding.charset=utf-8

/2.在.yml文件中:
server:
  servlet:
    encoding:
      force: true
      charset: utf-8
19.Knife4j(提供在线API文档的框架)

(1)Knife4j是一款可以提供在线API文档的框架,是基于Swagger框架实现的。

   在Spring Boot项目中:bootdemo工程:pom.xml中:
   使用Knife4j需要添加依赖knife4j-spring-boot-starter:
   
   
   
      com.github.xiaoymin
      knife4j-spring-boot-starter
      2.0.9
   
——————————————————————————————————————————————————————————————————————————
(2)然后,需要添加配置,
   则在boot-demo项目的cn.tedu.boot.demo.config包下创建Knife4jConfig类:

package cn.tedu.boot.demo.config;
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver;
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;
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfig {
    
    private String basePackage = "cn.tedu.boot.demo.controller";
    
    private String groupName = "product";
    
    private String host = "http://java.tedu.cn";
    
    private String title = "酷鲨商城在线API文档--商品管理";
    
    private String description = "酷鲨商城在线API文档--商品管理";
    
    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;

    @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();
    }
}


   注意:
   必须修改以上配置中的包名,保证是【当前项目中控制器类所在的包】!
   其它各项均可不修改,以上配置代码可以从Knife4j的官网找到!
——————————————————————————————————————————————————————————————————————————
(3)最后,还需要在配置文件 application.yml 中:开启Knife4j的增强模式:

# Knife4j配置
knife4j:
  # 是否开启增强模式
  enable: true
  
完成后,启动bootdemo项目,
在浏览器中访问 http://localhost:8080/doc.html 即可查看当前项目的API文档。

若启动项目失败,则检查pom.xml的springboot版本是不是2.5.9;
若是2.6.0版本过高也导致无法正常启动项目;
若刷新maven依然不行,那就电脑关机重启即可!

在AdminController中:将login的注解@RequestMapping改为:@PostMapping  
再次启动项目,可以在访问网站中做测试;

(4)在控制器类AdminController 上添加@Api注解,
   并配置tags属性,可以指定模块名称,例如:

@Api(tags = "管理员管理模块")  // 新增
@RestController
@RequestMapping(value = "/admins", produces = "application/json; charset=utf-8")
public class AdminController {    
    // ===== 原有其它代码 =====
}
——————————————————————————————————————————————————————————————————————————
(5)在AdminController:处理请求的方法上:
   添加@ApiOperation注解可以配置业务名称,例如:

@ApiOperation("管理员登录") // 新增
@PostMapping("/login")
public JsonResult login(@Validated AdminLoginDTO adminLoginDTO) {
    AdminSimpleVO adminSimpleVO = adminService.login(adminLoginDTO);
    return JsonResult.ok(adminSimpleVO);
}

方法不一定是先运行哪一个的,所以在页面中显示的顺序是随机的:

(6)当需要指定各业务在API文档中的显示顺序时,
   可以在处理请求的方法上添加@ApiOperationSupport注解,
   配置此注解的order属性,
   最终在显示API文档时,
   会根据order属性值【升序】排列,
   例如:
   
在AdminController:处理请求的方法上:
@ApiOperation("管理员登录")
@ApiOperationSupport(order = 900) // 新增
@PostMapping("/login")
public JsonResult login(@Validated AdminLoginDTO adminLoginDTO) {
    AdminSimpleVO adminSimpleVO = adminService.login(adminLoginDTO);
    return JsonResult.ok(adminSimpleVO);
}

:如图所示:

(7)通常,
   建议以上配置的order值至少是2位的数字,并且有预留位置,
   例如10~19之间的都是增加数据的业务,
   20~29之间的都是删除数据的业务,
   30~39之间都是修改数据的业务,
   40~49之间都是查询数据的业务。
————————————————————————————————————————————————————————————————
(8)
如果控制器处理请求的方法的参数是自定义的封装类型,
可以在封装类型的属性上添加:
  @ApiModelProperty来配置参数在文档中的显示,例如:
  
package cn.tedu.boot.demo.pojo.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
@Data
public class AdminLoginDTO implements Serializable {
    @ApiModelProperty(value = "用户名") // 配置参数名
    private String username;
    @ApiModelProperty("密码") // 配置参数名
    private String password;
}
————————————————————————————————————————————————————————————————
(9)
以上@ApiModelProperty除了可以配置参数在API文档中显示的名称以外,
还可以配置是否必须,例如:

@ApiModelProperty(value = "用户名", required = true)
————————————————————————————————————————————————————————————————
(10)
另外,还可以配置参数类型等,但是,并不是必须配置,通常框架可以正常自动识别。

对于部分名称可能比较特殊(一般人直接看不懂)的属性,
或者对值的规范性要求比较明确(例如某些取值为0或1)的属性,
可以列举示例,使得查看API文档的人可以参考,例如:

@ApiModelProperty(value = "用户名", required = true, example = "admin")
————————————————————————————————————————————————————————————————
(11)
除以配置请求参数以外,此属性还可以用于响应结果的类型,例如:

public class JsonResult implements Serializable {
    @ApiModelProperty("业务状态码")
    private Integer state;

    @ApiModelProperty("消息")
    private String message;

    @ApiModelProperty("数据")
    private T data;    
    // ......
————————————————————————————————————————————————————————————————
(12)
如果以上private T data;的实际值也需要添加说明,
则在对应的类的属性上继续使用@ApiModelProperty配置即可!
需要注意:此处data属性可以是任意数据类型,必须声明为泛型,
         不可以是Object,否则将无法应用@ApiModelProperty的配置。

另外,当添加在响应的类型的属性上时,
还可以在@ApiModelProperty注解中配置position属性,
用于设置各属性在响应的JSON中的显示顺序,例如:

@ApiModelProperty(value = "业务状态码", position = 5)
———————————————— 6、酷鲨商城:建议idea用2021.1.3版本 0.作业:
《作业1:——————>  添加管理员--持久层》

作业内容:
请参考csmall-product模块的模式,
在根项目下创建csmall-admin子模块,
并在csmall-admin下创建csmall-admin-webapi子模块,
在csmall-admin-webapi中实现“添加管理员”的持久层操作,
具体步骤至少包含:
  1.在csmall-admin-webapi中添加必要的持久层配置
  2.在csmall-pojo中添加Admin实体类型,及必要的其它POJO类型,例如VO类型
  3.在csmall-admin-webapi中的持久层实现“插入管理员数据”的功能
  4.在csmall-admin-webapi的测试中,自行添加必要的SQL脚本,并在测试中,
    结合SQL脚本实现测试,以检验“插入管理员数据”是否实现了预期的效果
           ————————>《已完成》<————————
——————————————————————————————————————————————————————————————
00.创建project工程

(1)idea File——>new Project

(2)勾选下面5个选项:

  • Developer Tools:【Lombok】
  • Web:【Spring Web】
  • SQL:【Mybatis Framework、MySQL Driver】
  • I/O:【Validation】

(3)将原来的版本2.7.0改为:————>2.5.9 点击刷新按钮:

(4)改动 pom.xml文件:
最终是下面这样的:



    4.0.0
    
    
        org.springframework.boot
        spring-boot-starter-parent
        2.5.9
         
    

    
    cn.tedu
    csmall-server
    0.0.1-SNAPSHOT


    
    
        1.8
        2.5.9
        2.2.2
        8.0.28
        1.18.22
    

        
            
            
                org.springframework.boot
                spring-boot-starter-validation
                ${spring-boot.version}
            
            
            
                org.springframework.boot
                spring-boot-starter-web
                ${spring-boot.version}
            
            
            
                org.mybatis.spring.boot
                mybatis-spring-boot-starter
                ${mybatis-boot.version}
            
            
            
                mysql
                mysql-connector-java
                runtime
                ${mysql.version}
            
            
            
                org.projectlombok
                lombok
                true
                ${lombok.version}
            
            
            
                org.springframework.boot
                spring-boot-starter-test
                test
                ${spring-boot.version}
            
        


01.关于此项目
(1)
此项目是一个自营性质电商类型的项目。

当前目标是设计后台管理相关功能。
02.关于项目的开发流程
开发项目的标准流程应该有:需求分析、可行性分析、总体设计、详细设计等。

建议课后学习《软件工程》。

在具体开发时,应该先创建数据库、数据表,然后创建项目进行开发。
03.创建数据库与数据表
(1)
创建mall_pms数据库:
——————>   CREATE DATABASE mall_pms;
——————————————————————————————————————————————————————————————————
(2)
在此数据库中创建数据表:

-- 数据库:mall_pms

-- 相册表:创建数据表
drop table if exists pms_album;
create table pms_album
(
    id           bigint unsigned auto_increment comment '记录id',
    name         varchar(50)      default null comment '相册名称',
    description  varchar(255)     default null comment '相册简介',
    sort         tinyint unsigned default 0 comment '自定义排序序号',
    gmt_create   datetime         default null comment '数据创建时间',
    gmt_modified datetime         default null comment '数据最后修改时间',
    primary key (id)
) comment '相册' charset utf8mb4;

-- 相册表:为相册名称字段添加索引
create index idx_album_name on pms_album (name);

-- 图片表:创建数据表
drop table if exists pms_picture;
create table pms_picture
(
    id           bigint unsigned auto_increment comment '记录id',
    album_id     bigint unsigned   default null comment '相册id',
    url          varchar(255)      default null comment '图片url',
    description  varchar(255)      default null comment '图片简介',
    width        smallint unsigned default null comment '图片宽度,单位:px',
    height       smallint unsigned default null comment '图片高度,单位:px',
    is_cover     tinyint unsigned  default 0 comment '是否为封面图片,1=是,0=否',
    sort         tinyint unsigned  default 0 comment '自定义排序序号',
    gmt_create   datetime          default null comment '数据创建时间',
    gmt_modified datetime          default null comment '数据最后修改时间',
    primary key (id)
) comment '图片' charset utf8mb4;

-- 品牌表:创建数据表
drop table if exists pms_brand;
create table pms_brand
(
    id                     bigint unsigned auto_increment comment '记录id',
    name                   varchar(50)      default null comment '品牌名称',
    pinyin                 varchar(50)      default null comment '品牌名称的拼音',
    logo                   varchar(255)     default null comment '品牌logo的URL',
    description            varchar(255)     default null comment '品牌简介',
    keywords               varchar(255)     default null comment '关键词列表,各关键词使用英文的逗号分隔',
    sort                   tinyint unsigned default 0 comment '自定义排序序号',
    sales                  int unsigned     default 0 comment '销量(冗余)',
    product_count          int unsigned     default 0 comment '商品种类数量总和(冗余)',
    comment_count          int unsigned     default 0 comment '买家评论数量总和(冗余)',
    positive_comment_count int unsigned     default 0 comment '买家好评数量总和(冗余)',
    enable                 tinyint unsigned default 0 comment '是否启用,1=启用,0=未启用',
    gmt_create             datetime         default null comment '数据创建时间',
    gmt_modified           datetime         default null comment '数据最后修改时间',
    primary key (id)
) comment '品牌' charset utf8mb4;

-- 品牌表:为品牌名称字段添加索引
create index idx_brand_name on pms_brand (name);

-- 类别表:创建数据表
drop table if exists pms_category;
create table pms_category
(
    id           bigint unsigned auto_increment comment '记录id',
    name         varchar(50)      default null comment '类别名称',
    parent_id    bigint unsigned  default 0 comment '父级类别id,如果无父级,则为0',
    depth        tinyint unsigned default 1 comment '深度,最顶级类别的深度为1,次级为2,以此类推',
    keywords     varchar(255)     default null comment '关键词列表,各关键词使用英文的逗号分隔',
    sort         tinyint unsigned default 0 comment '自定义排序序号',
    icon         varchar(255)     default null comment '图标图片的URL',
    enable       tinyint unsigned default 0 comment '是否启用,1=启用,0=未启用',
    is_parent    tinyint unsigned default 0 comment '是否为父级(是否包含子级),1=是父级,0=不是父级',
    is_display   tinyint unsigned default 0 comment '是否显示在导航栏中,1=启用,0=未启用',
    gmt_create   datetime         default null comment '数据创建时间',
    gmt_modified datetime         default null comment '数据最后修改时间',
    primary key (id)
) comment '类别' charset utf8mb4;

-- 类别表:为类别名称字段添加索引
create index idx_category_name on pms_category (name);

-- 品牌类别关联表:创建数据表
drop table if exists pms_brand_category;
create table pms_brand_category
(
    id           bigint unsigned auto_increment comment '记录id',
    brand_id     bigint unsigned default null comment '品牌id',
    category_id  bigint unsigned default null comment '类别id',
    gmt_create   datetime        default null comment '数据创建时间',
    gmt_modified datetime        default null comment '数据最后修改时间',
    primary key (id)
) comment '品牌与类别关联' charset utf8mb4;

-- 属性表:创建数据表
drop table if exists pms_attribute;
create table pms_attribute
(
    id                 bigint unsigned auto_increment comment '记录id',
    template_id        bigint unsigned  default null comment '所属属性模版id',
    name               varchar(50)      default null comment '属性名称',
    description        varchar(255)     default null comment '简介(某些属性名称可能相同,通过简介补充描述)',
    type               tinyint unsigned default 0 comment '属性类型,1=销售属性,0=非销售属性',
    input_type         tinyint unsigned default 0 comment '输入类型,0=手动录入,1=单选,2=多选,3=单选(下拉列表),4=多选(下拉列表)',
    value_list         varchar(255)     default null comment '备选值列表',
    unit               varchar(50)      default null comment '计量单位',
    sort               tinyint unsigned default 0 comment '自定义排序序号',
    is_allow_customize tinyint unsigned default 0 comment '是否允许自定义,1=允许,0=禁止',
    gmt_create         datetime         default null comment '数据创建时间',
    gmt_modified       datetime         default null comment '数据最后修改时间',
    primary key (id)
) comment '属性' charset utf8mb4;

-- 属性模版表:创建数据表
drop table if exists pms_attribute_template;
create table pms_attribute_template
(
    id           bigint unsigned auto_increment comment '记录id',
    name         varchar(50)      default null comment '属性模版名称',
    pinyin       varchar(50)      default null comment '属性模版名称的拼音',
    keywords     varchar(255)     default null comment '关键词列表,各关键词使用英文的逗号分隔',
    sort         tinyint unsigned default 0 comment '自定义排序序号',
    gmt_create   datetime         default null comment '数据创建时间',
    gmt_modified datetime         default null comment '数据最后修改时间',
    primary key (id)
) comment '属性模版' charset utf8mb4;

-- 属性模版表:为属性模版名称字段添加索引
create index idx_attribute_template_name on pms_attribute_template (name);

-- 类别与属性模版关联表:创建数据表
drop table if exists pms_category_attribute_template;
create table pms_category_attribute_template
(
    id                    bigint unsigned auto_increment comment '记录id',
    category_id           bigint unsigned default null comment '类别id',
    attribute_template_id bigint unsigned default null comment '属性模版id',
    gmt_create            datetime        default null comment '数据创建时间',
    gmt_modified          datetime        default null comment '数据最后修改时间',
    primary key (id)
) comment '类别与属性模版关联' charset utf8mb4;

-- SPU(Standard Product Unit)表:创建数据表
drop table if exists pms_spu;
create table pms_spu
(
    id                     bigint unsigned not null comment '记录id',
    name                   varchar(50)      default null comment 'SPU名称',
    type_number            varchar(50)      default null comment 'SPU编号',
    title                  varchar(255)     default null comment '标题',
    description            varchar(255)     default null comment '简介',
    list_price             decimal(10, 2)   default null comment '价格(显示在列表中)',
    stock                  int unsigned     default 0 comment '当前库存(冗余)',
    stock_threshold        int unsigned     default 0 comment '库存预警阈值(冗余)',
    unit                   varchar(50)      default null comment '计件单位',
    brand_id               bigint unsigned  default null comment '品牌id',
    brand_name             varchar(50)      default null comment '品牌名称(冗余)',
    category_id            bigint unsigned  default null comment '类别id',
    category_name          varchar(50)      default null comment '类别名称(冗余)',
    attribute_template_id  bigint unsigned  default null comment '属性模版id',
    album_id               bigint unsigned  default null comment '相册id',
    pictures               varchar(500)     default null comment '组图URLs,使用JSON数组表示',
    keywords               varchar(255)     default null comment '关键词列表,各关键词使用英文的逗号分隔',
    tags                   varchar(255)     default null comment '标签列表,各标签使用英文的逗号分隔,原则上最多3个',
    sales                  int unsigned     default 0 comment '销量(冗余)',
    comment_count          int unsigned     default 0 comment '买家评论数量总和(冗余)',
    positive_comment_count int unsigned     default 0 comment '买家好评数量总和(冗余)',
    sort                   tinyint unsigned default 0 comment '自定义排序序号',
    is_deleted             tinyint unsigned default 0 comment '是否标记为删除,1=已删除,0=未删除',
    is_published           tinyint unsigned default 0 comment '是否上架(发布),1=已上架,0=未上架(下架)',
    is_new_arrival         tinyint unsigned default 0 comment '是否新品,1=新品,0=非新品',
    is_recommend           tinyint unsigned default 0 comment '是否推荐,1=推荐,0=不推荐',
    is_checked             tinyint unsigned default 0 comment '是否已审核,1=已审核,0=未审核',
    check_user             varchar(50)      default null comment '审核人(冗余)',
    gmt_check              datetime         default null comment '审核通过时间(冗余)',
    gmt_create             datetime         default null comment '数据创建时间',
    gmt_modified           datetime         default null comment '数据最后修改时间',
    primary key (id)
) comment 'SPU(Standard Product Unit)' charset utf8mb4;

-- SPU详情表:创建数据表
drop table if exists pms_spu_detail;
create table pms_spu_detail
(
    id           bigint unsigned auto_increment comment '记录id',
    spu_id       bigint unsigned default null comment 'SPU id',
    detail       text            default null comment 'SPU详情,应该使用HTML富文本,通常内容是若干张图片',
    gmt_create   datetime        default null comment '数据创建时间',
    gmt_modified datetime        default null comment '数据最后修改时间',
    primary key (id)
) comment 'SPU详情' charset utf8mb4;

-- SKU(Stock Keeping Unit)表:创建数据表
drop table if exists pms_sku;
create table pms_sku
(
    id                     bigint unsigned not null comment '记录id',
    spu_id                 bigint unsigned  default null comment 'SPU id',
    title                  varchar(255)     default null comment '标题',
    bar_code               varchar(255)     default null comment '条型码',
    attribute_template_id  bigint unsigned  default null comment '属性模版id',
    specifications         varchar(2500)    default null comment '全部属性,使用JSON格式表示(冗余)',
    album_id               bigint unsigned  default null comment '相册id',
    pictures               varchar(500)     default null comment '组图URLs,使用JSON格式表示',
    price                  decimal(10, 2)   default null comment '单价',
    stock                  int unsigned     default 0 comment '当前库存',
    stock_threshold        int unsigned     default 0 comment '库存预警阈值',
    sales                  int unsigned     default 0 comment '销量(冗余)',
    comment_count          int unsigned     default 0 comment '买家评论数量总和(冗余)',
    positive_comment_count int unsigned     default 0 comment '买家好评数量总和(冗余)',
    sort                   tinyint unsigned default 0 comment '自定义排序序号',
    gmt_create             datetime         default null comment '数据创建时间',
    gmt_modified           datetime         default null comment '数据最后修改时间',
    primary key (id)
) comment 'SKU(Stock Keeping Unit)' charset utf8mb4;

-- SKU规格参数表(存储各SKU的属性与值,即规格参数):创建数据表
drop table if exists pms_sku_specification;
create table pms_sku_specification
(
    id              bigint unsigned auto_increment comment '记录id',
    sku_id          bigint unsigned  default null comment 'SKU id',
    attribute_id    bigint unsigned  default null comment '属性id',
    attribute_name  varchar(50)      default null comment '属性名称',
    attribute_value varchar(50)      default null comment '属性值',
    unit            varchar(10)      default null comment '自动补充的计量单位',
    sort            tinyint unsigned default 0 comment '自定义排序序号',
    gmt_create      datetime         default null comment '数据创建时间',
    gmt_modified    datetime         default null comment '数据最后修改时间',
    primary key (id)
) comment 'SKU数据' charset utf8mb4;

-- -------------------------- --
-- 以下是插入测试数据及一些测试访问 --
-- -------------------------- --

-- 品牌表:插入测试数据
insert into pms_brand (name, pinyin, description, keywords, enable)
values ('华为', 'huawei', '华为专注网络设备三十年', '华为,huawei,mate,magicbook', 1),
       ('小米', 'xiaomi', '小米,为发烧而生', '小米,xiaomi,发烧', 1),
       ('苹果', 'pingguo', '苹果,全球知名品牌', '苹果,apple,pingguo,iphone,mac', 1);

-- 类别表:插入测试数据
insert into pms_category (name, parent_id, depth, is_parent, keywords, enable, is_display)
values ('手机 / 运营商 / 数码', 0, 1, 1, null, 1, 1),
       ('手机通讯', 1, 2, 1, '手机,电话', 1, 1),
       ('智能手机', 2, 3, 0, null, 1, 1),
       ('非智能手机', 2, 3, 0, null, 1, 1),
       ('电脑 / 办公', 0, 1, 1, null, 1, 1),
       ('电脑整机', 5, 2, 1, '电脑,计算机,微机,服务器,工作站', 1, 1),
       ('电脑配件', 5, 2, 1, '配件,组装,CPU,内存,硬盘', 1, 1),
       ('笔记本', 6, 3, 0, '电脑,笔记本,微机,便携', 1, 1),
       ('台式机 / 一体机', 6, 3, 0, '台式机,一体机', 1, 1);

-- 品牌类别表:插入测试数据
insert into pms_brand_category (brand_id, category_id)
values (1, 3),
       (2, 3),
       (3, 3),
       (1, 8),
       (2, 8),
       (3, 8),
       (1, 9),
       (3, 9);

-- 关联测试查询:各品牌有哪些类别的产品
select pms_brand_category.id, pms_brand.name, pms_category.name
from pms_brand_category
         left join pms_brand
                   on pms_brand_category.brand_id = pms_brand.id
         left join pms_category
                   on pms_brand_category.category_id = pms_category.id
order by pms_brand.pinyin;

-- 属性表:插入测试数据
insert into pms_attribute (name, description, type, input_type, value_list, unit, is_allow_customize)
values ('屏幕尺寸', '智能手机屏幕尺寸', 0, 1, '6.1,6.3', '英寸', 1),
       ('屏幕尺寸', '笔记本电脑屏幕尺寸', 0, 1, '14,15', '英寸', 1),
       ('颜色', '智能手机颜色', 0, 1, '黑色,金色,白色', null, 1),
       ('颜色', '衬衣颜色', 0, 1, '白色,蓝色,灰色,黑色', null, 1),
       ('运行内存', '智能手机运行内存', 0, 1, '4,8,16', 'GB', 1),
       ('CPU型号', '智能手机CPU型号', 0, 1, '骁龙870,骁龙880', null, 1),
       ('机身毛重', '智能手机机身毛重', 0, 0, null, 'g', 0),
       ('机身存储', '智能手机机身存储', 0, 1, '64,128,256,512', 'GB', 0),
       ('操作系统', '智能手机操作系统', 0, 1, 'Android,iOS', null, 0),
       ('操作系统', '电脑操作系统', 0, 1, '无,Windows 7,Windows 10,Ubuntu,Mac OS', null, 0);

-- 属性模版表:插入测试数据
insert into pms_attribute_template (name, pinyin, keywords)
values ('智能手机', 'zhinengshouji', '手机'),
       ('服装-上身', 'fuzhuang', '服装,上衣'),
       ('服装-裤子', 'fuzhuang', '服装,裤'),
       ('笔记本电脑', 'bijibendiannao', '电脑,笔记本'),
       ('台式电脑', 'taishidiannao', '电脑,台式电脑,台式机');

-- 相册表:插入测试数据
insert into pms_album (name, description)
values ('iPhone 13', null),
       ('Mi 11 Ultra', null);

-- 图片表:插入测试数据
insert into pms_picture (album_id, url, description, width, height)
values (1, '模拟数据:iPhone 13图片URL-1', null, 1024, 768),
       (1, '模拟数据:iPhone 13图片URL-2', null, 1024, 768),
       (1, '模拟数据:iPhone 13图片URL-3', null, 1024, 768),
       (1, '模拟数据:iPhone 13图片URL-4', null, 1024, 768),
       (1, '模拟数据:iPhone 13图片URL-5', null, 1024, 768),
       (2, '模拟数据:Mi 11 Ultra图片URL-1', null, 1024, 768),
       (2, '模拟数据:Mi 11 Ultra图片URL-2', null, 1024, 768),
       (2, '模拟数据:Mi 11 Ultra图片URL-3', null, 1024, 768),
       (2, '模拟数据:Mi 11 Ultra图片URL-4', null, 1024, 768),
       (2, '模拟数据:Mi 11 Ultra图片URL-5', null, 1024, 768);

-- SPU表:插入测试数据
insert into pms_spu (id, name, type_number, title, description, list_price, stock, stock_threshold, unit, brand_id,
                     brand_name, category_id, category_name, keywords, tags)
values (202112010000001, 'iPhone 13', 'A2404', '苹果手机iPhone 13(A2404)', '2021年新款,全网首发',
        5199.99, 5000, 20, '部', 3, '苹果', 3, '智能手机', 'ip13,iPhone13,苹果13', '20w快充,NFC,无线充电'),
       (202112010000002, '小米11 Ultra', 'M112021', '小米11 Ultra(M112021)', '2021年最新旗舰机',
        5899.99, 8000, 20, '部', 2, '小米', 3, '智能手机', 'mi11,xiaomi11,ultra', '67w快充,1亿像素,5000毫安电池');

-- SPU详情表:插入测试数据
insert into pms_spu_detail (spu_id, detail)
values (1, '
iPhone 13的详情HTML
'), (2, '
小米11 Ultra的详情HTML
'); -- SKU(Stock Keeping Unit)表:插入测试数据 insert into pms_sku (id, spu_id, title, attribute_template_id, specifications, price, stock, stock_threshold) values (202112010000001, 2, '2021年新款,小米11 Ultra黑色512G,16G超大内存120Hz高刷67w快充', 1, '{"attributes":[{"id":1,"name":"屏幕尺寸","value":"6.1寸"},{"id":3,"name":"颜色","value":"黑色"},{"id":5,"name":"运行内存","value":"16GB"}]}', 6999.99, 3000, 50), (202112010000002, 2, '2021年新款,小米11 Ultra白色512G,8G超大内存120Hz高刷67w快充', 1, '{"attributes":[{"id":1,"name":"屏幕尺寸","value":"6.1寸"},{"id":3,"name":"颜色","value":"白色"},{"id":5,"name":"运行内存","value":"8GB"}]}', 6499.99, 3000, 50); -- SKU规格参数表(存储各SKU的属性与值,即规格参数):插入测试数据 insert into pms_sku_specification (sku_id, attribute_id, attribute_name, attribute_value, unit) values (1, 1, '屏幕尺寸', '6.1', '寸'), (1, 3, '颜色', '黑色', null), (1, 5, '运行内存', '16', 'GB'), (2, 1, '屏幕尺寸', '6.1', '寸'), (2, 3, '颜色', '白色', null), (2, 5, '运行内存', '8', 'GB'); -- 查看数据表结构 desc pms_album; desc pms_picture; desc pms_category; desc pms_brand; desc pms_brand_category; desc pms_attribute; desc pms_attribute_template; desc pms_spu; desc pms_spu_detail; desc pms_sku; desc pms_sku_specification;
04.关于Project与Module
Project:项目 / 工程

Module:模块

在较大规范的项目开发中,可能会把代码区分为多个Module进行开发,
即某1个Project中可能有多个Module,各Module允许独立开发、独立运行,
并且,可以在Project或某个父级Module中,对其子级的依赖项等部分的内容进行统筹管理。

需要注意:
并不是每个Module都是可以独立运行的,
在开发实践中,如果某些代码是多个Module都需要使用的,可以把这部分代码写在专门的Module中,
其它Module依赖这个专门的Module即可(就像添加某个依赖项一样)。

当使用Project结合多个Module开发时,Project基本上不需要编写任何与功能相关的代码,
只需要配置好pom.xml即可。
05.创建Project并在pom.xml中管理依赖:
(1)
上边【00.创建project工程】已经完成下边的创建:
使用Spring Boot创建向导来创建Project,相关参数:
   -Spring Boot父工程版本:2.5.9
   -groupId:cn.tedu
   -artifactId:csmall-server
   -version:0.0.1-SNAPSHOT
——————————————————————————————————————————————————————————————————
(2)
由于Project并不需要运行(也不能运行),
对于各依赖项,只需要管理即可,并不需要实际的添加!

在Project的pom.xml中:
如果直接使用节点配置依赖项,则每个Module可以直接使用这些依赖项!
但是,不推荐这样使用,
因为Spring Boot的许多依赖都是支持自动配置的,
如果在Module中自带了不需要使用的依赖项,反而容易导致错误,
即使不会导致错误,也会因为自带了不需要使用的依赖项而导致最终的目标文件大。

通常,
会将依赖项的代码放在节点中,
  - 例:
  - 将酷鲨商城pom.xml中的:
  - 【中的依赖都放到刚写的节点中】
  - 此时src/main/java/cn/tedu/csmall下的启动类会各种爆红!
  - 
则在之下的依赖项都不会默认出现在当前Project及各子级的Module中,
其主要作用是配置各依赖项的版本,
接下来,无论是在当前Project还是子级Module中,
使用添加依赖项时,都可以不指定节点来配置版本,
默认使用的就是在中配置的版本。

——————————————————————————————————————————————————————————————————
(3)
下面的步骤在【00.创建project】中已经完成:

为了在当前pom.xml集中管理各依赖的版本号,
推荐在中添加一些“变量”————> 用于配置版本号,
在和中添加依赖时,
都可以引用此处的“变量”表示版本,
例如:


    1.8
    2.5.9
    2.2.2
    8.0.28
    1.18.22

——————————————————————————————————————————————————————————————————
(4)
另外,
由于Project并不实现的编写需要运行的功能代码,更不会直接编译打包或运行,
所以,在Project的pom.xml中的节点是不必要的!
若节点报错,就删掉!

最后,把Project的src全部删除!
06.商品管理模块项目 (1)创建根级module:csmall-product
应该在Project下创建某个Module,
用于开发商品管理相关的功能,
为了便于过渡到后续将要使用的微服务架构(需要将业务逻辑层的接口声明在专门的Module中,
     							    便于被其它微服务Module依赖)
,
商品管理的Module需要再细分为2个,所以,目前项目结构应该是:

My-csmall-server[:父级project]
	csmall-product
		csmall-product-webapi
		csmall-product-service

(1.1)①:csmall-product:

(1.1):②因为【csmall-product】中只是为了包含上边两个项目,所以在下边的图片中什么也不勾选:

(1.1)③:在【My-csmall-server的pom.xml】中:

    cn.tedu
    csmall-server
    0.0.1-SNAPSHOT
    
将上边这几行代码复制到新创建的模块csmall-product的pom.xml中:

    
    
        cn.tedu
        csmall-server
        0.0.1-SNAPSHOT
    
———————————————————————————————————————————————————————————————
再将【csmall-product的pom.xml中】没用的节点删除,
最终【pom.xml】中全部的代码为:



    4.0.0

    
    
        cn.tedu
        csmall-server
        0.0.1-SNAPSHOT
    

    
    cn.tedu
    csmall-product
    0.0.1-SNAPSHOT

(2)
当存在Project和Module的“父子关系” 或 后续可能存在Module与Module的“父子关系”时,
应该在父级的My-csmall-server中:pom.xml中添加:


pom
——————————————————————————————————————————————————————————————————
(3)
并且,在父级【My-csmall-server中:pom.xml中】,
通过节点配置各子级Module,例如:

    
    
        csmall-product
    
——————————————————————————————————————————————————————————————————
(4)
新创建的【模块csmall-product下的src文件夹】必须删! 因为不删就报错!,
只留下【csmall-product.iml、pom.xml、.gitignore】这三个即可!
(2)csmall-product下:创建csmall-product-webapi

(1):

创建Module的操作步骤为:
  - 在My-csmall-server下创建csmall-product,
  - 再在csmall-product下创建csmall-product-webapi。
     创建Module时所选属性:
     -Name:csmall-product-webapi
     -Group:cn.tedu
     -Artifact:csmall-product-webapi
     -Package name:cn.tedu.csmall.product.webapi  注意加"."点
     -Java:8
     -点击Next
     -什么也不勾选,点击Finish即可



下面什么也不用勾选:

正确的项目结构:

(3)调整3个pom.xml文件
  • 当Project和各Module创建出来后,调整各pom.xml文件。
(4.1)
最终,My-csmall-server根级Project的pom.xml为:



    4.0.0

    
    
        org.springframework.boot
        spring-boot-starter-parent
        2.5.9
         
    

    
    cn.tedu
    csmall-server
    0.0.1-SNAPSHOT

    
    pom

    
    
        csmall-product
    

    
    
        1.8
        2.5.9
        2.2.2
        8.0.28
        1.18.22
        1.1.20
    

    
    
        
            
            
                org.springframework.boot
                spring-boot-starter-validation
                ${spring-boot.version}
            
            
            
                org.springframework.boot
                spring-boot-starter-web
                ${spring-boot.version}
            
            
            
                org.mybatis.spring.boot
                mybatis-spring-boot-starter
                ${mybatis-boot.version}
            
            
            
                mysql
                mysql-connector-java
                runtime
                ${mysql.version}
            
            
            
                org.projectlombok
                lombok
                true
                ${lombok.version}
            
            
            
                com.alibaba
                druid
                ${druid.version}
            
            
            
                org.springframework.boot
                spring-boot-starter-test
                test
                ${spring-boot.version}
            
        
    


——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	
(4.2)
而csmall-product的pom.xml为:



    4.0.0

    
    
        cn.tedu
        csmall-server
        0.0.1-SNAPSHOT
    

    
    cn.tedu
    csmall-product
    0.0.1-SNAPSHOT

    
    pom

    
    
        csmall-product-webapi
    


——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	
(4.3)
最后,在csmall-product-webapi的pom.xml为:
添加Druid数据库连接池依赖;并在父级My-csmall-server的pom.xml中指定版本号: 
  1.1.20



    4.0.0


    
    
        cn.tedu
        csmall-product
        0.0.1-SNAPSHOT
    

    
    cn.tedu
    csmall-product-webapi
    0.0.1-SNAPSHOT

    
    
        
        
            org.springframework.boot
            spring-boot-starter-validation
        
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
        
        
        
            mysql
            mysql-connector-java
            runtime
        
        
        
            org.projectlombok
            lombok
            true
        
        
        
            com.alibaba
            druid
        
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
    

此时,启动CsmallProductWebapiApplication 类,依然无法启动,可以发现控制台报url错误:
  'url' attribute is not specified and no embedded datasource could be configured.

我们在application.properties配置文件里添加以下内容保证可以正常启动项目:
spring.datasource.url=jdbc:mysql://localhost:3306/mall_pms

再次启动,已经可以正常启动了!
07.关于编写代码
根据已经添加的数据表,目前此项目中需要管理的数据类型大致有:
  - 品牌
  - 类别
  - 图片
  - 相册
  - 属性
  - 属性模版
  - SPU
  - SKU
  - 相关的关联表
  
首先,需要分析以上数据类型的开发先后顺序,部分数据之间存在依赖与被依赖关系,
例如 SKU肯定归属于某个SPU,
在开发时,必须先开发SPU,再开发SKU,
同理,必须先开发品牌,才可以开发SPU……

根据分析,
必要的顺序为:
————>(品牌 | 类别 | (相册 >>> 图片) | (属性模板 >>> 属性)) 
————>SPU
————>SKU

分析出必要顺序后,存在一些不需要严格区分顺序的数据类型,
例如以上的品牌和类型,实际的开发顺序可以是先简单、后复杂,
例如品牌数据通常比类别数据更加简单,则应该先开发品牌数据的管理,再开发类别数据的管理。

本次学习过程中,先开发类别,至于品牌,可自行课后完成!

当确定了需要处理类别的数据时,需要规划需要开发此数据的哪些管理功能,
例如:
添加类别、启用类别、禁用类别、修改类别的基本信息、根据id查询、根据parent_id查询列表……

以上管理类别数据的功能,
开发顺序应该是:
————>添加类别【开发顺序: 持久层/业务逻辑层/控制器层/页面】
————>查询(根据id查询 | 根据parent_id查询列表)
————>(启用类别 | 禁用类别 | 修改类别的基本信息)
— — — — — — — — — — — — — 【第一天完成 01-08<1> 即可】 08<1>.类别管理–添加类别–持久层 (1)配置
(1)
由于目前是本项目第1次开发持久层,必须先完成必要的配置,包括:
  连接到数据库(配置连接数据库的参数)
  使用 @MapperScan 指定接口所在的包(需要在下面创建配置类)
  配置mybatis.mapper-locations指定XML文件的位置

  详细步骤见下面:
————————————————————————————————————————————————————————————————
(2)
应该先在csmall-product-webapi的srcmainresources下:
创建复制的: 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
————————————————————————————————————————————————————————————————
(3)
然后,在application.properties中添加配置
     并将之前测试的连接数据库的配置删掉:

# 激活Profile配置
spring.profiles.active=dev
# 连接数据库的固定配置:
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

# Mybatis的XML文件位置
mybatis.mapper-locations=classpath:mapper
(4)
然后,需要在src/main/resources下自行创建mapper文件夹,与以上配置对应。
还需要自行创建配置类,用于配置Mapper接口文件所在的包:

则在cn.tedu.csmall.product.webapi下创建mapper子包,
并在cn.tedu.csmall.product.webapi下:
  - 创建config.MybatisConfiguration配置类,
    进行配置:

package cn.tedu.csmall.product.webapi.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("cn.tedu.csmall.product.webapi.mapper")
public class MybatisConfiguration {
}
————————————————————————————————————————————————————————————————
(5)
完成后,
可以尝试测试连接到数据库,
则在src/test/java下找到默认即存在的测试类,编写并执行测试:

package cn.tedu.csmall.product.webapi;
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 javax.sql.DataSource;
@SpringBootTest
class CsmallProductWebapiApplicationTests {
    @Autowired
    DataSource dataSource;//DataSource导包:import javax.sql.DataSource;

    
    @Test
    void contextLoads() {
    }

    
    @Test
    void testConnection() {
        //断言不抛出异常(assertDoesNotThrow),则获取连接(getConnection)成功
        Assertions.assertDoesNotThrow(() -> {
            dataSource.getConnection();
        });
    }
}
————————————————————————————————————————————————————————————————
(6)
执行整个测试,可以能够通过测试。控制台无报错信息
(2)规划需要执行的SQL语句
(1)
此次需要执行的是“增加类别”,
其数据操作的本质是向pms_category表中插入数据,需要执行的SQL语句大致是:
————> insert into pms_category (除了id以外的字段的列表) values (与字段列表匹配的各字段值);

在idea中的mall_pms数据库的客户端执行以下指令:【values后边的括号内为:值列表】
  insert into pms_category (name, parent_id, depth, keywords, sort, icon, enable, is_parent, is_display, gmt_create, gmt_modified) values ()
————————————————————————————————————————————————————————————————
(2)
另外,还应该考虑在“添加类别”时,是否需要执行相关的检查,
因为这些检查很可能是通过查询数据库来实现的,则在持久层也需要实现这些功能!

暂定规则“类别的名称不允许重复”,
则后续在Service层进行处理时,应该先根据尝试添加的类别的名称进行查询,
如果查询结果为null,表示此名称对应的类别尚不存在,将允许添加,
如果查询结果不为null,表示此名称对应的类别已经存在,将不允许添加!
——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——

要实现以上检查的效果,需要执行的SQL语句可以是:
————>  select * from pms_category where name=?;   【暂定使用第1种做法。】
 或者:
————>  select count(*) from pms_category where name=?;

——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——
以上2种做法,
第1种做法的查询效率相对较低,或者说性能消耗略高,
第2种做法的查询性能消耗更低,
但是,第1种做法的查询可能具有复用性(优点),而第2种做法的查询的复用性相对较低。

暂定可以使用以上第1种做法。
(3)接口与抽象方法
(0)
在插入数据之前,需要先创建“类别”对应的实体类型,
而这个实体类型不能直接创建在csmall-product-webapi模块中,
而是应该创建在根级的下边,和cs-mall-product同级别,
以便于其它各模块都可以使用到相同的实体类型
  --(例如商品的实体类型,在 商品管理模块 和 订单管理模块 中都将需要使用)。
————————————————————————————————————————————————————————————————
(1)
在My-csmall-server中创建新的子模块csmall-pojo:
   -Name:csmall-pojo
   -Group:cn.tedu
   -Artifact:csmall-pojo
   -Package  name:cn.tedu.csmall.pojo   注意加点:"."
   -Java:8
   -点击Next
   -什么也不勾选,直接点击Finish
————————————————————————————————————————————————————————————————
(2)
创建出来后,需要:
 /1.修改父项目:
	在csmall-pojo的pom.xml中进行修改;
	在根项目My-csmall-server的pom.xml中添加新的
 /2.确定所需的依赖项:
	需要依赖lombok,其它依赖项暂时不需要
 /3.删除不要的代码:
	在csmall-pojo的pom.xml中只需要:父项目、当前项目的信息、依赖项 
	启动类是不需要的,必须删除(因为没有依赖Spring Boot,不删除则报错)
	src/main/resource是不需要的(包含其下的application.properties),可以删除
	src/test是不需要的,必须删除(否则默认的测试类会报错)
————————————————————————————————————————————————————————————————
(3.1)
最终csmall_pojo的pom.xml:



    4.0.0

    
    
        cn.tedu
        csmall-server
        0.0.1-SNAPSHOT
    

    
    cn.tedu
    csmall-pojo
    0.0.1-SNAPSHOT

    
    
        
        
            org.projectlombok
            lombok
            true
        
    

——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——
(3.2)
最终csmall_server的pom.xml:



    4.0.0

    
    
        org.springframework.boot
        spring-boot-starter-parent
        2.5.9
         
    

    
    cn.tedu
    csmall-server
    0.0.1-SNAPSHOT

    
    pom

    
    
        csmall-product
        csmall-pojo
    

    
    
        1.8
        2.5.9
        2.2.2
        8.0.28
        1.18.22
        1.1.20
    

    
    
        
            
            
                org.springframework.boot
                spring-boot-starter-validation
                ${spring-boot.version}
            
            
            
                org.springframework.boot
                spring-boot-starter-web
                ${spring-boot.version}
            
            
            
                org.mybatis.spring.boot
                mybatis-spring-boot-starter
                ${mybatis-boot.version}
            
            
            
                mysql
                mysql-connector-java
                runtime
                ${mysql.version}
            
            
            
                org.projectlombok
                lombok
                true
                ${lombok.version}
            
            
            
                com.alibaba
                druid
                ${druid.version}
            
            
            
                org.springframework.boot
                spring-boot-starter-test
                test
                ${spring-boot.version}
            
        
    

————————————————————————————————————————————————————————————————
(4)
然后,
在cn.tedu.csmall.pojo.entity包下创建Category类:
在数据库客户端执行【创建表格并插入数据】命令中复制:
【-- 类别表:创建数据表】的相关属性——>到Category类中,根据属性的类型声明对应的属性的类型

package cn.tedu.csmall.pojo.entity;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
public class Category implements Serializable {
    private Long id;
    private String name;
    private Long parentId;
    private Integer depth;
    private String keywords;
    private Integer sort;
    private String icon;
    private Integer enable;
    private Integer isParent;
    private Integer isDisplay;
    private LocalDateTime gmtCreate;
    private LocalDateTime gmtModified;
}
————————————————————————————————————————————————————————————————
(5)
为了实现其它Module可以依赖csmall-pojo,
应该先在根级Project(My-csmall-server)的pom.xml中对csmall-pojo实现依赖管理:




    
    0.0.1-SNAPSHOT 




    
        
        
            cn.tedu
            csmall-pojo
            ${csmall.version}
                
		
————————————————————————————————————————————————————————————————
(6)
然后,在csmall-product-webapi的pom.xml中添加依赖:





    
    
        cn.tedu
        csmall-pojo
    
    
    

至此,在csmall-product-webapi中就已经成功的依赖了csmall-pojo,
可以理解为csmall-pojo已经成为csmall-product-webapi的一部分。

————————————————————————————————————————————————————————————————
(7)
在csmall-product-webapi 模块下:
在cn.tedu.csmall.product.webapi.mapper包下:
  创建CategoryMapper接口,
  并在接口上添加@Repository注解(主要是避免IntelliJ IDEA在自动装配时的误判错误),
  并在接口中添加抽象方法:
  

@Repository
public interface CategoryMapper {
    
    int insert(Category category);  
    // 查询--待定
}
(4)配置SQL语句
(1)
从其它位置(或项目)中复制粘贴:
  (比如在bootdemo工程中的AdminMapperxml复制过来并改名为CategoryMapper.xml)
CategoryMapper.xml 到 ————>csmall-product-webapi的src/main/resources/mapper下。




    
    
        insert into pms_category (
            name, parent_id, depth, keywords, sort,
            icon, enable, is_parent, is_display, gmt_create,
            gmt_modified
        ) values (
            #{name}, #{parentId}, #{depth}, #{keywords}, #{sort},
            #{icon}, #{enable}, #{isParent}, #{isDisplay}, #{gmtCreate},
            #{gmtModified}
        )
    

(5)测试:
(1)
在 csmall-product-webapi 下:
先在src/test下创建resources文件夹,
并在此文件夹中创建truncate.sql脚本,此脚本将用于重置数据表:
——————>  truncate pms_category;
————————————————————————————————————————————————————————————————
(6)
然后,在src/test/java的:cn.tedu.csmall.product.webapi:
     包下创建mapper.CategoryMapperTests,
     编写并执行测试:

package cn.tedu.csmall.product.webapi.mapper;
import cn.tedu.csmall.pojo.entity.Category;
import static 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 CategoryMapperTests {
    @Autowired
    CategoryMapper mapper;
    @Test
    @Sql("classpath:truncate.sql")
    public void testInsert() {
        // 测试数据
        Category category = new Category();
        category.setName("手机");
        // 断言不会抛出异常
        assertDoesNotThrow(() -> {
            int rows = mapper.insert(category);
            assertEquals(1, rows);
            assertEquals(1, category.getId());
        });
    }
}

执行整个类测试,控制台无异常
若不放心,可以查看数据库的 pms_category 表格:发现里边已经插入了一条数据:

— — — — — — — — — — — — — 【第二天完成 08<2>-10 即可】 08<2>.类别管理–添加类别–持久层 (1)配置
  • 续前日,无新增
(2)规划需要执行的SQL语句
  • 续前日,无新增
(3)接口与抽象方法
(1)
此前需要执行的SQL语句大致是:
————>  select id from pms_category where name=?;
————————————————————————————————————————————————————————————————————————
(2)封装具有可维护性:
在csmall-pojo的根包下创建vo.CategorySimpleVO类,用于封装以上查询结果:
@Data
public class CategorySimpleVO implements Serializable {
    private Long id;
}
————————————————————————————————————————————————————————————————————————
(3)
在csmall-product-webapi的 CategoryMapper 接口中添加抽象方法:
————> 
    
    CategorySimpleVO getByName(String name);
————————————————————————————————————————————————————————————————————————
(4)配置SQL语句
(1)
在csmall-product-webapi的 CategoryMapper.xml 中添加配置:



    select id from pms_category where name=#{name}



    

(5)测试
(1)
在csmall-product-webapi的srctestresources下:
创建 insert_data.sql 文件,用于插入测试数据:
————>   insert into pms_category (name) value ('类别001'), ('类别002');
————————————————————————————————————————————————————————————————————————
(2)
然后,在 CategoryMapperTests 中添加测试方法:

    
    @Test
    @Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
    public void testGetByNameSuccessfully() {
        // 测试数据
        String name = "类别001";
        // 断言不会抛出异常
        assertDoesNotThrow(() -> {
            // 执行查询
            CategorySimpleVO category = mapper.getByName(name);
            // 断言查询结果不为null
            assertNotNull(category);
        });
    }

    
    @Test
    @Sql({"classpath:truncate.sql"})
    public void testGetByNameFailBecauseNotFound() {
        // 测试数据
        String name = "类别999";
        // 断言不会抛出异常
        assertDoesNotThrow(() -> {
            // 执行查询
            CategorySimpleVO category = mapper.getByName(name);
            // 断言查询结果为null
            assertNull(category);
        });
    }
————————————————————————————————————————————————————————————————————————
(3)
完成后,执行整个测试类(将执行此类中所有测试方法),应该全部通过测试。
09.类别管理–添加类别–业务逻辑层(子模块csmall-product-service) (1)接口与抽象方法(创建子模块)
(0)
在使用Dubbol的微服务架构中,
需要将业务逻辑层的接口声明在专门的Module中,便于被其它微服务Module依赖,
所以,
先在csmall-product下创建新的Module,名为 csmall-product-service,创建参数:
  - Group:cn.tedu
  - Artifact:csmall-product-service
  - Package Name:cn.tedu.csmall.product.service
  - 点击Next
  - 什么也不勾选,点击Finish
————————————————————————————————————————————————————————————————————————
(1)
首先,应该调用修改(csmall-product-service)的 pom.xml 文件如下:



    4.0.0
    
    
    
        cn.tedu
        csmall-product
        0.0.1-SNAPSHOT
    

    
    cn.tedu
    csmall-product-service
    0.0.1-SNAPSHOT


    
    
        
        
            cn.tedu
            csmall-pojo
        
    

————————————————————————————————————————————————————————————————————————
(2)
然后,在 csmall-product 的pom.xml中:
  补充 子模块csmall-product-service 的Module:



    csmall-product-webapi
    csmall-product-service 

————————————————————————————————————————————————————————————————————————
(3)
接下来,需要删除不必要的文件:
  - 启动类
  - srcmainresources及其下的配置文件
  - srctest

接下来,需要创建接口并添加抽象方法,
方法的参数应该是封装的对象(因为一个String或Long等简单数据不足以完成添加类别的操作),
  则先在csmall-pojo的根包下:
  创建 dto.CategoryAddNewDTO 类,并在类中添加必要的属性:

package cn.tedu.csmall.pojo.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class CategoryAddNewDTO implements Serializable {
    private String name;
    private Long parentId;
    private String keywords;
    private Integer sort;
    private String icon;
    private Integer isDisplay;
}
————————————————————————————————————————————————————————————————————————
(4)
然后,在csmall-product-service 中,
  在 cn.tedu.csmall.product.service 下创建 ICategoryService接口:

public interface ICategoryService {
    
    void addNew(CategoryAddNewDTO categoryAddNewDTO);
}
(2)实现
(1)
在 csmall-product-service 中只存放业务逻辑层的接口,
而业务逻辑层的实现类仍在 csmall-product-webapi 中,
所以,需要在 csmall-product-webapi 中 依赖csmall-product-service。
具体实现:
先在Project(My-csmall-server)的pom.xml中添加对csmall-product-service的依赖管理:




    
        
        
            cn.tedu
            csmall-product-service
            ${csmall.version}
                
        
————————————————————————————————————————————————————————————————————————
(2)
然后,在 csmall-product-webapi 中添加依赖:




    
    
        cn.tedu
        csmall-product-service
        
    
————————————————————————————————————————————————————————————————————————
(3)
在 csmall-product-webapi 中:
  在cn.tedu.csmall.product.webapi下:
    创建 service.CategoryServiceImpl 类,
      此类应该实现 ICategoryService 接口,
      要自动装配mapper,所以此类还应该添加 @Service 注解:

package cn.tedu.csmall.product.webapi.service;
import cn.tedu.csmall.pojo.dto.CategoryAddNewDTO;
import cn.tedu.csmall.product.service.ICategoryService;
import org.springframework.stereotype.Service;

@Service
public class CategoryServiceImpl implements ICategoryService {    
    @Override
    public void addNew(CategoryAddNewDTO categoryAddNewDTO) {
    }    
}
————————————————————————————————————————————————————————————————————————
(4)
关于以上业务的实现分析:
在csmall-product-webapi中的CategoryServiceImpl中列出以下所需业务:

@Autowired
private CategoryMapper categoryMapper;

// 注意:需要创建异常
// 注意:需要在CategoryMapper中补充getById()方法,至少返回:depth
// 注意:需要在CategoryMapper中补充updateIsParentById()方法
public void addNew(CategoryAddNewDTO categoryAddNewDTO) {
    // 从参数中取出尝试添加的类别的名称
    // 调用categoryMapper.getByName()方法查询
    // 判断查询结果是否不为null
    // 是:抛出ServiceException
    
    // 从参数中取出父级类别的id:parentId
    // 判断parentId是否为0
    // 是:此次尝试添加的是一级类别,没有父级类别,则当前depth >>> 1
    // 否:此次尝试添加的不是一级类别,则应该存在父级类别,调用categoryMapper.getById()方法查询父级类别的信息
    // -- 判断查询结果是否为null
    // -- 是:抛出ServiceException
    // -- 否:当前depth >>> 父级depth + 1

    // 创建Category对象
    // 调用BeanUtils.copyProperties()将参数对象中的属性值复制到Category对象中
    // 补全Category对象中的属性值:depth >>> 前序运算结果
    // 补全Category对象中的属性值:enable >>> 1(默认即启用)
    // 补全Category对象中的属性值:isParent >>> 0
    // 补全Category对象中的属性值:gmtCreate, gmtModified >>> LocalDateTime.now()
    // 调用categoryMapper.insert(Category)插入类别数据,获取返回的受影响的行数
    // 判断返回的受影响的行数是否不为1
    // 是:抛出ServiceException
    
    // 判断父级类别的isParent是否为0
    // 是:调用categoryMapper.updateIsParentById()方法,将父级类别的isParent修改为1,获取返回的受影响的行数
    // 判断返回的受影响的行数是否不为1
    // 是:抛出ServiceException
}
————————————————————————————————————————————————————————————————————————
(5)
要实现以上业务,
需要先 在持久层完成“根据id查询类别信息”的功能,
则在 csmall-pojo模块:CategorySimpleVO 中添加:
  【 private Integer depth;//depth:深度 】属性
  (原getByName()方法对应的查询也作对应的修改,虽然不是必须的)。
——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	
(6)
然后,还需要继续在 CategorySimpleVO 中补充 【private Integer isParent;】属性,
并且,必须在接下的查询中,查出此值。
————————————————————————————————————————————————————————————————————————
(7)
然后在csmall-product-webapi中:CategeoryMapper接口中添加:
    
    CategorySimpleVO getById(Long id);

(8)
然后在csmall-product-webapi中:在CategoryMapper.xml中配置以上方法映射的SQL:



        select
        
        from
        pms_category
        where
        id=#{id}
    

    
    

    
        
            id, depth, is_parent
        
    
    
    
        
        
        
    


再次执行csmall-product-webapi中:CategoryMapperTests中的全部方法
依然可以在控制台全部测试成功
————————————————————————————————————————————————————————————————————————
(13)
在实现业务逻辑之前,还需要 创建自定义的异常类型,
由于后续还有不少需要被多个Module共同使用的类、接口等,
所以,【此异常类型和后续可能被共用的类、接口都应该放在一个公共的Module中】,
则在Project(My-csmall-server)下创建 csmall-common 这个新的 Module,
创建参数:
  - Group:cn.tedu
  - Artifact:csmall-common
  - Package Name:cn.tedu.csmall.common
  - 点击Next
  - 什么也不勾选,点击Finish

创建成功后,需要:
——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	  
  - 修改csmall-common的 pom文件:
     - 在csmall-common中,修改pom.xml中的父项目csmall-server
     - 在csmall-common中,在pom.xml删除依赖项
     - 在csmall-common中,在pom.xml删除配置


最终csmall-common的 pom配置如下:



    4.0.0

    
    
        cn.tedu
        csmall-server
        0.0.1-SNAPSHOT
    

    
    cn.tedu
    csmall-common
    0.0.1-SNAPSHOT

    
    

    

——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	——	     
  - 在csmall-common中,删除src/test
  - 在csmall-common中,删除src/main/resources
  - 在csmall-common中,删除启动类
  - 在Project的pom.xml中,添加:
       
       
           csmall-product
           csmall-admin
           csmall-pojo
           csmall-common 
       
       
  - 在Project的pom.xml中,添加对新Module的依赖管理:
            
            
                cn.tedu
                csmall-common
                ${csmall.version}
            
            
  - 在csmall-product-webapi中的pom.xml中,添加对csmall-common的依赖:
        
        
          
          
              cn.tedu
              csmall-common
          
		  
  		



————————————————————————————————————————————————————————————————————————
(14)
在csmall-common的根包下创建ex.ServiceException类:

package cn.tedu.csmall.common.ex;
public class ServiceException extends RuntimeException {
    // 暂时不加构造方法
}
————————————————————————————————————————————————————————————————————————
(15)
然后,在csmall-product-webapi中的:
     CategoryServiceImpl 中实现业务:

package cn.tedu.csmall.product.webapi.service;
import cn.tedu.csmall.common.ex.ServiceException;
import cn.tedu.csmall.pojo.dto.CategoryAddNewDTO;
import cn.tedu.csmall.pojo.entity.Category;
import cn.tedu.csmall.pojo.vo.CategorySimpleVO;
import cn.tedu.csmall.product.service.ICategoryService;
import cn.tedu.csmall.product.webapi.mapper.CategoryMapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@Service
public class CategoryServiceImpl implements ICategoryService {
    @Autowired
    CategoryMapper categoryMapper;

    @Override
    public void addNew(CategoryAddNewDTO categoryAddNewDTO) {
        // 从参数中取出尝试添加的类别的名称
        String name = categoryAddNewDTO.getName();
        // 调用categoryMapper.getByName()方法查询
        CategorySimpleVO queryResult = categoryMapper.getByName(name);
        // 判断查询结果是否不为null
        if (queryResult != null) {
            // 是:抛出ServiceException
            throw new ServiceException();
        }

        // 从参数中取出父级类别的id:parentId
        Long parentId = categoryAddNewDTO.getParentId();
        // 判断parentId是否为0,当前尝试新增的类别的depth默认为1
        Integer depth = 1;
        CategorySimpleVO parentCategory = null;
        if (parentId != 0) {
            // 否:此次尝试添加的不是一级类别,则应该存在父级类别,调用categoryMapper.getById()方法查询父级类别的信息
            parentCategory = categoryMapper.getById(parentId);
            // -- 判断查询结果是否为null
            if (parentCategory == null) {
                // -- 是:抛出ServiceException
                throw new ServiceException();
            }
            // -- 否:当前depth >>> 父级depth + 1
            depth = parentCategory.getDepth() + 1;
        }

        // 创建Category对象
        Category category = new Category();
        // 调用BeanUtils.copyProperties()将参数对象中的属性值复制到Category对象中
        BeanUtils.copyProperties(categoryAddNewDTO, category);
        // 补全Category对象中的属性值:depth >>> 前序运算结果
        category.setDepth(depth);
        // 补全Category对象中的属性值:enable >>> 1(默认即启用)
        category.setEnable(1);
        // 补全Category对象中的属性值:isParent >>> 0
        category.setIsParent(0);
        // 补全Category对象中的属性值:gmtCreate, gmtModified >>> LocalDateTime.now()
        LocalDateTime now = LocalDateTime.now();
        category.setGmtCreate(now);
        category.setGmtModified(now);
        // 调用categoryMapper.insert(Category)插入类别数据,获取返回的受影响的行数
        int rows = categoryMapper.insert(category);
        // 判断返回的受影响的行数是否不为1
        if (rows != 1) {
            // 是:抛出ServiceException
            throw new ServiceException();
        }

        // 判断父级类别的isParent是否为0
        // 以下判断条件有部分多余,但不会报错
        if (parentId != 0 && parentCategory != null && parentCategory.getIsParent() == 0) {
            // 是:调用categoryMapper.updateIsParentById()方法,将父级类别的isParent修改为1,获取返回的受影响的行数
            rows = categoryMapper.updateIsParentById(parentId, 1);
            // 判断返回的受影响的行数是否不为1
            if (rows != 1) {
                // 是:抛出ServiceException
                throw new ServiceException();
            }
        }
    }

}
(3)测试
在src/test/java下的根包下:
  创建service.CategoryServiceTests测试类,编写并执行测试:

package cn.tedu.csmall.product.webapi.service;
import cn.tedu.csmall.common.ex.ServiceException;
import cn.tedu.csmall.pojo.dto.CategoryAddNewDTO;
import cn.tedu.csmall.product.service.ICategoryService;
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;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
public class CategoryServiceTests {

    @Autowired
    ICategoryService service;

    
    @Test
    //@Sql("classpath:truncate.sql")
    public void testAddNewSuccessfully() {
        // 测试数据
        CategoryAddNewDTO category = new CategoryAddNewDTO();
        category.setName("太阳能");
        category.setParentId(2L);
        category.setIcon("未上传类别图标");
        category.setKeywords("未设置关键字");
        category.setSort(88);
        category.setIsDisplay(1);
        // 断言不会抛出异常
        //assertDoesNotThrow(() -> {
            // 执行测试
            service.addNew(category);
        //});
    }


    
    @Test
    @Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
    public void testAddNewFailBecauseNameDuplicate() {
        // 测试数据
        CategoryAddNewDTO category = new CategoryAddNewDTO();
        category.setName("类别001");
        // 断言不会抛出异常
        assertThrows(ServiceException.class, () -> {
            // 执行测试
            service.addNew(category);
        });
    }

    
    @Test
    @Sql({"classpath:truncate.sql"})
    public void testAddNewFailBecauseParentNotFound() {
        // 测试数据
        CategoryAddNewDTO category = new CategoryAddNewDTO();
        category.setName("类别001");
        category.setParentId(-1L);
        // 断言不会抛出异常
        assertThrows(ServiceException.class, () -> {
            // 执行测试
            service.addNew(category);
        });
    }

}

执行以上类中的方法,可以测试成功
10.类别管理–添加类别–控制器层
(1)
事务:是一种能够保证同一个业务中多个写(增删改)操作要么全部成功,要么失败的机制!

在业务方法上添加 @Transactional ,
即可保证此方法是业务性(要么全部成功,要么全部失败)的。
————————————————————————————————————————————————————————————————————————
(2)
在Spring JDBC中,处理事务的机制大致是:
开启事务:Begin

try {
    你的业务方法
    提交:Commit
} catch (RuntimeException e) {
    回滚:Rollback
}
————————————————————————————————————————————————————————————————————————
(3)
所以,为了保证事务性,所有的写操作在执行之后,必须有某个判定为失败的标准,
且判断定为失败后,必须抛出 RuntimeException 或 其子孙类异常 !

Spring JDBC默认对 RuntimeException 进行回滚处理,
有必要的话,也可以配置为其它异常类型
11.类别管理–添加类别–控制器层 12.类别管理–添加类别–前端页面
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/967319.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号