day1

计划

  1. 今天学完IO

笔记

1.File

  1. File的3个构造方法
  2. 表示什么:路径,可以表示文件或文件夹
  3. 常用方法
    1. 判断,获取
    2. 创建,删除
    3. 遍历
  4. 不能读写数据,读写需要用到IO流

2.IO流

1.字节流
  1. 计算机数据的最小单位是字节
  2. 字节流可以操作任何文件
  3. 字节流读取文本文件,需要读取为byte[],指定编码方式转为字符串,否则中文字符会出现乱码。
  4. 字节流没有缓冲区,可以用byte数组来做缓冲区,提升读取的效率
2.字符流
  1. 字符集有ASCII,GBK,Unicode等
    1. ASCII英文占用1字节
    2. GBK兼容ASCII,英文1字节,中文2字节。英文字节首位为0,中文高位字节首位为1,由此区分中英文
    3. Unicode编码方式多样
      1. UTF-8,1-4字节表示字符,英文1,中文3
      2. UTF-16,2或4字节表示字符
      3. UTF-32,4字节表示字符
    4. 字符串转为bite[]可以指定编码方式,bite[]构建字符串也可以指定编码。
  2. 字符流底层通过解码器按指定字符集将字节转换为字符。以 UTF-8 为例,解码器会识别字节头,决定本次读取 1~4 个字节拼成一个字符。
  3. 字符流可以在定义时指定编码格式,未指定就会用默认编码。
  4. 字符流的输入,输出流都有缓冲区
    1. 输入流,会先填满缓冲区,优先读取缓冲区,读完缓冲区后才会再取数据填满缓冲区。即使文件清空,也会先读完缓冲区的数据。
    2. 输出流,会先存到缓冲区,如果不调用 flush() 或 close(),数据会一直卡在内存中,直到缓冲区满了才会写入文件。

day11

计划

  1. 前后端分离开发规范与REST风格
  2. 工程搭建与基础代码结构
  3. 部门管理CRUD功能实现
  4. 参数接收方式详解
  5. MyBatis结果映射与驼峰命名
  6. Nginx部署与前后端联调
  7. Logback日志技术

笔记

1.前后端分离开发

前后端分离

将工程拆分为前端工程和后端工程,各自独立开发、独立部署。前端通过异步请求获取数据,后端根据接口文档提供数据。

开发流程
  1. 需求分析 → 理解产品原型和需求文档
  2. 接口定义 → 查阅接口文档,明确地址、参数、响应
  3. 前后端并行开发 → 各自按接口文档实现
  4. 接口测试 → 使用Apifox/Postman等工具测试后端接口
  5. 前后端联调 → 前端请求后端接口,验证整体功能

2.RESTful风格

