万字详解 MapStruct Plus,带你快速上手!

news2025/2/24 20:50:11

与其明天开始,不如现在行动!

文章目录

  • 前言
  • 一、为什么要用 MapStruct(背景)
  • 二、MapStruct Plus 的快速开始
    • 1. 引入依赖
    • 2. 指定对象映射关系
    • 3. 编写测试代码
    • 4. 运行结果
    • 5. 原理解析
  • 三、自定义实体类中的属性转换
    • 1. 自定义一个类型转换器
    • 2. 使用类型转换器
    • 3. 进行测试
  • 四、Map 转为 Object
    • 1. 使用步骤
    • 2. 定义对象
    • 3. 转换测试
  • 五、枚举类型转换
    • 1. 定义一个枚举类
    • 2. 定义要转换的对象
    • 3. 转换测试
    • 4. 注意
  • 六、一个类与多个类之间的转换
    • 1. 定义对象
    • 2. 转换测试
    • 3. 测试结果
  • 总结


前言

Mapstruct 是一个代码生成器,基于约定优于配置的方法,极大简化了 Java bean 类型之间映射的实现,特点:速度快、类型安全且易于理解。

Mapstruct Plus 是 MapStruct 的增强工具(类似于 Mybatis 和 Mybatis Plus 的关系),其在 MapStruct 的基础上,实现了自动生成 Mapper 接口的功能,并强化了部分功能,使 Java 类型转换更便捷、优雅。

MapStruct Plus 内嵌 MapStruct,和 MapStruct 完全兼容,如果之前已经使用 MapStruct,可以无缝替换依赖。
参考网站:
MapStruct 官网
MapStruct Plus 官网


一、为什么要用 MapStruct(背景)

目前的系统开发中,对象模型之间需要相互转换,比如一个 User 对象需要转换为 UserVo 对象:

@Data
public class User {
    private String name;
    private int age;
    private String password;
}
@Data
public class UserVo {
    private String name;
    private int age;
}

常规的有两种方式:

  1. 使用 getter 和 setter 方法进行赋值,但是这个方法有着大量枯燥且重复的工作,一旦出错也不易于发现,可读性差。
  2. 使用 spring 提供的 BeanUtils 工具类进行对象之间的转换,如下代码块所示,但是因为内部采用反射实现,性能低下,出现问题时不容易调试。
// 创建一个 User 对象
User user = new User();
user.setName("wen");
user.setAge(18);
user.setPassword("123456");
// 创建一个 UserVo 对象
UserVo userVo = new UserVo();
// 一行代码实现 user => userVo
BeanUtils.copyProperties(user, userVo);

所以 MapStruct 应运而生,这个框架是基于 Java 注释处理器,定义一个转换接口,在编译的时候会根据接口类和方法相关的注解,自动生成实现类,底层是基于 getter 和 setter 方法的,比 BeanUtils 的性能要高。然而美中不足的是,当需要转换的对象较多或者结构复杂的时候,需要定义较多的转换接口和转换方法。

此时,就可以使用 MapStruct Plus ,一个注解就可以生成两个类之间的转换接口,使 Java 类型转换更加便捷和优雅。

二、MapStruct Plus 的快速开始

本文以 Spring Boot 项目为例,版本:
Spring Boot:3.3.2
JDK:17
Lombok:1.18.34

1. 引入依赖

引入 mapstruct-plus-spring-boot-starter 依赖

<dependency>
    <groupId>io.github.linpeilie</groupId>
    <artifactId>mapstruct-plus-spring-boot-starter</artifactId>
    <version>1.4.3</version>
</dependency>

引入 Maven 插件,配置项目的构建过程(这一步非常非常重要!!!)
引入 Maven 插件,配置项目的构建过程(这一步非常非常重要!!!)
引入 Maven 插件,配置项目的构建过程(这一步非常非常重要!!!)

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.1</version>
				<configuration>
					<source>17</source>
					<target>17</target>
					<annotationProcessorPaths>
						<path>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
							<version>${lombok.version}</version>
						</path>
						<path>
							<groupId>io.github.linpeilie</groupId>
							<artifactId>mapstruct-plus-processor</artifactId>
							<version>${mapstruct-plus.version}</version>
						</path>
						<path>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok-mapstruct-binding</artifactId>
							<version>0.2.0</version>
						</path>
					</annotationProcessorPaths>
				</configuration>
			</plugin>
		</plugins>
	</build>

