JavaWeb-SpringBoot(Tilas案例)

JavaWeb

对自己学习JavaWeb的过程进行记录.

1.Maven

Maven介绍

Maven是一款用于管理和构建Java项目的工具,是apache旗下的一个开源项目

Maven主要作用如下:

  • 依赖管理: 方便快捷的管理项目依赖的资源(jar包)
  • 项目构建: 标准化的跨平台(Linux,Windows,MacOS)的自动化项目构建方式
  • 统一项目构建: 提供标准,统一的项目结构(目录结构)
  1. 依赖管理

    只需要创建好Maven项目后,在pom.xml配置依赖即可

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
    

    上述就是在pom.xml文件中加入lombok依赖.

    常见形式为

    <dependency>
        <groupId>...</groupId>
        <artifactId>...</artifactId>
        <version>...</version>
        <scope>...</scope>
        <type>...</type>
        <classifier>...</classifier>
        <optional>...</optional>
        <exclusions>
            <exclusion>
                <groupId>...</groupId>
                <artifactId>...</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
    标签名 说明
    <groupId> 依赖的组织名称(类似包名),用于唯一标识项目,如 org.springframework
    <artifactId> 构件名称(项目名),用于唯一标识项目下的模块,如 spring-core
    <version> 指定依赖的版本号,如 5.3.10
    <scope> 依赖作用范围,常见值如下:- compile(默认)- test(仅在测试时有效)- provided(由容器提供)- runtime(运行时需要)- system(本地系统路径提供)
    <type> 构件类型,默认是 jar,也可以是 warpom
    <classifier> 分类器,用于获取构件的某个特定构建,如 sourcesjavadoc
    <optional> 是否是可选依赖(truefalse),可影响传递性依赖
    <exclusions> 排除传递性依赖的子标签,用于避免冲突或冗余依赖

    此时你可能有一个疑惑?我不知道lombok需要添加上述信息,在哪里寻找?网站如下:

    https://mvnrepository.com/

    只需要搜索需要的依赖,找到需要的版本,最下方就会有需要的配置文件

  2. 项目构建

    一个java项目的运行需要包含下面四个流程:

    • 编译 -> 测试 -> 打包 -> 发布

    如果需要开发的是一个大型的项目,由于开发使用的模块较多,如果还使用上述的步骤,开发的时候就会比较困难.

    Maven制作了一套标准化的项目构建流程,直接使用Maven项目提供的指令,即可完成快速的项目构建

  3. 统一项目结构

    Java的开发工具有很多,例如早期的Eclipse,MyEclipse,IDEA,但是不同的开发工具构建出的Java项目目录结构不一样,就导致不同开发工具创建的项目无法在不同的开发工具中使用

    使用Maven构建的项目目录结构都是统一的,解决了这个问题

    Maven目录结构如下:

    my-app/                         # 项目根目录
    ├── pom.xml                     # Maven 项目描述文件
    └── src/
        ├── main/                   # 主要源码目录
        │   ├── java/               # Java 源代码
        │   ├── resources/          # 配置文件等资源
        │   └── webapp/             # Web 应用资源(如 JSP、HTML、CSS 等,适用于 web 项目)
        └── test/                   # 测试源码目录
            ├── java/               # 测试用 Java 源代码
            └── resources/          # 测试资源文件
    

单元测试

测试:是一种用来促进鉴定软件的正确性、完整性、安全性和质量的过程。

测试一般有四个阶段:单元测试、集成测试、系统测试、验收测试。

  • 单元测试:就是针对最小的功能单元(方法),编写测试代码对其正确性进行测试。
  • Junit:最流行的Java测试框架之一,提供了一些功能,方便程序进行单元测试。

首先在pom.xml加入JUnit依赖

maven repository 搜索 Junit

注意:JUnit单元测试类名命名规范为:XxxTest[规范]JUnit单元测试方法,必须声明为public void【规定】

例如:

@Test
public void testGetAge(){
    Integer age = new UserService().getAge("412819200311226789");
    System.out.println(age);
}

断言

  • Junit提供了一些辅助方法,用来帮我们确定被测试的方法是否按照预期的效果正常工作,这种方式成为断言

主要是用Junit提供了一个测试的工具类Assertions,提供了很多的静态的测试方法

方法 说明
assertEquals(expected, actual) 判断两个值是否相等(使用 equals() 方法)
assertNotEquals(expected, actual) 判断两个值是否不相等
assertSame(expected, actual) 判断两个对象引用是否相同
assertNotSame(expected, actual) 判断两个对象引用是否不同
assertNull(actual) 判断对象是否为 null
assertNotNull(actual) 判断对象是否不为 null
assertTrue(condition) 判断条件是否为 true
assertFalse(condition) 判断条件是否为 false
assertArrayEquals(expectedArray, actualArray) 判断两个数组是否相等(按元素顺序)

Junit案例

首先创建一个需要测试的方法

UserService.java

package com.itheima;

public class UserService {
    public static void main(String[] args) {
        System.out.println("Hello, World!"
        );
    }

    public int add(int a,int b){
        return a + b;
    }
}

手动测试

UserServiceTest.java

package com.itheima;


import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

public class UserServiceTest {
    UserService userService = new UserService();

    @DisplayName("add测试")
    @Test
    public void addTest(){
        Assertions.assertEquals(3,userService.add(1,2));
    }

    @ParameterizedTest
    @ValueSource(ints = {1,2,3})
    public void addTest2(int num){
        Assertions.assertEquals(3,userService.add(1,num));
    }

}

使用AI生成代码测试

UserServiceAiTest.java

package com.itheima;


import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

public class UserServiceAiTest {
    UserService userService = new UserService();

    @DisplayName("add测试")
    @Test
    public void addTest() {
        Assertions.assertEquals(3, userService.add(1, 2));
    }

    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3})
    public void addTest2(int num) {
        Assertions.assertEquals(3, userService.add(1, num));
    }

    @Test
    public void add_NegativeNumbers_ShouldReturnCorrectSum() {
        Assertions.assertEquals(-3, userService.add(-1, -2));
    }

    @Test
    public void add_ZeroAndPositiveNumber_ShouldReturnPositiveNumber() {
        Assertions.assertEquals(5, userService.add(0, 5));
    }

    @Test
    public void add_ZeroAndNegativeNumber_ShouldReturnNegativeNumber() {
        Assertions.assertEquals(-5, userService.add(0, -5));
    }

    @Test
    public void add_LargeNumbers_ShouldReturnCorrectSum() {
        int a = Integer.MAX_VALUE;
        int b = 0; // 为了防止溢出,我们选择0
        Assertions.assertEquals(a, userService.add(a, b));
    }
}

2.SpringBoot Web

入门

使用Idea或者https://start.spring.io/快速搭建一个初始的spring boot项目

/java/com.itheima也就是SpringTalisApplication主程序所在的目录创建一个controller

HelloController

package com.itheima.testspringboot;

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

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello(String name){
        return "Hello " + name + " ~";
    }
}

官方骨架问题

使用国内阿里云提供的脚手架,https://start.aliyun.com

请求协议(request)

requestController.java

package com.itheima.testspringboot;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class requestController {
    @RequestMapping("/request")
    public String request(HttpServletRequest request){
//        获取请求方式
        String method = request.getMethod();
        System.out.println("请求方式: " + method);
//        获取请求完整路径
        StringBuffer url = request.getRequestURL();
        System.out.println("请求完整路径: " + url);
//        获取请求路径
        String uri = request.getRequestURI();
        System.out.println("请求文件路径: " + uri);
//        获取请求协议
        String protocol = request.getProtocol();
        System.out.println("请求协议: " + protocol);
//        获取请求参数
        String queryString = request.getQueryString();
        System.out.println("请求参数: " + queryString);
//        获取请求头
        String header = request.getHeader("User-Agent");
        System.out.println("请求头: " + header);

        return "";
    }
}

响应协议(response)

package com.itheima.testspringboot;

import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
public class responseController {

//   使用servlet提供 response 返回结果
    @RequestMapping("/response")
    public void response(HttpServletResponse response){
        response.setStatus(401);
        response.setHeader("name","Junglezt");

        try {
            response.getWriter().write("<h1>hello response</h1>");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


//    基于 spring 提供的 ResponseEntity 返回结果
    @RequestMapping("/response2")
    public ResponseEntity<String> response2(){
        return ResponseEntity
                .status(401)
                .header("id","226")
                .body("<h2>hello ResponseEntity</h2>");
    }
}

SpringBoot Web案例

基于三层架构(MVC)开发Web程序案例,展示用户列表,读取的数据为user.txt文件

user.txt

1,daqiao,1234567890,大乔,22,2024-07-15 15:05:45
2,xiaoqiao,1234567890,小乔,18,2024-07-15 15:12:09
3,diaochan,1234567890,貂蝉,21,2024-07-15 15:07:16
4,lvbu,1234567890,吕布,28,2024-07-16 10:05:15
5,zhaoyun,1234567890,赵云,27,2024-07-16 11:03:28
6,zhangfei,1234567890,张飞,31,2024-07-16 11:03:29
7,guanyu,123567890,关羽,34,2024-07-16 12:05:12
8,liubei,1234567890,刘备,37,2024-07-16 15:03:28

开发流程

  1. 准备工作

    1. 创建SpringBoot项目,勾选Web依赖、lombok依赖
    2. resources中引入user.txt文件
    3. 定义一个实体类,用来封转用户信息

    pojo/User.java

    package com.itheima.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.time.LocalDateTime;
    
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class User {
        private Integer id;
        private String username;
        private String password;
        private String name;
        private Integer age;
        private LocalDateTime updatetime;
    }
    
  2. 开发服务端程序,接收请求,读取文本数据并响应

实操代码

  1. 首先准备前端页面

static/user.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>用户列表</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }
        h1 {
            color: #333;
            text-align: center;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 20px;
        }
        th, td {
            border: 1px solid #ddd;
            padding: 12px;
            text-align: left;
        }
        th {
            background-color: #f5f5f5;
            font-weight: bold;
        }
        tr:nth-child(even) {
            background-color: #f9f9f9;
        }
        tr:hover {
            background-color: #f1f1f1;
        }
    </style>
</head>

<body>
<div id="app">
    <h1>用户列表</h1>