传统URL vs RESTful
操作 传统URL RESTful URL
查询 /user/getById?id=1 GET /users/1 GET
新增 /user/saveUser POST /users POST
修改 /user/updateUser POST /users PUT
删除 /user/deleteUser?id=1 GET /users/1 DELETE
RESTful核心规则
  • URL定位资源:使用名词复数形式(如/depts/emps
  • HTTP动词描述操作:GET查询、POST新增、PUT修改、DELETE删除
  • 一句话总结:通过URL定位资源,通过HTTP请求方式描述操作

3.工程搭建

依赖引入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
application.yml配置
1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
application:
name: tlias-web-management
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/tlias
username: root
password: 1234

mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
包结构
1
2
3
4
5
6
com.zhang
├── controller/ # 控制层:接收请求、响应结果
├── service/ # 业务层:业务逻辑
│ └── impl/ # 业务实现类
├── mapper/ # 数据访问层:执行SQL
└── pojo/ # 实体类

4.统一响应结果Result

1
2
3
4
5
6
7
8
9
10
@Data
public class Result {
private Integer code; // 1成功,0失败
private String msg; // 提示信息
private Object data; // 响应数据

public static Result success() { ... } // 无数据返回
public static Result success(Object data) { ... } // 有数据返回
public static Result error(String msg) { ... } // 返回错误信息
}

5.部门管理CRUD

5.1 查询部门列表

Controller:

1
2
3
4
5
@GetMapping("/depts")
public Result list(){
ArrayList<Dept> data = deptService.findAll();
return Result.success(data);
}

**Service:**调用Mapper查询所有部门。

Mapper:

1
2
@Select("select id, name, create_time, update_time from dept")
ArrayList<Dept> findAll();
5.2 删除部门(根据ID)

请求方式: DELETE
请求路径: /depts?id=1

1
2
3
4
5
@DeleteMapping("/depts")
public Result deleteById(Integer id){
deptService.deleteById(id);
return Result.success();
}

Mapper:

1
2
@Delete("delete from dept where id=#{id}")
void deleteById(Integer id);
5.3 新增部门

请求方式: POST
请求路径: /depts
请求参数: JSON格式 {"name":"研发部"}

1
2
3
4
5
@PostMapping("/depts")
public Result insert(@RequestBody Dept dept){
deptService.insert(dept);
return Result.success();
}

**Service:**补全createTime和updateTime

1
2
3
4
5
public void insert(Dept dept) {
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
deptMapper.insert(dept);
}

Mapper:

1
2
@Insert("insert into dept(name,create_time,update_time) values(#{name},#{createTime},#{updateTime})")
void insert(Dept dept);
5.4 根据ID查询部门

请求方式: GET
请求路径: /depts/1(路径参数)

1
2
3
4
5
@GetMapping("/depts/{id}")
public Result getById(@PathVariable("id") Integer id){
Dept dept = deptService.getById(id);
return Result.success(dept);
}
5.5 修改部门

请求方式: PUT
请求路径: /depts
请求参数: JSON格式 {"id":1,"name":"研发部"}

1
2
3
4
5
@PutMapping("/depts")
public Result update(@RequestBody Dept dept){
deptService.update(dept);
return Result.success();
}

**Service:**更新updateTime

1
2
3
4
public void update(Dept dept) {
dept.setUpdateTime(LocalDateTime.now());
deptMapper.update(dept);
}

6.参数接收方式

6.1 简单参数接收
方式 代码示例 说明
HttpServletRequest request.getParameter("id") 繁琐,需手动转换,不推荐
@RequestParam @RequestParam("id") Integer id 指定参数名,推荐
形参同名 Integer id(参数名与形参名一致时) 最简洁,推荐
1
2
3
4
5
// 方式一:@RequestParam
public Result delete(@RequestParam("id") Integer id)

// 方式二:省略注解(参数名与形参名一致)
public Result delete(Integer id)
6.2 JSON参数接收

使用@RequestBody注解,Spring自动将JSON数据反序列化为Java对象。

1
public Result save(@RequestBody Dept dept)

注意:JSON的key必须与实体类的属性名一致。

6.3 路径参数接收

使用@PathVariable注解,从URL路径中获取参数。

1
2
@GetMapping("/depts/{id}")
public Result getById(@PathVariable("id") Integer id)

7.请求方式映射注解

注解 对应请求方式 用途
@GetMapping GET 查询数据
@PostMapping POST 新增数据
@PutMapping PUT 修改数据
@DeleteMapping DELETE 删除数据
@RequestMapping 所有方式 可通过method属性限定

推荐:使用@GetMapping等衍生注解,比@RequestMapping更简洁、语义更清晰。

类级别@RequestMapping

将公共路径抽取到类上,方法上的路径与类上的路径拼接形成完整请求路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RequestMapping("/depts")
@RestController
public class DeptController {

@GetMapping // 完整路径:/depts
public Result list() { ... }

@DeleteMapping // 完整路径:/depts
public Result delete(Integer id) { ... }

@PostMapping // 完整路径:/depts
public Result insert(@RequestBody Dept dept) { ... }

@GetMapping("/{id}") // 完整路径:/depts/{id}
public Result getById(@PathVariable Integer id) { ... }

@PutMapping // 完整路径:/depts
public Result update(@RequestBody Dept dept) { ... }
}

8.MyBatis结果映射

问题:属性名与字段名不一致

实体类属性(createTime)与数据库字段(create_time)命名风格不同,导致MyBatis无法自动封装。

三种解决方案
方案 代码示例 说明
手动映射 @Results({@Result(column="create_time",property="createTime")}) 灵活但繁琐
SQL起别名 select create_time createTime from dept 简单直接
驼峰映射 配置map-underscore-to-camel-case: true 推荐
驼峰命名配置

application.yml中开启自动驼峰命名映射:

1
2
3
mybatis:
configuration:
map-underscore-to-camel-case: true

映射规则create_timecreateTimeupdate_timeupdateTime

9.Nginx与前后端联调

Nginx核心功能
功能 说明
Web服务器 托管静态资源(HTML、CSS、JS、图片)
反向代理 转发客户端请求到后端服务器
负载均衡 将请求分发到多台后端服务器
请求流程
1
浏览器 → Nginx(:90) → 后端Tomcat(:8080) → Nginx → 浏览器
  1. 浏览器访问 http://localhost:90/api/depts
  2. Nginx接收到请求,根据/api/前缀匹配规则
  3. Nginx重写路径:/api/depts/depts
  4. Nginx将请求转发到 http://localhost:8080/depts
  5. 后端处理完返回数据,Nginx将结果返回给浏览器
Nginx关键配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server {
listen 90;
server_name localhost;

# 前端页面
location / {
root html;
index index.html;
try_files $uri $uri/ /index.html;
}

# 后端API代理
location ^~ /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://localhost:8080;
}
}

配置说明

配置项 作用
location / 匹配所有路径,返回前端页面
try_files 支持前端路由刷新不404
location ^~ /api/ 精确匹配API前缀,不再检查正则
rewrite 去掉/api/前缀,重写路径
proxy_pass 转发请求到后端服务器
为什么使用Nginx
  • 安全:后端Tomcat集群不直接暴露给前端
  • 灵活:后端增减服务器对前端无感知
  • 负载均衡:便于实现多服务器负载均衡

10.Apifox接口测试

Apifox是集成了API文档、调试、Mock、测试的协作平台。

使用步骤
  1. 创建项目,导入或编写接口文档
  2. 在”接口管理”中定义接口(路径、方法、参数、响应)
  3. 在”接口调试”中发送请求测试
  4. 在”Mock服务”中生成模拟数据供前端开发
  5. 支持GET/POST/PUT/DELETE等所有请求方式

11.Logback日志

