赞
踩
SpringBoot框架一般分为View层、Controller层、Service层、Mapper层、pojo层。
1、View层:视图层,根据接到的数据展示页面给用户
2、Controller层:响应用户需求,决定用什么视图,需要准备什么数据来显示。Controller层负责前后端交互,接收前端请求,调用Service层,接收Service层返回的数据,最后返回具体的数据和页面到客户端
3、Service层:Service层也可以分为三个方面
4、Mapper层:也可以称为DAO层,是数据库CRUD的接口,只有方法名,具体实现在mapper.xml文件中,对数据库进行数据持久化操作(把数据放到持久化的介质中,同时提供CRUD操作)
5、
(1)Pojo层:存放实体类,与数据库中的属性基本保持一致,一般包括getter、setter、toString方法(未使用插件lombok的情况下)。
Spring Boot是Spring官方的一个产品,
其本质上是一个基于Maven的、以Spring框架作为基础的进阶框架,
很好的支持了主流的其它框架,
并默认完成了许多的配置,
其核心思想是“约定大于配置”。
在IntelliJ IDEA中: 在创建向导中选择Spring Initializer即可开始创建Spring Boot工程: 在创建向导的界面中,需要关注的部分有: - Group Id - Artifact Id 以上2个值会共同构成一个Package name, 如果Artifact Id的名字中有减号,在Package name中会去除,推荐手动添加小数点进行分隔。 由于Spring Boot官方更新版本的频率非常高, 在创建项目时, 随便选取某个版本均可, 当项目创建成功后, 推荐打开pom.xml,将<parent>中的<version>(即Spring Boot父项目的版本) 改成熟悉的版本,例如:2.5.9 在创建过程中: 还可以在创建向导的界面中勾选所需要依赖项, 如果创建时没有勾选,也可以在创建工程之后手动在pom.xml中添加。
2.此处不勾选也可以,可以后面创建一个新的springboot项目,将相关的两个依赖复制到此工程的pom.xml中,
需要手动调节各依赖版本号相差不大!
3.打开 工程的pom.xml文件:将 parent节点中的版本由2.7.0改为2.5.9并刷新maven
注意:若点击刷新maven按钮后,下面多出【】节点报错,删掉即可!因为这个项目中我们用不到
(1)由于Spring Boot工程本质上就是一个Maven工程,所以,目录结构基本上没有区别。 ————————————————————————————————————————————————————————————— (2) 与普通Maven工程最大的不同在于: Spring Boot工程在src\main\java和src\test\java下默认已经存在Package, 是创建项目时指定的Package, ————>需要注意:此Package已经被配置为Spring执行组件扫描的根包, 所以,在编写代码时,所有的组件类都必须放在此包或其子孙包中! 通常,推荐将所有的类(及接口)都创建在此包及其子孙包下。 ————————————————————————————————————————————————————————————— (3)在src\main\java下的根包下, 默认就已经存在某个类, 其类名是创建项目时指定的Artifact与Application单词的组合, 例如BootDemoApplication, 此类中有main()方法,执行此类的main()就会启动整个项目, 如果当前项目是Web项目,还会自动将项目部署到Web服务器并启动服务器, 所以,此类通常也称之为“启动类”。 ————————————————————————————————————————————————————————————— (4)启动类本身也是配置类: 在启动类上, 默认添加了@SpringBootApplication注解,此注解的元注解中包含@SpringBootConfiguration, 而@SpringBootConfiguration的元注解中包含@Configuration, 所以,启动类本身也是配置类! 所以,允许将@Bean方法写在此类中,或者某些与配置相关的注解也可以添加在此类上! ————————————————————————————————————————————————————————————— (5)在src\test\java下的根包下, 默认就已经存在某个类,其类名是在启动类的名称基础上添加了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项, 在src\main\resources下默认就已经创建了static和templates文件夹, 如果没有勾选Web则没有这2个文件夹,可以后续自行补充创建。 ————————————————————————————————————————————————————————————— 在src\main\resources文件夹下, 默认就已经存在application.properties文件, 用于编写配置,Spring Boot会自动读取此文件(利用@PropertySource注解)。 ————————————————————————————————————————————————————————————— ————>小结:<———— (1)创建项目后默认的Package不要修改,避免出错 (2)在编码过程中,自行创建的所有类、接口均放在默认的Package或其子孙包中 (3)在src\main\java下默认已存在XxxApplication是启动类, 执行此类中的main()方法就会启动整个项目 (4)启动类本身也是配置类 (5)配置都应该编写到src\main\resources下的application.properties中, Spring Boot会自动读取 (6)测试类也必须放在src\test\java下的默认Package或其子孙包中 (7)在测试类上添加@SpringBootTest注解, 则其中的测试方法执行之前会自动加载Spring环境及当前项目的配置, 可以在测试类中使用自动装配
(1)创建工程时 spring中 打2个钩 或者 需要自行添加 2个 相关依赖项: 【mysql-connector-java 、 mybatis-spring-boot-starter】 <!--添加mysql的依赖项:--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!--添加mybatis.spring.boot依赖项:--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> (2)说明: 在Spring Boot工程,许多依赖项都是不需要显式的指定版本号的, 因为在父项目中已经对这些依赖项的版本进行了管理(配置版本号), 如果一定需要使用特定的版本,也可以自行添加`<version>`节点进行配置 (3)说明: 在依赖项的源代码中,当`<scope>`的值为`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;//数据源 /** * 测试获取连接 * @throws SQLException */ @Test void testGetConnection() throws SQLException { System.out.println(dataSource.getConnection()); } } 启动测试类 BootDemoApplicationTests,可以正常启动! 如果以上测试通过,则表示配置值无误,可以正确连接到数据库, 如果测试失败,则表示配置值错误, 需检查 配置值及本地环境(例如MySQL是否启动、是否已创建对应的数据库等) 。 —————————————————————————————————————————————————————————————
(1)在Spring Boot中,对Profile配置有很好的支持, 开发人员可以在src\main\resources下创建更多的配置文件, 这些配置文件的名称应该是: 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。 —————————————————————————————————————————————————————————————
(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最终的执行其实没有任何表现上的不同。
(1)Druid数据库连接是阿里巴巴团队研发的, 在Spring Boot项目中,如果需要显式的指定使用此连接池, 首先,需要在项目中添加依赖: 在pom.xml文件里添加依赖: <!--添加显式的指定使用此Druid连接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.20</version> </dependency> ————————————————————————————————————————————————————————————— (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 —————————————————————————————————————————————————————————————
(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种做法, 则应该在src\main\java包下: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/*.xml 【】*/]】 ————————————————————————————————————————————————————————————— (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.."】 <mapper namespace="cn.tedu.boot.demo.mapper.AdminMapper"> — — — — — — — — — — — — — — — — — — — — — — 编写完成后,应该及时测试,测试时, 推荐在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语句: <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="cn.tedu.boot.demo.mapper.AdminMapper"> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> insert into ams_admin (username, password, nickname, avatar, phone, email, description, is_enable, last_login_ip, login_count, gmt_last_login, gmt_create, gmt_modified) values (#{username}, #{password}, #{nickname}, #{avatar}, #{phone}, #{email}, #{description}, #{isEnable}, #{lastLoginIp}, #{loginCount}, #{gmtLastLogin}, #{gmtCreate}, #{gmtModified}) </insert> <select id="getByUsername" resultMap="BaseResultMap"> select <include refid="BaseQueryFields" /> from ams_admin where username=#{username} </select> <sql id="BaseQueryFields"> <if test="true"> id, username, password, nickname, avatar, phone, email, description, is_enable, last_login_ip, login_count, gmt_last_login, gmt_create, gmt_modified </if> </sql> <resultMap id="BaseResultMap" type="cn.tedu.boot.demo.entity.Admin"> <id column="id" property="id" /> <result column="username" property="username" /> <result column="password" property="password" /> <result column="nickname" property="nickname" /> <result column="avatar" property="avatar" /> <result column="phone" property="phone" /> <result column="email" property="email" /> <result column="description" property="description" /> <result column="is_enable" property="isEnable" /> <result column="last_login_ip" property="lastLoginIp" /> <result column="login_count" property="loginCount" /> <result column="gmt_last_login" property="gmtLastLogin" /> <result column="gmt_create" property="gmtCreate" /> <result column="gmt_modified" property="gmtModified" /> </resultMap> </mapper> — — — — — — — — — — — — — — — — — — — — — — — 编写完成后,应该及时测试,在测试类中添加【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值来测试或执行 —————————————————————————————————————————————————————————————
(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来标明:*/ // 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; /** * 用于处理密码加密 * 密码加密器:PasswordEncoder */ //声明成一个组件,每次要用的时候就自动装配 @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来标明:*/ // 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层已经写完了
(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依赖,可以在创建项目时勾选,也可以后期自行添加,依赖项的代码为: ——><!--添加Lombok依赖--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> ————————————————————————————————————————————————————————————————————————————————————— (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 再次提示:无论是否安装插件,都不影响代码的编写和运行!
在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】配置文件里,因为放在这个文件里更合理
(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:文摘测试 /** * 测试 MD5算法 * md5DigestAsHex():DigestUtils类中的静态方法,用于计算并返回给定字节的MD5摘要。 */ @Test public void md5Test(){ String rawPassword = "123456"; String encodePassword = DigestUtils.md5DigestAsHex(rawPassword.getBytes()); System.out.println("原密码:"+rawPassword); System.out.println("加密后的密码:"+encodePassword); /* 控制台显示: 原密码:123456 加密后的密码:e10adc3949ba59abbe56e057f20f883e */ } } ——————————————————————————————————————————————————————————————————— (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:文摘测试 /** * 测试 MD5算法 * md5DigestAsHex():DigestUtils类中的静态方法,用于计算给定字节的MD5摘要。 */ @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); /* 控制台显示: 原密码:123456 盐值:efegwgasgvetjhytjkshw 加盐值并加密后的密码:9af7a6beb9d9df703fb40f40f388080b */ } } 加随机的盐值: package cn.tedu.boot.demo; import org.junit.jupiter.api.Test; import org.springframework.util.DigestUtils; import java.util.UUID; public class DigestTests { //DigestTests:文摘测试 /** * 测试 MD5算法 * md5DigestAsHex():DigestUtils类中的静态方法,用于计算给定字节的MD5摘要。 */ @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); /* 控制台显示: 原密码:123456 随机盐值:b4ce9c24-996f-4d37-8623-3c08eb71a5f0 加随机盐值并加密后的密码:442e4d08e1943efc99d22f46faea7a21 */ } } ——————————————————————————————————————————————————————————————————— (8)加盐的目的是使得被运算数据变得更加复杂,盐值本身和用法并没有明确要求! 甚至, 在某些用法或算法中,还会使用随机的盐值,则可以使用完全相同的原消息对应的摘要却不同! 推荐了解:预计算的哈希链、彩虹表、雪花算法。 ——————————————————————————————————————————————————————————————————— (9)为了进一步保证密码安全,还可以使用多重加密,即反复调用消息摘要算法。 例: 写一个100000次的循环,将每次加密后的密码都作为原密码再去返回消息摘要: 在DigestTests测试类中再写一个方法用来测试: /** * 为了进一步保证密码安全,还可以使用多重加密,即反复调用消息摘要算法。 * 例:循环10万次来计算加密密码的消息摘要 */ @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; /** * 用于处理密码加密 * 密码加密器:PasswordEncoder */ //声明成一个组件,每次要用的时候就自动装配 @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; /** * 用于处理密码加密 * 密码加密器:PasswordEncoder */ //声明成一个组件,每次要用的时候就自动装配 @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); } }
(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】 <!--SpringBoot的依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> ——————————————————————————————————————————————————————————————————— (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<Void> addNew(AdminAddNewDTO adminAddNewDTO) { adminService.addNew(adminAddNewDTO); return JsonResult.ok(); } ——————————————————————————————————————————————————————————————————— 完成后,运行启动类,即可启动整个项目, 在spring-boot-starter-web中,包含了Tomcat的依赖项, 在启动时,会自动将当前项目打包并部署到此Tomcat上, 所以,执行启动类时,会执行此Tomcat, 同时,因为是内置的Tomcat,只为当前项目服务, 所以,在将项目部署到Tomcat时, 默认已经将Context Path(例如spring_mvc_war_exploded)配置为空字符串, 所以,在启动项目后,访问的URL中并没有此前遇到的Context Path值。 当项目启动成功后,即可在浏览器的地址栏中输入网址进行测试访问! ——————————————————————————————————————————————————————————————————— 注意:如果是未添加的管理员账号,可以成功执行结束,如果管理员账号已经存在,由于尚未处理异常,会提示500错误。 ——————————————————————————————————————————————————————————————————— 写到最后,当我们运行启动类后, 访问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; /** * 为保证更合理的处理异常,应该: * – 将处理异常的代码放在专门的类(GlobalExceptionHandler)中 * – 在此类上添加@ControllerAdvice注解 * – 由于目前主流的响应方式都是“响应正文”的, * 则可以将@ControllerAdvice替换为 @RestControllerAdvice */ @RestControllerAdvice //处理异常的类要加此注解 public class GlobalExceptionHandler {//GlobalExceptionHandler:全局异常处理程序 @ExceptionHandler(ServiceException.class) public JsonResult<Void> handleServiceException(ServiceException e) { if (e instanceof UsernameDuplicateException) { return JsonResult.fail(State.ERR_USERNAME, "用户名错误!"); } else { return JsonResult.fail(State.ERR_INSERT, "插入数据失败!"); } } } ——————————————————————————————————————————————————————————————————— 完成后,重新启动项目,运行启动类, 测试网站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<Void> handleServiceException(ServiceException e) { if (e instanceof UsernameDuplicateException) { return JsonResult.fail(State.ERR_USERNAME, e.getMessage()); } else { return JsonResult.fail(State.ERR_INSERT, e.getMessage()); } } ——————————————————————————————————————————————————————————————————— 完成后,启动测试类, 访问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<T> implements Serializable { // 状态码,例如:200 private Integer state; // 消息,例如:"登录失败,用户名不存在" @JsonInclude(JsonInclude.Include.NON_NULL) private String message; // 数据 @JsonInclude(JsonInclude.Include.NON_NULL) private T data; // ===== 原有其它代码 ===== } 则响应的JSON中只会包含不为null的部分。 此注解还可以添加在类上,则作用于当前类中所有的属性,例如: @Data @JsonInclude(JsonInclude.Include.NON_NULL) public class JsonResult<T> implements Serializable { // ===== 原有其它代码 ===== } 我们可以通过添加注解@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进行配置。 ———————————————————————————————————————————————————————————————————
:检查数据基本格式有效性的
此时我们发现,启动运行类后, 在网址输入:http://localhost:8080/admins/add-new 进行测试, 即使什么属性也没给,页面也会显示状态state=200 添加成功,这是不合理的. 当客户端向服务器提交请求时, 如果请求出现明显的问题(例如关键数据为null、字符串的长度不在可接受范围内、其它格式错误), 应该直接响应错误,而不是将明显错误的请求参数传递到Service! : 关于判断错误,只有涉及数据库中的数据才能判断出结果的,都由Service进行判断,而基本的格式判断,都由Controller进行判断。 Validation框架是专门用于解决检查数据基本格式有效性的,最早并不是Spring系列的框架,目前,Spring Boot提供了更好的支持,所以,通常结合在一起使用。 在SpringBoot项目中,需要添加spring-boot-starter-validation依赖项: 在bootdemo工程的pom.xml文件里添加依赖: <!--添加Spring Validation框架依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> ——————————————————————————————————————————————————————————————————— 首先,对需要检查数据格式的请求参数添加@Valid 或 @Validated注解(这两个注解没有区别), 在AdminController: //@Valid:表示需要检查 public JsonResult<Void> 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 errors<EOL>Field error in object 'adminAddNewDTO' on field 'username': rejected value [null]; codes [NotNull.adminAddNewDTO.username,NotNull.username,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [adminAddNewDTO.username,username]; arguments []; default message [username]]; default message [不能为null]] ——————————————————————————————————————————————————————————————————— 从警告信息中可以看到,当验证失败时(不符合所使用的注解对应的规则时), 会出现org.springframework.validation.BindException异常, 则自行处理此异常即可! : 在state类里添加新的枚举:【 ERR_BAD_REQUEST(400), 】 找到统一处理异常的类GlobalExceptionHandler: 添加新的处理异常的方法: @ExceptionHandler(BindException.class) public JsonResult<Void> 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 errors\nField error in object 'adminAddNewDTO' on field 'username': rejected value [null]; codes [NotNull.adminAddNewDTO.username,NotNull.username,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [adminAddNewDTO.username,username]; arguments []; default message [username]]; default message [不能为null]\nField error in object 'adminAddNewDTO' on field 'password': rejected value [null]; codes [NotNull.adminAddNewDTO.password,NotNull.password,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [adminAddNewDTO.password,password]; arguments []; default message [password]]; default message [不能为null]" } ——————————————————————————————————————————————————————————————————— 关于错误提示信息,以上内容中出现了不能为null的字样,是默认的提示文本, 可以通过@NotNull注解的message属性进行配置,例如: 在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<Void> 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注解进行验证
0.
(1)找到工程:vue-project-05:
(2)在电脑中路径:
D:\soft\01Java\00soft\01project\chengHhengJava\fiveJava\doc\note\01VueCli
中找到vue-project-server-0.0.1.jar 文件,并启动
(3)在idea界面下边的Terminal处 用命令npm run serve 启动05工程,
在浏览器输入网址:http://localhost:8080/ 跳转到用户登录页面
点击超链接:进入管理页面后
可以看到管理页面:
在使用前后端分离的开发模式下,前端项目和后端项目可能是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("/**") .allowedOriginPatterns("*") .allowedMethods("*") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }
(0)
(1)找到工程:vue-project-05: 在LoginView.vue登录页面: 改成与vue-project-04的登录页面相似的【添加管理员的页面】: 在https://element.eleme.cn/复制表单相关的代码并改动: <template> <div class="home"> <h1>添加管理员</h1> <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm"> <el-form-item label="用户名" prop="username"> <el-input v-model="ruleForm.username"></el-input> </el-form-item> <el-form-item label="密码" prop="password"> <el-input v-model="ruleForm.password"></el-input> </el-form-item> <el-form-item label="昵称" prop="nickname"> <el-input v-model="ruleForm.nickname"></el-input> </el-form-item> <el-form-item label="手机号码" prop="phone"> <el-input v-model="ruleForm.phone"></el-input> </el-form-item> <el-form-item label="电子邮箱" prop="email"> <el-input v-model="ruleForm.email"></el-input> </el-form-item> <el-form-item label="个人简介" prop="description"> <el-input v-model="ruleForm.description"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm('ruleForm')">立即创建</el-button> <el-button @click="resetForm('ruleForm')">重置</el-button> </el-form-item> </el-form> <el-link href="/admin" type="primary">进入管理页面</el-link> </div> </template> <script> export default { data() { return { ruleForm: { username: '', password: '', nickname: '', phone: '', email: '', description: '' }, rules: { username: [ { required: true, message: '请输入用户名', trigger: 'blur' }, { min: 4, max: 15, message: '用户名长度必须在4到15个字符', trigger: 'blur' } ], password: [ { required: true, message: '请输入密码', trigger: 'blur' }, { min: 4, max: 15, message: '密码长度必须在4到15个字符', trigger: 'blur' } ] } }; }, methods: { submitForm(formName) { this.$refs[formName].validate((valid) => { if (valid) { alert('submit!'); } else { console.log('error submit!!'); return false; } }); }, resetForm(formName) { this.$refs[formName].resetFields(); } } } </script>
(1)此时在idea界面下边的Terminal处 用命令npm run serve 启动05工程,
在浏览器输入网址:http://localhost:8080/ 跳转到添加管理员页面
————>页面显示:
(2)
因为前端页面的 npm 启动指令会指定8080端口,
所以我们先启动后端的启动类,
再启动前端时因为8080被后端占用,所以自动顺延自8081端口,
在浏览器测试:http://localhost:8081
(3)准备请求路径/请求参数
改动vue-project-05:LoginView.vue页面: <template> <div class="home"> <h1>添加管理员</h1> <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm"> <el-form-item label="用户名" prop="username"> <el-input v-model="ruleForm.username"></el-input> </el-form-item> <el-form-item label="密码" prop="password"> <el-input v-model="ruleForm.password"></el-input> </el-form-item> <el-form-item label="昵称" prop="nickname"> <el-input v-model="ruleForm.nickname"></el-input> </el-form-item> <el-form-item label="手机号码" prop="phone"> <el-input v-model="ruleForm.phone"></el-input> </el-form-item> <el-form-item label="电子邮箱" prop="email"> <el-input v-model="ruleForm.email"></el-input> </el-form-item> <el-form-item label="个人简介" prop="description"> <el-input v-model="ruleForm.description"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm('ruleForm')">立即创建</el-button> <el-button @click="resetForm('ruleForm')">重置</el-button> </el-form-item> </el-form> <el-link href="/admin" type="primary">进入管理页面</el-link> </div> </template> <script> export default { data() { return { ruleForm: { username: '', password: '', nickname: '', phone: '', email: '', description: '' }, rules: { username: [ { required: true, message: '请输入用户名', trigger: 'blur' }, { min: 4, max: 15, message: '用户名长度必须在4到15个字符', trigger: 'blur' } ], password: [ { required: true, message: '请输入密码', trigger: 'blur' }, { min: 4, max: 15, message: '密码长度必须在4到15个字符', trigger: 'blur' } ] } }; }, methods: { submitForm(formName) { this.$refs[formName].validate((valid) => { if (valid) { // alert('submit!'); let url = 'http://localhost:8080/admins/add-new'; console.log('url >>> ' + url); let data = 'username=' + this.ruleForm.username + '&password=' + this.ruleForm.password + '&nickname=' + this.ruleForm.nickname + '&phone=' + this.ruleForm.phone + '&email=' + this.ruleForm.email + '&description=' + this.ruleForm.description; console.log('data >>> ' + data); this.axios.post(url, data) .then((response) => { console.log(response); let json = response.data; let state = json.state; if (state == 200) { console.log('添加管理员成功!'); this.$message({ message: '添加管理员成功!', type: 'success' }); } else { console.log(json.message); this.$message.error(json.message); } }) .catch(function (error) { console.log(error); }); } else { console.log('error submit!!'); return false; } }); }, resetForm(formName) { this.$refs[formName].resetFields(); } } } </script>
(4)此时启动后端bootdemo的启动类后,
再启动前端vue-project-05工程,
浏览器测试:http://localhost:8081
当在页面输入信息时,点击创建,会弹出框显示创建成功!
此时控制台显示:
(1) 通常,客户端向服务器端发送请求时,请求参数可以有2种形式, 第1种是直接通过&拼接各参数与值, 例如: //username=root&password=123456&nickname=jackson&phone=13800138001&email=jackson@baidu.com&description=none let data = 'username=' + this.ruleForm.username + '&password=' + this.ruleForm.password + '&nickname=' + this.ruleForm.nickname + '&phone=' + this.ruleForm.phone + '&email=' + this.ruleForm.email + '&description=' + this.ruleForm.description; 第2种方式是使用JSON语法来组织各参数与值,例如: 将 vue-project-05: 在LoginView.vue改动【客户端提交请求参数的格式】: // let data = { // 'username': this.ruleForm.username, // 'root' // 'password': this.ruleForm.password, // '123456' // 'nickname': this.ruleForm.nickname, // 'jackson' // 'phone': this.ruleForm.phone, // '13800138001' // 'email': this.ruleForm.email, // 'jackson@baidu.com' // 'description': this.ruleForm.description // 'none' // }; ——————————————————————————————————————————————————————————————————— (2)在 bootdemo工程: AdminController: addNew方法的注解改为:【 @PostMapping("/add-new") 】 ——————————————————————————————————————————————————————————————————— 具体使用哪种做法,取决于服务器端的设计: - 如果服务器端处理请求的方法中,在参数前添加了@RequestBody, 则允许使用以上第2种做法(JSON数据)提交请求参数,不允许使用以上第1种做法(使用&拼接) - 如果没有使用@RequestBody,则只能使用以上第1种做法 ———————————————————————————————————————————————————————————————————
正常的项目开发流程大致是:
- 先整理出当前项目涉及的数据的类型
例如:电商类包含用户、商品、购物车、订单等
- 再列举各种数据类型涉及的数据操作
例如:用户类型涉及注册、登录等
- 再挑选相对简单的数据类型先处理
简单的易于实现,且可以积累经验
- 在各数据类型涉及的数据操作中,大致遵循增、查、删、改的开发顺序
-只有先增,还可能查、删、改
-只有查了以后,才能明确有哪些数据,才便于实现删、改
-删和改相比,删一般更加简单,所以先开发删,再开发改
- 在开发具体的数据操作时,应该大致遵循:
持久层 >> 业务逻辑层 >> 控制器层 >> 前端页面的开发顺序
如果是整个项目第1次开发持久层,在Spring Boot项目中,需要配置:
- 使用@MapperScan配置接口所在的根包
- 在配置文件中通过mybatis.mapper-locations配置XML文件的位置
如果第1次处理某种类型数据的持久层访问,需要:
- 创建接口
- 创建XML文件
本次需要开发的“管理员登录”并不需要再做以上操作
需要执行的sql语句大致是:select * from ams_admin where usernam=?
由于在ams_admin表中有大量字段,同时,不允许使用星号表示字段列表,则以上sql语句应该细化为:
:
select id,username,password,nickname,avatar,is_enable from ams_admin where username=?
例如在idea中的ams_admin表格中查询【存在的:admin777】数据
select id,username,password from ams_admin where username='AdmIn777';
提升:
理论上,还应该查出login_count
当登录成功后,还应该更新login_count、gmt_last_login等数据,此次暂不考虑
提示: 所有的查询结果,都应该使用VO类,而不要使用实体类, 根据阿里的开发规范,每张数据表中都应该有id、gmt_create、gmt_modified这3个字段, 而 gmt_create gmt_modified这两个字段都是用于特殊情况下排查问题的, 一般情况下均不会使用,所以,如果使用实体类,必然存在多余的属性, 同时,由于不适用星号作为字段列表,则一般也不会查询这两个字段的值, 会导致实体类对象中永远至少存在2个属性为null。 根据以上提示,以前已经写好的getByUsername()是不规范的, 应该调整已存在此方法,本次并不需要添加新的抽象方法。 则先创建cn.tedu.boot.demo.pojo.vo.AdminSimpleVO类,添加此次查询时需要的属性: package cn.tedu.boot.demo.pojo.vo; @Data public class AdminSimpleVO implements Serializable { private Long id; private String username; private String password; private String nickname; private String avatar; private Integer isEnable; } 然后,在AdminMapper接口文件中, 将原有的Admin getByUsername(String username);改为: ——————>AdminSimpleVO getByUsername(String username); 注意:一旦修改了原有代码,则调用了原方法的代码都会出现错误,包括: - 测试 - 业务逻辑层的实现类 应该及时修改错误的代码,但是,由于此时还未完成SQL配置,所以,相关代码暂时并不能运行。
在AdminMapper.xml: - 删除<sql>节点中不必查询的字段,————>注意:此处的字段列表最后不要有多余的逗号 - 修改<resultMap> - 在<resultMap>节点下,删除不必要的配置 <select id="getByUsername" resultMap="BaseResultMap"> select <include refid="BaseQueryFields" /> from ams_admin where username=#{username} </select> <sql id="BaseQueryFields"> <if test="true"> id, username, password, nickname, avatar, is_enable </if> </sql> <resultMap id="BaseResultMap" type="cn.tedu.boot.demo.pojo.vo.AdminSimpleVO"> <id column="id" property="id" /> <result column="username" property="username" /> <result column="password" property="password" /> <result column="nickname" property="nickname" /> <result column="avatar" property="avatar" /> <result column="is_enable" property="isEnable" /> </resultMap>
此次并不需要编写新的测试,使用原有的测试即可!
注意:由于本次是修改了原“增加管理员”就已经使用的功能,应该检查原功能是否可以正常运行。
在AdminMapperTests :测试testGetByUsername:分别测试存在的数据和不存在的数据:
@Test
void testGetByUsername() {
String username = "admin77777";
AdminSimpleVO admin = mapper.getByUsername(username);
System.out.println("admin = " + admin);
}
测试发现,若添加一个不存在的用户名,控制台显示:【admin = null】
如果第1次处理某种类型数据的业务逻辑层访问,需要:
- 创建接口
- 创建类,实现接口,并在类上添加@Service注解
本次需要开发的“管理员登录”并不需要再做以上操作
在设计抽象方法时, 如果参数的数量超过1个, 且多个参数具有相关性(是否都是客户端提交的,或是否都是控制器传递过来的等),就应该封装! 在处理登录时,需要客户端提交用户名和密码,则可以将用户名、密码封装起来: 在 【cn.tedu.boot.demo.pojo.dto】下创建AdminLoginDTO: package cn.tedu.boot.demo.pojo.dto; @Data public class AdminLoginDTO implements Serializable { private String username; private String password; } 在IAdminService中添加抽象方法: AdminSimpleVO login(AdminLoginDTO adminLoginDTO); 此时有一个关联错误,因为接口里添加了方法但没有实现
此次业务执行过程中,可能会出现: - 用户名不存在,导致无法登录 - 用户状态为【禁用】,导致无法登录 - 密码错误,导致无法登录 关于用户名不存在的问题,可以自行创建新的异常类,例如: 在cn.tedu.boot.demo.ex包下: 创建UserNotFoundException类————>表示用户数据不存在的异常, 继承自ServiceException,且添加5款基于父类的构造方法: package cn.tedu.boot.demo.ex; public class UserNotFoundException extends ServiceException { // 自动生成5个构造方法 } 再创建UserStateException表示用户状态异常,继承自ServiceException: package cn.tedu.boot.demo.ex; public class UserStateException extends ServiceException { // 自动生成5个构造方法 } 再创建PasswordNotMatchException表示密码错误异常,继承自ServiceException: package cn.tedu.boot.demo.ex; public class PasswordNotMatchException extends ServiceException { // 自动生成5个构造方法 } 在 AdminServiceImpl 添加login方法: 登录过程大致是: public AdminSimpleVO login(AdminLoginDTO adminLoginDTO) { // 通过参数得到尝试登录的用户名 // 调用adminMapper.getByUsername()方法查询 // 判断查询结果是否为null // 是:表示用户名不存在,则抛出UserNotFoundException异常 // 【如果程序可以执行到此步,则可以确定未抛出异常,即查询结果不为null】 // 【以下可视为:存在与用户名匹配的管理员数据】 // 判断查询结果中的isEnable属性值是否不为1 // 是:表示此用户状态是【禁用】的,则抛出UserStateException异常 // 【如果程序可以执行到此步,表示此用户状态是【启用】的】 // 从参数中取出此次登录时客户端提交的密码 // 调用PasswordEncoder对象的matches()方法,对客户端提交的密码和查询结果中的密码进行验证 // 判断以上验证结果 // true:密码正确,视为登录成功 // -- 将查询结果中的password、isEnable设置为null,避免响应到客户端 // -- 返回查询结果 // false:密码错误,视为登录失败,则抛出PasswordNotMatchException异常 return null; }
完成上一步骤的代码: 在AdminServiceImpl中重写接口中新增的抽象方法: @Override public AdminSimpleVO login(AdminLoginDTO adminLoginDTO) { // 日志 log.debug("即将处理管理员登录的业务,尝试登录的管理员信息:{}", adminLoginDTO); log.debug("第1步:判断用户名是否存在"); // 通过参数得到尝试登录的用户名 String username = adminLoginDTO.getUsername(); // 调用adminMapper.getByUsername()方法查询 AdminSimpleVO queryResult = adminMapper.getByUsername(username); // 判断查询结果是否为null if (queryResult == null) { // 是:表示用户名不存在,则抛出UserNotFoundException异常 log.warn("登录失败,用户名不存在!"); throw new UserNotFoundException("登录失败,用户名不存在!"); } // 【如果程序可以执行到此步,则可以确定未抛出异常,即查询结果不为null】 // 【以下可视为:存在与用户名匹配的管理员数据】 // 判断查询结果中的isEnable属性值是否不为1 log.debug("第2步:判断用户名是否存在此账号是否处于启用状态"); if (queryResult.getIsEnable() != 1) { // 是:表示此用户状态是【禁用】的,则抛出UserStateException异常 log.warn("登录失败,此账号已经被禁用!"); throw new UserStateException("登录失败,此账号已经被禁用!"); } // 【如果程序可以执行到此步,表示此用户状态是【启用】的】 // 从参数中取出此次登录时客户端提交的密码 log.debug("第3步:判断密码是否正确"); String rawPassword = adminLoginDTO.getPassword(); // 调用PasswordEncoder对象的matches()方法,对客户端提交的密码和查询结果中的密码进行验证 boolean matchResult = passwordEncoder.matches(rawPassword, queryResult.getPassword()); // 判断以上验证结果 if (!matchResult) { // false:密码错误,视为登录失败,则抛出PasswordNotMatchException异常 log.warn("登录失败,密码错误!"); throw new PasswordNotMatchException("登录失败,密码错误!"); } // true:密码正确,视为登录成功 // 将查询结果中的password、isEnable设置为null,避免响应到客户端 queryResult.setPassword(null); queryResult.setIsEnable(null); // 返回查询结果 log.debug("登录成功,即将返回:{}", queryResult); return queryResult; }
测试 DigestTests 中的testPasswordEncoderMatches方法: 可以看到返回true 在bootdemo工程下的src/test下创建包resources, 分别创建: insert_data.sql文件:在后边添加is_enable,下边添加/和0以保证不出现空指针异常 insert into ams_admin (username, password, is_enable) values ('admin001', '1a120503d2434840b84b67b5b795deaf0b3757bcb485546263c505b5e4578475', 1) , ('admin002', '1a120503d2434840b84b67b5b795deaf0b3757bcb485546263c505b5e4578475', 1) , ('admin003', '1a120503d2434840b84b67b5b795deaf0b3757bcb485546263c505b5e4578475', 1) , ('admin004', '1a120503d2434840b84b67b5b795deaf0b3757bcb485546263c505b5e4578475', 1) , ('admin005', '1a120503d2434840b84b67b5b795deaf0b3757bcb485546263c505b5e4578475', 0); truncate.sql文件: 【truncate ams_admin;】 在 AdminServiceTests 中添加: 1个成功的测试 和 3个失败的测试: @Sql({"classpath:truncate.sql", "classpath:insert_data.sql"}) @Test public void testLoginSuccessfully() { // 测试数据 String username = "admin001"; String password = "123456"; AdminLoginDTO adminLoginDTO = new AdminLoginDTO(); adminLoginDTO.setUsername(username); adminLoginDTO.setPassword(password); // 断言不会抛出异常 assertDoesNotThrow(() -> { // 执行测试 AdminSimpleVO adminSimpleVO = service.login(adminLoginDTO); log.debug("登录成功:{}", adminSimpleVO); // 断言测试结果 assertEquals(1L, adminSimpleVO.getId()); assertNull(adminSimpleVO.getPassword()); assertNull(adminSimpleVO.getIsEnable()); }); } @Sql({"classpath:truncate.sql"}) @Test public void testLoginFailBecauseUserNotFound() { // 测试数据 String username = "admin001"; String password = "123456"; AdminLoginDTO adminLoginDTO = new AdminLoginDTO(); adminLoginDTO.setUsername(username); adminLoginDTO.setPassword(password); // 断言会抛出UserNotFoundException assertThrows(UserNotFoundException.class, () -> { // 执行测试 service.login(adminLoginDTO); }); } @Sql({"classpath:truncate.sql", "classpath:insert_data.sql"}) @Test public void testLoginFailBecauseUserDisabled() { // 测试数据 String username = "admin005"; // 通过SQL脚本插入的此数据,is_enable为0 String password = "123456"; AdminLoginDTO adminLoginDTO = new AdminLoginDTO(); adminLoginDTO.setUsername(username); adminLoginDTO.setPassword(password); // 断言会抛出UserStateException assertThrows(UserStateException.class, () -> { // 执行测试 service.login(adminLoginDTO); }); } @Sql({"classpath:truncate.sql", "classpath:insert_data.sql"}) @Test public void testLoginFailBecausePasswordNotMatch() { // 测试数据 String username = "admin001"; String password = "000000000000000000"; AdminLoginDTO adminLoginDTO = new AdminLoginDTO(); adminLoginDTO.setUsername(username); adminLoginDTO.setPassword(password); // 断言会抛出PasswordNotMatchException assertThrows(PasswordNotMatchException.class, () -> { // 执行测试 service.login(adminLoginDTO); }); } 此时运行整个测试类,每个方法都会运行成功 再次测试整个类,还是都会运行成功 当然,我们还可以在上面【(3.4)中的:AdminServiceImpl】中添加日志: log.debug("第1步:判断用户名是否存在"); log.debug("第2步:判断用户名是否存在此账号是否处于启用状态"); log.debug("第3步:判断密码是否正确"); 来观察关键性数据的输出
如果是整个项目第1次开发控制器层,需要:
- 创建统一处理异常的类
- 添加@RestControllerAdvice
- 创建统一的响应结果类型及相关类型
- 例如:JsonResult及State
如果第1次处理某种类型数据的控制器层访问,需要:
- 创建控制器类
- 添加@RestController
- 添加@RequestMapping
本次需要开发的“管理员登录”并不需要再做以上操作
在 AdminLoginDTO 的各属性上添加:【验证基本有效性的注解】,例如: package cn.tedu.boot.demo.pojo.dto; import lombok.Data; import javax.validation.constraints.NotNull; import java.io.Serializable; @Data public class AdminLoginDTO implements Serializable { @NotNull(message = "登录失败,请提交用户名!") // 新增 private String username; @NotNull(message = "登录失败,请提交密码!") // 新增 private String password; } —————————————————————————————————————————————————————————————————————— 在 AdminController 中添加新增 处理请求 的方法: @RequestMapping("/login") // 暂时使用@RequestMapping,后续改成@PostMapping public JsonResult<AdminSimpleVO> login(@Validated AdminLoginDTO adminLoginDTO) { AdminSimpleVO adminSimpleVO = adminService.login(adminLoginDTO); return JsonResult.ok(adminSimpleVO); }
先在State中添加新创建的异常对应枚举: public enum State { OK(200), ERR_USERNAME(201), ERR_PASSWORD(202), ERR_STATE(203), // 新增 ERR_BAD_REQUEST(400), ERR_INSERT(500); // ===== 原有其它代码 ===== } —————————————————————————————————————————————————————————————————————— 在 GlobalExceptionHandler 的: handleServiceException()方法中: 添加更多分支,针对各异常进行判断,并响应不同结果: @ExceptionHandler(ServiceException.class) public JsonResult<Void> handleServiceException(ServiceException e) { if (e instanceof UsernameDuplicateException) { return JsonResult.fail(State.ERR_USERNAME, e.getMessage()); } else if (e instanceof UserNotFoundException) { //从此行起,是新增的 return JsonResult.fail(State.ERR_USERNAME, e.getMessage()); } else if (e instanceof UserStateException) { return JsonResult.fail(State.ERR_STATE, e.getMessage()); } else if (e instanceof PasswordNotMatchException) { return JsonResult.fail(State.ERR_PASSWORD, e.getMessage()); //到这行新增结束 } else { return JsonResult.fail(State.ERR_INSERT, e.getMessage()); } }
启动 bootdemo 项目, 我们发现数据库中的ams_admin表中依然还是5条数据,和上次测试并无区别: 可以看到is_enable状态前4个都是1 第五个是0 进行各种测试: /1. http://localhost:8080/admins/login?username=admin001&password=123456 类似的URL测试访问。 页面显示:【 {"state":200,"data":{"id":1,"username":"admin001"}} 】 /2.若我们在数据库ams_adminz中添加: 用户001的:nickname为: 刘老师 avatar为: 尚未设置头像 再次测试 http://localhost:8080/admins/login?username=admin001&password=123456, 页面显示: 【 {"state":200,"data":{"id":1,"username":"admin001","nickname":"刘老师","avatar":"尚未设置头像"}} 】 /3.若测试的网址密码输入一个不正确的,则页面显示{"state":202,"message":"登录失败,密码错误!"} /4.若测试的网址输入一个禁用的用户005,(http://localhost:8080/admins/login?username=admin005&password=123456) 则页面显示{"state":203,"message":"登录失败,此账号已经被禁用!"} /5.若测试的网址输入 http://localhost:8080/admins/login, 则页面显示:{"state":400,"message":"登录失败,请提交用户名!"} /6.若测试的网址在login后随便输入正确或者不正确的用户名都无所谓,因为网址输入到: 【...username=admin001】是控制层处理,并没有到达service层 测试:http://localhost:8080/admins/login?username=admin001 页面显示:{"state":400,"message":"登录失败,请提交密码!"} 因为测试的url上没有提交密码 注意:在测试访问之前,必须保证数据表中的数据状态是符合预期的。
vue-project-05工程中: 把LoginView.vue 复制粘贴在本包views下,并重命名为:AdminAddNewView 再把LoginView.vue 改为管理员登录的 测试http://localhost:8080/admins/login?username=admin001&password=123456 页面显示:{"state":200,"data":{"id":1,"username":"admin001"}} 若在数据库的ams_admin中: 在用户001中添加: nickname=刘老师 avatar=暂未设置头像 再次测试上边的网址:页面显示:{"state":200,"data":{"id":1,"username":"admin001","nickname":"刘老师","avatar":"暂未设置头像"}} 先启动bootdemo服务端; 再启动前端(idea界面的下边Terminal 用指令npm run serve 启动vue-project-05) 因为npm启动时,若之前启动的服务端使用了8080端口,此时再次启动会顺延至8081端口 浏览器访问:http://localhost:8081/ 跳转到 用户登录 页面 右键检查——>点击控制台 输入信息 admin001 123456点击登录,会弹出框:管理员登录成功 控制台显示: {data: {…}, status: 200, statusText: '', headers: {…}, config: {…}, …} LoginView.vue?e922:60 管理员登录成功! LoginView.vue?e922:61 管理员id:1 LoginView.vue?e922:62 管理员用户名:admin001 LoginView.vue?e922:63 管理员昵称:刘老师 LoginView.vue?e922:64 管理员头像:暂未设置头像 LoginView.vue?e922:46 url >>> http://localhost:8080/admins/login LoginView.vue?e922:53 data >>> username=admin002&password=123456 输入信息 admin002 123456点击登录,会弹出框:管理员登录成功 控制台显示: {data: {…}, status: 200, statusText: '', headers: {…}, config: {…}, …} LoginView.vue?e922:60 管理员登录成功! LoginView.vue?e922:61 管理员id:2 LoginView.vue?e922:62 管理员用户名:admin002 LoginView.vue?e922:63 管理员昵称:undefined LoginView.vue?e922:64 管理员头像:undefined 通过输入信息不同的测试,例如填写不正确的密码,不存在的用户信息,会弹出不同的提示框!
像上面那样测试就很麻烦,
我们在AdminController中的login方法上:
不应使用@RequestMapping注解(用 @RequestMapping方便我们在地址栏输入不同的测试数据),
应该使用 @PostMapping 注解,
这种登录的请求应该就是post的,
(1)关于控制器层,也可以写测试方式进行测试, 【在Spring Boot项目中,可以使用 MockMvc 进行模拟测试】,例如: 我们在bootdemo工程下:src/test/绿色java/cn.tedu.boot.demo下: 创建包:controller下:创建AdminControllerTsets类: 并添加 @SpringBootTest 注解: 首先 测试登录成功 package cn.tedu.boot.demo.controller; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; /** * 在整个测试中,自动装配mockMvc的配置 * 用来模拟向http发送请求,获得响应 */ @SpringBootTest @AutoConfigureMockMvc // 自动配置MockMvc public class AdminControllerTests { @Autowired MockMvc mockMvc; // Mock:模拟 /** * 测试登录成功 * @throws Exception */ @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)再添加一个【测试登录失败,因为找不到用户】的方法: /** * 测试登录失败,因为找不到用户 * @throws Exception */ @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
(1)Knife4j是一款可以提供在线API文档的框架,是基于Swagger框架实现的。 在Spring Boot项目中:bootdemo工程:pom.xml中: 使用Knife4j需要添加依赖knife4j-spring-boot-starter: <!-- 添加Knife4j依赖 --> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>2.0.9</version> </dependency> —————————————————————————————————————————————————————————————————————————— (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 { /** * 【重要】指定Controller包路径 */ 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文档--商品管理"; /** * 服务条款URL */ private String termsOfServiceUrl = "http://www.apache.org/licenses/LICENSE-2.0"; /** * 联系人 */ private String contactName = "Java教学研发部"; /** * 联系网址 */ private String contactUrl = "http://java.tedu.cn"; /** * 联系邮箱 */ private String contactEmail = "java@tedu.cn"; /** * 版本号 */ private String version = "1.0.0"; @Autowired private OpenApiExtensionResolver openApiExtensionResolver; @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<AdminSimpleVO> 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<AdminSimpleVO> 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<T> 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)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。