Spring 全栈学习笔记

@Trenchance 2025.06.10

JAVA 环境配置

环境变量,可以直接新建变量 JAVA_HOME 并在 Path 中加入。也可以直接放在 Path。

Maven 配置

在这里下载 https://maven.apache.org/download.cgi。

本地仓库配置 settings.xml:

添加:

<localRepository>D:\maven\repository</localRepository>

远程仓库配置同样配置 settings.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<mirrors>
<!-- mirror
| Specifies a repository mirror site to use instead of a given repository. The repository that
| this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used
| for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
|

-->
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>

</mirrors>

Spring 初始化配置

控制器

@RestController

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.ren.demo.controller;

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

@RestController
public class HelloController {

@GetMapping("/hello")
public String Hello() {
return "Hello, World!";
}
}

一般情况下,我们的 url 是 http://www.baidu.com/s/xx,我们的 /hello 定义了 /s/xx 的部分。

由于我们是本地项目,还未部署,因此访问方法需要 http://localhost:8085/hello。

@Controller 注解 默认只能返回要跳转的路径即跳转的 html/JSP 页面。只有添加了 @ResponseBody注解后才可以返回 方法中 指定的返回类型。一般配合 Thymeleaf 模板引擎使用,能够返回一个页面。

@RestController 注解 ,则返回的是方法指定的返回类型。例如:如果返回类型为 ModelAndView 则返回指定的jsp 页面,如果是 String 则返回字符串,如果是 JSONObject 则返回json对象。默认情况下,@RestController 注解返回 JSON 格式

如果采用前后端分离,那么不需要使用 @Controller 注解。

@RestController 注解具体使用如下:

HTTP 不同请求方法

GET

GET请求通过将请求参数附加到URL中,以查询字符串的形式出现,以便将信息发送给服务器。这种请求方法适用于获取数据,而不是修改数据,因此它通常用于查询操作,如获取网页内容、图片、视频等资源。

形如:http://localhost:8080/user/article/page?page=1&pageSize=5。这个就是 queryString 形式。

前端:

1
2
3
4
5
6
import request from '@/utils/request.js'

//用户查询
export const userQueryService = (userId)=>{
return request.get('/user/'+userId)
}

后端:

1
2
3
4
5
6
7
//用户查询
@GetMapping("/user/{userId}")
public Result<User> queryUser(@PathVariable Long userId) {
log.info("查询用户:{}",userId);
User user = userService.getById(userId);
return Result.success(user);
}

当你在 URL 中使用了占位符(如 /user/{id})(又叫路径参数),就需要使用 @PathVariable 从路径中获取这个参数。

POST

POST:用于向服务器提交数据并创建新资源或修改现有资源。它通过将数据包含在请求体中而不是URL中。POST请求通常用于提交表单数据、上传文件或在服务器上执行某些操作。与GET请求不同,POST请求更适合用于需要创建或修改服务器上资源的场景。‌

PUT

用于向服务器发送数据以更新已存在的资源。当你想要替换某个指定URL下的现有信息时,就使用PUT方法。它通常包含完整的资源内容,意味着服务器会在接收到请求后,完全用请求体中的数据替换原有资源。如果该资源不存在,一些服务器可能会返回404错误。PUT请求常用于更新用户资料、修改博客文章等场景。

PUT:用于向服务器上传文件或更新资源。PUT请求会将请求的数据存储在服务器上指定的位置。

GET 请求示例

普通 GET

1
2
3
4
5
6
7
@RestController
public class HelloController {
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String hello() {
return "Hello, World!";
}
}

上述代码等价于

1
2
3
4
5
6
7
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello, World!";
}
}

QueryString GET

1
2
3
4
@GetMapping("/greet")
public String greet(String nickname, String phone) {
return "Hello, " + nickname + "! Your phone number is " + phone + ".";
}

参数不一致的 QueryString GET

1
2
3
4
5
@GetMapping("/greet2")
public String greet2(@RequestParam(value = "nickname", defaultValue = "Guest") String name,
@RequestParam(value = "phone", required = false) String phone) {
return "Hello, " + name + "! Your phone number is " + phone + ".";
}

@RequestParam 实现了参数映射。

URL 通配符

路径中的通配符:?(匹配单个字符),(匹配除/外任意字符)、*/(匹配任意多个目录)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@GetMapping("/greet?")
public String greet3()
{
return "Hello, World!3";
}

@GetMapping("/greet*")
public String greet4()
{
return "Hello, World!4";
}

@GetMapping("/greet/*")
public String greet5()
{
return "Hello, World!5";
}

@GetMapping("/greet/**")
public String greet6()
{
return "Hello, World!6";
}

POST 请求与 Apipost

后端代码:

1
2
3
4
5
@PostMapping("/post1")
public String post1(String a){
System.out.println(a);
return "POST SUCCESS";
}

直接打开 APIPOST,新建项目,然后在“全部接口”中点击加号新建接口。

路径填写:localhost:8085/post1

由于这里没有发送请求体,因此控制台输出 null

一般参数的请求体构造

POST 的参数是放在请求体里面的,接下来演示含有请求体的,后端代码如下:

1
2
3
4
5
6
@PostMapping("/post1")
public String post1(String a, String password){
System.out.println(a);
System.out.println(password);
return "POST SUCCESS" + a + password;
}

这里注意修改为 Body,且注意修改为 urlencoded

当然也可以用 Query:

注意,POST 请求不能够在地址栏中直接发送,必须通过 Apipost 之类的调试工具。不然 405 错误。

传递对象的请求体构造

假如我们在项目的 entity 包中创建了 User 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package org.ren.demo.entity;

public class User {
private String Username;
private String Password;
private String Email;
private String Phone;

public String getUsername() {
return Username;
}

public void setUsername(String username) {
this.Username = username;
}

public String getPassword() {
return Password;
}

public void setPassword(String password) {
this.Password = password;
}

public String getEmail() {
return Email;
}

public void setEmail(String email) {
this.Email = email;
}

public String getPhone() {
return Phone;
}

public void setPhone(String phone) {
this.Phone = phone;
}

@Override
public String toString() {
return "User" +
"Username='" + Username + '\'' +
", Password='" + Password + '\'' +
", Email='" + Email + '\'' +
", Phone='" + Phone + '\'';
}
}

后端代码:

1
2
3
4
@PostMapping("/post2")
public String post2(User user){
return "POST SUCCESS" + user.toString();
}

APIPOST 调试:

当然,也可以通过 json 直接构造请求体:

后端代码需要加上 @RequestBody 注解:

1
2
3
4
@PostMapping("/post4")
public String post4(@RequestBody User2 user){
return "POST SUCCESS" + user.toString();
}

有一个非常奇怪的问题,如果我们的实体类中,属性含有大写字母, 同时我们 json 构造的时候也用大写字母,这个时候会出现参数传不过去的情况。

解决方案:在实体类的属性加上 @JsonProperty(value = "") 注解,在日期类型的属性上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") ;或者要么是“全小写如id”要么“小写字母后面不要紧挨着大写字母,如fossilEnergy”。

但是这样还会出很多莫名其妙问题,建议不要使用通过 json 直接构造请求体的方式,要么就全小写。

静态文件访问+上传+拦截

静态文件访问

如果我们放一个图片在 static 目录下,例如我们有 static/band.jpg,那么直接在浏览器输入 http://localhost:8085/band.jpg 即可访问。

