Java Web - 后端

news/2025/2/15 0:22:12/文章来源:https://www.cnblogs.com/wxgmjfhy/p/18716420

Java Web - 后端

Maven

基于项目对象模型 (POM) 的概念, 通过一小段描述信息来管理项目的构建

官网

  • 各种插件以构建生命周期/阶段

  • 依赖管理模型从仓库中查找 jar包

    • 仓库: 存储资源, 管理 jar包
      • 本地仓库: 自己计算机上的一个目录
      • 中央仓库: 由 Maven 团队维护的仓库
      • 远程仓库 (私服): 一般由公司团队搭建的私有仓库
    • 先看本地仓库, 如果有则关联; 否则连接中央仓库, 从中央仓库下载到本地仓库再关联
    • 如果配置了私服, 则是 本地 -> 私服 -> 中央 的次序
  • 坐标

    • 资源的唯一标识, 通过坐标可以唯一定位资源位置
    • 使用坐标来定义项目或引入项目中需要的依赖
    • 坐标组成
      • groupId: 定义当前 Maven 项目隶属组织名称
      • artifactId: 定义当前 Maven 项目名称 (通常是模块名称, 如 order-service, goods-service)
      • version: 定义当前项目版本号
        • SNAPSHOT: 功能不稳定, 尚处于开发中的版本, 即快照版本
        • RELEASE: 功能趋于稳定, 当前更新停止, 可以用于发行的版本

VSCode 创建 Maven 项目

安装 Maven 以及插件后, 在 VSCode 中通过右键点击工作区文件夹, 利用 “Maven - 从 Maven 原型构建项目” 功能可以快速创建 Maven 项目

  1. 打开 VSCode 并打开工作区文件夹
    • 启动 VSCode 后, 选择 “文件” -> “打开文件夹”, 选中你希望创建 Maven 项目的目标文件夹。
  2. 从 Maven 原型构建项目
    • 在 VSCode 的资源管理器中, 右键点击已打开的工作区文件夹。
    • 在弹出的菜单中, 找到 “Maven - 从 Maven 原型构建项目” 选项并点击。
  3. 选择 Maven 原型
    • 点击该选项后, VSCode 会弹出一个输入框, 列出一系列可用的 Maven 原型。常见的原型如 maven - archetype - quickstart (用于创建简单的 Java 项目)、maven - archetype - webapp (用于创建 Web 项目)等。
    • 使用上下箭头键选择你需要的原型, 然后按回车键确认。
  4. 输入项目信息
    • 接下来, VSCode 会依次提示你输入项目的相关信息:
      • Group Id:通常为公司或组织的域名倒序, 例如 com.example
      • Artifact Id:项目的名称, 如 my - maven - project
      • Version:项目的版本号, 默认一般为 1.0 - SNAPSHOT, 可根据需要修改。
      • Package:项目的包名, 一般与 Group Id 相同。
    • 按照提示依次输入相应信息, 每输入完一项按回车键确认。
  5. 等待项目创建完成
    • 输入完所有信息后, Maven 会根据你选择的原型和输入的信息开始创建项目。这一过程可能需要一些时间, 具体取决于你的网络状况和计算机性能。
    • 项目创建完成后, 你可以在工作区文件夹中看到新创建的 Maven 项目结构, 包括 src 源代码目录、pom.xml 项目配置文件等。

以上内容由豆包生成, 根据指令选取了 maven - archetype - quickstart 原型, 创建了 Maven 项目, 有如下 pom.xml 文件:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.example</groupId><artifactId>demo</artifactId><version>1.0-SNAPSHOT</version><name>demo</name><!-- FIXME change it to the project's website --><url>http://www.example.com</url><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.source>2.1</maven.compiler.source><maven.compiler.target>2.1</maven.compiler.target></properties><dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.11</version><scope>test</scope></dependency></dependencies><build><pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) --><plugins><!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle --><plugin><artifactId>maven-clean-plugin</artifactId><version>3.1.0</version></plugin><!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging --><plugin><artifactId>maven-resources-plugin</artifactId><version>3.0.2</version></plugin><plugin><artifactId>maven-compiler-plugin</artifactId><version>3.8.0</version></plugin><plugin><artifactId>maven-surefire-plugin</artifactId><version>2.22.1</version></plugin><plugin><artifactId>maven-jar-plugin</artifactId><version>3.0.2</version></plugin><plugin><artifactId>maven-install-plugin</artifactId><version>2.5.2</version></plugin><plugin><artifactId>maven-deploy-plugin</artifactId><version>2.8.2</version></plugin><!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle --><plugin><artifactId>maven-site-plugin</artifactId><version>3.7.1</version></plugin><plugin><artifactId>maven-project-info-reports-plugin</artifactId><version>3.0.0</version></plugin></plugins></pluginManagement></build>
</project>

需要在 <properties> 中的 2.1 改为对应的 JDK 版本, 如 21

作用1: 依赖管理

方便快捷的管理项目依赖的资源 (jar包)

  • 如果要添加依赖 commons-io-2.18.0, 应当如下修改:
  <dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.11</version><scope>test</scope></dependency><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.18.0</version></dependency></dependencies>
  • 排除依赖: 主动断开依赖的资源, 被排除的资源无需指定版本

    • 添加 springframework 后, 如图
    • 排除依赖, 无需指定版本
    <dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>6.1.4</version><exclusions><exclusion><groupId>io.micrometer</groupId><artifactId>micrometer-observation</artifactId></exclusion></exclusions>
    </dependency>
    
    • 刷新后如图
  • 依赖范围

    • 默认在任何地方可以使用
    • 可以通过 <scope>...</scope> 设置作用范围
    • 作用范围
      • 主程序范围有效 (main 文件夹范围内)
      • 测试程序范围有效 (test 文件夹范围内)
      • 是否参与打包运行 (package 指令范围内)

作用2: 项目构建

标准化的跨平台 (Linux, Windows, MacOS) 的自动化项目构建方式

Lifecycle

  • compile: 对项目中所有源代码进行编译, target 文件夹下的 classes 文件夹存储了编译后的字节码文件
  • package: 对项目进行打包, target 文件夹会得到 jar包 文件

作用3: 统一项目结构

提供标准, 统一的项目结构

  • src 存放源代码
    • main 存放主程序核心代码
      • java 存放 java 源代码
      • resources 存放项目配置文件
    • test 存放测试相关代码
      • java 存放 java 文件
      • resources 存放测试相关的配置文件
    • pom.xml Maven 配置文件

生命周期

每套生命周期包含一些阶段, 阶段有顺序, 后面的阶段依赖于前面的阶段

主要关注 5 个阶段:

  • clean

    • ...
    • clean 移除上一次构建生成的文件
    • ...
  • default

    • ...
    • compile 编译项目源代码
    • ...
    • test 使用合适的单元测试框架进行测试 (junit)
    • ...
    • package 将编译后的文件打包, 如: jar, war 等
    • ...
    • install 安装项目到本地仓库
    • ...
  • site

    • ...

注意: 在同一套生命周期中, 当运行后面的阶段时, 前面的阶段都会进行

  • 两种执行方式
    • 工具栏
    • 命令行

单元测试

