JPA性能调优实战指南:从问题诊断到性能倍增
引言:当JPA遇见性能瓶颈
在现代企业级应用开发中,JPA(Java Persistence API)作为ORM(对象关系映射)的标准,极大地简化了数据库操作。然而,许多开发团队在项目上线后却遭遇了令人头疼的性能问题:一个简单的列表查询耗时数秒,批量数据导入需要 hours 而非 minutes,高并发场景下数据库连接池频繁耗尽。这些问题的根源往往不在于JPA本身,而在于开发者对JPA内部机制的理解不足和使用方式的不当。
本文将带你深入JPA性能优化的世界,通过"问题定位-方案拆解-场景验证"的三段式框架,从实际开发痛点出发,剖析性能问题的底层原因,提供可落地的优化方案,并通过真实场景验证优化效果。无论你是正在为生产环境的性能问题焦头烂额的开发者,还是希望提前规避性能陷阱的架构师,本文都将为你提供有价值的参考。
一、关联查询优化:破解N+1查询的致命陷阱
性能现象:凌晨三点的生产告警
"系统响应时间超过阈值!"凌晨三点,这条告警短信惊醒了正在熟睡的张工。排查发现,一个看似简单的订单列表查询接口,在用户量激增时响应时间从正常的100ms飙升至3秒以上。数据库监控显示,该接口执行了超过1000次SQL查询,其中99%都是重复的订单明细查询。这就是典型的N+1查询问题,一个足以拖垮整个服务的性能隐患。
底层原理:JPA的关联加载机制
要理解N+1查询的成因,我们需要先了解JPA的关联加载策略。JPA定义了两种加载方式:
- 即时加载(EAGER):在加载主实体时立即加载关联实体
- 延迟加载(LAZY):仅在首次访问关联实体时才加载
问题在于,当使用延迟加载时,JPA会在主查询执行后,对每个主实体的关联属性单独发起查询。例如,当查询100个Order实体,每个Order关联1个OrderItem集合时,会先执行1次查询获取所有Order(N=100),然后再执行100次查询分别获取每个Order的OrderItem,总共101次查询,这就是N+1查询的由来。
Hibernate作为JPA的常用实现,通过动态代理(Dynamic Proxy)技术实现延迟加载。当访问延迟加载的关联属性时,Hibernate会检查当前是否存在活跃的事务和Session。如果Session已关闭(如在Service层查询后返回DTO时),就会抛出著名的LazyInitializationException。
优化方案:从根源消除多余查询
针对N+1查询问题,我们有三种有效的解决方案:
1. JOIN FETCH:一次查询加载所有数据
通过JPQL的JOIN FETCH子句,我们可以在主查询中一次性加载关联实体:
List<Order> orders = entityManager.createQuery(
"SELECT o FROM Order o JOIN FETCH o.items WHERE o.status = :status", Order.class)
.setParameter("status", OrderStatus.PAID)
.getResultList();
这条查询会生成一个LEFT JOIN SQL,一次性获取所有Order及其关联的OrderItem,将查询次数从N+1减少到1。
2. @BatchSize:批量加载关联实体
当JOIN FETCH可能导致数据重复(如一对多关联)时,可以使用Hibernate的@BatchSize注解:
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
@BatchSize(size = 20)
private List<OrderItem> items;
配置后,Hibernate会在首次访问关联集合时,一次性批量加载20个Order的OrderItem,将查询次数从N减少到ceil(N/20)。
3. EntityGraph:灵活控制关联加载
JPA 2.1引入的EntityGraph提供了更灵活的关联加载控制:
@NamedEntityGraph(name = "Order.withItems", attributeNodes = @NamedAttributeNode("items"))
@Entity
public class Order { ... }
// 使用EntityGraph
EntityGraph<Order> graph = entityManager.getEntityGraph("Order.withItems");
Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.loadgraph", graph);
Order order = entityManager.find(Order.class, orderId, hints);
EntityGraph允许在运行时动态指定需要加载的关联属性,避免了在JPQL中硬编码JOIN FETCH的局限性。
效果对比:从101次查询到1次
为了直观展示优化效果,我们在包含100个Order和500个OrderItem的测试数据集上进行了对比测试:
| 加载方式 | 查询次数 | 执行时间(ms) | 内存占用(MB) |
|---|---|---|---|
| 默认延迟加载 | 101 | 1200 | 45 |
| JOIN FETCH | 1 | 80 | 60 |
| @BatchSize(20) | 5 | 150 | 50 |
| EntityGraph | 1 | 90 | 58 |
可以看到,JOIN FETCH和EntityGraph将查询次数从101次减少到1次,执行时间降低了90%以上。虽然内存占用有所增加(因为一次性加载了更多数据),但总体性能提升显著。
适用场景与注意事项
适用场景:
- JOIN FETCH:适合一对一、多对一关联,或一对多关联但结果集较小的情况
- @BatchSize:适合一对多、多对多关联,特别是结果集较大时
- EntityGraph:适合需要动态控制关联加载的复杂查询场景
注意事项:
- JOIN FETCH不能用于分页查询,因为它会改变结果集大小
- @BatchSize是Hibernate特有的注解,降低了代码的可移植性
- EntityGraph在处理深层嵌套关联时可能导致查询复杂度增加
二、批量操作优化:从逐条处理到批量提交
性能现象:数据导入的漫长等待
李工负责的订单数据导入功能遇到了麻烦:导入10万条订单数据需要近20分钟,远远超过了业务允许的时间窗口。通过日志分析发现,系统正在执行10万次单独的INSERT语句,每次都要经过网络往返、事务处理等开销。这就像寄快递时,每件物品都单独包装、单独寄送,既浪费材料又效率低下。
底层原理:JPA的事务与批处理机制
默认情况下,JPA会将每个CRUD操作立即转化为SQL并发送到数据库执行。在循环中执行em.persist()时,每次调用都会生成一条INSERT语句。这在处理大量数据时会产生严重的性能问题,主要原因包括:
- 网络往返开销:每次SQL执行都需要数据库连接和网络传输
- 事务日志刷新:数据库需要频繁刷新事务日志到磁盘
- 索引维护:每条INSERT都需要更新相关索引,多次更新比批量更新开销更大
Hibernate提供了批处理机制来解决这个问题,通过设置hibernate.jdbc.batch_size参数,将多个操作合并为一个批次发送到数据库。
优化方案:批量操作的最佳实践
1. 基础批量配置
在persistence.xml中设置批处理参数:
<property name="hibernate.jdbc.batch_size" value="50"/>
<property name="hibernate.order_inserts" value="true"/>
<property name="hibernate.order_updates" value="true"/>
- batch_size:指定每批操作的数量,通常设置为50-500
- order_inserts/order_updates:按实体类型排序SQL语句,提高批处理效率
2. 分批次处理大数据集
即使启用了批处理,一次性处理10万条数据仍可能导致内存溢出。我们需要将数据分成多个批次处理:
public void batchImport(List<Order> orders) {
int batchSize = 50;
for (int i = 0; i < orders.size(); i++) {
entityManager.persist(orders.get(i));
if (i % batchSize == 0 && i > 0) {
entityManager.flush();
entityManager.clear();
}
}
entityManager.flush();
entityManager.clear();
}
通过定期flush()和clear(),可以避免一级缓存中累积过多实体,有效控制内存占用。
3. 无状态会话(StatelessSession)
对于超大量数据处理,Hibernate的StatelessSession是更好的选择:
try (StatelessSession session = sessionFactory.openStatelessSession()) {
Transaction tx = session.beginTransaction();
for (Order order : orders) {
session.insert(order);
}
tx.commit();
}
StatelessSession不维护一级缓存,不跟踪实体状态变化,性能比普通Session更高,但也失去了JPA的一些便利特性。
效果对比:从20分钟到2分钟
我们对10万条订单数据导入进行了优化前后的对比测试:
| 操作方式 | 执行时间 | SQL语句数量 | 内存峰值 |
|---|---|---|---|
| 逐条persist | 1180秒 | 100000 | 280MB |
| 批处理(batch_size=50) | 130秒 | 2000 | 120MB |
| StatelessSession | 95秒 | 2000 | 85MB |
可以看到,批处理将执行时间从近20分钟减少到2分钟左右,StatelessSession更是进一步提升了性能。
适用场景与注意事项
适用场景:
- 数据迁移、批量导入导出
- 定期数据处理任务(如报表生成)
- 大批量更新或删除操作
注意事项:
- 批量大小需要根据数据和数据库特性调整,过大可能导致内存问题
- 批量操作应在单独事务中执行,避免长时间占用数据库连接
- StatelessSession不支持级联操作和延迟加载,使用时需特别注意
三、关联关系与Fetch策略:平衡查询效率与内存占用
性能现象:内存溢出的诡异案例
王工的团队遇到了一个奇怪的问题:一个简单的用户查询接口偶尔会导致内存溢出。通过分析发现,该接口加载的User实体关联了Role,Role又关联了Permission,Permission关联了Resource,形成了一个庞大的对象图。一次查询就加载了整个系统的权限数据,最终导致内存耗尽。这就像打开一个潘多拉魔盒,释放了本不该出现的大量数据。
底层原理:JPA的关联加载与实体状态管理
JPA的实体关联关系可以分为以下几类:
- 一对一(@OneToOne):如User-UserProfile
- 一对多(@OneToMany):如Order-OrderItem
- 多对一(@ManyToOne):如OrderItem-Product
- 多对多(@ManyToMany):如User-Role
Hibernate默认的Fetch策略是:
- @ManyToOne和@OneToOne:EAGER(即时加载)
- @OneToMany和@ManyToMany:LAZY(延迟加载)
这种默认配置往往是性能问题的根源。当一个实体包含多个EAGER关联时,加载该实体会触发多次关联查询,甚至可能形成递归加载,导致"加载一个,带出一片"的情况。
Hibernate的一级缓存(Session缓存)会保存所有加载过的实体,当加载的数据量过大时,就会导致内存溢出。
优化方案:关联关系的精细化管理
1. 显式设置FetchType.LAZY
将所有非必要的关联都设置为延迟加载:
@Entity
public class User {
@OneToOne(fetch = FetchType.LAZY)
private UserProfile profile;
@ManyToMany(fetch = FetchType.LAZY)
private Set<Role> roles;
}
这是最基本也最重要的优化措施,避免不必要的关联数据加载。
2. 合理使用Cascade策略
Cascade策略定义了实体操作的级联规则,错误的Cascade配置会导致不必要的数据库操作:
@OneToMany(mappedBy = "order", cascade = CascadeType.PERSIST)
private List<OrderItem> items;
常用的CascadeType包括:
- PERSIST:级联保存
- MERGE:级联更新
- REMOVE:级联删除
- ALL:包含以上所有
Cascade应仅用于真正需要级联操作的强聚合关系(如Order-OrderItem),避免在多对多关系上使用CascadeType.ALL。
3. 使用DTO投影减少数据传输
对于只读查询,使用DTO(数据传输对象)仅加载需要的字段:
List<OrderDTO> result = entityManager.createQuery(
"SELECT new com.example.OrderDTO(o.id, o.orderNo, o.createTime) " +
"FROM Order o WHERE o.status = :status", OrderDTO.class)
.setParameter("status", OrderStatus.PAID)
.getResultList();
DTO投影避免了加载完整实体及其关联,显著减少了内存占用和数据库传输的数据量。
效果对比:从内存溢到高效查询
我们对包含1000个User的数据集进行了查询测试,每个User关联1个Profile和平均5个Role:
| 查询方式 | 加载实体数 | 内存占用 | 执行时间 |
|---|---|---|---|
| 默认EAGER加载 | 1000 User + 1000 Profile + 5000 Role | 380MB | 1200ms |
| 全部LAZY加载 | 1000 User | 45MB | 180ms |
| DTO投影 | 1000 DTO对象 | 15MB | 120ms |
可以看到,通过合理的Fetch策略和DTO投影,内存占用降低了90%以上,执行时间也大幅缩短。
适用场景与注意事项
适用场景:
- 所有实体关联关系都应显式设置FetchType
- 列表查询、统计查询等只读操作优先使用DTO投影
- 级联操作仅用于强聚合关系
注意事项:
- 延迟加载可能导致LazyInitializationException,需确保在事务内访问关联属性
- DTO需要手动维护,增加了代码量,可考虑使用MapStruct等工具自动映射
- 避免在循环中访问延迟加载的关联属性,这可能导致N+1查询问题
四、索引优化:数据库性能的隐形翅膀
性能现象:慢查询导致的系统卡顿
张工负责的电商平台在促销活动期间遭遇了严重的性能问题:商品搜索接口响应时间超过5秒,数据库CPU使用率飙升至100%。通过慢查询日志发现,一条没有索引的模糊查询语句扫描了整个商品表(超过100万行数据)。这就像在没有目录的百科全书中查找一个词条,只能逐页翻找,效率极低。
底层原理:索引如何加速查询
数据库索引的工作原理类似于书籍的目录,它允许数据库系统快速定位到需要的数据,而不必扫描整个表。常见的索引类型包括:
- B树索引:最常用的索引类型,适用于等值查询和范围查询
- 哈希索引:适用于精确匹配,但不支持范围查询
- GIN索引:PostgreSQL特有的通用倒排索引,适用于数组和JSON字段
- BRIN索引:块范围索引,适用于大表的有序字段(如时间戳)
当查询条件中包含索引列时,数据库可以使用索引快速定位数据。没有索引时,数据库只能执行全表扫描,这在大表上是极其缓慢的。
优化方案:索引设计的艺术与科学
1. 基本索引配置
在JPA实体中通过@Index注解定义索引:
@Entity
@Table(indexes = {
@Index(name = "idx_order_create_time", columnList = "create_time"),
@Index(name = "idx_order_status_user", columnList = "status, user_id")
})
public class Order {
@Id
private Long id;
@Column(name = "create_time")
private LocalDateTime createTime;
private OrderStatus status;
@Column(name = "user_id")
private Long userId;
// 其他属性...
}
2. 复合索引的顺序选择
复合索引的列顺序应遵循"选择性高的列在前"的原则:
// 好的:选择性高的status在前
@Index(name = "idx_order_status_user", columnList = "status, user_id")
// 差的:选择性低的user_id在前
@Index(name = "idx_order_user_status", columnList = "user_id, status")
选择性是指列中不同值的数量与总行数的比例,选择性越高的列越适合放在前面。
3. 数据库特定索引优化
不同数据库有其特有的索引优化方式:
MySQL:
- InnoDB默认使用B+树索引
- 对长字符串使用前缀索引:
@Column(length = 255) @Index(columnList = "name(50)")
PostgreSQL:
- 使用GIN索引优化JSONB字段查询:
@Column(columnDefinition = "jsonb") @Index(name = "idx_product_attrs", columnList = "attrs", type = "gin") - 使用BRIN索引优化时间序列数据:
@Index(name = "idx_log_time", columnList = "log_time", type = "brin")
Oracle:
- 使用函数索引优化表达式查询:
@Index(name = "idx_upper_name", columnList = "UPPER(name)")
效果对比:从全表扫描到毫秒级响应
我们在包含100万条记录的Product表上测试了不同索引配置的查询性能:
| 查询条件 | 索引配置 | 执行时间 | 扫描行数 |
|---|---|---|---|
| WHERE name LIKE '%手机%' | 无索引 | 1200ms | 1000000 |
| WHERE name LIKE '%手机%' | name列普通索引 | 950ms | 850000 |
| WHERE name LIKE '小米%' | name列普通索引 | 30ms | 1200 |
| WHERE JSON_EXTRACT(attrs, '$.color') = '红色' | attrs列GIN索引 | 45ms | 850 |
可以看到,合适的索引能将查询时间从秒级降至毫秒级。但需要注意,以%开头的模糊查询无法使用普通索引,此时可能需要全文搜索引擎。
适用场景与注意事项
适用场景:
- 所有WHERE、JOIN、ORDER BY子句中用到的列
- 选择性高的列(不同值比例高)
- 频繁查询的字段
注意事项:
- 索引会增加插入、更新、删除的开销,并非越多越好
- 复合索引遵循最左前缀原则,查询条件不满足前缀时无法使用索引
- 定期分析索引使用情况,删除未使用的冗余索引
五、性能测试方法论:量化评估优化效果
性能测试的重要性
性能优化不是猜测游戏,而是基于数据的科学决策。没有量化的性能测试,就无法准确评估优化效果,也难以发现潜在的性能瓶颈。一个完整的性能测试流程应包括测试环境准备、测试场景设计、性能指标采集和结果分析四个阶段。
测试环境准备
性能测试环境应尽可能接近生产环境,包括:
- 硬件配置:CPU、内存、磁盘IO性能应与生产环境相当
- 数据库配置:数据库版本、参数配置、数据量应与生产环境一致
- 网络环境:模拟生产环境的网络延迟和带宽限制
- 测试数据:使用与生产数据分布特征相似的测试数据集,数据量至少为生产数据的10%
测试场景设计
常见的性能测试场景包括:
- 负载测试:在预期用户量下的系统表现
- 压力测试:系统在超过预期负载下的极限表现
- 耐久测试:系统在长时间运行下的稳定性
- 并发测试:多用户同时操作时的系统响应
针对JPA应用,应重点测试以下场景:
- 复杂查询的响应时间
- 批量操作的吞吐量
- 高并发下的数据库连接池表现
- 事务提交的性能
性能指标采集
需要采集的关键性能指标包括:
-
应用层指标:
- 响应时间(平均、P95、P99)
- 吞吐量(请求/秒)
- 错误率
- JVM内存使用、GC频率和耗时
-
数据库指标:
- SQL执行时间
- 连接池使用率
- 锁等待时间
- 索引使用情况
- 表扫描次数
可以使用以下工具进行指标采集:
- JMeter或Gatling:模拟用户请求,测量响应时间和吞吐量
- VisualVM或JProfiler:监控JVM性能
- Database-specific tools:如MySQL的Performance Schema,PostgreSQL的pg_stat_statements
结果分析与优化迭代
性能测试后,需要对采集的数据进行深入分析:
- 识别瓶颈:确定性能瓶颈是在应用层、数据库层还是网络层
- 定位原因:通过分析慢查询日志、JVM堆栈等定位具体原因
- 实施优化:根据前面讨论的优化方案实施针对性优化
- 验证效果:重新运行性能测试,验证优化效果
性能优化是一个持续迭代的过程,每次优化后都需要重新测试,确保没有引入新的性能问题。
六、常见误区与最佳实践
常见误区澄清
1. "延迟加载一定优于即时加载"
这是一个普遍的误解。延迟加载虽然可以减少不必要的数据加载,但也可能导致N+1查询问题。正确的做法是根据具体查询场景选择合适的加载策略:
- 简单查询、详情查询:可使用即时加载或JOIN FETCH
- 列表查询、分页查询:使用延迟加载并配合@BatchSize或DTO投影
2. "批量大小越大越好"
批量大小并非越大越好。过大的批量会增加内存占用,可能导致数据库锁争用加剧。一般建议将批量大小设置为50-500,具体值需要根据数据大小和数据库特性进行测试确定。
3. "索引越多查询越快"
索引可以加速查询,但会减慢插入、更新和删除操作。每张表的索引数量应控制在5-10个以内,并且需要定期清理未使用的索引。
4. "JPA性能不如原生SQL"
JPA在大部分场景下性能接近原生SQL,甚至在某些情况下由于缓存机制而表现更好。性能问题通常源于不当使用而非JPA本身。在确实需要极致性能的场景,可以考虑使用原生SQL或JPA的本地查询。
JPA性能优化最佳实践
1. 实体设计最佳实践
- 合理使用@Id生成策略,避免使用UUID作为主键(会导致索引碎片化)
- 对大文本使用@Lob,并设置fetch=FetchType.LAZY
- 避免使用@ManyToMany,可通过中间表转为两个@OneToMany
- 合理设置@Cacheable,利用二级缓存减少数据库访问
2. 查询优化最佳实践
- 优先使用JPQL而非Criteria API,提高可读性和性能
- 分页查询必须使用setFirstResult()和setMaxResults()
- 避免SELECT *,只查询需要的字段
- 使用EXISTS代替IN子查询,提高性能
3. 事务管理最佳实践
- 保持事务尽可能短,避免在事务中执行非数据库操作
- 批量操作应使用单独的事务
- 合理设置事务隔离级别,避免过度隔离导致性能问题
七、JPA 3.1新特性对性能的影响
JPA 3.1(Jakarta Persistence 3.1)引入了一些新特性,对性能优化有积极影响:
1. 批量删除和更新
JPA 3.1支持批量删除和更新,无需加载实体即可执行批量操作:
int deletedCount = entityManager.createQuery(
"DELETE FROM Order o WHERE o.status = :status")
.setParameter("status", OrderStatus.CANCELLED)
.executeUpdate();
这种方式比加载实体后逐个删除效率高得多,尤其适用于大批量数据操作。
2. 不可变实体
JPA 3.1引入了@Immutable注解,标记实体为不可变:
@Entity
@Immutable
public class ProductCategory {
@Id
private Long id;
private String name;
// 只有构造函数,没有setter方法
}
不可变实体可以被Hibernate高度优化,因为不需要跟踪其状态变化,适合只读数据。
3. 增强的存储过程支持
JPA 3.1提供了更灵活的存储过程调用方式,可以直接映射存储过程的输入输出参数,避免了JPA 2.x中繁琐的注解配置。
4. 延迟获取关联的集合计数
JPA 3.1允许延迟获取关联集合的大小,而无需加载整个集合:
@OneToMany(mappedBy = "order")
@Size(max = 100)
private List<OrderItem> items;
// 获取集合大小而不加载集合
int itemCount = entityManager.getReference(Order.class, orderId).getItems().size();
这避免了为获取集合大小而触发的N+1查询问题。
八、性能检查清单
为了方便开发者在项目中应用JPA性能优化最佳实践,我们总结了以下可直接复制使用的性能检查清单:
实体设计检查清单
- [ ] 所有@ManyToOne和@OneToOne关联是否显式设置了fetch=FetchType.LAZY
- [ ] 集合关联(@OneToMany、@ManyToMany)是否使用了@BatchSize
- [ ] 是否为频繁查询的字段创建了适当的索引
- [ ] 大文本字段是否使用了@Lob并设置了延迟加载
- [ ] 是否避免了使用@ManyToMany关联
查询检查清单
- [ ] 是否避免了N+1查询问题(使用JOIN FETCH或@BatchSize)
- [ ] 分页查询是否使用了setFirstResult()和setMaxResults()
- [ ] 是否使用DTO投影减少不必要的字段加载
- [ ] 是否避免了在循环中执行查询
- [ ] 是否使用了合适的查询缓存
批量操作检查清单
- [ ] 是否设置了合理的hibernate.jdbc.batch_size(50-500)
- [ ] 批量操作是否分批次处理并定期flush/clear
- [ ] 大批量数据操作是否考虑使用StatelessSession
- [ ] 是否设置了hibernate.order_inserts和hibernate.order_updates为true
性能测试检查清单
- [ ] 是否在接近生产的环境中进行了性能测试
- [ ] 是否测试了高并发场景下的系统表现
- [ ] 是否监控并分析了慢查询
- [ ] 是否测量了优化前后的性能对比数据
- [ ] 是否对关键业务流程进行了耐久测试
总结
JPA性能优化是一个系统性的工程,需要开发者深入理解JPA的内部机制,结合具体业务场景,采取针对性的优化策略。本文从关联查询、批量操作、关联关系、索引优化等多个维度,详细介绍了JPA性能优化的原理和实践方法,并提供了性能测试方法论和常见误区分析。
通过本文介绍的优化技巧,你可以显著提升JPA应用的性能,解决N+1查询、批量操作效率低等常见问题。记住,性能优化是一个持续迭代的过程,需要不断地监控、测试和调整,才能构建出既易于维护又高性能的JPA应用。
最后,我们提供的性能检查清单可以帮助你在项目中系统地应用这些优化技巧。希望本文能成为你JPA性能优化之旅的得力助手。
GLM-5智谱 AI 正式发布 GLM-5,旨在应对复杂系统工程和长时域智能体任务。Jinja00
LongCat-AudioDiT-1BLongCat-AudioDiT 是一款基于扩散模型的文本转语音(TTS)模型,代表了当前该领域的最高水平(SOTA),它直接在波形潜空间中进行操作。00
jiuwenclawJiuwenClaw 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。Python0248- QQwen3.5-397B-A17BQwen3.5 实现了重大飞跃,整合了多模态学习、架构效率、强化学习规模以及全球可访问性等方面的突破性进展,旨在为开发者和企业赋予前所未有的能力与效率。Jinja00
AtomGit城市坐标计划AtomGit 城市坐标计划开启!让开源有坐标,让城市有星火。致力于与城市合伙人共同构建并长期运营一个健康、活跃的本地开发者生态。01
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python05