如果我们在配置文件 application.properties 中添加这样一句话:spring.mvc.static-path-pattern=/img/**,那么我们的访问路径就会改为:http://localhost:8085/img/band.jpg。即这句配置指定了静态资源的 URL 路径模式。

spring.web.resources.static-locations=classpath:/images/ 这行配置指定了静态资源在应用程序中的位置。这里是把静态资源全放在 images 目录下。

这里的 classpath 解释一下:classpath 是指类路径,我们编译后的文件都放在 target 目录下,其中 classes 就是类路径。但我们不需要手动放在这里,开发过程中放在 resources 目录即可。

可以修改文件上传的大小限制:

1
2
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB

文件上传

后端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package org.ren.demo.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;

@RestController
public class FileController {

@PostMapping("/upload")
public String up(String nickname, MultipartFile photo, HttpServletRequest request) throws IOException{
System.out.println(nickname);
System.out.println(photo.getOriginalFilename());
System.out.println(photo.getSize());
System.out.println(photo.getContentType());

// 因为最终文件需要上传到 web 服务器,需要动态获取 web 服务器运行目录
String path = request.getServletContext().getRealPath("/upload/");
System.out.println("Upload path: " + path);
saveFile(photo, path);
return "上传成功";
}

public void saveFile(MultipartFile photo, String path) throws IOException {
File dir = new File(path);
if (!dir.exists()) {
dir.mkdir(); // 创建目录
}

File file = new File(path+photo.getOriginalFilename());
photo.transferTo(file);
}
}

Apipost 调试:注意类型改为 form-data。

控制台打印:

我们可以看到,图片的确从本地上传到服务器(因为我们没有部署,这里的路径是 Tomcat 为我们分配的):

如果配置文件写这句话:spring.web.resources.static-locations=/upload/,这里的斜线就代表着服务器的路径,这个时候在地址栏访问 http://localhost:8085/83651130_p0.png 即可看到这张图。这时就是直接从服务器拿到这张图了。

拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.ren.demo.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
System.out.println("LoginInterceptor: preHandle false");
return false;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.ren.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.ren.demo.interceptor.LoginInterceptor;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/user/**");
}
}

需要注意的几点,拦截器必须注册之后才能够使用,这里我们创建一个 config 包,并创建 WebConfig 类,注意必须有 @Configuration 注解。

RESTful 设计风格

传统方法中,删除用户使用 GET 请求,然后在 url 中使用 delete 之类的动词,但是在 RESTful中,直接使用 DELETE请求,并且直接在 url 中传 /user/id,url 中不包括动词。

这里要注意 @PathVariable 注解,因为 id 是动态的放在路径里的。

Swagger 生成 API 文档

首先导入 Swagger 依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-spring-web</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>

然后配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package org.ren.demo.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {

@Bean
public Docket docket() {
// 创建一个 swagger 的 bean 实例
return new Docket(DocumentationType.SWAGGER_2)
// 配置基本信息
.apiInfo(apiInfo())
;
}

// 基本信息设置
private ApiInfo apiInfo() {
Contact contact = new Contact(
"米大傻", // 作者姓名
"https://blog.csdn.net/xhmico?type=blog", // 作者网址
"777777777@163.com"); // 作者邮箱
return new ApiInfoBuilder()
.title("多加辣-接口文档") // 标题
.description("众里寻他千百度,慕然回首那人却在灯火阑珊处") // 描述
.termsOfServiceUrl("https://www.baidu.com") // 跳转连接
.version("1.0") // 版本
.license("Swagger-的使用(详细教程)")
.licenseUrl("https://blog.csdn.net/xhmico/article/details/125353535")
.contact(contact)
.build();
}

}

如果启动报错,在 application.properties 中增加:

1
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

访问 http://localhost:8085/swagger-ui.html# 即可:

这个可以替代 Apipost,我们直接选择接口然后点击 Try it out 即可。

MyBatis-Plus

MySQL 安装

进入官网,点击 Downloads 页面,拉到底,点击 MySQL Community (GPL) Downloads »

选择下面那个就行,这俩无所谓:

打开 msi 进行安装,设置密码为 123456,服务名称 MYSQL80,端口如下:

安装完毕后,可以在开始菜单中找到 MySQL 8.0 Command Line Client:

下面的命令展示出我们所有的 schema(或者说 database):

现在让我们打开 MySQL Shell,有两种方法,一种是直接在开始菜单找到 MySQL Shell。

另一种方式是在 cmd 中输入命令 mysqlsh

在这里我们所有的命令都是以 \ 开头的。

注意,由于我们之前修改了端口,因此不能够直接用 root@localhost 进行连接,不然会报错,必须使用命令 \connect root@localhost:5005 进行连接。连接之后提示我们 type \use <schema> to set one

现在可以使用 use 语句进入某个数据库:

我们可以切换语言(目前是 JS),在 sql 状态下就可以正常写 sql 语言了,一定不要忘记 sql 语句以分号结尾:

ORM

数据准备

在下一章连接时候,我们的 url 是 jdbc:mysql://localhost:5005/mydb,这意味着我们的数据库名为 mydb,因此我们需要建一个名为 mydb 的数据库。

我们可以直接在 MySQL 8.0 Command Line Client 建数据库:

也可以在 MySQL Shell 中建数据库:

接下来配置数据源并测试连接:

在右侧 Database 中点击加号 -> Data Source -> MySQL:

配置 Drivers:

配置数据源:

现在我们就可以写 sql 语句对数据库进行操作了:

输入如下 sql 语句并执行:

1
2
3
4
5
6
7
8
9
CREATE table user (
username varchar(50) not null primary key,
password varchar(255) not null,
birthday date not null
);

insert into user (username, password, birthday) values
('lucy', '123', '1980-01-01'),
('haohao', '123', '1992-03-03');

直接查看 user 表:

MyBatis-Plus 配置

依赖配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- MyBatisPlus依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>

<!-- mysql驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>

<!-- 数据连接池 druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</dependency>

application.properties 配置:

1
2
3
4
5
6
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:5005/mydb?useSSL=false&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

Mapper 实现(MyBatis)

首先加 @MapperScan 注解,用来扫描 mapper 使其生效:

1
2
3
4
5
6
7
@SpringBootApplication
@MapperScan("org.ren.demo.mapper")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

然后创建 User 实体类,实体类的字段和数据库中字段对应,Alt + Insert,可以自动生成 Getter 和 Setter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package org.ren.demo.entity;

public class User {
private String username;
private String password;
private String birthday;

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getBirthday() {
return birthday;
}

public void setBirthday(String birthday) {
this.birthday = birthday;
}

@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", birthday='" + birthday + '\'' +
'}';
}
}

接着在 mapper 包中,新建接口 UserMapper,注意加上 @Mapper 注解,同时 @Select 注解中写 sql 语句。注意这个接口后续不需要进行实现,后续使用的时候只需要声明即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.ren.demo.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.ren.demo.entity.User;

import java.util.List;

@Mapper
public interface UserMapper {

@Select("SELECT * FROM user")
public List<User> find();
}

在控制器中使用 mapper,这里注意,使用 UserMapper 的时候只需要声明即可,记得加上 @Autowired 注解自动注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.ren.demo.controller;

import org.ren.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.ren.demo.entity.User;

import java.util.List;

@RestController
public class UserController {

@Autowired
private UserMapper userMapper; // 自动注入 UserMapper

@GetMapping("/user")
public String query() {
List<User> list = userMapper.find();
System.out.println("查询到的用户列表: " + list);
return "查询用户";
}
}

在浏览器访问:http://localhost:8085/user,控制台打印查询结果:

假如我们把控制器写成这样:

1
2
3
4
5
6
@GetMapping("/user")
public List query2() {
List<User> list = userMapper.find();
System.out.println("查询到的用户列表: " + list);
return list;
}

将会直接返回 json:

同时我们可以写一个用于增加的 Mapper:

1
2
@Insert("INSERT INTO user(username, password, birthday) VALUES(#{username}, #{password}, #{birthday})")
public int insert(User user); // 返回受影响行数

相应的后端控制器:

1
2
3
4
5
6
@PostMapping("/user")
public String save(User user) {
int result = userMapper.insert(user);
System.out.println("插入用户结果: " + result);
return "插入用户";
}

发送 POST 请求

得到结果:

如果我们的 User 类和数据库表均含有 id 字段,同时我们不想直接传入 id 字段,可以在建表时候设置 id 自增。

Mapper 实现(MyBatis-Plus)

将接口 UserMapper 继承 BaseMapper,尖括号中写类名,现在 UserMapper 里面几乎不需要再写代码,如下所示 UserMapper 直接为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.ren.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.ren.demo.entity.User;


@Mapper
public interface UserMapper extends BaseMapper<User> {
/* @Select("SELECT * FROM user")
public List<User> find();

@Insert("INSERT INTO user(username, password, birthday) VALUES(#{username}, #{password}, #{birthday})")
public int insert(User user); // 返回受影响行数*/
}

注意,如果表名和类名不一样,可以写一个注解 @TableName("")实体类中:

1
2
3
4
5
6
7
8
9
10
11
package org.ren.demo.entity;

import com.baomidou.mybatisplus.annotation.TableName;

@TableName("user") // 指定数据库表名
public class User {
private String username;
private String password;
private String birthday;
}
}

MyBatis-Plus 还包含了很多其他的注解,比如 @TableId 用于标记实体类中的主键字段(可以设置自增等策略,这样 MyBatis-Plus 会直接把你传过来的 User 对象中补全 id,而不是由数据库做这件事),@TableField 用于映射到数据库字段。MyBatis-Plus 官网有详细的文档。示例代码:

1
2
3
4
5
6
7
8
@TableName("sys_user")
public class User {
@TableId
private Long id;
@TableField("nickname") // 映射到数据库字段 "nickname"
private String name;
private Integer age;
}

查询直接在控制器中完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping("/user")
public List query2() {
List<User> list = userMapper.selectList(null); // 查询所有用户,null为条件
System.out.println("查询到的用户列表: " + list);
return list;
}

@PostMapping("/user")
public String save(User user) {
int result = userMapper.insert(user);
System.out.println("插入用户结果: " + result);
return "插入用户";
}

发送 POST 请求:

查询请求:

控制台输出:

多表查询

需要注意一件事,MyBatis-Plus 对多表查询无能为力。

如何知道自己现在在不在使用 MyBatis-Plus 呢,其实可以看控制器里调用 Mapper 的方法,看看这些方法是不是 MyBatis-Plus 的,如果是 MyBatis-Plus 的,Mapper 里面应该没有这个方法,因为他直接继承了 BaseMapper,用的是 BaseMapper 的方法,我们可以 Ctrl + 左键查看 BaseMapper 的方法有哪些。

如果控制器直接调用如 insert() 方法(这个可以在 BaseMapper 中找到)而不是我们自己在 Mapper 里面实现的,那他就是在使用 MyBatis-Plus 。如果控制器使用的方法是在 Mapper 中我们自己实现的,那就是用的 MyBatis,使用 MyBatis 的时候,和 @TableName("sys_user") 之类的注解就没关系了。

现在有这样两张表:

根据表创建两个实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package org.ren.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import java.util.List;

