Spring - 8 ( 10000 字 Spring 入门级教程 )

一: MyBatis

1.1 引入 MyBatis

我们学习 MySQL 数据库时,已经学习了 JDBC 来操作数据库, 但是 JDBC 操作太复杂了.

我们先来回顾⼀下 JDBC 的操作流程:

  1. 创建数据库连接池 DataSource
  2. 通过 DataSource 获取数据库连接 Connection
  3. 编写要执行带 ? 占位符的 SQL 语句
  4. 通过 Connection 及 SQL 创建操作命令对象 Statement
  5. 替换占位符:指定要替换的数据库字段类型,占位符索引及要替换的值
  6. 使用 Statement 执行 SQL 语句
  7. 查询操作:返回结果集 ResultSet,更新操作:返回更新的数量
  8. 处理结果集
  9. 释放资源

对于 JDBC 来说,整个操作非常的繁琐,我们不但要拼接每⼀个参数,而且还要按照模板代码方式,⼀步步的操作数据库,并且在每次操作完,还要手动关闭连接等,而所有的这些操作步骤都需要在每个方法中重复书写. 那有没有⼀种方法,可以更单、更方便的操作数据库呢?

这就是我们要学习 MyBatis 的真正原因,它可以帮助我们更方便、更快速的操作数据
库.

1.2 什么是 MyBatis

MyBatis 是⼀款优秀的持久层框架,⽤于简化 JDBC 的开发。

持久层:指的就是持久化操作的层, 通常指数据访问层(dao), 一般用来操作数据库.

在这里插入图片描述

二: MyBatis 入门

Mybatis操作数据库的步骤:

  1. 准备工作 (创建 springboot 工程、数据库表准备、实体类)
  2. 引入 Mybatis 的相关依赖,配置 Mybatis (数据库连接信息)
  3. 编写 SQL 语句 (注解 / XML)
  4. 测试

2.1 创建工程

  1. 创建 springboot 工程,并导入 mybatis 的起步依赖、mysql 的驱动包

在这里插入图片描述

Mybatis 是⼀个持久层框架, 具体的数据存储和数据操作还是在 MySQL 中操作的, 所以需要添加 MySQL 驱动

  1. 项目工程创建完成后自动在 pom.xml ⽂件中,导入 Mybatis 依赖和 MySQL 驱动依赖
<!--Mybatis 依赖包-->
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.3.1</version>
</dependency><!--mysql驱动包-->
<dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope>
</dependency>

2.2 数据准备

  1. 创建用户表, 并创建对应的实体类 User