为什么使用日志框架
System.out.println Logback
硬编码,不灵活 可通过配置文件控制
只能输出到控制台 支持输出到文件
无日志级别 支持debug/info/warn/error级别
不便于维护 灵活配置格式和输出位置
Logback入门

1. 引入依赖(SpringBoot已内置)

2. 配置文件logback.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<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">
<FileNamePattern>D:/tlias-%d{yyyy-MM-dd}-%i.log</FileNamePattern>
<MaxHistory>30</MaxHistory>
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>
<encoder>
<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>

3. 获取Logger对象

1
2
3
4
5
6
7
8
// 方式一:声明Logger对象
private static final Logger log = LoggerFactory.getLogger(ClassName.class);

// 方式二:使用Lombok @Slf4j注解
@Slf4j
public class ClassName {
// 自动生成log对象
}

4. 记录日志

1
2
3
4
log.debug("调试信息");
log.info("运行信息");
log.warn("警告信息");
log.error("错误信息");
日志级别
级别 说明 优先级
trace 追踪(很少使用)
debug 调试
info 运行信息
warn 警告
error 错误

规则:设置的级别越高,输出的日志越少。如设置info级别,则debug和trace级别不会输出。

12.核心知识点总结

知识点 说明
前后端分离 前端独立工程、后端独立工程,通过接口文档协作
RESTful风格 URL定位资源(复数名词),HTTP动词描述操作
@GetMapping GET请求查询数据
@PostMapping POST请求新增数据
@PutMapping PUT请求修改数据
@DeleteMapping DELETE请求删除数据
@RequestBody 接收JSON格式参数
@RequestParam 接收URL查询参数
@PathVariable 接收URL路径参数
@RequestMapping 类级别公共路径抽取
Result 统一响应封装类
map-underscore-to-camel-case 下划线自动转驼峰映射
Nginx 静态服务器+反向代理+负载均衡
Apifox 接口文档编写与测试工具
Logback 日志框架,支持控制台和文件输出
日志级别 debug < info < warn < error

计划

  1. JDBC基础入门
  2. JDBC查询与增删改
  3. SQL注入与预编译SQL
  4. MyBatis快速入门
  5. MyBatis注解写法与XML映射
  6. SpringBoot配置文件与数据库连接池

笔记

1.JDBC概述

JDBC是什么
  • JDBC:Java DataBase Connectivity,使用Java语言操作关系型数据库的一套API
  • 本质:Sun公司定义的一套接口规范,数据库厂商提供驱动jar包实现接口
  • 执行原理:Java程序调用JDBC接口,真正执行的是数据库驱动中的实现类
Java操作数据库技术
技术 说明
JDBC 最底层、最基础的数据库操作技术
MyBatis 基于JDBC封装的持久层框架,简化数据库开发
MyBatisPlus 在MyBatis基础上进一步增强
Hibernate / SpringDataJPA ORM思想更强的持久层框架

2.JDBC查询数据

查询需求

基于JDBC实现用户登录,本质是执行一条带用户名和密码条件的查询语句:

1
select * from user where username = 'daqiao' and password = '123456';
准备数据表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
create table user(
id int unsigned primary key auto_increment comment 'ID,主键',
username varchar(20) comment '用户名',
password varchar(32) comment '密码',
name varchar(10) comment '姓名',
age tinyint unsigned comment '年龄'
) comment '用户表';

insert into user(id, username, password, name, age) values
(1, 'daqiao', '123456', '大乔', 22),
(2, 'xiaoqiao', '123456', '小乔', 18),
(3, 'diaochan', '123456', '貂蝉', 24),
(4, 'lvbu', '123456', '吕布', 28),
(5, 'zhaoyun', '12345678', '赵云', 27);
JDBC查询步骤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/web", "root", "1234");

PreparedStatement pstmt = conn.prepareStatement(
"select * from user where username = ? and password = ?");
pstmt.setString(1, "daqiao");
pstmt.setString(2, "123456");

ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
Integer id = rs.getInt("id");
String username = rs.getString("username");
String password = rs.getString("password");
String name = rs.getString("name");
Integer age = rs.getInt("age");
System.out.println(id + "," + username + "," + password + "," + name + "," + age);
}

rs.close();
pstmt.close();
conn.close();
ResultSet结果集
  • ResultSet:封装DQL查询语句返回的结果
  • next():移动光标到下一行,有数据返回true,没有数据返回false
  • getXxx(…):获取当前行指定列的数据,推荐使用列名获取

3.预编译SQL与SQL注入

静态SQL与预编译SQL
写法 示例 问题
静态SQL username = 'daqiao' 参数写死,灵活性差
字符串拼接SQL 拼接用户输入 容易产生SQL注入
预编译SQL username = ? 安全、性能更高,推荐使用
PreparedStatement
1
2
3
4
5
PreparedStatement pstmt = conn.prepareStatement(
"select * from user where username = ? and password = ?");
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();
SQL注入
  • SQL注入:通过控制输入内容,修改原本SQL语句含义,达到绕过校验或攻击服务器的目的
  • 典型场景:登录功能中,如果使用字符串拼接SQL,输入特殊内容可能使条件恒成立
  • 解决方式:使用预编译SQL,用户输入会被当成普通参数值处理,不再改变SQL结构

