Java - Web 开发笔记 Ⅱ
👉 在线笔记:https://du1in9.github.io/javaweb.github.io/
第4章 - SpringBootWeb & 原理
4.1 SpringBootWeb
4.1.1 快速入门
Spring 发展到今天已经形成了一种开发生态圈,Spring 提供了若干个子项目,每个项目用于完成特定的功能
Spring Boot 可以帮助我们非常快速的构建应用程序、简化开发、提高效率
// 需求:浏览器发起请求 /hello 后,给浏览器返回字符串 "Hello World"
package com.itheima.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class hellocontroller {
@RequestMapping("/hello")
public String hello() {
return "hello world";
}
}
4.1.2 HTTP 协议
① HTTP - 概述
Hyper Text Transfer Protocol,超文本传输协议,规定了浏览器和服务器之间数据传输的规则
-
基于TCP协议:面向连接,安全
-
基于请求-响应模型的:一次请求对应一次响应
-
HTTP协议是无状态的协议:对于事务处理没有记忆能力,每次请求-响应都是独立的
缺点:多次请求间不能共享数据;优点:速度快
② HTTP - 请求协议
请求头 | 解释 |
---|---|
Host | 请求的主机名 |
User-Agent | 浏览器版本 |
Accept | 表示浏览器能接收的资源类型 |
Accept-Language | 表示浏览器偏好的语言 |
Accept-Encoding | 表示浏览器可以支持的压缩类型 |
Content-Type | 请求主体的数据类型 |
Content-Length | 请求主体的大小,单位 byte |
③ HTTP - 响应协议
状态码 | 英文描述 | 解释 |
---|---|---|
200 | OK | 客户端请求成功,即处理成功,这是我们最想看到的状态码 |
302 | Found | 资源已移动到由 Location 响应头给定的 URL,浏览器会自动重新访问到这个页面 |
304 | Not Modified | 请求资源至上次取得后,服务端并未更改,你直接用你本地缓存吧。隐式重定向 |
400 | Bad Request | 客户端请求有语法错误,不能被服务器所理解 |
403 | Forbidden | 服务器收到请求,但是拒绝提供服务,比如:没有权限访问相关资源 |
404 | Not Found | 请求资源不存在,一般是URL输入有误,或者网站资源被删除了 |
405 | Method Not Allowed | 请求方式有误,比如应该用GET请求方式的资源,用了POST |
428 | Precondition Required | 服务器要求有条件的请求,告诉客户端要想访问该资源,必须携带特定的请求头 |
429 | Too Many Requests | 指示用户在给定时间内发送了太多请求,配合 Retry-After响应头一起使用 |
431 | Request Header Fields Too Large | 请求头太大,服务器不愿意处理,因为它的头部字段太大,减少大小后重新提交 |
500 | Internal Server Error | 服务器发生不可预期的错误。服务器出异常了,赶紧看日志去吧 |
503 | Service Unavailable | 服务器尚未准备好处理请求,服务器刚刚启动,还未初始化好 |
响应头 | 解释 |
---|---|
Content-Type | 表示该响应内容的类型,例如 text/html,application/json |
Content-Length | 表示该响应内容的长度,单位 byte |
Content-Encoding | 表示该响应压缩算法,例如 gzip |
Cache-Control | 指示客户端应如何缓存,例如 max-age=300 表示最多缓存300秒 |
Set-Cookie | 告诉浏览器为当前页面所在的域设置 cookie |
4.1.3 WEB 服务器 - Tomcat
① 简介
-
Web 服务器:对 HTTP 协议操作进行封装,简化 web 程序开发;部署 web 项目,对外提供网上信息浏览服务
-
Tomcat:一个轻量级的 web 服务器,支持 servlet、jsp 等少量 javaEE 规范;也被称为 web 容器、servlet 容器
② 基本使用
- 启动:双击 bin\startup.bat
- 停止:双击 bin\shutdown.bat 或 Ctrl+C
- 部署:将项目放置到 webapps 目录下, 即部署完成
- 乱码:修改 conf\logging.properties 为:
java.util.logging.ConsoleHandler.encoding = GBK
- 注意:HTTP 协议默认端口号为80,如果将 Tomcat 端口号改为80,则访问时将不用输入端口号
③ 入门程序解析
- spring-boot-starter-web:包含了 web 应用开发所需要的常见依赖
- spring-boot-starter-test:包含了单元测试所需要的常见依赖
基于 Springboo t开发的 web 应用程序,内置了 tomcat 服务器,当启动类运行时会自动启动
4.1.4 SpringBootWeb 请求
① Postman
Postman 是一款功能强大的网页调试与发送网页 HTTP 请求的 Chrome 插件
作用:常用于进行接口测试
② 简单参数
- 原始方式获取请求参数
Controller 方法形参中声明 HttpServletRequest 对象,调用对象的 getParameter (参数名)
@RequestMapping("/simpleParam")
public String simpleParam(HttpServletRequest request){
String name = request.getParameter("name");
String ageStr = request.getParameter("age");
int age = Integer.parseInt(ageStr);
System.out.println(name+ ":" + age);
return "OK";
}
- SpringBoot 中接收简单参数
请求参数名与方法形参变量名相同,会自动进行类型转换
@RequestMapping("/simpleParam")
public String simpleParam(String name, Integer age){
System.out.println(name+ ":" + age);
return "OK";
}
// get: url: http://localhost:8080/simpleParam?name=Tom&age=20
// post: url: http://localhost:8080/simpleParam; body: name=Tom, age=20
- @RequestParam 注解
方法形参名称与请求参数名称不匹配,通过该注解完成映射
@RequestMapping("/simpleParam")
public String simpleParam(@RequestParam(name = "name") String username, Integer age){
System.out.println(username+ ":" + age);
return "OK";
}
该注解的 required 属性默认是 true,代表请求参数必须传递
③ 实体参数
-
简单实体对象:请求参数名与形参对象属性名相同,定义 POJO 接收即可
@RequestMapping("/simplePojo") public void simplePojo(User user){ System.out.println(user); } // get: ...?name=Tom&age=20
public class User { private String name; private Integer age; ...(javabean) }
-
复杂实体对象:请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套 POJO 属性参数
@RequestMapping("/complexPojo") public void complexPojo(User user){ System.out.println(user); } // get: ...?name=Tom&age=20&Address.province=beijing&Address.city=beijing
public class User { private String name; private Integer age; private Address address; ...(javabean) }
public class Address { private String province; private String city; ...(javabean) }
④ 数组集合参数
- 数组参数:请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数
- 集合参数:请求参数名与形参集合名称相同且请求参数为多个,@RequestParam 绑定参数关系
@RequestMapping("/arrayParam")
public void arrayParam(String[] hobby){
System.out.println(Arrays.toString(hobby)); // [rap, dance]
} // get: ...?hobby=rap&hobby=dance
@RequestMapping("/listParam")
public void listParam(@RequestParam List<String> hobby){
System.out.println(hobby); // [rap, dance]
} // get: ...?hobby=rap&hobby=dance
⑤ 日期参数
使用 @DateTimeFormat 注解完成日期参数格式转换
@RequestMapping("/dateParam")
public void dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime time){
System.out.println(time); // 2022-12-02T10:05:51
} // get: ...?time=2022-12-02 10:05:51
⑥ JSON 参数
JSON 数据键名与形参对象属性名相同,定义 POJO 类型形参即可接收参数,需要使用 @RequestBody 标识
@RequestMapping("/jsonParam")
public void jsonParam(@RequestBody User user){
System.out.println(user);
} // User{name='tom', age=20, address=Address{province='beijing', city='beijing'}}
// post: body: raw:
{
"name":"tom",
"age":20,
"address":{
"province":"beijing",
"city":"beijing"
}
}
⑦ 路径参数
通过请求 URL 直接传递参数,使用 {…} 来标识该路径参数,需要使用 @PathVariable 获取路径参数
@RequestMapping("/path/{id}")
public void pathParam(@PathVariable Integer id){
System.out.println(id); // 123
} // get: http://localhost:8080/path/123
@RequestMapping("/path/{id}/{name}")
public void pathParam2(@PathVariable Integer id , @PathVariable String name){
System.out.println(id + ":" + name); // 123:Alice
} // get: http://localhost:8080/path/123/Alice
4.1.5 SpringBootWeb 响应
① @ResponseBody
- 类型:方法注解、类注解
- 位置:Controller 方法上 / 类上
- 作用:将方法返回值直接响应,如果返回值类型是实体对象/集合 ,将会转换为 JSON 格式响应
- 说明:@RestController = @Controller + @ResponseBody
② 统一响应结果
@RequestMapping("/hello")
public Result hello(){
...
return Result.success("Hello World ~");
}
@RequestMapping("/getAddr")
public Result getAddr(){
...
return Result.success(addr);
}
@RequestMapping("/listAddr")
public Result listAddr(){
...
return Result.success(list);
}
public class Result {
private Integer code ; // 1 成功, 0 失败
private String msg; // 提示信息
private Object data; // 数据 date
public static Result success(){
return new Result(1, "success", null);
}
public static Result success(Object data){
return new Result(1, "success", data);
}
public static Result error(String msg){
return new Result(0, msg, null);
}
...(javabean)
}
③ 案例
需求:a. 获取员工数据,b. 返回统一响应结果,c. 在页面渲染展示
- 在 pom.xml 文件中引入 dom4j 的依赖,用于解析 XML 文件
<!-- pom.xml: 引入依赖, 用来解析xml文件 -->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
- 引入资料中提供的解析 XML 的工具类 XMLParserUtils、对应的实体类 Emp、XML 文件 emp.xml
// XMLParserUtils.java: 工具类, 用来解析xml文件
public class XmlParserUtils {...}
<!-- emp.xml: 员工数据 -->
<emps>
<emp>
<name>金毛狮王</name>
<age>55</age>
<image>https://web-framework.oss-cn-hangzhou.aliyuncs.com/web/1.jpg</image>
<!-- 1: 男, 2: 女 -->
<gender>1</gender>
<!-- 1: 讲师, 2: 班主任 , 3: 就业指导 -->
<job>1</job>
</emp>
...
</emps>
// Emp.java: 员工类
public class Emp {
private String name;
private Integer age;
private String image;
private String gender;
private String job;
...(javabean)
}
- 引入资料中提供的静态页面文件,放在 resources 下的 static 目录下
<!-- emp.html: 展示页面, 用来接收统一结果, c.并渲染展示 -->
<body>
<div id="app">
<el-table :data="tableData" style="width: 100%" stripe border >
...
</el-table>
</div>
</body>
<script>
new Vue({
mounted(){
axios.get('/listEmp').then(res=>{
if(res.data.code){ <!-- 根据 Result 的 code 判断 -->
this.tableData = res.data.data; <!-- 根据 Result 的 data 渲染 -->
}
});
}
});
</script>
注意:Springboot 项目的静态资源(html,css,js 等前端资源)默认存放目录为:classpath : /static 、/public、/resources
- 编写 Controller 程序,处理请求,响应数据
// EmpController.java: Controller程序, a.用来获取员工数据, b.并返回统一响应结果
@RestController
public class EmpController {
@RequestMapping("/listEmp")
public Result list(){
//1. 加载并解析emp.xml
String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
//2. 对数据进行转换处理 - gender, job
empList.stream().forEach(emp -> {
String gender = emp.getGender();
if("1".equals(gender)){
emp.setGender("男");
}else if("2".equals(gender)){
emp.setGender("女");
}
String job = emp.getJob();
if("1".equals(job)){
emp.setJob("讲师");
}else if("2".equals(job)){
emp.setJob("班主任");
}else if("3".equals(job)){
emp.setJob("就业指导");
}
});
//3. 响应数据
return Result.success(empList);
}
}
4.1.6 SpringBootWeb 分层解耦
① 三层架构
- Controller:控制层,接收前端发送的请求,对请求进行处理,并响应数据
public class EmpController {
private EmpService empService = new EmpServiceA(); // 调用service获取数据
@RequestMapping("/listEmp")
public Result list(){
List<Emp> empList = empService.listEmp();
return Result.success(empList); // 3. 响应数据
}
}
- Service:业务逻辑层,处理具体的业务逻辑
public interface EmpService {
public List<Emp> listEmp();
}
public class EmpServiceA implements EmpService {
private EmpDao empDao = new EmpDaoA(); // 调用dao获取数据
@Override
public List<Emp> listEmp() {
List<Emp> empList = empDao.listEmp();
empList.stream().forEach(emp -> {
... // 2. 对数据进行处理
});
return empList; // 返回service
}
}
- Dao:数据访问层 (Data Access Object)(持久层),负责数据访问操作,包括数据的增、删、改、查
public interface EmpDao {
public List<Emp> listEmp();
}
public class EmpDaoA implements EmpDao {
@Override
public List<Emp> listEmp() { // 1. 加载并解析emp.xml
String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
return empList; // 返回 dao
}
}
② 分层解耦
- 内聚:软件中各个功能模块内部的功能联系
- 耦合:衡量软件中各个层 / 模块之间的依赖、关联的程度
- 软件设计原则:高内聚低耦合
分层解耦
- 控制反转: Inversion Of Control,简称 IOC。对象的创建控制权由程序自身转移到外部容器,这种思想称为控制反转
- 依赖注入: Dependency Injection,简称 DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入
- Bean 对象:IOC 容器中创建、管理的对象,称之为 bean
③ IOC & DI
- Service 层及 Dao 层的实现类,交给 IOC 容器管理
@Component // 将当前对象交给 IOC 容器管理,成为 IOC 容器的 bean
public class EmpDaoA implements EmpDao {
}
@Component // 将当前对象交给 IOC 容器管理,成为 IOC 容器的 bean
public class EmpServiceA implements EmpService {
}
- 为 Controller 及 Service 注入运行时,依赖的对象
@Component
public class EmpServiceA implements EmpService {
@Autowired // 运行时,需要从 IOC 容器中获取该类型对象,赋值给该变量
private EmpDao empDao;
}
public class EmpController {
@Autowired // 运行时,需要从 IOC 容器中获取该类型对象,赋值给该变量
private EmpService empService;
}
1. IOC 详解
注解 | 说明 | 位置 |
---|---|---|
@Component | 声明bean的基础注解 | 不属于以下三类时,用此注解 |
@Controller | @Component的衍生注解 | 标注在控制器类上 |
@Service | @Component的衍生注解 | 标注在业务类上 |
@Repository | @Component的衍生注解 | 标注在数据访问类上(与 mybatis 整合,用的少) |
- 声明 bean 的时候,可以通过 value 属性指定 bean 的名字,如果没有指定,默认为类名首字母小写
- 使用以上四个注解都可以声明 bean,但是在 springboot 集成web开发中,声明控制器 bean 只能用 @Controller
@RestController
public class EmpController {...}
@Service
public class EmpServiceA implements EmpService {...}
@Repository("daoA")
public class EmpDaoA implements EmpDao {...}
@ComponentScan 注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解 @SpringBootApplication 中
// @ComponentScan({"com.itheima.dao","com.itheima"})
@SpringBootApplication //默认扫描当前包及其子包
public class SpringbootWebReqRespApplication {...}
2. DI 详解
@Autowired 注解,默认是按照类型进行,如果存在多个相同类型的 bean,将会报出错误
通过以下几种方案来解决:@Primary、@Qualifier、@Resource
public class EmpController {
@Qualifier("empServiceA")
@Autowired
private EmpService empService;
}
public class EmpController {
@Resource(name = "empServiceA")
private EmpService empService;
}
@Primary
@Service
public class EmpServiceA implements EmpService {...}
@Service
public class EmpServiceB implements EmpService {...}
@Resource 与 @Autowired 区别:(面试题)
- @Autowired 是 spring 框架提供的注解,而 @Resource 是 JDK 提供的注解
- @Autowired 默认是按照类型注入,而 @Resource 默认是按照名称注入
4.2 SpringBootWeb AOP
4.2.1 Spring 事务管理
① 事务管理
事务 是一组操作的集合,它是一个不可分割的工作单位,这些操作 要么同时成功,要么同时失败
案例:解散部门:删除部门,同时删除该部门下的员工
- 注解:@Transactional
- 位置:业务(service)层的方法上、类上、接口上
- 作用:将当前方法交给 spring 进行事务管理,方法执行前开启事务;成功执行完毕提交事务;出现异常回滚事务
// main.java.com.itheima.service\impl\EmpServiceImpl.java
@Transactional
@Override
public void delete(Integer id) {
deptMapper.deleteById(id);
int i = 1/0; // 回滚事务, 部门删除失败
empMapper.deleteByDeptId(id); // 异常处理, 未删除部门员工
}
// main.java.com.itheima.mapper\EmpMapper.java
@Delete("delete from emp where dept_id = #{deptId}")
void deleteByDeptId(Integer deptId);
- 默认情况只有 RuntimeException 才回滚异常。rollbackFor 属性用于控制出现何种异常类型,回滚事务
// main.java.com.itheima.service\impl\EmpServiceImpl.java
@Transactional(rollbackFor = Exception.class)
@Override
public void delete(Integer id) throws Exception {
deptMapper.deleteById(id);
if(true){throw new Exception("出错啦...");}
empMapper.deleteByDeptId(id);
}
② 事务传播行为
事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制
属性值 | 含义 |
---|---|
REQUIRED | 需要事务,有则加入,无则创建新事务【默认值】 |
REQUIRES_NEW | 需要新事务,无论有无,总是创建新事务 |
// REQUIRED :大部分情况下都是用该传播行为即可
@Transactional(propagation = Propagation.REQUIRED)
// REQUIRES_NEW: 当我们不希望事务之间相互影响时,可以使用该传播行为
// 比如: 下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功
@Transactional(propagation = Propagation.REQUIRES_NEW)
4.2.2 AOP 基础
① AOP 概述
-
AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),其实就是面向特定方法编程
-
场景:统计各个业务层方法执行耗时
案例部分功能运行较慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方法的执行耗时
-
实现:动态代理是面向切面编程最主流的实现
而 SpringAOP 是 Spring 框架的高级技术,旨在管理 bean 对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程
② AOP 快速入门
-
导入依赖:在 pom.xml 中导入 AOP 的依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
-
编写 AOP 程序:针对于特定方法根据业务需要进行编程
// main.java.com.itheima\aop\TimeAspect.java @Aspect // AOP类 public class TimeAspect { @Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))") // 切入点表达式 public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable { long begin = System.currentTimeMillis(); // 1. 获取方法运行开始时间 Object result = joinPoint.proceed(); // 2. 运行原始方法 long end = System.currentTimeMillis(); // 3. 获取方法运行结束时间,计算执行耗时 log.info(joinPoint.getSignature()+"方法执行耗时: {}ms", end-begin); return result; } }
③ AOP 核心概念
-
AOP 核心概念
-
连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
-
通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
-
切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
-
切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
-
目标对象:Target,通知所应用的对象
-
- AOP 执行流程
4.2.3 AOP 进阶
① 通知类型
@Slf4j
@Component
@Aspect
public class MyAspect1 {
// 该注解将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可
@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
private void pt(){}
@Before("pt()") // 前置通知
public void before(){log.info("before ...");}
@After("pt()") // 后置通知
public void after(){log.info("after ...");}
@AfterReturning("pt()") // 返回后通知, 了解
public void afterReturning(){log.info("afterReturning ...");}
@AfterThrowing("pt()") // 异常后通知, 了解
public void afterThrowing(){log.info("afterThrowing ...");}
@Around("pt()") // 环绕通知, 重点
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
Object result = proceedingJoinPoint.proceed();
log.info("around after ...");
return result;
}
}
② 通知顺序
当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行
-
不同切面类中,默认按照切面类的类名字母排序:
-
目标方法前的通知方法:字母排名靠前的先执行
-
目标方法后的通知方法:字母排名靠前的后执行
-
-
用 @Order(数字)加在切面类上来控制顺序:
-
目标方法前的通知方法:数字小的先执行
-
目标方法后的通知方法:数字小的后执行
-
@Order(2)
public class MyAspect1 {
@Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void before(){log.info("before ...1");}
@After("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void after(){log.info("after ...1");}
}
@Order(3)
public class MyAspect2 {
@Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void before(){log.info("before ...2");}
@After("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void after(){log.info("after ...2");}
}
@Order(1)
public class MyAspect3 {
@Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void before(){log.info("before ...3");}
@After("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void after(){log.info(" ...3");}
}
com.itheima.aop.MyAspect3 : before ...3
com.itheima.aop.MyAspect1 : before ...1
com.itheima.aop.MyAspect2 : before ...2
com.itheima.aop.MyAspect2 : after ...2
com.itheima.aop.MyAspect1 : after ...1
com.itheima.aop.MyAspect3 : after ...3
③ 切入点表达式
- 切入点表达式:描述切入点方法的一种表达式
- 作用:主要用来决定项目中的哪些方法需要加入通知
-
execution (……):根据方法的签名来匹配
语法格式:
execution (访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
@Pointcut("execution(public void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))") @Pointcut("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))") @Pointcut("execution(void delete(java.lang.Integer))") // 包名.类名不建议省略 @Pointcut("execution(void com.itheima.service.DeptService.delete(java.lang.Integer))")
* :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
.. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
@Pointcut("execution(void com.itheima.service.DeptService.*(java.lang.Integer))") @Pointcut("execution(* com.*.service.DeptService.*(*))") @Pointcut("execution(* com.itheima.service.*Service.delete*(*))") @Pointcut("execution(* com.itheima.service.DeptService.*(..))") @Pointcut("execution(* com..DeptService.*(..))") @Pointcut("execution(* com..*.*(..))") @Pointcut("execution(* *(..))") // 慎用
根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式
@Pointcut("execution(* com.itheima.service.DeptService.list()) || " + "execution(* com.itheima.service.DeptService.delete(java.lang.Integer))")
书写建议
- 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是 update开头
- 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
- 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 ..,使用 * 匹配单个包
-
@annotation (……) :根据注解匹配
@annotation 切入点表达式,用于匹配标识有特定注解的方法
// com.itheima.aop\MyAspect.java public class MyAspect { @Pointcut("@annotation(com.itheima.aop.MyLog)") private void pt(){} }
// com.itheima.aop\MyLog.java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyLog {}
// com.itheima.service.impl\DeptServiceImpl.java public class DeptServiceImpl implements DeptService { @MyLog public List<Dept> list() {...} @MyLog public void delete(Integer id) {...} }
④ 连接点
在 Spring 中用 JoinPoint 抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等
-
对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
-
对于其他四种通知,获取连接点信息只能使用 JoinPoint ,它是 ProceedingJoinPoint 的父类型
// com.itheima.aop\MyAspect.java
@Before("pt()")
public void before(JoinPoint joinPoint){}
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getTarget().getClass().getName();
log.info("目标对象的类名:{}", className);
String methodName = joinPoint.getSignature().getName();
log.info("目标方法的方法名: {}",methodName);
Object[] args = joinPoint.getArgs();
log.info("目标方法运行时传入的参数: {}", Arrays.toString(args));
Object result = joinPoint.proceed();
log.info("目标方法运行的返回值: {}",result);
return result;
}
4.2.4 AOP 案例
需求:将 SpringBootWeb 案例中 增、删、改 相关接口的操作日志记录到数据库表中
日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长
思路:
-
需要对所有业务类中的增、删、改 方法添加统一功能,使用 AOP 技术最为方便
-
由于增、删、改 方法名没有规律,可以自定义 @Log 注解完成目标方法匹配
准备:
-
在案例工程中引入 AOP 的起步依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
-
导入资料中准备好的数据库表结构,并引入对应的实体类
create table operate_log( id int unsigned primary key auto_increment comment 'ID', operate_user int unsigned comment '操作人ID', operate_time datetime comment '操作时间', class_name varchar(100) comment '操作的类名', method_name varchar(100) comment '操作的方法名', method_params varchar(1000) comment '方法参数', return_value varchar(2000) comment '返回值', cost_time bigint comment '方法执行耗时, 单位:ms' ) comment '操作日志表';
// com.itheima.pojo\OperateLog.java @Data @NoArgsConstructor @AllArgsConstructor public class OperateLog { private Integer id; // ID private Integer operateUser; // 操作人ID private LocalDateTime operateTime; // 操作时间 private String className; // 操作类名 private String methodName; // 操作方法名 private String methodParams; // 操作方法参数 private String returnValue; // 操作方法返回值 private Long costTime; // 操作耗时 }
// com.itheima.mapper\OperateLogMapper.java @Mapper public interface OperateLogMapper { @Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});") public void insert(OperateLog log); }
编码:
-
自定义注解 @Log
// com.itheima.anno\Log.java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Log {}
-
定义切面类,完成记录操作日志的逻辑
@Component @Aspect // 切面类 public class LogAspect { @Autowired private HttpServletRequest request; @Autowired private OperateLogMapper operateLogMapper; @Around("@annotation(com.itheima.anno.Log)") public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable { // 操作人ID String jwt = request.getHeader("token"); // 获取请求头中的jwt令牌, 解析令牌 Claims claims = JwtUtils.parseJWT(jwt); Integer operateUser = (Integer) claims.get("id"); // 操作时间 LocalDateTime operateTime = LocalDateTime.now(); // 操作类名 String className = joinPoint.getTarget().getClass().getName(); // 操作方法名 String methodName = joinPoint.getSignature().getName(); // 操作方法参数 Object[] args = joinPoint.getArgs(); String methodParams = Arrays.toString(args); long begin = System.currentTimeMillis(); Object result = joinPoint.proceed(); // 调用原始目标方法运行 long end = System.currentTimeMillis(); // 操作方法返回值 String returnValue = JSONObject.toJSONString(result); // 操作耗时 Long costTime = end - begin; // 记录操作日志 OperateLog operateLog = new OperateLog (null,operateUser,operateTime,className,methodName,methodParams,returnValue,costTime); operateLogMapper.insert(operateLog); return result; } }
// com.itheima.controller\DeptController.java @Log public Result delete(@PathVariable Integer id) throws Exception {...} @Log public Result add(@RequestBody Dept dept){...} // com.itheima.controller\EmpController.java @Log public Result delete(@PathVariable List<Integer> ids){...} @Log public Result save(@RequestBody Emp emp){...} @Log public Result update(@RequestBody Emp emp){...}
4.3 SpringBoot 原理
4.3.1 配置优先级
-
SpringBoot 中支持三种格式的配置文件:application.properties、application.yml、application.yaml
虽然 springboot 支持多种格式配置文件,但是在项目开发时,推荐统一使用一种格式的配置 (yml 是主流)
-
SpringBoot 除了支持配置文件属性配置,还支持 Java 系统属性和命令行参数的方式进行属性配置
- 打包前如何配置:
- 打包后如何配置:
java -Dserver.port=9000 -jar xxxx.jar --server.port=10010
- 优先级(低→高):yaml、yml、properties、java 系统属性、命令行参数
4.3.2 Bean 管理
① 获取 Bean
默认情况下,Spring 项目启动时,会把 bean 都创建好放在 IOC 容器中,如果想要主动获取这些 bean,可以通过如下方式:
@Autowired
private ApplicationContext applicationContext; // IOC 容器对象
@Test
public void testGetBean(){ // 获取 bean 对象
// 根据 bean 的名称获取
DeptController bean1 = (DeptController) applicationContext.getBean("deptController");
// 根据 bean 的类型获取
DeptController bean2 = applicationContext.getBean(DeptController.class);
// 根据 bean 的名称及类型获取
DeptController bean3 = applicationContext.getBean("deptController", DeptController.class);
}
② Bean 作用域
作用域 | 说明 |
---|---|
singleton | 容器内同名称 的 bean 只有一个实例(单例)(默认) |
prototype | 每次使用该 bean 时会创建新的实例(非单例) |
@Scope("prototype")
public class DeptController {
public DeptController(){
System.out.println("DeptController constructor ....");
}
}
@Test
public void testScope(){
for (int i = 0; i < 3; i++) {
DeptController deptController = applicationContext.getBean(DeptController.class);
System.out.println(deptController);
}
}
// DeptController constructor ....
// com.itheima.controller.DeptController@34d9df9f
// DeptController constructor ....
// com.itheima.controller.DeptController@35c8be21
// DeptController constructor ....
// com.itheima.controller.DeptController@60807fd9
③ 第三方 Bean
-
如果要管理的 bean 对象来自于第三方,是无法用 @Component 及衍生注解声明 bean 的,就需要用到 @Bean 注解
-
若要管理的第三方 bean 对象,建议对这些 bean 进行集中分类配置,可以通过 @Configuration 注解声明一个配置类
// com.itheima.config\CommonConfig.java
@Configuration // 配置类
public class CommonConfig {
@Bean // 将当前方法的返回值对象交给 IOC 容器管理, 成为 IOC 容器 bean
public SAXReader reader(){
return new SAXReader();
}
}
@Autowired
private SAXReader saxReader;
@Test // 第三方 bean 的管理
public void testThirdBean() throws Exception {...}
-
通过 @Bean 注解的 name 或 value 属性可以声明 bean 的名称,如果不指定,默认 bean 的名称就是方法名
-
如果第三方 bean 需要依赖其它 bean 对象,直接在 bean 定义方法中设置形参即可,容器会根据类型自动装配
@Configuration
public class CommonConfig {
@Bean
public SAXReader reader(DeptService deptService){
System.out.println(deptService);
return new SAXReader();
}
}
4.3.3 SpringBoot 原理
① 起步依赖
② 自动配置
当 spring 容器启动后,一些配置类、bean 对象就自动存入到了 IOC 容器中,从而简化了开发,省去了繁琐的配置操作
@Autowired
private DataSource dataSource;
@Test
public void testDataSource(){...}
A. 原理
方案一:@ComponentScan 组件扫描
@ComponentScan({"com.example","com.itheima","com.alibaba","com.google","org.springframework",...})
@SpringBootApplication
public class SpringbootWebConfig2Application {} // => 使用繁琐, 性能低
方案二:@Import 导入。使用 @Import 导入的类会被 Spring 加载到 IOC 容器中,导入形式主要有以下几种:
@Import({TokenParser.class}) // 1. 导入普通类
@Import({HeaderConfig.class}) // 2. 导入配置类
@Import({MyImportSelector.class}) // 3. 导入 ImportSelector 接口实现类
@EnableHeaderConfig // 4. @Enablexxxx 注解,封装 @Import 注解
@SpringBootApplication
public class SpringbootWebConfig2Application {} // => 方便, 优雅
@Component // a. 普通类
public class TokenParser {}
@Configuration // b. 配置类
public class HeaderConfig {
@Bean
public HeaderParser headerParser(){return new HeaderParser();}
@Bean
public HeaderGenerator headerGenerator(){return new HeaderGenerator();}
}
public class HeaderParser {}
public class HeaderGenerator {}
// c. ImportSelector 接口实现类
public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"com.example.HeaderConfig"};
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)
public @interface EnableHeaderConfig {} // d. 封装 @Import 注解
B. 源码跟踪
@SpringBootApplication 标识在 SpringBoot 工程引导类上,是 SpringBoot 中最最最重要的注解。该注解由三个部分组成:
- @SpringBootConfiguration:该注解与 @Configuration 注解作用相同,用来声明当前也是一个配置类
- @ComponentScan:组件扫描,默认扫描当前引导类所在包及其子包
- @EnableAutoConfiguration:SpringBoot实现自动化配置的核心注解
C. @Conditional
- 作用:按照一定的条件进行判断,在满足给定条件后才会注册对应的 bean 对象到 Spring IOC 容器中
- 位置:方法、类
- @Conditional 本身是一个父注解,派生出大量的子注解:
- @ConditionalOnClass:判断环境中是否有对应字节码文件,才注册 bean 到 IOC 容器
- @ConditionalOnMissingBean:判断环境中没有对应的 bean(类型或名称),才注册 bean 到 IOC 容器
- @ConditionalOnProperty:判断配置文件中有对应属性和值,才注册 bean 到 IOC 容器
③ 配置案例
-
场景:在实际开发中,经常会定义一些公共组件,提供给各个项目团队使用
而在 SpringBoot 的项目中,一般会将这些公共组件封装为SpringBoot 的 starter
需求:自定义 aliyun-oss-spring-boot-starter,完成阿里云 OSS 操作工具类 AliyunOSSUtils 的自动配置
目标:引入起步依赖引入之后,要想使用阿里云 OSS,注入 AliyunOSSUtils 直接使用即可
- 基础工程
- 创建 aliyun-oss-spring-boot-starter 模块
- 创建 aliyun-oss-spring-boot-autoconfigure 模块,在 starter 中引入该模块
<!-- aliyun-oss-spring-boot-starter\pom.xml -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-oss-spring-boot-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
- 在 aliyun-oss-spring-boot-autoconfigure 模块中定义自动配置功能
<!-- aliyun-oss-spring-boot-autoconfigure\pom.xml -->
<!-- 阿里云 OSS -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
<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>
// java.com.aliyun.oss\AliOSSProperties.java
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
...(javabean)
}
// java.com.aliyun.oss\AliOSSUtils.java
public class AliOSSUtils {
private AliOSSProperties aliOSSProperties;
public AliOSSProperties getAliOSSProperties() {
return aliOSSProperties;
}
public void setAliOSSProperties(AliOSSProperties aliOSSProperties) {
this.aliOSSProperties = aliOSSProperties;
}
public String upload(MultipartFile file) throws IOException {...}
}
// java.com.aliyun.oss\AliOSSAutoConfiguration.java
@Configuration
@EnableConfigurationProperties(AliOSSProperties.class)
public class AliOSSAutoConfiguration {
@Bean
public AliOSSUtils aliOSSUtils(AliOSSProperties aliOSSProperties){
AliOSSUtils aliOSSUtils = new AliOSSUtils();
aliOSSUtils.setAliOSSProperties(aliOSSProperties);
return aliOSSUtils;
}
}
- 定义自动配置文件 META-INF/spring/xxxx.imports
// resources.META-INF.spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.aliyun.oss.AliOSSAutoConfiguration
- 测试工程
<!-- springboot-autoconfiguration-test\pom.xml -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-oss-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
# resources\application.yml
aliyun:
oss:
endpoint: https://oss-cn-beijing.aliyuncs.com
accessKeyId: LTAI5tSH18FLKGFhWgsuJsGS
accessKeySecret: 5wPyUojv5seKI5XqsTbrexcHHIoCwD
bucketName: web-framework2024-5-8-01
@RestController
public class UploadController {
@Autowired
private AliOSSUtils aliOSSUtils;
@PostMapping("/upload")
public String upload(MultipartFile image) throws Exception {
String url = aliOSSUtils.upload(image);
return url;
}
}
// postman: post: body: form-data: image(file) => test.jpg
// https://web-framework2024-5-8-01.oss-cn-beijing.aliyuncs.com/dbb28c6d-9dfe-4e87-a6e0-4bc392ebd02f.jpg
第5章 - MySQL & Mybatis
5.1 MySQL
5.1.1 MySQL 概述
① 课程介绍
数据库:DataBase(DB),是存储和管理数据的仓库
数据库管理系统:DataBase Management System (DBMS),操纵和管理数据库的大型软件
SQL:Structured Query Language,操作关系型数据库的编程语言,定义了一套操作关系型数据库统一标准
MySQL 连接:mysql -uroot -p1234
MySQL 企业开发使用方式:mysql -h192.168.150.101 -P3306 -uroot -p1234
② 数据模型
关系型数据库(RDBMS): 建立在关系模型基础上,由多张相互连接的二维表组成的数据库
③ SQL 简介
SQL:一门操作关系型数据库的编程语言,定义操作所有关系型数据库的统一标准
SQL 通用语法
- SQL 语句可以单行 / 多行书写,以分号结束
- SQL 语句中可以增加缩进 / 空格来增强可读性
- SQL 语句中的关键字不区分大小写
- SQL 语句注释:单行注释(-- 注释),多行注释(/* 注释 */)
分类 | 全称 | 说明 |
---|---|---|
DDL | Data Definition Language | 数据定义语言,用来定义数据库对象 (数据库,表,字段) |
DML | Data Manipulation Language | 数据操作语言,用来对数据库表中的数据进行增删改 |
DQL | Data Query Language | 数据查询语言,用来查询数据库中表的记录 |
DCL | Data Control Language | 数据控制语言,用来创建数据库用户、控制数据库的访问权限 |
5.1.2 数据库设计 - DDL
① 项目开发流程
② 数据库操作
show databases; -- 查询所有数据库
select database(); -- 查询当前数据库
create database db01; -- 创建数据库
use db01; -- 使用数据库
drop db01; -- 删除数据库
# 注: 上述语法中的 database, 也可以替换成 schema. 如:create schema db01
图形化工具:
③ 表结构操作
1. 表结构 - 创建
create table tb_user(
id int primary key auto_increment comment 'ID, 唯一标识',
username varchar(20) not null unique comment '用户名',
name varchar(10) not null comment '姓名',
age int comment '年龄',
gender char(1) default '男' comment '性别'
) comment '用户表';
约束 | 描述 | 关键字 |
---|---|---|
非空约束 | 限制该字段值不能为null | not null |
唯一约束 | 保证字段的所有数据都是唯一、不重复的 | unique |
主键约束 | 主键是一行数据的唯一标识,要求非空且唯一 | primary key |
默认约束 | 保存数据时,如果未指定该字段值,则采用默认值 | default |
外键约束 | 让两张表的数据建立连接,保证数据的一致性和完整性 | foreign key |
类型 | 大小 (byte) | 有符号范围 | 无符号范围 | 描述 |
---|---|---|---|---|
tinyint | 1 | (-128,127) | (0,255) | 小整数值 |
smallint | 2 | (-32768,32767) | (0,65535) | 大整数值 |
mediumint | 3 | (-8388608,8388607) | (0,16777215) | 大整数值 |
int | 4 | (-2147483648,2147483647) | (0,4294967295) | 大整数值 |
bigint | 8 | (-263,263-1) | (0,2^64-1) | 极大整数值 |
float | 4 | 单精度浮点数值 | ||
double | 8 | 双精度浮点数值 | ||
decimal | 小数值 (精度更高) |
类型 | 大小 | 描述 |
---|---|---|
char | 0-255 bytes | 定长字符串 |
varchar | 0-65535 bytes | 变长字符串 |
tinyblob | 0-255 bytes | 不超过255个字符的二进制数据 |
tinytext | 0-255 bytes | 短文本字符串 |
blob | 0-65 535 bytes | 二进制形式的长文本数据 |
text | 0-65 535 bytes | 长文本数据 |
mediumblob | 0-16 777 215 bytes | 二进制形式的中等长度文本数据 |
mediumtext | 0-16 777 215 bytes | 中等长度文本数据 |
longblob | 0-4 294 967 295 bytes | 二进制形式的极大文本数据 |
longtext | 0-4 294 967 295 bytes | 极大文本数据 |
类型 | 大小 | 范围 | 格式 | 描述 |
---|---|---|---|---|
date | 3 | 1000-01-01 至 9999-12-31 | YYYY-MM-DD | 日期值 |
time | 3 | -838:59:59 至 838:59:59 | HH:MM:SS | 时间值或持续时间 |
year | 1 | 1901 至 2155 | YYYY | 年份值 |
datetime | 8 | 1000-01-01 00:00:00 至 9999-12-31 23:59:59 | YYYY-MM-DD HH:MM:SS | 混合日期和时间值 |
timestamp | 4 | 1970-01-01 00:00:01 至 2038-01-19 03:14:07 | YYYY-MM-DD HH:MM:SS | 混合日期和时间值,时间戳 |
2. 案例
需求:参考资料中提供的页面原型,设计员工管理模块的表结构 (暂不考虑所属部门字段)
注:添加员工时,会给员工设置一个默认的密码 123456,添加完成后,员工就可以通过该密码登录该后台管理系统了
注:create_time 记录的是当前这条数据插入的时间,update_time 记录当前这条数据最后更新的时间
3. 表结构 - 查询、修改、删除
-- 查看: 当前数据库下的表
show tables;
-- 查看: 查看指定表结构
desc tb_emp;
-- 查看: 数据库的建表语句 # 右键 -> Edit Source
show create table tb_emp;
-- 修改: 为表 tb_emp 添加字段 qq varchar(11)
alter table tb_emp add qq varchar(11) comment 'QQ';
-- 修改: 修改 tb_emp 字段类型 qq varchar(13) # 右键 -> Modify Table
alter table tb_emp modify qq_num varchar(13) comment 'QQ';
-- 修改: 修改 tb_emp 字段名 qq 为 qq_num varchar(13) # 右键 -> rename
alter table tb_emp change qq_num qq_num varchar(13) comment 'QQ';
-- 修改: 删除 tb_emp 的 qq_num 字段
alter table tb_emp drop column qq_num;
-- 修改: 将tb_emp 表名修改为 emp
rename table tb_emp to emp;
-- 删除: 删除 tb_emp 表 # 右键 -> Drop
drop table if exists tb_emp;
5.1.3 数据库操作 - DML
① 增加(insert)
-- 1. 为 tb_emp 表的 username, name, gender 字段插入值
insert into tb_emp(username,name,gender,create_time,update_time) values ('wuji','张无忌',1,now(),now());
-- 2. 为 tb_emp 表的 所有字段插入值
insert into tb_emp(id, username, password, name, gender, image, job, entrydate, create_time, update_time)
values (null,'zhiruo','123','周芷若',2,'1.jpg',1,'2010-01-01',now(),now());
insert into tb_emp values (null,'zhiruo2','123','周芷若',2,'1.jpg',1,'2010-01-01',now(),now());
-- 3. 批量为 为 tb_emp 表的 username , name , gender 字段插入数据
insert into tb_emp(username,name,gender,create_time,update_time) values
('weifuwang','韦一笑',1,now(),now()),('xieshiwang','谢逊',1,now(),now());
# 插入数据时,指定的字段顺序需要与值的顺序是一一对应的
# 字符串和日期型数据应该包含在引号中
# 插入的数据大小,应该在字段的规定范围内
② 修改(update)
-- 1. 将 tb_emp 表的ID为1员工 姓名name字段更新为 '张三'
update tb_emp set name = '张三' , update_time = now() where id = 1;
-- 2. 将 tb_emp 表的所有员工的入职日期更新为 '2010-01-01'
update tb_emp set entrydate = '2010-01-01', update_time = now();
# 修改语句的条件可以有,也可以没有,如果没有条件,则会修改整张表的所有数据
③ 删除(delete)
-- 1. 删除 tb_emp 表中 ID为1的员工
delete from tb_emp where id = 1;
-- 2. 删除 tb_emp 表中的所有员工
delete from tb_emp;
# DELETE 语句的条件可以有,也可以没有,如果没有条件,则会删除整张表的所有数据
# DELETE 语句不能删除某一个字段的值(如果要操作,可以使用UPDATE,将该字段的值置为NULL)
5.1.4 数据库操作 - DQL
① 介绍
DQL 英文全称是 Data Query Language (数据查询语言),用来查询数据库表中的记录
② 基本查询
-- 1. 查询指定字段 name,entrydate 并返回
select name,entrydate from emp ;
-- 2. 查询返回所有字段
select id, username, password, name, gender, image, job, entrydate, create_time, update_time from emp;
select * from emp; # * 号代表查询所有字段, 在实际开发中尽量少用(不直观、影响效率)
-- 3. 查询所有员工的 name,entrydate, 并起别名(姓名、入职日期)
select name as '姓名' ,entrydate as '入职日期' from emp ;
select name '姓名' ,entrydate '入职日期' from emp ;
-- 4. 查询员工有哪几种职位(不要重复) -- distinct
select distinct job from emp;
select * from emp where id = 1;
③ 条件查询
-- 1. 查询 姓名 为 杨逍 的员工
select * from emp where name = '杨逍';
-- 2. 查询在 id小于等于5 的员工信息
select * from emp where id <= 5;
-- 3. 查询 没有分配职位 的员工信息 -- 判断 null , 用 is null
select * from emp where job is null;
-- 4. 查询 有职位 的员工信息 -- 判断 不是null , 用 is not null
select * from emp where job is not null ;
-- 5. 查询 密码不等于 '123456' 的员工信息
select * from emp where password <> '123456';
select * from emp where password != '123456';
-- 6. 查询入职日期 在 '2000-01-01' (包含) 到 '2010-01-01'(包含) 之间的员工信息
select * from emp where entrydate between '2000-01-01' and '2010-01-01' ;
-- 7. 查询 入职时间 在 '2000-01-01' (包含) 到 '2010-01-01'(包含) 之间 且 性别为女 的员工信息
select * from emp where (entrydate between '2000-01-01' and '2010-01-01') and gender = 2;
-- 8. 查询 职位是 2 (讲师), 3 (学工主管), 4 (教研主管) 的员工信息
select * from emp where job = 2 or job = 3 or job = 4;
select * from emp where job in (2,3,4);
-- 9. 查询姓名为两个字的员工信息
select * from emp where name like '__';
-- 10. 查询姓 '张' 的员工信息
select * from emp where name like '张%';
-- 11. 查询姓名中包含 '三' 的员工信息
select * from emp where name like '%三%';
④ 分组查询
- 聚合函数
-- 1. 统计该企业员工数量
-- A. count(字段)
select count(id) from emp;
-- B. count(*) -- 推荐
select count(*) from emp;
-- C. count(值)
select count(1) from emp;
-- 2. 统计该企业员工 ID 的平均值
select avg(id) from emp;
-- 3. 统计该企业最早入职的员工的入职日期
select min(entrydate) from emp;
-- 4. 统计该企业最近入职的员工的入职日期
select max(entrydate) from emp;
-- 5. 统计该企业员工的 ID 之和
select sum(id) from emp;
- 分组查询
-- 1. 根据性别分组 , 统计男性和女性员工的数量
select gender,count(*) from emp group by gender;
-- 2. 先查询入职时间在 '2015-01-01' (包含) 以前的员工 , 并对结果根据职位分组 , 获取员工数量大于等于2的职位
select job,count(*) from emp where entrydate <= '2015-01-01' group by job having count(*) >= 2;
# 分组之后,查询的字段一般为聚合函数和分组字段,查询其他字段无任何意义
# 执行顺序: where > 聚合函数 > having
where 与 having 区别:(面试题)
- 执行时机不同:where 是分组之前进行过滤,不满足 where 条件,不参与分组;而 having 是分组之后对结果进行过滤
- 判断条件不同:where 不能对聚合函数进行判断,而 having 可以
⑤ 排序查询
-- 1. 根据入职时间, 对员工进行升序排序 -- 排序条件
select * from emp order by entrydate ;
-- 2. 根据入职时间, 对员工进行降序排序
select * from emp order by entrydate desc;
-- 3. 根据 入职时间 对公司的员工进行 升序排序 , 入职时间相同 , 再按照 ID 进行降序排序
select * from emp order by entrydate asc , id desc ;
⑥ 分页查询
-- 1. 查询第1页员工数据, 每页展示10条记录
select * from emp limit 0,10;
select * from emp limit 10;
-- 2. 查询第2页员工数据, 每页展示10条记录
select * from emp limit 10,10;
-- 公式 : 起始索引 = (页码 - 1) * 每页记录数
⑦ 案例
# 需求 : 员工管理列表查询 , 根据最后操作时间, 进行倒序排序
# 条件 : name , gender , entrydate
select *
from tb_emp
where name like '%张%'
and gender = 1
and entrydate between '2000-01-01' and '2010-01-01'
order by update_time desc
limit 0,10;
# 需求: 男性与女性员工的人数统计 (1 : 男性员工 , 2 : 女性员工)
-- 函数: if(条件表达式 , true , false)
select if(gender = 1, '男性员工', '女性员工') '性别',
count(*) '人数'
from emp
group by gender;
# 需求: 员工职位信息
-- 函数: case when ... then ... when ... then ... else ... end
select (case
when job = 1 then '班主任'
when job = 2 then '讲师'
when job = 3 then '教研主管'
when job = 4 then '学工主管'
else '无职位' end) '职位',
count(*)
from emp
group by job;
-- 函数: case ... when ... then ... when ... then ... else ... end
select (case job
when 1 then '班主任'
when 2 then '讲师'
when 3 then '教研主管'
when 4 then '学工主管'
else '无职位' end) '职位',
count(*)
from emp
group by job;
5.1.5 多表设计 & 查询
① 多表设计
1. 一对多
一对多关系实现:在数据库表中多的一方,添加字段,来关联一的一方的主键
# 根据页面原型及需求文档,完成部门及员工模块的表结构设计
# 部门名称,必填,唯一,长度为2-10位
-- 部门
create table tb_dept (
id int unsigned primary key auto_increment comment 'ID',
name varchar(10) not null unique comment '部门名称',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间'
) comment '部门表';
-- 员工
create table tb_emp (
dept_id int unsigned comment '归属的部门ID'
...
) comment '员工表';
目前上述的两张表,在数据库层面,并未建立关联,所以是无法保证数据的一致性和完整性的
2. 一对一
一对一关系,多用于单表拆分,将一张表的基础字段放在一张表中,其他字段放在另一张表中,以提升操作效率
- 案例 : 用户 与 身份证信息 的关系
- 实现 : 在任意一方加入外键,关联另外一方的主键,并且设置外键为唯一的 (UNIQUE)
3. 多对多
一个学生可以选修多门课程,一门课程也可以供多个学生选择
- 案例 : 学生 与 课程的关系
- 实现 : 建立第三张中间表,中间表至少包含两个外键,分别关联两方主键
4. 案例
参考资料中提供的页面原型,设计分类管理、菜品管理、套餐管理模块的表结构
1.阅读页面原型及需求文档,分析各个模块涉及到的表结构,及表结构之间的关系
2.根据页面原型及需求文档,分析各个表结构中具体的字段及约束
-
分类表
新增菜品 / 套餐名称:限制字符范围: 2-20字符
排序不能为空,内容限制:0-99整数数字
新增菜品 / 套餐分类,状态默认为 停用
- 菜品表
- 套餐表
- 套餐菜品关系表
② 多表查询
1. 概述
- 多表查询 : 指从多张表中查询数据
select * from tb_emp,tb_dept;
- 笛卡尔积 : 两个集合的所有组合情况 (在多表查询时,需要消除无效的笛卡尔积)
select * from tb_emp,tb_dept where tb_emp.dept_id = tb_dept.id;
- 连接查询 : 内连接、外连接、子查询
2. 内连接
-- A. 查询员工的姓名 , 及所属的部门名称 (隐式内连接实现)
select tb_emp.name,tb_dept.name from tb_emp,tb_dept where tb_emp.dept_id = tb_dept.id;
select e.name, d.name from tb_emp e, tb_dept d where e.dept_id = d.id; -- 起别名
-- B. 查询员工的姓名 , 及所属的部门名称 (显式内连接实现)
select tb_emp.name,tb_dept.name from tb_emp join tb_dept on tb_emp.dept_id = tb_dept.id;
3. 外连接
-- A. 查询员工表 所有 员工的姓名, 和对应的部门名称 (左外连接)
select e.name, d.name from tb_emp e left join tb_dept d on e.dept_id = d.id;
select e.name, d.name from tb_dept d right join tb_emp e on e.dept_id = d.id; -- 左右转换
-- B. 查询部门表 所有 部门的名称, 和对应的员工名称 (右外连接)
select e.name, d.name from tb_emp e right join tb_dept d on e.dept_id = d.id;
4. 子查询
-- A. 标量子查询: 子查询返回的结果为单个值. 常用的操作符: =, <>, >, >=, <, <=
-- 查询 "教研部" 的所有员工信息
select * from tb_emp where dept_id = (select id from tb_dept where name = '教研部');
-- 查询在 "方东白" 入职之后的员工信息
select * from tb_emp where entrydate > (select entrydate from tb_emp where name = '方东白');
-- B. 列子查询: 子查询返回的结果为一列. 常用的操作符: in, not in
-- 查询 "教研部" 和 "咨询部" 的所有员工信息
select * from tb_emp where dept_id in (select id from tb_dept where name = '教研部' or name = '咨询部');
-- C. 行子查询: 子查询返回的结果为一行. 常用的操作符: =, <>, in, not in
-- 查询与 "韦一笑" 的入职日期 及 职位都相同的员工信息 ;
select * from tb_emp where (entrydate,job)=(select entrydate,job from tb_emp where name = '韦一笑');
-- D. 表子查询: 子查询返回的结果为多行多列.
-- 查询入职日期是 "2006-01-01" 之后的员工信息 , 及其部门名称
select e.*, d.name from (select * from tb_emp where entrydate > '2006-01-01') e, tb_dept d where e.dept_id = d.id;
5. 案例
-- 1. 查询价格低于10元的菜品的名称、价格及其菜品的分类名称
-- 表: dish , category
select d.name, d.price, c.name
from dish d, category c
where d.category_id = c.id and d.price < 10;
-- 2. 查询所有价格在10元到50元之间且状态为'起售'的菜品, 展示出菜品的名称、价格及其菜品的分类名称
-- 表: dish , category
select d.name, d.price, c.name
from dish d left join category c on d.category_id = c.id
where d.price between 10 and 50 and d.status = 1;
-- 3. 查询每个分类下最贵的菜品, 展示出分类的名称、最贵的菜品的价格
-- 表: dish , category
select c.name, max(d.price)
from dish d, category c
where d.category_id = c.id group by c.name;
-- 4. 查询各个分类下菜品状态为'起售', 并且该分类下菜品总数量大于等于3的分类名称 .
-- 表: dish, category
select c.name, count(*)
from dish d, category c
where d.category_id = c.id and d.status = 1
group by c.name having count(*) >= 3;
-- 5. 查询出 "商务套餐A" 中包含了哪些菜品 (展示出套餐名称、价格, 包含的菜品名称、价格、份数).
-- 表: setmeal, setmeal_dish, dish
select s.name, s.price, d.name, d.price, sd.copies
from setmeal s, setmeal_dish sd, dish d
where s.id = sd.setmeal_id and sd.dish_id = d.id and s.name = '商务套餐A';
-- 6. 查询出低于菜品平均价格的菜品信息 (展示出菜品名称、菜品价格).
-- 表: dish
select * from dish where price < (select avg(price) from dish);
5.1.6 事务 & 索引
① 事务
-
事务介绍
事务是一组操作的集合,它是一个不可分割的工作单位
事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败
-
事务操作
# 场景:学工部 整个部门解散了,该部门及部门下的员工都需要删除了 # 问题:如果删除部门成功了,而删除该部门的员工时失败了,就造成了数据的不一致 -- 开启事务 start transaction; -- 删除部门 delete from tb_dept where id = 2; -- 删除部门下的员工 delete from tb_emp where dept_id == 2; -- 提交事务 commit; -- 回滚事务 rollback;
-
事务四大特性(面试题)
-
原子性(Atomicity):事务是不可分割的最小单元,要么全部成功,要么全部失败
-
一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态
-
隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行
-
持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的
-
② 索引
-
介绍
索引(index)是帮助数据库 高效获取数据 的 数据结构
select * from tb_sku where sn = '100000003145008'; -- 14s
select count(*) from tb_sku; -- 6000000
create index idx_sku_sn on tb_sku(sn);
select * from tb_sku where sn = '100000003145008'; -- 6ms
优点
-
提高数据查询的效率,降低数据库的IO成本
-
通过索引列对数据进行排序,降低数据排序的成本,降低CPU消耗
缺点(忽略不计)
-
索引会占用存储空间
-
索引大大提高了查询效率,同时却也降低了insert、update、delete的效率
-
结构
MySQL 数据库支持的索引结构有很多,如果没有特别指明,都是指默认的 B+Tree 结构组织的索引
无论是二叉搜索树,还是红黑树,在大数据量情况下,层级深,检索速度慢
B+Tree(多路平衡搜索树):
- 每一个节点,可以存储多个key(有n个key,就有n个指针)
- 所有的数据都存储在叶子节点,非叶子节点仅用于索引数据
- 叶子节点形成了一颗双向链表,便于数据的排序及区间范围查询
-
语法
-- 创建 : 为 tb_emp 表的 name 字段建立一个索引 create index idx_emp_name on tb_emp(name); -- 查询 : 查询 tb_emp 表的索引信息 show index from tb_emp; -- 删除: 删除 tb_emp 表中 name 字段的索引 drop index idx_emp_name on tb_emp;
- 主键字段,在建表时,会自动创建主键索引
- 添加唯一约束时,数据库实际上会添加唯一索引
5.2 Mybatis
5.2.1 快速入门
① 入门程序
MyBatis 是一款优秀的持久层框架,用于简化 JDBC 的开发
需求:使用 Mybatis 查询所有用户数据
- 准备工作(创建 springboot 工程、数据库表 user、实体类 User)
// src\main\java\com.itheima\pojo\User.java
public class User {
private Integer id;
private String name;
private Short age;
private Short gender;
private String phone;
...(javabean)
}
-
引入 Mybatis 的相关依赖,配置 Mybatis(数据库连接信息)
<!--mybatis 的起步依赖--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <!-- mysql 驱动包--> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>
# 驱动类名称 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # 数据库连接的 url spring.datasource.url=jdbc:mysql://localhost:3306/mybatis # 连接数据库的用户名 spring.datasource.username=root # 连接数据库的密码 spring.datasource.password=123456
-
编写SQL语句(注解 / XML)
// src\main\java\com.itheima\mapper\UserMapper.java @Mapper public interface UserMapper { @Select("select * from user") public List<User> list(); }
// src\test\java\com.itheima\Test.java @SpringBootTest class SpringbootMybatisQuickstartApplicationTests { @Autowired private UserMapper userMapper; @Test public void testListUser(){ List<User> userList = userMapper.list(); userList.stream().forEach(user -> { System.out.println(user); }); } }
② 配置 SQL 提示
- 默认在 mybatis 中编写 SQL 语句是不识别的。可以做如下配置:
-
数据库表识别不了
-
产生原因:Idea 和数据库没有建立连接,不识别表信息
-
解决方式:在 Idea 中配置 MySQL 数据库连接
-
③ JDBC 简介
JDBC ( Java DataBase Connectivity ),就是使用 Java 语言操作关系型数据库的一套 API
5.2.2 连接池 & lombok
① 连接池
数据库连接池是一个容器,负责分配、管理数据库连接 (Connection)
-
优势:资源复用、提升系统响应速度
-
接口:DataSource
-
产品:Druid、Hikari
Druid(德鲁伊)连接池是阿里巴巴开源的数据库连接池项目,是 Java 语言最好的数据库连接池之一
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.8</version> </dependency>
② lombok
Lombok 是一个实用的 Java 类库,能通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString 等方法(即 javabean),并可以自动化生成日志变量,简化 java 开发、提高效率
注解 | 作用 |
---|---|
@Getter / @Setter | 为所有的属性提供 get / set 方法 |
@ToString | 会给类自动生成易阅读的 toString 方法 |
@EqualsAndHashCode | 根据类所拥有的非静态字段自动重写 equals 方法和 hashCode 方法 |
@Data | 提供了更综合的生成代码功能(@Getter + @Setter + @ToString + @EqualsAndHashCode) |
@NoArgsConstructor | 为实体类生成无参的构造器方法 |
@AllArgsConstructor | 为实体类生成除了static修饰的字段之外带有各参数的构造器方法 |
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer id;
private String name;
private Short age;
private Short gender;
private String phone;
}
5.2.3 基础操作
① 需求
根据资料中提供的页面原型及需求,完成员工管理的需求开发
② 准备
- 准备数据库表 emp
- 创建一个新的 springboot 工程,选择引入对应的起步依赖(mybatis、mysql 驱动、lombok)
- application.properties 中引入数据库连接信息
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.username=root
spring.datasource.password=123456
- 创建对应的实体类 Emp(实体类属性采用驼峰命名)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp {
private Integer id; // ID
private String username; // 用户名
private String password; // 密码
private String name; // 姓名
private Short gender; // 性别, 1 男, 2 女
private String image; // 图像 url
private Short job; // 职位, 1 班主任, 2 讲师, 3 学工主管, 4 教研主管, 5 咨询师
private LocalDate entrydate; // 入职日期
private Integer deptId; // 部门 ID
private LocalDateTime createTime; // 创建时间
private LocalDateTime updateTime; // 修改时间
}
- 准备 Mapper 接口 EmpMapper
@Mapper
public interface EmpMapper {
}
③ 删除
@Mapper
public interface EmpMapper {
@Delete("delete from emp where id = #{id}")
public void delete(Integer id);
}
@SpringBootTest
class Test {
@Autowired
private EmpMapper empMapper;
@Test
public void testDelete(){
empMapper.delete(16);
}
}
可以在 application.properties 中,打开 mybatis 的日志,并指定输出到控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
预编译 SQL 优点
性能更高:
更安全(防止 SQL 注入):
select count(*) from emp where user name = '?' and password = '?'; -- 正常登录
select count(*) from emp where user name = 'abcd' and password = '0' or '1' = '1'; -- 注入成功
select count(*) from emp where user name = ? and password = ?; -- 预编译 SQL
select count(*) from emp where user name = abcd and password = 0' or '1' = '1; -- 注入失败
④ 新增
@Mapper
public interface EmpMapper {
// 自动将生成的主键值,赋值给 emp 对象的 id 属性
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) values (#{username},#{name},#{gender},#{image},#{job},#{entrydate},#{deptId},#{createTime},#{updateTime})")
public void insert(Emp emp);
}
@SpringBootTest
class Test {
@Autowired
private EmpMapper empMapper;
@Test
public void testInsert(){
Emp emp = new Emp();
emp.setUsername("Tom3");
...
empMapper.insert(emp);
System.out.println(emp.getId());
}
}
⑤ 更新
@Mapper
public interface EmpMapper {
@Update("update emp set username = #{username}, name = #{name}, gender = #{gender}, image = #{image}, job = #{job}, entrydate = #{entrydate}, dept_id = #{deptId},update_time = #{updateTime} where id = #{id}")
public void update(Emp emp);
}
@SpringBootTest
class Test {
@Autowired
private EmpMapper empMapper;
@Test
public void testInsert(){
Emp emp = new Emp();
emp.setUsername("Tom3");
...
empMapper.insert(emp);
System.out.println(emp.getId());
}
}
⑥ 查询
@Mapper
public interface EmpMapper {
@Select("select * from emp where id = #{id}")
public Emp getById(Integer id);
}
@SpringBootTest
class Test {
@Autowired
private EmpMapper empMapper;
@Test
public void testGetById(){
Emp emp = empMapper.getById(20);
System.out.println(emp);
}
}
如果实体类属性名 和 数据库表查询返回的字段名不一致,不能自动封装
开启驼峰命名:如果字段名与属性名符合驼峰命名规则,mybatis 会自动通过驼峰命名规则映射
mybatis.configuration.map-underscore-to-camel-case=true
5.2.4 XML 配置文件
使用 Mybatis 的注解,主要是来完成一些简单的增删改查功能。如果需要实现复杂的 SQL 功能,建议使用 XML 来配置映射语句
// java\com\itheima\mapper\EmpMapper.java
@Mapper
public interface EmpMapper {
public List<Emp> list(String name, Short gender, LocalDate begin , LocalDate end);
}
<!-- 1. XML 映射文件名称与 Mapper 接口一致,并且放置在相同包下 -->
<!-- resources\com\itheima\mapper\EmpMapper.xml -->
<!-- 2. XML 映射文件的 namespace 属性为 Mapper 接口全限定名一致 -->
<mapper namespace="com.itheima.mapper.EmpMapper">
<!-- 3. XML 映射文件中 sql 语句的 id 与 Mapper 接口中的方法名一致, 且返回类型一致 -->
<select id="list" resultType="com.itheima.pojo.Emp">
select * from emp where name like concat('%',#{name},'%') and gender = #{gender} and entrydate between #{begin} and #{end} order by update_time desc
</select>
</mapper>
5.2.5 动态 SQL
① 介绍
随着用户的输入或外部条件的变化而变化的 SQL 语句,我们称为动态 SQL
② 动态 SQL - if
@Mapper
public interface EmpMapper {
public List<Emp> list(String name, Short gender, LocalDate begin , LocalDate end);
}
<mapper namespace="com.itheima.mapper.EmpMapper">
<select id="list" resultType="com.itheima.pojo.Emp">
select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time, from emp
<where>
<if test="name != null">
name like concat('%', #{name}, '%')
</if>
<if test="gender != null">
and gender = #{gender}
</if>
<if test="begin != null and end != null">
and entrydate between #{begin} and #{end}
</if>
</where>
order by update_time desc
</select>
</mapper>
@Mapper
public interface EmpMapper {
public void update(Emp emp);
}
<mapper namespace="com.itheima.mapper.EmpMapper">
<update id="update">
update emp
<set>
<if test="username != null">username = #{username},</if>
<if test="name != null">name = #{name},</if>
<if test="gender != null">gender = #{gender},</if>
<if test="image != null">image = #{image},</if>
<if test="job != null">job = #{job},</if>
<if test="entrydate != null">entrydate = #{entrydate},</if>
<if test="deptId != null">dept_id = #{deptId},</if>
<if test="updateTime != null">update_time = #{updateTime}</if>
</set>
where id = #{id}
</update>
</mapper>
③ 动态 SQL - foreach
@Mapper
public interface EmpMapper {
public void deleteByIds(List<Integer> ids);
}
<mapper namespace="com.itheima.mapper.EmpMapper">
<delete id="deleteByIds">
delete from emp where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
</mapper>
④ 动态 SQL - sql & include
<mapper namespace="com.itheima.mapper.EmpMapper">
<sql id="commonSelect">
select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp
</sql>
<select id="list" resultType="com.itheima.pojo.Emp">
<include refid="commonSelect"/> <where> ... </where> order by update_time desc
</select>
</mapper>
5.3 课程总结
参考链接:https://www.bilibili.com/video/BV1m84y1w7Tb?p=1&vd_source=ed621eaa6bcf9bf6acb7d0527c30489a