后端 Web 实战:部门管理模块全流程(前后端分离 + RESTful + 三层架构 + 日志)
大家好!前面几篇我们把 Web 开发的基础知识都过了一遍:Maven、HTTP 协议、SpringBootWeb、IOC/DI、MySQL、JDBC、MyBatis...... 从这一篇开始,我们就要进入后端 Web 实战篇 了------把前面学的东西全部用起来,完成一个大的综合案例:Tlias 智能学习辅助系统。
本文完整覆盖本讲全部章节:前后端分离开发规范 → RESTful 风格 → Apifox 工具 → 工程搭建 → 部门管理的查询 / 删除 / 新增 / 修改四大功能(三层架构 + 各种参数接收方式)→ 前后端联调(Nginx 反向代理)→ 日志技术(Logback)。
一、前言
在实战篇中,我们要用前面学的知识完成一个综合案例------Tlias 智能学习辅助系统,它包含以下功能模块:
- 部门管理

- 员工管理

- 员工信息统计

- 学员信息统计

- 班级、学员管理
📌 在整个实战篇中,我们需要完成如下功能:
- 部门管理:查询、新增、修改、删除
- 员工管理:查询、新增、修改、删除 + 文件上传
- 报表统计
- 登录认证
- 日志管理
- 班级管理(自己实战内容)
- 学员管理(自己实战内容)
那今天呢,我们就先来完成第一个模块:部门管理。
二、准备工作
2.1 开发规范
前后端分离开发
在之前的课程中我们介绍过,现在的企业项目开发有两种开发模式:前后台混合开发 和前后台分离开发。
前后台混合开发,顾名思义就是前台后台代码混在一起开发,如下图所示:

这种开发模式有如下缺点:
- 沟通成本高:后台人员发现前端有问题,需要找前端人员修改,前端修改成功,再交给后台人员使用;
- 分工不明确:后台开发人员既要开发后台代码,也要开发部分前端代码,很难培养专业人才;
- 不便管理:所有的代码都在一个工程中;
- 难以维护:前端代码更新和后台无关,但是需要整个工程包括后台一起重新打包部署。
所以我们现在基本都是采用前后台分离开发方式,如下图所示:

我们将原先的工程分为前端工程 和后端工程两个工程,前端工程交给专业的前端人员开发,后端工程交给专业的后端人员开发。
前端页面需要数据,可以通过发送异步请求从后台工程获取。但是,前后台是分开开发的,前端人员怎么知道后台返回数据的格式呢?后端人员怎么知道前端需要的数据格式呢?
所以针对这个问题,前后台统一制定了一套规范,前后台开发人员都需要遵循这套规范开发------这就是接口文档。
接口文档有离线版和在线版,可以查看课程资料「资料/接口文档」。接口文档的内容,是后台开发者根据产品经理提供的产品原型和需求文档撰写出来的,产品原型示例可以参考「资料/页面原型」。
基于前后台分离开发的模式下,后台开发者开发一个功能的具体流程如下:

- 需求分析:阅读需求文档,分析需求,理解需求;
- 接口定义:查询接口文档中关于需求的接口定义,包括地址、参数、响应数据类型等;
- 前后台并行开发:各自按照接口文档进行开发,实现需求;
- 测试:前后台开发完了,各自按照接口文档进行测试;
- 前后台联调测试:前端工程请求后端工程,测试功能。
RESTful 风格
我们的案例是基于当前最为主流的前后端分离模式进行开发的:

在前后端分离的开发模式中,前后端开发人员都需要根据提前定义好的接口文档来进行开发。
📌 后端开发人员:必须严格遵守提供的接口文档进行后端功能开发(保障开发的功能可以和前端对接)。
而在前后端进行交互的时候,我们需要基于当前主流的 REST 风格的 API 接口进行交互。
什么是 REST 风格呢?
- REST(Representational State Transfer),表述性状态转换,它是一种软件架构风格。
传统 URL 风格如下:
http://localhost:8080/user/getById?id=1------ GET:查询 id 为 1 的用户http://localhost:8080/user/saveUser------ POST:新增用户http://localhost:8080/user/updateUser------ POST:修改用户http://localhost:8080/user/deleteUser?id=1------ GET:删除 id 为 1 的用户
我们看到,原始的传统 URL 定义比较复杂,而且将资源的访问行为对外暴露出来了。而且对于开发人员来说,每个人都有自己命名习惯:就拿"根据 id 查询用户信息"来说,不同的开发人员定义的路径可能是 getById、selectById、queryById、loadById...... 如果一个项目组有几十上百号人,都按各自习惯来,最终的项目将会难以维护,没有一个统一标准。
基于 REST 风格 URL 如下:
http://localhost:8080/users/1------ GET:查询 id 为 1 的用户http://localhost:8080/users------ POST:新增用户http://localhost:8080/users------ PUT:修改用户http://localhost:8080/users/1------ DELETE:删除 id 为 1 的用户
其中总结起来就一句话:通过 URL 定位要操作的资源,通过 HTTP 动词(请求方式)来描述具体的操作。
在 REST 风格的 URL 中,通过四种请求方式来操作数据的增删改查:

- GET:查询
- POST:新增
- PUT:修改
- DELETE:删除
基于 REST 风格定义 URL,URL 将会更加简洁、更加规范、更加优雅。
📌 注意事项:
- REST 是风格、是约定方式,约定不是规定,可以打破;
- 描述模块的功能通常使用复数(加 s 的格式),表示此类资源而非单个资源,如:users、emps、books......
Apifox
上面讲到,在这个案例中我们基于 RESTful 风格的接口进行交互,其中涉及 POST、DELETE、PUT、GET 四种请求方式。
因为在浏览器中所发起的请求,全都是 GET 方式。那大家就需要思考两个问题:
- 前后端都在并行开发,后端开发完对应接口之后,如何对接口进行请求测试呢?
- 前后端都在并行开发,前端开发过程中,如何获取数据、测试页面的渲染展示呢?
这里我们可以借助一些接口测试工具,比如:Postman、Apipost、Apifox 等。

这些工具的使用基本类似,只不过 Apifox 的功能更加强大、更加完善,所以在课程中采用功能更强大的 Apifox 工具。
**介绍:**Apifox 是一款集成了 Api 文档、Api 调试、Api Mock、Api 测试的一体化协作平台。
- 作用:接口文档管理、接口请求测试、Mock 服务;
- 官网:apifox.com/

**安装:**下载 Apifox 的安装包(资料中已经提供),直接双击安装:

安装完成之后,扫码登录 Apifox 就可以使用了:


2.2 工程搭建
1)创建 SpringBoot 工程,并引入 web 开发起步依赖、mybatis、mysql 驱动、lombok

2)创建数据库及对应的表结构,并在 application.yml 中配置数据库基本信息
- 创建
tlias数据库,并准备dept部门表:
sql
CREATE TABLE dept (
id int unsigned PRIMARY KEY AUTO_INCREMENT COMMENT 'ID, 主键',
name varchar(10) NOT NULL UNIQUE COMMENT '部门名称',
create_time datetime DEFAULT NULL COMMENT '创建时间',
update_time datetime DEFAULT NULL COMMENT '修改时间'
) COMMENT '部门表';
INSERT INTO dept VALUES (1,'学工部','2023-09-25 09:47:40','2024-07-25 09:47:40'),
(2,'教研部','2023-09-25 09:47:40','2024-08-09 15:17:04'),
(3,'咨询部','2023-09-25 09:47:40','2024-07-30 21:26:24'),
(4,'就业部','2023-09-25 09:47:40','2024-07-25 09:47:40'),
(5,'人事部','2023-09-25 09:47:40','2024-07-25 09:47:40'),
(6,'行政部','2023-11-30 20:56:37','2024-07-30 20:56:37');
- 在
application.yml配置文件中配置数据库的连接信息:
yaml
spring:
application:
name: tlias-web-management
#mysql连接配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/tlias
username: root
password: 1234
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3)准备基础包结构,并引入实体类 Dept 及统一的响应结果封装类 Result
- 准备基础包结构(
controller / service / service.impl / mapper / pojo):

