Web后端实战

Tlias智能学习辅助系统

  • 需求:

  • 部门管理:

    • 查询、新增、修改、删除
  • 员工管理:

    • 查询、新增、修改、删除
    • 文件上传
  • 报表统计

  • 登录认证

  • 日志管理

  • 班级、学员管理(实战内容)

准备工作

开发规范 - 开发模式

前后端混合开发

前后端分离开发

  • 当前最为主流的开发模式:前后端分离

此处的接口为功能接口,接口是依据原型+ 需求进行设计的

该项目的接口文档:接口文档 - 飞书云文档

基本的开发流程为:

开发规范 - Restful风格

  • REST(REpresentational State Transfer),表述性状态转换,它是一种软件架构风格。

传统风格:不规范、难维护

REST风格:URL定位资源,HTTP动词描述操作

注意:

1.REST是风格,是约定方式,约定不是规定,可以打破

2.描述功能模块通常使用复数形式(加s),表示此类资源,而非单个资源。如:users、books…

  • 思考:

    • 前后端都在并行开发,后端开发完对应的接口之后,如何对接口进行请求测试呢?

      浏览器中发送的请求都是GET方式的请求,无法发送其他三个请求

    • 前后端都在并行开发,前端开发过程中,如何获取到数据,测试页面的渲染展示呢?

此时需要借助两个工具(选其一即可):

此处使用Apifox工具:

  • 介绍:Apifox是一款集成了Api文档、Api调试、Api Mock、Api测试的一体化协作平台

  • 作用:接口文档管理、接口请求测试、Mock服务

    Mock服务:(Apifox为前端提供的模拟的URL地址,访问这个地址,就会给你返回对应的数据,以便前端测试)

  • 官网: https://apifox.com/

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

此处是打开了之前写的Springboot的入门程序的代码进行测试的

在输入URL地址后,就可以看到接受的数据了

工程搭建

  • 创建SpringBoot工程,并引入web开发起步依赖、mybatis、mysql驱动、lombok。
  • 创建数据库表dept,并在application.yml中配置数据库的基本信息。
  • 准备基础代码结构,并引入实体类Dept及统一的响应结果封装类 Result。

  • 创建项目:

  • 创建tlias数据库,并准备dept部门表。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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,'学工部','2024-09-25 09:47:40','2024-09-25 09:47:40'),
                      (2,'教研部','2024-09-25 09:47:40','2024-09-09 15:17:04'),
                      (3,'咨询部','2024-09-25 09:47:40','2024-09-30 21:26:24'),
                      (4,'就业部','2024-09-25 09:47:40','2024-09-25 09:47:40'),
                      (5,'人事部','2024-09-25 09:47:40','2024-09-25 09:47:40'),
                      (6,'行政部','2024-11-30 20:56:37','2024-09-30 20:56:37');
  • application.yml 配置文件中配置数据库的连接信息。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  application:
    name: tlias-web-management
    # 配置数据库连接信息
  datasource:
    url: jdbc:mysql://localhost:3306/tlias
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
  configuration:
    #配置Mybatis的日志输出
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  • 准备基础包结构

  • 实体类Dept
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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;
    }

}
  • 基础代码结构

  • DeptMapper
1
2
3
4
5
6
7
package com.itheima.mapper;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface DeptMapper {
}
  • DeptService
1
2
3
4
package com.itheima.service;

public interface DeptService {
}
  • DeptServiceImpl
1
2
3
4
package com.itheima.service;

public interface DeptService {
}
  • DeptServiceImpl
1
2
3
4
5
6
7
8
package com.itheima.service.impl;

import com.itheima.service.DeptService;
import org.springframework.stereotype.Service;

@Service
public class DeptServiceImpl implements DeptService {
}
  • DeptController
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package com.itheima.controller;

import org.springframework.web.bind.annotation.RestController;

/**
 * 部门管理控制器
 */
@RestController
public class DeptController {
}

查询部门

基本实现

需求

查询所有的部门数据,查询出来展示在部门管理的页面中。页面原型效果如下:

接口描述

参照课程资料中提供的接口文档。 部门管理 -> 部门列表查询

地址:接口文档 - 飞书云文档

思路分析

  • 明确了查询部门的需求之后,再来梳理一下实现该功能时,三层架构每一次的职责:

    • Controller层,负责接收前端发起的请求,并调用service查询部门数据,然后响应结果。
    • Service层,负责调用Mapper接口方法,查询所有部门数据。
    • Mapper层,执行查询所有部门数据的操作。

代码实现

实现顺序:

1). Controller层

DeptController 中,增加 list 方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 部门管理控制器
 */