最新版本依赖可以查看:MapStruct Plus 的 Maven 仓库地址

2. 指定对象映射关系

在 User 或者 UserVo 上面增加注解 —— @AutoMapper,并设置 target 为对方类。
以下面代码举例,添加注解:@AutoMapper(target = UserVo.class)

  1. User 类
@Data
@AutoMapper(target = UserVo.class)
public class User {
    private String username;
    private int age;
    private String password;
}
  1. UserVo 类
@Data
@AutoMapper(target = UserVo.class)
public class User {
    private String username;
    private int age;
    private String password;
}

3. 编写测试代码

@SpringBootTest
public class QuickStartTest {

    @Autowired
    private Converter converter;

    @Test
    public void test() {
        // 创建 User 对象
        User user = new User();
        user.setUsername("wen");
        user.setAge(18);
        user.setPassword("123456");

        // 使用 MapStruct plus 进行对象间转换:User =》 UserVo
        UserVo userVo = converter.convert(user, UserVo.class);
        // 输出转换之后的对象
        System.out.println(userVo);
        // 断言测试
        assert user.getUsername().equals(userVo.getUsername());
        assert user.getAge() == userVo.getAge();
    }

}

4. 运行结果

测试通过,输出:
在这里插入图片描述

5. 原理解析

通过以上示例可以看出,User 对象转化为 UserVo 对象主要是UserVo userVo = converter.convert(user, UserVo.class);这行代码,其底层也很简单,原理是通过 getter 和 setter 实现的:

public UserVo convert(User arg0) {
        if ( arg0 == null ) {
            return null;
        }

        UserVo userVo = new UserVo();

        userVo.setUsername( arg0.getUsername() );
        userVo.setAge( arg0.getAge() );

        return userVo;
    }

该代码被保存在 target 包中,具体路径:target/generated-sources/annotations/实体类存放路径
在这里插入图片描述
通过上图,可以看到,哪怕没有给 UserVo 实体类使用@AutoMapper注解,MapStruct Plus 会自动生成 User 转 UserVo 的接口和实现类,同时也会生成 UserVo 转换为 User 的实体类和接口。

以上为重要规则,下面也能用得到!!!


三、自定义实体类中的属性转换

在上面的例子中,两个实体类中对应的属性都是同一种类型,那么想要自定义属性比如:后端存储的是字符串 String 类型的属性,想给前端返回一个 List 类型的属性,可以根据规则进行转换。

下面的举例是 String 属性和 List 属性之间的相互转化(String 《===》List)

有两种方式:

  1. 自定义一个类型转换器,通过 @AutoMapperuses 属性引入
  2. 通过 @AutoMapping 中配置的 expression 表达式配置

1. 自定义一个类型转换器

首先定义两个类型转换器,一个是 String 转为 List,一个是 List 是 String。且两个类型转换器需要定义为 Spring 的 Bean,即使用 @Component 注解。
String 转为 List 的转换器:

@Component
public class StringToListConverter {

    public List<String> stringToList(String str) {
        if (str == null) {
            return Collections.emptyList();
        }
        return Arrays.asList(str.split(","));
    }

}

List 转为 String 的转换器:

@Component
public class ListToStringConverter {

    public String listToString(List<String> list) {
        if (list == null || list.isEmpty()) {
            return null;
        }
        return String.join(",", list);
    }

}

2. 使用类型转换器

第二步,使用该类型转换器,即在 @AutoMapper 注解中使用 uses,且给需要转化的属性加上 @AutoMapping 注解,target 指向另一个需要转化的属性。
User 类:

@Data
@AutoMapper(target = UserVo.class, uses = StringToListConverter.class)
public class User {
    private String name;
    private int age;
    private String password;
    @AutoMapping(target = "tagList")
    private String tags;
}

UserVo 类:

@Data
@AutoMapper(target = User.class, uses = ListToStringConverter.class)
public class UserVo {
    private String name;
    private int age;
    @AutoMapping(target = "tags")
    private List<String> tagList;
}