- 实体类 Dept:
java
package com.itheima.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Dept {
private Integer id;
private String name;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
- 统一响应结果 Result:
java
package com.itheima.pojo;
import lombok.Data;
import java.io.Serializable;
/**
* 后端统一返回结果
*/
@Data
public class Result {
private Integer code; //编码:1成功,0为失败
private String msg; //错误信息
private Object data; //数据
public static Result success() {
Result result = new Result();
result.code = 1;
result.msg = "success";
return result;
}
public static Result success(Object object) {
Result result = new Result();
result.data = object;
result.code = 1;
result.msg = "success";
return result;
}
public static Result error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}
}
- 基础代码结构(各层空壳):

java
// DeptMapper
package com.itheima.mapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DeptMapper {
}
java
// DeptService
package com.itheima.service;
public interface DeptService {
}
java
// DeptServiceImpl
package com.itheima.service.impl;
import com.itheima.service.DeptService;
import org.springframework.stereotype.Service;
@Service
public class DeptServiceImpl implements DeptService {
}
java
// DeptController
package com.itheima.controller;
import org.springframework.web.bind.annotation.RestController;
/**
* 部门管理控制器
*/
@RestController
public class DeptController {
}
三、查询部门
3.1 基本实现
**需求:**查询所有的部门数据,查询出来展示在部门管理的页面中。页面原型效果如下:

**接口描述:**参照课程资料中提供的接口文档 ------ 部门管理 -> 部门列表查询。
**实现思路:**明确了需求之后,梳理一下实现该功能时三层架构每一层的职责:

- Controller 层:负责接收前端发起的请求,调用 service 查询部门数据,然后响应结果;
- Service 层:负责调用 Mapper 接口方法,查询所有部门数据;
- Mapper 层:执行查询所有部门数据的操作。
代码实现:
1)Controller 层 ------ 在 DeptController 中增加 list 方法:
java
/**
* 部门管理控制器
*/
@RestController
public class DeptController {
@Autowired
private DeptService deptService;
/**
* 查询部门列表
*/
@RequestMapping("/depts")
public Result list(){
List<Dept> deptList = deptService.findAll();
return Result.success(deptList);
}
}
2)Service 层 ------ 在 DeptService 与 DeptServiceImpl 中增加 findAll 方法:
java
public interface DeptService {
/**
* 查询所有部门
*/
public List<Dept> findAll();
}
java
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
public List<Dept> findAll() {
return deptMapper.findAll();
}
}
3)Mapper 层 ------ 在 DeptMapper 中增加 findAll 方法:
java
@Mapper
public interface DeptMapper {
/**
* 查询所有部门
*/
@Select("select * from dept")
public List<Dept> findAll();
}
**接口测试:**启动项目,打开 Apifox 进行测试:

我们发现已经查询出了所有的部门数据,并且响应回来的就是 json 格式的数据,与接口文档一致。那接下来再测试一下,用 post、put、delete 方式请求,是否可以获取到数据:

经过测试发现,现在可以通过任何方式的请求来访问查询部门的接口。而接口文档中明确要求该接口的请求方式为 GET,那么如何限制请求方式呢?
- 方式一 :在 controller 方法的
@RequestMapping注解中通过method属性来限定:
java
@RestController
public class DeptController {
@Autowired
private DeptService deptService;
/**
* 查询部门列表
*/
@RequestMapping(value = "/depts", method = RequestMethod.GET)
public Result list(){
List<Dept> deptList = deptService.findAll();
return Result.success(deptList);
}
}
- 方式二 :使用
@RequestMapping的衍生注解@GetMapping,该注解标识当前方法必须以 GET 方式请求:
java
@RestController
public class DeptController {
@Autowired
private DeptService deptService;
/**
* 查询部门列表
*/
@GetMapping("/depts")
public Result list(){
List<Dept> deptList = deptService.findAll();
return Result.success(deptList);
}
}
📌 上述两种方式,在项目开发中推荐使用第二种,简洁、优雅。
- GET 方式:
@GetMapping- POST 方式:
@PostMapping- PUT 方式:
@PutMapping- DELETE 方式:
@DeleteMapping
3.2 数据封装
在上述测试中,我们发现部门数据中 id、name 两个属性有值,但 createTime、updateTime 两个字段值并未成功封装,而数据库中是有对应字段值的,这是为什么呢?

