我正在参加「启航方案」
1. 关于Spring Boot
Spring Boot是Spring官方的一个产品,其本质上是一个依据Maven的、以Spring结构作为根底的进阶结构,很好的支撑了干流的其它结构,并默许完成了许多的装备,其间心思想是“约定大于装备”。
2. 创立Spring Boot工程
在IntelliJ IDEA中,在创立导游中选择Spring Initializer即可开端创立Spring Boot工程,在创立导游的界面中,需求重视的部分有:
- Group Id
- Artifact Id
以上2个值会共同构成一个Package name,假如Artifact Id的名字中有减号,在Package name中会去除,引荐手动增加小数点进行分隔。
因为Spring Boot官方更新版别的频率十分高,在创立项目时,随意选取某个版别均可,当项目创立成功后,引荐翻开pom.xml
,将<parent>
中的<version>
(即Spring Boot父项目的版别)改成熟悉的版别,例如:2.5.9
在创立进程中,还能够在创立导游的界面中勾选所需求依靠项,假如创立时没有勾选,也能够在创立工程之后手动在pom.xml
中增加。
3. Spring Boot工程的结构
因为Spring Boot工程本质上便是一个Maven工程,所以,目录结构根本上没有差异。
与一般Maven工程最大的不同在于:Spring Boot工程在src\main\java
和src\test\java
下默许现已存在Package,是创立项目时指定的Package,**需求留意:此Package现已被装备为Spring履行组件扫描的根包,所以,在编写代码时,一切的组件类都有必要放在此包或这今后代包中!**一般,引荐将一切的类(及接口)都创立在此包及这今后代包下。
在src\main\java
下的根包下,默许就现已存在某个类,其类名是创立项目时指定的Artifact与Application
单词的组合,例如BootDemoApplication
,此类中有main()
办法,履行此类的main()
就会发动整个项目,假如当时项目是Web项目,还会主动将项目布置到Web服务器并发动服务器,所以,此类一般也称之为“发动类”。
在发动类上,默许增加了@SpringBootApplication
注解,此注解的元注解中包括@SpringBootConfiguration
,而@SpringBootConfiguration
的元注解中包括@Configuration
,所以,发动类自身也是装备类!所以,答应将@Bean
办法写在此类中,或者某些与装备相关的注解也能够增加在此类上!
在src\test\java
下的根包下,默许就现已存在某个类,其类名是在发动类的称号根底上增加了Tests
单词的组合,例如BootDemoApplicationTests
,此类默许没有增加public
权限,乃至其内部的默许的测验办法也是默许权限的,此测验类上增加了@SpringBootTest
注解,其元注解中包括@ExtendWith(SpringExtension.class)
,与运用spring-test
时的@SpringJUnitTest
注解中的元注解相同,所以,@SpringBootTest
注解也会使得当时测验类在履行测验办法之前是加载了Spring环境的,在实践编写测验时,能够经过主动安装得到任何已存在于Spring容器中的方针,在各测验办法中只需求重视被测验的方针即可。
在pom.xml
中,默许现已增加了spring-boot-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
注解)。
小结:
- 创立项目后默许的Package不要修正,防止犯错
- 在编码进程中,自行创立的一切类、接口均放在默许的Package或这今后代包中
- 在
src\main\java
下默许已存在XxxApplication
是发动类,履行此类中的main()
办法就会发动整个项目 - 发动类自身也是装备类
- 装备都应该编写到
src\main\resources
下的application.properties
中,Spring Boot会主动读取 - 测验类也有必要放在
src\test\java
下的默许Package或这今后代包中 - 在测验类上增加
@SpringBootTest
注解,则其间的测验办法履行之前会主动加载Spring环境及当时项目的装备,能够在测验类中运用主动安装
4. 在Spring Boot工程中运用Mybatis
需求增加相关依靠项:
mysql-connector-java
mybatis-spring-boot-starter
其依靠的代码为:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
阐明:在Spring Boot工程,许多依靠项都是不需求显式的指定版别号的,因为在父项目中现已对这些依靠项的版别进行了办理(装备版别号),假如必定需求运用特定的版别,也能够自行增加<version>
节点进行装备
阐明:在依靠项的源代码中,当<scope>
的值为runtime
时,表明此依靠项是运转进程中需求的,可是,在编译时并不需求参与编译
需求留意:当增加了以上数据库编程的依靠后,假如发动项目,将失利!
因为增加了数据库编程的依靠项后,Spring Boot就会测验主动安装数据源(DataSource
)等方针,安装时所需的衔接数据库的装备信息(例如URL、登录数据库的用户名和暗码)应该是装备在application.properties
中的,可是,假如尚未装备,就会导致失利!
关于衔接数据库的装备信息,Spring Boot要求对应的特点名是:
# 衔接数据库的URL
spring.datasource.url=???
# 登录数据库的用户名
spring.datasource.username=???
# 登录数据库的暗码
spring.datasource.password=???
在装备时,也有必要运用以上特点名进行装备,则Spring Boot会主动读取这些特点对应的值,用于创立数据源方针!
例如,装备为:
# 衔接数据库的URL
spring.datasource.url=jdbc:mysql://localhost:3306/mall_ams?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
# 登录数据库的用户名
spring.datasource.username=root
# 登录数据库的暗码
spring.datasource.password=1234
因为Spring Boot在发动时仅仅加载以上装备,并不会实践的衔接到数据库,所以,当以上装备存在时,发动就不会报错,可是,无法查验以上装备的值是否正确!
能够在测验类中增加测验办法,测验衔接数据库,以查验以上装备值是否正确:
@SpringBootTest
class BootDemoApplicationTests {
@Autowired
DataSource dataSource;
@Test
void testGetConnection() throws Exception {
System.out.println(dataSource.getConnection());
}
}
假如以上测验经过,则表明装备值无误,能够正确衔接到数据库,假如测验失利,则表明装备值过错,需查看装备值及本地环境(例如MySQL是否发动、是否已创立对应的数据库等)。
5. 关于Profile装备
在Spring Boot中,对Profile装备有很好的支撑,开发人员能够在src\main\resources
下创立更多的装备文件,这些装备文件的称号应该是application-???.properties
(其间的???
是某个称号,是自界说的)。
例如:
- 仅在开发环境中运用的装备值能够写在
application-dev.properties
中 - 仅在测验环境中运用的装备值能够写在
application-test.properties
中 - 仅在出产环境(项目上线的环境)中运用的装备值能够写在
application-prod.properties
中
当把装备写在以上这类文件后,Spring Boot默许并不会运用以上这些文件中的装备,当需求运用某个装备时,需求在application.properties
中激活某个Profile装备,例如:
# 激活Profile装备
spring.profiles.active=dev
提示:以上装备值中的dev
是需求激活的装备文件的文件名后缀,当装备为dev
时,就会激活application-dev.properties
,同理,假如以上装备值为test
,就会激活application-test.properties
。
6. 关于YAML装备
Spring Boot也支撑运用YAML装备,在开发实践中,YAML的装备也运用得比较多。
YAML装备便是把原有的.properties
装备的扩展改为yml
。
YAML装备本来并不是Spring系列结构内置的装备语法,假如在项目中需求运用这种语法进行装备,解析这类文件需求增加相关依靠,在Spring Boot中默许已增加此依靠。
在YAML装备中,本来在.properties
的装备体现为运用多个小数点分隔的装备将改为换行并运用2个空格缩进的语法,换行前的部分运用冒号表明完毕,最终的特点名与值之间运用冒号和1个空格进行分隔,假如有多条特点在.properties
文件中特点名有重复的前缀,在yml
中不用也不能重复写。
例如,本来在.properties
中装备为:
spring.datasource.username=root
spring.datasource.password=123456
则在yml
文件中装备为:
spring:
datasource:
username: root
password: 123456
提示:在IntelliJ IDEA中编写yml
时,当需求缩进2个空格时,仍能够运用键盘上的TAB键进行缩进,IntelliJ IDEA会主动将其转换为2个空格。
无论是.properties
仍是yml
,仅仅装备文件的扩展名和文件内部的装备语法有差异,关于Spring Boot终究的履行其实没有任何体现上的不同。
7. 运用Druid数据库衔接池
Druid数据库衔接是阿里巴巴团队研制的,在Spring Boot项目中,假如需求显式的指定运用此衔接池,首要,需求在项目中增加依靠:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.20</version>
</dependency>
当增加了此依靠,在项目中需求运用时,需求在装备文件中指定spring.datasource.type
特点,取值为以上依靠项的jar包中的DruidDataSource
类型的全限定名。
例如,在yml
中装备为:
# Spring系列结构的装备
spring:
# 衔接数据库的相关装备
datasource:
# 运用的数据库衔接池类型
type: com.alibaba.druid.pool.DruidDataSource
8. 编写耐久层(数据拜访层)代码
数据耐久化:在开发领域中,评论数据时,一般指定是正在履行或处理的数据,这些数据都是在内存中的,而内存(RAM)的特征包括”一旦断电,数据将全部丢失“,为了让数据永久保存下来,一般会将数据存储到能够永久存储数据的介质中,一般是核算机的硬盘,硬盘上的数据都是以文件的办法存在的,所以,当需求永久保存数据时,能够将数据存储到文本文件中,或存储到XML文件中,或存储到数据库中,这些保存的做法便是数据耐久化,而文本文件、XML文件都不利于完成增修正查中的一切数据拜访操作,而数据库是完成增修正查这4种操作都比较便利的,所以,一般在评论数据耐久化时,默许指的都是运用数据库存储数据。
在项目中,会将代码(各类、接口)区分一些层次,各层用于处理不同的问题,其间,耐久层便是用于处理数据耐久化问题的,乃至,简略来说,耐久层对应的便是数据库编程的相关文件或代码。
现在,运用Mybatis技术完成耐久层编程,需求:
- 编写一次性的根底装备
- 运用
@MapperScan
指定接口地点的Base Package - 指定装备SQL句子的XML文件的方位
- 运用
- 编写每个数据拜访功用的代码
- 在接口中增加有必要的笼统办法
- 可能需求创立相关的POJO类
- 在XML文件中装备笼统办法映射的SQL句子
- 在接口中增加有必要的笼统办法
关于一次性的装备,@MapperScan
注解需求增加在装备类上,有2种做法:
- 直接将此注解增加在发动类上,因为发动类自身也是装备类
- 自行创立装备类,在此装备类上增加
@MapperScan
假如采用以上的第2种做法,则应该在src\main\java
的根包下,创立config.MybatisConfig
类,并在此类运用@MapperScan
注解:
package cn.tedu.boot.demo.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("cn.tedu.boot.demo.mapper")
public class MybatisConfig {
}
别的,关于指定装备SQL句子的XML文件的方位,需求在application.yml
(或application.properties
)中装备mybatis.mapper-locations
特点,例如:
# Mybatis相关装备
mybatis:
# 用于装备SQL句子的XML文件的方位
mapper-locations: classpath:mapper/*.xml
依据以上的装备值,还应该在src/main/resources
下自行创立名为mapper
的文件夹。
至此,关于运用Mybatis完成数据库编程的一次性装备完毕!
接下来,能够运用任何你已知的Mybatis运用办法完成所需的数据拜访。
现在,设定方针为:终究完成”增加办理员账号“的功用。则在数据拜访层需求做到:
- 刺进办理员数据
- 创立
cn.tedu.boot.demo.entity.Admin
类 - 在
cn.tedu.boot.demo.mapper
包(不存在,则创立)下创立AdminMapper
接口,并在接口中声明int insert(Admin admin);
办法 - 在
src/main/resources/mapper
文件夹下经过粘贴得到AdminMapper.xml
文件,在此文件中装备与以上笼统办法映射的SQL句子 - 编写完成后,应该及时测验,测验时,引荐在
src/test/java
的根包下创立mapper.AdminMapperTests
测验类,并在此类中编写测验办法
- 创立
- 依据用户名查询办理员数据
- 后续,在每次刺进数据之前,会调用此功用进行查询,以此保证”重复的用户名不会被增加到数据库中“
- 即便在数据表中用户名现已增加了
unique
,可是,不应该让程序履行到此处
- 即便在数据表中用户名现已增加了
- 在
AdminMapper
接口中增加Admin getByUsername(String username);
办法 - 在
AdminMapper.xml
文件中增加与以上笼统办法映射的SQL句子 - 编写完成后,应该及时测验
- 后续,在每次刺进数据之前,会调用此功用进行查询,以此保证”重复的用户名不会被增加到数据库中“
- 其它问题暂不考虑,例如在
ams_admin
中,其实phone
和email
也是设置了unique
的,假如完好的完成,则还需求增加依据phone
查询办理员的功用,和依据email
查询办理员的功用,在不完成这2个功用的情况下,后续进行测验和运用时,应该不运用重复的phone
和email
值来测验或履行
9. 关于事务逻辑层(Service层)
事务逻辑层是被Controller直接调用的层(Controller不答应直接调用耐久层),一般,在事务逻辑层中编写的代码是为了保证数据的完好性和安全性,使得数据是随着我们设定的规矩而发生或发生变化。
一般,在事务逻辑层的代码会由接口和完成类组件,其间,接口被视为是有必要的
- 引荐运用依据接口的编程办法
- 部分结构在处理某些功用时,会运用依据接口的代理办法,例如Spring JDBC结构在处理事务时
在接口中,声明笼统办法时,仅以操作成功为前提来规划回来值类型(不考虑失利),假如事务在履行进程可能呈现某些失利(不符合所设定的规矩),能够经过抛出反常来表明!
关于抛出的反常,一般是自界说的反常,而且,自界说反常一般是RuntimeException
的子类,主要原因:
- 不用显式的抛出或捕获,因为事务逻辑层的反常永久是抛出的,而控制器层会调用事务逻辑层,在控制器层的Controller中其实也是永久抛出反常的,这些反常会经过Spring MVC一致处理反常的机制进行处理,关于反常的整个进程都是固定流程,所以,没有必要显式抛出或捕获
- 部分结构在处理某些工作时,默许只对
RuntimeException
的后代类进行识别并处理,例如Spring JDBC结构在处理事务时
所以,在实践编写事务逻辑层之前,应该先规划反常,例如先创立ServiceException
类:
package cn.tedu.boot.demo.ex;
public class ServiceException extends RuntimeException {
}
接下来,再创立详细的对应某种“失利”的反常,例如,在增加办理员时,可能因为“用户名现已存在”而失利,则创立对应的UsernameDuplicateException
反常:
package cn.tedu.boot.demo.ex;
public class UsernameDuplicateException extends ServiceException {
}
别的,当刺进数据时,假如回来的受影响行数不是1时,必定是某种过错,则创立对应的刺进数据反常:
package cn.tedu.boot.demo.ex;
public class InsertException extends ServiceException {
}
关于笼统办法的参数,应该规划为客户端提交的数据类型或对应的封装类型,不能够是数据表对应的实体类型!假如运用封装的类型,这品种型在类名上应该增加某种后缀,例如DTO
或其它后缀,例如:
package cn.tedu.boot.demo.pojo.dto;
public class AdminAddNewDTO implements Serializable {
private String username;
private String password;
private String nickname;
private String avatar;
private String phone;
private String email;
private String description;
// Setters & Getters
// hashCode(), equals()
// toString()
}
然后,在cn.tedu.boot.demo.service
包下声明接口及笼统办法:
package cn.tedu.boot.demo.service;
public interface IAdminService {
void addNew(AdminAddNewDTO adminAddNewDTO);
}
并在以上service
包下创立impl
子包,再创立AdminServiceImpl
类:
package cn.tedu.boot.demo.service.impl;
@Service // @Component, @Controller, @Repository
public class AdminServiceImpl implements IAdminService {
@Autowired
private AdminMapper adminMapper;
@Override
public void addNew(AdminAddNewDTO adminAddNewDTO) {
// 经过参数获取用户名
// 调用adminMapper的Admin getByUsername(String username)办法履行查询
// 判别查询成果是否不为null
// -- 是:表明用户名现已被占用,则抛出UsernameDuplicateException
// 经过参数获取原暗码
// 经过加密办法,得到加密后的暗码encodedPassword
// 暂时不加密,写为String encodedPassword = adminAddNewDTO.getPassword();
// 创立当时时刻方针now > LocalDateTime.now()
// 创立Admin方针
// 补全Admin方针的特点值:经过参数获取username,nickname……
// 补全Admin方针的特点值:password > encodedPassword
// 补全Admin方针的特点值:isEnable > 1
// 补全Admin方针的特点值:lastLoginIp > null
// 补全Admin方针的特点值:loginCount > 0
// 补全Admin方针的特点值:gmtLastLogin > null
// 补全Admin方针的特点值:gmtCreate > now
// 补全Admin方针的特点值:gmtModified > now
// 调用adminMapper的insert(Admin admin)办法刺进办理员数据,获取回来值
// 判别以上回来的成果是否不为1,抛出InsertException反常
}
}
以上事务代码的完成为:
package cn.tedu.boot.demo.service.impl;
import cn.tedu.boot.demo.entity.Admin;
import cn.tedu.boot.demo.ex.InsertException;
import cn.tedu.boot.demo.ex.UsernameDuplicateException;
import cn.tedu.boot.demo.mapper.AdminMapper;
import cn.tedu.boot.demo.pojo.dto.AdminAddNewDTO;
import cn.tedu.boot.demo.service.IAdminService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@Service
public class AdminServiceImpl implements IAdminService {
@Autowired
private AdminMapper adminMapper;
@Override
public void addNew(AdminAddNewDTO adminAddNewDTO) {
// 经过参数获取用户名
String username = adminAddNewDTO.getUsername();
// 调用adminMapper的Admin getByUsername(String username)办法履行查询
Admin queryResult = adminMapper.getByUsername(username);
// 判别查询成果是否不为null
if (queryResult != null) {
// 是:表明用户名现已被占用,则抛出UsernameDuplicateException
throw new UsernameDuplicateException();
}
// 经过参数获取原暗码
String password = adminAddNewDTO.getPassword();
// 经过加密办法,得到加密后的暗码encodedPassword
String encodedPassword = password;
// 创立当时时刻方针now > LocalDateTime.now()
LocalDateTime now = LocalDateTime.now();
// 创立Admin方针
Admin admin = new Admin();
// 补全Admin方针的特点值:经过参数获取username,nickname……
admin.setUsername(username);
admin.setNickname(adminAddNewDTO.getNickname());
admin.setAvatar(adminAddNewDTO.getAvatar());
admin.setPhone(adminAddNewDTO.getPhone());
admin.setEmail(adminAddNewDTO.getEmail());
admin.setDescription(adminAddNewDTO.getDescription());
// 以上这些从一个方针中把特点赋到另一个方针中,还能够运用:
// BeanUtils.copyProperties(adminAddNewDTO, admin);
// 补全Admin方针的特点值:password > encodedPassword
admin.setPassword(encodedPassword);
// 补全Admin方针的特点值:isEnable > 1
admin.setIsEnable(1);
// 补全Admin方针的特点值:lastLoginIp > null
// 补全Admin方针的特点值:loginCount > 0
admin.setLoginCount(0);
// 补全Admin方针的特点值:gmtLastLogin > null
// 补全Admin方针的特点值:gmtCreate > now
admin.setGmtCreate(now);
// 补全Admin方针的特点值:gmtModified > now
admin.setGmtModified(now);
// 调用adminMapper的insert(Admin admin)办法刺进办理员数据,获取回来值
int rows = adminMapper.insert(admin);
// 判别以上回来的成果是否不为1,抛出InsertException反常
if (rows != 1) {
throw new InsertException();
}
}
}
以上代码未完成对暗码的加密处理!关于暗码加密,相关的代码应该界说在别的某个类中,不应该直接将加密进程编写在以上代码中,因为加密的代码需求在多处运用(增加用户、用户登录、修正暗码等),而且,从分工的角度上来看,也不应该是事务逻辑层的使命!所以,在cn.tedu.boot.demo.util
(包不存在,则创立)下创立PasswordEncoder
类,用于处理暗码加密:
package cn.tedu.boot.demo.util;
@Component
public class PasswordEncoder {
public String encode(String rawPassword) {
return "aaa" + rawPassword + "aaa";
}
}
完成后,需求在AdminServiceImpl
中主动安装以上PasswordEncoder
,并在需求加密时调用PasswordEncoder
方针的encode()
办法。
10. 运用Lombok结构
在编写POJO类型(包括实体类、VO、DTO等)时,都有一致的编码标准,例如:
- 特点都是私有的
- 一切特点都有对应的Setter & Getter办法
- 应该重写
equals()
和hashCode()
办法,以保证:假如2个方针的字面值完全相同,则equals()
对比成果为true
,且hashCode()
回来值相同,假如2个方针的字面值不相同,则equals()
对比成果为false
,且hashCode()
回来值不同 - 完成
Serializable
接口
别的,为了便于观察方针的各特点值,一般还会重写toString()
办法。
因为以上操作办法十分固定,且触及的代码量尽管不难,可是篇幅较长,而且,当类中的特点需求修正时(包括修正原有特点、或增加新特点、删去原有特点),对应的其它办法都需求修正(或从头生成),办理起来比较费事。
运用Lombok结构能够极大的简化这些操作,此结构能够经过注解的办法,在编译期来生成Setters & Getters、equals()
、hashCode()
、toString()
,乃至生成结构办法等,所以,一旦运用此结构,开发人员就只需求在类中声明各特点、完成Serializable
、增加Lombok指定的注解即可。
在Spring Boot中,增加Lombok依靠,能够在创立项目时勾选,也能够后期自行增加,依靠项的代码为:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
完成后,在各POJO类型中,将不再需求在源代码增加Setters & Getters、equals()
、hashCode()
、toString()
这些办法,只需求在POJO类上增加@Data
注解即可!
当增加@Data
注解,且删去相关办法后,因为源代码中没有相关办法,则调用了相关代码的办法可能会报错,可是,并不影响程序运转!
为了防止IntelliJ IDEA判别失误而提示了正告和过错,引荐安装Lombok插件,可参阅:
- doc.canglaoshi.org/doc/idea_lo…
再次提示:无论是否安装插件,都不影响代码的编写和运转!
11. Slf4j日志结构
在开发实践中,不答应运用System.out.println()
或相似的输出句子来输出显现要害数据(中心数据、敏感数据等),因为,假如是这样运用,无论是在开发环境,仍是测验环境,仍是出产环境中,这些输出句子都将输出相关信息,而删去或增加这些输出句子的操作本钱比较高,操作可行性低。
引荐的做法是运用日志结构来输出相关信息!
当增加了Lombok依靠后,能够在需求运用日志的类上增加@Slf4j
注解,然后,在类的恣意中,均可运用名为log
的变量,且调用其办法来输出日志(名为log
的变量也是Lombok结构在编译期主动弥补的声明并创立方针)!
在Slf4j日志结构中,将日志的可显现等级依据其重要程度(严重程度)由低到高分为:
- trace:盯梢信息
- debug:调试信息
- info:一般信息,一般不触及要害流程和敏感数据
- warn:正告信息,一般代码能够运转,但不行完美,或不标准
- error:过错信息
在装备文件中,能够经过logging.level.包名.类名
来设置当时类的日志显现等级,例如:
logging.level.cn.tedu.boot.demo.service.impl.AdminServiceImpl: info
当设置了显现的日志等级后,仅显现设置等级和更重要的等级的日志,例如,设置为info
时,只显现info
、warn
、error
,不会显现debug
、trace
等级的日志!
当输出日志时,经过log
变量调用trace()
办法输出的日志便是trace
等级的,调用debug()
办法输出的日志便是debug()
等级的,以此类推,可调用的办法还有info()
、warn()
、error()
。
在开发实践中,要害数据和敏感数据都应该经过trace()
或debug()
进行输出,在开发环境中,能够将日志的显现等级设置为trace
,则会显现一切日志,当需求交付到出产环境中时,只需求将日志的显现等级调整为info
即可!
默许情况下,日志的显现等级是info
,所以,即便没有在装备文件中进行正确的装备,一切info、warn、error等级的日志都会输出显现。
在装备时,特点称号中的logging.level
部分是有必要的,在这今后,有必要写至少1级包名,例如:
logging.level.cn: trace
以上装备表明cn
包及这今后代包下的一切类中的日志都依照trace
等级进行显现!
在开发实践中,特点称号一般装备为logging.level.项目根包
,例如:
logging.level.cn.tedu.boot.demo: trace
在运用Slf4j时,经过log
调用的每种等级的办法都被重载了多次(各等级对应除了办法称号不同,重载的次数和参数列表均相同),引荐运用的办法是参数列表为(String format, Object... arguments)
的,例如:
public void trace(String format, Object... arguments);
public void debug(String format, Object... arguments);
public void info(String format, Object... arguments);
public void warn(String format, Object... arguments);
public void error(String format, Object... arguments);
以上办法中,第1个参数是行将输出的字符串的办法(模版),在此字符串中,假如需求包括某个变量值,则运用{}
表明,假如有多个变量值,均是如此,然后,再经过第2个参数(是可变参数)顺次表明各{}
对应的值,例如:
log.debug("加密前的暗码:{},加密后的暗码:{}", password, encodedPassword);
运用这种做法,能够防止多变量时频频的拼接字符串,别的,日志结构会将第1个参数进行缓存,以此进步后续每一次的履行效率。
在开发实践中,应该对程序履行要害方位增加日志的输出,一般包括:
- 每个办法的第1行有用句子,表明代码现已履行到此办法内,或此办法现已被成功调用
- 假如办法是有参数的,还应该输出参数的值
- 要害数据或中心数据在改变之前和之后
- 例如对暗码加密时,应该经过日志输出加密前和加密后的暗码
- 重要的操作履行之前
- 例如测验刺进数据之前、修正数据之前,应该经过日志输出相关值
- 程序走到某些重要的分支时
- 例如经过判别,走向抛出反常之前
其实,Slf4j日志结构仅仅日志的一种标准,并不是详细的完成(感觉上与Java中的接口有点相似),常见有详细完成了日志功用的结构有log4j、logback等,为了一致标准,所以才呈现了Slf4j,一同,因为log4j、logback等结构完成功用并不一致,所以,Slf4j提供了对干流日志结构的兼容,在Spring Boot工程中,spring-boot-starter
就现已依靠了spring-boot-starter-logging
,而在此依靠下,一般包括Slf4j、详细的日志结构、Slf4j对详细日志结构的兼容。
12. 暗码加密
【这并不是Spring Boot结构的知识点】
对暗码进行加密,能够有用的保障暗码安全,即便呈现数据库泄密,暗码安全也不会受到影响!为了完成此方针,需求在对暗码进行加密时,运用不可逆的算法进行处理!
一般,不能够运用加密算法对暗码进行加暗码处理,从严厉界说上来看,一切的加密算法都是能够逆向运算的,即一同存在加密和解密这2种操作,加密算法只能用于保证传输进程的安全,并不应该用于保证需求存储下来的暗码的安全!
哈希算法都是不可逆的,一般,用于处理暗码加密的算法中,典型的是一些音讯摘要算法,例如MD5、SHA256或以上位数的算法。
音讯摘要算法的主要特征有:
- 音讯相一同,摘要必定相同
- 某种算法,无论音讯长度多少,摘要的长度是固定的
- 音讯不一同,摘要几乎不会相同
在音讯摘要算法中,以MD5为例,其运算成果是一个128位长度的二进制数,一般会转换成十六进制数显现,所所以32位长度的十六进制数,MD5也被称之为128位算法。理论上,会存在2的128次方品种的摘要成果,且对应2的128次方种不同的音讯,假如在未超过2的128次方种音讯中,存在2个或多个不同的音讯对应了相同的摘要,则称之为:发生了磕碰。一个音讯摘要算法是否安全,取决其实践的磕碰概率,关于音讯摘要算法的破解,也是研究其磕碰概率。
存在穷举音讯和摘要的对应联系,并运用摘要在此对应联系进行查询,从而得知音讯的做法,可是,因为MD5是128位算法,全部穷举是不可能完成的,所以,只需原始暗码(音讯)满足杂乱,就不会被收录到所记录的对应联系中去!
为了进一步进步暗码的安全性,在运用音讯摘要算法进行处理时,一般还会加盐!盐值能够是恣意的字符串,用于与暗码一同作为被音讯摘要算法运算的数据即可,例如:
@Test
public void md5Test() {
String rawPassword = "123456";
String salt = "kjfcsddkjfdsajfdiusf8743urf";
String encodedPassword = DigestUtils.md5DigestAsHex(
(salt + salt + rawPassword + salt + salt).getBytes());
System.out.println("原暗码:" + rawPassword);
System.out.println("加密后的暗码:" + encodedPassword);
}
加盐的目的是使得被运算数据变得愈加杂乱,盐值自身和用法并没有明确要求!
乃至,在某些用法或算法中,还会运用随机的盐值,则能够运用完全相同的原音讯对应的摘要却不同!
引荐了解:预核算的哈希链、彩虹表、雪花算法。
为了进一步保证暗码安全,还能够运用多重加密,即反复调用音讯摘要算法。
除此以外,还能够运用安全系数更高的算法,例如SHA-256是256位算法,SHA-384是384位算法,SHA-512是512位算法。
一般的运用办法能够是:
public class PasswordEncoder {
public String encode(String rawPassword) {
// 加密进程
// 1. 运用MD5算法
// 2. 运用随机的盐值
// 3. 循环5次
// 4. 盐的处理办法为:盐 + 原暗码 + 盐 + 原暗码 + 盐
// 留意:因为运用了随机盐,盐值有必要被记录下来,本次的回来成果运用$分隔盐与密文
String salt = UUID.randomUUID().toString().replace("-", "");
String encodedPassword = rawPassword;
for (int i = 0; i < 5; i++) {
encodedPassword = DigestUtils.md5DigestAsHex(
(salt + encodedPassword + salt + encodedPassword + salt).getBytes());
}
return salt + encodedPassword;
}
public boolean matches(String rawPassword, String encodedPassword) {
String salt = encodedPassword.substring(0, 32);
String newPassword = rawPassword;
for (int i = 0; i < 5; i++) {
newPassword = DigestUtils.md5DigestAsHex(
(salt + newPassword + salt + newPassword + salt).getBytes());
}
newPassword = salt + newPassword;
return newPassword.equals(encodedPassword);
}
}
13. 控制器层开发
Spring MVC是用于处理控制器层开发的,在运用Spring Boot时,在pom.xml
中增加spring-boot-starter-web
即可整合Spring MVC结构及相关的常用依靠项(包括jackson-databind
),能够将已存在的spring-boot-starter
直接改为spring-boot-starter-web
,因为在spring-boot-starter-web
中现已包括了spring-boot-starter
。
先在项目的根包下创立controller
子包,并在此子包下创立AdminController
,此类应该增加@RestController
和@RequestMapping(value = "/admins", produces = "application/json; charset=utf-8")
注解,例如:
@RestController
@RequestMapping(values = "/admins", produces = "application/json; charset=utf-8")
public class AdminController {
}
因为现已决议了服务器端呼应时,将呼应JSON格局的字符串,为保证能够呼应JSON格局的成果,处理恳求的办法回来值应该是自界说的数据类型,则从此前学习的spring-mvc
项目中找到JsonResult
类及相关类型,复制到当时项目中来。
接下来,即可在AdminController
中增加处理“增加办理员”的恳求:
@Autowired
private IAdminService adminService;
// 留意:暂时运用@RequestMapping,不要运用@PostMapping,以便于直接在浏览器中测验
// http://localhost:8080/admins/add-new?username=root&password=1234
@RequestMapping("/add-new")
public JsonResult<Void> addNew(AdminAddNewDTO adminAddNewDTO) {
adminService.addNew(adminAddNewDTO);
return JsonResult.ok();
}
完成后,运转发动类,即可发动整个项目,在spring-boot-starter-web
中,包括了Tomcat的依靠项,在发动时,会主动将当时项目打包并布置到此Tomcat上,所以,履行发动类时,会履行此Tomcat,一同,因为是内置的Tomcat,只为当时项目服务,所以,在将项目布置到Tomcat时,默许现已将Context Path(例如spring_mvc_war_exploded)装备为空字符串,所以,在发动项目后,拜访的URL中并没有此前遇到的Context Path值。
当项目发动成功后,即可在浏览器的地址栏中输入网址进行测验拜访!
留意:假如是未增加的办理员账号,能够成功履行完毕,假如办理员账号现已存在,因为尚未处理反常,会提示500过错。
关于处理反常,应该先在State
中保证有每种反常对应的枚举值,例如本次需求弥补InsertException
对应的枚举值:
public enum State {
OK(200),
ERR_USERNAME(201),
ERR_PASSWORD(202),
ERR_INSERT(500); // 新增的枚举值
// 原有其它代码
}
然后,在cn.tedu.boot.demo.controller
下创立handler.GlobalExceptionHandler
类,用于一致处理反常,例如:
package cn.tedu.boot.demo.controller.handler;
import cn.tedu.boot.demo.ex.ServiceException;
import cn.tedu.boot.demo.ex.UsernameDuplicateException;
import cn.tedu.boot.demo.web.JsonResult;
import cn.tedu.boot.demo.web.State;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ServiceException.class)
public JsonResult<Void> handleServiceException(ServiceException e) {
if (e instanceof UsernameDuplicateException) {
return JsonResult.fail(State.ERR_USERNAME, "用户名过错!");
} else {
return JsonResult.fail(State.ERR_INSERT, "刺进数据失利!");
}
}
}
完成后,从头发动项目,当增加办理员时的用户名没有被占用时,将正常增加,当用户名现已被占用时,会依据处理反常的成果进行呼应!
因为在一致处理反常的机制下,同一种反常,无论是在哪种事务中呈现,处理反常时的描绘信息都是完全相同的,也无法精准的表达过错信息,这是不合适的!别的,依据面向方针的“分工”思想,关于过错信息(反常对应的描绘信息),应该是由Service来描绘,即“谁抛出谁描绘”,因为抛出反常的代码片段是最了解、最明确呈现反常的原因的!
为了更好的描绘反常的原因,应该在自界说的ServiceException
和这今后代类反常中增加依据父类的全部结构办法(5个),然后,在AdminServiceImpl
中,当抛出反常时,能够在反常的结构办法中增加String
类型的参数,对反常发生的原因进行描绘,例如:
@Override
public void addNew(AdminAddNewDTO adminAddNewDTO) {
// ===== 原有其它代码 =====
// 判别查询成果是否不为null
if (queryResult != null) {
// 是:表明用户名现已被占用,则抛出UsernameDuplicateException
log.error("此账号现已被占用,将抛出反常");
throw new UsernameDuplicateException("增加办理员失利,用户名(" + username + ")现已被占用!");
}
// ===== 原有其它代码 =====
// 判别以上回来的成果是否不为1,抛出InsertException反常
if (rows != 1) {
throw new InsertException("增加办理员失利,服务器忙,请稍后再次测验!");
}
}
最终,在处理反常时,能够调用反常方针的getMessage()
办法获取抛出时封装的描绘信息,例如:
@ExceptionHandler(ServiceException.class)
public JsonResult<Void> handleServiceException(ServiceException e) {
if (e instanceof UsernameDuplicateException) {
return JsonResult.fail(State.ERR_USERNAME, e.getMessage());
} else {
return JsonResult.fail(State.ERR_INSERT, e.getMessage());
}
}
完成后,再次重启项目,当用户名现已存在时,能够显现在Service中描绘的过错信息!
最终,当增加成功时,呼应的JSON数据例如:
{
"state":200,
"message":null,
"data":null
}
当用户名抵触,增加失利时,呼应的JSON数据例如:
{
"state":201,
"message":"增加办理员失利,用户名(liuguobin)现已被占用!",
"data":null
}
能够看到,无论是成功仍是失利,呼应的JSON中都包括了不用要的数据(为null
的数据),这些数据特点是没有必要呼应到客户端的,假如需求去除这些不用要的值,能够在对应的特点上运用注解进行装备,例如:
@Data
public class JsonResult<T> implements Serializable {
// 状况码,例如:200
private Integer state;
// 音讯,例如:"登录失利,用户名不存在"
@JsonInclude(JsonInclude.Include.NON_NULL)
private String message;
// 数据
@JsonInclude(JsonInclude.Include.NON_NULL)
private T data;
// ===== 原有其它代码 =====
}
则呼应的JSON中只会包括不为null
的部分。
此注解还能够增加在类上,则作用于当时类中一切的特点,例如:
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class JsonResult<T> implements Serializable {
// ===== 原有其它代码 =====
}
即便增加在类上,也只对当时类的3个特点有用,后续,当呼应某些数据时,data
特点可能是用户、产品、订单等类型,这些类型的数据中为null
的部分仍然会被呼应到客户端去,所以,还需求对这些类型也增加相同的注解装备!
以上做法相对比较繁琐,能够在application.properties
/ application.yml
中增加大局装备,则作用于当时项目中一切呼应时触及的类,例如在properties
中装备为:
spring.jackson.default-property-inclusion=non_null
在yml
中装备为:
spring:
jackson:
default-property-inclusion: non_null
留意:当你需求在yml
中增加以上装备时,前缀特点名可能现已存在,则不答应呈现重复的前缀特点名,例如以下装备便是过错的:
spring:
profiles:
active: dev
spring: # 此处就呈现了相同的前缀特点名,是过错的
jackson:
default-property-inclusion: non_null
正确的装备例如:
spring:
profiles:
active: dev
jackson:
default-property-inclusion: non_null
最终,以上装备仅仅“默许”装备,假如在某些类型中还有不同的装备需求,仍能够在类或特点上经过@JsonInclude
进行装备。
14. Validation结构
当客户端向服务器提交恳求时,假如恳求数据呈现显着的问题(例如要害数据为null
、字符串的长度不在可接受范围内、其它格局过错),应该直接呼应过错,而不是将显着过错的恳求参数传递到Service!
关于判别过错,只要触及数据库中的数据才能判别出成果的,都由Service进行判别,而根本的格局判别,都由Controller进行判别。
Validation结构是专门用于处理查看数据根本格局有用性的,最早并不是Spring系列的结构,现在,Spring Boot提供了更好的支撑,所以,一般结合在一同运用。
在Spring Boot项目中,需求增加spring-boot-starter-validation
依靠项,例如:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
在控制器中,首要,对需求查看数据格局的恳求参数增加@Valid
或@Validated
注解(这2个注解没有差异),例如:
@RequestMapping("/add-new")
public JsonResult<Void> addNew(@Validated AdminAddNewDTO adminAddNewDTO) {
adminService.addNew(adminAddNewDTO);
return JsonResult.ok();
}
真正需求查看的是AdminAddNewDTO
中各特点的值,所以,接下来需求在此类的各特点上经过注解来装备查看的规矩,例如:
@Data
public class AdminAddNewDTO implements Serializable {
@NotNull // 验证规矩为:不答应为null
private String username;
// ===== 原有其它代码 =====
}
重启项目,经过不提交用户名的URL(例如:http://localhost:8080/admins/add-new)进行拜访,在浏览器上会呈现400过错页面,而且,在IntelliJ IDEA的控制台会呈现以下正告:
2022-06-07 11:37:53.424 WARN 6404 --- [nio-8080-exec-8] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [
org.springframework.validation.BindException:
org.springframework.validation.BeanPropertyBindingResult: 1 errors<EOL>Field error in object 'adminAddNewDTO' on field 'username': rejected value [null]; codes [NotNull.adminAddNewDTO.username,NotNull.username,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [adminAddNewDTO.username,username]; arguments []; default message [username]]; default message [不能为null]]
从正告信息中能够看到,当验证失利时(不符合所运用的注解对应的规矩时),会呈现org.springframework.validation.BindException
反常,则自行处理此反常即可!
假如有多个特点需求验证,则多个特点都需求增加注解,例如:
@Data
public class AdminAddNewDTO implements Serializable {
@NotNull
private String username;
@NotNull
private String password;
// ===== 原有其它代码 =====
}
首要,在State
中增加新的枚举:
public enum State {
OK(200),
ERR_USERNAME(201),
ERR_PASSWORD(202),
ERR_BAD_REQUEST(400), // 新增
ERR_INSERT(500);
// ===== 原有其它代码 =====
}
然后,在GlobalExceptionHandler
中增加新的处理反常的办法:
@ExceptionHandler(BindException.class)
public JsonResult<Void> handleBindException(BindException e) {
return JsonResult.fail(State.ERR_BAD_REQUEST, e.getMessage());
}
完成后,再次重启项目,继续运用为null
的用户名提交恳求时,能够看到反常现已被处理,此时,呼应的JSON数据例如:
{
"state":400,
"message":"org.springframework.validation.BeanPropertyBindingResult: 2 errors\nField error in object 'adminAddNewDTO' on field 'username': rejected value [null]; codes [NotNull.adminAddNewDTO.username,NotNull.username,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [adminAddNewDTO.username,username]; arguments []; default message [username]]; default message [不能为null]\nField error in object 'adminAddNewDTO' on field 'password': rejected value [null]; codes [NotNull.adminAddNewDTO.password,NotNull.password,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [adminAddNewDTO.password,password]; arguments []; default message [password]]; default message [不能为null]"
}
关于过错提示信息,以上内容中呈现了不能为null
的字样,是默许的提示文本,能够经过@NotNull
注解的message
特点进行装备,例如:
@Data
public class AdminAddNewDTO implements Serializable {
@NotNull(message = "增加办理员失利,请提交用户名!")
private String username;
@NotNull(message = "增加办理员失利,请提交暗码!")
private String password;
// ===== 原有其它代码 =====
}
然后,在处理反常时,经过反常信息获取自界说的提示文本:
@ExceptionHandler(BindException.class)
public JsonResult<Void> handleBindException(BindException e) {
BindingResult bindingResult = e.getBindingResult();
String defaultMessage = bindingResult.getFieldError().getDefaultMessage();
return JsonResult.fail(State.ERR_BAD_REQUEST, defaultMessage);
}
再次运转,在不提交用户名和暗码的情况下,会随机的提示用户名或暗码验证失利的提示文本中的某1条。
在Validation结构中,还有其它许多注解,用于进行不同格局的验证,例如:
-
@NotEmpty
:只能增加在String
类型上,不许为空字符串,例如""
即视为空字符串 -
@NotBlank
:只能增加在String
类型上,不答应为空白,例如一般的空格可视为空白,运用TAB键输入的内容也是空白,(尽管不太可能在此处呈现)换行发生的空白区域也是空白 -
@Size
:约束巨细 -
@Min
:约束最小值 -
@Max
:约束最大值 -
@Range
:能够装备min
和max
特点,一同约束最小值和最大值 -
@Pattern
:只能增加在String
类型上,自行指定正则表达式进行验证 - 其它
以上注解,包括@NotNull
是答应叠加运用的,即答应在同一个参数特点上增加多个注解!
以上注解均能够装备message
特点,用于指定验证失利的提示文本。
一般:
- 关于有必要提交的特点,都会增加
@NotNull
- 关于数值类型的,需求考虑是否增加
@Range
(则不需求运用@Min
和@Max
) - 关于字符串类型,都增加
@Pattern
注解进行验证
15. 处理跨域问题
在运用前后端别离的开发办法下,前端项目和后端项目可能是2个完全不同的项目,而且,各自己独立开发,独立布置,在这种做法中,假如前端直接向后端发送异步恳求,默许情况下,在前端会呈现相似以下过错:
Access to XMLHttpRequest at 'http://localhost:8080/admins/add-new' from origin 'http://localhost:8081' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
以上过错信息的要害字是CORS
,一般称之为“跨域问题”。
在依据Spring MVC结构的项目中,当需求处理跨域问题时,需求一个Spring MVC的装备类(完成了WebMvcConfigurer
接口的类),并重写其间的办法,以答应指定条件的跨域拜访,例如:
package cn.tedu.boot.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
16. 关于客户端提交恳求参数的格局
一般,客户端向服务器端发送恳求时,恳求参数能够有2种办法,第1种是直接经过&
拼接各参数与值,例如:
// FormData
// username=root&password=123456&nickname=jackson&phone=13800138001&email=jackson@baidu.com&description=none
let data = 'username=' + this.ruleForm.username
+ '&password=' + this.ruleForm.password
+ '&nickname=' + this.ruleForm.nickname
+ '&phone=' + this.ruleForm.phone
+ '&email=' + this.ruleForm.email
+ '&description=' + this.ruleForm.description;
第2种办法是运用JSON语法来安排各参数与值,例如:
let data = {
'username': this.ruleForm.username, // 'root'
'password': this.ruleForm.password, // '123456'
'nickname': this.ruleForm.nickname, // 'jackson'
'phone': this.ruleForm.phone, // '13800138001'
'email': this.ruleForm.email, // 'jackson@baidu.com'
'description': this.ruleForm.description // 'none'
};
详细运用哪种做法,取决于服务器端的规划:
- 假如服务器端处理恳求的办法中,在参数前增加了
@RequestBody
,则答应运用以上第2种做法(JSON数据)提交恳求参数,不答应运用以上第1种做法(运用&
拼接) - 假如没有运用
@RequestBody
,则只能运用以上第1种做法
17. 处理登录
17.1. 开发流程
正常的项目开发流程大致是:
- 先整理出当时项目触及的数据的类型
- 例如:电商类包括用户、产品、购物车、订单等
- 再列举各种数据类型触及的数据操作
- 例如:用户类型触及注册、登录等
- 再选择相对简略的数据类型先处理
- 简略的易于完成,且能够堆集经历
- 在各数据类型触及的数据操作中,大致遵循增、查、删、改的开发顺序
- 只要先增,还可能查、删、改
- 只要查了今后,才能明确有哪些数据,才便于完成删、改
- 删和改相比,删一般愈加简略,所以先开发删,再开发改
- 在开发详细的数据操作时,应该大致遵循耐久层 >> 事务逻辑层 >> 控制器层 >> 前端页面的开发顺序
17.2. 办理员登录-耐久层
17.2.1. 创立或装备
假如是整个项目第1次开发耐久层,在Spring Boot项目中,需求装备:
- 运用
@MapperScan
装备接口地点的根包 - 在装备文件中经过
mybatis.mapper-locations
装备XML文件的方位
假如第1次处理某品种型数据的耐久层拜访,需求:
- 创立接口
- 创立XML文件
本次需求开发的“办理员登录”并不需求再做以上操作
17.2.2. 规划需求履行的SQL句子
需求履行的SQL句子大致是:
select * from ams_admin where username=?
因为在ams_admin
表中有很多字段,一同,不答应运用星号表明字段列表,则以上SQL句子应该细化为:
select id, username, password, nickname, avatar, is_enable from ams_admin where username=?
提示:理论上,还应该查出login_count
,当登录成功后,还应该更新login_count
、gmt_last_login
等数据,此次暂不考虑。
17.2.3. 在接口中增加笼统办法(含创立必要的VO类)
提示:一切的查询成果,都应该运用VO类,而不要运用实体类,依据阿里的开发标准,每张数据表中都应该有id
、gmt_create
、gmt_modified
这3个字段,而gmt_create
、gmt_modified
这2个字段都是用于特殊情况下排查问题的,一般情况下均不会运用,所以,假如运用实体类,必定存在剩余的特点,一同,因为不运用星号作为字段列表,则一般也不会查询这2个字段的值,会导致实体类方针中永久至少存在2个特点为null
。
依据以上提示,以前现已写好的getByUsername()
是不标准的,应该调整已存在此办法,本次并不需求增加新的笼统办法。
则先创立cn.tedu.boot.demo.pojo.vo.AdminSimpleVO
类,增加此次查询时需求的特点:
package cn.tedu.boot.demo.pojo.vo;
@Data
public class AdminSimpleVO implements Serializable {
private Long id;
private String username;
private String password;
private String nickname;
private String avatar;
private Integer isEnable;
}
然后,在AdminMapper
接口文件中,将原有的Admin getByUsername(String username);
改为:
AdminSimpleVO getByUsername(String username);
留意:一旦修正了原有代码,则调用了原办法的代码都会呈现过错,包括:
- 测验
- 事务逻辑层的完成类
应该及时修正过错的代码,可是,因为此时还未完成SQL装备,所以,相关代码暂时并不能运转。
17.2.4. 在XML中装备SQL
在AdminMapper.xml
中,需求调整:
- 删去
<sql>
中不用查询的字段,留意:此处的字段列表最终不要有剩余的逗号 - 修正
<resultMap>
节点的type
特点值 - 在
<resultMap>
节点下,删去不用要的装备
<select id="getByUsername" resultMap="BaseResultMap">
select
<include refid="BaseQueryFields" />
from
ams_admin
where
username=#{username}
</select>
<sql id="BaseQueryFields">
<if test="true">
id,
username,
password,
nickname,
avatar,
is_enable
</if>
</sql>
<resultMap id="BaseResultMap" type="cn.tedu.boot.demo.pojo.vo.AdminSimpleVO">
<id column="id" property="id" />
<result column="username" property="username" />
<result column="password" property="password" />
<result column="nickname" property="nickname" />
<result column="avatar" property="avatar" />
<result column="is_enable" property="isEnable" />
</resultMap>
17.2.5. 编写并履行测验
此次并不需求编写新的测验,运用原有的测验即可!
留意:因为本次是修正了原“增加办理员”就现已运用的功用,应该查看原功用是否能够正常运转。
17.3. 办理员登录-事务逻辑层
17.3.1. 创立
假如第1次处理某品种型数据的事务逻辑层拜访,需求:
- 创立接口
- 创立类,完成接口,并在类上增加
@Service
注解
本次需求开发的“办理员登录”并不需求再做以上操作
17.3.2. 在接口中增加笼统办法(含创立必要的DTO类)
在规划笼统办法时,假如参数的数量超过1个,且多个参数具有相关性(是否都是客户端提交的,或是否都是控制器传递过来的等),就应该封装!
在处理登录时,需求客户端提交用户名和暗码,则能够将用户名、暗码封装起来:
package cn.tedu.boot.demo.pojo.dto;
@Data
public class AdminLoginDTO implements Serializable {
private String username;
private String password;
}
在IAdminService
中增加笼统办法:
AdminSimpleVO login(AdminLoginDTO adminLoginDTO);
17.3.3. 在完成类中规划(打草稿)事务流程与事务逻辑(含创立必要的反常类)
此次事务履行进程中,可能会呈现:
- 用户名不存在,导致无法登录
- 用户状况为【禁用】,导致无法登录
- 暗码过错,导致无法登录
关于用户名不存在的问题,能够自行创立新的反常类,例如,在cn.tedu.boot.demo.ex
包下创立UserNotFoundException
类表明用户数据不存在的反常,继承自ServiceException
,且增加5款依据父类的结构办法:
package cn.tedu.boot.demo.ex;
public class UserNotFoundException extends ServiceException {
// 主动生成5个结构办法
}
再创立UserStateException
表明用户状况反常:
package cn.tedu.boot.demo.ex;
public class UserStateException extends ServiceException {
// 主动生成5个结构办法
}
再创立PasswordNotMatchException
表明暗码过错反常:
package cn.tedu.boot.demo.ex;
public class PasswordNotMatchException extends ServiceException {
// 主动生成5个结构办法
}
登录进程大致是:
public AdminSimpleVO login(AdminLoginDTO adminLoginDTO) {
// 经过参数得到测验登录的用户名
// 调用adminMapper.getByUsername()办法查询
// 判别查询成果是否为null
// 是:表明用户名不存在,则抛出UserNotFoundException反常
// 【假如程序能够履行到此步,则能够确定未抛出反常,即查询成果不为null】
// 【以下可视为:存在与用户名匹配的办理员数据】
// 判别查询成果中的isEnable特点值是否不为1
// 是:表明此用户状况是【禁用】的,则抛出UserStateException反常
// 【假如程序能够履行到此步,表明此用户状况是【启用】的】
// 从参数中取出此次登录时客户端提交的暗码
// 调用PasswordEncoder方针的matches()办法,对客户端提交的暗码和查询成果中的暗码进行验证
// 判别以上验证成果
// true:暗码正确,视为登录成功
// -- 将查询成果中的password、isEnable设置为null,防止呼应到客户端
// -- 回来查询成果
// false:暗码过错,视为登录失利,则抛出PasswordNotMatchException反常
}
17.3.4. 在完成类中完成事务
在AdminServiceImpl
中重写接口中新增的笼统办法:
@Override
public AdminSimpleVO login(AdminLoginDTO adminLoginDTO) {
// 日志
log.debug("行将处理办理员登录的事务,测验登录的办理员信息:{}", adminLoginDTO);
// 经过参数得到测验登录的用户名
String username = adminLoginDTO.getUsername();
// 调用adminMapper.getByUsername()办法查询
AdminSimpleVO queryResult = adminMapper.getByUsername(username);
// 判别查询成果是否为null
if (queryResult == null) {
// 是:表明用户名不存在,则抛出UserNotFoundException反常
log.warn("登录失利,用户名不存在!");
throw new UserNotFoundException("登录失利,用户名不存在!");
}
// 【假如程序能够履行到此步,则能够确定未抛出反常,即查询成果不为null】
// 【以下可视为:存在与用户名匹配的办理员数据】
// 判别查询成果中的isEnable特点值是否不为1
if (queryResult.getIsEnable() != 1) {
// 是:表明此用户状况是【禁用】的,则抛出UserStateException反常
log.warn("登录失利,此账号现已被禁用!");
throw new UserNotFoundException("登录失利,此账号现已被禁用!");
}
// 【假如程序能够履行到此步,表明此用户状况是【启用】的】
// 从参数中取出此次登录时客户端提交的暗码
String rawPassword = adminLoginDTO.getPassword();
// 调用PasswordEncoder方针的matches()办法,对客户端提交的暗码和查询成果中的暗码进行验证
boolean matchResult = passwordEncoder.matches(rawPassword, queryResult.getPassword());
// 判别以上验证成果
if (!matchResult) {
// false:暗码过错,视为登录失利,则抛出PasswordNotMatchException反常
log.warn("登录失利,暗码过错!");
throw new PasswordNotMatchException("登录失利,暗码过错!");
}
// 暗码正确,视为登录成功
// 将查询成果中的password、isEnable设置为null,防止呼应到客户端
queryResult.setPassword(null);
queryResult.setIsEnable(null);
// 回来查询成果
log.debug("登录成功,行将回来:{}", queryResult);
return queryResult;
}
17.3.5. 编写并履行测验
在AdminServiceTests
中增加测验:
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
@Test
public void testLoginSuccessfully() {
// 测验数据
String username = "admin001";
String password = "123456";
AdminLoginDTO adminLoginDTO = new AdminLoginDTO();
adminLoginDTO.setUsername(username);
adminLoginDTO.setPassword(password);
// 断语不会抛出反常
assertDoesNotThrow(() -> {
// 履行测验
AdminSimpleVO adminSimpleVO = service.login(adminLoginDTO);
log.debug("登录成功:{}", adminSimpleVO);
// 断语测验成果
assertEquals(1L, adminSimpleVO.getId());
assertNull(adminSimpleVO.getPassword());
assertNull(adminSimpleVO.getIsEnable());
});
}
@Sql({"classpath:truncate.sql"})
@Test
public void testLoginFailBecauseUserNotFound() {
// 测验数据
String username = "admin001";
String password = "123456";
AdminLoginDTO adminLoginDTO = new AdminLoginDTO();
adminLoginDTO.setUsername(username);
adminLoginDTO.setPassword(password);
// 断语会抛出UserNotFoundException
assertThrows(UserNotFoundException.class, () -> {
// 履行测验
service.login(adminLoginDTO);
});
}
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
@Test
public void testLoginFailBecauseUserDisabled() {
// 测验数据
String username = "admin005"; // 经过SQL脚本刺进的此数据,is_enable为0
String password = "123456";
AdminLoginDTO adminLoginDTO = new AdminLoginDTO();
adminLoginDTO.setUsername(username);
adminLoginDTO.setPassword(password);
// 断语会抛出UserStateException
assertThrows(UserStateException.class, () -> {
// 履行测验
service.login(adminLoginDTO);
});
}
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
@Test
public void testLoginFailBecausePasswordNotMatch() {
// 测验数据
String username = "admin001";
String password = "000000000000000000";
AdminLoginDTO adminLoginDTO = new AdminLoginDTO();
adminLoginDTO.setUsername(username);
adminLoginDTO.setPassword(password);
// 断语会抛出PasswordNotMatchException
assertThrows(PasswordNotMatchException.class, () -> {
// 履行测验
service.login(adminLoginDTO);
});
}
17.4. 办理员登录-控制器层
17.4.1. 创立
假如是整个项目第1次开发控制器层,需求:
- 创立一致处理反常的类
- 增加
@RestControllerAdvice
- 增加
- 创立一致的呼应成果类型及相关类型
- 例如:
JsonResult
及State
- 例如:
假如第1次处理某品种型数据的控制器层拜访,需求:
- 创立控制器类
- 增加
@RestController
- 增加
@RequestMapping
- 增加
本次需求开发的“办理员登录”并不需求再做以上操作
17.4.2. 增加处理恳求的办法,验证恳求参数的根本有用性
在AdminLoginDTO
的各特点上增加验证根本有用性的注解,例如:
package cn.tedu.boot.demo.pojo.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
@Data
public class AdminLoginDTO implements Serializable {
@NotNull(message = "登录失利,请提交用户名!") // 新增
private String username;
@NotNull(message = "登录失利,请提交暗码!") // 新增
private String password;
}
在AdminController
中增加处理恳求的办法:
@RequestMapping("/login") // 暂时运用@RequestMapping,后续改成@PostMapping
public JsonResult<AdminSimpleVO> login(@Validated AdminLoginDTO adminLoginDTO) {
AdminSimpleVO adminSimpleVO = adminService.login(adminLoginDTO);
return JsonResult.ok(adminSimpleVO);
}
17.4.3. 处理反常(按需)
先在State
中增加新创立的反常对应枚举:
public enum State {
OK(200),
ERR_USERNAME(201),
ERR_PASSWORD(202),
ERR_STATE(203), // 新增
ERR_BAD_REQUEST(400),
ERR_INSERT(500);
// ===== 原有其它代码 =====
}
在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());
}
}
17.4.4. 测验
发动项目,暂时经过 http://localhost:8080/admins/login?username=admin001&password=123456 相似的URL测验拜访。留意:在测验拜访之前,有必要保证数据表中的数据状况是符合预期的。
17.5. 办理员登录-前端页面
18. 控制器层的测验
关于控制器层,也能够写测验办法进行测验,在Spring Boot项目中,能够运用MockMvc
进行模仿测验,例如:
package cn.tedu.boot.demo.controller;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
@SpringBootTest
@AutoConfigureMockMvc // 主动装备MockMvc
public class AdminControllerTests {
@Autowired
MockMvc mockMvc; // Mock:模仿
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
@Test
public void testLoginSuccessfully() throws Exception {
// 预备测验数据,不需求封装
String username = "admin001";
String password = "123456";
// 恳求途径,不需求写协议、服务器主机和端口号
String url = "/admins/login";
// 履行测验
// 以下代码相对比较固定
mockMvc.perform( // 履行发出恳求
MockMvcRequestBuilders.post(url) // 依据恳求办法决议调用的办法
.contentType(MediaType.APPLICATION_FORM_URLENCODED) // 恳求数据的文档类型,例如:application/json; charset=utf-8
.param("username", username) // 恳求参数,有多个时,多次调用param()办法
.param("password", password)
.accept(MediaType.APPLICATION_JSON)) // 接纳的呼应成果的文档类型,留意:perform()办法到此完毕
.andExpect( // 预判成果,相似断语
MockMvcResultMatchers
.jsonPath("state") // 预判呼应的JSON成果中将有名为state的特点
.value(200)) // 预判呼应的JSON成果中名为state的特点的值,留意:andExpect()办法到此完毕
.andDo( // 需求履行某使命
MockMvcResultHandlers.print()); // 打印日志
}
}
履行以上测验时,并不需求发动当时项目即可测验。
在履行以上测验时,呼应的JSON中假如包括中文,可能会呈现乱码,需求在装备文件(application.properties
或application.yml
这类文件)中增加装备。
在.properties
文件中:
server.servlet.encoding.force=true
server.servlet.encoding.charset=utf-8
在.yml
文件中:
server:
servlet:
encoding:
force: true
charset: utf-8