3. 进行测试

第三步,进行测试。

@SpringBootTest
public class QuickStartTest {

    @Autowired
    private Converter converter;

    @Test
    public void test() {
        // 创建一个 User 对象
        User user = new User();
        user.setName("wen");
        user.setAge(18);
        user.setPassword("123456");
        user.setTags("Java,Python,C++");

        // 转换
        UserVo userVo = converter.convert(user, UserVo.class);
        System.out.println(userVo);
        assert userVo.getTagList().size() == 3;
    }

}

测试结果:
测试用例通过,User 类中的 String 类型的 tags 属性,成功转化为 UserVo 类中的 List 类型的 tagList 属性。
在这里插入图片描述

还有一种方法是直接在注解中写表达式,但是博主觉得这种方式没有自定义转换器好,所以在本文中不列举
如果感兴趣,详情请参考:表达式自定义属性转换


四、Map 转为 Object

MapStruct Plus 提供了 Map<String, Object> 转化为对象的功能。

转换逻辑:
针对目标类中的一个属性,首先会判断 Map 中是否存在该键,如果存在的话,首先判断类型,

  • 如果类型相同,直接强转
  • 若果类型不同,会使用 Hutool 提供的类型转换工具尝试转换为目标类型

MapStruct Plus 在 1.4.0+ 版本取消了内置 Hutool 框架,如果需要用到 Map 转化为对象的功能时,需要引入 hutool-core 这个依赖,最新版本查看:Hutool 依赖库

<dependency>
   <groupId>cn.hutool</groupId>
   <artifactId>hutool-core</artifactId>
   <version>5.8.29</version>
</dependency>

1. 使用步骤

  1. 引入 hutool-core 依赖
  2. 在目标类上添加 @AutoMapMapper 注解
  3. 同时支持自定义类作为属性,需要在自定义类上增加 @AutoMapMapper 注解

2. 定义对象

为了更好的理解,直接用最复杂的 Map 转对象的例子举例,即内部属性既有基本类型,也有自定义的对象

定义一个 Body 类,里面有身高体重属性,定义一个 Person 类,里面有基本信息和一个 Body 类型的属性。

Body 类:

@Data
@AutoMapMapper
public class Body {
    private int height;
    private int weight;
}

Person 类:

@Data
@AutoMapMapper
public class Person {
    private String name;
    private Integer age;
    private Body body;
}

3. 转换测试

@SpringBootTest
public class MapToObjectTest {

    @Autowired
    private Converter converter;

    @Test
    public void test() {
        // 创建一个 Map,键是 Body 的属性名,值是属性值
        Map<String, Object> map1 = new HashMap<>();
        map1.put("height", 180);
        map1.put("weight", 150);

        // 创建第二个 Map,键是 Person 的属性名,值是属性值
        Map<String, Object> map2 = new HashMap<>();
        map2.put("name", "wen");
        map2.put("age", 18);
        map2.put("body", map1);

        // 将 Map2 转化为 Person 对象
        Person person = converter.convert(map2, Person.class);
        System.out.println(person);
    }

}

测试成功,Map 对象成功转化为 Person 对象:
在这里插入图片描述


五、枚举类型转换

枚举类型的转换,需要在枚举类上添加 @AutoEnumMapper 注解,增加该注解后,在任意类型中需要转换该枚举时都可以自动转换。
使用 @AutoEnumMapper 注解的时候,需要注意:这个枚举类必须要有一个可以保证唯一的字段,并将该字段添加到注解的 value属性中

1. 定义一个枚举类

定义一个状态枚举类,唯一字段是 code,用来表示开始还是关闭:

@Getter
@AllArgsConstructor
@AutoEnumMapper("code")
public enum StateEnum {
    ENABLE(1, "启用"),
    DISABLE(0, "禁用");
    private final int code;
    private final String desc;
}

2. 定义要转换的对象

定义一个保存枚举类的类 Course,再定义一个需要转换的 CourseVo 类:

Course 类:

@Data
@AutoMapper(target = CourseVo.class)
public class Course {

    private StateEnum state;

}

CourseVo 类:

@Data
public class CourseVo {