@RestController
public class DeptController {

	//依赖注入,Spring 容器会自动将 DeptService 的实现类注入到这里,实现业务逻辑层的调用。
    @Autowired
    private DeptService deptService;

    /**
     * 查询部门列表
     */
    // 映射 HTTP 请求路径
    // 这里表示该方法处理访问路径为 /depts 的请求(默认支持所有 HTTP 方法,如 GET, POST 等)。  
    @RequestMapping("/depts")
    public Result list(){
    	// 调用业务逻辑层(Service)的 findall 方法,获取部门列表数据
        List<Dept> deptList = deptService.findAll();
        // 将查询到的 deptList 封装在 Result 对象中返回,通常包含状态码、消息和数据。
        return Result.success(deptList);
    }
}

快捷操作:当写完deptService.findAll();报红时,将光标放在此处,按alt + enter即可自动跳转至相应代码文件中

2). Service层

DeptService 中,增加 findAll方法,代码如下:

1
2
3
4
5
6
public interface DeptService {
    /**
     * 查询所有部门
     */
    public List<Dept> findAll();
}

DeptServiceImpl 中,增加 findAll方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Service
public class DeptServiceImpl implements DeptService {
    
    @Autowired
    private DeptMapper deptMapper;

    public List<Dept> findAll() {
        return deptMapper.findAll();
    }
}

3). Mapper层

DeptMapper 中,增加 findAll方法,代码如下:

1
2
3
4
5
6
7
8
9
@Mapper
public interface DeptMapper {
    /**
     * 查询所有部门
     */
    @Select("select * from dept")
    public List<Dept> findAll();
    
}

接口测试

启动项目,然后我们就可以打开Apifox进行测试了。

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

经过测试,我们发现,现在我们其实是可以通过任何方式的请求来访问查询部门的这个接口的。 而在接口文档中,明确要求该接口的请求方式为GET,那么如何限制请求方式呢?

  • 方式一:在controller方法的@RequestMapping注解中通过method属性来限定。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@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);
    }
}

此时在去查看Apifox就会发现请求失败了,错误信息为“请求不被允许”

  • 方式二:在controller方法上使用,@RequestMapping的衍生注解 @GetMapping。 该注解就是标识当前方法,必须以GET方式请求。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@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

小结

功能实现:

数据封装

在上述测试中,我们发现部门的数据中,id、name两个属性是有值的,但是createTime、updateTime两个字段值并未成功封装(前端页面均返回null),而数据库中是有对应的字段值的,这是为什么呢?

原因如下:

  • 实体类属性名和数据库表查询返回的字段名一致,mybatis会自动封装。
  • 如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。

解决方案:

  • 手动结果映射:通过@Results及@Result进行手动结果映射
  • 起别名:在SQL语句中,对不一样的列名起别名,别名和实体类属性名一样
  • 开启驼峰命名:如果字段名与属性名符合驼峰命名规则,Mybatis会自动通过驼峰命名规则映射
  • 手动结果映射:

  • 起别名

结果同上

  • 开启驼峰命名(要求:xxx_abc -> xxxAbc)

结果同上

前后端联调测试

联调测试

完成了查询部门的功能,我们也通过 Apifox 工具测试通过了,下面我们再基于前后端分离的方式进行接口联调。具体操作如下:

  • 下载nginx

  • 启动nginx,访问测试:http://localhost:90

双击 nginx.exe 启动Nginx,一闪而过,就说明nginx已启动完成。

如果在任务管理器中,能看到上述两个进程,就说明nginx已经启动成功。

请求访问流程

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

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

1). 浏览器发起请求,请求的是localhost:90 ,那其实请求的是nginx服务器。

2). 在nginx服务器中呢,并没有对请求直接进行处理,而是将请求转发给了后端的tomcat服务器,最终由tomcat服务器来处理该请求。

这个过程就是通过nginx的反向代理实现的。 那为什么浏览器不直接请求后端的tomcat服务器,而是直接请求nginx服务器呢,主要有以下几点原因:

1). 安全:由于后端的tomcat服务器一般都会搭建集群,会有很多的服务器,把所有的tomcat暴露给前端,让前端直接请求tomcat,对于后端服务器是比较危险的。

2). 灵活:基于nginx的反向代理实现,更加灵活,后端想增加、减少服务器,对于前端来说是无感知的,只需要在nginx中配置即可。

3). 负载均衡:基于nginx的反向代理,可以很方便的实现后端tomcat的负载均衡操作。

反向代理是一种网络架构,通过代理服务器为后端的服务器做代理,客户端的请求直接请求代理服务器,然后转发给后端的服务器。