原因如下:
- 实体类属性名和数据库表查询返回的字段名一致,MyBatis 会自动封装;
- 实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。
解决方案有三种:手动结果映射、起别名、开启驼峰命名。
1)手动结果映射
在 DeptMapper 接口方法上,通过 @Results 及 @Result 进行手动结果映射:
java
@Results({@Result(column = "create_time", property = "createTime"),
@Result(column = "update_time", property = "updateTime")})
@Select("select id, name, create_time, update_time from dept")
public List<Dept> findAll();
说明:
@Results注解源码:

@Result注解源码:

2)起别名
在 SQL 语句中,对不一样的列名起别名,别名和实体类属性名一样:
java
@Select("select id, name, create_time createTime, update_time updateTime from dept")
public List<Dept> findAll();
3)开启驼峰命名(推荐)
如果字段名与属性名符合驼峰命名规则,MyBatis 会自动通过驼峰命名规则映射。驼峰命名规则:abc_xyz => abcXyz(表中字段名 abc_xyz、类中属性名 abcXyz)。
在 application.yml 中做如下配置开启开关:
yaml
mybatis:
configuration:
map-underscore-to-camel-case: true
📌 要使用驼峰命名,前提是实体类的属性与数据库表中的字段名严格遵守驼峰命名。
3.3 前后端联调
**联调测试:**完成了查询部门的功能并通过 Apifox 测试后,下面基于前后端分离的方式进行接口联调。具体操作如下:
1)将资料中"前端环境"文件夹中的压缩包,拷贝到一个没有中文、不带空格的目录下:

**2)**解压(解压到当前目录):

**3)**双击 nginx.exe 启动 Nginx,一闪而过就说明 nginx 已启动完成。如果在任务管理器中能看到相关进程,就说明 nginx 启动成功:

**4)**打开浏览器,访问 http://localhost:90/:

**请求访问流程:**前端工程请求服务器的地址为 http://localhost:90/api/depts,是如何访问到后端的 tomcat 服务器的?

其实这里是通过前端服务 Nginx 中提供的反向代理功能实现的:

- 浏览器发起请求,请求的是
localhost:90,那其实请求的是 nginx 服务器; - 在 nginx 服务器中并没有对请求直接进行处理,而是将请求转发给了后端的 tomcat 服务器,最终由 tomcat 服务器处理该请求。
那为什么浏览器不直接请求后端的 tomcat 服务器,而是请求 nginx 服务器呢?主要有以下几点原因:

- 安全:后端的 tomcat 服务器一般都会搭建集群,会有很多服务器,把所有的 tomcat 暴露给前端直接请求,对后端服务器比较危险;
- 灵活:基于 nginx 的反向代理实现更加灵活,后端想增加、减少服务器,对前端是无感知的,只需在 nginx 中配置即可;
- 负载均衡:基于 nginx 的反向代理,可以很方便地实现后端 tomcat 的负载均衡操作。
具体的请求访问流程如下:

location:用于定义匹配特定 uri 请求的规则;^~ /api/:表示精确匹配,即只匹配以/api/开头的路径;rewrite:该指令用于重写匹配到的 uri 路径;proxy_pass:该指令用于代理转发,它将匹配到的请求转发给位于后端的服务器。
四、删除部门
**需求:**删除部门数据。点击"删除"按钮,会根据 ID 删除部门数据:

**接口描述:**参照课程资料中提供的接口文档 ------ 部门管理 -> 删除部门。
**思路分析:**明确了需求之后,梳理三层架构每一层的职责:

**简单参数接收:**在 controller 中需要接收前端传递的请求参数(id)。在服务器端的 Controller 程序中,获取这类简单参数有如下三种方案:
- 方案一:通过原始的
HttpServletRequest对象获取请求参数
java
/**
* 根据ID删除部门 - 简单参数接收: 方式一 (HttpServletRequest)
*/
@DeleteMapping("/depts")
public Result delete(HttpServletRequest request){
String idStr = request.getParameter("id");
int id = Integer.parseInt(idStr);
System.out.println("根据ID删除部门: " + id);
return Result.success();
}
这种方案实现较为繁琐,而且还需要进行手动类型转换。【项目开发很少用】
- 方案二:通过 Spring 提供的
@RequestParam注解,将请求参数绑定给方法形参
java
@DeleteMapping("/depts")
public Result delete(@RequestParam("id") Integer deptId){
System.out.println("根据ID删除部门: " + deptId);
return Result.success();
}
@RequestParam 注解的 value 属性需要与前端传递的参数名保持一致。
📌
@RequestParam注解的 required 属性默认为 true,代表该参数必须传递,如果不传递将报错。如果参数可选,可以将属性设置为 false。
- 方案三:如果请求参数名与形参变量名相同,直接定义方法形参即可接收(省略 @RequestParam)
java
@DeleteMapping("/depts")
public Result delete(Integer id){
System.out.println("根据ID删除部门: " + deptId);
return Result.success();
}
对于以上三种方案,我们推荐第三种方案。
代码实现(三层架构):
1)Controller 层:
java
/**
* 根据id删除部门 - delete http://localhost:8080/depts?id=1
*/
@DeleteMapping("/depts")
public Result delete(Integer id){
System.out.println("根据id删除部门, id=" + id);
deptService.deleteById(id);
return Result.success();
}
2)Service 层:
java
/**
* 根据id删除部门
*/
void deleteById(Integer id);
java
public void deleteById(Integer id) {
deptMapper.deleteById(id);
}
3)Mapper 层:
java
/**
* 根据id删除部门
*/
@Delete("delete from dept where id = #{id}")
void deleteById(Integer id);
如果 mapper 接口方法形参只有一个普通类型的参数,
#{...}里面的属性名可以随便写,如#{id}、#{value}。DML 语句执行完毕是有返回值的,返回值代表增删改操作影响的记录数,所以可以将执行 DML 语句的方法返回值设置为 Integer。但一般开发时不需要这个返回值,也可以设置为 void。
代码编写完毕之后,启动服务进行测试:

五、新增部门
**需求:**点击"新增部门"按钮之后,弹出新增部门表单,填写部门名称,点击确定之后保存部门数据:

**接口描述:**参照课程资料中提供的接口文档 ------ 部门管理 -> 新增部门。
**思路分析:**明确了需求之后,梳理三层架构每一层的职责:

**json 参数接收:**前端传递的请求参数是 json 格式(如 {"name":"研发部"})。在 Controller 中,如何获取 json 格式的参数呢?
- JSON 格式的参数,通常会使用一个实体对象进行接收;
- 规则:JSON 数据的键名与方法形参对象的属性名相同,并使用
@RequestBody注解标识。
这里我们可以通过一个对象来接收,只需要保证对象中有 name 属性即可:

代码实现(三层架构):
1)Controller 层:
java
/**
* 新增部门 - POST http://localhost:8080/depts 请求参数:{"name":"研发部"}
*/
@PostMapping("/depts")
public Result save(@RequestBody Dept dept){
System.out.println("新增部门, dept=" + dept);
deptService.save(dept);
return Result.success();
}
2)Service 层:
java
/**
* 新增部门
*/
void save(Dept dept);
java
public void save(Dept dept) {
//补全基础属性
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
//保存部门
deptMapper.insert(dept);
}
3)Mapper 层:
java
/**
* 保存部门
*/
@Insert("insert into dept(name,create_time,update_time) values(#{name},#{createTime},#{updateTime})")
void insert(Dept dept);
如果在 mapper 接口中需要传递多个参数,可以把多个参数封装到一个对象中。在 SQL 语句中获取参数时,
#{...}里面写的是对象的属性名【注意是属性名,不是表的字段名】。
代码编写完毕之后,启动服务进行测试:


六、修改部门
对于任何业务的修改功能来说,一般都会分为两步进行:查询回显、修改数据。

6.1 查询回显
**需求:**点击"编辑"按钮时,根据 ID 查询部门数据,用于页面回显展示:

**接口描述:**参照课程资料中提供的接口文档 ------ 部门管理 -> 根据ID查询。
**思路分析:**明确了需求之后,梳理三层架构每一层的职责:

路径参数接收: /depts/1、/depts/2 这种在 url 中传递的参数,我们称之为路径参数 。路径参数通过请求 URL 直接传递,使用 {...} 来标识,需要使用 @PathVariable 获取:

如果路径参数名与 controller 方法形参名称一致,
@PathVariable注解的 value 属性是可以省略的。
代码实现(三层架构):
1)Controller 层:
java
/**
* 根据ID查询 - GET http://localhost:8080/depts/1
*/
@GetMapping("/depts/{id}")
public Result getById(@PathVariable Integer id){
System.out.println("根据ID查询, id=" + id);
Dept dept = deptService.getById(id);
return Result.success(dept);
}
2)Service 层:
java
/**
* 根据id查询部门
*/
Dept getById(Integer id);
java
public Dept getById(Integer id) {
return deptMapper.getById(id);
}
3)Mapper 层:
java
/**
* 根据ID查询部门数据
*/
@Select("select id, name, create_time, update_time from dept where id = #{id}")
Dept getById(Integer id);
代码编写完毕之后,启动服务进行测试:

6.2 修改数据
**需求:**查询回显回来之后,就可以对部门信息进行修改,修改完毕点击确定,此时就需要根据 ID 修改部门的数据:

**接口描述:**参照课程资料中提供的接口文档 ------ 部门管理 -> 修改部门。
**思路分析:**参照接口文档,梳理三层架构每一层的职责:

通过接口文档可以看到,前端传递的请求参数是 json 格式,在 Controller 的方法中可以通过 @RequestBody 注解接收,并封装到一个对象中。
代码实现(三层架构):
1)Controller 层:
java
/**
* 修改部门 - PUT http://localhost:8080/depts 请求参数:{"id":1,"name":"研发部"}
*/
@PutMapping("/depts")
public Result update(@RequestBody Dept dept){
System.out.println("修改部门, dept=" + dept);
deptService.update(dept);
return Result.success();
}
**2)Service 层:**由于是修改操作,每一次修改数据都需要更新 updateTime:
java
/**
* 修改部门
*/
void update(Dept dept);
java
public void update(Dept dept) {
//补全基础属性
dept.setUpdateTime(LocalDateTime.now());
//保存部门
deptMapper.update(dept);
}
3)Mapper 层:
java
/**
* 更新部门
*/
@Update("update dept set name = #{name},update_time = #{updateTime} where id = #{id}")
void update(Dept dept);
代码编写完毕之后,启动服务进行测试:

修改完成之后,可以看到最新的数据:

6.3 @RequestMapping
到此,基本的部门增删改查功能已经实现了。我们会发现,在 DeptController 中定义的方法,所有请求路径都是以 /depts 开头的------只要操作的是部门数据,请求路径就是 /depts 开头。
那么这个时候,其实可以把公共路径 /depts 抽取到类上 ,各个方法上就可以省略 /depts 路径了,代码如下:

java
@RestController
@RequestMapping("/depts")
public class DeptController {
@Autowired
private DeptService deptService;
@GetMapping
public Result list(){
List<Dept> deptList = deptService.findAll();
return Result.success(deptList);
}
@DeleteMapping
public Result delete(Integer id){
deptService.deleteById(id);
return Result.success();
}
@PostMapping
public Result save(@RequestBody Dept dept){
deptService.save(dept);
return Result.success();
}
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id){
Dept dept = deptService.getById(id);
return Result.success(dept);
}
@PutMapping
public Result update(@RequestBody Dept dept){
deptService.update(dept);
return Result.success();
}
}
✅ 一个完整的请求路径,应该是类上的 @RequestMapping 的 value 属性 + 方法上的 @RequestMapping 的 value 属性。
七、日志技术
7.1 概述
- 什么是日志?
日志就好比生活中的日记,可以随时随地记录生活中的点点滴滴。程序中的日志,是用来记录应用程序的运行信息、状态信息、错误信息的。
-
为什么要在程序中记录日志?
- 便于追踪应用程序中的数据信息、程序的执行过程;
- 便于对应用程序的性能进行优化;
- 便于应用程序出现问题之后排查问题、解决问题;
- 便于监控系统的运行状态;
- ......
-
之前我们编写程序时,也可以通过
System.out.println(...)输出日志,为什么还要学习单独的日志技术呢?
因为通过 System.out.println(...) 记录日志,会存在以下几点问题:
- 硬编码:所有记录日志的代码都是硬编码,没办法灵活控制,要想不输出日志只能删除记录日志的代码;
- 只能输出日志到控制台;
- 不便于程序的扩展、维护。
所以,在现在的项目开发中,我们一般都会使用专业的日志框架来解决这些问题。
7.2 日志框架

- JUL:JavaSE 平台提供的官方日志框架,配置相对简单,但不够灵活,性能较差;
- Log4j:一个流行的日志框架,提供了灵活的配置选项,支持多种输出目标;
- Logback:基于 Log4j 升级而来,提供了更多的功能和配置选项,性能优于 Log4j;
- Slf4j:(Simple Logging Facade for Java)简单日志门面,提供了一套日志操作的标准接口及抽象类,允许应用程序使用不同的底层日志框架。
7.3 Logback 入门
1)准备工作:引入 logback 的依赖(SpringBoot 中无需引入,已经传递了此依赖)
xml
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>
2)引入配置文件 logback.xml (资料中已经提供,拷贝进 src/main/resources 目录下;或者直接 AI 生成)
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n</pattern>
</encoder>
</appender>
<!-- 日志输出级别 -->
<root level="ALL">
<appender-ref ref="STDOUT" />
</root>
</configuration>
3)记录日志:定义日志记录对象 Logger,记录日志
java
public class LogTest {
//定义日志记录对象
private static final Logger log = LoggerFactory.getLogger(LogTest.class);
@Test
public void testLog(){
log.debug("开始计算...");
int sum = 0;
int[] nums = {1, 5, 3, 2, 1, 4, 5, 4, 6, 7, 4, 34, 2, 23};
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
log.info("计算结果为: "+sum);
log.debug("结束计算...");
}
}
运行单元测试,可以在控制台中看到输出的日志。可以看到输出的日志信息中,不仅输出了日志的信息,还包括:日志的输出时间、线程名、具体在哪个类中输出的。