@TableName("t_user") // 指定数据库表名
public class UserT {
@TableId(type = IdType.AUTO) // 自动生成ID
private int id;
private String username;
private String password;
private String birthday;

// 描述用户订单,并注明 order 字段在数据库 t_user 中其实不存在
@TableField(exist = false)
private List<Order> orders;


public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getBirthday() {
return birthday;
}

public void setBirthday(String birthday) {
this.birthday = birthday;
}

@Override
public String toString() {
return "userT{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", birthday='" + birthday + '\'' +
'}';
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package org.ren.demo.entity;

public class Order {
private int id;
private String orderTime;
private double total;
private int uid;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getOrderTime() {
return orderTime;
}

public void setOrderTime(String orderTime) {
this.orderTime = orderTime;
}

public double getTotal() {
return total;
}

public void setTotal(double total) {
this.total = total;
}

public int getUid() {
return uid;
}

public void setUid(int uid) {
this.uid = uid;
}

@Override
public String toString() {
return "Order{" +
"id=" + id +
", orderTime='" + orderTime + '\'' +
", total=" + total +
", uid=" + uid +
'}';
}
}

重点!!!

UserTMapper 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package org.ren.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.*;
import org.ren.demo.entity.UserT;

import java.util.List;


@Mapper
public interface UserTMapper extends BaseMapper<UserT> {

@Select("SELECT * FROM t_user WHERE id = #{id}")
UserT selectById(int id);

@Select("SELECT * FROM t_user") // 查询所有用户
@Results(
{
@Result(column = "id", property = "id"),
@Result(column = "username", property = "username"),
@Result(column = "password", property = "password"),
@Result(column = "birthday", property = "birthday"),
@Result(column = "id", property = "orders", javaType = List.class,
many = @Many(select = "org.ren.demo.mapper.OrderMapper.selectByUserId"))
}
)
List<UserT> selectAllUserAndOrders(); // 查询所有用户及其订单信息
}

@Results 用来做结果集的映射,即我从数据库里查出来的东西,应该怎么给对象赋值。

我们知道 UserT 类中存在字段 private List<Order> orders,重点就在于如何给这个字段赋值。

@Results 的里面可以传入多个 @Result,小括号括花括号。

每个 @Result 都是一个参数,column 是数据库查出来的列名,property 是 UserT 的属性,如 @Result(column = "id", property = "id") 的含义是:数据库中查出来的 id 列,赋值给 UserT 的 id 属性。

解释一下这句话:

1
2
@Result(column = "id", property = "orders", javaType = List.class,
many = @Many(select = "org.ren.demo.mapper.OrderMapper.selectByUserId"))

javaType = List.class 指的是 orders 的类型是 List。接下来我们需要通过 id 查询订单。

因此还需要一个 OrderMapper 实现根据用户ID查询订单:

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.ren.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import org.ren.demo.entity.Order;

import java.util.List;

public interface OrderMapper extends BaseMapper<Order> {
// 根据用户ID查询订单
@Select("SELECT * FROM t_order WHERE uid = #{uid}")
List<Order> selectByUserId(int uid);
}

many = @Many(select = "org.ren.demo.mapper.OrderMapper.selectByUserId")) 这句话是在 UserTMapper 中调用 OrderMapper 中的方法,column = "id" 作为参数传递给了 selectByUserId 方法,得到所有的订单放在 property = "orders" 也即 UserT 类中的 orders 字段。

完整 Mapper 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package org.ren.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.*;
import org.ren.demo.entity.UserT;

import java.util.List;


@Mapper
public interface UserTMapper extends BaseMapper<UserT> {

@Select("SELECT * FROM t_user WHERE id = #{id}")
UserT selectById(int id);

@Select("SELECT * FROM t_user") // 查询所有用户
@Results(
{
@Result(column = "id", property = "id"),
@Result(column = "username", property = "username"),
@Result(column = "password", property = "password"),
@Result(column = "birthday", property = "birthday"),
@Result(column = "id", property = "orders", javaType = List.class,
many = @Many(select = "org.ren.demo.mapper.OrderMapper.selectByUserId"))
}
)
List<UserT> selectAllUserAndOrders(); // 查询所有用户及其订单信息
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.ren.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import org.ren.demo.entity.Order;

import java.util.List;

public interface OrderMapper extends BaseMapper<Order> {

// 根据用户ID查询订单
@Select("SELECT * FROM t_order WHERE uid = #{uid}")
List<Order> selectByUserId(int uid);

}

加上控制器代码:

1
2
3
4
5
6
@GetMapping("/user/findAll")
public List<UserT> findAll() {
List<UserT> users = userTMapper.selectAllUserAndOrders(); // 查询所有用户
System.out.println("查询到的用户列表: " + users);
return users;
}

查询结果:

多表查询的另外方法:MyBatis-Plus-Join

条件查询

方法一:MyBatis 方法,直接在 Select 语句写 Where 条件。

方法二:MyBatis-Plus 方法,直接在控制器中写 QueryWrapper

1
2
3
4
5
6
@GetMapping("/user/find1")
public List<UserT> find1() {
QueryWrapper<UserT> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", "Alice"); // 根据ID查询用户
return userTMapper.selectList(queryWrapper);
}

查询结果:

一定注意,在用这种方法时别忘记这个注解:

1
2
@TableField(exist = false)
private List<Order> orders;

分页查询

新建配置类,也即分页的拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.ren.demo.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}

控制器这样写:

1
2
3
4
5
@GetMapping("/user/find2")
public IPage find2(){
Page<UserT> page = new Page<>(1, 1); // 创建分页对象,页码为1,每页1条记录
return userTMapper.selectPage(page, null); // 查询所有用户并分页
}

返回的是 IPage,而不是 UserT:

Vue

前端框架:Vue,React 等,这里我们学习 Vue。

MVVM 设计模式:

基本用法

注:为了方便代码和界面对照,我们将代码与渲染出的界面截在一张图中,而不采用 markdown 代码块的形式展示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>

<!-- 导入 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>

<!-- 声明要被 vue 控制的 DOM 区域 -->
<div id="app">{{ message }}</div>

<!-- 创建 vue 的实例对象 -->
<script>
Vue.createApp({
// 数据源,即 MVVM 模式中的 Model
data() {
const message = 'Hello vue'
return {
message
}
}
}).mount('#app')
</script>
</body>
</html>

小插曲:我做到这里时候 IDEA 的 Github Copilot 突然不能用了,解决方案是把网络改为 native,并清空 IDE 缓存:


让我们继续:

我们观察上面的代码与渲染结果,我们发现第三行是纯文本,第四行是超链接。

在 Vue 框架中,v-html 是一个指令,用于将数据作为 HTML 插入到元素中。当使用 v-html 绑定数据时,Vue 会将数据解析为 HTML 并将其渲染到 DOM 中,而不是像普通插值语法 {{}} 那样作为纯文本处理。


上图我们看到,属性 href,placeholder,src,style 前面都有冒号,这就是用来绑定属性的。


上图中我们使用了一些基本 JS 表达式。


上图是简单的事件绑定(比如点按钮做出操作),Vue 中数据变化页面会自己刷新,因此 count 会随着我们按下按钮变化而我们无需刷新。

原生 JS 事件监听用 onclick,Vue 中使用 v-on,v-on 可以简写为 @。

注意我们的 vm 对象中现在存在 data 和 methods,前者数据,后者方法。


上图展示了条件渲染。v-if 如果为 false,该标签根本不会被创建;v-show 如果为 false,该标签会被创建,但是通过 css 将其隐藏。v-show 性能更高。v-if 还可以配合 v-else 使用。


上图为列表渲染。使用 v-for 进行渲染。

基本用法总结

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>

<!-- 导入 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>

<!-- 声明要被 vue 控制的 DOM 区域 -->
<div id="app">{{ message }}</div>

<!-- 创建 vue 的实例对象 -->
<script>
Vue.createApp({
// 数据源,即 MVVM 模式中的 Model
data() {
const message = 'Hello vue'
return {
message
}
}
}).mount('#app')
</script>
</body>
</html>

HTML 内容渲染指令v-html。用于渲染 HTML 页面元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://unpkg.com/vue@3"></script>
</head>
<body>
<div id="app">
<p>姓名:{{username}}</p>
<p>性别:{{gender}}</p>
<p>{{desc}}</p>
<p v-html="desc"></p>
</div>

<script>
const vm = {
data: function(){
return {
username: 'zhangsan',
gender: '男',
desc: '<a href="http://www.baidu.com">百度</a>'
}
}
}
const app = Vue.createApp(vm)
app.mount('#app')
</script>
</body>
</html>

属性绑定指令:。用于修改元素的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://unpkg.com/vue@3"></script>
</head>
<body>
<div id="app">
<a :href="link">百度</a>
<input type="text" :placeholder="inputValue">
<img :src="imgSrc" :style="{width: w}" alt="">
</div>

<script>
const vm = {
data: function(){
return {
link: "http://www.baidu.com",
inputValue: '请输入内容',
imgSrc: './images/demo.jpg',
w: '500px'
}
}
}
const app = Vue.createApp(vm)
app.mount('#app')
</script>
</body>
</html>

JS 表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://unpkg.com/vue@3"></script>
</head>
<body>
<!-- vue 实例要控制的 DOM 区域 -->
<div id="app">
<p>{{number + 1}}</p>
<p>{{ok ? 'True' : 'False'}}</p>
<p>{{message.split('').reverse().join('')}}</p>
<p :id="'list-' + id">xxx</p>
<p>{{user.name}}</p>
</div>

<script>
const vm = {
data: function(){
return {
number: 9,
ok: false,
message: 'ABC',
id: 3,
user: {
name: 'zs',
}
}
}
}
const app = Vue.createApp(vm)
app.mount('#app')
</script>
</body>
</html>

事件绑定指令v-on。取代原生 JS 的 onclick

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://unpkg.com/vue@3"></script>
</head>
<body>
<div id="app">
<h3>count 的值为: {{count}}</h3>
<button v-on:click="addCount">+1</button>
<button @click="count-=1">-1</button>
</div>

<script>
const vm = {
data: function(){
return {
count: 0,
}
},
methods: {
// 点击按钮,让 count 自增 +1
addCount() {
this.count += 1
},
}
}
const app = Vue.createApp(vm)
app.mount('#app')
</script>
</body>
</html>

条件渲染指令v-if

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://unpkg.com/vue@3"></script>
</head>
<body>
<div id="app">
<button @click="flag = !flag">Toggle Flag</button>
<p v-if="flag">请求成功 --- 被 v-if 控制</p>
<p v-show="flag">请求成功 --- 被 v-show 控制</p>
</div>

<script>
const vm = {
data: function(){
return {
flag: false,
}
}
}
const app = Vue.createApp(vm)
app.mount('#app')
</script>
</body>
</html>

列表渲染指令v-for

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://unpkg.com/vue@3"></script>
</head>
<body>
<div id="app">
<ul>
<li v-for="(user, i) in userList">索引是:{{i}},姓名是:{{user.name}}</li>
</ul>
</div>

<script>
const vm = {
data: function(){
return {
userList: [
{ id: 1, name: 'zhangsan' },
{ id: 2, name: 'lisi' },
{ id: 3, name: 'wangwu' },
],
}
}
}
const app = Vue.createApp(vm)
app.mount('#app')
</script>
</body>
</html>

v-for 的 key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://unpkg.com/vue@3"></script>
</head>
<body>
<div id="app">
<!-- 添加用户的区域 -->
<div>
<input type="text" v-model="name">
<button @click="addNewUser">添加</button>
</div>

<!-- 用户列表区域 -->
<ul>
<li v-for="(user, index) in userList" :key="user.id">
<input type="checkbox" />
姓名:{{ user.name }}
</li>
</ul>
</div>

<script>
const vm = {
data: function(){
return {
// 用户列表
userList: [
{ id: 1, name: 'zhangsan' },
{ id: 2, name: 'lisi' }
],
// 输入的用户名
name: '',
// 下一个可用的 id 值
nextId: 3
}
},
methods: {
// 点击了添加按钮
addNewUser() {
this.userList.unshift({ id: this.nextId, name: this.name })
this.nextId++
this.name = ''
}
}
}
const app = Vue.createApp(vm)
app.mount('#app')
</script>
</body>
</html>

v-model="name" 双向绑定,name 变化,页面会跟着变化;如果页面发生变化,也会影响 name 的值。

unshift 是往数组的起始位置加元素,这里我们写进去一个新的对象,id 为 nextId。

重点<li v-for="(user, index) in userList" :key="user.id"> 这里我们给每一个 <li> 添加一个 :key="user.id",这样每个列表和用户的 id 匹配,而不是和列表的第几项匹配。

NPM

类似于 Maven。下载 Node.js 安装即可在命令行使用。

Vue CLI

安装截图如下:

命令:

1
2
3
4
npm cache clean --force
npm config set strict-ssl false
npm config set registry http://registry.npm.taobao.org
npm install -g @vue/cli --registry=https://registry.npm.taobao.org

环境变量中用户变量增加:

1
C:\Users\30516\AppData\Roaming\npm

之后即可在项目目录下执行,创建名为 hello 的项目:

1
vue create hello

这里选择最后一项:

余下配置如下所示,这里的 package.json 可以类比之前的 pom.xml

回车即可开始创建项目,结果如下,项目目录下出现一个 hello 的文件夹:

现在我们可以在 hello 目录下运行:

1
npm run serve

访问:http://localhost:8080/ 即可进入前端界面。

注意,我们很多通过 npm install 下载的东西都存在 node_modules 目录中,这个目录很大,一般情况下在 Github 下载下来的项目中不会有 node_modules。但只要有 package.json,我们直接在项目目录输入指令 npm install 即可,他会自己读 package.json 并下载相关依赖。

Vue 组件化

App.vue 是我们的根组件。现在我们要自己创建一个 vue component。在 src/components 目录下新建一个 vue 文件叫做 Hello.vue:

1
2
3
4
5
<template>
<h1>HellOOOO</h1>
</template>
<script>
</script>

这是一个非常简单的组件,只有一个一级标题。现在我们要使用这个组件,需要在 App.vue 进行导入注册,具体需要在 script 标签中,进行 import,同时在 components 中加上我们的组件名称。然后我们就可以在 template 标签中进行使用了。下面的代码我们使用了两次 Hello:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
![](logo.png)
<Hello></Hello>
<Hello></Hello>
<HelloWorld msg="Welcome to Vue.js App"/>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'
import Hello from './components/Hello.vue'

export default {
name: 'App',
components: {
HelloWorld,
Hello
}
}
</script>

组件重用与传递数据

我们现在想让组件重用,比如我们现在创建一个 Movie 组件,这个组件可以显示标题。使用 props: ["title"] 可以从外部传数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="movie">
<h1>{{title}}</h1>
</div>
</template>

<script>
export default {
name: 'Movie',
props: ["title"],
data:function () {
return {

}
},
}
</script>

在 App.vue 中我们就可以使用属性传入数据:

1
2
3
4
5
6
7
<template>
<div id="app">
<!-- ![](logo.png)
<HelloWorld msg="Welcome to Your Vue.js App"/>-->
<Movie title="Inception"></Movie>
</div>
</template>

现在这个组件就是一个通用的组件。

下面进一步重用该组件,先修改下 Movie 让其内容更多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div class="movie">
<h1>{{title}}</h1>
<span>{{rating}}</span>
<button @click="fun">收藏</button>
</div>
</template>

<script>
export default {
name: 'Movie',
props: ["title", "rating"],
data:function () {
return {
}
},
methods: {
fun() {
alert("收藏成功");
}
}
}
</script>

然后修改 App.vue,让 App.vue 引用 Movie 组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<div id="app">
<!-- ![](logo.png)
<HelloWorld msg="Welcome to Your Vue.js App"/>-->
<Movie v-for="movie in movies" :key="movie.id" :title="movie.title" :rating="movie.rating"></Movie>
</div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'
import Movie from './components/Movie.vue'

export default {
name: 'App',
data:function () {
return {
movies: [
{ id:1, title: "Inception", rating: "5.0" },
{ id:2, title: "The Matrix", rating: "4.8" },
{ id:3, title: "Interstellar", rating: "4.9" }
]
}
},
components: {
HelloWorld,
Movie
}
}
</script>

这里我们在 App.vue 的 data 中模拟后端传过来的数据,并通过 v-for:key 列表渲染,注意后面的字段前面要加上冒号,冒号来绑定属性。

注意 props 只能用于父子组件,兄弟组件之间共享数据 props 无能为力。比如下面这种就是兄弟组件:

1
2
3
4
5
6
<template>
<div id="app">
<Movie v-for="movie in movies" :key="movie.id" :title="movie.title" :rating="movie.rating"></Movie>
<Hello></Hello>
</div>
</template>

总结一下组件的数据传递:

使用第三方组件

因为 element-ui 只支持 vue2,我们先创建一个新的 vue2 项目。

下载:npm i element-ui -S

在 main.js 做如下修改,需要注意我们之前的组件注册都是局部注册,而这个(Vue.use(ElementUI);)是全局注册,这样哪个组件都能用:

1
2
3
4
5
6
7
8
9
10
11
import Vue from 'vue'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue'

Vue.config.productionTip = false
Vue.use(ElementUI);

new Vue({
render: h => h(App),
}).$mount('#app')

假如我们现在想做一个表格,我们可以直接在 element-ui 网站中“组件”一栏进行寻找:

然后按需要把源码复制到我们的项目中,比如我们创建一个 Hello 组件,在 Hello 组件中使用这个表格:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<template>
<el-table
:data="tableData"
style="width: 100%"
:row-class-name="tableRowClassName">
<el-table-column
prop="date"
label="日期"
width="180">
</el-table-column>
<el-table-column
prop="name"
label="姓名"
width="180">
</el-table-column>
<el-table-column
prop="address"
label="地址">
</el-table-column>
</el-table>
</template>

<script>
export default {
name: 'Hello',
data() {
return {
tableData: [
{ date: '2016-05-03', name: '王小明', address: '上海市普陀区金沙江路 1518 弄' },
{ date: '2016-05-02', name: '张三', address: '上海市普陀区金沙江路 1517 弄' },
{ date: '2016-05-04', name: '李四', address: '上海市普陀区金沙江路 1519 弄' }
]
};
},
methods: {
tableRowClassName({row, rowIndex}) {
if (rowIndex === 0) {
return 'warning-row';
} else if (rowIndex === 1) {
return 'success-row';
}
return '';
}
}
};
</script>

<style>
.el-table .warning-row {
background: oldlace;
}

.el-table .success-row {
background: #f0f9eb;
}
</style>

el-table 是 element-ui 自己做的组件,我们可以在这里直接用,各种属性(如 row-class-name)可以在官网查询其含义。

注意 vue2 中只能有一个根标签,如果我们想在一个组件中,使用多个组件,可以用 div 括起来,像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<template>
<div>
<el-table
:data="tableData"
style="width: 100%"
:row-class-name="tableRowClassName">
<el-table-column
prop="date"
label="日期"
width="180">
</el-table-column>
<el-table-column
prop="name"
label="姓名"
width="180">
</el-table-column>
<el-table-column
prop="address"
label="地址">
</el-table-column>
</el-table>
<el-date-picker
v-model="value1"
type="datetime"
placeholder="选择日期时间">
</el-date-picker>
</div>
</template>

<script>
export default {
name: 'Hello',
data() {
return {
tableData: [
{ date: '2016-05-03', name: '王小明', address: '上海市普陀区金沙江路 1518 弄' },
{ date: '2016-05-02', name: '张三', address: '上海市普陀区金沙江路 1517 弄' },
{ date: '2016-05-04', name: '李四', address: '上海市普陀区金沙江路 1519 弄' }
],
value1: ''
};
},
methods: {
tableRowClassName({row, rowIndex}) {
if (rowIndex === 0) {
return 'warning-row';
} else if (rowIndex === 1) {
return 'success-row';
}
return '';
}
}
};
</script>

<style>
.el-table .warning-row {
background: oldlace;
}

.el-table .success-row {
background: #f0f9eb;
}
</style>

现在的 App.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<div id="app">
<Movie v-for="movie in movies" :key="movie.id" :title="movie.title" :rating="movie.rating"></Movie>
<Hello></Hello>
</div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'
import Movie from './components/Movie.vue'
import Hello from './components/Hello.vue'

export default {
name: 'App',
data:function () {
return {
movies: [
{ id:1, title: "Inception", rating: "5.0" },
{ id:2, title: "The Matrix", rating: "4.8" },
{ id:3, title: "Interstellar", rating: "4.9" }
]
}
},
components: {
HelloWorld,
Movie,
Hello
}
}
</script>

现在的效果:

Vue 生命周期函数

什么是生命周期:从Vue实例创建、运行、到销毁期间,总是伴随着各种各样的事件,这些事件,统称为生命周期。

生命周期函数举例,我们在 App 和 Movie 组件中添加生命周期函数 created

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
import Movie from './components/Movie.vue'

export default {
name: 'App',
data:function () {
return {
}
},
created: function() {
console.log("App created");
},
components: {
HelloWorld,
Movie,
Hello
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
export default {
name: 'Movie',
props: ["title", "rating"],
data:function () {
return {
}
},
created: function() {
console.log("Movie created");
},
methods: {
fun() {
alert("收藏成功");
}
}
}
</script>

鼠标右键,点击检查,在右侧 console 中可以看到消息:

Axios 网络请求

一般项目中都是前端发送 HTTP 请求,拿到对应的数据,再进行渲染。浏览器是主动方,服务器被动接收请求,浏览器拿到数据之后再通过 vue 进行数据绑定。

安装:npm install axios

官网:https://axios-http.com/docs/intro

引用(main.js):

1
2
import axios from 'axios'
Vue.prototype.$axios = axios

组件中使用 this.$axios 访问

1
2
3
4
5
6
7
8
<script>
export default {
created: function() {
this.$axios.get("/user/findAll").then(function(response) {
})
},
}
</script>

方法2:在组件中导入 axios

1
2
3
<script>
import axios from 'axios' // 导入 axios
</script>

由于我们要实现前后端连接,先测试一下后端还能不能正常工作,这里测试一下之前的 http://localhost:8085/user/findAll:

Axios 请求语法参考:

现在让我们写前端代码,在 App.vue 中写 created 函数,在这里发出 /user/findAll GET 请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
export default {
name: 'App',
data:function () {
return {
}
},
created: function() {
this.$axios.get("http://localhost:8085/user/findAll").then(function(response) {
console.log(response)
})
},
}
</script>

访问前端界面,发现出错(右键检查,Console):

被 CORS 策略阻止!!!

跨域

原理

上图中:后端 8080,前端 8081。

解决方案

方法一:

方法二:直接在控制器上加注解 @CrossOrigin

1
2
3
@RestController
@CrossOrigin // 允许跨域请求
public class UserController

重启 Spring 服务之后进入前端,发现获取到了数据:

我们可以在 Network 中查看 findAll 的 Headers,发现 Access-Control-Allow-Origin 是 *

渲染后端数据

后端控制器,注意 @CrossOrigin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package org.ren.demo.controller;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.ren.demo.entity.UserT;
import org.ren.demo.mapper.UserMapper;
import org.ren.demo.mapper.UserTMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.ren.demo.entity.User;

import java.util.List;

@RestController
@CrossOrigin // 允许跨域请求
public class UserController {

@Autowired
private UserMapper userMapper; // 自动注入 UserMapper
@Autowired
private UserTMapper userTMapper;

@GetMapping("/user/findAll")
public List<UserT> findAll() {
List<UserT> users = userTMapper.selectAllUserAndOrders(); // 查询所有用户
System.out.println("查询到的用户列表: " + users);
return users;
}
}

前端组件 Hello.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<template>
<div>
<el-table
:data="tableData"
style="width: 100%"
:row-class-name="tableRowClassName">
<el-table-column
prop="id"
label="编号"
width="180">
</el-table-column>
<el-table-column
prop="username"
label="姓名"
width="180">
</el-table-column>
<el-table-column
prop="birthday"
label="生日">
</el-table-column>
</el-table>
<el-date-picker
v-model="value1"
type="datetime"
placeholder="选择日期时间">
</el-date-picker>

</div>
</template>

<script>
export default {
name: 'Hello',
data() {
return {
tableData: [],
value1: ''
};
},
created: function() {
this.$axios.get("http://localhost:8085/user/findAll").then((response)=>{
this.tableData = response.data
})
},
methods: {
tableRowClassName({row, rowIndex}) {
if (rowIndex === 0) {
return 'warning-row';
} else if (rowIndex === 1) {
return 'success-row';
}
return '';
}
}
};
</script>

<style>
.el-table .warning-row {
background: oldlace;
}

.el-table .success-row {
background: #f0f9eb;
}
</style>

界面展示:

核心部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
export default {
name: 'Hello',
data() {
return {
tableData: []
};
},
created: function() {
this.$axios.get("http://localhost:8085/user/findAll").then((response)=>{
this.tableData = response.data
})
}
};
</script>

稍微解释一下:response 是 axios 发送 HTTP 请求后服务器返回的响应对象,它包含了服务器返回的数据和相关信息。在 axios 请求中,response 对象通常有以下结构:

这里的 data 字段就是后端传来的实际数据,也即一个 json。

在我们的后端代码中,控制器返回的是一个 List<UserT> 也即列表:

1
2
3
4
5
6
@GetMapping("/user/findAll")
public List<UserT> findAll() {
List<UserT> users = userTMapper.selectAllUserAndOrders(); // 查询所有用户
System.out.println("查询到的用户列表: " + users);
return users;
}

这里 response.data 就是一个长成这样的列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
[
{
"id": 1,
"username": "Alice",
"password": "alice123",
"birthday": "1990-01-01",
"orders": [
{
"id": 1,
"orderTime": "2025-06-12 10:00:00",
"total": 150.5,
"uid": 1
},
{
"id": 4,
"orderTime": "2025-06-12 14:00:00",
"total": 50,
"uid": 1
}
]
},
{
"id": 2,
"username": "Bob",
"password": "bob123",
"birthday": "1992-05-15",
"orders": [
{
"id": 2,
"orderTime": "2025-06-12 11:30:00",
"total": 200.75,
"uid": 2
}
]
},
{
"id": 3,
"username": "Alice",
"password": "charlie123",
"birthday": "1995-08-20",
"orders": [
{
"id": 3,
"orderTime": "2025-06-12 12:45:00",
"total": 99.99,
"uid": 3
}
]
}
]

在我们 Hello.vue 中的 data 部分定义了 tableData: [],这也是一个列表,通过 this.tableData = response.data 实现了从后端 API 获取的数据到 Vue 组件数据属性的赋值操作。

el-table 组件通过 :data="tableData" 绑定到这个数据,当 tableData 更新时,表格会自动渲染新数据。

同时在 el-table-column 中我们可以指定要展示到前端的属性(id,username,birthday),通过 props 实现。

VueRouter 路由(未完成)

创建新 vue2 项目:vue create router-demo

安装 vue-router 3:npm install vue-router@3

现在创建三个组件,组件里的内容随意设置即可:

修改 App.vue,这里的router-link 标签类似于 HTML 的超链接。这里我们还没有确定链接和组件之间的对应关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div id="app">
<!-- 声明路由链接 -->
<router-link to="/discover">发现音乐</router-link>
<router-link to="/my">我的音乐</router-link>
<router-link to="/friend">关注</router-link>
</div>
</template>

<script>
export default {
name: 'App',
components: {
}
}
</script>

接下来新建一个文件夹 src/router,新建一个叫做 index.js 的文件,用来将路径和组件匹配起来。并在最后导出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import VueRouter from "vue-router";
import Vue from "vue";
import Discover from "../components/Discover.vue";
import Friends from "../components/Friends.vue";
import My from "../components/My.vue";

Vue.use(VueRouter);

const router = new VueRouter({
routes: [
{ path:'/discover', component: Discover },
{ path:'/friends', component: Friends },
{ path:'/my', component: My },
]
})

export default router;

在 main.js 导入上面的 router:

1
2
3
4
5
6
7
8
9
10
import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
render: h => h(App),
router: router
}).$mount('#app')

那么这些组件显示在哪里呢?我们还需要一个路由占位符 <router-view></router-view>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div id="app">
<!-- 声明路由链接 -->
<router-link to="/discover">发现音乐</router-link>
<router-link to="/my">我的音乐</router-link>
<router-link to="/friend">关注</router-link>
<router-view></router-view>
</div>
</template>

<script>
export default {
name: 'App',
components: {
}
}
</script>

如果我们希望在首页也出现“发现”,可以在 index.js 设置重定向:

1
2
3
4
5
6
7
8
const router = new VueRouter({
routes: [
{ path: '/', redirect: '/discover' },
{ path:'/discover', component: Discover },
{ path:'/friends', component: Friends },
{ path:'/my', component: My },
]
})

如果希望嵌套跳转,如 /discover/play-list,可以在 component 后面加一个 children 属性,嵌套声明子路由。代码略。

现在思考一个问题,如果我们在购物界面,要显示许多商品,不可能把每一个商品都写一遍路由映射。

因此可以使用动态路由:

暂时先不实现,等以后有时间再做。

状态管理 VueX(未完成)

注意,B站教程【1天搞定SpringBoot+Vue全栈开发】 中本章仅仅介绍概念,不涉及具体敲代码实现。

state 是一个对象,包含应用中共享的数据

getter 是对 state 的计算结果(可以理解为 state 的派生状态)。

mutation更改状态的唯一推荐方式,必须是同步的

action 用于处理异步操作(例如后端接口调用),然后提交 mutation 来更改状态。

当你的项目变大,store 很臃肿时,就可以使用 modules 进行拆分。

Mock.js

写前端的时候,后端往往还没写好,因此需要模拟一些数据。当然我们可以直接写在 data 里面,但是 Mock.js 允许我们正常通过 Axios 发送请求,请求会被 Mock.js 拦截,拦截后生成随机响应数据,模拟后端服务器响应。

等后端开发好之后,只需要移除 mock 即可。

JWT 跨域认证

云服务器与云端部署

ECS,弹性计算服务,它让你可以像使用物理服务器一样,在云端快速创建、启动、管理和释放虚拟机

ECS 就是“云上的电脑”或“云主机”,你可以用它部署网站、跑程序、开数据库、搭服务,想开就开,想关就关,按量计费。

(就和北邮大三下大数据技术基础用的华为云ECS服务器一样)

建议使用XShell和Xftp进行服务器远程连接,后者是用于本地和云端文件传输的。先把JDK,Mysql,Nginx安装包传到云服务器。然后配置mysql,配置完就可以连接到服务器的mysql了,IP就是公网IP地址。

最后是前后端项目的部署。

注意,npm run serve是开发环境中使用的,在云服务器中不要用node,而是使用nginx服务器。在前端项目中,终端输入npm run build,打包的过程就是把vue文件生成为html代码(打包过程中需要修改.env.production中的base api配置,配置为服务器的公网IP)。打包完成后会放在dist目录中。把dist文件夹传到云服务器上。

进入/etc/nginx/conf.d创建vue.conf文件,配置:

1
2
3
4
5
6
7
8
9
server {
listen 80;
server_name localhost;

location / {
root /usr/app/dist;
index index.html
}
}

root /usr/app/dist;是你的dist目录路径。

修改后nginx -s reload加载配置。再回到我们本地浏览器,访问服务器公网IP,就可以访问到主页了。

后端我们在IDEA进行打包(maven栏点package),target目录下有jar包,上传到服务器,启动jar包即可启动后端。

至此,云服务器部署结束。

END 2025/07/18

Git

区域

工作区:自己电脑上的目录;

暂存区:临时区域,保存即将提交到git仓库的修改内容;

本地仓库:用git init创建的仓库。

状态有:未跟踪,未修改,已修改,已暂存:

.gitignore

自动生成链接:gitignore.io - 为你的项目创建必要的 .gitignore 文件

一点语法规则的示例:

本地仓库命令

git status -s:查看本地仓库状态,查看本地仓库在哪个分支,查看Modified,Untrack的文件等;

git add:放入暂存区;

git rm --cached:取消暂存,返回untrack态;

git rm:从工作目录中物理删除文件,从 Git 的暂存区中删除文件,将删除操作记录到下一次提交中;

git commit -m "msg":提交到仓库,只会提交暂存区的文件,而不会提交untrack文件;

git commit -am "msg":同时执行add和commit;

git log --oneline:提交记录。

git reset:用于撤销更改或移动 HEAD 指针(版本回退)。它有三种主要模式,每种模式对工作目录和暂存区的影响不同(这里的<commit>是回退的版本ID):

git reset --soft <commit>,只移动 HEAD 指针到指定提交,不修改暂存区(index),不修改工作目录。可用于合并多个提交为一个。

git reset --mixed <commit> 或简写 git reset <commit>重置暂存区到指定提交的状态,不修改工作目录(文件更改保留,但变为未暂存状态)。

git reset --hard <commit>重置暂存区重置工作目录到指定提交的状态(会丢失所有未提交的更改!)。

git diff:查看文件在工作区,暂存区,仓库之间的差异;查看文件在两个版本之间的差异;查看文件在两个分支之间的差异。默认为工作区和暂存区的差异。git diff HEAD~ HEAD 用于查看两次提交之间的差异。

远程仓库:

首先需要 Github 创建一个远程仓库,然后在本地连接到远程仓库,先在空目录 git init(如果这个目录原本就是git管理的目录,不要git init)再 git remote add origin <repository-url>,然后使用git push -u origin main 推送,把本地仓库master分支推送给远程仓库的main分支:

git fetch:只下载远程数据(更新本地存储的远程分支引用),不改变本地工作区。

git pull = git fetch + git merge(自动合并)

分支:

git branch:查看分支列表;

git branch <分支名> :创建分支;

git switch -c <分支名>:创建并切换分支;

git switch:切换分支,在切换分支的时候,磁盘上文件会出现变化,如果在dev分支修改,在切换到main分支之后,dev分支的修改不会显示。

举个例子,如果我们在main分支下创建了1.txt,并执行add和commit,再切换回dev分支,此时磁盘中(工作区)是没有1.txt文件的。

git merge <被合并分支>:把被合并分支合并到当前分支中。想要合并远程仓库,使用git pull 。一般建议先git switch切换分支,然后再merge。

git branch -d:删除分支。

合并分支中的冲突问题:修改同一文件同一行代码。

注意这种情况在 git merge 输入之后,所有不冲突的部分都会被 merge,冲突的部分会显示成下面这样:

1
2
3
4
5
<<<<<<< HEAD
对方的帆帆大大方方
=======
22121122112
>>>>>>> dev

也就是merge之后发现有冲突并不会回退到没merge之前,而是能merge的都merge了,如果想回退:git merge --abort 终止合并。

不想回退就直接手动编辑解决冲突,解决后commit即可,因为能merge的都已经merge了,只需要修改不能merge的冲突部分即可。

变基 Rebase:

rebase机制:先找到两个分支共同祖先,然后把当前分支上从共同祖先到现在最新的记录全部移动到目标分支后面

Git 管理规范:

Git Flow 工作流:

主要分支master上存放的是最稳定的正式版本,并且该分支的代码应该是随时可在生产环境中使用的代码。任何人不允许在主要分支上进行代码的直接提交,只接受其他分支的合入。原则上主要分支上的代码必须是合并自经过多轮测试及已经发布一段时间且线上稳定的预发分支。

开发分支develop是主开发分支,其上更新的代码始终反映着下一个发布版本需要交付的新功能。当开发分支到达一个稳定的点并准备好发布时,应该从该点拉取一个预发分支release并附上发布版本号。也有人称开发分支为集成分支,因为会基于该分支和持续集成工具做自动化的构建。开发分支接受其他辅助分支的合入,最常见的就是功能分支feature,开发一个新功能时拉取新的功能分支,开发完成后再并入开发分支。需要注意的是,合入开发的分支必须保证功能完整,不影响开发分支的正常运行

功能分支一般命名为 Feature/xxx,用于开发即将发布版本或未来版本的新功能或者探索新功能。该分支通常存在于开发人员的本地代码库而不要求提交到远程代码库上,除非几个人合作在同一个功能分支开发。

预发分支一般命名为 Release/1.2(后面是版本号),允许做小量级的Bug修复和准备发布版本的元数据信息(版本号、编译时间等)。预发分支需要提交到服务器上,交由测试工程师进行测试,并由开发工程师修复Bug。同时根据该分支的特性我们可以部署自动化测试以及生产环境代码的自动化更新和部署。预发分支只能拉取自开发分支,合并回开发分支和主要分支。

热修复分支一般命名为Hotfix/1.2.1(后面是版本号),当生产环境的代码(主要分支上代码)遇到严重到必须立即修复的缺陷时,就需要从主要分支上指定的tag版本(比如1.2)拉取热修复分支进行代码的紧急修复,并附上版本号(比如1.2.1)。热修复分支只能主要分支上拉取,测试通过后合并回主要分支和开发分支。

Maven

POM (Project Object Model):项目对象模型。每个 Maven 项目的核心都是一个 pom.xml 文件。它定义了项目的所有信息,包括项目坐标、依赖、插件、构建配置等。POM 是 Maven 工作的基础。

**坐标 (Coordinates / GAV)**:Maven 使用坐标来唯一标识一个项目、依赖或插件。坐标由以下三个向量组成:

  • groupId:项目所属的组织或公司,通常是反向域名(如 com.google.guava)。
  • artifactId:项目的唯一名称(如 guava)。
  • version:项目的版本号(如 31.0.1-jre)。
  • GAV (GroupId, ArtifactId, Version) 共同确定了一个独一无二的构件 (Artifact)。

**仓库 (Repository)**:用于存储所有项目构件(JAR、POM 等)的地方。Maven 的仓库分为三类:

  • 本地仓库 (Local Repository):位于你自己的电脑上(默认在 ~/.m2/repository)。当你构建项目时,Maven 会先从本地仓库查找依赖。如果找不到,它会从远程仓库下载并存入本地仓库。
  • 中央仓库 (Central Repository):Maven 官方提供的全球性仓库,包含了绝大多数流行的开源库。
  • 远程仓库/私服 (Remote Repository):通常由公司或团队在内部搭建,用于存放公司内部的构件或作为中央仓库的缓存代理。

构建生命周期 (Build Lifecycle)、阶段 (Phase) 和目标 (Goal)

  • 生命周期 (Lifecycle):代表一个完整的构建过程,由一系列有序的阶段组成。Maven 有三个主要的生命周期:
    • default:处理项目的构建和部署,这是最核心的生命周期。
    • clean:清理上一次构建生成的文件。
    • site:生成项目站点文档。
  • **阶段 (Phase)**:生命周期中的一个步骤。例如,default 生命周期包含 validate, compile, test, package, install, deploy 等阶段。当你执行一个阶段时,它之前的所有阶段都会被依次执行。例如,执行 mvn package 会先执行 compiletest
  • 插件 (Plugin) 和目标 (Goal):实际的工作是由插件完成的。一个插件可以包含一个或多个目标,每个目标对应一个具体的任务。Maven 会将插件的目标绑定到特定的生命周期阶段上。例如,maven-compiler-plugincompile 目标被绑定到 compile 阶段,用于编译源代码。

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<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">

<!-- 模型版本,对于 Maven 2 和 3 来说,这里总是 4.0.0 -->
<modelVersion>4.0.0</modelVersion>

<!-- 项目的坐标 (GAV) -->
<groupId>com.example</groupId>
<artifactId>my-app</artifactId>
<version>1.0-SNAPSHOT</version>

</project>

基本信息

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 项目坐标 GAV -->
<groupId>com.example</groupId>
<artifactId>my-app</artifactId>
<version>1.0-SNAPSHOT</version>

<!-- 项目打包方式,默认为 jar。可以是 war, ear, pom 等 -->
<packaging>jar</packaging>

<!-- 项目的可读名称和描述 -->
<name>My Awesome Application</name>
<description>A simple application to demonstrate Maven.</description>
<url>http://example.com/my-app</url>

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<dependencies>
<!-- 每个 <dependency> 标签代表一个依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<!-- 依赖范围 (scope),非常重要! -->
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
<!-- 默认 scope 是 compile -->
</dependency>

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<!-- provided 表示该依赖由容器(如 Tomcat)提供,打包时无需包含 -->
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.15</version>
<!-- 排除传递性依赖 -->
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

scope(依赖范围)详解:

  • compile (默认): 对编译、测试、运行都有效,会被打包。
  • test: 只对测试代码的编译和运行有效,不会被打包。例如 JUnit。
  • provided: 对编译和测试有效,但运行时由环境(如 JDK 或 Web 容器)提供,不会被打包。例如 Servlet API。
  • runtime: 对测试和运行有效,但在编译时不需要,会被打包。例如 JDBC 驱动。
  • system: 与 provided 类似,但需要你手动指定 JAR 包的路径,不推荐使用。
  • import: 只能在 <dependencyManagement> 中使用,用于导入其他 POM 的依赖管理配置。

属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<properties>
<!-- 定义项目编码 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 定义 Java 版本 -->
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<!-- 统一管理依赖版本 -->
<spring.version>5.3.15</spring.version>
</properties>

<!-- 在其他地方通过 ${...} 的方式引用 -->
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>

构建配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<build>
<!-- 自定义最终构件的名称 -->
<finalName>${project.artifactId}-${project.version}</finalName>

<!-- 插件管理 -->
<plugins>
<!-- 配置具体的插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<!-- 在这里配置插件的参数 -->
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>

父子项目:

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
<groupId>com.example</groupId>
<artifactId>my-parent-project</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 父项目的打包方式必须是 pom -->
<packaging>pom</packaging>

<!-- 声明所有子模块 -->
<modules>
<module>my-core-module</module>
<module>my-web-module</module>
</modules>

<!--
使用 <dependencyManagement> 统一管理所有子模块的依赖版本。
这里只是声明,子模块需要显式引入才会生效。
-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.15</version>
</dependency>
</dependencies>
</dependencyManagement>

子模块 POM (my-web-module/pom.xml):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<parent>
<!-- 继承自父项目 -->
<groupId>com.example</groupId>
<artifactId>my-parent-project</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<!-- 子模块的 artifactId -->
<artifactId>my-web-module</artifactId>
<packaging>war</packaging>

<dependencies>
<!--
引入 spring-context,无需指定 version。
它会自动继承父 POM 中 <dependencyManagement> 里声明的版本。
-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
</dependencies>
  • <parent>:实现继承。子模块继承父模块的 groupId, version, dependencies, plugins 等配置。
  • <modules>:实现聚合。在父项目目录下执行 Maven 命令(如 mvn package),Maven 会自动构建所有子模块。

环境配置:允许你为不同的环境(如开发 dev、测试 test、生产 prod)定义不同的构建配置。

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
<profiles>
<!-- 定义一个 'production' 环境 -->
<profile>
<id>production</id>
<properties>
<database.url>jdbc:mysql://prod-db.example.com/mydb</database.url>
</properties>
<build>
<plugins>
<!-- 在生产环境下启用代码混淆插件 -->
</plugins>
</build>
</profile>

<!-- 定义一个 'development' 环境 -->
<profile>
<id>development</id>
<!-- 默认激活该环境 -->
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<database.url>jdbc:mysql://dev-db.example.com/mydb</database.url>
</properties>
</profile>
</profiles>

使用时通过 -P 参数激活指定的 profile,例如:mvn package -P production

常用命令