    private Integer state;

}

3. 转换测试

@SpringBootTest
public class EnumToValueTest {

    @Autowired
    private Converter converter;

    @Test
    public void test() {
        // 创建一个 Course 对象
        Course course = new Course();
        course.setState(StateEnum.ENABLE);

        // 将 Course 对象转换为 CourseVo 对象
        CourseVo courseVo = converter.convert(course, CourseVo.class);
        System.out.println(courseVo);

        // 将 CourseVo 对象转换为 Course 对象
        Course course1 = converter.convert(courseVo, Course.class);
        System.out.println(course1);
    }

}

测试成功,Enum 可以转化为整形,整形也可以转化为 Enum:
在这里插入图片描述

4. 注意

枚举和使用枚举的类需要在同一个模块(module)中。

当枚举与要使用的类型,不在同一个模块中,是不能自动转换的,需要指定依赖关系。在 @AutoMapper 注解中,可以通过 useEnums 来指定需要依赖的枚举类列表。


六、一个类与多个类之间的转换

MapStruct Plus 还支持一个类和多个类进行转换,可以通过 @AutoMappers 来配置,该注解支持配置多个 @AutoMapper

在配置多个类进行转化的时候,多个类可能有相同的属性,为了解决属性冲突的问题,可以使用 @AutoMappings 指定多个转换规则,并且在使用 @AutoMapping 注解时,配置 targetClass 属性,指定当前规则的目标转化类。

如果配置 @AutoMapping 注解时,没有指定 targetClass,那么当前规则就会应用所有类转换。

1. 定义对象

定义一个 User 类,一个 Course 类,一个 UserVo 类。其中 UserVo 类将与 User 类和 Course 类互相映射(UserVo 《===》User、Course)。User 类和 Course 类都有 name 属性,但是只将 User 类中的 name 属性映射。

User 类:

@Data
@AutoMapper(target = UserVo.class, uses = StringToListConverter.class)
public class User {

    private String name;

    private int age;

    private String password;

    @AutoMapping(target = "tagList")
    private String tags;
}

Course 类:

@Data
@AutoMapper(target = UserVo.class)
public class Course {

    @AutoMapping(targetClass = UserVo.class, ignore = true) // 忽略 UserVo 中的 name 属性
    private String name;

    private String teacher;

}

UserVo 类:

@Data
@AutoMappers({
        @AutoMapper(target = User.class, uses = ListToStringConverter.class),
        @AutoMapper(target = Course.class)
})
public class UserVo {

    @AutoMappings({
            @AutoMapping(targetClass = User.class),
            @AutoMapping(targetClass = Course.class, ignore = true)
    })
    private String name;

    private int age;

    @AutoMapping(targetClass = User.class, target = "tags")
    private List<String> tagList;

    private String teacher;
}

2. 转换测试

@SpringBootTest
public class OneToOthersTest {

    @Autowired
    private Converter converter;

    @Test
    public void test() {
        // 创建 User 对象
        User user = new User();
        user.setName("wen");
        user.setAge(18);
        user.setPassword("123456");
        user.setTags("Java,Python,Go,C++");

        // 创建 Course 对象
        Course course = new Course();
        course.setName("Java 开发");
        course.setTeacher("教 Java 的老师");

        // 转换(User 对象和 Course 对象)为 UserVo 对象
        UserVo userVo = converter.convert(user, UserVo.class);
        userVo = converter.convert(course, userVo);
        System.out.println(userVo);

        // 转换 UserVo 对象为(User 对象和 Course 对象)
        user = converter.convert(userVo, User.class);
        course = converter.convert(userVo, Course.class);
        System.out.println(user);
        System.out.println(course);
    }

}

3. 测试结果

在这里插入图片描述


总结

本文使用大量示例详细解释了在 Spring Boot 项目开发中使用 MapStruct Plus 的方法,多加练习熟能生巧。技术没有高低之分,不管是使用原始的 getter/setter 方法,还是使用 BeanUtils,亦或者使用本文所介绍的 MapStruct Plus,只要找到解决问题的合适方案就可以。