4.JDBC增删改数据

执行DML语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/web", "root", "1234");

String sql = "update user set password = ?, age = ? where id = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "123456");
pstmt.setInt(2, 25);
pstmt.setInt(3, 1);

int rows = pstmt.executeUpdate();
System.out.println(rows);

pstmt.close();
conn.close();
executeQuery与executeUpdate
方法 执行语句 返回值
executeQuery() DQL查询语句 ResultSet结果集
executeUpdate() DML增删改语句 影响的记录数

5.MyBatis概述

MyBatis是什么
  • MyBatis:一款优秀的持久层框架,用于简化JDBC开发
  • 持久层:数据访问层,负责操作数据库
  • 框架:半成品软件,提供通用基础代码,提高开发效率
JDBC的不足
  1. 数据库连接信息硬编码在Java代码中
  2. 查询结果解析和对象封装繁琐
  3. 每次操作都要创建和关闭连接,资源浪费明显
MyBatis的改进
  1. 数据库连接信息放到SpringBoot配置文件中
  2. 查询结果由MyBatis自动映射为Java对象
  3. 底层使用数据库连接池复用连接

6.MyBatis快速入门

使用步骤
  1. 创建SpringBoot工程,引入mybatis-spring-boot-startermysql-connector-j
  2. 创建数据库表,并创建与表字段对应的实体类
  3. 在配置文件中配置数据库连接信息
  4. 编写Mapper接口,并在接口上添加@Mapper
  5. 选择注解方式或XML方式编写SQL
  6. 在业务代码或测试代码中注入Mapper接口,调用方法操作数据库
核心关系
组成 作用 复习重点
实体类 封装表中一行数据 属性名尽量与字段名一致
Mapper接口 定义数据库操作方法 方法名表达清楚要执行的操作
SQL语句 真正操作数据库 可写在注解中,也可写在XML中
配置文件 配置数据源和MyBatis行为 数据库连接四要素、日志输出
Mapper接口基本模板
1
2
3
4
5
6
7
8
9
10
11
12
13
@Mapper
public interface UserMapper {
List<User> findAll();

int deleteById(Integer id);

int insert(User user);

int update(User user);

User findByUsernameAndPassword(@Param("username") String username,
@Param("password") String password);
}

细节

  • Mapper接口不需要自己写实现类,MyBatis运行时会生成代理对象
  • @Mapper的作用是让MyBatis识别该接口,并把代理对象交给Spring IOC容器管理
  • DML语句可以用int作为返回值,表示影响的记录数
  • 查询多条数据用List<实体类>,查询单条数据用实体类

7.MyBatis注解写法

适用场景

注解写法适合简单SQL,例如单表的基础增删改查。优点是直观、文件少;缺点是复杂SQL写在Java注解中可读性差。

查询
1
2
3
4
5
6
@Select("select * from user")
List<User> findAll();

@Select("select * from user where username = #{username} and password = #{password}")
User findByUsernameAndPassword(@Param("username") String username,
@Param("password") String password);

细节

  • @Select用于写查询语句
  • 多条件查询时,建议使用@Param给参数命名,SQL中通过#{参数名}取值
  • 查询结果字段名与实体类属性名一致时,MyBatis可以自动封装
删除
1
2
@Delete("delete from user where id = #{id}")
int deleteById(Integer id);

细节

  • #{id}不是字符串拼接,而是预编译占位符
  • 返回值int表示删除了几条记录
新增
1
2
@Insert("insert into user(username, password, name, age) values(#{username}, #{password}, #{name}, #{age})")
int insert(User user);

细节

  • 参数是对象时,#{username}表示调用对象的getUsername()取值
  • 自增主键字段通常不需要手动插入
  • 字段顺序要与values中的参数顺序保持一致
修改
1
2
@Update("update user set username = #{username}, password = #{password}, name = #{name}, age = #{age} where id = #{id}")
int update(User user);

细节

  • 修改语句必须注意where条件,缺少条件会更新整张表
  • 修改对象必须包含用于定位数据的主键值

8.MyBatis XML映射写法

适用场景

XML写法适合复杂SQL,例如多条件动态查询、多表查询、复杂字段映射。优点是SQL集中、可读性更好;缺点是接口和XML之间要严格对应。

XML开发规范
规范 要求
文件位置 XML映射文件与Mapper接口包路径保持一致
文件名称 XML文件名与Mapper接口名一致,如UserMapper.xml
namespace 必须写Mapper接口的全限定名
SQL的id 必须与Mapper接口方法名一致
resultType 查询返回单条记录要封装成的实体类类型
XML基本模板
1
2
3
4
5
6
7
<?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.zhang.mapper.UserMapper">

</mapper>
查询写法
1
2
3
4
5
6
7
<select id="findAll" resultType="com.zhang.pojo.User">
select * from user
</select>

<select id="findByUsernameAndPassword" resultType="com.zhang.pojo.User">
select * from user where username = #{username} and password = #{password}
</select>

细节

  • resultType写的是单条记录封装成什么类型,不是List<User>
  • 如果Mapper方法返回List<User>,XML中仍然写resultType="com.zhang.pojo.User"
  • id必须和接口方法名一致,否则MyBatis找不到对应SQL