测试

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

  • 测试方法

    • 白盒测试
      • 清楚软件内部结构, 代码逻辑
      • 用于验证代码, 逻辑正确性
    • 黑盒测试
      • 不清楚软件内部结构, 代码逻辑
      • 用于验证软件的功能, 兼容性等
    • 灰盒测试
      • 结合白盒和黑盒
  • 阶段划分

    • 单元测试
      • 对软件的基本组成单位进行测试, 最小测试单位
      • 目的: 检验软件基本组成单位的正确性
      • 测试人员: 开发人员
      • 白盒测试
    • 集成测试
      • 将已经通过测试的单元, 按设计要求组合成系统或子系统, 再进行测试
      • 目的: 检验单元之间的协作是否正确
      • 测试人员: 开发人员
      • 灰盒测试
    • 系统测试
      • 对已经集成好的软件系统进行彻底的测试
      • 目的: 验证软件系统的正确性和性能是否满足指定的要求
      • 测试人员: 测试人员
      • 黑盒测试
    • 验收测试
      • 交付测试, 是针对用户需求, 业务流程进行的正式的测试
      • 目的: 验证软件系统是否满足验收标准
      • 测试人员: 客户/需求方
      • 黑盒测试

单元测试

  • main 方法测试的缺点

    • 测试代码和源代码未分开, 难以维护
    • 一个方法测试失败, 影响后面方法
    • 无法自动化测试, 得到测试报告
  • JUnit: 最流行的 Java 测试框架之一

    • 测试代码和源代码分开, 便于维护
    • 可根据需要进行自动化测试
    • 可自动分析测试结果, 产出测试报告
  • 案例

    • 引入依赖
    <dependencies><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.9.1</version><scope>test</scope></dependency>
    </dependencies>
    
    • 在 test/java 目录下, 创建测试类 UserServiceTest.java (命名规范: XxxxTest), 编写对应的测试方法 (JUnit 单元测试的方法必须声明为 public void), 并在方法上声明 @Test 注释
    @Test
    public void testGetAge() {Integer age = new UserService().getAge("1123534234235");System.out.println(age);
    }
    
    • 可以单独运行一个测试方法, 也可以运行所有测试方法
  • 断言 assert: 上述案例只检验了方法能不能运行, 但没检验方法的效果是否正常工作, 因此需要使用断言

    Assertions.assertEquals(Object exp, Object act, String msg)
    // 检验期望值 exp 和实际结果 act 是否一直, 不一致报错 msg
    // msg 可以传递, 也可以不传递
    // 还有 assertNotEquals, assertNull, assertNotNull, assertTrue, assertFalse, assertThrows
    
    • e.g.
    @Test
    public void testGetGender() {String gender = new UserService().getGender("1113513413");Assertions.assertEquals("男", gender);
    }
    

    • e.g.
    @Test
    public void testGetGender() {Assertions.assertThrows(IllegalArgumentException.class, () -> {new UserService().getGender(null);});
    }
    
  • 注解

    • @BeforeEach @BeforeAll 可以执行方法前初始化操作
    • @AfterEach @AfterAll 可以执行方法后释放资源
    • e.g.
    @DisplayName("测试用户性别")
    @ParameterizedTest
    @ValueSource(strings = {"102384932413431", "54324331212135"})
    public void testGetGender(String idCard) {String gender = new UserService.getGender(idCard);Assertions.assertEquals("男", gender);
    }
    
  • 测试规范

    • 尽可能覆盖业务方法中所有可能的情况, 尤其是边界值
    • 右键运行按钮, 还有覆盖率测试: 语句覆盖率, 方法覆盖率, 分支覆盖率
    • 可以使用 AI 生成测试类

常见问题

问题: 依赖报错

原因: 由于如网络原因, 依赖没有下载完整, 产生了 xxx.lastUpdated 文件, 文件不删除不会重新下载

方案:

  • 根据坐标, 手动删除 xxx.lastUpdated 文件, 再重新加载项目
  • 通过命令 del /s *.lastUpdated 批量递归删除当前目录下的 xxx.lastUpdated 文件, 再重新加载项目

Web 基础

  • 资源

    • 静态资源: 服务器上存储的不会改变的数据, 通常不会根据用户的请求而变化

    • 比如: HTML, CSS, JS, 图片, 视频等 (负责页面展示)

    • 动态资源: 服务器端根据用户请求和其他数据动态生成的, 内容可能会在每次请求时发生变化

    • 比如: Spring 等 (负责逻辑处理)

  • 架构

    • B/S 架构: 浏览器/服务器架构模式, 客户端只需浏览器, 应用程序的逻辑和数据都存在服务器端

      • 维护方便
      • 体验一般 (网速差的时候)
    • C/S 架构: 客户端/服务器架构模式, 需要单独开发维护客户端

      • 体验不错
      • 开发维护麻烦

Spring Boot

Spring 官网

Spring Boot 可以帮助我们非常快速的构建应用程序, 简化开发, 提高效率

Spring Boot Web 入门

需求: 浏览器发起请求 /hello 后, 给浏览器返回一个字符串 "Hello Xxx"

创建项目

安装插件后, 创建 Spring Boot 项目, 选择 Spring Boot 版本, 构建工具 Maven, 打包方式等, 注意 Java 版本不要选错, 否则 Java Projects 无法识别到创建的 Spring Boot 中的 Java 文件, 然后添加的依赖要选取 spring-web

创建后, 可以重启 VSCode, 然后 JAVA PROJECTS 和 Maven 都会识别到项目

项目 src/main/java/com.example 下自动生成一个启动类/引导类, 运行 main 方法可以启动项目

而在 resource 下, 有 static 文件夹, 用于存储静态资源相关的文件, templates 文件夹用于存储模板文件, 还有 Spring Boot 的核心配置文件 application.properties

编写请求处理类

package com.example.spring_boot_demo;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) {System.out.println("name: " + name);return "Hello " + name + "~";}
}

常见问题

如果 Spring 官网脚手架连接不上, 可以去连接阿里云 (暂时不需要)

起步依赖

  • 起步依赖 Starter:
    • spring-boot-starter-web: 包含 web 应用开发所需要的常见依赖
    • spring-boot-starter-test: 包含了单元测试所需要的常见依赖

main 方法启动了依赖引入的内置 Tomcat 服务器, 因此才可以运行整个程序

HTTP 协议

超文本传输协议, 规定了浏览器和服务器之间数据传输的规则

  • 基于 TCP 协议: 面向连接, 安全
  • 基于请求-响应模型: 一次请求对应一次响应
  • HTTP 协议是无状态的协议: 对于事务处理没有记忆能力, 每次请求-响应都是独立的
    • 缺点: 多次请求间不能共享数据
    • 优点: 速度快

请求协议

  • 格式

    • 请求行 (请求数据第一行)
      • 请求方式
      • 访问路径
      • 协议与协议版本
    • 请求头 (第二行开始, 格式为 key: value)
      • Host: 请求的主机名
      • User-Agent: 浏览器版本
      • Accept: 浏览器能接收的资源类型
      • Accept-Language: 浏览器偏好的语言
      • Accept-Encoding: 浏览器可以支持的压缩类型
      • Content-Type: 请求主体的数据类型
      • Content-Length: 请求主体的大小
    • 请求体 (GET 请求没有, POST 请求才有, 用于存放请求参数)
  • 请求数据获取: Web 服务器 (Tomcat) 对 HTTP 协议的请求数据进行解析, 并进行了封装 (封装为 HttpServletRequest 对象), 在调用 Controller 方法的时候传递给了该方法, 这样程序员不必直接对协议进行操作, 开发更便捷