  • mvn clean: 清理 target 目录,删除上次构建的结果。
  • mvn compile: 编译 src/main/java 下的 Java 文件。
  • mvn test: 运行 src/test/java 下的测试用例。
  • mvn package: 将项目打包成 jarwar 文件,存放在 target 目录。
  • mvn install: 将打包好的构件安装到本地仓库,供其他项目依赖。
  • mvn deploy: 将构件部署到远程仓库/私服。
  • mvn a_phase -DskipTests: 执行某个阶段,并跳过测试。
  • mvn dependency:tree: 以树状图展示项目的依赖关系,非常适合排查依赖冲突。

Docker

Docker 是一个开源的应用容器引擎,它允许开发者将应用及其所有依赖打包到一个可移植的、轻量级的、自包含的容器(Container)中,然后可以发布到任何支持 Docker 的 Linux、Windows 或 macOS 机器上,实现“一次构建,到处运行”(Build Once, Run Anywhere)。

Docker Daemon (dockerd):服务器端。一个在宿主机(Host OS)上以后台进程形式运行的服务。负责接收来自 Docker 客户端的命令,并管理 Docker 对象,包括镜像(Images)、容器(Containers)、网络(Networks)和卷(Volumes)的生命周期。它是 Docker 架构的核心。

Docker Client (docker):客户端。用户与 Docker Daemon 交互的主要接口,通常通过命令行交互。用户通过 docker 命令(如 docker run, docker pull, docker build 等)向 Docker Daemon 发送 REST API 请求,以执行相应的操作。客户端可以和 Daemon 在同一台宿主机上,也可以通过远程连接控制不同机器上的 Daemon。

Docker Registry:镜像仓库。用于存储和分发 Docker 镜像的服务。Registry 分为公共(Public)和私有(Private)两种。最著名的公共 Registry 是 Docker Hub,它提供了大量官方和社区维护的镜像。企业内部通常会搭建私有 Registry(如 Harbor)来管理自己的应用镜像。

镜像:一个只读的模板,包含了创建 Docker 容器所需的文件系统和运行参数。它是一个分层的文件系统(Layered Filesystem),由一系列的层(Layers) 构成。

容器:镜像的一个可运行实例,可以在这个实例中运行应用程序。容器是从镜像创建的,它拥有自己独立的文件系统、网络空间、进程空间和资源限制。每个容器都有自己的 pid (进程)、net (网络)、ipc (进程间通信)、mnt (挂载点)、uts (主机名) 和 user (用户) 命名空间。这使得容器内的进程看不到宿主机或其他容器的进程和网络。容器可以被创建、启动、停止、暂停、恢复和删除。

Dockerfile:一个文本文件,包含了一系列指令(Instructions),用于自动化地从一个基础镜像(Base Image)开始,一步步构建出一个新的 Docker 镜像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 1. 使用官方的、轻量级的 Node.js 16 镜像作为基础
FROM node:16-alpine

# 2. 添加元数据
LABEL maintainer="Docker Team <team@docker.com>"

# 3. 设置环境变量,方便路径管理和配置
ENV NODE_ENV=production
ENV APP_HOME=/usr/src/app

# 4. 创建应用目录并设置为工作目录
# 使用 WORKDIR 会自动创建目录,且后续指令都在此目录下执行
WORKDIR ${APP_HOME}

# 5. 复制依赖描述文件
# 利用 Docker 的缓存机制,只要 package*.json 不变,下面这层就不会重新构建
COPY package*.json ./

# 6. 安装生产环境的依赖
# --only=production 避免安装 devDependencies
# npm ci 更快更可靠
RUN npm ci --only=production && npm cache clean --force

# 7. 复制应用源代码到工作目录
# 放在安装依赖之后,这样修改代码时不会导致依赖重新安装
COPY venv .

# 8. 声明容器对外暴露的端口
EXPOSE 3000

# 9. (可选) 出于安全考虑,创建一个非 root 用户并切换
# RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# USER appuser

# 10. 定义容器启动时执行的默认命令
CMD [ "node", "server.js" ]

又如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# 使用Ubuntu镜像
FROM ubuntu:22.04

RUN sed -i "s@http://.*archive.ubuntu.com@http://mirrors.tuna.tsinghua.edu.cn@g" /etc/apt/sources.list
RUN sed -i "s@http://.*security.ubuntu.com@http://mirrors.tuna.tsinghua.edu.cn@g" /etc/apt/sources.list

WORKDIR /root/


# 安装JDK等
RUN apt update && \
apt install -y openjdk-8-jdk wget openssh-server nano vim less net-tools iputils-ping curl
ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64


# # 配置SSH免密码登录
RUN ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa && \
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys

COPY scripts/* /root/scripts/
RUN chmod -R 777 /root/scripts
ENV PATH=/root/scripts:$PATH

# 下载并解压Hadoop
# RUN wget https://mirrors.huaweicloud.com/apache/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz
COPY hadoop-3.3.6.tar.gz /root/
RUN tar -xzvf hadoop-3.3.6.tar.gz && \
rm hadoop-3.3.6.tar.gz && \
mv hadoop-3.3.6 hadoop && \
chmod -R 777 hadoop

COPY apache-zookeeper-3.9.3-bin.tar.gz /root/
RUN tar -xzvf apache-zookeeper-3.9.3-bin.tar.gz && \
rm apache-zookeeper-3.9.3-bin.tar.gz && \
mv apache-zookeeper-3.9.3-bin zookeeper && \
chmod -R 777 zookeeper

# COPY hbase-2.5.8-bin.tar.gz /root/
# RUN tar -xzvf hbase-2.5.8-bin.tar.gz && \
# rm hbase-2.5.8-bin.tar.gz && \
# mv hbase-2.5.8 hbase && \
# chmod -R 777 hbase

COPY flink-1.14.6-bin-scala_2.12.tgz /root/
RUN tar -xzvf flink-1.14.6-bin-scala_2.12.tgz && \
rm flink-1.14.6-bin-scala_2.12.tgz && \
mv flink-1.14.6 flink && \
chmod -R 777 flink

# flink-shaded-hadoop-3-uber-3.1.1.7.2.9.0-173-9.0.jar
# flink-shaded-zookeeper-3-3.8.3-18.0.jar
COPY flink-shaded-*.jar /root/flink/lib/
# commons-cli-1.4.jar
COPY commons-cli*.jar /root/flink/lib/
COPY flink_config/ /root/

COPY kafka_2.12-3.7.0.tgz /root/
RUN tar -xzvf kafka_2.12-3.7.0.tgz && \
rm kafka_2.12-3.7.0.tgz && \
mv kafka_2.12-3.7.0 kafka && \
chmod -R 777 kafka


# 配置环境变量
# ENV CLASSPATH=/usr/lib/jvm/java-8-openjdk-amd64/lib
# hadoop
RUN echo "slave1" > /root/hadoop/etc/hadoop/workers
RUN echo "slave2" >> /root/hadoop/etc/hadoop/workers
RUN echo "slave3" >> /root/hadoop/etc/hadoop/workers

ENV HADOOP_HOME=/root/hadoop
RUN mkdir $HADOOP_HOME/tmp
ENV HADOOP_TMP_DIR=$HADOOP_HOME/tmp
RUN mkdir $HADOOP_HOME/namenode
RUN mkdir $HADOOP_HOME/datanode
ENV HADOOP_CONFIG_HOME=$HADOOP_HOME/etc/hadoop
ENV PATH=$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$PATH
ENV HADOOP_CLASSPATH=$HADOOP_HOME/share/hadoop/tools/lib/*:$HADOOP_HOME/share/hadoop/common/lib/*:$HADOOP_HOME/share/hadoop/common/*:$HADOOP_HOME/share/hadoop/hdfs/*:$HADOOP_HOME/share/hadoop/hdfs/lib/*:$HADOOP_HOME/share/hadoop/yarn/*:$HADOOP_HOME/share/hadoop/yarn/lib/*:$HADOOP_HOME/share/hadoop/mapreduce/*:$HADOOP_HOME/share/hadoop/mapreduce/lib/*:$HADOOP_CLASSPATH
ENV HDFS_NAMENODE_USER="root"
ENV HDFS_DATANODE_USER="root"
ENV HDFS_SECONDARYNAMENODE_USER="root"
ENV YARN_RESOURCEMANAGER_USER="root"
ENV YARN_NODEMANAGER_USER="root"

# zookeeper
ENV ZOOKEEPER_HOME=/root/zookeeper
ENV PATH=$ZOOKEEPER_HOME/bin:$PATH

# hbase
# ENV HBASE_HOME=/root/hbase
# ENV PATH=$HBASE_HOME/bin:$PATH

# flink
ENV FLINK_HOME=/root/flink
ENV PATH=$FLINK_HOME/bin:$PATH

# kafka
ENV KAFKA_HOME=/root/kafka
ENV PATH=$KAFKA_HOME/bin:$PATH

# COPY flink_config/* /root/flink/conf/
# RUN echo "master" > /root/flink/conf/workers
# RUN echo "slave1" >> /root/flink/conf/workers
# RUN echo "slave2" >> /root/flink/conf/workers
# RUN echo "slave3" >> /root/flink/conf/workers

# RUN echo "master:8081" > /root/flink/conf/masters
# RUN echo "slave1:8081" >> /root/flink/conf/masters

# hadoop
COPY hadoop_config/* /root/hadoop/etc/hadoop/
RUN sed -i '1i export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64' /root/hadoop/etc/hadoop/hadoop-env.sh
# zookeeper
RUN mkdir /root/zookeeper/tmp
RUN cp /root/zookeeper/conf/zoo_sample.cfg /root/zookeeper/conf/zoo.cfg
COPY zookeeper_config/* /root/zookeeper/conf/
# echo "1" > /root/zookeeper/tmp/myid
# hbase
# COPY hbase_config/* /root/hbase/conf/
# RUN echo "export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64" >> /root/hbase/conf/hbase-env.sh
# RUN echo "export HBASE_MANAGES_ZK=false" >> /root/hbase/conf/hbase-env.sh
# RUN echo "export HBASE_LIBRARY_PATH=/root/hadoop/lib/native" >> /root/hbase/conf/hbase-env.sh
# RUN echo 'export HBASE_DISABLE_HADOOP_CLASSPATH_LOOKUP="true"' >> /root/hbase/conf/hbase-env.sh
# kafka
RUN sed -i "s/zookeeper.connect=.*/zookeeper.connect=master:2181,slave1:2181,slave2:2181,slave3:2181/" /root/kafka/config/server.properties

# ssh
RUN echo "StrictHostKeyChecking no" >> /etc/ssh/ssh_config
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
RUN echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config

RUN echo "export HADOOP_CLASSPATH=`hadoop classpath`" >> /root/.bashrc

ENTRYPOINT ["/root/scripts/inithosts.sh"]

RUN和CMD的区别,RUN 在镜像构建过程中执行指定的命令。每条 RUN 指令都会在当前镜像的顶部创建一个新的层,然后提交结果。常用于安装软件包、创建目录等;CMD 定义了容器启动时的行为。

镜像管理指令:

命令 作用 常用示例
docker pull 从 Docker Registry (如 Docker Hub) 拉取镜像 docker pull ubuntu:20.04
docker images 列出本地所有镜像 docker imagesdocker image ls
docker build 根据 Dockerfile 构建镜像 docker build -t my-app:1.0 .
docker rmi 删除一个或多个本地镜像 docker rmi my-app:1.0
docker tag 为镜像打上新的标签(tag) docker tag my-app:1.0 my-repo/my-app:latest
docker push 将镜像推送到 Docker Registry docker push my-repo/my-app:latest
docker history 显示镜像的构建历史(各层信息) docker history ubuntu:20.04
docker save 将镜像保存为一个 tar 归档文件 docker save -o my-app.tar my-app:1.0
docker load 从一个 tar 归档文件加载镜像 docker load -i my-app.tar

容器管理指令:

下面的指令可以进入容器或者在容器中进行操作:

命令 作用 常用示例
docker exec 在正在运行的容器中执行命令 docker exec -it my-web /bin/bash
docker attach 连接到正在运行的容器的标准输入/输出/错误流 docker attach my-interactive-container
docker logs 获取容器的日志 docker logs my-webdocker logs -f my-web (实时跟踪)
docker stats 实时显示容器的资源使用情况(CPU、内存等) docker stats
docker top 显示一个容器内正在运行的进程 docker top my-web
docker inspect 获取容器或镜像的详细元数据(JSON 格式) docker inspect my-web
docker cp 在容器和宿主机之间复制文件/文件夹 docker cp ./index.html my-web:/usr/share/nginx/html/

Docker Compose

Docker Compose 是一个用于定义和运行多容器 Docker 应用程序的工具。 它允许你使用一个 YAML 文件(通常是 docker-compose.yml)来配置应用的所有服务,然后用一条命令,就可以根据这个配置创建并启动所有服务。

假设我们要部署一个简单的 Web 应用,它包含一个 Python Flask 应用 (web) 和一个 Redis 缓存 (redis)。

我们的 docker-compose.yml 文件可能长这样:

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
# 使用 Compose 文件格式版本 3.8
version: '3.8'

# 定义所有服务
services:
# 第一个服务,名为 'web'
web:
# 从当前目录的 Dockerfile 构建镜像
build: .
# 将宿主机的 8000 端口映射到容器的 5000 端口
ports:
- "8000:5000"
# 挂载当前目录到容器的 /code 目录,方便代码修改立即生效
volumes:
- .:/code
# 设置一个环境变量
environment:
- FLASK_ENV=development
# 声明 'web' 服务依赖于 'redis' 服务
depends_on:
- redis

# 第二个服务,名为 'redis'
redis:
# 直接使用 Docker Hub 上的官方 redis 镜像
image: "redis:alpine"

又如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
services:
master:
image: exp_flink:1.0
container_name: master
hostname: master
ports:
- "9870:9870" # Expose the Flink master web UI
- "16010:16010" # Expose the Flink RPC port
- "8088:8088" # Expose the Flink job manager web UI
- "8081:8081" # Expose the Flink web UI
command: ["1"] # myid=1

slave1:
image: exp_flink:1.0
container_name: slave1
hostname: slave1
ports:
- "8082:8081" # Expose the Flink web UI
command: ["2"]

slave2:
image: exp_flink:1.0
container_name: slave2
hostname: slave2
ports:
- "8083:8081" # Expose the Flink web UI
command: ["3"]

slave3:
image: exp_flink:1.0
container_name: slave3
ports:
- "8084:8081" # Expose the Flink web UI
hostname: slave3
command: ["4"]

相关命令如下:

命令 作用 常用示例
docker-compose up 创建并启动 docker-compose.yml 中定义的所有服务 docker-compose up -d (后台运行)
docker-compose down 停止并移除 docker-compose.yml 定义的容器、网络等 docker-compose down --volumes (同时删除卷)
docker-compose ps 列出 Compose 项目中的所有容器 docker-compose ps
docker-compose build 构建或重新构建服务 docker-compose build
docker-compose logs 查看服务的日志输出 docker-compose logs -f my-service
docker-compose exec 在一个正在运行的服务容器中执行命令 docker-compose exec web /bin/bash

MQ 消息队列

用于解决分布式系统或微服务架构中不同组件(服务、应用、进程)之间的通信问题。

如:RabbitMQ,Apache Kafka,Apache RocketMQ,ActiveMQ

为什么使用MQ