本文中若是有出现的错误请在评论区或者私信指出,我再进行改正优化。文章创作不易,如果对你有所帮助,请给博主一个宝贵的三连,感谢大家😘!!!


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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【IO】使用父子进程完成两个文件的拷贝,父进程拷贝前一半内容,子进程拷贝后一半内容,子进程结束后退出,父进程回收子进程的资源

1、使用父子进程完成两个文件的拷贝&#xff0c;父进程拷贝前一半内容&#xff0c;子进程拷贝后一半内容&#xff0c;子进程结束后退出&#xff0c;父进程回收子进程的资源 #include <myhead.h>int main(int argc, const char *argv[]) {//判断输入的格式是否符合要求i…

预测性维护:一种基于因果技术语言处理 (CTLP) 的智能故障诊断方法

关键词&#xff1a;预测性维护、因果贝叶斯网络、ROX数据 在工业运营和维护领域&#xff0c;资产绩效最大化和风险最小化至关重要。随着工业设备组件的磨损和恶化&#xff0c;系统会表现出一系列变化&#xff0c;这些变化的严重程度会逐渐增加&#xff0c;直到最终发生故障。在…

C++_string_知识总结(初学)

基础认识&#xff1a; string严格意义上不属于STL容器&#xff0c;其出现的时间早于STL&#xff0c;这也导致了string官方库中部分函数冗余。但是这也体现了语言发展中的一个重要现象——向前兼容。和很多STL容器一样&#xff0c;string是一个类&#xff0c;核心是一个顺序表&…

MySQL:GROUP BY 分组查询

分组查询是SQL中一个非常强大的功能&#xff0c;它允许我们将数据按照一个或多个字段进行分组&#xff0c;并对每个分组进行聚合计算&#xff08;如求和、平均值、最大值、最小值等&#xff09;。在MySQL中&#xff0c;我们使用 GROUP BY 关键字来实现分组查询。 核心语法 SE…

笑出腹肌的饼图绘制秘籍:Matplotlib让你秒变数据烘焙大师!

1. 引言 亲们&#xff0c;还在为数据报告里的饼图头疼吗&#xff1f;别怕&#xff0c;Matplotlib来救场啦&#xff01;它不只是个绘图工具&#xff0c;简直是数据界的魔术师&#xff0c;让你的饼图既专业又有趣。跟我学几招&#xff0c;保证让你的观众边吃边看&#xff0c;爱不…

Linux驱动开发—编写第一个最简单的驱动模块

文章目录 开发驱动准备工作1.正常运行的Linux系统的开发板2.内核源码树3.nfs挂载的rootfs4.得心趁手的IDE 第一个Hello world 驱动程序常见模块的操作命令模块的初始化和清理模块的版本信息模块中的各种宏 示例Hello World代码printk函数解析 使用MakeFile编译驱动模块使用insm…

谷歌账号异常,成功通过验证后这个界面操作指引:建议增加辅助手机和邮箱

许多朋友对下面这个界面都很熟悉&#xff0c;通常是账号被停用后的时候输入账号和密码后&#xff0c;还需要再次输入手机号码验证。而且这个时候输入国内的号码或者谷歌账号绑定的辅助手机号码都不管用&#xff0c;提示此电话号码用于验证的次数过多&#xff0c;或者此电话号码…

链表篇:03-合并有序链表

解题思路&#xff1a; 使用双指针&#xff0c;一个指针指向头节点&#xff0c;然后另外一个指针进行移动。让其头节点保持不动&#xff0c;最后循环遍历两个链表&#xff0c;将其挂到头指针所在的节点上。 temp 守卫节点&#xff0c;用于指向头节点&#xff0c;防止头节点丢…

机械学习—零基础学习日志(高数17——极限局部有界性)

零基础为了学人工智能&#xff0c;真的开始复习高数 这里我们更加详细讲解函数极限性质。上一篇文章里有一些内容还需要进一步补充。 局部有界性 这里是局部有界性的需要注意的事项。第3点&#xff0c;如果函数在闭区间内连续&#xff0c;则必定有界。试想一下&#xff0c;如…

Log4j2漏洞

Log4j2漏洞 步骤一:执行以下命令启动靶场环境并在浏览器访问!!! systemctl start docker cd vulhub/log4j/CVE-2021-44228 vi docker-compose.yml //编写docker-compose.xml的端口和版本号 docker-compose up -d # 访问网址 http://192.168.30.131:8983/solr/#/步骤二:先在自…

