Sa-Token

简介

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。
官方文档

常见功能

请添加图片描述

登录认证

本框架

  1. 用户提交 name + password 参数,调用登录接口。
  2. 登录成功,返回这个用户的 Token 会话凭证
  3. 用户后续的每次请求,都携带上这个 Token。
  4. 服务器根据 Token 判断此会话是否登录成功。
    请添加图片描述
测试
/*** 登录测试 */
@RestController
@RequestMapping("/acc/")
public class LoginController {// 测试登录  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456@RequestMapping("doLogin")public SaResult doLogin(String name, String pwd) {// 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) { 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等。Sa-Token 为这个账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端。StpUtil.login(10001);return SaResult.ok("登录成功");}return SaResult.error("登录失败");}// 查询登录状态  ---- http://localhost:8081/acc/isLogin@RequestMapping("isLogin")public SaResult isLogin() {return SaResult.ok("是否登录:" + StpUtil.isLogin());}// 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo@RequestMapping("tokenInfo")public SaResult tokenInfo() {return SaResult.data(StpUtil.getTokenInfo());}// 测试注销  ---- http://localhost:8081/acc/logout@RequestMapping("logout")public SaResult logout() {StpUtil.logout();return SaResult.ok();}}

Session

基于 Session 的认证是一种服务器端维护用户会话状态的机制,旨在解决 HTTP 协议无状态的问题

  1. 用户登录:客户端提交用户名和密码至服务器,服务器验证身份后生成唯一的 Session ID,并将用户信息(如角色、权限等)存储在服务器的Session 对象中
  2. Session 传递:服务器通过响应头的 Set-Cookie 字段将 Session ID返回给客户端,客户端浏览器自动保存此 Cookie
  3. 会话验证:后续请求中,客户端自动携带该 Cookie(包含 SessionID),服务器通过 Session ID 查找对应的 Session 对象,验证用户身份
  4. 会话管理:若 Session请添加图片描述
    超时或用户主动登出,服务器销毁 Session 对象,客户端 Cookie 失效

JWT

JWT(JSON Web Token) 是一种开放标准(RFC 7519),用于在网络应用间安全传输 JSON 格式的信息。其核心设计为 紧凑性(体积小)和 自包含性(信息完整无需额外存储),由三部分组成Header 、Payload、Signature

组成
  1. Header(头部)
    包含令牌类型(typ)和签名算法(如 HS256 或 RSA)
{ "alg": "HS256", "typ": "JWT" }

2.Payload(载荷)
存储用户身份信息及其他声明(Claims),分为三类:

  • 注册声明(标准字段):如 sub(用户标识)、exp(过期时间)、iat(签发时间)等。
  • 公共声明(自定义但建议标准化):如用户姓名、角色等。
  • 私有声明(业务自定义):如用户偏好设置。

注意:Payload 内容虽可验证但非加密,避免存储敏感信息(如密码)

  • Signature(签名)
    使用密钥对 Header 和 Payload 的编码结果进行签名,确保数据完整性和真实性。签名是 JWT 安全的核心,密钥泄露将导致伪造风险
流程

请添加图片描述