增删改写法
1
2
3
4
5
6
7
8
9
10
11
12
13
<insert id="insert">
insert into user(username, password, name, age)
values (#{username}, #{password}, #{name}, #{age})
</insert>

<update id="update">
update user set username = #{username}, password = #{password}, name = #{name}, age = #{age}
where id = #{id}
</update>

<delete id="deleteById">
delete from user where id = #{id}
</delete>

细节

  • <insert><update><delete>标签一般不需要写resultType
  • DML执行后返回影响行数,Mapper接口方法可以写int
  • XML和注解不要同时给同一个方法配置SQL,否则会冲突或造成理解混乱

9.注解写法与XML写法对比

对比点 注解写法 XML写法
SQL位置 Mapper接口方法上 resources下的Mapper.xml中
适合场景 简单增删改查 复杂SQL、动态SQL、多表查询
优点 写法直接,文件少 SQL更清晰,便于维护复杂语句
缺点 SQL复杂时可读性差 配置规则多,需要接口和XML对应
选择建议 入门和简单SQL优先使用 复杂业务SQL优先使用

10.MyBatis参数传递细节

#{...}${...}
符号 本质 结果 使用建议
#{...} 参数占位符 会替换为?,走预编译SQL 传递普通参数时强烈推荐
${...} 字符串拼接 直接拼接到SQL语句中 只有动态表名、字段名等特殊场景才考虑
为什么推荐#{...}
  1. 可以防止SQL注入
  2. 可以使用预编译SQL,提高性能
  3. 参数值会被当成普通数据处理,不会改变SQL结构
参数来源
Mapper方法参数 SQL中取值方式
单个简单参数 #{参数名}#{任意名}通常都能取到
多个简单参数 推荐使用@Param("名称")后通过#{名称}取值
对象参数 通过#{属性名}取对象属性值

11.数据库连接池

连接池作用
  • 数据库连接池是一个容器,负责分配和管理数据库连接
  • 程序启动时,连接池中会提前创建一定数量的Connection对象
  • 执行SQL时从连接池获取连接,执行完毕后归还连接池
  • 空闲过久的连接可以被自动释放,减少连接泄露风险
连接池优点
  1. 资源重用
  2. 提升系统响应速度
  3. 避免频繁创建和销毁连接
  4. 降低数据库连接遗漏风险
常见连接池
连接池 说明
Hikari SpringBoot默认连接池,性能好
Druid 阿里巴巴开源连接池,功能强大
C3P0 早期常见连接池
DBCP Apache提供的连接池

12.SpringBoot配置文件

properties与yml
配置文件 格式 特点
application.properties key=value 简单直接,配置多时层级不够清晰
application.yml / application.yaml 缩进表示层级 简洁清晰,以数据为中心,推荐使用
yml语法规则
  1. 大小写敏感
  2. 数值前必须有空格作为分隔符
  3. 使用缩进表示层级关系
  4. 缩进不能使用Tab,只能使用空格
  5. 相同层级左侧对齐
  6. #表示注释
  7. 以0开头的值建议使用引号包裹,避免被识别为八进制
数据源配置模板
1
2
3
4
5
6
7
8
9
10
11
12
spring:
application:
name: springboot-mybatis-quickstart1
datasource:
url: jdbc:mysql://localhost:3306/db02
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 数据库密码

mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
MyBatis日志配置
1
2
3
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

开启后,控制台可以看到MyBatis执行的SQL语句、参数和结果,方便排查数据库操作问题。

13.复习时容易忽略的细节

  1. Mapper接口只定义方法,不写实现类,实现类由MyBatis动态代理生成
  2. @Mapper不能忘,否则Mapper接口不会被MyBatis识别并交给Spring管理
  3. 注解和XML是两种SQL配置方式,同一个方法不要两边都写SQL
  4. XML中的namespace必须等于Mapper接口全限定名
  5. XML中SQL标签的id必须等于Mapper接口的方法名
  6. 查询集合时,XML的resultType仍写集合中单个元素的类型
  7. DML语句返回int可以知道影响了几条记录
  8. 修改和删除一定要注意where条件,避免影响整张表
  9. 普通参数传递优先使用#{...},不要为了省事使用${...}
  10. 开启MyBatis日志后,可以在控制台观察最终执行的SQL和参数

14.核心知识点总结

知识点 说明
JDBC Java操作关系型数据库的底层API
DriverManager 获取数据库连接
Connection 数据库连接对象
PreparedStatement 预编译SQL执行对象
ResultSet 查询结果集对象
executeQuery() 执行DQL查询语句
executeUpdate() 执行DML增删改语句
SQL注入 通过输入改变SQL语义的攻击方式
MyBatis 简化JDBC开发的持久层框架
@Mapper 声明Mapper接口,交给MyBatis和Spring管理
注解写法 通过@Select、@Insert、@Update、@Delete在接口方法上写SQL
#{…} 参数占位符,生成预编译SQL
${…} 字符串拼接符,存在注入风险
XML映射 将SQL写在XML中,适合复杂SQL
Hikari SpringBoot默认数据库连接池
application.yml SpringBoot推荐配置文件格式

day2

计划

  1. 学完IO流

笔记

1.缓冲流

字节缓冲流
  1. 缓冲流默认缓冲区大小为8192字节(8KB)
  2. 使用方式:在基本流外包装一层缓冲流
  3. 关闭缓冲流时,底层基本流也会自动关闭
  4. 性能对比
    1. 缓冲流逐字节读取复制:35ms(内部缓冲区已优化磁盘I/O)
    2. 缓冲流配合 byte[] 数组读取复制:4ms
  5. 缓冲流与缓冲数组同时使用是不必要的
    1. 缓冲流内部已有 8KB 缓冲区,逐字节读取时已通过内部缓冲区大幅减少磁盘 I/O,这是性能提升的核心
    2. 在缓冲流之上再使用 byte[] 数组,相当于多了一层冗余的缓冲层级,增加了内存占用和代码复杂度
    3. 实际开发中,单独使用缓冲流即可,或直接使用基本流配合自己的缓冲区,没必要两层都用
字符缓冲流
  1. BufferedReader 特有方法:readLine(),读取一行文本,遇到文件末尾返回 null
  2. BufferedWriter 特有方法:newLine(),用于多平台换行(Windows: \r\n, Linux: \n)
  3. 字符缓冲流也可以指定编码格式

2.转换流

  1. InputStreamReader:将字节输入流转换为字符输入流
  2. OutputStreamWriter:将字节输出流转换为字符输出流
  3. 可以指定编码格式,解决乱码问题
  4. 常与缓冲流配合使用,形成:BufferedReader -> InputStreamReader -> FileInputStream
  5. Java 11+ FileReader/FileWriter 已支持指定编码,但转换流仍有用武之地
    1. 处理非文件来源的字节流(如网络流、内存流)
    2. 需要精确控制编码转换场景

3.序列化流与反序列化流

  1. 对象序列化:将对象写入文件,需要实现 Serializable 接口
  2. 对象反序列化:从文件读取对象
  3. serialVersionUID:序列化版本号,确保反序列化时类版本一致
    1. 如果类结构发生变化(如新增字段),版本号不匹配会抛出 InvalidClassException
    2. 建议显式声明版本号,避免编译器自动生成导致的兼容性问题
  4. transient 关键字:修饰的成员变量不参与序列化,反序列化时为默认值
  5. 序列化集合对象:将整个集合写入文件,便于批量操作

4.压缩流与解压缩流

ZipEntry
  1. 表示压缩包里的每个文件或目录
  2. 压缩流与解压流中有一个当前条目的概念,就是你最近获取到的或创建的ZipEntry对象
  3. 所以必须要及时关闭当前条目
解压缩
  1. 使用 ZipInputStream 读取压缩包
  2. getNextEntry() 获取下一个 ZipEntry
  3. 判断是目录还是文件,分别处理
  4. 读取完成后调用 closeEntry() 关闭当前条目
压缩
  1. 使用 ZipOutputStream 写入压缩包
  2. putNextEntry() 创建新的 ZipEntry
  3. 递归处理目录结构,保持原有的层级关系
    1. 遍历目录下所有文件和子目录
    2. 文件则直接写入压缩条目
    3. 目录则递归调用,传入更新后的路径名
  4. 每个文件写入完成后调用 closeEntry()

5.打印流

  1. PrintStream:字节打印流,System.out 就是 PrintStream 的实例
  2. PrintWriter:字符打印流
  3. 特点
    1. 可以直接打印各种类型的数据(int、double、String、对象等)
    2. 可以设置自动刷新(autoFlush),调用 println() 时自动刷新缓冲区,换行。

6.URL网络读取

  1. 使用 URLURLConnection 建立网络连接
  2. 通过 getInputStream() 获取输入流
  3. 使用 InputStreamReader 按字符读取网页内容
  4. 可以配合第三方工具包(如 Hutool)简化文件写入操作

7.工具包

  1. commons-io
  2. hutool
  3. 两个都是第三方工具包,提供了很多包装好的工具,导入即可直接使用。

前几天陆陆续续写了苍穹外卖的员工管理,菜品,分类,套餐管理等,未坚持每日学习,未写复盘

1. 今天做了什么

(day5的内容)

  1. Redis入门
    1. 安装
    2. Redis里的数据类型:字符串,哈希,集合,有序集合,列表
    3. Redis操作的命令
    4. java程序中操作Redis数据库:使用Spring Data Redis
      1. 配置
      2. 使用
  2. 店铺相关接口 , 使用Redis存储
    1. 设置店铺营业状态
    2. 获取店铺状态(用户端与管理端)
  3. 接口分组展示的配置

2. 核心知识点

Redis的基础使用

3. 深度思考

  1. 为什么使用Redis
    1. Redis将数据存在内存中,访问效率更高,可用于存方高频使用的数据
    2. 店铺状态只有一个值,使用Redis需要为它单独创建一个一行一列的表,而使用Redis这种键值对的方式就很方便

4. 踩过的坑

  1. 获取店铺状态有用户端与管理端,必须在使用@RestController(“userShopController”)注解是对其命名进行区分,否则会因为Bean对象名冲突报错

5. 遗留问题 / 想不通

  1. 通过@Bean注解在配置类中配置的Bean,使用范围是?
    1. 只有该配置类在springboot扫描范围内,才会加入IOC容器管理
    2. 只有被IOC容器管理的类,其非静态属性,才可以使用@Autowired注入Bean
  2. redisTemplate.setKeySerializer(new StringRedisSerializer());的作用
    1. 对key进行序列化
    2. java操作Redis时,使用java字符串作为key,所以为Key指定字符串序列化器,可以确保java字符串存进Redis还是该字符串
    3. 若不指定序列化器,使用默认序列化器,存进Redis的key与java字符串会不一致
    4. 也可以为Value设置序列化器
      // 设置 Value 的序列化器(通常用 Jackson,存 JSON 格式,方便其他语言读取)
      redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
1
2
3
4
5
6
7
@Bean  
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());// 设置key序列化器
return redisTemplate;
}