package com.example.spring_boot_demo;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import jakarta.servlet.http.HttpServletRequest;@RestController
public class RequestController {@RequestMapping("/request")public String request(HttpServletRequest request) {// 获取请求参数的 nameString name = request.getParameter("name");System.out.println(name);// 获取请求路径的 uri 和 urlString uri = request.getRequestURI(); // /requestSystem.out.println(uri);String url = request.getRequestURL().toString(); // http://localhost:8080/requestSystem.out.println(url);// 获取请求头 User-AgentString userAgent = request.getHeader("User-Agent");System.out.println(userAgent);// 获取请求方式String method = request.getMethod(); // GETSystem.out.println(method);// 获取请求协议String protocol = request.getProtocol(); // HTTP/1.1System.out.println(protocol);// 获取请求的查询字符串String queryString = request.getQueryString();System.out.println(queryString);return "requests success";}
}

响应协议

  • 格式

    • 响应行 (请求数据第一行)
      • 协议与协议版本
      • 状态码
        • 1xx 响应中, 临时状态码
        • 2xx 成功
          • 比如 200
        • 3xx 重定向
          • 例如用 http 访问百度, 百度返回 307 以及一个 location 给浏览器, 浏览器就会去访问 location, 即 https://www.baidu.com/
        • 4xx 客户端错误
          • 比如 404 请求资源不存在
        • 5xx 服务器错误
          • 比如 500 服务器端发送错误
      • 描述
    • 响应头 (第二行开始, 格式为 key: value)
      • Content-Type: 响应内容的类型
      • Content-Length: 相应内容的长度
      • Content-Encoding: 响应压缩算法
      • Cache-Control: 指示客户端如何缓存
      • Set-Cookie: 告诉浏览器对当前页面所在的域设置 cookie
    • 响应体 (存放响应数据)
  • 响应数据设置: Web 服务器对 HTTP 协议的响应数据进行了封装 (HttpServletResponse), 在调用 Controller 方法的时候传递给了该方法, 这样程序员不必直接对协议进行操作, 开发更便捷

package com.example.spring_boot_demo;import org.springframework.web.bind.annotation.RestController;import java.io.IOException;import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;import jakarta.servlet.http.HttpServletResponse;@RestController
public class ResponseController {/** 方式 1: HttpServletResponse*/@RequestMapping("/response")public void response(HttpServletResponse response) throws IOException {// 设置响应状态码response.setStatus(HttpServletResponse.SC_OK);// 设置响应头response.setHeader("name", "wxgmjfhy");// 设置响应体response.getWriter().write("<h1>hello response</h1>");}/** 方式 2: Spring 提供的 ResponseEntity*/@RequestMapping("/response2")public ResponseEntity<String> response2() {return ResponseEntity.status(401) // 设置响应状态码.header("name", "wxgmjfhy") // 设置响应头.body("<h1>hello response</h1>"); // 设置响应体}
}

注意: 响应状态码和响应头如果没有特殊要求的话, 通常不手动设定, 服务器会根据请求处理的逻辑, 自动设置

案例