  1. 用户登录:
    用户提交凭证(如用户名/密码),服务器验证成功后生成 JWT,包含用户身份信息和有效期
  2. Token 下发:服务器将 JWT 返回客户端,客户端需存储于 Cookie、LocalStorage 或 SessionStorage 中。推荐通过Authorization 请求头传递
  3. Token 验证:服务器接收请求后: 解码并验证签名:使用密钥验证数据是否被篡改。 检查有效期:如 exp 字段是否过期。 提取用户信息:直接从
    Payload 中获取用户身份,无需查询数据库
实战

JWT工具类:生成和解析JWT

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;public class JwtUtil {/*** 生成jwt* 使用Hs256算法, 私匙使用固定秘钥** @param secretKey jwt秘钥* @param ttlMillis jwt过期时间(毫秒)* @param claims    设置的信息* @return*/public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {// 指定签名的时候使用的签名算法,也就是header那部分SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;// 生成JWT的时间long expMillis = System.currentTimeMillis() + ttlMillis;Date exp = new Date(expMillis);// 设置jwt的bodyJwtBuilder builder = Jwts.builder()// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的.setClaims(claims)// 设置签名使用的签名算法和签名使用的秘钥.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))// 设置过期时间.setExpiration(exp);return builder.compact();}/*** Token解密** @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个* @param token     加密后的token* @return*/public static Claims parseJWT(String secretKey, String token) {// 得到DefaultJwtParserClaims claims = Jwts.parser()// 设置签名的秘钥.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))// 设置需要解析的jwt.parseClaimsJws(token).getBody();return claims;}}

定义拦截器

import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/*** jwt令牌校验的拦截器*/
@Component //生成Bean
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {@Autowiredprivate JwtProperties jwtProperties;/*** 校验jwt** @param request* @param response* @param handler* @return* @throws Exception*/public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//判断当前拦截到的是Controller的方法还是其他资源if (!(handler instanceof HandlerMethod)) {//当前拦截到的不是动态方法,直接放行return true;}//1、从请求头中获取令牌String token = request.getHeader(jwtProperties.getAdminTokenName());//2、校验令牌try {log.info("jwt校验:{}", token);Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());log.info("当前员工id:{}", empId);//前端发一次请求,是一次线程(拦截器,controller,service,mapper共有同一线程) (threadLocal)BaseContext.setCurrentId(empId);//3、通过,放行return true;} catch (Exception ex) {//4、不通过,响应401状态码response.setStatus(401);return false;}}
}

注册拦截器

/*** 配置类,注册web层相关组件*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {@Autowiredprivate JwtTokenAdminInterceptor jwtTokenAdminInterceptor;@Autowiredprivate JwtTokenUserInterceptor jwtTokenUserInterceptor;/*** 注册自定义拦截器  配置路径** @param registry*/protected void addInterceptors(InterceptorRegistry registry) {log.info("开始注册自定义拦截器...");registry.addInterceptor(jwtTokenAdminInterceptor)//在以下路径判断.addPathPatterns("/admin/**")//排除以下路径.excludePathPatterns("/admin/employee/login");}/*** 设置静态资源映射* @param registry*/protected void addResourceHandlers(ResourceHandlerRegistry registry) {log.info("开始设置静态资源映射...");registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");}//扩展springmvc框架的消息转化器@Overrideprotected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {log.info("扩展消息转化器...");//创建一个消息转化器对象MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();//需要为消息转化器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据converter.setObjectMapper(new JacksonObjectMapper());//将自己的消息转化器加入容器中converters.add(0,converter);}
}

权限认证

自定义注解+AOP实现

实战(判断管理员)

自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthCheck {/*** 必须有某个角色** @return*/String mustRole() default "";}

编写切面类

@Aspect
@Component
public class AuthInterceptor {@Resourceprivate UserService userService;/*** 执行拦截** @param joinPoint* @param authCheck* @return*/@Around("@annotation(authCheck)")public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {String mustRole = authCheck.mustRole();RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();// 当前登录用户User loginUser = userService.getLoginUser(request);UserRoleEnum mustRoleEnum = UserRoleEnum.getEnumByValue(mustRole);// 不需要权限,放行if (mustRoleEnum == null) {return joinPoint.proceed();}// 必须有该权限才通过UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(loginUser.getUserRole());if (userRoleEnum == null) {throw new BusinessException(ErrorCode.NO_AUTH_ERROR);}// 如果被封号,直接拒绝if (UserRoleEnum.BAN.equals(userRoleEnum)) {throw new BusinessException(ErrorCode.NO_AUTH_ERROR);}// 必须有管理员权限if (UserRoleEnum.ADMIN.equals(mustRoleEnum)) {// 用户没有管理员权限,拒绝if (!UserRoleEnum.ADMIN.equals(userRoleEnum)) {throw new BusinessException(ErrorCode.NO_AUTH_ERROR);}}// 通过权限校验,放行return joinPoint.proceed();}
}

补充AOP
切面(Aspect):封装横切逻辑的模块(如日志切面类)
连接点(Join Point):程序执行过程中的特定点(如方法调用、异常抛出)
通知(Advice):切面在连接点执行的操作,分为:

  • 前置通知(Before):方法执行前触发(如权限校验)。
  • 后置通知(After):方法执行后触发(无论是否异常)。
  • 返回通知(AfterReturning):方法正常返回后触发。
  • 异常通知(AfterThrowing):方法抛出异常后触发。
  • 环绕通知(Around):包裹目标方法,控制执行流程(如事务管理)。在连接点执行目标方法调用前后可以自己编写逻辑。
@Aspect
@Component
public class LoggingAspect {@Around("execution(* com.example.service.*.*(..))")public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {// 前置逻辑(如参数校验)Object result = pjp.proceed(); // 调用目标方法// 后置逻辑(如日志记录)return result; // 可修改返回值}
}

切点(Pointcut):定义哪些连接点会被切面拦截(通过表达式指定),在本业务中只有有自定义注解并是admin,AOP就会拦截。

利用Sa-token来进行权限校验

链接

实战

1.引入依赖

<!-- Sa-Token 权限认证 -->
<dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-boot-starter</artifactId><version>1.39.0</version>
</dependency>

2.编写配置文件

# Sa-Token 配置
sa-token:# token 名称(同时也是 cookie 名称)token-name: mianshiya# token 有效期(单位:秒) 默认30天,-1 代表永久有效timeout: 2592000# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结active-timeout: -1# 是否允许同一账号多地同时登录(为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)is-concurrent: false# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)is-share: true# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)token-style: uuid# 是否输出操作日志 is-log: true

3.注册Sa-token拦截器

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {// 注册 Sa-Token 拦截器,打开注解式鉴权功能 @Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册 Sa-Token 拦截器,打开注解式鉴权功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");    }
}

4.定义权限与角色获取逻辑
实现 StpInterface 接口。该接口提供了获取当前登录用户的权限和角色的方法,在每次调用鉴权代码时,都会执行接口中的方法。

@Component // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展 
public class StpInterfaceImpl implements StpInterface {/*** 返回一个账号所拥有的权限码集合 (目前没用)*/@Overridepublic List<String> getPermissionList(Object loginId, String s) {return new ArrayList<>();}/*** 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)*/@Overridepublic List<String> getRoleList(Object loginId, String s) {// 从当前登录用户信息中获取角色User user = (User) StpUtil.getSessionByLoginId(loginId).get(USER_LOGIN_STATE);return Collections.singletonList(user.getUserRole());}
}

5.设备信息获取工具类

/*** 设备工具类*/
public class DeviceUtils {/*** 根据请求获取设备信息**/public static String getRequestDevice(HttpServletRequest request) {String userAgentStr = request.getHeader(Header.USER_AGENT.toString());// 使用 Hutool 解析 UserAgentUserAgent userAgent = UserAgentUtil.parse(userAgentStr);ThrowUtils.throwIf(userAgent == null, ErrorCode.OPERATION_ERROR, "非法请求");// 默认值是 PCString device = "pc";// 是否为小程序if (isMiniProgram(userAgentStr)) {device = "miniProgram";} else if (isPad(userAgentStr)) {// 是否为 Paddevice = "pad";} else if (userAgent.isMobile()) {// 是否为手机device = "mobile";}return device;}/*** 判断是否是小程序* 一般通过 User-Agent 字符串中的 "MicroMessenger" 来判断是否是微信小程序**/private static boolean isMiniProgram(String userAgentStr) {// 判断 User-Agent 是否包含 "MicroMessenger" 表示是微信环境return StrUtil.containsIgnoreCase(userAgentStr, "MicroMessenger")&& StrUtil.containsIgnoreCase(userAgentStr, "MiniProgram");}/*** 判断是否为平板设备* 支持 iOS(如 iPad)和 Android 平板的检测**/private static boolean isPad(String userAgentStr) {// 检查 iPad 的 User-Agent 标志boolean isIpad = StrUtil.containsIgnoreCase(userAgentStr, "iPad");// 检查 Android 平板(包含 "Android" 且不包含 "Mobile")boolean isAndroidTablet = StrUtil.containsIgnoreCase(userAgentStr, "Android")&& !StrUtil.containsIgnoreCase(userAgentStr, "Mobile");// 如果是 iPad 或 Android 平板,则返回 truereturn isIpad || isAndroidTablet;}
}

6.编写方法


// Sa-Token 登录,并指定设备,同端登录互斥
StpUtil.login(user.getId(), DeviceUtils.getRequestDevice(request));
StpUtil.getSession().set(USER_LOGIN_STATE, user);
@Override
public User getLoginUser(HttpServletRequest request) {// 先判断是否已登录Object loginUserId = StpUtil.getLoginIdDefaultNull();if (loginUserId == null) {throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);}// 从数据库查询(追求性能的话可以注释,直接走缓存)User currentUser = this.getById((String) loginUserId);if (currentUser == null) {throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);}return currentUser;
}

7.在方法上添加注解

@SaCheckRole(UserConstant.ADMIN_ROLE)

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

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

相关文章

ADZS-ICE-2000和AD-ICE2000仿真器在线升级固件

作者的话 近期发现有些兄弟的ICE-2000仿真器链接DSP报错&#xff0c;然后test第四步不通过&#xff0c;我就拿我的仿真器也试了一下&#xff0c;发现ADI悄咪咪的在线升级仿真器固件&#xff0c;有些兄弟不会操作&#xff0c;就会导致仿真器升级失败&#xff0c;连不上目标板&a…

C++概述

1 什么是面向对象】 概念上来说&#xff1a;就是以对象(具体的变量)为导向的编程思路 专注于&#xff1a;一个对象具体能实现哪些过程(哪些功能) 面向对象 n * 面向过程 结论&#xff1a;面向对象需要做的事情 1&#xff1a;我们要想清楚&#xff0c;我们现在需要编写一个…

Java 大视界 -- 基于 Java 的大数据隐私计算在医疗影像数据共享中的实践探索(158)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

数字化如何赋能食品抽检全流程升级,助力食品安全监管现代化

食品安全是关乎民众健康和社会稳定的重要问题。食品抽检作为保障食品安全的核心监管手段&#xff0c;通过对食品生产、加工、销售等环节的随机抽样检测&#xff0c;及时发现潜在的食品安全问题&#xff0c;防止不合格产品流入市场&#xff0c;同时为政府监管、企业自查和消费者…

HBase入门教程

HBase入门教程 HBase是一个开源的、分布式的、版本化的非关系型数据库&#xff0c;是Apache Hadoop生态系统的重要组成部分。本文将全面介绍HBase的基础知识&#xff0c;帮助你快速入门。 文章目录 HBase入门教程1. HBase简介1.1 什么是HBase&#xff1f;1.2 HBase核心特点 2.…

vscode连接服务器失败问题解决

文章目录 问题描述原因分析解决方法彻底删除VS Code重新安装较老的版本 问题描述 vscode链接服务器时提示了下面问题&#xff1a; 原因分析 这是说明VScode版本太高了。 https://code.visualstudio.com/docs/remote/faq#_can-i-run-vs-code-server-on-older-linux-distribu…

redis常用部署架构之redis分片集群。

redis 3.x版本后开始支持 作用&#xff1a; 1.提升数据读写速度 2..提升可用性 分片集群就是将业务服务器产生的数据储存在不同的机器上。 redis分片集群的架构 如上图所示&#xff0c;会将数据分散存储到不同的服务器上&#xff0c;相比于之前来说&#xff0c;redis要处…

Modbus主站EtherNet/IP转ModbusRTU/ASCII工业EIP网关串口服务器

型号 2路总线EIP网关 MS-A1-2021 4路总线EIP网关 MS-A1-2041 4路总线EIP网关&#xff08;双网口&#xff09; MS-A2-2041 8路总线EIP网关 MS-A1-2081 8路总线EIP网关&#xff08;双网口&#xff09; MS-A2-2081 EtherNet/IP 串口网关 EtherNet/IP 转 RS485 …

Centos7 安装 TDengine

Centos7 安装 TDengine 1、简介 官网&#xff1a; https://www.taosdata.com TDengine 是一款开源、高性能、云原生的时序数据库&#xff08;Time Series Database, TSDB&#xff09;, 它专为物联网、车联网、工业互联网、金融、IT 运维等场景优化设计。同时它还带有内建的缓…

基于社交裂变的S2B2C电商模式创新研究——以“颜值PK+礼品卡+AI智能名片“融合生态为例

摘要 本文构建了融合开源AI技术、社交裂变机制与S2B2C商业模式的创新模型。通过开发具备AI智能名片功能的商城小程序&#xff0c;实现用户日均停留时长提升171%、社交转化效率提高2.8倍的实证效果。研究发现&#xff1a;基于GAN的虚拟形象生成技术可降低用户决策成本32%&…

王者荣耀服务器突然崩了

就在刚刚王者荣耀服务器突然崩了 #王者荣耀崩了#的话题毫无预兆地冲上热搜&#xff0c;许多玩家发现游戏登录界面反复弹出异常提示&#xff0c;匹配成功后卡在加载界面&#xff0c;甚至出现对局数据丢失的情况。根据官方公告&#xff0c;目前技术团队已在全力抢修服务器 #王者…

LabVIEW医疗设备备用电源实时监控系统

开发了一个基于LabVIEW的医疗设备备用电源实时监控系统。系统提高医疗设备备用电源的管理效能与使用安全&#xff0c;通过实时监测与数据分析&#xff0c;确保医疗设施在电力供应中断时的可靠运行。 ​ 项目背景 医院中的医疗设备对电源的连续供应有着极高的要求&#xff0c;…

04-SpringBoot3入门-配置文件(多环境配置)

1、简介 在 SpringBoot 中&#xff0c;不同的环境&#xff08;如开发、测试、生产&#xff09;可以编写对应的配置文件&#xff0c;例如数据库连接信息、日志级别、缓存配置等。在不同的环境中使用对应的配置文件。 2、配置环境 # 开发环境 zbj:user:username: root # 测试环…

C++链表详解:从基础概念到高级应用

C++链表详解:从基础概念到高级应用 链表是计算机科学中最基础也是最重要的数据结构之一,它在内存管理、算法实现和实际应用中扮演着关键角色。本文将详细介绍链表的概念、类型、C++实现以及实际应用场景,帮助读者全面理解这一重要的数据结构。 文章目录 C++链表详解:从基础…

了解图像质量评价指标PSNR

一、PSNR是什么 1.1 定义与数学公式 峰值信噪比&#xff08;Peak Signal-to-Noise Ratio&#xff0c;PSNR&#xff09;是数字图像处理领域最经典的客观质量评价指标之一。其核心思想是通过计算原始图像与失真图像之间的均方误差&#xff08;MSE&#xff09;来衡量失真程度&am…

NX二次开发刻字功能——布尔运算

刻字功能在经历、创建文本、拉伸功能以后就剩下布尔运算了。布尔运算的目的就是实现文本时凸还是凹。这部分内容很简单。 1、首先识别布尔运算的类型&#xff0c;我这里用到一个枚举类型的选项&#xff0c;凸就是布尔求和&#xff0c;凹就是布尔求差。 2、其放置位置为创建拉伸…

《C语言实现金字塔图案打印》

&#x1f680;个人主页&#xff1a;BabyZZの秘密日记 &#x1f4d6;收入专栏&#xff1a;C语言练习题分享 &#x1f30d;文章目入 程序代码程序功能程序分析外层循环内层循环输出结果 示例运行总结 在学习编程的过程中&#xff0c;打印图案是一个非常有趣的练习&#xff0c;它可…

Shiro学习(一):Shiro介绍和基本使用

一、Shiro介绍 1、百科对shiro的定义如下&#xff1a; Apache Shiro 一个强大且易于使用的 Java 安全框架&#xff0c;它提供了身份验证、授权、加密和会话管理等功能。Shiro 的设计目标是简化企业级应用程序的安全性开发过程&#xff0c;同时保持代码的简洁和易于维护。 2、…

Java多线程与高并发专题——关于Condition

Condition接口 源码注释 还是老样子&#xff0c;看看源码注释&#xff1a; Condition factors out the Object monitor methods (wait, notify and notifyAll) into distinct objects to give the effect of having multiple wait-sets per object, by combining them with t…

JavaScript 性能优化实战:突破瓶颈,打造极致 Web 体验

在当今快节奏的互联网时代&#xff0c;用户对于 Web 应用的性能要求越来越高。一个响应迅速、流畅运行的 Web 页面能够极大地提升用户体验&#xff0c;反之&#xff0c;缓慢的加载速度和卡顿的交互则可能导致用户流失。JavaScript 作为 Web 开发的核心语言之一&#xff0c;其性…