Spring踩坑:抽象类作为父类,使用子类@Autowired属性进行填充,属性值为null
Spring踩坑:抽象类作为父类,使用子类@Autowired属性进行填充,属性值为null
- Spring Boot中抽象类和依赖注入的最佳实践
- 引言
- 在抽象类中使用@Autowired注解
- protected vs private修饰符
- 低版本Spring Boot的注意事项
- 构造器中的依赖注入陷阱
- 为什么不能在构造器中使用注入的属性?
- 子类构造的问题
- @PostConstruct的使用
- 正确使用@PostConstruct的例子
- 子类中的@PostConstruct
- 避免在构造器中使用ApplicationContext.getBean
- 错误示例
- 正确做法
- 最佳实践示例
- 常见问题和解决方案
- 1. 循环依赖
- 2. 依赖注入在单元测试中的问题
- 3. 属性注入vs构造器注入
- 4. 抽象类中的 @Autowired 方法
- 5. 运行时依赖注入
- 最佳实践总结
- 结论
Spring Boot中抽象类和依赖注入的最佳实践
引言
在Spring Boot应用程序中,抽象类经常被用作一种强大的设计模式,用于封装共同的行为和属性。然而,当涉及到依赖注入时,特别是在抽象类中,我们需要格外小心。本文将深入探讨在Spring Boot 2.0及以上版本中使用抽象类作为父类时的最佳实践,特别关注依赖注入的正确使用方式。
在抽象类中使用@Autowired注解
在Spring Boot 2.0及以上版本中,我们可以直接在抽象类的属性上使用@Autowired注解进行依赖注入。这为我们提供了一种方便的方式来在父类中定义共同的依赖,供子类使用。
protected vs private修饰符
当在抽象类中使用@Autowired注解时,我们通常有两种选择来修饰这些属性:protected或private。
-
使用protected修饰符:
public abstract class AbstractService {@Autowiredprotected SomeRepository repository; }优点:
- 允许子类直接访问注入的依赖
- 提供了更大的灵活性,子类可以根据需要重写或扩展这些依赖的使用
缺点:
- 可能会破坏封装性,因为子类可以直接修改这些依赖
-
使用private修饰符:
public abstract class AbstractService {@Autowiredprivate SomeRepository repository;protected SomeRepository getRepository() {return repository;} }优点:
- 保持了良好的封装性
- 父类可以控制子类如何访问这些依赖
缺点:
- 需要额外的getter方法来允许子类访问这些依赖
在Spring Boot 2.0中,这两种方式都是可行的。选择哪种方式主要取决于你的设计需求和偏好。如果你希望严格控制依赖的访问,使用private加getter方法可能是更好的选择。如果你希望提供最大的灵活性给子类,使用protected可能更合适。
低版本Spring Boot的注意事项
在低于2.0的Spring Boot版本中,使用protected修饰符通常是更安全的选择。这是因为在一些早期版本中,private字段的自动注入可能会遇到问题。如果你正在使用较旧的Spring Boot版本,建议使用protected修饰符来确保依赖能够正确注入。
构造器中的依赖注入陷阱
在抽象类中,我们经常需要在构造器中执行一些初始化逻辑。然而,这里有一个重要的陷阱需要注意:不应该在构造器中引用通过@Autowired注入的属性。
为什么不能在构造器中使用注入的属性?
原因在于Spring的bean生命周期和依赖注入的时机。当Spring创建一个bean时,它遵循以下步骤:
- 实例化bean(调用构造器)
- 注入依赖(设置@Autowired字段)
- 调用初始化方法(如@PostConstruct注解的方法)
这意味着在构造器执行时,@Autowired注解的属性还没有被注入,它们的值为null。如果你在构造器中尝试使用这些属性,很可能会遇到NullPointerException。
让我们看一个错误的例子:
public abstract class AbstractService {@Autowiredprivate SomeRepository repository;public AbstractService() {// 错误:此时repository还是nullrepository.doSomething();}
}
这段代码会在运行时抛出NullPointerException,因为在构造器执行时,repository还没有被注入。
子类构造的问题
这个问题在子类中更加复杂。当你创建一个抽象类的子类时,子类的构造器会首先调用父类的构造器。这意味着即使是在子类的构造器中,父类中@Autowired注解的属性仍然是null。
public class ConcreteService extends AbstractService {public ConcreteService() {super(); // 调用AbstractService的构造器// 错误:此时父类中的repository仍然是nullgetRepository().doSomething();}
}
这段代码同样会抛出NullPointerException,因为在调用子类构造器时,父类中的依赖还没有被注入。
@PostConstruct的使用
为了解决构造器中无法使用注入依赖的问题,Spring提供了@PostConstruct注解。被@PostConstruct注解的方法会在依赖注入完成后被自动调用,这使得它成为执行初始化逻辑的理想位置。
正确使用@PostConstruct的例子
public abstract class AbstractService {@Autowiredprivate SomeRepository repository;@PostConstructpublic void init() {// 正确:此时repository已经被注入repository.doSomething();}
}
在这个例子中,init()方法会在所有依赖注入完成后被调用,因此可以安全地使用repository。
子类中的@PostConstruct
子类也可以定义自己的@PostConstruct方法,这些方法会在父类的@PostConstruct方法之后被调用:
public class ConcreteService extends AbstractService {@Autowiredprivate AnotherDependency anotherDependency;@PostConstructpublic void initChild() {// 父类的init()方法已经被调用// 可以安全地使用父类和子类的所有依赖getRepository().doSomething();anotherDependency.doSomethingElse();}
}
这种方式确保了所有的初始化逻辑都在依赖注入完成后执行,避免了NullPointerException的风险。
避免在构造器中使用ApplicationContext.getBean
另一个常见的陷阱是在构造器中使用ApplicationContext.getBean()方法来获取bean。这种做法应该被避免,原因如下:
- 在构造器执行时,ApplicationContextAware接口可能还没有被调用,这意味着ApplicationContext可能还不可用。
- 即使ApplicationContext可用,其他bean可能还没有被完全初始化,调用getBean()可能会返回未完全初始化的bean或触发意外的初始化。
- 使用ApplicationContext.getBean()会使你的代码与Spring框架紧密耦合,降低了可测试性和可维护性。
错误示例
public abstract class AbstractService implements ApplicationContextAware {private ApplicationContext context;public AbstractService() {// 错误:此时context还是nullSomeBean someBean = context.getBean(SomeBean.class);}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.context = applicationContext;}
}
这段代码会抛出NullPointerException,因为在构造器执行时,setApplicationContext()方法还没有被调用。
正确做法
正确的做法是使用依赖注入,让Spring容器管理对象的创建和依赖关系:
public abstract class AbstractService {@Autowiredprivate SomeBean someBean;@PostConstructpublic void init() {// 正确:此时someBean已经被注入someBean.doSomething();}
}
这种方式不仅避免了NullPointerException,还降低了与Spring框架的耦合度,使代码更易于测试和维护。
最佳实践示例
让我们通过一个完整的例子来展示这些最佳实践:
@Service
public abstract class AbstractUserService {@Autowiredprivate UserRepository userRepository;@Autowiredprivate EmailService emailService;protected AbstractUserService() {// 构造器中不做任何依赖相关的操作}@PostConstructprotected void init() {// 初始化逻辑System.out.println("AbstractUserService initialized with " + userRepository.getClass().getSimpleName());}public User findUserById(Long id) {return userRepository.findById(id).orElse(null);}protected void sendEmail(User user, String message) {emailService.sendEmail(user.getEmail(), message);}// 抽象方法,由子类实现public abstract void processUser(User user);
}@Service
public class ConcreteUserService extends AbstractUserService {@Autowiredprivate SpecialProcessor specialProcessor;@PostConstructprotected void initChild() {System.out.println("ConcreteUserService initialized with " + specialProcessor.getClass().getSimpleName());}@Overridepublic void processUser(User user) {User processedUser = specialProcessor.process(user);sendEmail(processedUser, "Your account has been processed.");}
}// 使用示例
@RestController
@RequestMapping("/users")
public class UserController {@Autowiredprivate ConcreteUserService userService;@GetMapping("/{id}")public ResponseEntity<User> getUser(@PathVariable Long id) {User user = userService.findUserById(id);if (user != null) {userService.processUser(user);return ResponseEntity.ok(user);} else {return ResponseEntity.notFound().build();}}
}
在这个例子中:
-
AbstractUserService 是一个抽象类,它定义了一些通用的用户服务逻辑。 - 依赖(
UserRepository 和EmailService)通过@Autowired 注入到抽象类中。 - 初始化逻辑放在
@PostConstruct 注解的init() 方法中,确保在所有依赖注入完成后执行。 -
ConcreteUserService 继承自AbstractUserService,并实现了抽象方法。 -
ConcreteUserService 有自己的依赖(SpecialProcessor)和初始化逻辑。 - 在
UserController 中,我们注入并使用ConcreteUserService。
这个设计遵循了我们讨论的所有最佳实践:
- 在抽象类中使用
@Autowired 注入依赖 - 避免在构造器中使用注入的依赖
- 使用
@PostConstruct 进行初始化 - 不使用
ApplicationContext.getBean()
常见问题和解决方案
在使用抽象类和依赖注入时,开发者可能会遇到一些常见问题。以下是一些问题及其解决方案:
1. 循环依赖
问题:当两个类相互依赖时,可能会导致循环依赖问题。
解决方案:
- 重新设计以消除循环依赖
- 使用
@Lazy 注解来延迟其中一个依赖的初始化 - 使用 setter 注入而不是构造器注入
@Service
public class ServiceA {private ServiceB serviceB;@Autowiredpublic void setServiceB(@Lazy ServiceB serviceB) {this.serviceB = serviceB;}
}@Service
public class ServiceB {@Autowiredprivate ServiceA serviceA;
}
2. 依赖注入在单元测试中的问题
问题:在单元测试中,可能难以模拟复杂的依赖注入场景。
解决方案:
- 使用 Spring 的测试支持,如
@SpringBootTest - 为测试创建一个简化的配置类
- 使用模拟框架如 Mockito 来模拟依赖
@SpringBootTest
class ConcreteUserServiceTest {@MockBeanprivate UserRepository userRepository;@Autowiredprivate ConcreteUserService userService;@Testvoid testFindUserById() {when(userRepository.findById(1L)).thenReturn(Optional.of(new User(1L, "Test User")));User user = userService.findUserById(1L);assertNotNull(user);assertEquals("Test User", user.getName());}
}
3. 属性注入vs构造器注入
问题:虽然属性注入(使用 @Autowired on fields)很方便,但它可能使得依赖关系不那么明显。
解决方案:考虑使用构造器注入,特别是对于必需的依赖。这使得依赖关系更加明确,并有助于创建不可变的服务。
@Service
public abstract class AbstractUserService {private final UserRepository userRepository;private final EmailService emailService;@Autowiredprotected AbstractUserService(UserRepository userRepository, EmailService emailService) {this.userRepository = userRepository;this.emailService = emailService;}// ... 其他方法
}@Service
public class ConcreteUserService extends AbstractUserService {private final SpecialProcessor specialProcessor;@Autowiredpublic ConcreteUserService(UserRepository userRepository, EmailService emailService,SpecialProcessor specialProcessor) {super(userRepository, emailService);this.specialProcessor = specialProcessor;}// ... 其他方法
}
这种方法的优点是:
- 依赖关系更加明确
- 有助于创建不可变的服务
- 更易于单元测试
4. 抽象类中的 @Autowired 方法
问题:有时我们可能想在抽象类中有一个被 @Autowired 注解的方法,但这个方法在子类中被重写了。
解决方案:使用 @Autowired 注解抽象方法,并在子类中实现它。
public abstract class AbstractService {@Autowiredprotected abstract Dependencies getDependencies();@PostConstructpublic void init() {getDependencies().doSomething();}
}@Service
public class ConcreteService extends AbstractService {@Autowiredprivate Dependencies dependencies;@Overrideprotected Dependencies getDependencies() {return dependencies;}
}
这种方法允许子类控制依赖的具体实现,同时保持父类的通用逻辑。
5. 运行时依赖注入
问题:有时我们可能需要在运行时动态注入依赖,而不是在启动时。
解决方案:使用 ObjectProvider<T> 来延迟依赖的解析。
@Service
public abstract class AbstractDynamicService {@Autowiredprivate ObjectProvider<DynamicDependency> dependencyProvider;protected DynamicDependency getDependency() {return dependencyProvider.getIfAvailable();}// ... 其他方法
}
这种方法允许我们在需要时才解析依赖,这在某些场景下可能很有用,比如条件性的bean创建。
最佳实践总结
基于我们的讨论,以下是在Spring Boot中使用抽象类和依赖注入的最佳实践总结:
- 在抽象类中使用 @Autowired: 可以直接在抽象类的字段上使用 @Autowired 注解。使用 protected 修饰符可以让子类直接访问这些依赖,而使用 private 加 getter 方法可以提供更好的封装。
- 避免在构造器中使用注入的依赖: 构造器执行时,依赖还没有被注入,因此不应该在构造器中使用它们。
- 使用 @PostConstruct 进行初始化: 将需要依赖的初始化逻辑放在 @PostConstruct 注解的方法中,确保所有依赖都已注入。
- 不要在构造器中使用 ApplicationContext.getBean: 这可能导致意外的行为,因为在构造器执行时,ApplicationContext 可能还未完全准备好。
- 考虑使用构造器注入: 对于必需的依赖,构造器注入可以使依赖关系更加明确,并有助于创建不可变的服务。
- 处理循环依赖: 使用 @Lazy 注解或 setter 注入来解决循环依赖问题。
- 合理使用抽象方法: 在抽象类中定义抽象方法可以让子类控制某些依赖的具体实现。
- 使用 ObjectProvider 进行动态依赖注入: 当需要在运行时动态解析依赖时,考虑使用 ObjectProvider。
- 注意测试: 在单元测试中,使用 Spring 的测试支持和模拟框架来处理复杂的依赖注入场景。
- 遵循 SOLID 原则: 特别是单一责任原则和依赖倒置原则,这有助于创建更易维护和测试的代码。
结论
在Spring Boot中使用抽象类和依赖注入是一种强大的技术,可以帮助我们创建灵活、可维护的代码。然而,它也带来了一些挑战,特别是在处理依赖注入的时机和方式上。
通过遵循本文讨论的最佳实践,我们可以避免常见的陷阱,充分利用Spring Boot提供的依赖注入功能。记住,关键是要理解Spring Bean的生命周期,合理使用 @PostConstruct 注解,避免在不适当的时候访问依赖,并选择适合你的项目的依赖注入方式。
最后,虽然这些是普遍认可的最佳实践,但每个项目都有其独特的需求。因此,始终要根据你的具体情况来调整这些实践。持续学习和实践是掌握Spring Boot中抽象类和依赖注入的关键。
相关文章:
Spring踩坑:抽象类作为父类,使用子类@Autowired属性进行填充,属性值为null
Spring踩坑:抽象类作为父类,使用子类Autowired属性进行填充,属性值为null Spring Boot中抽象类和依赖注入的最佳实践引言在抽象类中使用Autowired注解protected vs private修饰符低版本Spring Boot的注意事项 构造器中的依赖注入陷阱为什么不…...
C#网络连接:TCP/IP模式下的网络连接与同步
1,目的 为了测试局域网的消息同步,简单写了下TCP/IP模式的同步,参考这个帖子。 2,核心库部分 using System; using System.Net; using System.Net.Sockets; using System.Text;namespace Coldairarrow.Util.Sockets {/// <s…...
基于树莓派(Raspberry Pi) 的智能电表监测系统设计:集成 Home Assistant、SQLite 和 MQTT 协议
在全球对可持续发展和能源节约的关注日益加深的背景下,智能能源管理系统(IEMS)应运而生。该系统利用现代科技(如物联网、云计算和大数据分析)来优化能源使用,提高能效,降低能源成本。本文将详细…...
C语言程序设计(二)
四.找素数 素数:除了1和它本身不再有其他因数的自然数。换句话说:一个大于1的自然数 ,如果只能被1和它本身整除,那就是素数(质数)。 在打印中遇到的问题就是,知道怎么写却总是运行不起来。主要…...
Oracle对数据库行和数据库的监控
前言: Oracle对表的监控分为数据行修改DML的监控、对表的DDL监控 1、对表的DML监控(数据的增删改) -- 创建测试表 create table tab_test01( id varchar2(100) default sys_guid(), name varchar2(100), insert_date date default sysdate…...
论文阅读:面向自动驾驶场景的多目标点云检测算法
论文地址:面向自动驾驶场景的多目标点云检测算法 概要 点云在自动驾驶系统中的三维目标检测是关键技术之一。目前主流的基于体素的无锚框检测算法通常采用复杂的二阶段修正模块,虽然在算法性能上有所提升,但往往伴随着较大的延迟。单阶段无锚框点云检测算法简化了检测流程,…...
Vite + Vue3 + TS项目配置前置路由守卫
在现代前端开发中,使用 Vue 3 和 TypeScript 的组合是一种流行且高效的开发方式。Vite 是一个极速的构建工具,可以显著提升开发体验。本文博主将指导你如何在 Vite Vue 3 TypeScript 项目中配置前置路由守卫(Navigation Guards)…...
设计模式-备忘录
备忘录(Memento)设计模式是为了保存对象当前状态,并在需要的时候恢复到之前保存的状态。以下是一个简单的C#备忘录模式的实现: // Originator 类,负责创建和恢复备忘录 class Originator {private string state;publi…...
openEuler安装docker,加速镜像拉取
文章目录 文章来源1.配置镜像源2.编辑配置文件3.安装想要的版本4. ~ 原神!5.由于很多镜像无法拉取配置镜像源 文章来源 http://t.csdnimg.cn/zYDYy 原文连接 由于之前的仓库不让用且 1.配置镜像源 由于 国外的镜像仓库好多不让用 所以配置阿里的镜像源 yum-confi…...
angular入门基础教程(七)系统路由
路由的实现 当我们系统越来复杂,功能越来越多,路由也就是必须的了。在 ng 中如何实现路由呢? 启用路由 在 app 目录下,新建一个 router 目录,把 app.routers.ts 文件拷贝过来,并修改一下。 import { Ro…...
Unity Canvas动画:UI元素的动态展示
在Unity中,Canvas是用于管理和展示用户界面(UI)元素的系统。Canvas动画是UI设计中的重要组成部分,它能够提升用户体验,使界面更加生动和响应用户操作。本文将探讨Unity Canvas动画的基本概念、实现方法以及一些实用的技…...
apache.commons.pool2 使用指南
apache.commons.pool2 使用指南 为什么要使用池 创建对象耗时较长,多线程频繁调用等因素限制了我们不能每次使用时都重新创建对象,使用池化思想将对象放进池内,不同线程使用同一个池来获取对象,极大的减少每次业务的调用时间。 …...
【Python面试题收录】Python编程基础练习题②(数据类型+文件操作+时间操作)
本文所有代码打包在Gitee仓库中https://gitee.com/wx114/Python-Interview-Questions 一、数据类型 第一题 编写一个函数,实现:先去除左右空白符,自动检测输入的数据类型,如果是整数就转换成二进制形式并返回出结果;…...
typescript 定义类型
type infoType string; let name: infoType "全易"; let location: infoType "北京"; // let age: infoType 18; // 报错 infoType string|number 就不报错了 let job: infoType "开发"; let love: infoType "吃喝玩乐&q…...
基于Java+SpringBoot+Vue的的课程作业管理系统
前言 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN[新星计划]导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取项目下载方式🍅 哈喽兄弟们,好久不见哦࿵…...
分布式日志分析系统--ELK
文章目录 ELK概述ELK主要特点ELK应用架构 Elasticsearch原理JSON格式倒排索引 ES与关系型数据库ES相关概念ES安装说明1.环境初始化2.优化系统资源限制配置3.编辑ES服务文件elasticsearch. yml 优化ELK集群安装脚本scp的使用集群安装成功 Shell命令API使用创建索引创建Type创建分…...
Linux初学基本命令
linux文件目录 1、bin->usr/bin binary存放命令 所有账户可以使用 Linux可以执行的文件,我们称之为命令command 2、boot 存放系统启动文件 3、dev device存放设备文件 4、etc 存放配置文件的目录 configration files 5、home home家目录 存…...
如何优化PyTorch以加快模型训练速度?
PyTorch是当今生产环境中最流行的深度学习框架之一。随着模型变得日益复杂、数据集日益庞大,优化模型训练性能对于缩短训练时间和提高生产力变得至关重要。 本文将分享几个最新的性能调优技巧,以加速跨领域的机器学习模型的训练。这些技巧对任何想要使用…...
用最简单的方法对大数据进行处理 vs spark(不需要安装大数据处理工具)
一、大文件处理策略 (一)、难点 内存管理: 大文件无法一次性加载到内存中,因为这可能会导致内存溢出(OutOfMemoryError)。 因此,需要使用流(Stream)或缓冲区(…...
非线性校正算法在红外测温中的应用
非线性校正算法在红外测温中用于修正传感器输出与实际温度之间的非线性关系。红外传感器的输出信号(通常是电压或电流)与温度的关系理论上是线性的,但在实际应用中,由于传感器特性的限制,这种关系往往呈现出非线性。非…...
TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...
微信小程序之bind和catch
这两个呢,都是绑定事件用的,具体使用有些小区别。 官方文档: 事件冒泡处理不同 bind:绑定的事件会向上冒泡,即触发当前组件的事件后,还会继续触发父组件的相同事件。例如,有一个子视图绑定了b…...
脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)
一、数据处理与分析实战 (一)实时滤波与参数调整 基础滤波操作 60Hz 工频滤波:勾选界面右侧 “60Hz” 复选框,可有效抑制电网干扰(适用于北美地区,欧洲用户可调整为 50Hz)。 平滑处理&…...
《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》
引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...
c++ 面试题(1)-----深度优先搜索(DFS)实现
操作系统:ubuntu22.04 IDE:Visual Studio Code 编程语言:C11 题目描述 地上有一个 m 行 n 列的方格,从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子,但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
【HTTP三个基础问题】
面试官您好!HTTP是超文本传输协议,是互联网上客户端和服务器之间传输超文本数据(比如文字、图片、音频、视频等)的核心协议,当前互联网应用最广泛的版本是HTTP1.1,它基于经典的C/S模型,也就是客…...
2023赣州旅游投资集团
单选题 1.“不登高山,不知天之高也;不临深溪,不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...
Spring是如何解决Bean的循环依赖:三级缓存机制
1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间互相持有对方引用,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...
【Nginx】使用 Nginx+Lua 实现基于 IP 的访问频率限制
使用 NginxLua 实现基于 IP 的访问频率限制 在高并发场景下,限制某个 IP 的访问频率是非常重要的,可以有效防止恶意攻击或错误配置导致的服务宕机。以下是一个详细的实现方案,使用 Nginx 和 Lua 脚本结合 Redis 来实现基于 IP 的访问频率限制…...