1. 今天做了什么

外卖day7的一部分

  1. 缓存菜品
  2. 缓存套餐

2. 核心知识点

  1. 两种实现缓存的方式
    1. 使用Redis自己实现缓存
      1. 根据分类id查询菜品,将分类id作为key,将查询到的集合作为value
      2. 查询时先查缓存是否存在,存在直接返回,不存在则查询后存入缓存返回
      3. 一旦管理端发生新增,修改,删除,起售停售,都要对缓存进行清理
    2. 使用Spring Cache实现缓存
      1. 完全使用注解实现,很方便
      2. 在启动类使用注解进行开启
      3. 在相应查询接口上添加注解,实现查询时先查缓存是否存在,存在直接返回,不存在则查询后存入缓存返回
      4. 在管理端新增,修改,删除操作添加清除缓存的注解
      5. Spring Cache底层可以用多种方式实现,这里使用Redis,添加了Redis的依赖,可以自动识别

3. 深度思考

  1. 为什么要做缓存
    1. 在业务执行过程中,数据库查询操作从磁盘读取数据,比较耗时
    2. 用户使用量比较大时加载较慢,影响使用体验
    3. 在Redis中做缓存,从内存中读取速度很快

4. 踩过的坑

  1. redisTemplate.delete()不支持通配符,redisTemplate.keys()支持,先查询keys,再删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@ApiOperation("修改菜品")  