  • User.java 存放于创建的文件夹 pojo 下
package com.example.spring_boot_demo2.pojo;import java.time.LocalDateTime;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/** 用户信息*/@Data // 生成 setXxx, getXxx
@NoArgsConstructor // 生成空参构造
@AllArgsConstructor // 生成全参构造
public class User {private Integer id;private String username;private String password;private String name;private Integer age;private LocalDateTime updateTime;
}
  • UserController 存放于创建的文件夹 controller 下
package com.example.spring_boot_demo2.controller;import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import com.example.spring_boot_demo2.pojo.User;import cn.hutool.core.io.IoUtil;/** 用户信息 Controller*/
@RestController
public class UserController {@RequestMapping("/list")public List<User> list() throws Exception {// 加载并读取 user.txt 文件, 获取用户数据InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");ArrayList<String> lines = IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList<>());// 解析用户信息, 封装为 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();// 返回 json 格式数据 (自动转化)return userList;}
}
  • user.txt 存放于 resources 下
1,daqiao,1234567890,大乔,22,2024-07-15 15:05:45
2,xiaoqiao,1234567890,小乔,18,2024-07-15 15:12:09
  • user.html 存放于 resources/static 下 (内容由豆包生成)
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>用户信息列表</title><style>table {border-collapse: collapse;width: 100%;}th,td {border: 1px solid #ddd;padding: 8px;text-align: left;}th {background-color: #f2f2f2;}</style>
</head><body><h1>用户信息列表</h1><table id="userTable"><thead><tr><th>ID</th><th>用户名</th><th>密码</th><th>姓名</th><th>年龄</th><th>更新时间</th></tr></thead><tbody><!-- 这里将动态插入用户数据 --></tbody></table><script>document.addEventListener('DOMContentLoaded', function () {// 发起请求获取用户数据fetch('/list').then(response => {if (!response.ok) {throw new Error(`HTTP error! status: ${response.status}`);}return response.json();}).then(data => {const tbody = document.getElementById('userTable').getElementsByTagName('tbody')[0];data.forEach(user => {const row = document.createElement('tr');Object.values(user).forEach(value => {const cell = document.createElement('td');cell.textContent = value;row.appendChild(cell);});tbody.appendChild(row);});}).catch(error => {console.error('获取用户数据时出错:', error);});});</script>
</body></html>

运行程序, 访问 localhost:8080/user.html, 效果如图

  • 返回的 List<User> 如何自动转成 json 格式
    • @RestController 注解封装了 @ResponseBody 注解
    • @ResponseBody 作用是将 controller 返回值直接作为响应体的数据直接响应
    • 返回值是对象/集合时, 会先转成 json 再响应

分层解耦

三层架构

UserController 中, 数据访问, 逻辑处理, 接受请求, 响应数据都写在一个方法里了, 不符合单一职责原则, 复用性差, 可维护性差

  • 三层架构

    • Controller: 控制层, 接受前端发送的请求, 对请求进行处理, 并响应数据
    • Service: 业务逻辑层, 处理具体的业务逻辑
    • Dao: 数据访问层 (持久层), 负责数据访问操作, 包括数据的增, 删, 改, 查
  • Dao

    • UserDao 接口
    package com.example.spring_boot_demo2.dao;import java.util.List;public interface UserDao {/** 加载用户数据*/public List<String> findAll();}
    
    • UserDaoImpl 实习类
    package com.example.spring_boot_demo2.dao.impl;import java.io.InputStream;
    import java.nio.charset.StandardCharsets;
    import java.util.ArrayList;
    import java.util.List;import com.example.spring_boot_demo2.dao.UserDao;import cn.hutool.core.io.IoUtil;public class UserDaoImpl implements UserDao {@Overridepublic List<String> findAll() {// 加载并读取 user.txt 文件, 获取用户数据InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");ArrayList<String> lines = IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList<>());return lines;}
    }
    
  • Service

    • UserService 接口
    package com.example.spring_boot_demo2.service;import java.util.List;import com.example.spring_boot_demo2.pojo.User;public interface UserService {/** 查找所有用户信息*/public List<User> findAll();}
    
    • UserServiceImpl 实现类
    package com.example.spring_boot_demo2.service.impl;import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.List;import com.example.spring_boot_demo2.dao.UserDao;
    import com.example.spring_boot_demo2.dao.impl.UserDaoImpl;
    import com.example.spring_boot_demo2.pojo.User;
    import com.example.spring_boot_demo2.service.UserService;public class UserServiceImpl implements UserService {private UserDao userDao = new UserDaoImpl();@Overridepublic List<User> findAll() {// 调用 dao, 获取数据List<String> lines = userDao.findAll();// 解析用户信息, 封装为 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();return userList;}
    }
    
  • Controller

    • UserController
    package com.example.spring_boot_demo2.controller;import java.util.List;import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;import com.example.spring_boot_demo2.pojo.User;
    import com.example.spring_boot_demo2.service.UserService;
    import com.example.spring_boot_demo2.service.impl.UserServiceImpl;/*
    * 用户信息 Controller
    */
    @RestController
    public class UserController {private UserService userService = new UserServiceImpl();@RequestMapping("/list")public List<User> list() throws Exception {// 调用 service, 获取数据List<User> userList = userService.findAll();// 返回 json 格式数据 (自动转化)return userList;}
    }
    

IOC 与 DI

  • 耦合: 衡量软件中各个层/各个模块的依赖关联程度
  • 内聚: 软件中各个功能模块内部的功能联系
  • 软件设计原则: 高内聚低耦合

之前我们在编写代码时, 需要什么对象, 就直接 new 一个就可以了, 这样层与层之间代码就耦合了, 当 service 层的实现变了之后, 我们还需要修改 controller 层的代码

解决方案是: 将要用到的对象交给一个容器管理, 应用程序中用到这个对象, 就直接从容器中获取

而又有问题: 如果将对象交给容器管理? 容器如何为程序提供依赖的对象?

  • 控制反转 IOC
    • 对象的创建控制权由程序自身转移到外部 (容器)
  • 依赖注入 DI
    • 容器为应用程序提供运行时, 所依赖的资源
  • bean 对象
    • IOC 容器中创建, 管理的对象

因此此处需要:

  • 将 Dao 和 Service 层的实现类, 交给 IOC 容器管理

    • 在实现类上方加 @Component
    @Component
    public class UserServiceImpl implements UserService {}
    
  • 为 Controller 和 Service 注入运行时所依赖的对象

    • 不再 new 一个对象, 而是在成员变量上面加 @Autowired
    @Autowired
    private UserDao userDao;
    

要把某个对象交给 IOC 容器管理, 需要在对应的类上加上以下注解之一:

  • @Component: 声明 bean 的基础注解
  • @Controller: @Component 的衍生注解, 标注在控制层类上
  • @Service: @Component 的衍生注解, 标注在业务层类上
  • @Repository: @Component 的衍生注解, 标注在数据访问层类上

这四个注解要生效, 需要被 组件扫描注解 @ComponentScan 扫描

这个注解没有显示配置, 但实际包含在启动类声明注解 @SpringBootApplication 中, 默认的扫描范围是启动类所在包及其子包

基于 @Autowired 进行依赖注入的常见方式有三种:

  • 属性注入
@RestController
public class UserController {@Autowiredprivate UserService userService;
}
  • 构造函数注入
@RestController
public class UserController {private final UserService userService;@Autowiredpublic UserController(UserService userService) {this.userService = userService;}
}
  • setter 注入
@RestController
public class UserController {private UserService userService;@Autowiredpublic void setUserService(UserService userService) {this.userService = userService;}
}

当 IOC 容器中存在多个相同类型的 bean 对象:

  • @Primary 确定默认的实现
@Primary
@Service
public class UserServiceImpl implements UserService {}
  • @Qualifier 指定注入的 bean 的名称
@RestController
public class UserController {@Qualifier("userServiceImpl")@Autowiredprivate UserService userService;}
  • @Resource 指定注入的 bean 的名称
@RestController
public class UserController {@Resource(name = "userServiceImpl")private UserService userService;}

个人感想: 还是有耦合, 不过 @Primary 耦合程度较低, 然后 bean 的名称写在注解里可能比在代码里更为清晰明了

SpringBoot 配置文件

properties 文件太臃肿, 层级结构不清晰, 因此要用 yaml/yml 文件

  • yml 文件: 简洁, 以数据为中心
spring:application:name: mybatis_demodatasource:url: jdbc:mysql://localhost:3306/db02driver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: xxxxmybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  • 格式

    • 数值前面必须有空格, 作为分隔符
    • 使用缩进表示层级关系, 缩进时, 不允许使用 Tab 键, 只能用空格 (VSCode 选取"使用空格缩进")
    • 缩进的空格数目不重要, 只能相同层级的元素左侧对齐即可
    • # 表示一行注释
  • 定义对象 / Map 集合

user:name: 张三age: 18password: 123456
  • 定义数组 / List / Set 集合
hobby:- java- game- sport

注意: 配置项的值是以 0 开头的, 值需要用 '' 引起来, 因为 0 开头在 yml 中表示 8 进制的数据

MySQL

数据库 DB: 存储和管理数据的仓库

数据库管理系统 DBMS: 操纵和管理数据库的大型软件

SQL: 操作关系型数据库的编程语言, 定义了一套关于操作关系型数据库的统一标准

安装与启动

网址

下载了 8.4.4 LTS 版本的 zip 文件, 根据 安装文档 操作

MySQL 服务器启动完毕后, 然后再使用如下指令, 来连接 MySQL 服务器:

mysql -u用户名 -p密码 [-h数据库服务器的IP地址 -P端口号]
  • -h 参数不加, 默认连接的是本地 127.0.0.1 的 MySQL 服务器
  • -P 参数不加, 默认连接的端口号是 3306

上述的 MySQL 服务器我们是安装在本地的, 这个仅仅是在我们学习阶段, 在真实的企业开发中, MySQL 数据库服务器是不会在我们本地安装的, 是在公司的服务器上安装的, 而服务器还需要放置在专门的 IDC 机房中的, IDC 机房需要保证恒温、恒湿、恒压, 而且还要保证网络、电源的可靠性(备用电源及网络); 我们要想使用服务器上的这台 MySQL 服务器, 就需要在我们的电脑上去远程连接这台 MySQL

  • 通过 MySQL 的客户端命令行, 连接服务器上部署的 MySQL:
mysql [-h数据库服务器的IP地址 -P端口号] -u用户名 -p密码

数据模型

关系型数据库: 建立在关系模型基础上, 由多张相互连接的二维表组成的数据库

特点:

  • 使用表存储数据, 格式统一, 便于维护
  • 使用 SQL 语言操作, 标准统一, 使用方便, 可用于复杂查询

  • 创建数据库
    mysql> create database db01;
    
    • 如果指令错误, 比如把 ; 输成了 :, 可以取消指令
    mysql> create database db01:-> \c
    mysql> create database db01;
    
    • 会在 data 文件夹下生成一个 db01 空文件夹

SQL 语句

分类:

  • DDL 数据定义语言, 用来定义数据库对象 (数据库, 表, 字段)
  • DML 数据操作语言, 用来对数据库表中的数据进行增删改
  • DQL 数据查询语言, 用来查询数据库中表的记录
  • DCL 数据控制语言, 用来创建数据库用户, 控制数据库的访问权限

DDL

数据定义语言, 用来定义数据库对象 (数据库, 表, 字段)

库操作
-- 查询所有数据库
show databases;-- 查询当前数据库
select database();-- 使用/切换数据库
use 数据库名;-- 创建数据库, 可以指定字符集 (MySQL8 版本中默认 utf8mb4)
create database [if not exists] 数据库名 [default charset utf8mb4];-- 删除数据库
drop database [if exists] 数据库名;

上述语法中的 database, 也可以替换成 schema

  • MySQL 客户端工具-图形化工具: 暂时选用了 VSCode 上 MySQL 插件 (Weijan Chen)
表操作
  • 创建表:
create table tablename(字段1 字段类型 [约束] [comment 字段1注释]...字段2 字段类型 [约束] [comment 字段2注释]
)[comment 表注释];
  • e.g. (还没有约束)
create table user(id INT COMMENT 'ID, 唯一标识',username VARCHAR(50) COMMENT '用户名',name VARCHAR(10) COMMENT '姓名',age INT COMMENT '年龄',gender CHAR(1) COMMENT '性别'
) COMMENT '用户信息表';
  • 约束: 作用于表中字段上的规则, 用于限制存储在表中的数据

    • 目的: 保证数据库中数据的正确性, 有效性和完整性
    • 比如:
      • id 唯一
      • username 非空, 唯一
      • name 非空
      • gender 默认男
  • 常见五种约束

    • 非空约束: 限制字段值不能为 null, 关键字 not null

    • 唯一约束: 保证字段的所有数据都是唯一, 不重复的, 关键字 unique

    • 主键约束: 主键是一行数据的唯一标识, 要求非空且唯一, 关键字 primary key

    • 默认约束: 保存数据时如果未指定该字段值, 则采取默认值, 关键字 default

    • 外键约束: 让两张表的数据建立连接, 保证数据的一致性和完整性, 关键字 foreign key

    • e.g.

    create table user(id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID, 唯一标识',-- 主键约束 AUTO_INCREMENT 使得自增, 无需填写username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',-- 非空且唯一, 多个约束空格分开即可name VARCHAR(10) NOT NULL COMMENT '姓名',-- 非空age INT COMMENT '年龄',-- 无约束gender CHAR(1) DEFAULT '男' COMMENT '性别'-- 默认
    ) COMMENT '用户信息表';
    
  • 数据类型

    • 数值类型
      • 数值类型的选取原则: 在满足业务需求的情况下, 尽可能选择占用磁盘空间小的
      • e.g.
      -- 年龄字段 ---不会出现负数, 而且人的年龄不会太大
      age tinyint unsigned-- 分数 ---总分100分, 最多出现一位小数
      score double(4,1)
      
    • 字符串类型
      • 定长则固定占用指定的字符空间
        • 性能略高
        • 浪费磁盘空间
        • e.g. idcard, phone 都是固定长度的
      • 变长则最多占用指定的字符空间, 空间动态分配
        • 性能略低
        • 节约磁盘空间
        • e.g. username 不是固定长度的
    • 日期类型
      • e.g. birthday -> data, operateTime -> datetime
  • 表结构偶设计-案例

    • 设计表流程:
      • 阅读页面原型及需求文档
      • 基于页面原则和需求文档 确定原型字段 (类型, 长度限制, 约束)
      • 再增加表设计所需要的业务基础字段 (id, create_time, update_time)
        • create_time: 记录的是当前这条数据插入的时间
        • update_time: 记录当前这条数据最后更新的时间
    • 需求如图, 此外文档还要求员工默认密码 123456 (暂时不考虑归属部门)
    • 创建表
    CREATE TABLE emp(id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT 'ID, 主键', -- 业务基础字段username VARCHAR(20) NOT NULL UNIQUE COMMENT '用户名',password VARCHAR(32) NOT NULL 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 '入职日期',create_time DATETIME COMMENT '创建时间', -- 业务基础字段update_time DATETIME COMMENT '修改时间' -- 业务基础字段
    ) COMMENT '员工表';
    
  • 查询

show tables; -- 查询当前数据库的所有表desc 表名; -- 查询表结构show create table 表明; -- 查询建表语句

  • 修改
alter table 表名 add 字段名 类型(长度) [comment 注释] [约束]; -- 添加字段
alter table 表名 modify 字段名 新数据类型(长度); -- 修改字段类型
alter table 表名 change 旧字段名 新字段名 类型(长度) [comment 注释] [约束]; -- 修改字段与字段类型
alter table 表名 rename to 新表名; -- 修改表名
  • 删除
alter table 表名 drop column 字段名; -- 删除字段
drop table [if exists] 表名; -- 删除表, 同时删除表的数据
  • 图形化工具: 关于表结构的查看、修改、删除操作, 工作中一般都是直接基于图形化界面操作
    • 点击 emp 右侧 设计表 选项
    • 点击 + 号新建列
    • 编辑字段, 删除...等等

DML

数据操作语言, 用来对数据库表中的数据进行增删改

  • insert

    -- 指定字段添加数据
    insert into 表名 (字段1, 字段2) values (值1, 值2);
    -- 全部字段添加数据
    insert into 表名 values (值1, 值2, ...);
    -- 批量添加数据 (指定字段)
    insert into 表名 (字段1, 字段2) values (值1, 值2), (值1, 值2);
    -- 批量添加数据 (全部字段)
    insert into 表名 values (值1, 值2, ...), (值1, 值2, ...);
    
    • e.g. (now() 获取当前系统时间)
    INSERT INTO emp (id, username, password, name, gender, phone, job, salary, image, entry_date, create_time, update_time) VALUES (null, 'Jack', '12345678', '杰克', 1, '13300001111', 1, 6000, '1.jpg', '2020-01-01', now(), now());
    
  • update

    update 表名 set 字段名1 = 值1, 字段名2 = 值2, ... [where 条件];
    
    • e.g. 将 emp 表中 id 为 1 的员工, name 字段更新为'张三'
    update emp set name = '张三', update_time = now() where id = 1;
    
    • e.g. 将 emp 表的所有员工 (不带 where 条件, 会有警告) 入职日期更新为'2010-01-01'
    update emp set entry_date = '2010-01-01', update_time = now();
    
  • delete

    delete from 表名 [where 条件];
    
    • e.g. 删除 emp 表中 id 为 1 的员工
    delete from emp where id = 1;
    
    • e.g. 删除 emp 表中所有员工 (不带 where 条件, 会有警告)
    delete from emp;
    
    • delete 不能删除某一个字段的值 (可以使用 update 将该字段置为 null 即可)

DQL

数据查询语言, 用来查询数据库中表的记录

select字段列表
from表名列表
where条件列表
group by分组字段列表
having分组后条件列表
order by排序字段列表
limit分页参数
基本查询
-- 查询多个字段
select 字段1, 字段2, 字段3 from 表名;
-- 查询所有字段
select * from 表名;
-- 为查询字段设置别名, as 可以省略
select 字段1 [as 别名1], 字段2 [as 别名2] from 表名;
-- 去重查询
select distinct 字段列表 from 表名;
  • 删除原来的数据, 重置自增计数器, 添加 30 条数据 (由豆包生成)
DELETE FROM emp;ALTER TABLE emp AUTO_INCREMENT = 1;INSERT INTO emp (id, username, password, name, gender, phone, job, salary, image, entry_date, create_time, update_time) 
VALUES 
(null, 'Jack', '12345678', '杰克', 1, '13300001111', 1, 6000, '1.jpg', '2020-01-01', now(), now()),
(null, 'Alice', '12345678', '爱丽丝', 2, '13300002222', 2, 7000, '2.jpg', '2020-02-01', now(), now()),
(null, 'Bob', '12345678', '鲍勃', 1, '13300003333', 1, 6500, '3.jpg', '2020-03-01', now(), now()),
(null, 'Charlie', '12345678', '查理', 1, '13300004444', 3, 8000, '4.jpg', '2020-04-01', now(), now()),
(null, 'David', '12345678', '大卫', 1, '13300005555', 2, 7500, '5.jpg', '2020-05-01', now(), now()),
(null, 'Eve', '12345678', '伊芙', 2, '13300006666', 1, 6200, '6.jpg', '2020-06-01', now(), now()),
(null, 'Frank', '12345678', '弗兰克', 1, '13300007777', 3, 8500, '7.jpg', '2020-07-01', now(), now()),
(null, 'Grace', '12345678', '格蕾丝', 2, '13300008888', 2, 7200, '8.jpg', '2020-08-01', now(), now()),
(null, 'Henry', '12345678', '亨利', 1, '13300009999', 1, 6300, '9.jpg', '2020-09-01', now(), now()),
(null, 'Ivy', '12345678', '艾薇', 2, '13300010000', 3, 8200, '10.jpg', '2020-10-01', now(), now()),
(null, 'Jacky', '12345678', '杰基', 1, '13300011000', 2, 7300, '11.jpg', '2020-11-01', now(), now()),
(null, 'Kelly', '12345678', '凯莉', 2, '13300012000', 1, 6400, '12.jpg', '2020-12-01', now(), now()),
(null, 'Leo', '12345678', '利奥', 1, '13300013000', 3, 8300, '13.jpg', '2021-01-01', now(), now()),
(null, 'Mia', '12345678', '米娅', 2, '13300014000', 2, 7400, '14.jpg', '2021-02-01', now(), now()),
(null, 'Noah', '12345678', '诺亚', 1, '13300015000', 1, 6600, '15.jpg', '2021-03-01', now(), now()),
(null, 'Olivia', '12345678', '奥利维亚', 2, '13300016000', 3, 8400, '16.jpg', '2021-04-01', now(), now()),
(null, 'Peter', '12345678', '彼得', 1, '13300017000', 2, 7600, '17.jpg', '2021-05-01', now(), now()),
(null, 'Quinn', '12345678', '奎因', 2, '13300018000', 1, 6700, '18.jpg', '2021-06-01', now(), now()),
(null, 'Ryan', '12345678', '瑞安', 1, '13300019000', 3, 8600, '19.jpg', '2021-07-01', now(), now()),
(null, 'Sophia', '12345678', '索菲亚', 2, '13300020000', 2, 7700, '20.jpg', '2021-08-01', now(), now()),
(null, 'Tom', '12345678', '汤姆', 1, '13300021000', 1, 6800, '21.jpg', '2021-09-01', now(), now()),
(null, 'Uma', '12345678', '乌玛', 2, '13300022000', 3, 8700, '22.jpg', '2021-10-01', now(), now()),
(null, 'Victor', '12345678', '维克多', 1, '13300023000', 2, 7800, '23.jpg', '2021-11-01', now(), now()),
(null, 'Wendy', '12345678', '温迪', 2, '13300024000', 1, 6900, '24.jpg', '2021-12-01', now(), now()),
(null, 'Xavier', '12345678', '泽维尔', 1, '13300025000', 3, 8800, '25.jpg', '2022-01-01', now(), now()),
(null, 'Yvonne', '12345678', '伊冯娜', 2, '13300026000', 2, 7900, '26.jpg', '2022-02-01', now(), now()),
(null, 'Zach', '12345678', '扎克', 1, '13300027000', 1, 7100, '27.jpg', '2022-03-01', now(), now()),
(null, 'Ava', '12345678', '阿瓦', 2, '13300028000', 3, 8900, '28.jpg', '2022-04-01', now(), now()),
(null, 'Ben', '12345678', '本', 1, '13300029000', 2, 8100, '29.jpg', '2022-05-01', now(), now()),
(null, 'Cara', '12345678', '卡拉', 2, '13300030000', 1, 7200, '30.jpg', '2022-06-01', now(), now());
  • emp. 可省略

  • 起别名 (别名中间没有空格时可以不加引号) (as 可以省略)

  • 去重查询

条件查询
select 字段列表 from 表名 where 条件列表;

SELECT * from emp where name = '杰克';select * from emp where salary <= 7000;select * from emp WHERE job is not null;SELECT * FROM emp WHERE password = '12345678';SELECT * FROM emp WHERE (entry_date BETWEEN '2020-01-01' AND '2020-06-01') && (gender = 2);SELECT * FROM emp WHERE job = 2 || job = 3;SELECT * FROM emp WHERE job in (2, 3); -- 等效上一个-- 查询姓名为两个字的员工信息 (_: 单个字符; %: 任意个字符)
SELECT * FROM emp WHERE name like '__';-- 查询姓'杰'的员工信息
SELECT * FROM emp WHERE name like '杰%';-- 查询姓名包含'克'的员工信息
SELECT * FROM emp WHERE name like '%克%';

聚合函数

聚合函数: 将一列数据作为一个整体, 进行纵向计算

  • 统计员工数量
-- count(字段)
SELECT COUNT(emp.id) FROM emp; -- 注意: 所有的聚合函数都不参与 null 的统计, 因此如果统计的不是 id, 可能数量有误-- count(*)
SELECT COUNT(*) FROM emp; -- 推荐, 底层有优化-- count(常量)
SELECT COUNT(1) FROM emp;
SELECT COUNT(0) FROM emp;
SELECT COUNT('A') FROM emp;
  • 统计薪资
SELECT avg(emp.salary) FROM emp;SELECT max(emp.salary) FROM emp;SELECT min(emp.salary) FROM emp;SELECT sum(emp.salary) FROM emp;
分组查询
select 字段列表 from 表名 [where 条件列表] group by 分组字段名 [having 分组后过滤条件];
  • where 与 having 区别

    • 执行时机不同: where 是分组之前进行过滤, 不满足 where 条件则不参与分组; having 是分组之后对结果进行过滤
    • 判断条件不同: where 不能对聚合函数进行判断, having 可以
  • 注意: 分组之后, select 后的字段列表不能随意书写, 能写的一般是 分组字段 + 聚合函数

SELECT gender, count(*) FROM emp GROUP BY gender;

  • 查询入职时间在 '2020-06-01' 及以前, 并对结果根据职位分组, 获取员工数量 >=2 的职位
SELECT job, count(*) whe FROM emp WHERE entry_date <= '2020-06-01' GROUP BY job HAVING count(*) >= 2;
-- 注意: where 不能对聚合函数进行判断, having 可以
排序查询
select 字段列表 from 表民 [where 条件列表] [group by 分段字段名 having 分组后的过滤条件] order by 排序字段 排序方式;
  • 排序方式:
    • 升序 (asc), 降序 (desc); 默认升序, 可不写
    • 多字段排序, 第一个字段相同时才对第二个字段排序
SELECT * FROM emp ORDER BY entry_date; -- SELECT * FROM emp ORDER BY entry_date asc;SELECT * FROM emp ORDER BY entry_date desc;SELECT * FROM emp ORDER BY entry_date desc, salary desc;SELECT * FROM emp ORDER BY entry_date, salary desc; -- SELECT * FROM emp ORDER BY entry_date asc, salary desc;
分页查询
select 字段列表 from 表民 [where 条件列表] [group by 分段字段名 having 分组后的过滤条件] [order by 排序字段 排序方式] limit 起始索引, 查询记录数;

注意:

  • 起始索引从 0 开始

  • 分页查询是数据库的方言, 不同的数据库有不同的实现

  • 如果起始索引为 0, 起始索引可以省略, 直接简写为 limit 查询记录数

  • e.g. 每页查询记录数为 5

SELECT * FROM emp LIMIT 0, 5

JDBC

JDBC: 使用 Java 语言操作关系型数据库的一套 API

MyBatis: 基于 JDBC 封装的高级框架

入门程序

  • 添加依赖: MySQL 是 8.4.4 版本, 不清楚 8.0.33 版本能否兼容, 因此选择了 8.3.0 版本
<dependencies><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><version>8.3.0</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.9.3</version><scope>test</scope></dependency>
</dependencies>
  • 编写程序
package com.example;import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;import org.junit.jupiter.api.Test;public class JdbcTest {@Testpublic void testUpdate() throws Exception {// 注册驱动Class.forName("com.mysql.cj.jdbc.Driver");// 获取数据库连接String url = "jdbc:mysql://localhost:3306/db02";String username = "root";String password = "xxxx";Connection connection = DriverManager.getConnection(url, username, password);// 获取 SQL 语句执行对象Statement statement = connection.createStatement();// 执行 SQLint i = statement.executeUpdate("update user set age = 25 where id = 1"); // DMLSystem.out.println("SQL 语句执行完毕影响的记录数为: " + i);// 释放资源statement.close();connection.close();}
}
  • 如果连接不上数据库, 注意检查 SQL 服务是否启动

执行 DQL 语句

  • DQL 语句, 要求将查询的结果封装到 User 中
select * from user where username = 'daqiao' and password = '123456'
  • User
package com.example.pojo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {private Integer id;private String username;private String password;private String name;private Integer age;
}
@Test
public void testSelect() {String url = "jdbc:mysql://localhost:3306/db02";String username = "root";String password = "xxxx";Connection conn = null;PreparedStatement stmt = null;ResultSet rs = null; // 结果集对象, 封装查询返回的结果try {// 注册 JDBC 驱动Class.forName("com.mysql.cj.jdbc.Driver");// 打开链接conn = DriverManager.getConnection(url, username, password);// 执行查询String sql = "select * from user where username = ? and password = ?"; // 预编译 SQLstmt = conn.prepareStatement(sql);stmt.setString(1, "Tom"); // 为第一个占位符赋值stmt.setString(2, "123456"); // 为第二个占位符赋值rs = stmt.executeQuery();while (rs.next()) {User user = new User(rs.getInt("id"),rs.getString("username"),rs.getString("password"),rs.getString("name"),rs.getInt("age"));System.out.println(user);}} catch (SQLException se) {se.printStackTrace();} catch (Exception e) {e.printStackTrace();} finally {try {if (rs != null) rs.close();if (stmt != null) stmt.close();if (conn != null) conn.close();} catch (SQLException se) {se.printStackTrace();}}
}
  • 预编译 SQL
    String sql = "select * from user where username = ? and password = ?"; // 预编译 SQL
    
    • 可以防止 SQL 注入, 更安全
      • SQL 注入: 通过控制输入来修改事先定义好的 SQL 语句, 以达到执行代码对服务器进行攻击的方法
    • 性能更高
      • SQL 语句执行的流程:
        • SQL 语法解析检查
        • 优化 SQL
        • 编译 SQL
        • 执行 SQL
      • 预编译 SQL 会将 SQL 语句进行预编译并缓存, 再次执行相同结构的 SQL 语句, 数据库可以直接从缓存中获取编译后的代码, 跳过语法检查, 优化, 编译的步骤

MyBatis

一款优秀的持久层 (Dao, 负责数据的增删改查) 框架, 用于简化 JDBC 的开发

网站

入门程序

  • 创建 SpringBoot 项目, 引入依赖 mybatis, mysql, lombok

  • 在 SpringBoot 核心配置文件 application.properties 中配置数据库连接信息

spring.application.name=mybatis_demo# 配置数据库的连接信息
spring.datasource.url=jdbc:mysql://localhost:3306/db02
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=xxxx
  • 定义 Mapper 接口
package com.example.mybatis_demo.mapper;import java.util.List;import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;import com.example.mybatis_demo.pojo.User;@Mapper // 应用程序在运行时, 会自动的为该接口创建一个实现类对象 (代理对象), 并且会自动将该对象存入 IOC 容器
public interface UserMapper {/** 查询所有用户*/@Select("select * from user") // 定义该接口方法执行时要执行的 sql 语句public List<User> findAll(); // List<User> 是查询的返回值
}
  • 编写单元测试类
package com.example.mybatis_demo;import java.util.List;import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import com.example.mybatis_demo.mapper.UserMapper;
import com.example.mybatis_demo.pojo.User;@SpringBootTest // SpringBoot 单元测试的注解, 当前测试类中的测试方法运行时, 会启动 SpringBoot 项目, IOC 容器也会创建
class MybatisDemoApplicationTests {@Autowiredprivate UserMapper userMapper; // 注入 bean 对象@Testpublic void testFindAll() {List<User> userList = userMapper.findAll();userList.forEach(System.out::println);}
}

辅助配置

  • java 文件中编写 sql 语句高亮和语法提示

    • VSCode 里面不会搞
  • 配置日志输出

    • application.properties 中
    # 配置 mybatis 的日志输出
    mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
    

数据库连接池

mybatis 使用了数据库连接池技术, 避免频繁的创建连接、销毁连接而带来的资源浪费

  • 数据库连接池是个容器, 负责分配、管理数据库连接 (Connection)

    • 程序在启动时, 会在数据库连接池 (容器)中, 创建一定数量的 Connection 对象
  • 允许应用程序重复使用一个现有的数据库连接, 而不是再重新建立一个

    • 客户端在执行 SQL 时, 先从连接池中获取一个 Connection 对象, 然后在执行 SQL 语句, SQL 语句执行完之后, 释放 Connection 时就会把 Connection 对象归还给连接池 (Connection 对象可以复用)
  • 释放空闲时间超过最大空闲时间的连接, 来避免因为没有释放连接而引起的数据库连接遗漏

    • 客户端获取到 Connection 对象了, 但是 Connection 对象并没有去访问数据库 (处于空闲), 数据库连接池发现 Connection 对象的空闲时间 > 连接池中预设的最大空闲时间, 此时数据库连接池就会自动释放掉这个连接对象

好处:

  • 资源复用
  • 提升系统响应速度
  • 避免数据库连接遗漏

如何实现数据库连接池:

  • 官方 (sun) 提供了数据库连接池标准 (javax.sql.DataSource 接口)
  • 功能: 获取连接
public Connection getConnection() throws SQLException;
  • 常见的数据库连接池
    • Hikari SpringBoot 默认
    • Druid 阿里开源
      • 切换为 Druid
        • 在 pom.xml 文件中引入依赖
        <dependency><!-- Druid 连接池依赖 --><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.19</version>
        </dependency>
        
        • 在 application.properties 中修改数据库连接配置
        # 配置数据库的连接信息
        spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
        spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
        spring.datasource.druid.url=jdbc:mysql://localhost:3306/db02
        spring.datasource.druid.username=root
        spring.datasource.druid.password=xxxx
        
        • 运行结果: 报错, 未知原因

增删改查

增添

  • 添加一个用户
@Mapper
public interface UserMapper {// #{...} 里面写的是 对象的属性名 而不是 字段名@Insert("insert into user(username, password, name, age) values(#{username}, #{password}, #{name}, #{age})")public void insert(User user);
}
@Test
public void testInsert() {User user = new User(null, "Jack", "123456", "杰克", 18);userMapper.insert(user);
}

删除

  • 根据 ID 删除用户信息
@Mapper
public interface UserMapper {@Delete("delete from user where id = #{id}")public Integer deleteById(Integer id);
}
@Test
public void testDeleteById() {Integer i = userMapper.deleteById(1);System.out.println("执行完毕, 影响的记录数: " + i);
}
  • #$
    • #{...} 占位符, 执行时会替换成 ?, 生成预编译 SQL
      • 场景: 参数值传递
      • 安全, 性能高
    • ${...} 拼接符, 直接将参数拼接在 SQL 语句中, 存在 SQL 注入问题
      @Select("select id, name, score from ${tableName} order by ${sortField}")
      
      • 场景: 表名, 字段名动态设置时使用
      • 不安全, 性能低

修改

  • 根据 ID 更新用户信息
@Mapper
public interface UserMapper {@Update("update user set username=#{username}, password=#{password}, name=#{name}, age=#{age} where id=#{id}")public void update(User user);
}
@Test
public void testUpdate() {User user = new User(1, "Jack", "123456", "杰克", 18);userMapper.update(user);
}

查询

  • 根据用户名和密码查询用户信息
  • 多个参数时, 需要使用 @Param 为形参起名字; 基于官方骨架创建的 SpringBoot 项目中, 接口编译时会保留方法形参名, @Param 注解可以省略 (不保留则在字节码文件中会变成 (String var1, String var2))
@Mapper
public interface UserMapper {/** 根据用户名和密码查询用户信息*/@Select("select * from user where username=#{username} and password=#{password}")public User findByUsernameAndPassword(@Param("username") String username, @Param("password") String password);// 根据用户名和密码查询用户信息最多只会找到一个, 因此返回值为 User
}
@Test
public void testFindByUsernameAndPassword() {User user = userMapper.findByUsernameAndPassword("Jack", "123456");System.out.println(user);
}

XML 映射配置

可以通过注解配置 SQL 语句, 也可以通过 XML 配置文件配置 SQL 语句

默认规则:

  • XML 映射文件的名称与 Mapper 接口名称一致, 并且将 XML 映射文件和 Mapper 接口放置在相同包下
    • 同包同名: 一个在 java 下的 com.example.mybatis_demo.mapper 中, 一个在 resource 下的 com.example.mybatis_demo.mapper 中
  • XML 映射文件的 namespace 属性为 Mapper 接口全限定名一致
<mapper namespace="com.example.mybatis_demo.mapper.UserMapper"></mapper>
  • XML 映射文件中 sql 语句的 id 与 Mapper 接口中的方法名一致, 并保持返回类型一致
<mapper><select id="findAll" resultType="com.example.mybatis_demo.pojo.User">select id, username, password, name, age from user</select>
</mapper>
  • e.g.

    • XML 头信息 (约束) 与基本结构
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace=""></mapper>
    
    • 配置 SQL 语句
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.mybatis_demo.mapper.UserMapper"><!-- resultType: 查询返回的单条记录的类型 --><select id="findAll" resultType="com.example.mybatis_demo.pojo.User">        select id, username, password, name, age from user</select>
    </mapper>
    
    • 记得把注解删除
  • 使用注解还是 XML?

    • 注解: 简单的增删改查功能
    • XML: 复杂的 SQL 功能
  • 辅助配置

    • application.properties 中配置 XML 映射文件的位置 (感觉没啥用)
    mybatis.mapper-locations=classpath:mapper/*.xml
    
    • MyBatisX 插件, 可以快速在 java 文件和 xml 文件之间切换

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/884002.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

汇编语言笔记_1

汇编语言学习笔记(一)1.基础知识 汇编语言是直接在硬件上工作的语言,本章重在了解硬件系统结构 1.1 机器语言 CPU(中央处理单元)是一种微处理器,功能是执行机器指令每一种微处理器由于硬件设计和内部结构的不同,有自己的机器指令集,也就是机器语言由于机器码晦涩难懂和…

基于FPGA的图像双线性插值算法verilog实现,包括tb测试文件和MATLAB辅助验证

1.算法运行效果图预览 (完整程序运行后无水印)这里实现的是256*256双线性插值到512*512的系统模块局部放大:将数据导入到matlab,得到插值效果图:2.算法运行软件版本 matlab2022avivado2019.23.部分核心程序 (完整版代码包含详细中文注释和操作步骤视频)`timescale 1ns / 1…

Windows Teminal 自定义标题

背景与痛点场景: 开发环境,用命令行开了好几个微服务,窗口标题都是一样的,不好分清哪个窗口是哪个服务的了。所有窗口默认显示相同的标题(如"C:\Windows\System32\cmd.exe dotnet run")。窗口多了,切换也不方便。 解决方案: 使用Windows Teminal 来运行命令启…

前端开发day2

前端开发day2 今日概要:案例应用(利用之前所学知识) CSS知识点 模板 + CSS + 构建页面1.CSS案例 1.1 内容回顾HTML标签 固定格式,记住标签长什么样子,例如: h/div/span/a/img/ul/li/table/input/formCSS样式引用CSS:标签、头部、文件 .xx{... }<div class=xx xx>&…

基于电导增量MPPT控制算法的光伏发电系统simulink建模与仿真

1.课题概述 基于电导增量MPPT控制算法的光伏发电系统simulink建模与仿真。输出MPPT跟踪后的系统电流,电压以及功率。2.系统仿真结果3.核心程序与模型 版本:MATLAB2022a4.系统原理简介电导增量调制(Incremental Conductance, IC)算法是光伏发电系统中广泛应用的…

可持久化权值线段树(主席树)笔记

可持久化权值线段树(主席树)笔记 区别于普通线段树,权值线段树维护的信息不同普通线段树:节点区间是序列的下标区间,维护区间最值,区间和等信息 权值线段树:节点区间是序列的值域,维护值域内数出现的次数*图片引自董晓算法给定一个区间,询问该区间内的第 \(k\) 小值是…

基于排队理论的客户结账等待时间MATLAB模拟仿真

1.程序功能描述 基于排队理论的客户结账等待时间MATLAB模拟仿真,分析平均队长,平均等待时长,不能结账的概率,损失顾客数,到达顾客数,服务顾客数,平均服务时间。 2.测试软件版本以及运行结果展示MATLAB2022A版本运行 3.核心程序figure; plot(mean(mLen_seq,2),-b^…

【牛客训练记录】牛客2025年情人节比赛

训练情况赛后反思 今年比赛比去年有意思多了,太搞笑了 A题 我们构造一对就可以了,和为 \(x\),直接扔上去 \(1\) 和 \(x-1\) 即可点击查看代码 #include <bits/stdc++.h> // #define int long long #define endl \nusing namespace std;void solve(){int x; cin>>…

PVE8.3.1 直通硬盘

直通可以分为全盘直通和硬件直通,硬件直通会将整个 SATA 控制器直通给虚拟机,这可能会导致所有硬盘都分配给了一个虚拟机,这里介绍全盘直通。 1,查看硬盘IDls /dev/disk/by-id如上图ata 开头的设备就表示 sata 硬盘2,硬盘直通qm set 虚拟机id -sata0 /dev/disk/by-id/ata…

CTFCryto01-URL编码

URL编码URL编码,也称为百分号编码,是一种用于在URL(统一资源定位符)中传输特殊字符的编码方式 当 URL 路径或者查询参数中,带有中文或者特殊字符的时候,就需要对 URL 进行编码(采用十六进制编码格式)。URL 编码的原则是使用安全字符去表示那些不安全的字符 安全字符:指…