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 > <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()); 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 <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 () { 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 <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.4.2</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.30</version > </dependency > <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; @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> {}
注意,如果表名和类名不一样 ,可以写一个注解 @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") 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 ); 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) private int id; private String username; private String password; private String birthday; @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> { @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> { @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" ); 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 ); 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 > <div id ="app" > {{ message }}</div > <script > Vue .createApp ({ 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 > <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 : { 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 : '' , 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 的项目:
这里选择最后一项:
余下配置如下所示,这里的 package.json 可以类比之前的 pom.xml:
回车即可开始创建项目,结果如下,项目目录下出现一个 hello 的文件夹:
现在我们可以在 hello 目录下运行:
访问: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>  <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"> <!--  <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"> <!--  <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; @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 会先执行 compile 和 test。
插件 (Plugin) 和目标 (Goal):实际的工作是由插件完成的。一个插件可以包含一个或多个 目标 ,每个目标对应一个具体的任务。Maven 会将插件的目标绑定到特定的生命周期阶段上。例如,maven-compiler-plugin 的 compile 目标被绑定到 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" > <modelVersion > 4.0.0</modelVersion > <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 <groupId > com.example</groupId > <artifactId > my-app</artifactId > <version > 1.0-SNAPSHOT</version > <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 > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.13.2</version > <scope > test</scope > </dependency > <dependency > <groupId > com.google.guava</groupId > <artifactId > guava</artifactId > <version > 31.0.1-jre</version > </dependency > <dependency > <groupId > javax.servlet</groupId > <artifactId > javax.servlet-api</artifactId > <version > 4.0.1</version > <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 > <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 > <packaging > pom</packaging > <modules > <module > my-core-module</module > <module > my-web-module</module > </modules > <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 > my-web-module</artifactId > <packaging > war</packaging > <dependencies > <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 > <profile > <id > production</id > <properties > <database.url > jdbc:mysql://prod-db.example.com/mydb</database.url > </properties > <build > <plugins > </plugins > </build > </profile > <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: 将项目打包成 jar 或 war 文件,存放在 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 FROM node:16 -alpineLABEL maintainer="Docker Team <team@docker.com>" ENV NODE_ENV=productionENV APP_HOME=/usr/src/appWORKDIR ${APP_HOME} COPY package*.json ./ RUN npm ci --only=production && npm cache clean --force COPY venv . EXPOSE 3000 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 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/ 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-amd64RUN 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:$PATHCOPY 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 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 COPY flink-shaded-*.jar /root/flink/lib/ 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 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/hadoopRUN mkdir $HADOOP_HOME /tmp ENV HADOOP_TMP_DIR=$HADOOP_HOME/tmpRUN mkdir $HADOOP_HOME /namenode RUN mkdir $HADOOP_HOME /datanode ENV HADOOP_CONFIG_HOME=$HADOOP_HOME/etc/hadoopENV PATH=$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$PATHENV 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_CLASSPATHENV HDFS_NAMENODE_USER="root" ENV HDFS_DATANODE_USER="root" ENV HDFS_SECONDARYNAMENODE_USER="root" ENV YARN_RESOURCEMANAGER_USER="root" ENV YARN_NODEMANAGER_USER="root" ENV ZOOKEEPER_HOME=/root/zookeeperENV PATH=$ZOOKEEPER_HOME/bin:$PATHENV FLINK_HOME=/root/flinkENV PATH=$FLINK_HOME/bin:$PATHENV KAFKA_HOME=/root/kafkaENV PATH=$KAFKA_HOME/bin:$PATHCOPY 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 RUN mkdir /root/zookeeper/tmp RUN cp /root/zookeeper/conf/zoo_sample.cfg /root/zookeeper/conf/zoo.cfg COPY zookeeper_config/* /root/zookeeper/conf/ RUN sed -i "s/zookeeper.connect=.*/zookeeper.connect=master:2181,slave1:2181,slave2:2181,slave3:2181/" /root/kafka/config/server.properties 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 images 或 docker 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-web 或 docker 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 version: '3.8' services: web: build: . ports: - "8000:5000" volumes: - .:/code environment: - FLASK_ENV=development depends_on: - redis 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" - "16010:16010" - "8088:8088" - "8081:8081" command: ["1" ] slave1: image: exp_flink:1.0 container_name: slave1 hostname: slave1 ports: - "8082:8081" command: ["2" ] slave2: image: exp_flink:1.0 container_name: slave2 hostname: slave2 ports: - "8083:8081" command: ["3" ] slave3: image: exp_flink:1.0 container_name: slave3 ports: - "8084:8081" 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:消息消费者。消费队列中存储的消息
这些组成部分是如何协同工作的呢,大概的流程如下,请看下图:
建立连接 (建立通道)
寄件人 (Producer) 首先要和 快递公司 (Broker) 建立一条 **专用通道 (Connection)**。
为了提高效率,寄件人在这个通道上开辟了一条 **运输车道 (Channel)**。之后所有的包裹都通过这条车道发送。
同样,收件人 (Consumer) 也通过同样的方式与快递公司建立好了自己的联系。
生产者发送消息 (寄件人发货)
寄件人 (Producer) 写好了一个 **包裹 (Message)**。
他在 快递单 (Message Properties) 上填写了一个重要的信息:**地址/关键词 (Routing Key)**,例如 log.error.db。
然后,他把这个包裹交给了指定的 **快递分拣中心 (Exchange)**,比如一个名为 logs_exchange 的交换机。
生产者只与交换机打交道,它并不知道消息会进入哪个具体的队列。这实现了生产者和队列的解耦。
交换机进行路由 (分拣中心开始工作)
分拣中心 (Exchange) 收到了这个包裹。它的工作核心是 如何转发 。它本身不存储任何包裹。
它会查看自己的 **分拣规则手册 (Bindings)**。这个手册记录了“哪个自提柜 (Queue) 对什么样的包裹感兴趣”。
分拣规则的核心,取决于交换机的类型。这是 RabbitMQ 最灵活、最强大的部分。
Exchange的四种类型(重点):
Direct Exchange (直接匹配型),只有当消息的 Routing Key 与 Binding 的 Binding 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 Flink RPC,Duddo,Spring Cloud