@PutMapping
public Result update(@RequestBody DishDTO dishDTO){
log.info("修改菜品:{}", dishDTO);
dishService.update(dishDTO);

//更新菜品后,清除缓存
clearCache("dish_*");
return Result.success();
}


private void clearCache(String pattern){
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}
  1. Spring Cache的@CacheEvict注解,清除所有setmealcache的缓存:
1
@CacheEvict(cacheNames = "setmealcache", allEntries = true)

5. 遗留问题 / 想不通

1. 今天做了什么

(外卖day6的内容)

  1. HttpClient的使用
  2. 微信小程序开发入门
    1. 项目目录结构
    2. 获取头像与昵称
    3. 获取授权码
  3. 微信登录
    1. 流程:小程序获取授权码->携带授权码向服务端发起请求->服务端使用授权码向微信的接口发起请求,获取openid(微信用户唯一标识)->查询若为新用户则1创建->创建jwt令牌,与用户基本信息一起返回给小程序
    2. 设置拦截器,对除登录与获取营业状态的其他来自用户端的请求进行令牌校验

2. 核心知识点

  1. HttpClient的使用
  2. 登录流程

3. 深度思考

4. 踩过的坑

  1. 从json字符串中解析出信息:
1
2
JSONObject jsonObject = JSON.parseObject(resultJson);//将json字符串转换为json对象  
String openid = jsonObject.getString("openid");//从json对象中获取openid

5. 遗留问题 / 想不通

计划

多线程

总结

1.线程创建

3种方式
  1. 继承Thread
  2. 实现Runnable
  3. 实现Callable
特点:

3可以有返回值,1,2无返回值
1因为单继承,无法继承其他类

方法:

设置/获取线程名,获取当前线程名,出让,插入,睡眠等

2.同步安全问题

原因
  1. 大多操作系统对线程采用抢占式调度
  2. 线程之间会争夺CPU执行权
  3. 多个线程对同一资源进行操作的过程中,CUP执行权随时会被夺走,被其他线程占用,从而造成安全问题
解决方案
  1. 对操作共享资源的代码采用锁,只有线程拿到锁,才能进入,执行代码,操作相应资源。未拿到锁的线程即使拿到CPU执行权也无法进入。
  2. 同步代码块
    1. 对一段代码设置锁,锁对象可以自己指定
    2. 代码执行完锁会自动释放
  3. 同步方法
    1. 非静态方法以this作为锁对象
    2. 静态方法以类名.class做为锁对象
  4. Lock锁
    1. 需要手动设置锁与释放锁
    2. 为确保锁被释放,可将释放语句写进try的finally中
注意:
  1. CPU执行权的抢夺与锁的抢夺是独立的
    1. 线程只有拿到CPU执行权才可以去抢夺锁
    2. 线程即使抢到锁,也随时可能被夺走CPU执行权,但因为锁未释放,其他线程无法进入同步代码块,只有等下次锁被释放
  2. 避免锁的嵌套,防止出现死锁,即外层锁与内层锁被不同线程拿到,全部陷入阻塞

3.等待唤醒机制