    <table>
        <thead>
        <tr>
            <th>序号</th>
            <th>用户名</th>
            <th>密码</th>
            <th>姓名</th>
            <th>年龄</th>
            <th>创建时间</th>
        </tr>
        </thead>
        <tbody>
        <tr v-for="(user, index) in users" :key="user.id">
            <td>{{ user.id }}</td>
            <td>{{ user.username }}</td>
            <td>{{ user.password }}</td> <!-- 密码打码显示 -->
            <td>{{ user.name }}</td>
            <td>{{ user.age }}</td>
            <td>{{ user.updatetime }}</td>
        </tr>
        </tbody>
    </table>
</div>

    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script type="module">
        import { createApp, ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';

        createApp({
            data() {
                return {
                    users: []
                }
            },
            mounted() {
                this.search();
            },
            methods: {
                async search() {
                    let result = await axios.get('http://192.168.202.1:8080/list');
                    // console.log(result.data)5
                    this.users = result.data;

                },
            }
        }).mount('#app');
    </script>
</body>
</html>
  1. 定义Controller层代码

controller/UserController.java

package com.itheima.controller;

import cn.hutool.core.io.IoUtil;
import com.itheima.pojo.User;
import com.itheima.service.UserService;
import com.itheima.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.sql.Array;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import static java.lang.System.in;

@RestController
public class UserController {


//    不使用三层架构实现
//    @RequestMapping("/list")
//    public List<User> list() throws FileNotFoundException {
////        1.加载并读取user.txt文件
//        InputStream in = new FileInputStream(new File("E:\\source\\java\\springboot-web-01\\src\\main\\resources\\user.txt"));
//
////        System.out.println(this.getClass().getClassLoader().getResourceAsStream("user.txt").toString());
////        InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");
//        ArrayList<String> lines = IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList<>());
//
////        2.解析用户数据封装为 User 对象 -> List
//        List<User> userList = lines.stream().map(line -> {
//            String[] parts = line.split(",");
//            Integer id = Integer.parseInt(parts[0]);
//            String username = parts[1];
//            String password = parts[2];
//            String name = parts[3];
//            Integer age = Integer.parseInt(parts[4]);
//            LocalDateTime updatetime = LocalDateTime.parse(parts[5], DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
//            return new User(id, username, password, name, age, updatetime);
//        }).toList();
//
////        3.返回json数据
////        使用 @RestController 注解,默认会将对象自动转换为 json 格式返回
//        return userList;
//    }


//    使用三层架构实现

////    方法1 属性注入
//    @Autowired
//    private UserService userservice;

    
////    方法2  构造器注入
//    private final UserService userservice;
////    @Autowired        // 如果当前类中只存在一个构造函数,Autowired注解可以省略
//    public UserController(UserService userservice) {
//        this.userservice = userservice;
//    }

//    方法3 setter 注入
    private UserService userservice;
    @Autowired
    public void setUserservice(UserService userservice) {
        this.userservice = userservice;
    }


    @RequestMapping("/list")
    @CrossOrigin(origins = "*")
    public List<User> list2() throws FileNotFoundException {

        List<User> userlist = userservice.findall();
        return userlist;
    }
}
  1. 定义UserService接口

service/UserService.java

package com.itheima.service;

import com.itheima.pojo.User;

import java.io.FileNotFoundException;
import java.util.List;

public interface UserService {

    public List<User> findall() throws FileNotFoundException;
}
  1. 实现UserService接口,并调用Dao层读取user.txt数据,处理后返回给Controller层

service/impl/UserServiceImpl.java

package com.itheima.service.impl;

import com.itheima.dao.UserDao;
import com.itheima.dao.impl.UserDaoImpl;
import com.itheima.pojo.User;
import com.itheima.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.io.FileNotFoundException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

@Service
//@Component  // 将当前类交给 ioc 容器管理
public class UserServiceImpl implements UserService {

//    private UserDao userdao = new UserDaoImpl();
//    使用 spring ioc 注入
    @Autowired      // 应用程序运行时,会自动查找UserDao类型的bean对象,并复制给userdao成员变量
    private UserDao userdao;
    @Override
    public List<User> findall() throws FileNotFoundException {
        ArrayList<String> lines = userdao.list();
        List<User> userList = lines.stream().map(line -> {
            String[] parts = line.split(",");
            Integer id = Integer.parseInt(parts[0]);
            String username = parts[1];
            String password = parts[2];
            String name = parts[3];
            Integer age = Integer.parseInt(parts[4]);
            LocalDateTime updatetime = LocalDateTime.parse(parts[5], DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            return new User(id, username, password, name, age, updatetime);
        }).toList();

        return userList;
    }
}
  1. 定义UserDao接口

dao/UserDao.java

package com.itheima.dao;

import com.itheima.pojo.User;

import java.io.FileNotFoundException;
import java.util.ArrayList;

public interface UserDao {

    ArrayList<String> list() throws FileNotFoundException;
}
  1. 实现UserDao接口,读取user.txt文件并返回

dao/impl/UserDaoImpl.java

package com.itheima.dao.impl;

import cn.hutool.core.io.IoUtil;
import com.itheima.dao.UserDao;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;

@Repository     // Repository  将当前类交给 ioc 容器管理
//@Component  // 将当前容器交给 ioc 容器处理
public class UserDaoImpl implements UserDao {

    @Override
    public ArrayList<String> list() throws FileNotFoundException {
        InputStream in = new FileInputStream(new File("E:\\source\\java\\springboot-web-01\\src\\main\\resources\\user.txt"));
        ArrayList<String> lines = IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList<>());
        return lines;
    }
}

3.JDBC

JDBC: (Java DataBase Connectivity),就是使用Java语言操作关系型数据库的一套API

要连接不同的数据库,需要下载不同数据库厂商提供的JDBC实现,例如使用mysql是需要在Maven repository找到对应版本的pom.xml即可

JDBC连接主要有四个步骤

  1. 导入jdbc驱动
  2. 连接数据库
  3. 创建statement对象
  4. 执行sql语句

下方就是Mysql JDBC的基础案例

查询

package jdbc.jdbcTest;

import java.sql.*;

public class Example01 {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/users?serverTimezone=GMT%2B8";
        String username = "root";
        String password = "root";
        try {
//          导入mysql jdbc驱动
            Class.forName("com.mysql.jdbc.Driver");
//          使用 DriverManager.getConnection 连接数据库
            Connection conn = DriverManager.getConnection(url, username, password);
//          使用 createStatement 创建 Statement 对象
            Statement stmt = conn.createStatement();
//          使用 statement 对象执行 sql 语句
            String sql = "select * from users";
            ResultSet result = stmt.executeQuery(sql);

            while(result.next()) {
                int id = result.getInt("id");
                String name = result.getString("name");
                String email = result.getString("email");
                Date birthday = result.getDate("birthday");
//                System.out.println("id: " + id + " name: " + name + " email: " + email + " phone: " + birthday);
                System.out.println(id + " " + name + " " + email + " " + birthday);
            }
        } catch (ClassNotFoundException e) {
            System.out.println("找不到驱动程序");
        } catch (SQLException e) {
            System.out.println("数据库连接失败");
        }
    }
}

增加

package jdbc.jdbcTest;

import java.sql.*;

public class Example02 {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/users?serverTimezone=GMT%2B8";
        String username = "root";
        String password = "root";
        try {
//          导入mysql jdbc驱动
            Class.forName("com.mysql.jdbc.Driver");
//          使用 DriverManager.getConnection 连接数据库
            Connection conn = DriverManager.getConnection(url, username, password);
//          使用 createStatement 创建 Statement 对象
            Statement stmt = conn.createStatement();
//          使用 statement 对象执行 sql 语句
            String sql = "insert into users(id,name,password,email,birthday) values(5,'王六','8888','333@qq.com','2004-02-22')";

            int result = stmt.executeUpdate(sql);
            if (result > 0) {
                System.out.println(result + "成功插入数据");
            }
        } catch (ClassNotFoundException e) {
            System.out.println("找不到驱动程序");
        } catch (SQLException e) {
            System.out.println("数据库连接失败");
        }
    }
}

删除

package jdbc.jdbcTest;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class Example03 {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/users?serverTimezone=GMT%2B8";
        String username = "root";
        String password = "root";
        try {
//          导入mysql jdbc驱动
            Class.forName("com.mysql.jdbc.Driver");
//          使用 DriverManager.getConnection 连接数据库
            Connection conn = DriverManager.getConnection(url, username, password);
//          使用 createStatement 创建 Statement 对象
            Statement stmt = conn.createStatement();
//          使用 statement 对象执行 sql 语句
            String sql = "delete from users where name='王六'";

            int result = stmt.executeUpdate(sql);
            if (result > 0) {
                System.out.println(result + "删除成功");
            }
        } catch (ClassNotFoundException e) {
            System.out.println("找不到驱动程序");
        } catch (SQLException e) {
            System.out.println("数据库连接失败");
        }
    }
}

增加(使用预编译PreparedStatement)

package jdbc.jdbcTest;

import java.sql.*;

public class Example04 {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/users?serverTimezone=GMT%2B8";
        String username = "root";
        String password = "root";
        try {
//          导入mysql jdbc驱动
            Class.forName("com.mysql.jdbc.Driver");
//          使用 DriverManager.getConnection 连接数据库
            Connection conn = DriverManager.getConnection(url, username, password);
//          使用 createStatement 创建 Statement 对象
            //Statement stmt = conn.createStatement();
            String sql = "insert into users(id,name,password,email,birthday) values(?,?,?,?,?)";
            PreparedStatement pstmt = conn.prepareStatement(sql);

            pstmt.setInt(1, 4);
            pstmt.setString(2, "John");
            pstmt.setString(3, "123456");
            pstmt.setString(4, "john@gmail.com");
            pstmt.setString(5, "2024-12-16");

            int result = pstmt.executeUpdate();
            if (result > 0) {
                System.out.println("Insert successful");
            }

        } catch (ClassNotFoundException e) {
            System.out.println("找不到驱动程序");
        } catch (SQLException e) {
            System.out.println("数据库连接失败");
        }
    }
}

JDBCUtils

package jdbc.jsj.utils;

import java.sql.*;
/**
 * 连接数据库、释放资源
 */
public class JDBCUtils {
//        连接数据库
    public static Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        String url = "jdbc:mysql://localhost:3306/users";
        String user = "root";
        String password = "root";
        Connection conn = DriverManager.getConnection(url, user, password);
        return conn;
    }

    //  释放sql查询时需要关闭三个对象
    public static void release(Connection conn, PreparedStatement pstmt, ResultSet rs) {
//        关闭ResultSet对象
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
            rs = null;
        }
//        关闭执行sql语句的preparedStatement对象
        if (pstmt != null) {
            try {
                pstmt.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
            pstmt = null;
        }
//        关闭数据库连接对象
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }



//    执行增删改操作不需要使用PreparedStatement对象
    public static void release(Connection conn, PreparedStatement pstmt) {

//        关闭执行sql语句的preparedStatement对象
        if (pstmt != null) {
            try {
                pstmt.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
            pstmt = null;
        }
//        关闭数据库连接对象
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

UserDao(实例)

package jdbc.jsj.DAO;

import jdbc.jsj.domain.User;
import jdbc.jsj.utils.JDBCUtils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
//实现增删改查

/**
 * 基本功能:
 * 1. 增加用户
 * 2. 删除用户(根据id)
 * 3. 修改用户(根据id)
 * 4. 查询用户(根据id)
 * 5. 查询所有用户
 */
public class UserDao {
//    1. 增加用户
    public boolean insertUser(User user){
        Connection conn = null;
        PreparedStatement pstmt = null;
//        1.连接数据库
        try {
            conn = JDBCUtils.getConnection();
//            2.定义sql语句,获取sql语句执行对象PreparedStatement
            String sql = "insert into users(name,password,email,birthday) values(?,?,?,?)";

            pstmt = conn.prepareStatement(sql);
//            3.指定增加用户信息
            pstmt.setString(1,user.getName());
            pstmt.setString(2,user.getPassword());
            pstmt.setString(3,user.getEmail());
            pstmt.setString(4,user.getBirthday());
//            4.执行sql语句
            int resp = pstmt.executeUpdate();

            return resp > 0;

        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally{
            JDBCUtils.release(conn,pstmt);
        }
    }


//    2.删除用户
    public boolean deleteUser(int id){
        Connection conn = null;
        PreparedStatement pstmt = null;
        try {
//            1. 连接数据库
            conn = JDBCUtils.getConnection();
//            2. 定义sql语句,获取prepareStatement执行sql一句对象
            String sql = "delete from users where id=?";
            pstmt = conn.prepareStatement(sql);
//            3. 设置删除参数id
            pstmt.setInt(1,id);
//            4. 执行sql语句
            int resp = pstmt.executeUpdate();
//            根据返回结果返回true/false
            return resp > 0;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally{
            JDBCUtils.release(conn,pstmt);
        }
    }


//    3.更改信息
    public boolean updateUser(User user,int id){
        Connection conn = null;
        PreparedStatement pstmt = null;
//        1.连接数据库
        try {
            conn = JDBCUtils.getConnection();
//            2. 定义sql语句,获取执行sql语句对象preparedStatement
            String sql = "update users set id=?,name=?,password=?,email=?,birthday=? where id=?";
            pstmt = conn.prepareStatement(sql);
//            3. 设置更改数据
            pstmt.setInt(1,user.getId());
            pstmt.setString(2,user.getName());
            pstmt.setString(3,user.getPassword());
            pstmt.setString(4,user.getEmail());
            pstmt.setString(5,user.getBirthday());
//            根据id更改用户信息
            pstmt.setInt(6,id);
//            4.执行sql语句
            int resp = pstmt.executeUpdate();
            return resp > 0;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally{
            JDBCUtils.release(conn,pstmt);
        }
    }



//    4. 查询指定用户
    public User getUserById(int id){
        Connection conn = null;
        PreparedStatement pstmt = null;
//        1.连接数据库
        try {
            conn = JDBCUtils.getConnection();
//            2. 定义sql语句,获取执行sql语句对象preparedStatement对象
            String sql = "select * from users where id=?";
            pstmt = conn.prepareStatement(sql);
//            3. 设置查询数据
            pstmt.setInt(1,id);
//            4. 执行sql语句获取返回结果ResultSet对象
            ResultSet rs = pstmt.executeQuery();
//            5. 处理并返回ResultSet对象
            while(rs.next()){
                User user = new User();
                user.setId(rs.getInt("id"));
                user.setName(rs.getString("name"));
                user.setPassword(rs.getString("password"));
                user.setEmail(rs.getString("email"));
                user.setBirthday(rs.getString("birthday"));
                return user;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally{
            JDBCUtils.release(conn,pstmt);
        }
        return null;
    }



//    5. 查询所有用户
    public ArrayList<User> getAllUser(){
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = JDBCUtils.getConnection();
            String sql = "select * from users";
            pstmt = conn.prepareStatement(sql);
            rs = pstmt.executeQuery();
            ArrayList<User> userList = new ArrayList<>();
            while(rs.next()){
                User user = new User();
                user.setId(rs.getInt("id"));
                user.setName(rs.getString("name"));
                user.setPassword(rs.getString("password"));
                user.setEmail(rs.getString("email"));
                user.setBirthday(rs.getString("birthday"));
                userList.add(user);
            }
            return userList;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            JDBCUtils.release(conn,pstmt,rs);
        }
    }


//        6.根据用户名和密码查询用户
    public User getUserByNamePwd(String username,String pwd){
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = JDBCUtils.getConnection();
            String sql = "select * from users where name=? and password=?";
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1,username);
            pstmt.setString(2,pwd);
            rs = pstmt.executeQuery();
            while(rs.next()){
                User user = new User();
                user.setId(rs.getInt("id"));
                user.setName(rs.getString("name"));
                user.setPassword(rs.getString("password"));
                user.setEmail(rs.getString("email"));
                user.setBirthday(rs.getString("birthday"));
                return user;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally{
            JDBCUtils.release(conn,pstmt,rs);
        }
        return null;
    }
}

4. Mybatis

MyBatis是一款优秀的持久层框架,终于简化JDBC的开发。

使用Mybatis查询所有用户信息

  • 准备工作:
    1. 创建Springboot工程,引入Mybatis相关依赖
    2. 准备数据库表user、实体类User
    3. 配置Mybatis(在application.properties中数据库连接信息)
  • 编写Mybatis程序:编写Mybatis的持久层接口,定义SQL(注解/XML)

1.mybatis数据库下创建t_student表

/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 50726 (5.7.26)
 Source Host           : localhost:3306
 Source Schema         : mybatis

 Target Server Type    : MySQL
 Target Server Version : 50726 (5.7.26)
 File Encoding         : 65001

 Date: 26/05/2025 15:24:47
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_student
-- ----------------------------
DROP TABLE IF EXISTS `t_student`;
CREATE TABLE `t_student`  (
  `sid` int(11) NOT NULL AUTO_INCREMENT,
  `sname` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
  `sage` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`sid`) USING BTREE
) ENGINE = MyISAM AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_student
-- ----------------------------
INSERT INTO `t_student` VALUES (1, '彭于晏', 23);
INSERT INTO `t_student` VALUES (2, 'Lili', 20);
INSERT INTO `t_student` VALUES (3, 'Jim', 21);
INSERT INTO `t_student` VALUES (4, '小红', 18);

2.创建pojo/User实体类

package com.itheima.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private int sid;
    private String sname;
    private int sage;
}

3.配置Mybatis(application)

#配置数据库连接
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

#配置Mybatis日志输出
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

#配置数据库连接池为Druid(默认使用Hikari)
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

注意:上述如果要配置连接池为Druid德鲁伊,需要在pom.xml文件中导入

4. 创建Mapper接口

Mapper/UserMapper.java

package com.itheima.Mapper;

import com.itheima.pojo.User;
import org.apache.ibatis.annotations.*;

import java.util.List;

@Mapper     // 应用程序在运行时,会自动的为该接口创建一个实现类对象(代理对象),自动的将该实现对象注入IOC中 - bean
public interface UserMapper {

    /**
     * 查询所有用户信息
     */
//    @Select("select * from t_student")
    public List<User> findall();

    @Delete("delete from t_student where sid = #{id}")
    public Integer delUser(Integer id);

    @Insert("insert into t_student (sid, sname, sage) values (#{sid},#{sname},#{sage})")
    public void addUser(User user);

    @Update("update t_student set sname = #{sname},sage = #{sage} where sid = #{sid}")
    public void updateUser(User user);

    /**
     * 根据用户名和年龄查询用户信息
     */
    @Select("select * from t_student where sname = #{sname} and sage = #{sage}")
//    public User findByNameAndAge(@Param("sname") String sname, @Param("sage") int sage);
    // 基于官方声称的springboot项目,可以不添加 Param 注解
    public User findByNameAndAge(String sname, int sage);

}

5.在SpringBoot中测试Mapper接口

SpringBootTest

package com.itheima;

import com.itheima.Mapper.UserMapper;
import com.itheima.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest        // springboot单元测试的注解
class SpringbootMybatisApplicationTests {

//    注入UserMapper
    @Autowired
    private UserMapper userMapper;


    @Test
    public void testFindAll() {
        List<User> userList = userMapper.findall();
        userList.forEach(System.out::println);
    }

    @Test
    public void testDelUser() {
        Integer result = userMapper.delUser(4);
        System.out.println("受影响的行数:" + result);
    }


    @Test
    public void addUser(){
        User user = new User(4,"小红",18);
        userMapper.addUser(user);
    }

    @Test
    public void updateUser(){
        User user = new User(1,"彭于晏",23);
        userMapper.updateUser(user);
    }


    @Test
    public void findByNameAndAge(){
        User user = userMapper.findByNameAndAge("小红", 18);
        System.out.println(user);

    }
}

XML映射配置

  • Mybatis中,既可以通过注解配置SQL语句,也可以通过XML配置文件配置SQL语句。
  • 默认规则:
    1. XML映射文件的名称与Mapper接口名称一致,并且将XMl映射文件和Mapper接口放置在相同包下(同包同名)
    2. XML映射文件的namespace属性为Mapper接口全限定名一致。
    3. XML映射文件中sql语句的id与Mapper接口中的方法名一致,并保持返回类型一致。

java/com.itheima.Mapper/UserMapper.java

package com.itheima.Mapper;

import com.itheima.pojo.User;
import org.apache.ibatis.annotations.*;
import java.util.List;

@Mapper
public interface UserMapper {

    /**
     * 查询所有用户信息
     */
//    @Select("select * from t_student")
    public List<User> findall();
}

resources/com.itheima.Mapper/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 namespace="com.itheima.Mapper.UserMapper">
    <select id="findall" resultType="com.itheima.pojo.User">
        select * from t_student
    </select>
</mapper>

配置XML映射文件位置

配置application.properties

#指定XMl映射文件的位置
mybatis.mapper-locations=classpath:mapper/*.xml

5.SpringBoot配置文件

在上述中,默认使用的是SpringBoot默认的配置文件application.properties配置文件,但是在大型项目配置文件过多,代码很多表现很臃肿,导致层级结构不清晰

所以推荐使用yml、yaml进行文件配置

例如下方是application.propertiesapplication.yml的区别

application.properties

#配置数据库连接
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mybatis
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root

很明显可以看出yaml格式文件结构比较清晰

6.RestFul风格

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

🧾传统风格 API 示例

操作 方法 URL 描述
获取用户列表 GET /getUsers 获取所有用户信息
获取用户信息 GET /getUser?id=123 获取指定 ID 的用户信息
创建用户 POST /createUser 新建一个用户
更新用户 POST /updateUser 更新用户信息
删除用户 POST /deleteUser 删除用户

🌐 RESTful 风格 API 示例

操作 方法 URL 描述
获取用户列表 GET /users 获取所有用户信息
获取用户信息 GET /users/123 获取指定 ID 的用户信息
创建用户 POST /users 新建一个用户
更新用户 PUT /users/123 更新指定 ID 的用户信息
删除用户 DELETE /users/123 删除指定 ID 的用户

7. Tilas案例

在处理前端的同方式的请求参数,SpringBoot提供了不同的方式获取前端传参

环境准备

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

  2. 创建数据库表dept,在application.yml中配置数据库的基本信息

    dept数据库.sql

    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    
    -- ----------------------------
    -- Table structure for dept
    -- ----------------------------
    DROP TABLE IF EXISTS `dept`;
    CREATE TABLE `dept`  (
      `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID, 主键',
      `name` varchar(10) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '部门名称',
      `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
      `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
      PRIMARY KEY (`id`) USING BTREE,
      UNIQUE INDEX `name`(`name`) USING BTREE
    ) ENGINE = MyISAM AUTO_INCREMENT = 17 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci COMMENT = '部门表' ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of dept
    -- ----------------------------
    INSERT INTO `dept` VALUES (1, '学工部', '2023-09-25 09:47:40', '2023-09-25 09:47:40');
    INSERT INTO `dept` VALUES (2, '教研部', '2023-09-25 09:47:40', '2023-10-09 15:17:04');
    INSERT INTO `dept` VALUES (3, '咨询888', '2023-09-25 09:47:40', '2025-04-16 11:59:39');
    INSERT INTO `dept` VALUES (4, '就业部', '2023-09-25 09:47:40', '2023-09-25 09:47:40');
    INSERT INTO `dept` VALUES (5, '人事部', '2023-09-25 09:47:40', '2023-09-25 09:47:40');
    INSERT INTO `dept` VALUES (15, '行政部', '2023-11-30 20:56:37', '2023-11-30 20:56:37');
    
    SET FOREIGN_KEY_CHECKS = 1;
    

    application.yml

    spring:
      application:
        name: spring-tailis
      datasource:
        url: jdbc:mysql://127.0.0.1:3306/talis
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: root
        password: root
    
  3. 准备基础代码结构,并引入实体类Dept以及统一的响应结果封装类Result

    分别创建controllermapperpojoserviceservice/impl

    并创建

    • controller/DeptController.java
    • mapper/DeptMapper.java
    • service/DeptService.java
    • service/impl/DeptServiceImpl.java

    pojo/Dept.java

    package com.itheima.pojo;
    
    import lombok.Data;
    
    import java.time.LocalDateTime;
    
    @Data
    public class Dept {
        private Integer id;
        private String name;
        private LocalDateTime createTime;
        private LocalDateTime updateTime;
    }
    

    pojo/Result.java

    package com.itheima.pojo;
    
    import lombok.Data;
    
    @Data
    public class Result {
        private Integer code;;
        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(){
            Result result = new Result();
            result.code = 0;
            result.msg = "error";
            return result;
        }
    
        public static Result error(Object object){
            Result result = new Result();
            result.data = object;
            result.code = 0;
            result.msg = "error";
            return result;
        }
    }
    

列表查询

请求方式: Get

接口地址:/depts

采用MVC开发模式

注意:下方代码写了代码基础模板,含有@Autowired注入,后续将只写方法代码,不写外层类

分析数据库查询语句

select id,name,create_time,update_time from dept order by update_time desc

Controller代码

@RequestMapping("/depts")
@RestController
public class DeptController {
    @Autowired
    private DeptService deptService;
    
    @GetMapping
    public Result list(){
        System.out.println("查询全部的部门数据");
        List<Dept> deptList = deptService.findall();
        return Result.success(deptList);
    }
}

Service接口和实现

public interface DeptService {
    List<Dept> findall();
}
@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;
    
    @Override
    public List<Dept> findall() {
        return deptMapper.findAll();
    }
}

Mapper接口

@Select("select id,name,create_time,update_time from dept order by update_time desc")
List<Dept> findAll();

由于数据库中使用create_time下划线命名方式,在JavaDept实体类中使用的是createTime驼峰命名方式,在实体类和数据库列映射时可能出现偏差,导致数据库查询出错。

解决方案:

  1. 开启一个mybatis配置文件即可完成create_time == createTime

    application.yml

    mybatis:
      configuration:
    #    将mybatis日志输出到控制台
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    #    开启驼峰命名开关
        map-underscore-to-camel-case: true
    
  2. SQL语句中as别名

    @Select("select id,name,create_time createTime,update_time updateTime from dept order by update_time desc")
    
  3. 手动结果映射

    @Results({
        @Result(column = "create_time",property = "createTime"),
        @Result(column = "update_time",property = "updateTime")
    })
    

删除接口

请求方式: Delete

接口地址: /dept/delete?id=1

通过RESTful风格路径,删除接口案例学习获取前端GET传参数据

Controller

  • 前端使用Delete

  • Get传参id=1

  • 有三种方式获取参数

    1. 原生Servlet获取请求参数

      //    
      @DeleteMapping("/delete")
      public Result delete(HttpServletRequest request){
          String idStr = request.getParameter("id");
          int id = Integer.parseInt(idStr);
          System.out.println("删除部门的id:" + id);
          return Result.success();
      
    2. 使用spring提供的 @RequestParam 注解获取请求参数

      一但声明了@RequestParam注解,那么这个参数必须存在,否则会报错(默认requiredtrue)

      @DeleteMapping("/delete")
      public Result delete(@RequestParam(value = "id",required = false) Integer id) {
          System.out.println("删除部门的id:" + id);
          return Result.success();
      }
      
    3. 省略 @RequestParam 注解,直接使用参数名称获取请求参数

      条件: 请求参数名和方法参数名称相同即可

      @DeleteMapping
      public Result delete(Integer id){
          System.out.println("删除部门id:" + id);
          deptService.deleteById(id);
          return Result.success();
      }
      

Service

service/DeptService.ava

void deleteById(Integer id);

service/impl/DeptServiceImpl.java

/**
 * 根据id查询部门
 * @param id
 */
@Override
public void deleteById(Integer id) {
    deptMapper.deleteById(id);
}

Mapper

mapper/DeptMapper.java

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

新增部门

请求方式: Post

接口地址: /dept

Controller

/**
 * 新增部门
 */
//    使用 @RequestBody 注解,将请求体中的json数据封装到 dept 对象中
//    注: @RequestBody 要求请求体中的键名要和 dept 对象的属性名一致
@PostMapping
public Result add(@RequestBody Dept dept){
    System.out.println("新增部门" + dept);
    deptService.add(dept);
    return Result.success();
}

Service

/**
 * 新增部门
 */
@Override
public void add(Dept dept) {
    //  补全基础属性 - createTime, updatetime
    dept.setCreateTime(LocalDateTime.now());
    dept.setUpdateTime(LocalDateTime.now());
    deptMapper.add(dept);
}

Mapper

/**
 * 新增部门
 * @param dept
 */
@Insert("insert into dept (id, name, create_time, update_time) values (#{id},#{name},#{createTime},#{updateTime})")
void add(Dept dept);

修改部门

修改部门点击修改时会根据当前班级的用户id查询一次数据库,在编辑页面显示,所以修改部门需要两次数据库查询

  1. 点击编辑按钮,查询当前编辑用户信息并返回
  2. 修改用户信息,点击提交更改用户信息

首先实现根据id查询部门

请求方法: Get

请求路径: /depts/{id}

Controller

/**
 * 修改部门,根据id修改
 */
//    @GetMapping("/depts/{id}")
//    public Result edit(@PathVariable("id") Integer deptid){
//        System.out.println("修改部门的id: " + deptid);
//        return Result.success();


//    若deptid的名称和路径参数的名称一致,可以直接使用@PathVariable注解获取路径参数
@GetMapping("/{id}")
public Result edit(@PathVariable Integer id){
    System.out.println("修改部门的id: " + id);
    Dept dept = deptService.getById(id);
    return Result.success(dept);
}
//    如果请求路径为 /depts/{id}/{name}
//    可以使用 @PathVariable("id") Integer deptid,@PathVariable("name") 获取

Service

/**
 * 根据id查询部门
 * @param id
 * @return
 */
@Override
public Dept getById(Integer id) {
    return deptMapper.getById(id);
}

Mapper

/**
 * 根据id查询部门
 *
 * @param id
 * @return
 */
@Select("select id,name,create_time,update_time from dept where id = #{id}")
Dept getById(Integer id);

接着实现更改用户信息

请求方法: Put

请求地址: /depts

Controller

@PutMapping
public Result update(@RequestBody Dept dept){
    System.out.println("修改的部门: " + dept);
    deptService.update(dept);
    return Result.success();
}

Service

/**
 * 更新部门
 * @param dept
 */
@Override
public void update(Dept dept) {
//        1.设置更新时间
    dept.setUpdateTime(LocalDateTime.now());
//        2.调用mapper
    deptMapper.update(dept);
}

Mapper

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

8.Logback日志

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

常用的日志技术:

  • JUL(java.util.logging):这是JavaSE平台提供的官方日志框架。配置相对简单,但不够灵活,性能较差。

  • Log4j:一个流行的日志框架,提供了灵活的配置选项,支持多种输出目标。

  • Logback:基于log4j升级而来,提供了更多的功能和配置选项,性能优于Log4j(同一作者)

  • Slf4j(Simple Logging Facade for Java):简单日志门面,提供了一套日志操作的标准接口及抽象类,允许应用程序使用不同的底层日志框架。

准备工作

  • 引入Logback依赖(springboot项目中该依赖已传递),配置logback.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个字符宽度  %logger{50}: 最长50个字符(超出.切割)  %msg:日志消息,%n是换行符 -->
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            </encoder>
        </appender>
        
        <!-- 日志输出级别 -->
        <root level="debug">
            <appender-ref ref="STDOUT" />
        </root>
    </configuration>
    
  • 记录日志:定义日志记录对象,记录日志

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

LogTest.java

package com.itheima;

import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.LocalDateTime;

public class LogTest {

    // 定义日志记录对象
    private final static Logger log = LoggerFactory.getLogger(LogTest.class);

    @Test
    public void testLog(){
//        System.out.println(LocalDateTime.now() + " : 开始计算...");
        log.debug("开始计算...");

        int sum = 0;
        int[] nums = {1, 5, 3, 2, 1, 4, 5, 4, 6, 7, 4, 34, 2, 23};
        for (int num : nums) {
            sum += num;
        }

        log.info("计算结果为: " + sum);
//        System.out.println("计算结果为: "+sum);
//        System.out.println(LocalDateTime.now() + "结束计算...");


        log.debug("结束计算...");
    }

}

配置文件解析

配置文件: logback.xml

  • 该配置文件是对Logback日志框架输出的日志进行控制的,可以来配置输出的格式、位置及日志开关等

  • 常用的两种输出日志的位置: 控制台、系统文件

<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">...</appender>
<!-- 系统文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">...</appender>
  • 开启日志(ALL),关闭日志(OFF)
<!-- 日志输出级别 -->
<root level="ALL">
    <appender-ref ref="STDOUT" />
    <appender-ref ref="FILE" />
</root>

完整日志案例

<?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>

日志级别

日志级别 说明
TRACE 最详细的日志,用于追踪程序运行的细节
DEBUG 调试级别日志,记录开发和调试过程中的信息
INFO 运行时正常状态的信息,如启动、关闭等
WARN 潜在的问题或警告,不影响程序运行
ERROR 错误信息,程序可能无法继续运行
OFF 关闭日志记录
  • 级别从低到高为:TRACE < DEBUG < INFO < WARN < ERROR < OFF

  • 一旦设置了某个级别,例如INFO,那么它只会记录INFO及以上级别(WARN、ERROR),不会记录DEBUGTRACE

  • OFF 表示完全关闭日志记录。

使用

要在开发SpringBoot项目中使用,只需要在类上方加入@Slf4j注解,下方直接使用log.{level}()记录登记即可

@Slf4j
@RestController
@RequestMapping("/emps")
public class EmpController {
	@GetMapping
    public Result page(EmpQueryParam empQueryParam){
        // 使用 Logback 记录日志
        log.info("分页查询: {}",empQueryParam);
        PageResult<Emp> pageResult = empService.page(empQueryParam);
        return Result.success(pageResult);
    } 
}

建议一般设置日志登记为Debug或者是Info级别

9.员工列表查询

请求路径:/emps

请求方式:GET

接口描述:该接口用于员工列表数据的条件分页查询

请求参数:

/emps?name=张&gender=1&begin=2007-09-01&end=2022-09-01&page=1&pageSize=10

响应格式:

{
  "code": 1,
  "msg": "success",
  "data": {
    "total": 2,
    "rows": [
       {
        "id": 1,
        "username": "jinyong",
        "password": "123456",
        "name": "金庸",
        "gender": 1,
        "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-53B.jpg",
        "job": 2,
        "salary": 8000,
        "entryDate": "2015-01-01",
        "deptId": 2,
        "deptName": "教研部",
        "createTime": "2022-09-01T23:06:30",
        "updateTime": "2022-09-02T00:29:04"
      },
      {
        "id": 2,
        "username": "zhangwuji",
        "password": "123456",
        "name": "张无忌",
        "gender": 1,
        "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-53B.jpg",
        "job": 2,
        "salary": 6000,
        "entryDate": "2015-01-01",
        "deptId": 2,
        "deptName": "教研部",
        "createTime": "2022-09-01T23:06:30",
        "updateTime": "2022-09-02T00:29:04"
      }
    ]
  }
}

准备工作:

  1. 准备数据库表 emp、emp_expr
  2. 准备实体类 EMP、EmpExpr
  3. 准备三层架构的基本代码结构: EmpController、EmpService/EmpServiceImpl、EmpMapper

数据表

-- 员工表
create table emp(
    id int unsigned primary key auto_increment comment 'ID,主键',
    username varchar(20) not null unique comment '用户名',
    password varchar(32) default '123456' comment '密码',
    name varchar(10) not null comment '姓名',
    gender tinyint unsigned not null comment '性别, 1:男, 2:女',
    phone char(11) not null unique comment '手机号',
    job tinyint unsigned comment '职位, 1 班主任, 2 讲师 , 3 学工主管, 4 教研主管, 5 咨询师',
    salary int unsigned comment '薪资',
    image varchar(255) comment '头像',
    entry_date date comment '入职日期',
    dept_id int unsigned comment '部门ID',
    create_time datetime comment '创建时间',
    update_time datetime comment '修改时间'
) comment '员工表';


INSERT INTO emp VALUES 
(1,'shinaian','123456','施耐庵',1,'13309090001',4,15000,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2000-01-01',2,'2023-10-20 16:35:33','2023-11-16 16:11:26'),
(2,'songjiang','123456','宋江',1,'13309090002',2,8600,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2015-01-01',2,'2023-10-20 16:35:33','2023-10-20 16:35:37'),
(3,'lujunyi','123456','卢俊义',1,'13309090003',2,8900,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2008-05-01',2,'2023-10-20 16:35:33','2023-10-20 16:35:39'),
(4,'wuyong','123456','吴用',1,'13309090004',2,9200,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2007-01-01',2,'2023-10-20 16:35:33','2023-10-20 16:35:41'),
(5,'gongsunsheng','123456','公孙胜',1,'13309090005',2,9500,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2012-12-05',2,'2023-10-20 16:35:33','2023-10-20 16:35:43'),
(6,'huosanniang','123456','扈三娘',2,'13309090006',3,6500,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2013-09-05',1,'2023-10-20 16:35:33','2023-10-20 16:35:45'),
(7,'chaijin','123456','柴进',1,'13309090007',1,4700,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2005-08-01',1,'2023-10-20 16:35:33','2023-10-20 16:35:47'),
(8,'likui','123456','李逵',1,'13309090008',1,4800,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2014-11-09',1,'2023-10-20 16:35:33','2023-10-20 16:35:49'),
(9,'wusong','123456','武松',1,'13309090009',1,4900,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2011-03-11',1,'2023-10-20 16:35:33','2023-10-20 16:35:51'),
(10,'linchong','123456','林冲',1,'13309090010',1,5000,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2013-09-05',1,'2023-10-20 16:35:33','2023-10-20 16:35:53'),
(11,'huyanzhuo','123456','呼延灼',1,'13309090011',2,9700,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2007-02-01',2,'2023-10-20 16:35:33','2023-10-20 16:35:55'),
(12,'xiaoliguang','123456','小李广',1,'13309090012',2,10000,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2008-08-18',2,'2023-10-20 16:35:33','2023-10-20 16:35:57'),
(13,'yangzhi','123456','杨志',1,'13309090013',1,5300,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2012-11-01',1,'2023-10-20 16:35:33','2023-10-20 16:35:59'),
(14,'shijin','123456','史进',1,'13309090014',2,10600,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2002-08-01',2,'2023-10-20 16:35:33','2023-10-20 16:36:01'),
(15,'sunerniang','123456','孙二娘',2,'13309090015',2,10900,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2011-05-01',2,'2023-10-20 16:35:33','2023-10-20 16:36:03'),
(16,'luzhishen','123456','鲁智深',1,'13309090016',2,9600,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2010-01-01',2,'2023-10-20 16:35:33','2023-10-20 16:36:05'),
(17,'liying','12345678','李应',1,'13309090017',1,5800,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2015-03-21',1,'2023-10-20 16:35:33','2023-10-20 16:36:07'),
(18,'shiqian','123456','时迁',1,'13309090018',2,10200,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2015-01-01',2,'2023-10-20 16:35:33','2023-10-20 16:36:09'),
(19,'gudasao','123456','顾大嫂',2,'13309090019',2,10500,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2008-01-01',2,'2023-10-20 16:35:33','2023-10-20 16:36:11'),
(20,'ruanxiaoer','123456','阮小二',1,'13309090020',2,10800,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2018-01-01',2,'2023-10-20 16:35:33','2023-10-20 16:36:13'),
(21,'ruanxiaowu','123456','阮小五',1,'13309090021',5,5200,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2015-01-01',3,'2023-10-20 16:35:33','2023-10-20 16:36:15'),
(22,'ruanxiaoqi','123456','阮小七',1,'13309090022',5,5500,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2016-01-01',3,'2023-10-20 16:35:33','2023-10-20 16:36:17'),
(23,'ruanji','123456','阮籍',1,'13309090023',5,5800,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2012-01-01',3,'2023-10-20 16:35:33','2023-10-20 16:36:19'),
(24,'tongwei','123456','童威',1,'13309090024',5,5000,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2006-01-01',3,'2023-10-20 16:35:33','2023-10-20 16:36:21'),
(25,'tongmeng','123456','童猛',1,'13309090025',5,4800,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2002-01-01',3,'2023-10-20 16:35:33','2023-10-20 16:36:23'),
(26,'yanshun','123456','燕顺',1,'13309090026',5,5400,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2011-01-01',3,'2023-10-20 16:35:33','2023-11-08 22:12:46'),
(27,'lijun','123456','李俊',1,'13309090027',2,6600,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2004-01-01',2,'2023-10-20 16:35:33','2023-11-16 17:56:59'),
(28,'lizhong','123456','李忠',1,'13309090028',5,5000,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2007-01-01',3,'2023-10-20 16:35:33','2023-11-17 16:34:22'),
(30,'liyun','123456','李云',1,'13309090030',NULL,NULL,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2020-03-01',NULL,'2023-10-20 16:35:33','2023-10-20 16:36:31'),
(36,'linghuchong','123456','令狐冲',1,'18809091212',2,6800,'https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg','2023-10-19',2,'2023-10-20 20:44:54','2023-11-09 09:41:04');


-- 员工工作经历信息
create table emp_expr(
    id int unsigned primary key auto_increment comment 'ID, 主键',
    emp_id int unsigned comment '员工ID',
    begin date comment '开始时间',
    end  date comment '结束时间',
    company varchar(50) comment '公司名称',
    job varchar(50) comment '职位'
)comment '工作经历';

导入实体类

pojo/Emp.java

package com.itheima.pojo;

import lombok.Data;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

@Data
public class Emp {
    private Integer id; //ID,主键
    private String username; //用户名
    private String password; //密码
    private String name; //姓名
    private Integer gender; //性别, 1:男, 2:女
    private String phone; //手机号
    private Integer job; //职位, 1:班主任,2:讲师,3:学工主管,4:教研主管,5:咨询师
    private Integer salary; //薪资
    private String image; //头像
    private LocalDate entryDate; //入职日期
    private Integer deptId; //关联的部门ID
    private LocalDateTime createTime; //创建时间
    private LocalDateTime updateTime; //修改时间
}

pojo/EmpExpr.java

package com.itheima.pojo;

import lombok.Data;

import java.time.LocalDate;

/**
 * 工作经历
 */
@Data
public class EmpExpr {
    private Integer id; //ID
    private Integer empId; //员工ID
    private LocalDate begin; //开始时间
    private LocalDate end; //结束时间
    private String company; //公司名称
    private String job; //职位
}

准备三层架构,创建实体类,加上注解,这些不再叙述

准备sql语句

// 查询所有员工信息,以及该员工所属的部门名称
select e.*, d.name  from emp e left join dept d on e.dept_id = d.id

原始分页查询

通过分析,需要进行两条SQL语句

  • 查询总记录数
  • 分页查询

sql语句如下

查询总记录数

select count(*) from emp e left join dept d on e.dept_id = d.id

分页查询

select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.id 
    order by update_time desc 
    limit 0,10;

根据响应格式创建PageResult返回封装类

pojo/PageResult.java

package com.itheima.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;

@NoArgsConstructor
@AllArgsConstructor
@Data
public class PageResult<T> {
    private Long total;
    private List<T> rows;
}

Mapper

先开发Mapper接口

  1. 查询总记录数
  2. 分页查询
/**
 * 查询总记录数
 * @return
 */
@Select("select count(*) from emp e left join dept d on e.dept_id = d.id")
public Long count();

/**
 * 分页查询
 */
@Select("select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.id " 			+ "order by update_time desc limit #{start},#{pageSize};")
public List<Emp> list(Integer start, Integer pageSize);

上述中,分页查询会返回一个name列,由于结构文档需要名称为deptName,所以使用别名更改为deptName

deptName是在dept表中,并不属于pojo/Emp.java实体类属性,但是最后返回结果封装给了Emp类,所以需要在pojo/Emp.java加入一个属性

pojo/Emp.java

private String deptName;

Controller

  1. 接受参数
  2. 调用Service
  3. 响应结果
@GetMapping
public Result page(@RequestParam(defaultValue = "1")Integer page,
                   @RequestParam(defaultValue = "10") Integer pageSize){
    log.info("分页查询: {},{}",page,pageSize);
    PageResult<Emp> pageResult = empService.list(page,pageSize);
    return Result.success(pageResult);
}

Service

  1. 调用Mapper接口,查询总记录数
  2. 调用Mapper接口,查询结果列表
  3. 封装PageResult对象返回
/**
 * 分页查询,原始方法
 * @param page
 * @param pageSize
 * @return
 */
@Override
public PageResult<Emp> list(Integer page, Integer pageSize) {
    //1. 调用Mapper接口,查询总记录数
    Long total = empMapper.count();
    
    //2. 调用Mapper接口,查询结果列表, -1 是因为 mysql的limit从0开始
    Integer start = (page - 1) * pageSize;
    List<Emp> rows = empMapper.list(start, pageSize);
    
    //3. 封装结果到PageResult对象并返回
    return new PageResult<Emp>(total, rows);
}

PageHelper插件分页查询

PageHelper是第三方提供的Mybatis框架中用来实现分页的插件,用来简化分页操作提高开发效率

  • 使用步骤
    1. 引入PageHelper依赖
    2. 定义Mapper接口的查询方法(无需考虑分页)
    3. 在Service方法中实现分页查询
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.7</version>
</dependency>

Mapper

@Select("select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.id order by update_time desc")
public List<Emp> list();

Service

/**
 * 使用PageHelper插件进行用户信息的分页查询
 * @param page
 * @param pageSize
 * @return
 */
@Override
public PageResult<Emp> list(Integer page, Integer pageSize) {
//        1.设置分页参数
    PageHelper.startPage(page,pageSize);
//        2.执行查询
    List<Emp> empList = empMapper.list();
//        3.解析查询结果
    Page<Emp> p = (Page<Emp>) empList;
    return new PageResult<Emp>(p.getTotal(),p.getResult());
}

Controller不用更改

多条件分页查询

虽然完成了分页查询,该接口需要提供用户名、性别、最近修改时间进行查询

Controller

@GetMapping
public Result page(@RequestParam(defaultValue = "1")Integer page,
                   @RequestParam(defaultValue = "10") Integer pageSize,
                   String name, Integer gender,
                   @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
                   @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){
    log.info("分页查询: {},{},{},{},{},{}",page,pageSize,name,gender,begin,end);
    PageResult<Emp> pageResult = empService.page(page,pageSize,name,gender,begin,end);
    return Result.success(pageResult);
}

Service

/**
 * 动态条件分页查询
 * @param page
 * @param pageSize
 * @param name
 * @param gender
 * @param begin
 * @param end
 * @return
 */
@Override
public PageResult<Emp> page(Integer page, Integer pageSize, String name, Integer gender, LocalDate begin, LocalDate end) {
//        1.设置分页参数
    PageHelper.startPage(page, pageSize);

//        2.执行查询
    List<Emp> empList =  empMapper.page(name, gender, begin, end);

//        3.解析查询结果并返回
    Page<Emp> p = (Page<Emp>) empList;
    return new PageResult<>(p.getTotal(), p.getResult());

}

Mapper

/**
 * 动态条件分页查询
 * @param name
 * @param gender
 * @param begin
 * @param end
 * @return
 */
List<Emp> page(String name, Integer gender, LocalDate begin, LocalDate end);

EmpMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpMapper">
    
    <select id="page" resultType="com.itheima.pojo.Emp">
        select e.*, d.name  from emp e left join dept d on e.dept_id = d.id
        <where>
            <if test="name != null and name != ''">
                e.name like concat('%',#{name},'%')
            </if>
            <if test="gender != null and gender != ''">
                and gender = #{gender}
            </if>
            <if test="begin != null and end != null">
                and entry_date between #{begin} and #{end}
            </if>
        </where>
        order by update_time desc
    </select>
    
</mapper>

多条件分页查询改造

上述中,在Controler接受参数处定义了很多的参数传递,由于参数较多,可以将请求参数封装到一个实体类中,方便代码编写,逻辑更清晰

创建EmpQueryParam实体类,封装请求参数

pojo/EmpQueryParam.java

package com.itheima.pojo;

import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;

@Data
public class EmpQueryParam {
    private Integer page = 1;
    private Integer pageSize = 10;
    String name;
    Integer gender;
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    LocalDate begin;
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    LocalDate end;
}

Controller

/**
 * 分页查询,使用Mybatis动态条件查询
 * @param empQueryParam
 * @return
 */
@GetMapping
public Result page(EmpQueryParam empQueryParam){
    log.info("分页查询: {}",empQueryParam);
    PageResult<Emp> pageResult = empService.page(empQueryParam);
    return Result.success(pageResult);
}

Service

/**
 * 使用PageHelper插件进行用户信息的分页查询
 * @param empQueryParam
 * @return
 */
@Override
public PageResult<Emp> page(EmpQueryParam empQueryParam) {
//        1.设置分页参数
    PageHelper.startPage(empQueryParam.getPage(), empQueryParam.getPageSize());

//        2.执行查询
    List<Emp> empList =  empMapper.page(empQueryParam);

//        3.解析查询结果并返回
    Page<Emp> p = (Page<Emp>) empList;
    return new PageResult<>(p.getTotal(), p.getResult());
}

Mapper

/**
 * 使用Mapper配置文件来实现分页查询
 * @param empQueryParam
 * @return
 */
List<Emp> page(EmpQueryParam empQueryParam);

EmpMapper.xml(不变)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpMapper">
    
    <select id="page" resultType="com.itheima.pojo.Emp">
        select e.*, d.name  from emp e left join dept d on e.dept_id = d.id
        <where>
            <if test="name != null and name != ''">
                e.name like concat('%',#{name},'%')
            </if>
            <if test="gender != null and gender != ''">
                and gender = #{gender}
            </if>
            <if test="begin != null and end != null">
                and entry_date between #{begin} and #{end}
            </if>
        </where>
        order by update_time desc
    </select>
    
</mapper>

10.新增员工信息

新增员工需要增加员工信息和员工的工作经历信息,分别在两个表中存储,下方是接口需要系统的数据

请求路径:/emps

请求方式:POST

接口描述:该接口用于添加员工的信息

请求数据样例:

{
  "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-03-07-37-38222.jpg",
  "username": "linpingzhi",
  "name": "林平之",
  "gender": 1,
  "job": 1,
  "entryDate": "2022-09-18",
  "deptId": 1,
  "phone": "18809091234",
  "salary": 8000,
  "exprList": [
      {
         "company": "百度科技股份有限公司",
         "job": "java开发",
         "begin": "2012-07-01",
         "end": "2019-03-03"
      },
      {
         "company": "阿里巴巴科技股份有限公司",
         "job": "架构师",
         "begin": "2019-03-15",
         "end": "2023-03-01"
      }
   ]
}

准备数据

创建pojo/EmpExpr.java

package com.itheima.pojo;

import lombok.Data;
import java.time.LocalDate;

/**
 * 工作经历
 */
@Data
public class EmpExpr {
    private Integer id; //ID
    private Integer empId; //员工ID
    private LocalDate begin; //开始时间
    private LocalDate end; //结束时间
    private String company; //公司名称
    private String job; //职位
}

pojo/Emp.java增加工作经历,最后返回结果封装到Emp实体类

package com.itheima.pojo;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Emp {
    // ......(之前的代码)
    private List<EmpExpr> exprList;
}

Controller

@PostMapping
public Result add(@RequestBody Emp emp){
    log.info("新增员工: {}",emp);
    empService.add(emp);
    return Result.success();
}

Service

    /**
     * 添加 用户信息 和 工作经历
     * @param emp
     */
//    开启事务回滚
    @Transactional(rollbackFor = {Exception.class})
    @Override
    public void add(Emp emp) {
//        1.新增用户基本信息
//        用户并有传递创建时间和修改时间,需要手动设置
        emp.setCreateTime(LocalDateTime.now());
        emp.setUpdateTime(LocalDateTime.now());
        empMapper.add(emp);

//        验证事务回滚
//        Integer num = 1/0;


//        2.新增用户工作经历
        List<EmpExpr> exprList = emp.getExprList();
        if(!CollectionUtils.isEmpty(exprList)){
            // 遍历集合,为 empId 赋值
            exprList.forEach(empExpr ->{
                empExpr.setEmpId(emp.getId());
            });
            empExprMapper.add(exprList);
        }
    }

Service层需要注意的是,新增用户工作经历时,前端并没有传入新增用户的id值,需要在新增用户基本信息时的id进行插入,可以使用Spingboot提供的主键返回注解,将id返回

见下方Mapper层注解Options(useGeneratedKeys = true, keyProperty = "id")

Mapper

mapper/EmpMapper.java

// Mybatis提供 主键返回,执行后
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into emp(username, name, gender, phone, job," +
        "                salary, image, entry_date, dept_id," +
        "                create_time, update_time)" +
        "    values (#{username}, #{name}, #{gender}, #{phone},#{job}, " +
        "   #{salary}, #{image}, #{entryDate}, #{deptId}, " +
        "#{createTime}, #{updateTime})")
void add(Emp emp);

mapper/EmpExprMapper.java

package com.itheima.mapper;

@Mapper
public interface EmpExprMapper {

    /**
     * 新增员工工作经历
     * @param exprList
     */
    void add(List<EmpExpr> exprList);
}

EmpExprMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpExprMapper">

<!--    collection 集合名称-->
<!--    item 集合遍历出来的元素-->
<!--    separator 每次遍历使用的分隔符-->
<!--    open 遍历开始拼接的片段-->
<!--    close 遍历结束拼接的片段-->
    <insert id="add">
        insert into emp_expr(emp_id, begin, end, company, job) values
        <foreach collection="exprList" item="expr" separator=",">
            (#{expr.empId},#{expr.begin},#{expr.end},#{expr.company},#{expr.job})
        </foreach>
    </insert>

</mapper>

11.事物管理

介绍

例子: 上述新增员工信息中,若保存员工的基本信息成功了,保存工作经历失败了,怎么办?

就会造成数据库数据不完整、不一致。

事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败

操作

事务控制主要三步操作:开启事务、提交事务/回滚事务

-- 开启事务
start transaction;

# 执行员工插入的sql语句
# ......
# ......


-- 提交事务
commit;

-- 回滚事务
rollback;

注意: 进行事务管理需要mysql数据库中数据表类型是InnoDB类型,使用下方命令查询/修改当前数据库类型

-- 查看当前表使用的存储引擎
SHOW TABLE STATUS LIKE 'your_table_name';


-- 查看数据库中所有表的引擎
SELECT table_name, engine 
FROM information_schema.tables 
WHERE table_schema = 'your_database_name';


-- 修改表的存储引擎为 InnoDB
ALTER TABLE your_table_name ENGINE = InnoDB;

由于在日常项目总进行事务管理,需要执行SQL语句,非常麻烦,于是SpringBoot提供了事务管理的的注解:

  • 注解:@Transactional
  • 作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务
  • 位置:业务(service)层的方法上、类上,接口上;推荐放到业务层方法上,避免不必要的资源浪费

由于默认设置日志级别为Info,JDBC连接默认使用的是Debug级别日志,所以需要单独配置显示日志

application.yml

logging:
  level:
#   由于将日志级别设置为info不会显示debug日志,事务管理需要显示,所以仅显示事务管理的debug日志
    org.springframework.jdbc.support.JdbcTransactionManager: debug

例如可以参考上方新增员工信息的Service层代码

    /**
     * 添加 用户信息 和 工作经历
     * @param emp
     */
//    开启事务回滚
    @Transactional(rollbackFor = {Exception.class})
    @Override
    public void add(Emp emp) {
//        1.新增用户基本信息
//        用户并有传递创建时间和修改时间,需要手动设置
        emp.setCreateTime(LocalDateTime.now());
        emp.setUpdateTime(LocalDateTime.now());
        empMapper.add(emp);

//        验证事务回滚
//        Integer num = 1/0;


//        2.新增用户工作经历
        List<EmpExpr> exprList = emp.getExprList();
        if(!CollectionUtils.isEmpty(exprList)){
            // 遍历集合,为 empId 赋值
            exprList.forEach(empExpr ->{
                empExpr.setEmpId(emp.getId());
            });
            empExprMapper.add(exprList);
        }
    }

上述代码中其实直接使用@Transactional注解就可以完成事务管理的操作。

所以为什么要加入rollbackFor = {Exception.class}参数?

  • rollbackFor执行出现何种异常,回滚事务

  • Exception.class表示所有异常

在直接使用@Transcational注解时:默认出现RuntimeException才会回滚事务

若手动会抛出异常,或出现其他类型异常,事务还是会进行提交,所以建议加上rollbackFor = {Exceptin.class},遇到所有异常,回滚事务

12.文件上传

  • 文件上传:是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程
  • 文件上传在项目中应用非常广泛,我们经常发微博、微信朋友圈都用到了文件上传功能

文件上传

文件上传通常前端为一个form表单

<form action="/upload" method="post" enctype="multipart/form-data">
    姓名:<input type="text" name="name"> <br>
    年龄:<input type="text" name="age"> <br>
    图像:<input type="file" name="file"> <br>
    <input type="submit" value="上传文件" name="submit">
</form>

这个表单具备上传文件所需的三个关键要素:

  1. method="post":使用 POST 方法提交数据。
  2. enctype="multipart/form-data":编码类型用于文件上传。
  3. <input type="file" name="file">:用于选择文件的输入框。

本地文件上传

首先准备上传页面

resources/static/upload.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>文件上传示例</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: Arial, sans-serif; background: #f5f5f5; display: flex;
            justify-content: center; align-items: center; height: 100vh; }
        .container { background: #fff; padding: 2rem; border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1); width: 320px; }
        h1 { text-align: center; margin-bottom: 1.5rem; font-size: 1.5rem; }
        form label { display: block; margin: 0.8rem 0 0.4rem; font-weight: bold; }
        form input[type="text"],
        form input[type="number"],
        form button { width: 100%; padding: 0.6rem; border: 1px solid #ccc;
            border-radius: 4px; }
        form button { margin-top: 1.2rem; background: #007bff; color: #fff;
            border: none; cursor: pointer; }
        /* 隐藏原生 file input */
        input[type="file"] { display: none; }
        /* 自定义 file label 按钮 */
        .custom-file-label { display: block; width: 100%; padding: 0.6rem;
            background: #28a745; color: white; border-radius: 4px;
            text-align: center; cursor: pointer; margin-top: 0.4rem; }
        .file-name { margin-top: 0.4rem; font-size: 0.9rem; color: #555; }
    </style>
</head>
<body>
<div class="container">
    <h1>上传您的文件</h1>
    <form action="/upload" method="POST" enctype="multipart/form-data">
        <label for="username">用户名:</label>
        <input type="text" id="username" name="name" required />

        <label for="age">年龄:</label>
        <input type="number" id="age" name="age" min="0" max="120" required />

        <label for="file">选择文件:</label>
        <label class="custom-file-label" for="file">点击选择文件</label>
        <input type="file" id="file" name="file" required />
        <div class="file-name" id="file-name">未选择文件</div>

        <button type="submit">上传</button>
    </form>
</div>
<script>
    const fileInput = document.getElementById('file');
    const fileNameDiv = document.getElementById('file-name');
    fileInput.addEventListener('change', () => {
        const name = fileInput.files.length > 0 ? fileInput.files[0].name : '未选择文件';
        fileNameDiv.textContent = name;
    });
</script>
</body>
</html>

创建UploadController

controller/UploadController.java

@Slf4j
@RestController
public class UploadController {
    @PostMapping("/upload")
    public Result upload(String name, Integer age, MultipartFile file) throws IOException {
        log.info("文件上传: {},{},{}",name,age,file);

//        获取上传文件名
        String fileName = file.getOriginalFilename();

//        获取上传文件后缀名
//        使用substring截取字符串,使用lastIndexOf找到最后一个.的索引开始截取
        String suffix = fileName.substring(fileName.lastIndexOf(".")); // .png

//        放置同名文件上传覆盖,使用UUID对图片进行重命名
        String newFileName = UUID.randomUUID().toString() + suffix;

//        保存文件
        file.transferTo(new File("E:/source/" + newFileName));

        return Result.success();
    }
}

注意:在SpringBoot项目中,默认单个文件上传大小不能超过1MB,超过需要调整配置文件

**application.yml

spring:
  ......
  ......
  servlet:
    multipart:
#      最大单个文件大小
      max-file-size: 5MB
#     最大请求大小(包含表单和文件上传数据)
      max-request-size: 10MB

阿里云OSS

准备

阿里云对象存储OSS(Object Storage Service),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。

参考阿里云官方文档

https://help.aliyun.com/zh/oss/developer-reference/preface-1/?spm=5176.8465980.console-base_help.dexternal.4e7014506rSR9l

创建aliyun账号,创建Bucket,生成AccessKey

在本地环境变量设置AccessKey

# 设置
setx OSS_ACCESS_KEY_ID "YOUR_ACCESS_KEY_ID"
setx OSS_ACCESS_KEY_SECRET "YOUR_ACCESS_KEY_SECRET"

# 查看
echo %OSS_ACCESS_KEY_ID%
echo %OSS_ACCESS_KEY_SECRET%

添加依赖

# aliyunOSS依赖
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.17.4</version>
</dependency>
# JABX依赖
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.3</version>
</dependency>

文件上传Demo

在测试目录创建AliyunDemo,从官方文档复制

AliyunDemo.java

package com.itheima;

import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.*;
import com.aliyun.oss.common.comm.SignVersion;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import java.io.ByteArrayInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class AliyunDemo {

    public static void main(String[] args) throws Exception {
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-beijing.aliyuncs.com";
        // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "junglezt";
        // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
        String objectName = "test.png";
        // 填写Bucket所在地域。以华东1(杭州)为例,Region填写为cn-hangzhou。
        String region = "cn-beijing";

        // 创建OSSClient实例。
        ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
        clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
        OSS ossClient = OSSClientBuilder.create()
                .endpoint(endpoint)
                .credentialsProvider(credentialsProvider)
                .clientConfiguration(clientBuilderConfiguration)
                .region(region)
                .build();

        try {
            // 填写字符串。
            String filePath = "D:\\图片\\【哲风壁纸】唯美背景-性感.png";
            Path path = Paths.get(filePath);
            byte[] content = Files.readAllBytes(path);

            // 创建PutObjectRequest对象。
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, new ByteArrayInputStream(content));

            // 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
            // ObjectMetadata metadata = new ObjectMetadata();
            // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
            // metadata.setObjectAcl(CannedAccessControlList.Private);
            // putObjectRequest.setMetadata(metadata);

            // 上传字符串。
            PutObjectResult result = ossClient.putObject(putObjectRequest);
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }
}   

上述代码中,前面的变量填写基本信息,后面的代码重要的如下:

  • bucketName为变量配置的junglezt
  • ObjectName为上传后的文件名
  • content为上传文件的内容(需要文件字节流)
// 前面的代码
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-beijing.aliyuncs.com";
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
// 填写Bucket名称,例如examplebucket。
String bucketName = "junglezt";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "test.png";
// 填写Bucket所在地域。以华东1(杭州)为例,Region填写为cn-hangzhou。
String region = "cn-beijing";

......
......

// 后面的代码
// 填写字符串。
String filePath = "D:\\图片\\【哲风壁纸】唯美背景-性感.png";
Path path = Paths.get(filePath);
byte[] content = Files.readAllBytes(path);

// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, new ByteArrayInputStream(content));

新增员工信息(文件上传案例)

请求路径:/upload

请求方式:POST

接口描述:上传图片接口

请求参数:

参数格式:multipart/form-data

参数说明:

参数名称 参数类型 是否必须 示例 备注
file file

响应数据样例:

{
    "code": 1,
    "msg": "success",
    "data": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-0400.jpg"
}

在使用aliyunOSS上传文件时,前后端需要以下步骤

  1. 接收前端上传的图片
  2. 将图片存储起来(OSS)
  3. 返回图片访问的URL

案例实现步骤:

  1. 引入阿里云OSS文件上传工具类(根据官方代码改造)
  2. 上传文件接口开发
Utils

使用AI根据Demo类改造

package com.itheima.utils;

import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
import com.aliyun.oss.common.comm.SignVersion;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;

@Component
public class AliyunOSSOperator {
    private String endpoint = "https://oss-cn-beijing.aliyuncs.com";
    private String bucketName = "junglezt";
    private String region = "cn-beijing";


    public String upload(byte[] content, String fileName) throws com.aliyuncs.exceptions.ClientException {
//        从环境变量读取accessKey
        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();



//        设置上传文件名规则
        // 获取当前年月,例如 202504
        String folderName = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
        // 获取文件后缀(扩展名)
        String extension = "";
        int dotIndex = fileName.lastIndexOf('.');
        if (dotIndex >= 0) {
            extension = fileName.substring(dotIndex);
        }
        // 生成随机 UUID 文件名
        String  uuidFileName = UUID.randomUUID().toString().replace("-", "") + extension;
        // 拼接最终路径
        String ObjectName = folderName + "/" + uuidFileName;




        // 创建OSSClient实例。
        ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
        clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
        OSS ossClient = OSSClientBuilder.create()
                .endpoint(endpoint)
                .credentialsProvider(credentialsProvider)
                .clientConfiguration(clientBuilderConfiguration)
                .region(region)
                .build();

        try {
            // 创建PutObjectRequest对象。
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, ObjectName, new ByteArrayInputStream(content));

            // 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
            // ObjectMetadata metadata = new ObjectMetadata();
            // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
            // metadata.setObjectAcl(CannedAccessControlList.Private);
            // putObjectRequest.setMetadata(metadata);

            // 上传字符串。
            PutObjectResult result = ossClient.putObject(putObjectRequest);
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }

        return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + ObjectName;
    }
}

该文件上传工具类需要传入文件字节流文件名称,通过文件上传后,会返回一个图片的访问地址

Controller
@Slf4j
@RestController
public class UploadController {

    @Autowired
    private AliyunOSSOperator aliyunOSSOperator;
    
    @PostMapping("/upload")
    public Result upload(MultipartFile file) throws IOException, ClientException {
        log.info("文件上传: {}", file);
        String image_url = aliyunOSSOperator.upload(file.getBytes(), file.getOriginalFilename());
        log.info("图片上传成功,图片地址为:{}",image_url);
        return Result.success(image_url);
    }
    
}

通过Controller获取文件字节流文件名,将参数传入Utils工具类进行文件上传,将返回的image_url地址返回给前端

13.配置文件程序优化

@Value注解

上述在进行AliyunOss进行文件上传时,需要配置Bucketendpoint等地址,为了方便项目的管理与维护,这些配置一般建议统一配置到application.yaml配置文件,需要使用@Value注解来实现

具体案例如下

原先的数据

@Component
public class AliyunOSSOperator {
    private String endpoint = "https://oss-cn-beijing.aliyuncs.com";
    private String bucketName = "junglezt";
    private String region = "cn-beijing";
}

优化后的数据

@Component
public class AliyunOSSOperator {
    //    通过@Value注解获取属性值
    @Value("${aliyun.oss.endpoint}")
    private String endpoint;
    @Value("${aliyun.oss.bucketName}")
    private String bucketName;
    @Value("${aliyun.oss.region}")
    private String region;
}

application.yml增加下方代码

#阿里云OSS
aliyun:
  oss:
    endpoint: https://oss-cn-beijing.aliyuncs.com
    bucketName: junglezt
    region: cn-beijing

@ConfigurationProperties

问题分析:虽然上述配置信息都配置到了application.yml文件中,但是每个属性都需要@Value进行注入一次,如果该配置需要注入的位置较多,也是不太方便代码的维护。

这时可以使用到另外一个注解:@ConfigurationProperties

原先的数据

@Component
public class AliyunOSSOperator {
    private String endpoint = "https://oss-cn-beijing.aliyuncs.com";
    private String bucketName = "junglezt";
    private String region = "cn-beijing";
}

优化后的数据

创建Utils/AliyunOSSProperties.java实体类

package com.itheima.utils;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
// prefix = "aliyun.oss" 表示读取application.yml文件中的 aliyun.oss 节点下的属性
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliyunOSSProperties {
    private String endpoint;
    private String bucketName;
    private String region;
}

在配置文件application.yml添加配置信息

#阿里云OSS
aliyun:
  oss:
    endpoint: https://oss-cn-beijing.aliyuncs.com
    bucketName: junglezt
    region: cn-beijing
    

修改源代码如下

//通过@ConfigurationProperties注解获取属性值
@Autowired private AliyunOSSProperties aliyunOSSProperties;

// 在类中的方法中使用get方法获取属性值
public String upload(byte[] content, String fileName) throws com.aliyuncs.exceptions.ClientException {
    String endpoint = aliyunOSSProperties.getEndpoint();
    String bucketName = aliyunOSSProperties.getBucketName();
    String region = aliyunOSSProperties.getRegion();
}

总结

上述两种程序配置优化方式,都需要在application.yml文件中配置,根据配置使用的次数

  • 配置使用较少:@Value
  • 配置使用较多: @ConfigurationProperties

14.删除员工信息

请求路径:/emps

请求方式:DELETE

接口描述:该接口用于批量删除员工的数据信息

请求参数样例:

/emps?ids=1,2,3

响应数据样例:

{
    "code":1,
    "msg":"success",
    "data":null
}

进行员工删除时,含有单个员工删除和多个员工删除,在接口开发时,需要思考开发几个删除接口?

其实只需要开发一个批量删除的删除即可,因为单独的员工删除算是一个独特的批量员工删除

SQL语句分析

员工信息包含员工基本信息和员工的工作经历信息,所以需要两条sql语句

-- 删除员工基本信息
delete from emp where id in (1,2,3);

-- 删除员工工作经历信息
delete from emp_expr where emp_id in (1,2,3);

Controller

需要获取前端ids=1,2,3逗号分割方式的传参,以下两种方式

第一种:

@DeleteMapping
public Result delete(Integer[] ids){
    log.info("删除员工: {}", Arrays.toString(ids));
    return Result.success();
}

第二种:

//    使用@RequestParam配合List获取
    @DeleteMapping
    public Result delete(@RequestParam List<Integer> ids){
        log.info("删除员工: {}", ids);
        empService.delete(ids);
        return Result.success();
    }

@RequestParam List<Integer> ids这种格式可以获取下方两种方式的传参

  • GET /emps?ids=1,2,3
  • GET /items?ids=1&ids=2&ids=3

Service

//    开启事务回滚
    @Transactional(rollbackFor = {Exception.class})
    @Override
    public void delete(List<Integer> ids) {
//        1.批量删除用户信息
        empMapper.delete(ids);

//        2.批量删除用户工作经历
        empExprMapper.delete(ids);
    }

Mapper

Mapper/EmpMapper.java

/**
 * 根据 id 列表,批量删除用户
 * @param ids
 */
void delete(List<Integer> ids);

EmpMapper.xml

<!--    批量删除员工基本信息 (1,2,3)  -->
<delete id="delete">
    delete from emp where id in
    <foreach collection="ids" item="id" separator="," open="(" close=")">
        #{id}
    </foreach>
</delete>

mapper/EmpExprMapper.java

/**
 * 批量删除员工工作经历
 * @param empIds
 */
void delete(List<Integer> empIds);

EmpExprMapper.xml

<!--    批量删除员工工作经历信息 (1,2,3) -->
<delete id="delete">
    delete from emp_expr where emp_id in
    <foreach collection="empIds" item="empId" separator="," open="(" close=")">
        #{empId}
    </foreach>
</delete>

15.修改员工信息

分析:在点击编辑按钮修改员工信息的时候,需要查询一次当前修改用户的信息,方便修改,也就是根据当前编辑用户的id查询并返回当前用户的信息

那么修改员工信息分为两个步骤:

  • 查询当前员工信息
  • 修改员工信息

根据id查询当前员工信息

请求路径:/emps/{id}

请求方式:GET

接口描述:该接口用于根据主键ID查询员工的信息

请求参数样例:

/emps/1

响应数据样例:

{
  "code": 1,
  "msg": "success",
  "data": {
    "id": 2,
    "username": "zhangwuji",
    "password": "123456",
    "name": "张无忌",
    "gender": 1,
    "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-53B.jpg",
    "job": 2,
    "salary": 8000,
    "entryDate": "2015-01-01",
    "deptId": 2,
    "createTime": "2022-09-01T23:06:30",
    "updateTime": "2022-09-02T00:29:04",
    "exprList": [
      {
        "id": 1,
        "begin": "2012-07-01",
        "end": "2019-03-03"
        "company": "百度科技股份有限公司"
        "job": "java开发",
        "empId": 2
      },
      {
        "id": 2,
        "begin": "2019-3-15",
        "end": "2023-03-01"
        "company": "阿里巴巴科技股份有限公司"
        "job": "架构师",
        "empId": 2
      }
    ]
  }
}

Controller

/**
 * 根据id查询员工信息
 * @param id
 * @return
 */
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id){
    log.info("根据id查询员工信息: {}", id);
    Emp emp = empService.getById(id);
    return Result.success(emp);
}

Service

@Override
public Emp getById(Integer id) {
    return empMapper.getById(id);
}

Mapper

/**
 * 根据id查询用户信息,含用户工作经历
 * @param id
 * @return
 */
Emp getById(Integer id);

EmpMapper.xml

<!--    定制resultMap -->
<resultMap id="empResultMap" type="com.itheima.pojo.Emp">
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <result column="password" property="password"/>
    <result column="name" property="name"/>
    <result column="gender" property="gender"/>
    <result column="phone" property="phone"/>
    <result column="job" property="job"/>
    <result column="salary" property="salary"/>
    <result column="image" property="image"/>
    <result column="entry_date" property="entryDate"/>
    <result column="dept_id" property="deptId"/>
    <result column="create_time" property="createTime"/>
    <result column="update_time" property="updateTime"/>

    <collection property="exprList" ofType="com.itheima.pojo.EmpExpr">
        <id column="ee_id" property="id"/>
        <result column="ee_empid" property="empId"/>
        <result column="ee_begin" property="begin"/>
        <result column="ee_end" property="end"/>
        <result column="ee_company" property="company"/>
        <result column="ee_job" property="job"/>

    </collection>
</resultMap>

<select id="getById" resultMap="empResultMap">
    select
    e.*,
    ee.id ee_id,
    ee.emp_id ee_empid,
    ee.begin ee_begin,
    ee.end ee_end,
    ee.company ee_company,
    ee.job ee_job
    from emp e left join emp_expr ee on e.id = ee.emp_id
    where e.id = #{id}
</select>

修改员工信息

请求路径:/emps

请求方式:PUT

接口描述:该接口用于修改员工的数据信息

请求数据样例:

{
    "id": 2,
    "username": "zhangwuji",
    "password": "123456",
    "name": "张无忌",
    "gender": 1,
    "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-53B.jpg",
    "job": 2,
    "salary": 8000,
    "entryDate": "2015-01-01",
    "deptId": 2,
    "createTime": "2022-09-01T23:06:30",
    "updateTime": "2022-09-02T00:29:04",
    "exprList": [
      {
        "id": 1,
        "begin": "2012-07-01",
        "end": "2015-06-20"
        "company": "中软国际股份有限公司"
        "job": "java开发",
        "empId": 2
      },
      {
        "id": 2,
        "begin": "2015-07-01",
        "end": "2019-03-03"
        "company": "百度科技股份有限公司"
        "job": "java开发",
        "empId": 2
      },
      {
        "id": 3,
        "begin": "2019-3-15",
        "end": "2023-03-01"
        "company": "阿里巴巴科技股份有限公司"
        "job": "架构师",
        "empId": 2
      }
    ]
  }

响应数据样例:

{
    "code":1,
    "msg":"success",
    "data":null
}

Controller

@PutMapping
public Result update(@RequestBody Emp emp){
    log.info("修改员工信息: {}", emp);
    empService.update(emp);
    return Result.success();
}

Service

@Override
public void update(@RequestBody Emp emp) {
//        1.根据id更改用户信息
    emp.setUpdateTime(LocalDateTime.now());
    empMapper.update(emp);

//        2. 根据id修改工作经历
//        2.1根据id删除用户原有工作经历
    empExprMapper.delete(Arrays.asList(emp.getId()));

//        2.2根据id添加工作经历
    List<EmpExpr> exprList = emp.getExprList();
    if(!CollectionUtils.isEmpty(exprList)){
        exprList.forEach(empExpr -> empExpr.setEmpId(emp.getId()));
        empExprMapper.add(exprList);
    }
}

Mapper

/**
 * 修改员工信息
 * @param emp
 */
void update(Emp emp);

EmpMapper.xml

<!--    set标签会自动生成set关键字; 自动删除更新字段后多余的,-->
<update id="update">
    UPDATE emp
    <set>
        <if test="username != null and username != ''">username = #{username},</if>
        <if test="name != null and name != ''">name = #{name},</if>
        <if test="gender != null">gender = #{gender},</if>
        <if test="phone != null and phone != ''">phone = #{phone},</if>
        <if test="job != null">job = #{job},</if>
        <if test="salary != null">salary = #{salary},</if>
        <if test="image != null and image != ''">image = #{image},</if>
        <if test="entryDate != null">entry_date = #{entryDate},</if>
        <if test="deptId != null">dept_id = #{deptId},</if>
        <if test="updateTime != null">update_time = #{updateTime},</if>
    </set>
    WHERE id = #{id}
</update>

16.全局异常处理器

使用@RestControllerAdvice修饰类

使用@ExceptionHandler修饰异常方法

exception/GlobalExceptionHandler.java

package com.itheima.exception;

import com.itheima.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler
    public Result handlerException(Exception e){
        log.error("捕获到异常: {}", e);
//        log.error("捕获到异常: {}", e.getMessage());  // 仅获取报错的异常信息
        return Result.error("出错啦,请联系管理员~");
    }


    @ExceptionHandler
    public Result handlerDuplicateKeyException(DuplicateKeyException e){
        log.error("捕获到异常: {}", e);
        String message = e.getMessage();
        int i = message.indexOf("Duplicate entry");
        String errMsg = message.substring(i);
        String[] s = errMsg.split(" ");
        return Result.error(s[2] + "已存在");
    }

}

17.员工职位人数统计

请求路径:/report/empJobData

请求方式:GET

接口描述:统计各个职位的员工人数

请求参数:

响应数据样例:

{
  "code": 1,
  "msg": "success",
  "data": {
    "jobList": ["教研主管","学工主管","其他","班主任","咨询师","讲师"],
    "dataList": [1,1,2,6,8,13]
  }
}

准备JobOption实体类

pojo/JobOption.java

package com.itheima.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class JobOption {
    private List jobList;
    private List dataList;
}

Controller

@GetMapping("/empJobData")
public Result getEmpJobData(){
    log.info("统计员工职位人数");
    JobOption jobOption = reportService.getEmpJobData();
    return Result.success(jobOption);
}

Service

/**
 * 查询员工职位人数
 * @return
 */
@Override
public JobOption getEmpJobData() {
//        1.调用EmpMapper查询员工职位,职位人数
    List<Map<String, Object>> list = empMapper.countEmpJobData(); // map: pos=班主任, num = 1

//        2.组装结果,并返回
    List<Object> jobList = list.stream().map(dataMap -> dataMap.get("pos")).toList();
    List<Object> dataList = list.stream().map(dataMap -> dataMap.get("num")).toList();

    return new JobOption(jobList, dataList);
}

Mapper

/**
 * 查询员工职位人数
 * @return
 */
@MapKey("pos")  // 防止mybatisx报错
List<Map<String, Object>> countEmpJobData();

EmpMapper.xml

<!--    统计员工职位人数-->
<select id="countEmpJobData" resultType="java.util.Map">
    select
    (case job when 1 then '班主任'
    when 2 then '讲师'
    when 3 then '学工主管'
    when 4 then '教研主管'
    when 5 then '咨询师'
    else '其他' end) pos,
    count(*) num
    from emp group by job order by num;
</select>

18.员工性别统计

请求路径:/report/empGenderData

请求方式:GET

接口描述:统计员工性别信息

请求参数:

响应数据样例:

{
  "code": 1,
  "msg": "success",
  "data": [
    {"name": "男性员工","value": 5},
    {"name": "女性员工","value": 6}
  ]
}

Controller

@GetMapping("/empGenderData")
public Result getEmpGenderData(){
    log.info("统计员工性别人数");
    List<Map<String, Object>> genderList =  reportService.getEmpGenderData();
    return Result.success(genderList);
}

Service

/**
 * 查询员工性别人数
 * @return
 */
@Override
public List<Map<String, Object>> getEmpGenderData() {
    return empMapper.getEmpGenderData();
}

Mapper

/**
 * 查询员工性别人数
 * @return
 */
@MapKey("gender")   // 防止mybatisx报错
List<Map<String, Object>> getEmpGenderData();

EmpMapper.xml

<!--    查询员工性别人数-->
<select id="getEmpGenderData" resultType="java.util.Map">
    select
    (
        if(gender = 1,'男性员工','女性员工')
    ) name,
    (count(*)) value
    from emp group by gender;
</select>

19.登录接口

请求路径:/login

请求方式:POST

接口描述:该接口用于员工登录Tlias智能学习辅助系统,登录完毕后,系统下发JWT令牌。

请求数据样例:

{
	"username": "jinyong",
    "password": "123456"
}

响应数据样例:

{
    "code": 1,
    "msg": "success",
    "data": {
        "id": 2,
        "username": "songjiang",
        "name": "宋江",
        "token": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJzb25namlhbmciLCJleHAiOjE2OTg3MDE3NjJ9.w06EkRXTep6SrvMns3w5RKe79nxauDe7fdMhBLK-MKY"
    }
}

准备工作

准备LoginInfo响应结果实体类

pojo/LoginInfo.java

package com.itheima.pojo;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfo {
    private Integer id;
    private String username;
    private String name;
    private String token;
}

准备JwtUtil工具类

utils/JwtUtil.java

package com.itheima.utils;

public class JwtUtil {

    // 签名密钥
    private static final String SECRET_KEY = "SnVuZ2xlenQ=";
    // 过期时间:12小时(毫秒)
    private static final long EXPIRATION = 12 * 60 * 60 * 1000;

    /**
     * 生成JWT令牌
     *
     * @param claims 要封装在token中的数据
     * @return 返回生成的JWT字符串
     */
    public static String generateJwt(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims) // 设置自定义声明
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)) // 设置过期时间
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 使用HMAC-SHA算法签名
                .compact(); // 构建JWT字符串
    }

    /**
     * 解析JWT令牌
     *
     * @param token 要解析的JWT字符串
     * @return 返回解析后的Claims对象
     */
    public static Claims parseJwt(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY) // 设置签名密钥
                .parseClaimsJws(token) // 解析JWT
                .getBody(); // 获取payload部分
    }
}

后续详细介绍JwtToken

Controller

package com.itheima.controller;

@Slf4j
@RestController
@RequestMapping("/login")
public class LoginController {

    @Autowired
    private EmpService empService;

    @PostMapping
    public Result login(@RequestBody Emp emp){
        log.info("登录信息: {}",emp);
        LoginInfo loginInfo = empService.login(emp);
//        1.如果返回数据不为空
        if(loginInfo != null){
            return Result.success(loginInfo);
        }
//        2.返回空数据
        return Result.error("用户名或密码错误");
    }
}

Service

/**
 * 用户登录
 * @param emp
 * @return
 */
@Override
public LoginInfo login(Emp emp) {
//        1.调用EmpMapper查询用户
    Emp e = empMapper.getByUsernameAndPassword(emp);

//        2.判断该用户是否存在,若存在,返回封装用户信息
    if(e != null){
        log.info("登录用户信息: {}",e);

        // 生成jwt token
        Map<String,Object> claims = new HashMap<>();
        claims.put("id",e.getId());
        claims.put("username",e.getUsername());
        String jwt = JwtUtil.generateJwt(claims);

        return new LoginInfo(e.getId(),e.getUsername(),e.getName(),jwt);
    }
//        3.不存在返回空
    return null;
}

Mapper

@Select("select id,username,name from emp where username = #{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);

20.会话技术介绍

现在前端常用的会话技术如下:

  • Cookie
  • Session
  • 令牌(Jwt)

1. 原理

  • 响应头Set-Cookie
  • 请求头Cookie

2. 优缺点

✅ 优点:

  • HTTP 协议原生支持,兼容性强

❌ 缺点:

  • 移动端 App 无法使用 Cookie
  • 不安全,用户可以手动禁用 Cookie
  • Cookie 不能跨域使用

Session 会话

1. 原理

  • Session 的底层依赖于 Cookie(通过 Set-CookieCookie 实现标识传递)

2. 优缺点

✅ 优点:

  • 数据存储在服务端,更加安全

❌ 缺点:

  • 在服务器集群环境下,Session 共享困难(需额外处理)
  • 仍然依赖 Cookie,因此继承了 Cookie 的部分缺点

令牌会话

优点 ✅

  • 支持 PC 端与移动端,适应性强
  • 能有效解决服务器集群环境下的认证问题
  • 减轻服务器端的存储压力(无状态)

缺点 ❌

  • 需要开发者自行实现令牌机制(如生成、验证、刷新等)

21.Jwt令牌

介绍

全称:JSON Web Token(https://jwt.io/)
定义了一种简洁的、自包含的格式,用于在通信双方以json的数据格式安全的传输信息

组成:

  • 第一部分:Hedaer(头),记录令牌类型签名算法等。例如: {"alg":"HS256","type","JWT"}
  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{"id":"1","username":"Tom"}
  • 第三部分: Signature(签名),放置Token被篡改、确保安全性。将header、payload融入,并加入指定密钥,通过指定签名算法计算而来。

具体形式如下:前两段使用Base64编码

eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTc0ODYwODQ5MH0.ZNBWg8-otaJyXtvfLVZMFKVIlI1F3DgsVrogVzoHTWA

想要使用Jwt需要引入依赖

<!--		jwt依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

Jwt测试

package com.itheima;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtTest {


    /**
     * 生成JWT令牌
     */
    @Test
    public void testGenerateJwt(){
//        定义Map对象
        Map<String, Object> dataMap = new HashMap<>();
        dataMap.put("id",1);
        dataMap.put("username","admin");
//        SnVuZ2xlenQ= 为 Junglezt的Base64编码
        String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "SnVuZ2xlenQ=")// 签名算法
                .addClaims(dataMap) // 添加数据
                .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) // 设置过期时间
                .compact();// 生成JWT

        System.out.println(jwt);
    }


    /**
     * 解析JWT令牌
     */
    @Test
    public void testParseJwt(){
        String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTc0NzEwNTc2OH0.EEDQSTuVKAI3iHevDnRioJqJibAkJkWE273YLwfDPcw";
        Claims claims = Jwts.parser().setSigningKey("SnVuZ2xlenQ=")
                .parseClaimsJws(jwt) // 解析JWT令牌
                .getBody(); // 获取令牌中自定义信息
        System.out.println(claims);
    }
}

JwtUtils

package com.itheima.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.Map;

public class JwtUtil {

    // 签名密钥
    private static final String SECRET_KEY = "SnVuZ2xlenQ=";
    // 过期时间:12小时(毫秒)
    private static final long EXPIRATION = 12 * 60 * 60 * 1000;

    /**
     * 生成JWT令牌
     *
     * @param claims 要封装在token中的数据
     * @return 返回生成的JWT字符串
     */
    public static String generateJwt(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims) // 设置自定义声明
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)) // 设置过期时间
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 使用HMAC-SHA算法签名
                .compact(); // 构建JWT字符串
    }

    
    
    /**
     * 解析JWT令牌
     *
     * @param token 要解析的JWT字符串
     * @return 返回解析后的Claims对象
     */
    public static Claims parseJwt(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY) // 设置签名密钥
                .parseClaimsJws(token) // 解析JWT
                .getBody(); // 获取payload部分
    }
}

提供了一个生成和解析Jwt令牌的方法

22.Filter过滤器

介绍

  • Filter过滤器,是JavaWeb三大组件(Servlet、Filter、Listener)之一。

  • 过滤器可以把对自愿的请求拦截下来,从而实现一些特殊的功能。

  • 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。

Filter快速入门

  1. 定义Filter:定义一个类,实现Filter接口,并实现其所有方法。
  2. 配置Filter:Filter类上加@WebFilter注解,配置拦截路径。引导类上加@ServletComponentScan开启Servlet组件支持。

filter/DemoFilter.java

package com.itheima.filter;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;

//@WebFilter("/*") // 拦截所有请求
@Slf4j
public class DemoFilter implements Filter {

    @Override
    // 初始化方法,启动时执行
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("init Filter初始化方法");
    }

    @Override
    // 过滤方法,每次请求时执行
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.info("拦截请求");
//        放行请求
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    // 销毁方法,关闭时执行
    public void destroy() {
        log.info("销毁方法");
    }
}

Filter令牌校验

filter/TokenFilter.java

package com.itheima.filter;

@WebFilter(urlPatterns = "/*")
@Slf4j
public class TokenFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
//        1.获取请求路径
        String requestURI = request.getRequestURI();

//        2.判断路径是否包含/login,如果是,放行
        if(requestURI.contains("/login")){
            log.info("登录操作,放行...");
            filterChain.doFilter(request,response);
            return;
        }

//        3.获取请求头中的token
        String token = request.getHeader("token");

//        4.判断token是否为空,返回401状态码
        if(token == null || token.isEmpty()){
            log.info("token为空");
            response.setStatus(401);
            return;
        }

//        5.校验token准确性,不通过返回401状态码
        try{
            JwtUtil.parseJwt(token);
        } catch (Exception e) {
            log.info("token校验不通过");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

//        6.放行
        log.info("token校验通过,放行...");
        filterChain.doFilter(request,response);

    }
}

23.Interceptor拦截器

介绍

  • 拦截器是一种动态拦截方法调用机制,类似于过滤器。Spring框架中提供,主要用来动态拦截控制器方法的执行。
  • 作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。

Interceptor快速入门

  1. 定义拦截器,实现HandlerInterceptor接口,并实现其所有方法。
  2. 注册拦截器

interceptor/DemoInterceptory.java

package com.itheima.interceptor;

@Slf4j
@Component
public class DemoInterceptor implements HandlerInterceptor {

    // 在controller执行之前执行,return true表示放行,false表示拦截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("执行preHandle...");
        return false;
    }


    // controller执行之后,渲染视图之前执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("执行postHandle...");
    }


    // 渲染视图之后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("执行afterCompletion");
    }
}

config/WebConfig.java

package com.itheima.config;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private DemoInterceptor demoInterceptor;

    // 拦截器注册
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 拦截所有请求
        registry.addInterceptor(demoInterceptor).addPathPatterns("/**");
    }
}

Interceptor令牌校验

定义拦截器

interceptor/TokenInterceptor.java

package com.itheima.interceptor;

@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        1.获取请求路径
        String requestURI = request.getRequestURI();

//        2.判断路径是否包含/login,如果是,放行
        if(requestURI.contains("/login")){
            log.info("登录操作,放行...");
            return true;
        }

//        3.获取请求头中的token
        String token = request.getHeader("token");

//        4.判断token是否为空,返回401状态码
        if(token == null || token.isEmpty()){
            log.info("token为空");
            response.setStatus(401);
            return false;
        }

//        5.校验token准确性,不通过返回401状态码
        try{
            JwtUtil.parseJwt(token);
        } catch (Exception e) {
            log.info("token校验不通过");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }

//        6.放行
        log.info("token校验通过,放行...");
        return true;
    }
}

注册拦截器

config/Webconfig.java

package com.itheima.config;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    private TokenInterceptor tokenInterceptor;

    // 拦截器注册
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 拦截所有请求
        registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");
    }
}

24.SpringAOP

AOP: Aspect Oriented Proggramming(面向切面编程、面相方面编程),可简单理解为就是面向特定的方法编程。

场景:案例中部分业务方法运行较慢,定位执行耗时较长的方法,此时需要统计每一个业务方法的执行耗时。

原始解决方案: 如果在默认场景,我们需要在每一个业务逻辑代码之前记录代码执行的时间,然后再业务逻辑之后再次记录代码执行的时间。

如果只有一两个业务方法还好,若是在项目中,几百上千个业务方法,就需要使用SpringAOP技术,减少重复代码,提高开发效率,方便业务维护。

AOP基础

统计所有业务层方法的执行耗时

  1. 导入依赖:在pom.xml中引入AOP依赖
<!-- spring-aop 依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  1. 编写AOP程序:针对于对顶的业务方法根据业务需要进行编程

aop/RecordTimeAspect.java

package com.itheima.aop;


import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect  // 标识当前是一个AOP类
@Slf4j
@Component
public class RecordTimeAspect {

    // 针对于 com.itheima.service.impl 包下的所有方法
    @Around("execution(* com.itheima.service.impl.*.*(..))")
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        // 1. 获取方法运行开始时间
        long begin = System.currentTimeMillis();

        // 2. 执行原始的方法
        Object result = pjp.proceed();

        // 3. 记录方法的结束时间,计算耗时
        long end = System.currentTimeMillis();
        // getSignature() 获取方法签名(方法执行的完整路径还有传参)
        log.info("方法{} 执行耗时:{}ms", pjp.getSignature(),end - begin);
        return result;

    }
}
posted @ 2025-11-29 09:25  Junglezt  阅读(17)  评论(0)    收藏  举报