  • 解耦。如图所示。假设有系统B、C、D都需要系统A的数据,于是系统A调用三个方法发送数据到B、C、D。这时,系统D不需要了,那就需要在系统A把相关的代码删掉。假设这时有个新的系统E需要数据,这时系统A又要增加调用系统E的代码。为了降低这种强耦合,就可以使用MQ,系统A只需要把数据发送到MQ,其他系统如果需要数据,则从MQ中获取即可

  • 异步。如图所示。一个客户端请求发送进来,系统A会调用系统B、C、D三个系统,同步请求的话,响应时间就是系统A、B、C、D的总和,也就是800ms。如果使用MQ,系统A发送数据到MQ,然后就可以返回响应给客户端,不需要再等待系统B、C、D的响应,可以大大地提高性能。对于一些非必要的业务,比如发送短信,发送邮件等等,就可以采用MQ。

  • 削峰。如图所示。这其实是MQ一个很重要的应用。假设系统A在某一段时间请求数暴增,有5000个请求发送过来,系统A这时就会发送5000条SQL进入MySQL进行执行,MySQL对于如此庞大的请求当然处理不过来,MySQL就会崩溃,导致系统瘫痪。如果使用MQ,系统A不再是直接发送SQL到数据库,而是把数据发送到MQ,MQ短时间积压数据是可以接受的,然后由消费者每次拉取2000条进行处理,防止在请求峰值时期大量的请求直接发送到MySQL导致系统崩溃

RabbitMQ的Hello World

这里我们以Windows为例,Linux端建议直接拉docker镜像。

首先RabbitMQ开发语言是Erlang,我们需要先下载安装Erlang(注意选择旧版本,这个链接有Erlang版本和RabbitMQ版本的兼容性矩阵:Erlang Version Requirements | RabbitMQ),链接Downloads - Erlang/OTP

安装后配置环境变量:

接下来安装RabbitMQ:Release RabbitMQ 3.7.3 · rabbitmq/rabbitmq-server

在sbin目录下执行./rabbitmq-plugins enable rabbitmq_management

然后双击rabbitmq-server.bat启动脚本:

访问RabbitMQ Management(localhost的15672端口):账号密码均为guest,即可登录。至此服务端安装完毕。现在创建一个Java Maven项目,这里采用父子项目。先创建一个父项目,pom.xml中声明<modules>,然后创建两个模块,分别是生产者和消费者。

生产者

引入依赖(消费者也需要进行引入):

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

在application.yml文件加上RabbitMQ的配置信息:

1
2
3
4
5
6
7
8
spring:
application:
name: producer
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest

创建一个Direct交换机以及队列的配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package org.example.producer.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {

@Bean
public DirectExchange directExchange() {
return new DirectExchange("direct.exchange");
}

@Bean
public Queue directQueue() {
return new Queue("direct.queue");
}

@Bean
public Binding directBinding(Queue directQueue, DirectExchange directExchange) {
return BindingBuilder.bind(directQueue).to(directExchange).with("direct.routing.key");
}
}

然后再创建一个发送消息的Service类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.example.producer.service;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MessageService {

@Autowired
private RabbitTemplate rabbitTemplate;

public void sendMessage(String message) {
rabbitTemplate.convertAndSend("direct.exchange", "direct.routing.key", message);
}
}

最后创建一个控制器类,调用service层的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.example.producer.controller;

import org.example.producer.service.MessageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MessageController {

@Autowired
private MessageService messageService;

@PostMapping("/send")
public String sendMessage(@RequestBody String message) {
messageService.sendMessage(message);
return "Message sent: " + message;
}
}

消费者

pom.xml和yaml略。消费者只需要创建一个Listener类,@RabbitListener注解写上监听队列的名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example.consumer.listener;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class MessageListener {

@RabbitListener(queues = "direct.queue")
public void receiveMessage(String message) {
System.out.println("Received message: " + message);
}
}

(实际使用中都是用DTO封装消息,且Routing Key不使用硬编码)

启动与测试:

先启动生产者,使用apipost发送一条消息,然后启动消费者:

测试成功。

RabbitMQ的组成