流程
  1. 生产者产出资源,消费者消耗资源
  2. 生产者
    1. 有资源:等待
    2. 无资源
      1. 生产资源
      2. 唤醒正在等待的线程(消费者)
  3. 消费者
    1. 无资源:等待
    2. 有资源:
      1. 消耗资源
      2. 唤醒正在等待的进程(生产者)
细节
  1. 等待与唤醒都需要通过同个锁对象(任意类),唤醒的时候才能唤醒所有通过该锁对象进入等待的线程
  2. 进程进入等待时,释放锁,并交出CPU执行权。在被唤醒时,继续抢夺锁,拿到锁后继续执行后续语句。
实现
  1. 自己用同步代码块synchronized实现,对操作共享资源的代码设置锁
  2. 可以使用阻塞队列,阻塞队列内时线程安全的。内部也实现了等待与唤醒。没一个放入与取出操作时一定会成功执行的,无法放入进入等待,下次唤醒会继续放入。

4.线程池

意义
  1. 降低资源消耗:复用已创建的线程,避免频繁创建和销毁线程(new Thread + start())带来的性能开销
  2. 提高响应速度:任务到达时,无需等待线程创建,可直接使用空闲线程立即执行
  3. 提高线程可管理性:线程是稀缺资源,统一分配、调优和监控,防止无限制创建导致系统崩溃(OOM)
核心参数(ThreadPoolExecutor 的7个参数)
  1. 核心线程数(corePoolSize):线程池中一直存活的线程数,即使空闲也不会被回收(除非设置了allowCoreThreadTimeOut
  2. 最大线程数(maximumPoolSize):线程池中允许的最大线程数
  3. 存活时间(keepAliveTime):非核心线程空闲超过该时间会被回收
  4. 时间单位(unit)keepAliveTime 的时间单位(如 TimeUnit.SECONDS
  5. 阻塞队列(workQueue):存放等待执行的任务(如 ArrayBlockingQueueLinkedBlockingQueue
  6. 线程工厂(threadFactory):创建线程的工厂,可自定义线程名、优先级等(默认 Executors.defaultThreadFactory()
  7. 拒绝策略(handler):当线程池和队列都满时,对新提交的任务的处理方式
执行流程(任务提交后的流转)
  1. 核心线程:当前线程数 < 核心线程数 → 创建新线程执行任务
  2. 阻塞队列:核心线程已满 → 任务放入队列等待
  3. 最大线程:队列已满 → 创建非核心线程执行任务(直到达到最大线程数)
  4. 拒绝策略:最大线程也满了 → 执行拒绝策略

    注意:流程是 核心 → 队列 → 最大,而非很多人误以为的核心 → 最大 → 队列

拒绝策略(4种内置实现)
  1. AbortPolicy(默认):直接抛出 RejectedExecutionException,阻止系统正常运行
  2. CallerRunsPolicy:由调用者线程(提交任务的线程)自己执行该任务,提供反馈机制,减缓任务提交速度
  3. DiscardPolicy:直接丢弃任务,不抛出异常(丢失任务)
  4. DiscardOldestPolicy:丢弃队列中最旧的任务,然后重试提交当前任务
提交任务的两种方式
  1. execute(Runnable command)
    • 提交无返回值的任务
    • 异常直接抛出(控制台打印),调用者无法捕获
  2. submit(Callable/Runnable task)
    • 提交有返回值(Callable)或无返回值(Runnable)的任务
    • 返回 Future 对象,可通过 future.get() 获取结果或捕获异常
    • Runnable 提交时,Future.get() 返回 null
    • Runnable + 指定结果:submit(Runnable task, T result)Future.get() 返回指定的 result
注意
  1. 线程池中的线程是复用的:底层 Worker 通过 while 循环不断从队列中取任务执行,执行完一个任务后不会销毁,而是继续取下一个任务
  2. Thread 对象与内核线程Thread 对象只是堆内存中的普通对象,真正执行能力来自操作系统内核线程(LWP)。线程池复用的是内核线程,而不是 Thread 对象本身
  3. 不能直接操作池中某个线程:线程池封装了线程的个体操作,应通过 Future 控制任务(如 future.cancel(true) 中断任务),而非直接操作线程
  4. executesubmit 底层关系submit 底层也是调用 execute,但会将 Runnable/Callable 包装成 RunnableFuture(即 FutureTask),从而支持返回值和异常捕获
  5. 线程池关闭:使用 shutdown() 优雅关闭(不再接收新任务,等待已提交任务执行完毕),或 shutdownNow() 立即停止(尝试中断正在执行的任务,返回未执行的任务列表)

1. 今天做了什么

(外卖day07,day08)

  1. 购物车相关接口
  2. 用户下单相关接口

2. 核心知识点

  1. 订单表与订单明细表的设计
    1. 订单表存储订单相关的信息
    2. 订单明细表存储订单中的菜品,套餐的单价,数量等数据,一条菜品或套餐对应一条数据
    3. 二者是一对多的
  2. 冗余字段
    1. 数据表中某些字段可以通过其他字段在其他表中查询到
    2. 为什么设计冗余字段
      1. 在插入数据时将冗余字段从其他表查出来,一并插入其中
      2. 以后查询的时候可以直接从表中查到,不用进行多表联查

3. 深度思考

4. 踩过的坑

  1. 多表操作一定要开启事务

5. 遗留问题 / 想不通

0%