-- 创建数据库
DROP DATABASE IF EXISTS mybatis_test;
CREATE DATABASE mybatis_test DEFAULT CHARACTER SET utf8mb4;
-- 使用数据库
USE mybatis_test;
-- 创建表[用户表]
DROP TABLE IF EXISTS userinfo;
CREATE TABLE `userinfo` (`id` INT(11) NOT NULL AUTO_INCREMENT,`username` VARCHAR(127) NOT NULL,`password` VARCHAR(127) NOT NULL,`age` TINYINT(4) NOT NULL,`gender` TINYINT(4) DEFAULT '0' COMMENT '1-男 2-女 0-默认',`phone` VARCHAR(15) DEFAULT NULL,`delete_flag` TINYINT(4) DEFAULT 0 COMMENT '0-正常, 1-删除',`create_time` DATETIME DEFAULT NOW(),`update_time` DATETIME DEFAULT NOW(),PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;
-- 添加用户信息
INSERT INTO mybatis_test.userinfo (username, `password`, age, gender, phone)
VALUES ('admin', 'admin', 18, 1, '18612340001');
INSERT INTO mybatis_test.userinfo (username, `password`, age, gender, phone)
VALUES ('zhangsan', 'zhangsan', 18, 1, '18612340002');
INSERT INTO mybatis_test.userinfo (username, `password`, age, gender, phone)
VALUES ('lisi', 'lisi', 18, 1, '18612340003');
INSERT INTO mybatis_test.userinfo (username, `password`, age, gender, phone)
VALUES ('wangwu', 'wangwu', 18, 1, '18612340004');
  1. 创建对应的实体类 UserInfo
import lombok.Data;
import java.util.Date;
@Data
public class UserInfo {private Integer id;private String username;private String password;private Integer age;private Integer gender;private String phone;private Integer deleteFlag;private Date createTime;private Date updateTime;
}

2.3 配置数据库连接字符串

Mybatis 中要连接数据库,需要数据库相关参数配置:

  • MySQL驱动类
  • 登录名
  • 密码
  • 数据库连接字符串
  1. application.yml 文件
# 数据库连接配置
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=falseusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driver
  1. application.properties 文件
#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver#数据库连接的url
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false#连接数据库的⽤⼾名
spring.datasource.username=root#连接数据库的密码
spring.datasource.password=root

2.4 写持久层代码

在项目中, 创建持久层接口 UserInfoMapper

在这里插入图片描述

import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;import java.util.List;@Mapper
public interface UserInfoMapper {//查询所有⽤⼾@Select("select username, `password`, age, gender, phone from userinfo")public List<UserInfo> queryAllUser();
}
  • @Mapper注解:这个注解标识了这个接口是一个 MyBatis 的 Mapper 接口,它告诉MyBatis框架要为这个接口创建一个实现类,并且这个实现类会自映射SQL语句的方法。
  • @Select注解:这个注解标识了 queryAllUser() 方法使用的查询语句,当需要执行数据库操作时,应用程序会调用 UserInfoMapper 接口中的 queryAllUser() 方法。

注意:Mybatis 的持久层接口规范⼀般都叫 XxxMapper

2.5 单元测试

2.5.1 使用 test 测试类

在创建出来的 SpringBoot 工程中,在 src 下的 test 目录下,已经自动帮我们创建好了测试类 ,我们可以直接使用这个测试类来进行测试.

import com.example.demo.mapper.UserInfoMapper;
import com.example.demo.model.UserInfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTest
class DemoApplicationTests {@Autowiredprivate UserInfoMapper userInfoMapper;@Test void contextLoads() {List<UserInfo> userInfoList = userInfoMapper.queryAllUser();System.out.println(userInfoList);}
}
  • @SpringBootTest 用于指示该类是一个 Spring Boot 应用程序的集成测试类。这个注解告诉测试运行器去加载整个 Spring 应用程序上下文,包括所有的 bean,并为测试提供一个实例化的应程序环境。
  • @Test 注解用于标识一个测试方法。当运行测试类时,它会搜索类中带有 @Test 注解的方法,并执行这些方法。

运行结果如下:

在这里插入图片描述

返回结果中, 可以看到, 只有 SQL 语句中查询的列对应的属性才有赋值

2.5.2 使用 Idea 自动生成测试类

除此之外, 也可以使用 Idea 自动生成测试类

  1. 在需要测试的Mapper接口中, 右键 -> Generate -> Test

在这里插入图片描述
2. 选择要测试的方法, 点击 OK

在这里插入图片描述

  1. 书写测试代码
import com.example.demo.model.UserInfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTest
class UserInfoMapperTest {@Autowiredprivate UserInfoMapper userInfoMapper;@Testvoid queryAllUser() {List<UserInfo> userInfoList = userInfoMapper.queryAllUser();System.out.println(userInfoList);}
}

记得加 @SpringBootTest 注解, 加载 Spring 行环境

运行结果:

在这里插入图片描述

三: MyBatis 的基础操作

上面我们学习了 Mybatis 的查询操作, 接下来我们学习 MyBatis 的增, 删, 改操作,在学习这些操作之前, 我们先来学习 MyBatis 日志打印

3.1 打印日志

在 Mybatis 当中我们可以借助日志, 查看到 sql 语句的执行语句、执行传递参数以及执行结果

  1. yml 配置
mybatis:configuration: # 配置打印 MyBatis⽇志log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  1. properties 配置
#指定mybatis输出⽇志的位置, 输出控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

重新运行程序, 可以看到 SQL 执行内容, 以及传递参数和执行结果

在这里插入图片描述

  • ①: 查询语句
  • ②: 传递参数及类型
  • ③: SQL执行结果

3.2 参数传递

@Select("select username, `password`, age, gender, phone from userinfo where id= 4 ")
UserInfo queryById();

这条语句只能查找 id=4 的数据,有时候我们需要 id 的数值是动态的,我们该怎么做呢

解决方案:

  1. 在 queryById 方法中添加⼀个参数 (id),将方法中的参数,传给 SQL 语句
  2. 然后使用 #{} 的方式获取方法中的参数
@Select("select username, `password`, age, gender, phone from userinfo where id= #{id} ")
UserInfo queryById(Integer id);

如果 mapper 接口方法形参只有⼀个普通类型的参数,#{…} 里面属性名可以随便写,但如果有多个参数的话,建议和参数名保持⼀致

  1. 测试用例
@Test
void queryById() {UserInfo userInfo = userInfoMapper.queryById(4);System.out.println(userInfo);
}
  1. 运行结果

在这里插入图片描述

我们也可以通过 @Param , 设置参数的别名, 如果使用 @Param 设置别名, #{…} 里面的属性名必须和 @Param 设置的⼀样

@Select("select username, `password`, age, gender, phone from userinfo where id= #{userid} ")
UserInfo queryById(@Param("userid") Integer id);

3.3 增 (Insert)

  1. SQL 语句:
insert into userinfo (username, `password`, age, gender, phone) values ("zhaoliu","zhaoliu",19,1,"18700001234")
  1. 把 SQL 中的常量替换为动态的参数,用 Mapper 接口:
@Insert("insert into userinfo (username, `password`, age, gender, phone) values (#{username},#{password},#{age},#{gender},#{phone})")
Integer insert(UserInfo userInfo);
  1. 测试代码:
@Test
void insert() {UserInfo userInfo = new UserInfo();userInfo.setUsername("zhaoliu");userInfo.setPassword("zhaoliu");userInfo.setGender(2);userInfo.setAge(21);userInfo.setPhone("18612340005");userInfoMapper.insert(userInfo);
}
  1. 如果设置了 @Param 属性, #{…} 需要使用参数.属性来获取
@Insert("insert into userinfo (username, `password`, age, gender, phone) values (#{userinfo.username},#{userinfo.password},#{userinfo.age},#{userinfo.gender},#{userinfo.phone})")
Integer insert(@Param("userinfo") UserInfo userInfo);

3.3.1 返回主键

Insert 语句默认返回的是受影响的行数,但有些情况下我们需要获取到新插入数据的 id ,如果想要拿到自增 id, 需要在 Mapper 接口的方法上添加⼀个 Options 的注解

@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into userinfo (username, age, gender, phone) values (#{userinfo.username},#{userinfo.age},#{userinfo.gender},#{userinfo.phone})")
Integer insert(@Param("userinfo") UserInfo userInfo);
  • @Options: 这是 Myatis 中的一个注解,用于配置一些选项以影响 SQL 语句的执行方式。
  • useGeneratedKeys = true 这个选项告诉 MyBatis 使用数据库自动生成的主键。
  • Property = “id”: 这个选项指定了用来接收生成的主键值的属性名。这个例子中,自动生成的主键值将会被赋给 UserInfo 对象中名为 id 的属性。

测试数据:

@Test
void insert() {UserInfo userInfo = new UserInfo();userInfo.setUsername("zhaoliu");userInfo.setPassword("zhaoliu");userInfo.setGender(2);userInfo.setAge(21);userInfo.setPhone("18612340005");Integer count = userInfoMapper.insert(userInfo);System.out.println("添加数据条数:" +count +", 数据ID:" + userInfo.getId());
}

运行结果:

在这里插入图片描述

注意: 设置 useGeneratedKeys=true 之后, 方法返回值依然是受影响的行数, 自增 id 会设置在上述 keyProperty 指定的属性中.

3.4 删(Delete)

SQL 语句:

delete from userinfo where id=6

把 SQL 中的常量替换为动态的参数:

@Delete("delete from userinfo where id = #{id}")
void delete(Integer id);

3.5 改(Update)

SQL 语句:

update userinfo set username="zhaoliu" where id=5

把 SQL 中的常量替换为动态的参数:

@Update("update userinfo set username=#{username} where id=#{id}")
void update(UserInfo userInfo);

3.6 查(Select)

我们在上面查询时发现, 有几个字段是没有赋值的, 只有Java对象属性和数据库字段⼀模⼀样时, 才会进行赋值,接下来我们多查询⼀些数据

@Select("select id, username, `password`, age, gender, phone, delete_flag, create_time, update_time from userinfo")
List<UserInfo> queryAllUser();
  1. 查询结果:

在这里插入图片描述

从运行结果上可以看到, 我们 SQL 语句中, 查询了 delete_flag, create_time, update_time, 但是这几个属性却没有赋值.

  1. 原因分析:

当自动映射查询结果时,MyBatis 会获取结果中返回的列名并在 Java 类中查找相同名字的属性(忽略大小写)。 这意味着如果发现了 ID 列和 id 属性,MyBatis 会将列 ID 的值赋给 id 属性,但如果名字不一样就无法进行赋值了。

在这里插入图片描述

解决办法:

  1. 起别名
  2. 结果映射
  3. 开启驼峰命名

注意:MyBatis 会根据方法的返回结果进行赋值.

  • 方法用对象 UserInfo 接收返回结果, MySQL 查询出来数据为⼀条, 就会自动赋值给对象.
  • 方法用 List< UserInfo > 接收返回结果, MySQL 查询出来数据为⼀条或多条时, 也会自动赋值给 List.
  • 但如果 MySQL 查询返回多条, 但是方法使用 UserInfo 接收, MyBatis 执行就会报错.

3.6.1 起别名

在 SQL 语句中,给列名起别名,保持别名和实体类属性名⼀样

@Select("select id, username, `password`, age, gender, phone, delete_flag as deleteFlag, " + "create_time as createTime, update_time as updateTime from userinfo")
public List<UserInfo> queryAllUser();

SQL 语句太长时, 使用加号 + 进行字符串拼接

3.6.2 结果映射

@Select("select id, username, `password`, age, gender, phone, delete_flag, create_time, update_time from userinfo")
@Results({@Result(column = "delete_flag",property = "deleteFlag"),@Result(column = "create_time",property = "createTime"),@Result(column = "update_time",property = "updateTime")
})
List<UserInfo> queryAllUser();

如果其他 SQL, 也希望可以复用这个映射关系, 可以给这个 Results 定义⼀个名称

@Select("select id, username, `password`, age, gender, phone, delete_flag, create_time, update_time from userinfo")
@Results(id = "resultMap",value = {@Result(column = "delete_flag",property = "deleteFlag"),@Result(column = "create_time",property = "createTime"),@Result(column = "update_time",property = "updateTime")
})
List<UserInfo> queryAllUser();@Select("select id, username, `password`, age, gender, phone, delete_flag, create_time, update_time " + "from userinfo where id= #{userid} ")@ResultMap(value = "resultMap")
UserInfo queryById(@Param("userid") Integer id);

使用 id 属性给该 Results 定义别名, 使用 @ResultMap 注解来复用其他定义的 ResultMap

在这里插入图片描述

3.6.3 开启驼峰命名 (推荐)

通常数据库列使用蛇形命名法进行命名,而 Java 属性⼀般遵循驼峰命名法约定,为了在这两种命名方式之间启用自动映射,需要将 mapUnderscoreToCamelCase 设置为 true。

mybatis:configuration:map-underscore-to-camel-case: true #配置驼峰⾃动转换

Java 代码不做任何处理

@Select("select id, username, `password`, age, gender, phone, delete_flag as deleteFlag, " + "create_time as createTime, update_time as updateTime from userinfo")
public List<UserInfo> queryAllUser();

添加上述配置, 运行代码:

在这里插入图片描述

字段全部进行正确赋值.

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

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

相关文章

21岁的人生赚51W!拒绝捞男捞女,翻身也要爱惜身体!——早读(逆天打工人爬取热门微信文章解读)

身体是革命的本钱 引言Python 代码第一篇 卢克文工作室 捞女在今天的中国是怎样的存在第二篇 人民日报 来啦 新闻早班车要闻社会政策 结尾 我将我的健康视为生活的基石 不会为了短暂的成功而牺牲 我珍惜身体 知道健康是实现梦想的前提 引言 这里毕竟是一个程序员的代码学习平台…

LVS/NAT工作模式介绍及配置

1.1 LVS/NAT模式工作原理 LVS&#xff08;Linux Virtual Server&#xff09;的网络地址转换&#xff08;NAT&#xff09;模式是一种在网络层&#xff08;第四层&#xff09;实现负载均衡的方法。在NAT模式中&#xff0c;Director Server&#xff08;DS&#xff09;充当所有服务…

54.HarmonyOS鸿蒙系统 App(ArkTS)tcp socket套接字网络连接收发测试

工程代码https://download.csdn.net/download/txwtech/89258409?spm1001.2014.3001.5501 54.HarmonyOS鸿蒙系统 App(ArkTS)tcp socket套接字网络连接收发测试 import socket from ohos.net.socket; import process from ohos.process; import wifiManager from ohos.wifiMana…

sql 中having和where区别

where 是用于筛选表中满足条件的行&#xff0c;不可以和聚类函数一起使用 having 是用于筛选满足条件的组 &#xff0c;可与聚合函数一起使用 所以having语句中不能使用select中定义的名字

51单片机软件环境安装

keli5的安装 把CID放到破解程序中 破解程序会给一串数字然后填到那个框中 驱动程序的安装 安装完了以后 设备管理器会出现这个 同时c盘会出现这个文件夹

Flask教程3:jinja2模板引擎

文章目录 模板的导入与使用 模板的导入与使用 Flask通过render_template来实现模板的渲染&#xff0c;要使用这个方法&#xff0c;我们需要导入from flask import rander_template&#xff0c;模板中注释需放在{# #}中 模板的第一个参数为指定的模板文件名称&#xff0c;如自定…

Rust Rocket创建第一个hello world的Web程序 Rust Rocket开发常用网址和Rust常用命令

一、Rust Rocket简介 Rust Rocket 是一个用 Rust 语言编写的 Web 应用框架&#xff0c;它结合了 Rust 的安全性和性能优势&#xff0c;以及 Web 开发的便利性。以下是 Rust Rocket 框架的一些优点&#xff1a; 安全性&#xff1a;Rust 是一种注重安全性的编程语言&#xff0c;…

【C++】学习笔记——vector_2

文章目录 七、vector2. vecotr的使用3. vector的模拟实现 未完待续 七、vector 2. vecotr的使用 上节我们以二维数组结束&#xff0c;这一节我们以二维数组开始。 // 二维数组 vector<vector<int>> vv;二维数组在底层是连续的一维数组。vv[i][j] 是怎样访问的&a…

分布式与一致性协议之一致哈希算法(二)

一致哈希算法 使用哈希算法有什么问题 通过哈希算法&#xff0c;每个key都可以寻址到对应的服务器&#xff0c;比如&#xff0c;查询key是key-01,计算公式为hash(key-01)%3,警告过计算寻址到了编号为1的服务器节点A&#xff0c;如图所示。 但如果服务器数量发生变化&#x…

vue3使用el-autocomplete请求远程数据

服务器端 RestController RequestMapping("/teacher") public class TeacherController {Resourceprivate TeacherService teacherService;GetMapping({"/v1/getTop10TeacherByName/","/v1/getTop10TeacherByName/{name}"})public ResultBean&l…

快速批量重命名文件(夹)

首先&#xff0c;需要用到的这个工具&#xff1a; 度娘网盘 提取码&#xff1a;qwu2 蓝奏云 提取码&#xff1a;2r1z 我这里处理这4个文本&#xff0c;实际可以处理任意数量的文本和文件夹 1、打开工具&#xff0c;进入文件批量复制版块 2、点击“重命名” 3、把要重命名的…

使用Python爬取淘宝商品并做数据分析

使用Python爬取淘宝商品并做数据分析&#xff0c;可以按照以下步骤进行操作&#xff1a; 确定需求&#xff1a;确定要爬取的淘宝商品的种类、数量、关键词等信息。 编写爬虫程序&#xff1a;使用Python编写爬虫程序&#xff0c;通过模拟浏览器请求&#xff0c;获取淘宝商品的页…

Docker 中的 Nginx 服务为什么要启用 HTTPS

一安装容器 1 安装docker-20.10.17 2 安装所需的依赖 sudo yum install -y yum-utils device-mapper-persistent-data lvm23 添加Docker官方仓库 sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo4 安装Docker CE 20.10.17 s…

第182期 23ai:惊喜的全功能缓存True Cache-2安装部署(20240505)

数据库管理182期 2024-05-05 数据库管理-第182期 23ai:惊喜的全功能缓存True Cache-2安装部署&#xff08;20240505&#xff09;1 主机配置2 操作系统配置2.1 基础配置2.2 配置hosts2.3 安装preinstall RPM包2.4 创建目录2.5 配置环境变量 3 部署数据库3.1 部署DB软件3.2 创建监…

机器学习:基于K-近邻(KNN)、高斯贝叶斯(GaussianNB)、SVC、随机森林(RF)、梯度提升树(GBDT)对葡萄酒质量进行预测

前言 系列专栏&#xff1a;机器学习&#xff1a;高级应用与实践【项目实战100】【2024】✨︎ 在本专栏中不仅包含一些适合初学者的最新机器学习项目&#xff0c;每个项目都处理一组不同的问题&#xff0c;包括监督和无监督学习、分类、回归和聚类&#xff0c;而且涉及创建深度学…

linux上如何排查JVM内存过高?

怎么排查JVM内存过高&#xff1f; 前言&#xff1a; 想必工作一两年以后的同学都会逐渐面临到&#xff0c;jvm等问题&#xff0c;但是可能苦于无法熟练的使用一些工具&#xff1b;本文将介绍几个比较常用分析工具的使用方法&#xff0c;带着大家一步步定位分析问题。 1、top 查…

springmvc下

第二类初始化操作 multipartResolver应用 localeResolver应用 themeResolver应用 handlerMapping应用 handlerAdapter应用 handlerExceptionReslver requestToViewNameTranslator应用 viewResolver应用 flashMapManager应用 dispatcherServlet逻辑处理 processRequest处理web请…

目标跟踪—卡尔曼滤波

目标跟踪—卡尔曼滤波 卡尔曼滤波引入 滤波是将信号中特定波段频率滤除的操作&#xff0c;是抑制和防止干扰的一项重要措施。是根据观察某一随机过程的结果&#xff0c;对另一与之有关的随机过程进行估计的概率理论与方法。 历史上最早考虑的是维纳滤波&#xff0c;后来R.E.卡…

【C语言】分支和循环(上)

【C语言】分支和循环&#xff08;上&#xff09; 1、if语句1.2 else1.3分支中包含多条语句1.4嵌套if1.5悬空else问题 2、关系操作符3、条件操作符4、逻辑操作符&#xff1a;与、或、非&#xff08;取反&#xff09;&#xff08;&&&#xff0c;||&#xff0c;&#xff0…

小猪APP分发平台 – 掌握移动应用的推广新途径

在移动互联网高速发展的背景下小猪APP分发平台 – 掌握移动应用的推广新途径&#xff0c;app分发已成为开发者和企业关注的重点。小猪APP分发平台作为行业内的新星小猪APP分发平台 – 掌握移动应用的推广新途径&#xff0c;提供了一个创新且高效的方式帮助开发者推广他们的应用…