文章目录
Java 异步编排与同步工具类对比
一、Java 异步编排概述
1. 什么是异步编排?
异步编排(Asynchronous Composition)是指通过组合多个异步任务,实现任务间的依赖关系、并行执行或结果聚合的编程方式。核心目标是提高程序响应速度和系统吞吐量。
2. 核心工具:CompletableFuture
- Java 8 引入,是
Future
接口的增强版本。 - 支持链式调用、组合操作(如
thenApply
,thenAccept
,thenCompose
)。 - 提供异常处理、超时控制、回调机制等功能。
- 适用场景:需要处理多个异步任务的依赖关系、并行执行、结果聚合等。
二、CompletableFuture 的优点
优点 | 说明 |
---|---|
非阻塞式调用 | 通过 supplyAsync 或 runAsync 提交任务后,主线程无需等待结果即可继续执行其他操作。 |
任务组合能力 | 支持链式调用(如 thenApply 处理结果)、多任务并行(如 thenCombine 合并结果)、任务依赖(如 thenCompose )。 |
异常处理机制 | 通过 .exceptionally() 或 .handle() 捕获异常并提供兜底逻辑。 |
回调支持 | 支持 whenComplete 、handle 等回调方法,灵活处理任务完成后的逻辑。 |
资源管理优化 | 可指定自定义线程池,避免阻塞主线程,提升系统并发性能。 |
三、同步工具类对比
1. CountDownLatch
- 功能:等待一组线程完成操作后再继续执行。
- 特点:
- 计数器只减不增,不可重用。
- 适用于一次性事件(如初始化、任务分发后等待完成)。
- 示例:
CountDownLatch latch = new CountDownLatch(3); for (int i = 0; i < 3; i++) { new Thread(() -> { // 执行任务 latch.countDown(); }).start(); } latch.await(); // 主线程等待
2. CyclicBarrier
- 功能:让一组线程互相等待,直到所有线程都到达屏障点后再同时继续执行。
- 特点:
- 计数器可重置,支持多次使用。
- 适用于分阶段协作(如多线程计算后聚合结果)。
- 示例:
CyclicBarrier barrier = new CyclicBarrier(3, () -> { // 所有线程到达后执行的回调 }); for (int i = 0; i < 3; i++) { new Thread(() -> { // 执行任务 barrier.await(); }).start(); }
3. Semaphore
- 功能:控制同时访问某个资源的线程数量。
- 特点:
- 通过许可证机制限制资源访问。
- 适用于资源池管理(如连接池、限流)。
- 示例:
Semaphore semaphore = new Semaphore(2); // 最多允许2个线程访问 semaphore.acquire(); try { // 访问资源 } finally { semaphore.release(); }
四、CompletableFuture 与同步工具类的对比
对比维度 | CompletableFuture | CountDownLatch | CyclicBarrier | Semaphore |
---|---|---|---|---|
用途 | 异步任务组合、依赖处理、结果聚合 | 等待一组线程完成 | 多线程同步屏障 | 控制资源访问数量 |
是否可重用 | ✅(支持多次使用) | ❌(一次性的) | ✅(可重置) | ✅(许可证可释放) |
是否阻塞主线程 | ❌(非阻塞) | ✅(通过 await() 阻塞) | ✅(通过 await() 阻塞) | ❌(通过 acquire() 阻塞) |
异常处理 | ✅(支持 .exceptionally() ) | ❌(需手动捕获异常) | ❌(需手动捕获异常) | ❌(需手动捕获异常) |
适用场景 | 高并发异步任务编排、微服务调用聚合 | 等待多线程初始化完成 | 分阶段协作任务 | 资源池、限流、互斥访问 |
五、典型应用场景
1. CompletableFuture
- 微服务调用聚合:同时调用多个远程服务,合并结果。
- 批量数据处理:并行处理多个独立任务,最后汇总结果。
- 异步任务链式依赖:A 任务完成后触发 B 任务,B 任务完成后触发 C 任务。
2. CountDownLatch
- 主线程等待所有子线程完成:如主线程需要收集所有子线程的计算结果。
- 初始化任务:等待多个初始化任务完成后启动主流程。
3. CyclicBarrier
- 并行计算分阶段:如 MapReduce 中的多阶段计算任务。
- 多线程协同:所有线程完成当前阶段后,统一进入下一阶段。
4. Semaphore
- 连接池管理:限制数据库连接数或 HTTP 客户端连接数。
- 限流:控制单位时间内访问某个资源的线程数量。
六、总结
工具 | 最佳使用场景 |
---|---|
CompletableFuture | 复杂异步任务编排、非阻塞式高并发处理 |
CountDownLatch | 一次性等待多线程完成 |
CyclicBarrier | 多线程分阶段协作、屏障同步 |
Semaphore | 资源访问控制、限流 |
建议:
- 如果需要处理异步任务的依赖关系或聚合结果,优先选择 CompletableFuture。
- 如果需要简单等待多线程完成,使用 CountDownLatch。
- 如果需要多线程分阶段同步,使用 CyclicBarrier。
- 如果需要控制资源访问数量,使用 Semaphore。
CommandLineRunner
接口功能详解
一、核心功能
CommandLineRunner
是 Spring Boot 提供的一个接口,用于在 Spring 应用上下文完全初始化后、应用正式提供服务前 执行特定的代码逻辑。其核心功能包括:
- 执行初始化任务
在应用启动时加载数据、初始化缓存、检查配置等。 - 接收命令行参数
通过String[] args
直接访问应用启动时传递的命令行参数。 - 支持多任务协作
可以定义多个CommandLineRunner
实现类,并通过@Order
注解控制执行顺序。
二、接口定义
@FunctionalInterface
public interface CommandLineRunner {
/**
* 在 SpringApplication 启动后回调
* @param args 来自应用程序的命令行参数
* @throws Exception 如果发生错误
*/
void run(String... args) throws Exception;
}
三、使用方法
1. 实现接口
创建一个类并实现 CommandLineRunner
接口,重写 run
方法。
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class MyStartupRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("应用启动完成,开始执行初始化操作...");
// 业务逻辑(如加载数据、初始化缓存等)
if (args.length > 0) {
System.out.println("接收到的命令行参数: " + String.join(", ", args));
}
}
}
2. 添加注解
- 使用
@Component
注解,Spring 会自动扫描并注册该 Bean。 - 如果需要控制执行顺序,使用
@Order(int value)
注解。
import org.springframework.core.annotation.Order;
@Component
@Order(1) // 执行顺序为1(数字越小优先级越高)
public class FirstRunner implements CommandLineRunner { ... }
@Component
@Order(2)
public class SecondRunner implements CommandLineRunner { ... }
四、典型应用场景
场景 | 说明 |
---|---|
数据初始化 | 在应用启动时加载初始数据到数据库或缓存中。例如:插入默认用户、角色或权限数据。 |
配置检查 | 验证配置文件或环境变量是否正确(如检查数据库连接、外部服务地址)。 |
缓存预热 | 在应用启动时预先加载热点数据到 Redis 或本地缓存中,避免首次请求性能下降。 |
后台任务启动 | 启动定时任务、消息监听器或异步任务。 |
命令行参数处理 | 根据启动参数执行特定逻辑(如 --init 参数触发初始化操作)。 |
五、示例代码
1. 基础示例
@Component
public class SimpleRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("应用启动后执行初始化逻辑");
if (args.length > 0 && "init".equals(args[0])) {
System.out.println("检测到 init 参数,执行初始化操作");
}
}
}
2. 带日志和参数解析
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class DataInitializer implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(DataInitializer.class);
@Override
public void run(String... args) throws Exception {
logger.info("开始初始化数据...");
if (args.length > 0 && "--force-init".equals(args[0])) {
// 执行强制初始化逻辑
logger.info("执行强制初始化操作");
}
logger.info("数据初始化完成");
}
}
六、注意事项
1. 执行时机
CommandLineRunner
的run
方法会在 Spring 上下文初始化完成后 被调用。- 此时,所有 Bean 已经完成初始化,可以安全地调用依赖的 Bean。
2. 异常处理
- 如果
run
方法抛出异常,Spring Boot 会终止应用启动流程(除非异常被捕获)。 - 建议在方法内部使用
try-catch
捕获异常并记录日志。
3. 命令行参数
- 参数通过
String[] args
传递,需手动解析(如--key=value
格式)。 - 示例:
启动命令:java -jar app.jar --init --env=prod
参数解析:for (String arg : args) { if (arg.startsWith("--env=")) { String env = arg.substring(6); System.out.println("环境参数: " + env); } }
4. 执行顺序
- 默认情况下,多个
CommandLineRunner
的执行顺序是 无序的。 - 使用
@Order
注解或实现Ordered
接口可控制顺序。
5. 关闭方法
CommandLineRunner
本身没有关闭逻辑,但可以通过以下方式控制行为:- 在
run
方法中添加条件判断(如检查环境参数)。 - 抛出异常终止应用启动(需谨慎使用)。
- 在
七、与 ApplicationRunner
的区别
特性 | CommandLineRunner | ApplicationRunner |
---|---|---|
参数类型 | String[] args (原始字符串数组) | ApplicationArguments (封装后的参数对象) |
适用场景 | 简单参数解析 | 复杂参数处理(支持解析 --key=value 格式) |
接口复杂度 | 简单 | 稍复杂 |
八、最佳实践
-
避免长时间阻塞主线程
如果初始化逻辑耗时较长,建议将任务提交到线程池异步执行。 -
依赖检查
确保run
方法中调用的 Bean 已正确注入(如数据库连接、外部服务)。 -
日志记录
在关键步骤添加日志,便于调试和监控。 -
参数校验
对命令行参数进行校验,避免因参数错误导致初始化失败。 -
幂等性设计
如果任务可能重复执行(如多次启动应用),确保初始化逻辑具有幂等性。
九、总结
功能 | 说明 |
---|---|
核心作用 | 应用启动后执行初始化任务 |
执行时机 | Spring 上下文初始化完成后 |
参数支持 | 直接接收命令行参数 |
适用场景 | 数据初始化、配置检查、缓存预热等 |
扩展性 | 支持多个 Runner 并控制执行顺序 |
通过合理使用 CommandLineRunner
,可以显著提升 Spring Boot 应用的灵活性和健壮性。
BitMap
Redis提供了四个常用命令处理二进制数组
- SETBIT:为数组指定偏移量上的二进制位设置值,偏移量从0开始计数,二进制位的值只能为0/1.(设置下标值)
- GETBIT:获取某个下标的值
- BITCOUNT:统计二进制为1的数量
- BITOP:对多个位数组进行运算
分布式缓存Redis
分布式解锁问题
RouYi @InnerAuth
网关清除请求头信息,防止伪造内部交互。
@Aspect
@Component
public class InnerAuthAspect implements Ordered
{
@Around("@annotation(innerAuth)")
public Object innerAround(ProceedingJoinPoint point, InnerAuth innerAuth) throws Throwable
{
String source = ServletUtils.getRequest().getHeader(SecurityConstants.FROM_SOURCE);
// 内部请求验证
if (!StringUtils.equals(SecurityConstants.INNER, source))
{
throw new InnerAuthException("没有内部访问权限,不允许访问");
}
String userid = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USER_ID);
String username = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USERNAME);
// 用户信息验证
if (innerAuth.isUser() && (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)))
{
throw new InnerAuthException("没有设置用户信息,不允许访问 ");
}
return point.proceed();
}
/**
* 确保在权限认证aop执行前执行
*/
@Override
public int getOrder()
{
return Ordered.HIGHEST_PRECEDENCE + 1;
}
}
RBAC权限模型
RouYi H5端会员登录