  • Broker:消息队列服务进程。此进程包括两个部分:Exchange和Queue。
  • Exchange:消息队列交换机。按一定的规则将消息路由转发到某个队列
  • Queue:消息队列,存储消息的队列。
  • Producer:消息生产者。生产方客户端将消息同交换机路由发送到队列中。
  • Consumer:消息消费者。消费队列中存储的消息

这些组成部分是如何协同工作的呢,大概的流程如下,请看下图:

  1. 建立连接 (建立通道)
    • 寄件人 (Producer) 首先要和 快递公司 (Broker) 建立一条 **专用通道 (Connection)**。
    • 为了提高效率,寄件人在这个通道上开辟了一条 **运输车道 (Channel)**。之后所有的包裹都通过这条车道发送。
    • 同样,收件人 (Consumer) 也通过同样的方式与快递公司建立好了自己的联系。
  2. 生产者发送消息 (寄件人发货)
    • 寄件人 (Producer) 写好了一个 **包裹 (Message)**。
    • 他在 快递单 (Message Properties) 上填写了一个重要的信息:**地址/关键词 (Routing Key)**,例如 log.error.db
    • 然后,他把这个包裹交给了指定的 **快递分拣中心 (Exchange)**,比如一个名为 logs_exchange 的交换机。
    • 生产者只与交换机打交道,它并不知道消息会进入哪个具体的队列。这实现了生产者和队列的解耦。
  3. 交换机进行路由 (分拣中心开始工作)
    • 分拣中心 (Exchange) 收到了这个包裹。它的工作核心是 如何转发。它本身不存储任何包裹。
    • 它会查看自己的 **分拣规则手册 (Bindings)**。这个手册记录了“哪个自提柜 (Queue) 对什么样的包裹感兴趣”。
    • 分拣规则的核心,取决于交换机的类型。这是 RabbitMQ 最灵活、最强大的部分。

Exchange的四种类型(重点):

Direct Exchange (直接匹配型),只有当消息的 Routing KeyBindingBinding Key 完全匹配时,消息才会被路由到对应的队列。

Fanout Exchange (广播型),一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。

Topic Exchange (主题/模式匹配型),使用通配符匹配。

Headers Exchange (首部匹配型),不是用routingKey进行路由匹配,而是在匹配请求头中所带的键值进行路由。

推荐教程:超详细的RabbitMQ入门,看这篇就够了!-阿里云开发者社区

Kafka

主题Topic:生产者把不同类型的消息放到不同的主题中,不同消费者可以订阅不同的主题;

分区Partition:每个主题可以细分为分区,每个分区可以被不同消费者线程并行处理;分区内消息有序,分区间不保证有序;如果想要保证交易记录是有序的,可以把一个用户对应一个分区,这样可以保证每个用户是有序的;

偏移量Offset:Offset 是 Partition 中每条消息的唯一标识,可以看作是数组的下标。

Broker:Kafka 集群中的一个服务器实例或节点。它本质上是一个运行着 Kafka 服务的进程。它负责接收生产者的消息,分配 Offset,持久化到磁盘,并响应消费者的拉取请求。每个Broker可以存储多个主题的多个分区

Broker集群:由多个 Broker 组成的分布式系统,实现可扩展性(可以通过增加 Broker 来线性扩展集群的存储容量和处理能力,可以实现负载均衡)和高可用性(Kafka 会为每个 Partition 创建多个副本,并将它们分布在不同的 Broker 上)。

Leader:在任意时刻,每个 Partition 的所有副本中,只有一个是 Leader Replica(领导者副本),所有读写请求都只由 Leader 处理。 生产者只向 Leader 发送数据,消费者只从 Leader 拉取数据。

Follower:Follower 的唯一任务就是不断地从 Leader 拉取数据,以保持与 Leader 的数据同步。如果某个 Partition 的 Leader所在的 Broker 挂掉了,Kafka 集群的控制器 (Controller)(也是一个 Broker)会从该 Partition 的 ISR 中选举一个新的 Follower 成为新的 Leader。

群组Group:多个消费者可以组成一个消费者组,共同消费同一或多个主题的消息,每条消息只能被同一组中的一个消费者消费,但可以被多个组消费。当一个消息需要被不同系统消费时,就可以用消费者组。

Kafka使用

首先进行安装配置,一般使用docker(win11家庭版需要强制配置Hyper-v,配置方法:csdn),终端输入:docker pull apache/kafka:4.0.1-rc0,拉取;

然后:docker run -d --name kafka -p 9192:9092 apache/kafka:4.0.1-rc0,后台启动;

最后:docker exec -it kafka bash,进入容器:

查询与创建主题(当前在/opt/kafka目录下):

1
2
3
bin/kafka-topics.sh --bootstrap-server localhost:9092 --list
bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe
bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic my-topic

在终端中发送与接收消息:

1
2
3
bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic my-topic
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic my-topic
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --from-beginning --topic my-topic

zookeeper与kafka集群(未完成!!)

Spring与kafka(未完成!!)

Hadoop

scala

RPC,Duddo,Spring Cloud