具体的访问流程如下:

  1. **location:**用于定义匹配特定uri请求的规则。
  2. **^~ /api/:**表示精确匹配,即只匹配以/api/开头的路径。
  3. 补充:^ $:正则的开始与结束;(.*):.代表单个任意字符,*代表出现的次数为任意次数
  4. **rewrite:**该指令用于重写匹配到的uri路径。
  5. 说明:此处的意思就是将路径/api/depts重写为/depts
  6. **proxy_pass:**该指令用于代理转发,它将匹配到的请求转发给位于后端的指令服务器。

我们可以打开Nginx的配置文件看看是不是这样的

选择图中文件打开

删除部门

需求

查询所有的部门数据,查询出来展示在部门管理的页面中。页面原型效果如下:

接口文档:删除部门

思路分析

  • 明确了删除部门的需求之后,再来梳理一下实现该功能时,三层架构每一次的职责:

  • 方式一:通过原始的 HttpServletRequest 对象获取请求参数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/**
* 根据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();
}

由于Date是给前端返回的数据,但是删除不需要给前端响应数据,所以返回值置空就可以

这种方案实现较为繁琐,而且还需要进行手动类型转换。【项目开发很少用】

  • 方案二:通过Spring提供的 @RequestParam 注解,将请求参数绑定给方法形参
1
2
3
4
5
@DeleteMapping("/depts")
public Result delete(@RequestParam("id") Integer deptId){
    System.out.println("根据ID删除部门: " + deptId);
    return Result.success();
}

结果同上

@RequestParam 注解的value属性,需要与前端传递的参数名保持一致 。

@RequestParam注解required属性默认为true,代表该参数必须传递,如果不传递将报错。 如果参数可选,可以将属性设置为false。

1
2
3
4
5
@DeleteMapping("/depts")
public Result delete(@RequestParam(value = "id",required = false) Integer deptId){
    System.out.println("根据ID删除部门: " + deptId);
    return Result.success();
}

此时就算继续传递参数也不会报错

  • 方案三:如果请求参数名与形参变量名相同,直接定义方法形参即可接收。(省略@RequestParam)
  • 【推荐】
1
2
3
4
5
@DeleteMapping("/depts")java
public Result delete(Integer id){
    System.out.println("根据ID删除部门: " + id);
    return Result.success();
}

结果同上

代码实现

1). Controller层

DeptMapper 中,增加 delete 方法,代码实现如下:

1
2
3
4
5
6
7
8
9
/**
 * 根据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层

DeptService 中,增加 deleteById 方法,代码实现如下:

1
2
3
4
/**
 * 根据id删除部门
 */
void deleteById(Integer id);

3). Mapper层

DeptMapper 中,增加 deleteById 方法,代码实现如下:

1
2
3
4
5
/**
 * 根据id删除部门
 */
@Delete("delete from dept where id = #{id}")
void deleteById(Integer id);

如果mapper接口方法形参只有一个普通类型的参数,#{…} 里面的属性名可以随便写,如:#{id}#{value}

对于 DML 语句来说,执行完毕,也是有返回值的,返回值代表的是增删改操作,影响的记录数,所以可以将执行 DML 语句的方法返回值设置为 Integer。 但是一般开发时,是不需要这个返回值的,所以也可以设置为void。

代码编写完毕之后,我们就可以启动服务,进行测试了。

初始数据库:

用上面的方法在Apifox中删除id=6的数据(此处就不放页面展示了)

更新后数据库:

前后端联调测试

此时可以发现已经删除成功了

现在测试前端删除:

数据库中删除成功

测试成功

新增部门

需求

点击 “新增部门” 的按钮之后,弹出新增部门表单,填写部门名称之后,点击确定之后,保存部门数据。

接口文档:添加部门

思路分析

明确了删除部门的需求之后,再来梳理一下实现该功能时,三层架构每一次的职责:

json参数接收

我们看到,在controller中,需要接收前端传递的请求参数。 那接下来,我们就先来看看在服务器端的Controller程序中,如何获取json格式的参数。

  • JSON格式的参数,通常会使用一个实体对象进行接收 。
  • 规则:JSON数据的键名与方法形参对象的属性名相同,并需要使用@RequestBody注解标识。

前端传递的请求参数格式为json,内容如下:{"name":"研发部"}。这里,我们可以通过一个对象(dept)来接收,只需要保证对象中有name属性即可。

代码实现

1). Controller层

DeptController中增加方法save,具体代码如下:

1
2
3
4
5
6
7
8
9
/*
* 新增部门
* */
@PostMapping("/depts")
public Result add(@RequestBody Dept dept){
    System.out.println("添加部门:" + dept);
    deptService.addDept(dept);
    return Result.success();
}

