一、SpringBoot关于静态资源的处理
1.1 介绍
springboot启动时会根据配置文件自动配置相应场景的组件xxxAutoConfiguration,web项目启动时会初始化WebMvcAutoConfiguration组件处理请求相关的操作.
其中有默认处理静态资源的方法:

注:this.resourceProperties.getStaticLocations()获取初始化指定放在静态资源的默认位置
1.2 静态资源存放的位置
由于我们在springboot项目中加入了spring-boot-starter-web,也就是web场景启动器;springboot项目启动后,会自动加载web场景启动器,web场景启动器会进行自动化初始配置,设置静态资源的默认存放位置为:
{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"}
从该方法中可以发现:
1、默认情况下:
(1)匹配/webjars/** 的请求,都去 classpath:/META-INF/resources/webjars/ 找资源;
(2)匹配 "/**" 的请求,都去(静态资源的文件夹)找映射,静态资源文件夹路径如下:
"classpath:/META‐INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"
"/":当前项目的根路径
2、自定义配置静态资源路径:
spring.web.resources.static-locations=自定义路径
(1)设置自定义静态资源路径,默认静态资源位置不生效。
(2)自定义:此时寻找静态资源就会从/pages 去寻找
spring.web.resources.static-locations=classpath:/pages/
注意:自定义静态资源路径后,默认静态资源路径失效!
1.3 案例实现

访问测试:

二、SpringBoot的自动配置
Springboot使用注解对一些常规的配置项做默认配置,减少或不使用xml配置,让你的项目快速运行起来;
Springboot还为大量的开发常用框架封装了starter,如今引入框架只要引入一个starter,你就可以使用这个框架,只需少量的配置甚至是不需要任何配置,这些自动配置的东西都有它自己的默认值,以端口号为例:springboot设置tomcat默认端口号 server.port的默认值为8080。文件上传默认最大值为1M(spring.servlet.multipart.max-file-size=1MB)。访问静态资源的默认位置等等,都是springboot自动配置的东西;
以前,比如spring springMvc整合的时候,需要指定很多包扫描之类的一大堆东西,springboot给我们自动配置了默认的注解扫描规则, 只要是主程序所在的包,或者下面的子包,里面的所有组件都能扫描的到。
2.2 springboot的自动配置加载流程
自动装配流程可以参考: https://developer.aliyun.com/article/1681308
2.2.1 配置加载流程

我们看到,Day02Springboot01Application作为入口类,入口类中有一个main方法,这个方法其实就是一个标准的Java应用的入口方法,一般在main方法中使用SpringApplication.run()来启动整个应用。值得注意的是,这个入口类要使用@SpringBootApplication注解声明,它是SpringBoot的核心注解。

1. @SpringBootConfiguration:标记当前类为配置类
根据其源码可以知道,@SpringBootConfiguration注解包含@Configuration,所以其拥有@Configuration注解相似的功能,而@Configuration注解又包含@Companent注解,所以配置类也存在于IOC容器中。
2. @ComponentScan(excludeFilters = {@Filter(type = FilterType.CUSTOM,classes = {TypeExcludeFilter.class}):是注解扫描器,扫描程序入口所在包及下面所有子包里面的所有组件扫描到Spring容器;
3. 这个注解里面,最主要的就是@EnableAutoConfiguration,作用开启自动配置, 它会根据类路径下的jar包、bean定义和各种属性来自动配置和注册bean。
根据其源码得出其主要由@AutoConfigurationPackages注解和@Import注解组成;
点进去@EnableAutoConfiguration的源码, 它通过使用
@Import(AutoConfigurationImportSelector.class)来导入配置,这个选择器负责从META-INF/spring.factories文件中读取自动配置的类名,并根据条件(如类路径上是否有某个库)来决定哪些自动配置类应该被加载。

/*
// 第一步:
可以看到,在@EnableAutoConfiguration注解内使用到了@import注解来完成导入配置的功能,
我们可以查看这个类selectImports()方法的内容,他返回了一个 autoConfigurationEntry ,
来自 this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
这个方法。我们继续跟踪;
*/
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
//获取自动配置的内容
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
/*
// 第二步:
这个方法中有一个值 : List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
叫做获取候选的配置 , 我们点击去继续跟踪;
*/
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
// 获取配置类信息
List<String> configurations =
this.getCandidateConfigurations(annotationMetadata, attributes);
// 根据情况,自动配置需要的配置类和不需要的配置了
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
// 返回最终需要的配置
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
/*
// 第三步:
这里面有一个 SpringFactoriesLoader.loadFactoryNames() ,
我们继续进去看 , 它又调用了 loadSpringFactories 方法;继续跟踪。
发现它去获得了一个资源文件:"META-INF/spring.factories"
*/
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
// 主要就是为了获取META-INF/spring.factories 文件下EnableAutoConfiguration对应的value值,并返回该值
List<String> configurations =
SpringFactoriesLoader.loadFactoryNames(
this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories.
If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
//第四步
/*
我们继续进去看 , 它又调用了 loadSpringFactories 方法;继续跟踪。
发现它去获得了一个资源文件:"META-INF/spring.factories"
*/
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
//获取要自动注入的组件信息
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
//第五步
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
HashMap result = new HashMap();
try {
//它将读取到的资源封装在url中,然后遍历url , 将这些url文件封装在Properties文件中;最后返回封装好的结果;获取spring.factories文件中要自动注入的组件信息
Enumeration urls = classLoader.getResources("META-INF/spring.factories");
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().
collect(Collectors.collectingAndThen(Collectors.toList(),
Collections::unmodifiableList));
});
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
throw new IllegalArgumentException
("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}
说明了这个逻辑就是 从properties中获取到EnableAutoConfiguration.class类(类名)对应的值,然后把他们添加在容器中。
总结一句话就是:将类路径下 META-INF/spring.factories 里面配置的所有EnableAutoConfiguration的值加入到了容器中;我们从源码中拿过来。
下面是2.6.13.RELEASE实现源码:
总结:在@SpringBootApplication中有一个注解@EnableAutoConfiguration,这个注解也是一个派生注解,其中的关键功能由@Import提供,其导入的AutoConfigurationImportSelector的selectImports()方法通过SpringFactoriesLoader.loadFactoryNames()扫描所有具有META-INF/spring.factories的jar包。spring-boot-autoconfigure-x.x.x.x.jar里就有一个这样的spring.factories文件。
这个spring.factories文件也是一组一组的key=value的形式,其中一个key是EnableAutoConfiguration类的全类名,而它的value是一个xxxxAutoConfiguration(启动器)的类名的列表;
在SpringApplication.run(...)的内部就会执行selectImports()方法,找到所有JavaConfig自动配置类的全限定名对应的class,然后将所有自动配置类加载到Spring容器中。
2.2.2 配置生效
springboot所有自动配置都是在启动的时候扫描并加载:spring.factories所有的自动配置类都在这里面,但是不一定生效,要判断条件是否成立(@ConditionalOnXXX注解判断条件),只要导入了对应的start,就有对应的启动器了,有了启动器,我们自动装配就会生效,然后就配置成功。
例如我们要让ServletWebServerFactoryAutoConfiguration自动配置生效,当前项目中必须加入web启动器:

每一个XxxxAutoConfiguration自动配置类都是在某些条件之下才会生效的,这些条件的限制在Spring Boot中以注解的形式体现,常见的条件注解有如下几项:

以ServletWebServerFactoryAutoConfiguration配置类为例,解释一下全局配置文件中的属性如何生效,比如:server.port=8081,是如何生效的(当然不配置也会有默认值,这个默认值来自于org.apache.catalina.startup.Tomcat)

在ServletWebServerFactoryAutoConfiguration类上,有一个@EnableConfigurationProperties注解:开启配置属性,而它后面的参数是一个ServerProperties类,这就是习惯优于配置的最终落地点。

在这个类上,我们看到了一个非常熟悉的注解:@ConfigurationProperties,它的作用就是从配置文件中绑定属性到对应的bean上,而@EnableConfigurationProperties负责导入这个已经绑定了属性的bean到spring容器中(见上面源码)。
至此,我们大致可以了解。在全局配置的属性如:server.port等,通过@ConfigurationProperties注解,绑定到对应的XxxxProperties配置实体类上封装为一个bean,然后再通过@EnableConfigurationProperties注解导入到Spring容器中。
2.2.3 获取springboot自动配置到ioc容器中的实例

三、Lombok插件
3.1 Lombok插件简介
Lombok是一个插件,用途是使用注解给你类里面的字段,自动的加上属性,构造器,ToString方法,Equals方法等等,比较方便的一点是,你在更改字段的时候,Lombok会立即发生改变以保持和你代码的一致性。
3.2 常用的 lombok 注解介绍
1. @Getter 加在类上,可以自动生成参数的getter方法
2. @Setter 加在类上,可以自动生成参数的setter方法
3. @ToString 加在类上,调用toString()方法,可以输出实体类中所有属性的值
4. @RequiredArgsConstructor会生成一个包含常量,和标识了NotNull的变量的构造方法。生成的构造方法是私有的private。这个我用的很少
5. @EqualsAndHashCode
它会生成equals和hashCode方法
默认使用非静态的属性
可以通过exclude参数排除不需要生成的属性
可以通过of参数来指定需要生成的属性
它默认不调用父类的方法,只使用本类定义的属性进行操作,可以使用callSuper=true来解决,会在@Data中进行讲解。
@Data这个是非常常用的注解,这个注解其实是五个注解的合体:整合了Getter、Setter、ToString、EqualsAndHashCode、RequiredArgsConstructor注解。
@NoArgsConstructor生成一个无参数的构造方法。
@AllArgsConstructor生成一个包含所有变量的构造方法。
3.3 IDEA安装Lombok插件
首先我们需要安装IntelliJ IDEA中的Lombok插件,打开IntelliJ IDEA后点击菜单栏中的File-->Settings,或者使用快捷键Ctrl+Alt+S进入到设置页面

我们点击设置中的Plugins进行插件的安装,然后在搜索页面输入Lombok变可以查询到下方的Lombok,鼠标点击Lombok可在右侧看到Install按钮,点击该按钮便可安装。

安装完成之后重启idea即可。
3.4 Lombok插件的使用
3.4.1 使用Lombok需要引入Lombok的依赖

四、SpringBoot整合Junit
4.1 Junit5 介绍
Spring Boot 2.2.0 版本开始引入 Junit 5 作为单元测试默认库
作为最新版本的Junit框架,Junit5与之前版本的Junit框架有很大的不同。由三个不同子项目的几个不同模块组成。
Junit 5 = Junit Platform + Junit Jupiter + Junit Vintage
Junit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。
Junit Jupiter: Junit Jupiter提供了JUnit5的新的编程模型,是Junit5新特性的核心。内部 包含了一个测试引擎,用于在Junit Platform上运行。
Junit Vintage: 由于Junit已经发展多年,为了照顾老的项目,Junit Vintage提供了兼容Junit4.x,Junit3.x的测试引擎。
4.2 SpringBoot整合Junit
第一步: 构建工程添加依赖

第二步: 创建User类进行测试

application.yml

第三步: 创建测试类

第四步: 测试类上添加注解
@SpringBootTest
第五步: 测试类注入测试对象
加入@SpringBootTest注解后台,表名当前类是一个测试类,可以自动装配容器中的对象:

五、SpringBoot整合MyBatis
5.1 名词学习
在前后端分离的开发架构中,VO、BO、DTO 是用于数据传递和封装的重要对象,它们各自有明确的应用场景和职责边界,合理使用可以提高代码的可读性、可维护性和安全性。
以下是三者的详细区别:
5.1.1 VO(View Object,视图对象)
- 定义:用于封装前端页面(视图层)需要展示的数据,是后端返回给前端的最终数据格式。
- 核心作用:
- 按需暴露数据,避免将数据库字段或内部业务数据直接返回给前端(如隐藏敏感字段:密码、手机号等)。
- 对数据进行格式化或组合(如将日期转为字符串、拼接姓名和昵称等),方便前端直接使用。
- 使用场景:
- 控制器(Controller)接口返回给前端的数据对象。
- 前端表单提交时,若仅需部分字段,也可使用 VO 接收(但更推荐用 DTO 接收提交数据)。
- 示例:
用户详情页面需要展示 “用户名、昵称、注册时间(格式化)”,则 VO 可能包含:public class UserVO { private Long id; // 用户ID private String username; // 用户名 private String nickname; // 昵称 private String registerTime; // 注册时间(格式化后,如"2023-10-01 12:00:00") }
5.1.2 DTO(Data Transfer Object,数据传输对象)
- 定义:用于前后端之间或服务之间的数据传输,封装接口调用时的请求参数或响应数据(但响应给前端的最终数据通常是 VO,DTO 更多用于请求参数)。
- 核心作用:
- 规范接口的输入输出格式,明确接口需要接收或返回的数据字段。
- 减少接口调用的次数(如合并多个参数为一个 DTO,避免多次请求)。
- 使用场景:
- 前端提交表单数据(如新增 / 修改用户时,前端传递的参数封装为 DTO)。
- 服务间调用时传递的数据(如微服务架构中,A 服务调用 B 服务时传递的参数)。
- 示例:
新增用户时,前端需要传递 “用户名、密码、昵称”,则 DTO 可能包含:public class UserDTO { private String username; // 用户名(必填) private String password; // 密码(必填,后端会加密处理) private String nickname; // 昵称(可选) }
5.1.3 BO(Business Object,业务对象)
- 定义:用于封装业务逻辑处理过程中的数据,是服务层(Service)内部使用的对象,承载业务计算、规则验证等逻辑。
- 核心作用:
- 封装业务数据和业务行为,使服务层专注于业务逻辑处理。
- 可以组合多个数据源的数据(如合并用户表和订单表的数据进行业务计算)。
- 使用场景:
- 服务层(Service)内部处理业务时使用(如从数据库查询到 PO 后,转换为 BO 进行业务处理)。
- 业务逻辑的载体(如包含计算用户积分、验证用户权限等方法)。
- 示例:
处理用户登录业务时,BO 可能包含用户的原始数据和业务处理结果:public class UserBO { private Long id; private String username; private String password; // 数据库中的加密密码 private Integer status; // 账号状态(1-正常,0-禁用) // 业务方法:验证密码是否匹配 public boolean checkPassword(String inputPassword) { // 实际逻辑:加密输入密码后与数据库密码对比 return encrypt(inputPassword).equals(this.password); } }
三者的核心区别与联系:
| 维度 | VO(视图对象) | DTO(数据传输对象) | BO(业务对象) |
| 使用范围 | 前端视图层(后端返回给前端) | 前后端 / 服务间数据传输 | 后端服务层内部业务处理 |
| 核心职责 | 展示数据(按需、格式化) | 规范传输数据格式 | 承载业务逻辑和数据处理 |
| 数据来源 | 通常由 BO 或 DTO 转换而来 | 前端输入或服务间传递的数据 | 通常由数据库实体(PO)转换而来 |
| 是否含逻辑 | 无业务逻辑,仅数据封装 | 无业务逻辑,仅数据封装 | 包含业务逻辑方法 |
数据流转流程(以 “新增用户” 为例)
- 前端提交表单数据 → 后端控制器用UserDTO接收参数。
- 控制器调用服务层,将UserDTO通过
BeanUtils转换为UserBO。 - 服务层对UserBO进行业务处理(如密码加密、参数校验),再转换为数据库实体(PO)存入数据库。
- 服务层返回处理结果(如新增的用户 ID),控制器将结果封装为 UserVO(如包含用户 ID、用户名、注册时间)返回给前端。
通过明确区分 VO、DTO、BO 的职责,可以使代码分层更清晰,降低各层之间的耦合度,尤其在复杂业务系统中作用显著。

5.2 整合MyBatis
本案例实现一个简化的用户管理系统,采用前后端分离架构,重点展示各层对象 (VO、DTO、BO) 的规范使用及配置的集中管理。
- 后端技术栈:Spring Boot 3.5.13 + MyBatis + Druid 连接池 + 分页插件
- 前端技术栈:Vue 3 (组合式 API) + ElementPlus + Axios + Vue Router
效果图:
查询所有并分页包含条件查询:

新增用户:

修改用户:

删除用户:

5.2.1 数据库设计

5.2.2 后端实现
5.2.2.1 项目结构

查询用户信息分析:

新增用户信息分析:

编辑信息:
分两步来实现:
1. 回显用户信息

2. 修改用户信息

5.2.2.2 Maven 配置 (pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.13</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.sy</groupId>
<artifactId>day02_springboot_mybatis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- 版本配置 -->
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mybatis.version>3.5.15</mybatis.version>
<mybatis-spring.version>3.0.3</mybatis-spring.version>
<pagehelper.version>1.4.7</pagehelper.version>
<druid.version>1.2.20</druid.version>
<spring-boot.version>3.5.4</spring-boot.version>
<bcrypt.version>0.4</bcrypt.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Spring Boot 事务支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- MyBatis 核心包 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!-- MyBatis 与 Spring 整合 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>${mybatis-spring.version}</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Druid 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- MyBatis 分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper.version}</version>
</dependency>
<!-- Lombok 简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Validation 参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
<!--密码加密-->
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>${bcrypt.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Spring Boot 打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
5.2.2.3 配置文件 (application.yml)
# 服务器配置
server:
port: 8080
# Spring 配置
spring:
# 数据源配置
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sy_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
# Druid 连接池配置
druid:
initial-size: 5 # 初始化连接数
min-idle: 5 # 最小空闲连接数
max-active: 20 # 最大活跃连接数
max-wait: 60000 # 获取连接时的最大等待时间(毫秒)
time-between-eviction-runs-millis: 60000 # 检测空闲连接的间隔时间
min-evictable-idle-time-millis: 300000 # 连接最小生存时间
# 事务配置
transaction:
default-timeout: 60 # 事务默认超时时间(秒)
rollback-on-commit-failure: true # 提交失败时回滚
# MyBatis 配置
mybatis:
type-aliases-package: com.sy.pojo # 实体类别名扫描包
mapper-locations: classpath*:mapper/*.xml # mapper.xml 路径
configuration:
map-underscore-to-camel-case: true # 驼峰命名转换
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 日志实现
default-statement-timeout: 30 # 超时时间
# 分页插件配置
pagehelper:
helper-dialect: mysql # 数据库方言
reasonable: true # 合理化分页
support-methods-arguments: true # 支持方法参数分页
params: count=countSql # 计数参数
# 日志配置
logging:
level:
com.sy.mapper: debug # 打印 mapper 日志
org.springframework.jdbc.datasource.DataSourceTransactionManager: debug # 打印事务日志
5.2.2.4 启动类

5.2.2.5 配置类
跨域配置
package com.sy.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* 跨域配置,解决前后端分离架构中的跨域问题
*/
@Configuration
public class WebConfig {
@Bean
public CorsFilter corsFilter() {
// 1.创建CORS配置对象
CorsConfiguration config = new CorsConfiguration();
// 允许所有源访问
config.addAllowedOriginPattern("*");
// 允许所有请求头
config.addAllowedHeader("*");
// 允许所有请求方法
config.addAllowedMethod("*");
// 允许携带cookie
config.setAllowCredentials(true);
// 预检请求的有效期,单位秒
config.setMaxAge(3600L);
// 2.添加映射路径
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
// 3.返回新的CorsFilter
return new CorsFilter(source);
}
}
5.2.2.6 工具类
响应状态码
package com.sy.utils;
/**
* 响应状态码枚举
*/
public enum ResultCode {
SUCCESS(200, "成功"),
ERROR(500, "失败"),
PARAM_ERROR(400, "参数错误"),
NOT_FOUND(404, "资源不存在");
private final int code;
private final String msg;
ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
统一响应结果

密码加密
package com.sy.utils;
import org.mindrot.jbcrypt.BCrypt;
/**
* 密码加密工具类
*/
public class PasswordUtils {
//1.对密码进行加密处理
public static String encryptPassword(String plainPassword){
//判断密码是否为空
if (plainPassword == null || plainPassword == "" || plainPassword.isEmpty()){
throw new IllegalCallerException("密码不能为空");
}
/**
* plainPassword:明文密码
* BCrypt.gensalt():生成盐(固定的字符串) 12:迭代次数(加载因子)
* BCrypt加密方式: 不可逆 加密的次数范围4-31(值越大, 安全性越高, 加密越慢 耗费内存和性能) 取值为:21
* 如何加密的: 明文密码 + 盐(固定字符串) + 加载因子(加密的次数)
*/
try {
return BCrypt.hashpw(plainPassword, BCrypt.gensalt(12));
} catch (Exception e) {
throw new IllegalCallerException("密码加载失败");
}
}
/**
* 2.校验密码
* @param plainPassword 明文密码
* @param encryptedPassword 加密后的密码
* @return 验证结果 true:表示匹配 ,false:表示不匹配
*/
public static boolean checkPassword(String plainPassword,String encryptedPassword){
//判断密码是否为空
if (
plainPassword == null || plainPassword == "" || plainPassword.isEmpty()
|| encryptedPassword == null || encryptedPassword == "" || encryptedPassword.isEmpty()
){
return false;
}
try {
//验证密码
return BCrypt.checkpw(plainPassword,encryptedPassword);
} catch (Exception e) {
return false;
}
}
}
分页结果 DTO

5.2.2.7 实体类 (pojo)
package com.sy.pojo;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户实体类,与数据库表结构一一对应
*/
@Data
public class User {
/**
* 用户ID
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 真实姓名
*/
private String realName;
/**
* 手机号
*/
private String phone;
/**
* 状态:0-禁用,1-正常
*/
private Integer status;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
5.2.2.8 业务对象 (BO)
package com.sy.bo;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户业务对象,用于服务层内部业务处理
* 包含业务处理所需的字段和方法
*/
@Data
public class UserBO {
/**
* 用户ID
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 真实姓名
*/
private String realName;
/**
* 手机号
*/
private String phone;
/**
* 状态:0-禁用,1-正常
*/
private Integer status;
/**
* 状态名称,用于业务展示
*/
private String statusName;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 转换状态码为状态名称
*/
public void convertStatus() {
if (this.status != null) {
this.statusName = this.status == 1 ? "正常" : "禁用";
}
}
}
5.2.2.9 数据传输对象 (DTO)
查询请求 DTO
package com.sy.dto.request;
import lombok.Data;
/**
* 用户查询请求数据传输对象
* 用于接收前端传递的查询参数
*/
@Data
public class UserQueryRequest {
/**
* 用户名(模糊查询)
*/
private String username;
/**
* 真实姓名(模糊查询)
*/
private String realName;
/**
* 状态:0-禁用,1-正常
*/
private Integer status;
/**
* 当前页码
*/
private Integer pageNum = 1;
/**
* 每页条数
*/
private Integer pageSize = 10;
}
添加请求 DTO

更新请求 DTO

5.2.2.10 视图对象 (VO)

5.2.2.11 Mapper 接口和 XML
Mapper 接口
package com.sy.mapper;
import com.sy.dto.request.UserQueryRequest;
import com.sy.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 用户Mapper接口,定义数据库操作方法
*/
@Mapper
public interface UserMapper {
/**
* 根据ID查询用户
* @param id 用户ID
* @return 用户实体
*/
User selectById(Long id);
/**
* 根据用户名查询用户
* @param username 用户名
* @return 用户实体
*/
User selectByUsername(String username);
/**
* 分页查询用户列表
* @param queryRequest 查询条件
* @return 用户列表
*/
List<User> getUserPage(UserQueryRequest queryRequest);
/**
* 新增用户
* @param user 用户实体
* @return 影响行数
*/
int insert(User user);
/**
* 更新用户
* @param user 用户实体
* @return 影响行数
*/
int update(User user);
/**
* 根据ID删除用户
* @param id 用户ID
* @return 影响行数
*/
int deleteById(Long id);
}
UserMapper.xml
<?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接口 -->
<mapper namespace="com.sy.mapper.UserMapper">
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, username, password, real_name, phone, status, create_time
</sql>
<!-- 根据ID查询 -->
<select id="selectById" parameterType="java.lang.Long" resultType="com.sy.pojo.User">
SELECT
<include refid="Base_Column_List"/>
FROM user
WHERE id = #{id}
</select>
<!-- 根据用户名查询 -->
<select id="selectByUsername" parameterType="java.lang.String" resultType="com.sy.pojo.User">
SELECT
<include refid="Base_Column_List"/>
FROM user
WHERE username = #{username}
</select>
<!-- 分页查询 -->
<select id="selectByPage" parameterType="com.sy.dto.request.UserQueryRequest" resultType="com.sy.pojo.User">
SELECT
<include refid="Base_Column_List"/>
FROM user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="realName != null and realName != ''">
AND real_name LIKE CONCAT('%', #{realName}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
ORDER BY create_time DESC
</select>
<!-- 新增用户 -->
<insert id="insert" parameterType="com.sy.pojo.User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (
username, password, real_name, phone, status, create_time
) VALUES (
#{username}, #{password}, #{realName}, #{phone}, #{status}, NOW()
)
</insert>
<!-- 更新用户 -->
<update id="update" parameterType="com.sy.pojo.User">
UPDATE user
<set>
<if test="realName != null">real_name = #{realName},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="status != null">status = #{status},</if>
</set>
WHERE id = #{id}
</update>
<!-- 删除用户 -->
<delete id="deleteById" parameterType="java.lang.Long">
DELETE FROM user WHERE id = #{id}
</delete>
</mapper>
5.2.2.12 服务层 (Service)
服务接口
package com.sy.service;
import com.sy.vo.UserVO;
import com.sy.dto.request.UserQueryRequest;
import com.sy.dto.request.UserAddRequest;
import com.sy.dto.request.UserUpdateRequest;
import com.sy.dto.response.PageResult;
/**
* 用户服务接口,定义业务逻辑方法
*/
public interface UserService {
/**
* 根据ID查询用户
* @param id 用户ID
* @return 用户视图对象
*/
UserVO getUserById(Long id);
/**
* 分页查询用户列表
* @param queryRequest 查询条件
* @return 分页结果
*/
PageResult<UserVO> getUserPage(UserQueryRequest queryRequest);
/**
* 新增用户
* @param addRequest 新增用户参数
* @return 新增的用户ID
*/
Long addUser(UserAddRequest addRequest);
/**
* 更新用户
* @param updateRequest 更新用户参数
* @return 是否更新成功
*/
boolean updateUser(UserUpdateRequest updateRequest);
/**
* 根据ID删除用户
* @param id 用户ID
* @return 是否删除成功
*/
boolean deleteUser(Long id);
}
服务实现类
package com.sy.service.impl;
import com.sy.bo.UserBO;
import com.sy.pojo.User;
import com.sy.vo.UserVO;
import com.sy.mapper.UserMapper;
import com.sy.service.UserService;
import com.sy.dto.request.UserQueryRequest;
import com.sy.dto.request.UserAddRequest;
import com.sy.dto.request.UserUpdateRequest;
import com.sy.dto.response.PageResult;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* 用户服务实现类,实现具体业务逻辑
*/
@Service
public class UserServiceImpl implements UserService {
// 使用@Autowired注入Mapper
@Autowired
private UserMapper userMapper;
/**
* 根据ID查询用户
*/
@Override
public UserVO getUserById(Long id) {
// 查询数据库
User user = userMapper.selectById(id);
if (user == null) {
return null;
}
// 转换为业务对象并处理
UserBO userBO = new UserBO();
BeanUtils.copyProperties(user, userBO);
userBO.convertStatus();
// 转换为视图对象返回
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userBO, userVO);
return userVO;
}
/**
* 分页查询用户列表
*/
@Override
public PageResult<UserVO> getUserPage(UserQueryRequest queryRequest) {
// 启用分页插件
PageHelper.startPage(queryRequest.getPageNum(), queryRequest.getPageSize());
// 查询数据
List<User> userList = userMapper.selectByPage(queryRequest);
PageInfo<User> pageInfo = new PageInfo<>(userList);
// 转换为业务对象并处理
List<UserVO> userVOList = new ArrayList<>();
for (User user : userList) {
UserBO userBO = new UserBO();
BeanUtils.copyProperties(user, userBO);
userBO.convertStatus();
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userBO, userVO);
userVOList.add(userVO);
}
// 封装分页结果
return new PageResult<>(
pageInfo.getTotal(),
pageInfo.getPages(),
pageInfo.getPageNum(),
pageInfo.getPageSize(),
userVOList
);
}
/**
* 新增用户 - 带事务
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Long addUser(UserAddRequest addRequest) {
// 检查用户名是否已存在
User existUser = userMapper.selectByUsername(addRequest.getUsername());
if (existUser != null) {
throw new RuntimeException("用户名已存在");
}
// 转换为实体类
User user = new User();
BeanUtils.copyProperties(addRequest, user);
// 密码加密处理(MD5)
String encryptedPassword = DigestUtils.md5DigestAsHex(
addRequest.getPassword().getBytes(StandardCharsets.UTF_8));
user.setPassword(encryptedPassword);
// 插入数据库
userMapper.insert(user);
return user.getId();
}
/**
* 更新用户 - 带事务
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateUser(UserUpdateRequest updateRequest) {
// 检查用户是否存在
User existUser = userMapper.selectById(updateRequest.getId());
if (existUser == null) {
throw new RuntimeException("用户不存在");
}
// 转换为实体类
User user = new User();
BeanUtils.copyProperties(updateRequest, user);
// 更新数据库
int rows = userMapper.update(user);
return rows > 0;
}
/**
* 删除用户 - 带事务
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteUser(Long id) {
// 检查用户是否存在
User existUser = userMapper.selectById(id);
if (existUser == null) {
throw new RuntimeException("用户不存在");
}
// 删除用户
int rows = userMapper.deleteById(id);
return rows > 0;
}
}
5.2.2.13 控制层 (Controller)
package com.sy.controller;
import com.sy.vo.UserVO;
import com.sy.service.UserService;
import com.sy.utils.Result;
import com.sy.dto.request.UserQueryRequest;
import com.sy.dto.request.UserAddRequest;
import com.sy.dto.request.UserUpdateRequest;
import com.sy.dto.response.PageResult;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 用户控制器,处理前端请求
*/
@RestController
@RequestMapping("/user")
public class UserController {
// 使用@Autowired注入服务
@Autowired
private UserService userService;
/**
* 根据ID查询用户
*/
@GetMapping("/{id}")
public Result<UserVO> getUserById(@PathVariable Long id) {
UserVO userVO = userService.getUserById(id);
return Result.success(userVO);
}
/**
* 分页查询用户列表
*/
@GetMapping("/page")
public Result<PageResult<UserVO>> getUserPage(UserQueryRequest queryRequest) {
PageResult<UserVO> pageResult = userService.getUserPage(queryRequest);
return Result.success(pageResult);
}
/**
* 新增用户
*/
@PostMapping
public Result<Long> addUser(@Valid @RequestBody UserAddRequest addRequest) {
Long userId = userService.addUser(addRequest);
return Result.success(userId);
}
/**
* 更新用户
*/
@PutMapping
public Result<Boolean> updateUser(@Valid @RequestBody UserUpdateRequest updateRequest) {
boolean success = userService.updateUser(updateRequest);
return Result.success(success);
}
/**
* 删除用户
*/
@DeleteMapping("/{id}")
public Result<Boolean> deleteUser(@PathVariable Long id) {
boolean success = userService.deleteUser(id);
return Result.success(success);
}
}
六、SpringBoot使用logback日志框架
6.1 前言
项目中日志系统是必不可少的,目前比较流行的日志框架有 log4j、logback 等,可能大家还不知道,这两个框架的作者是同一个人,Logback 旨在作为流行的 log4j 项目的后续版本,从而恢复 log4j 离开的位置。
另外 slf4j(Simple Logging Facade for Java) 则是一个日志门面框架,提供了日志系统中常用的接口,logback 和 log4j 则对slf4j 进行了实现。
我们本文将讲述如何在 SpringBoot 中应用 logback+slf4j 实现日志的记录。
6.2 为什么使用logback
- Logback 是log4j 框架的作者开发的新一代日志框架,它效率更高、能够适应诸多的运行环境,同时天然支持 SLF4J。
- Logback 的定制性更加灵活,同时也是 SpringBoot 的内置日志框架。
6.3 开始使用
6.3.1 添加依赖
实际开发中我们直接引入spring-boot-starter-web依赖即可,因为spring-boot-starter-web包含了spring-boot-starter。而spring-boot-starter包含了spring-boot-starter-logging,所以我们只需要引入 web 组件即可。


6.3.2 logback-spring.xml详解
SpringBoot 官方推荐优先使用带有
-spring的文件名作为你的日志配置(如使用logback-spring.xml,而不是logback.xml),命名为logback-spring.xml的日志配置文件,将 xml 放至src/main/resource下面。也可以使用自定义的名称,比如
logback-config.xml,只需要在 application.properties 文件中使用logging.config=classpath:logback-config.xml指定即可。在讲解 logback-spring.xml之前我们先来了解三个单词:
- Logger(记录器)
- Appenders(附加器)
- Layouts(布局)
6.3.3 详细的logback-spring.xml示例
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<!-- appender是configuration的子节点,是负责写日志的组件。 -->
<!-- ConsoleAppender:把日志输出到控制台 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!--配置日志输出的格式-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--%c.%M:类名和方法名-->
<!-- %15.15():如果记录的线程字符长度小于15(第一个)则用空格在左侧补齐,如果字符长度大于15(第二个),则从开头开始截断多余的字符 -->
<!-- %msg:日志打印详情 -->
<!-- %n:换行符 -->
<!-- %highlight():转换说明符以粗体红色显示其级别为ERROR的事件,红色为WARN,BLUE;为INFO以及其他级别的默认颜色。 -->
<!-- %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %msg %cyan(%logger{5}).%M\(%F:%L\)%n-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) --- [%15.15(%thread)] %cyan(%(%logger{40})).%M\(%F:%L\) : %msg%n</pattern>
<!-- 控制台也要使用UTF-8,不要使用GBK,否则会中文乱码 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 设置日志输出文件的话,格式的配置-->
<!-- RollingFileAppender:日志记录到文件 -->
<!-- 以下的大概意思是:
1.先按日期存日志,日期变了,将前一天的日志文件名重命名为XXX%日期%索引,新的日志仍然是
project_data.log
2.如果日期没有发生变化,但是当前日志的文件大小超过10MB时,对当前日志进行分割 重命名-->
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志文件路径和名称-->
<File>F:\logs/project_data.log</File>
<!--是否追加到文件末尾,默认为true-->
<append>true</append>
<!--rollingPolicy是RollingFileAppender交互的重要子组件,负责执行翻转所需的操作。-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件的名字会根据fileNamePattern的值,每隔一段时间改变一次 -->
<!-- 文件名:logs/project_info.2023-12-05.0.log -->
<!-- 注意:SizeAndTimeBasedRollingPolicy中 %i和%d都是强制性的,必须存在,要不会报错 -->
<fileNamePattern>F:\logs/project_data.%d.%i.log</fileNamePattern>
<!-- 每产生一个日志文件,该日志文件的保存期限为30天;
ps:maxHistory的单位是根据fileNamePattern中的翻转策略自动推算出来的,
例如上面选用了yyyy-MM-dd,则单位为天
如果上面选用了yyyy-MM,则单位为月,另外上面的单位默认为yyyy-MM-dd-->
<maxHistory>30</maxHistory>
<!-- 每个日志文件到10mb的时候开始切分,最多保留30天,但最大到20GB(该文件夹最大存放20GB的文件),
哪怕没到30天也要删除多余的日志 -->
<totalSizeCap>20GB</totalSizeCap>
<!-- maxFileSize:这是活动文件的大小,默认值是10MB,测试时可改成5KB看效果 -->
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>
<!--编码器-->
<encoder>
<!-- pattern节点,用来设置日志的输入格式
ps:日志文件中没有设置颜色,否则颜色部分会有ESC[0:39em等乱码-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level --- [%15.15(%thread)] %(%logger{40}).%M\(%F:%L\) : %msg%n</pattern>
<!-- 记录日志的编码:此处设置字符集 - -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- configuration中最多允许一个root,别的logger如果没有设置级别则从父级别root继承;
例如name=STDOUT没有设置日志的级别,那么默认使用root中声明的INFO级别-->
<!--使用name=STDOUT的配置输出info以及info以上级别的日志;
级别依次为【从高到低】:FATAL > ERROR > WARN > INFO > DEBUG > TRACE -->
<!--level="INFO":如果name=STDOUT和name=file没有设置日志级别会使用priority的info级别;
如果name=STDOUT或者name=file设置了日志级别为error,那么以error为准;
如果name=STDOUT或者name=file设置了日志级别为了debug,那么以info为
准 -->
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="file" />
</root>
<!-- 指定项目中某个包,当有日志操作行为时的日志记录级别;
当com.sy.service出现WARN级别及以上级别的日志的时候使用name=file这个配置记录日志到文件;
当前配置的优先级高于全局配置,全局配置日志级别为info,如果sy.servic出现info级别的日志,也不会输出
,只有sy.service包中出现warn及其以上级别的日志才会使用name=file配置输出日志;
为防止日志重复输出,我们需要把additivity设置为false,如果sy.service出现了error级别的日志,只会
使用name=file配置输出日志,将不再使用全局配置输出日志
-->
<logger name="com.sy.service" level="WARN" additivity="false">
<appender-ref ref="file" />
</logger>
</configuration>
6.3.3 使用案例
使用slf4j的API很简单。使用LoggerFactory初始化一个Logger实例,然后调用Logger对应的打印等级函数就行了。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class App {
private static final Logger log = LoggerFactory.getLogger(App.class);
public static void main(String[] args) {
String msg = "print log, current level: ";
//级别依次为【从高到低】:FATAL > ERROR > WARN > INFO > DEBUG > TRACE
log.error(msg + "error");
log.warn(msg + "warn");
log.info(msg + "info");
log.debug(msg + "debug");
log.trace(msg + "trace");
}
}
注解@Slf4j的使用
声明:如果不想每次都写private final Logger logger = LoggerFactory.getLogger(当前类名.class); 可以用注解@Slf4j;
1、使用idea在pom文件加入lombok的依赖
pom.xml
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
2、类上面添加@Sl4j注解,然后使用log打印日志;


被折叠的 条评论
为什么被折叠?