MyBatis入门如何使用操作数据库及常见错误(yml配置)

一&#xff0c;什么是MyBatis 是一款优秀的持久层框架&#xff0c;用于简化jdbc的开发 持久层&#xff1a;指的就是持久化操作的层&#xff0c;通常也就是数据访问层&#xff08;dao&#xff09;&#xff0c;也就是用来操作数据库。 也就是MyBatis是让你更加简单完成程序与数…

ECCV 2024前沿科技速递:GLARE-基于生成潜在特征的码本检索点亮低光世界,低光环境也能拍出明亮大片!

在计算机视觉与图像处理领域&#xff0c;低光照条件下的图像增强一直是一个极具挑战性的难题。暗淡的光线不仅限制了图像的细节表现&#xff0c;还常常引入噪声和失真&#xff0c;极大地影响了图像的质量和可用性。然而&#xff0c;随着ECCV 2024&#xff08;欧洲计算机视觉会议…

form表单按钮根据编辑/只读状态显示和隐藏

1. 场景阐述: form表单自定义按钮,在编辑模式显示,在只读模式隐藏 2. 效果: 这里的保存按钮是自定义按钮,在编辑状态的时候显示,非编辑状态下隐藏 3. 解决方案: 如下所示,只需要在按钮中添加odoo自带class类oe_edit_only即可 <header><button type"object"…

桌面管理利器:2024年度待办事项工具评选

国内外主流的10款待办事项桌面工具对比&#xff1a;PingCode、Worktile、滴答清单、番茄ToDo、Teambition、Tower、有道云笔记、TickTick、Any.do、Trello。 在忙碌的工作日中&#xff0c;管理日常任务和待办事项常常让人感到不胜其烦。选择合适的待办事项桌面工具&#xff0c;…

【Python实战因果推断】67_图因果模型2

目录 Are Consultants Worth It? Crash Course in Graphical Models Chains Are Consultants Worth It? 为了展示有向无环图(DAG)的力量&#xff0c;让我们考虑一个更有趣但处理因素并未随机化的情况。假设你是某公司的经理&#xff0c;正在考虑是否聘请顶级咨询顾问。你…

[数据结构] AVL树 模拟实现AVL树

标题&#xff1a;[数据结构] AVL树 && 模拟实现AVL树 水墨不写bug 正文开始&#xff1a; 目录 &#xff08;一&#xff09;普通二叉搜索树的痛点 &#xff08;二&#xff09;AVL树简介 &#xff08;1&#xff09;AVL树的概念 &#xff08;三&#xff09;AVL树的…

LeetCode面试150——189轮转数组

题目难度&#xff1a;中等 默认优化目标&#xff1a;最小化平均时间复杂度。 Python默认为Python3。 目录 1 题目描述 2 题目解析 3 算法原理及程序实现 3.1 暴力求解 3.2 循环链表 3.3 环状替代 3.4 数组翻转 4 题目难度 参考文献 1 题目描述 给定一个整数数组 nu…

运维.Linux.bash学习笔记.数组及其使用

运维专题 Bash Shell数组及其使用 此笔记当前仍在修改和编写。 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:http…

基于N32L406+Freertos+letter_shell终端开源库移植

移植教程 这里首先感谢作者的开源 https://gitee.com/zhang-ge/letter-shell) [Letter shell 3.0 全新出发 | Letter (nevermindzzt.github.io)](https://nevermindzzt.github.io/2020/01/19/Letter shell 3.0全新出发/) 1.复制代码 将litter_shell文件夹中的所有文件复制到…

本地使用Git同步、配合Gitee同步至仓库并下拉到本地(亲手调试,全能跑通)

这几天在公司&#xff0c;同事都在使用Gitee上传项目&#xff0c;进行同步&#xff0c;我也进行了简单学习了解了一下版本控制软件Git&#xff0c;挺不错的&#xff0c;故写个笔记记录一下。 本篇博文主要涉及的内容&#xff1a; 1&#xff0c;本地写代码&#xff0c;通过Git同…