后端 Web 实战:部门管理模块全流程(前后端分离 + RESTful + 三层架构 + 日志)

后端 Web 实战:部门管理模块全流程(前后端分离 + RESTful + 三层架构 + 日志)

大家好!前面几篇我们把 Web 开发的基础知识都过了一遍:Maven、HTTP 协议、SpringBootWeb、IOC/DI、MySQL、JDBC、MyBatis...... 从这一篇开始,我们就要进入后端 Web 实战篇 了------把前面学的东西全部用起来,完成一个大的综合案例:Tlias 智能学习辅助系统。

本文完整覆盖本讲全部章节:前后端分离开发规范 → RESTful 风格 → Apifox 工具 → 工程搭建 → 部门管理的查询 / 删除 / 新增 / 修改四大功能(三层架构 + 各种参数接收方式)→ 前后端联调(Nginx 反向代理)→ 日志技术(Logback)。

一、前言

在实战篇中,我们要用前面学的知识完成一个综合案例------Tlias 智能学习辅助系统,它包含以下功能模块:

  • 部门管理
  • 员工管理
  • 员工信息统计
  • 学员信息统计
  • 班级、学员管理

📌 在整个实战篇中,我们需要完成如下功能:

  • 部门管理:查询、新增、修改、删除
  • 员工管理:查询、新增、修改、删除 + 文件上传
  • 报表统计
  • 登录认证
  • 日志管理
  • 班级管理(自己实战内容)
  • 学员管理(自己实战内容)

那今天呢,我们就先来完成第一个模块:部门管理。

二、准备工作

2.1 开发规范

前后端分离开发

在之前的课程中我们介绍过,现在的企业项目开发有两种开发模式:前后台混合开发 和前后台分离开发。

前后台混合开发,顾名思义就是前台后台代码混在一起开发,如下图所示:

这种开发模式有如下缺点:

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

所以我们现在基本都是采用前后台分离开发方式,如下图所示:

我们将原先的工程分为前端工程 和后端工程两个工程,前端工程交给专业的前端人员开发,后端工程交给专业的后端人员开发。

前端页面需要数据,可以通过发送异步请求从后台工程获取。但是,前后台是分开开发的,前端人员怎么知道后台返回数据的格式呢?后端人员怎么知道前端需要的数据格式呢?

所以针对这个问题,前后台统一制定了一套规范,前后台开发人员都需要遵循这套规范开发------这就是接口文档。

接口文档有离线版和在线版,可以查看课程资料「资料/接口文档」。接口文档的内容,是后台开发者根据产品经理提供的产品原型和需求文档撰写出来的,产品原型示例可以参考「资料/页面原型」。

基于前后台分离开发的模式下,后台开发者开发一个功能的具体流程如下:

  1. 需求分析:阅读需求文档,分析需求,理解需求;
  2. 接口定义:查询接口文档中关于需求的接口定义,包括地址、参数、响应数据类型等;
  3. 前后台并行开发:各自按照接口文档进行开发,实现需求;
  4. 测试:前后台开发完了,各自按照接口文档进行测试;
  5. 前后台联调测试:前端工程请求后端工程,测试功能。
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 中提供的反向代理功能实现的:

  1. 浏览器发起请求,请求的是 localhost:90,那其实请求的是 nginx 服务器;
  2. 在 nginx 服务器中并没有对请求直接进行处理,而是将请求转发给了后端的 tomcat 服务器,最终由 tomcat 服务器处理该请求。

那为什么浏览器不直接请求后端的 tomcat 服务器,而是请求 nginx 服务器呢?主要有以下几点原因:

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

具体的请求访问流程如下:

  1. location:用于定义匹配特定 uri 请求的规则;
  2. ^~ /api/:表示精确匹配,即只匹配以 /api/ 开头的路径;
  3. rewrite:该指令用于重写匹配到的 uri 路径;
  4. 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);

八、小结

本讲我们完成了部门管理模块的完整闭环,核心收获如下:

  1. 开发模式 :前后台分离开发是主流,前后端通过接口文档统一规范并行开发;
  2. RESTful 风格:通过 URL 定位资源、HTTP 动词描述操作(GET 查询 / POST 新增 / PUT 修改 / DELETE 删除),URL 简洁规范优雅;
  3. 三层架构:Controller(接收请求响应)→ Service(业务逻辑)→ Mapper(数据库操作),各司其职;
  4. 参数接收 :简单参数(同名形参推荐 / @RequestParam / HttpServletRequest)、JSON 参数(@RequestBody + 实体对象)、路径参数(@PathVariable);
  5. 数据封装 :字段名与属性名不一致时,推荐开启驼峰命名映射 (map-underscore-to-camel-case: true);
  6. Nginx 反向代理 :解决安全、灵活、负载均衡三大问题,location 匹配 + rewrite 重写 + proxy_pass 转发;
  7. 日志技术 :用专业的日志框架(Logback + Slf4j)替代 System.out.println,灵活控制输出位置与级别,@Slf4j 一步搞定日志器。

参考内容 heuqqdmbyk.feishu.cn/wiki/VT2ewT... 黑马磊哥

相关推荐
房西杭1 小时前
背包<->仓库转移接口(BagMgr + StoreMgr,Lua)
后端
tntxia1 小时前
设计模式中模板方法的钩子使用
后端
我的div丢了肿么办1 小时前
go语言对路由中的业务逻辑进行抽离
后端·go
leo在掘金1 小时前
纯Rust重写Photoshop,一个月7.2万Star
后端·架构
韩振方1 小时前
Kubernetes 通过资源 requests 决定 Pending Pod 能否找到节点
后端
用户360055579001 小时前
Kestrel_VLLM - 18 · 投机解码位级对拍收口:batch 与 M=1 一致,注意力路径与 CPU 逐字节对齐
后端
Thneonl1 小时前
git reset --hard 之后先别哭:reflog 里存着你刚弄丢的那个分支
运维·后端
弦儿1 小时前
后端 Web 实战:员工管理模块(多表关系 + 多表查询 + 分页查询 + 条件分页)
后端·sql
王中阳Go1 小时前
网页上答对、接口里答错:同一个知识库,差的不是模型是入口
人工智能·后端·架构