7.4 Logback 配置文件
Logback 日志框架的配置文件叫 logback.xml,用来控制日志的输出格式、位置及日志开关等。常用的两种输出日志的位置:控制台、系统文件。
1)输出日志到控制台:
xml
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d 表示日期,%thread 表示线程名,%-5level表示级别从左显示5个字符宽度,%msg表示日志消息,%n表示换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n</pattern>
</encoder>
</appender>
2)输出日志到文件:
xml
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件输出的文件名, %i表示序号 -->
<FileNamePattern>D:/tlias-%d{yyyy-MM-dd}-%i.log</FileNamePattern>
<!-- 最多保留的历史日志文件数量 -->
<MaxHistory>30</MaxHistory>
<!-- 最大文件大小,超过这个大小会触发滚动到新文件,默认为 10MB -->
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d 表示日期,%thread 表示线程名,%-5level表示级别从左显示5个字符宽度,%msg表示日志消息,%n表示换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n</pattern>
</encoder>
</appender>
3)日志开关配置(开启日志 ALL,取消日志 OFF):
xml
<!-- 日志输出级别 -->
<root level="ALL">
<!--输出到控制台-->
<appender-ref ref="STDOUT" />
<!--输出到文件-->
<appender-ref ref="FILE" />
</root>
7.5 Logback 日志级别
日志级别指的是日志信息的类型,日志都会分级别,常见的日志级别如下(优先级由低到高):
| 日志级别 | 说明 | 记录方式 |
|---|---|---|
| trace | 追踪,记录程序运行轨迹【使用很少】 | log.trace("...") |
| debug | 调试,记录程序调试过程中的信息,实际应用中一般将其视为最低级别【使用较多】 | log.debug("...") |
| info | 记录一般信息,描述程序运行的关键事件,如:网络连接、io 操作【使用较多】 | log.info("...") |
| warn | 警告信息,记录潜在有害的情况【使用较多】 | log.warn("...") |
| error | 错误信息【使用较多】 | log.error("...") |
可以在配置文件 logback.xml 中灵活地控制输出哪些类型的日志(大于等于配置的日志级别的日志才会输出):
xml
<!-- 日志输出级别 -->
<root level="info">
<!--输出到控制台-->
<appender-ref ref="STDOUT" />
<!--输出到文件-->
<appender-ref ref="FILE" />
</root>
7.6 案例日志记录
在部门管理案例中,我们可以用日志替换掉原来的 System.out.println,并使用 Lombok 提供的 @Slf4j 注解简化日志记录器的定义:
java
/**
* 部门管理控制器
*/
@Slf4j
@RequestMapping("/depts")
@RestController
public class DeptController {
@Autowired
private DeptService deptService;
/**
* 查询部门列表
*/
@GetMapping
public Result list(){
log.info("查询部门列表");
List<Dept> deptList = deptService.findAll();
return Result.success(deptList);
}
/**
* 根据id删除部门 - delete http://localhost:8080/depts?id=1
*/
@DeleteMapping
public Result delete(Integer id){
log.info("根据id删除部门, id: {}" , id);
deptService.deleteById(id);
return Result.success();
}
/**
* 新增部门 - POST http://localhost:8080/depts 请求参数:{"name":"研发部"}
*/
@PostMapping
public Result save(@RequestBody Dept dept){
log.info("新增部门, dept: {}" , dept);
deptService.save(dept);
return Result.success();
}
/**
* 根据ID查询 - GET http://localhost:8080/depts/1
*/
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id){
log.info("根据ID查询, id: {}" , id);
Dept dept = deptService.getById(id);
return Result.success(dept);
}
/**
* 修改部门 - PUT http://localhost:8080/depts 请求参数:{"id":1,"name":"研发部"}
*/
@PutMapping
public Result update(@RequestBody Dept dept){
log.info("修改部门, dept: {}" , dept);
deptService.update(dept);
return Result.success();
}
}
✅ Lombok 提供的
@Slf4j注解可以简化定义日志记录器这步操作。添加了该注解,就相当于在类中定义了日志记录器,即下面这句代码:private static Logger log = LoggerFactory.getLogger(Xxx.class);
八、小结
本讲我们完成了部门管理模块的完整闭环,核心收获如下:
- 开发模式 :前后台分离开发是主流,前后端通过接口文档统一规范并行开发;
- RESTful 风格:通过 URL 定位资源、HTTP 动词描述操作(GET 查询 / POST 新增 / PUT 修改 / DELETE 删除),URL 简洁规范优雅;
- 三层架构:Controller(接收请求响应)→ Service(业务逻辑)→ Mapper(数据库操作),各司其职;
- 参数接收 :简单参数(同名形参推荐 /
@RequestParam/HttpServletRequest)、JSON 参数(@RequestBody+ 实体对象)、路径参数(@PathVariable); - 数据封装 :字段名与属性名不一致时,推荐开启驼峰命名映射 (
map-underscore-to-camel-case: true); - Nginx 反向代理 :解决安全、灵活、负载均衡三大问题,
location匹配 +rewrite重写 +proxy_pass转发; - 日志技术 :用专业的日志框架(Logback + Slf4j)替代
System.out.println,灵活控制输出位置与级别,@Slf4j一步搞定日志器。
参考内容 heuqqdmbyk.feishu.cn/wiki/VT2ewT... 黑马磊哥