2). Service层

DeptService中增加接口方法save,具体代码如下:

1
2
3
4
/**
 * 新增部门
 */
void addDept(Dept dept);

DeptServiceImpl中增加save方法,完成添加部门的操作,具体代码如下:

1
2
3
4
5
6
7
8
public void addDept(Dept dept) {
    //补全基础属性 - createTime,updateTime
    dept.setCreateTime(LocalDateTime.now());
    dept.setUpdateTime(LocalDateTime.now());
   
    //调用Mapper接口方法插入数据
    deptMapper.insert(dept);
}

3). Mapper层

1
2
3
4
5
6
/**
 * 新增部门
 */
 //注意:此处的values的后两个写的是属性名(驼峰式命名),而不是字段名(下划线)
@Insert("insert into dept(name,create_time,update_time) values(#{name},#{createTime},#{updateTime})")
void insert(Dept dept);

此处序号为9是因为前面代码有误,删除了前面的序号后重新添加的

前后端联调测试

此时刷新前端页面发现校验部添加成功

再测试前端添加

由于我们查询是倒序查询,所以新增的部门排在最上面

发现数据库中添加成功

修改部门

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

查询回显

需求

当我们点击 “编辑” 的时候,需要根据ID查询部门数据,然后用于页面回显展示。

接口描述:根据id查询部门

了解了需求之后,我们再看看接口文档中,关于根据ID查询部门的接口的描述,然后根据接口文档进行服务端接口的开发 。

/depts/1/depts/2 这种在url中传递的参数,我们称之为路径参数。 那么如何接收这样的路径参数呢 ?

路径参数:通过请求URL直接传递参数,使用{…}来标识该路径参数,需要使用 @PathVariable获取路径参数。如下所示:

如果路径参数名与controller方法形参名称一致,@PathVariable注解的value属性是可以省略的。

代码实现

1). Controller层

DeptController 中增加 getById方法,具体代码如下:

1
2
3
4
5
6
7
8
9
/**
 * 根据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层

DeptService 中增加 getById方法,具体代码如下:

1
2
3
4
/**
 * 根据id查询部门
 */
Dept getById(Integer id);

DeptServiceImpl 中增加 getById方法,具体代码如下:

1
2
3
public Dept getById(Integer id) {
    return deptMapper.getById(id);
}

3). Mapper层

DeptMapper 中增加 getById 方法,具体代码如下:

1
2
3
4
5
/**
* 根据ID查询部门数据
*/
@Select("select id, name, create_time, update_time from dept where id = #{id}")
Dept getById(Integer id);

代码编写完毕之后,我们就可以启动服务,进行测试了。

前后端分离测试

点击修改,发现部门已经回显回来了,说明测试成功

小结

修改数据

需求

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

接口文档:修改部门

思路分析

  • 明确了删除部门的需求之后,再来梳理一下实现该功能时,三层架构每一次的职责:

通过接口文档,我们可以看到前端传递的请求参数是json格式的请求参数,在Controller的方法中,我们可以通过 @RequestBody 注解来接收,并将其封装到一个对象中。

代码实现

1). Controller层

DeptController 中增加 update 方法,具体代码如下:

1
2
3
4
5
6
7
8
9
/**
 * 修改部门 - 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层

DeptService 中增加 update 方法。

1
2
3
4
/**
 * 修改部门
 */
void update(Dept dept);

DeptServiceImpl 中增加 update 方法。 由于是修改操作,每一次修改数据,都需要更新updateTime。所以,具体代码如下:

1
2
3
4
5
6
public void update(Dept dept) {
    //补全基础属性 - updateTime
    dept.setUpdateTime(LocalDateTime.now());
    //调用Mapper接口方法更新部门
    deptMapper.update(dept);
}

3). Mapper层

DeptMapper 中增加 update 方法,具体代码如下:

1
2
3
4
5
/**
 * 更新部门
 */
@Update("update dept set name = #{name},update_time = #{updateTime} where id = #{id}")
void update(Dept dept);

代码编写完毕之后,我们就可以启动服务,进行测试了。

前后端分离测试

前端修改名字

数据库更新成功

@RequestMapping

到此呢,关于基本的部门的增删改查功能,我们已经实现了。 我们会发现,我们在 DeptController 中所定义的方法,所有的请求路径,都是 /depts 开头的,只要操作的是部门数据,请求路径都是 /depts 开头。

那么这个时候,我们其实是可以把这个公共的路径 /depts 抽取到类上的,那在各个方法上,就可以省略了这个 /depts 路径。 代码如下:

一个完整的请求路径,应该是类上的 @RequestMapping 的value属性 + 方法上的 @RequestMapping的value属性。

日志技术

思考

  • 程序中的日志,是用来记录应用程序的运行信息、状态信息、错误信息等

  • 为什么要在程序中记录日志呢?

    • 便于追踪应用程序中的数据信息、程序的执行过程。
    • 便于对应用程序的性能进行优化
    • 便于应用程序出现问题之后,排查问题,解决问题。
    • 便于监控系统的运行状态。
  • 之前我们编写程序时,也可以通过 System.out.println(...) 来输出日志,为什么我们还要学习单独的日志技术呢?

    ​ 这是因为,如果通过 System.out.println(...) 来记录日志,会存在以下几点问题:

    • 硬编码。所有的记录日志的代码,都是硬编码,没有办法做到灵活控制,要想不输出这个日志了,只能删 除掉记录日志的代码。
    • 只能输出日志到控制台。
    • 不便于程序的扩展、维护。

所以,在现在的项目开发中,我们一般都会使用专业的日志框架,来解决这些问题。

日志框架

  • JUL:这是JavaSE平台提供的官方日志框架,也被称为JUL。配置相对简单,但不够灵活,性能较差。
  • Log4j:一个流行的日志框架,提供了灵活的配置选项,支持多种输出目标。
  • Logback:基于Log4j升级而来,提供了更多的功能和配置选项,性能由于Log4j。(推荐)
  • Slf4j:(Simple Logging Facade for Java)简单日志门面,提供了一套日志操作的标准接口及抽象类,允许应用程序使用不同的底层日志框架。

LogBack入门

  • 准备工作:引入logback的依赖**(springboot项目中该依赖已传递)**、配置文件logback.xml。
  • 记录日志:定义日志记录对象Logger,记录日志。

1).springboot中无需引入,在springboot中已经传递了此依赖

1
2
3
4
5
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.11</version>
</dependency>

2). 引入配置文件 logback.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?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,记录日志

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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("结束计算...");
    }

}

运行单元测试,可以在控制台中看到输出的日志,如下所示:

Logback配置文件详解

  • 配置文件名:logback.xml
  • 该配置文件是对Logback日志框架输出的日志进行控制的,可以来配置输出的格式、位置及日志开关等。
  • 常用的两种输出日志的位置:控制台、系统文件

  • 开启日志(ALL),关闭日志(OFF)

1
2
3
4
<!-- 日志输出级别 -->
<root level="off">
    <appender-ref ref="STDOUT" />
</root>

完整版日志文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?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个字符宽度,%logger显示日志记录器的名称, %msg表示日志消息,%n表示换行符 -->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n</pattern>
        </encoder>
    </appender>

    <!-- 系统文件输出 -->
    <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>

    <!-- 日志输出级别 -->
    <!-- 区分大小写-->
    <root level="ALL">
        <!--输出到控制台-->
        <appender-ref ref="STDOUT"/>
        <!--输出到文件-->
        <appender-ref ref="FILE"/>
    </root>
</configuration>

Logback日志级别

  • 日志级别指的是日志信息的类型,日志都会分级别,常见的日志级别如下:(级别由高到低):

  • 可以在配置文件中,灵活的控制输出那些类型的日志**(大于等于配置的日志级别的日志才会输出)**
  • 例如:在上面的root中输入:info,那么warn,error级别的日志都会输出,trace和debug则不会输出

现在就可以将上面Controller层的代码替换为以下的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
 * 部门管理控制器
 */
@Slf4j
@RequestMapping("/depts")
@RestController
public class DeptController {

    @Autowired
    private DeptService deptService;

    /**
     * 查询部门列表
     */
    //@RequestMapping(value = "/depts", method = RequestMethod.GET)
    @GetMapping
    public Result list(){
        //System.out.println("查询部门列表");
        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){
        //System.out.println("根据id删除部门, id=" + 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){
        //System.out.println("新增部门, 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){
        //System.out.println("根据ID查询, id=" + 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){
        //System.out.println("修改部门, dept=" + dept);
        log.info("修改部门, dept: {}" , dept);
        deptService.update(dept);
        return Result.success();
    }
}

注意:如果上面写了注解@Slf4j,那么下面就不用在类中再写:

private static final Logger log = LoggerFactory.getLogger(DeptController.class);

日志代码的两种写法:(以删除部门为例)

1.log.info(“根据id删除部门, id: {}” , id);(推荐)

2.log.info(“根据id删除部门, id: " + id);

本站于2026年3月31日建立
使用 Hugo 构建
主题 StackJimmy 设计