Spring(四) 关于AOP的源码解析与思考

Spring(四) 关于AOP的源码解析与思考

每种语言都有其独特的机制和特点,那么说到Java你可能会首先想到反射,反射是Java语言提供的一种能够在程序运行时动态操作类或对象的能力,比如获取某个对象的类定义、获取类声明的属性和方法、调用方法或构造对象、动态修改属性值等,这是因为JVM内存方法区中保存了加载的类元数据信息,并且每个对象实例的对象头中都包含指向类元数据的指针,这为Java提供了强大的灵活性和创造性,几乎所有Java技术和框架中都有反射的影子,通过反射获取Class类对象的方式包括 具体类.class对象实例.getClass()Class.forName()ClassLoader.loadClass()

除此之外,Java是面向对象的语言,即支持OOPObject Oriented Programming)面向对象编程,这是一种技术或思想,OOP的核心是抽象模型、反应客观事物的普遍规律和行为特征、模拟人类在现实世界中的思维认知,比如类与对象(整体与个体)、属性与方法(特征与行为)、对象实例间的通信(实体间的联系)等,其核心原则我们也耳熟能详即封装、继承、多态,可以看出OOP强调的是多样性、灵活性。与之相对的是AOPAspect Oriented Programming)面向切面编程,切面(Aspect)可以理解为关注点、在程序中要切入的方面,AOP的核心是将跨越多个业务或模型的横切关注点分离出来并进行模块化、模板化、流程化,这些关注点通常与核心业务逻辑无关,它要解决的是业务交叉、代码纠缠问题,比如日志、事务、安全、监控、审计等,AOP强调的是统一性、复用性。需要注意的是,OOPAOP代表了两种不同的代码组织和模块化方式,但OOPAOP不是对立关系而是互补关系,OOP通过对象封装和层次关系在纵向构建了应用程序的核心结构和业务逻辑(主要范式),而AOP则通过分离横切关注点在横向解决了业务交叉或跨越模型上的复杂性和代码纠缠问题。

上述内容可能比较抽象,但该部分已经把OOPAOP的核心观念与设计思想说得非常清楚,接下来我们将主要通过几个实例和实践来讲解下AOP思想/技术在Spring中的应用与实现。本篇纯干货!

1. 动态代理

首当其冲的就是动态代理,我们知道静态代理和动态代理都是代理模式的实现方式,该设计模式可以简单看作是对目标方法(关注点)做增强,它本质上也是AOP思想的实现。相比静态代理来说动态代理能在运行时动态生成代理类,这依赖于字节码生成技术,Spring常用的两种动态代理技术分别是JDK ProxyCGLIB,其中JDK Proxy是基于接口实现的,即其只能代理实现了接口的类或直接代理接口;而CGLIB则是基于ASM直接修改.class文件生成对应子类,因此其无需接口即可代理任意普通类,Spring AOP默认优先使用JDK代理,无接口时使用CGLIB代理。当然,在动态代理中也有反射的结合,代理方法中通常需要通过反射来调用目标方法,但动态代理和反射两种机制不可混淆。

2. AspectJ

AspectJJava生态系统中完全独立、完整且强大、功能丰富的专业AOP框架,AspectJ有着对AOP中切点、通知、切面等概念的完整实现与拓展,并支持编译时织入(在编译时将切面逻辑织入到目标类的字节码中)、编译后织入(在编译后织入到现有class类文件或jar文件中)、加载时织入(在Java类被类加载器加载到JVM时进行织入)三种织入方式,AspectJ可以拦截任何Java对象的执行(不受Spring容器限制),其可以拦截方法执行、构造器调用、字段读写、静态初始化、异常处理等几乎所有连接点。这里我们再看下AOP中的几个核心概念:

  • 连接点:程序执行过程中可以插入切面代码、实现增强的位置,在SpringAOP中通常为可调用方法的执行;
  • 切点:决定在哪些连接点应用通知(切点可以看作是连接点的子集),通常表现为能够匹配连接点的表达式(AspectJ 切点表达式);
  • 通知:在切点处执行插入增强的具体逻辑
    • 前置通知:目标方法执行前;
    • 后置通知:目标方法执行后(无论成功/失败都会执行);
    • 环绕通知:包围目标方法执行;
    • 返回通知:目标方法成功返回后;
    • 异常通知:目标方法抛出异常后;
  • 切面:切点+通知的封装组成切面,表示一个完整模块化、解耦化的横切关注点(比如日志、事务等)
  • 织入:将切面应用到目标对象的实现过程,比如Spring通过运行时动态代理实现切面;

3. AOP 实现

Spring AOP本身是一个简化版的、基于代理实现的AOP框架(更加轻量),其核心实现方式与完整的AspectJ框架不同,它是在运行时通过动态代理机制(JDK ProxyCGLIB)实现切面功能,因此Spring AOP仅支持作用于Spring IoC 容器管理的Bean,而无法拦截非Spring管理的对象或AOP Bean对自身方法的调用。虽然Spring AOPAspectJ在底层实现的本质上是不同的,但Spring AOP直接使用了AspectJ项目的注解风格(AspectJ定义的@Aspect@Pointcut@Before@After@Around等核心注解)和切点表达式语法,可以认为Spring AOP利用了AspectJ的方言和概念,提供了与AspectJ兼容的编程模型。

3.1 引入AOP依赖

首先需要在项目中添加Spring Boot Starter AOP依赖,该依赖包含了AspectJ中的核心注解以及切点表达式语法。

<!-- AOP -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>

3.2 定义AOP切面

3.2.1 业务实现
package com.example.aop.service;@Service
public class EverythingService {// 我们的业务逻辑public void processOrders(int type){if(type == 1){// 模拟抛出异常System.out.println("Processing orders error...");throw new RuntimeException("Error processing orders");}else{System.out.println("Processing orders success...");}}public void dealPayments(){System.out.println("Deal payments success...");}
}
3.2.2 切面实现
  • 定义切面:@Aspect+@Component
  • 定义切点:@Pointcut+切点表达式
  • 定义通知:@Before、@Around、@After、@AfterReturning、@AfterThrowing+通知增强逻辑
/*** 0.定义切面 Spring AOP*   - @Aspect: 声明切面以实现匹配和织入*   - @Component: 将切面对象加入到Spring管理*/
@Aspect
@Component
public class LogAop {/*** 1.定义切点: @Pointcut(切点表达式)* 1.1 匹配方法 execution(<访问修饰符-可选> <返回类型> <全限定包名.类名.⽅法(⽅法参数列表)> <异常-可选>)*   1.1.1 通配符*     - *: 匹配任意字符元素(返回类型、类名、方法名)*     - ..: 匹配任意子包或多级目录、任意数量参数*   1.1.2 例子*     - execution(* com.example.aop.service.*.*(..)) 匹配com.example.aop.service包下所有类的所有方法*     - execution(* com.example.aop.service..*.find*(..)) 匹配com.example.aop.service包及其所有子包下所有类中以find开头的所有方法* 1.2 匹配注解 @annotation(注解全限定名称)*   - 例子: @annotation(com.example.aop.annotation.Loggable) 匹配带有自定义@Loggable注解的方法*/@Pointcut("execution(* com.example.aop.service..*.process*(..))")public void logPointCut() {}/*** 2.定义通知-前置通知: @Before(切点/切点表达式)* @param joinPoint 切点对象(可选)*/@Before("logPointCut()")public void beforePointCut(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();System.out.println("【前置通知】" + methodName);}/*** 2.定义通知-后置通知: @After(切点/切点表达式)* @param joinPoint 切点对象(可选)*/@After("logPointCut()")public void afterPointCut(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();System.out.println("【后置通知】" + methodName);}/*** 2.定义通知-环绕通知: @Around(切点/切点表达式)* @param joinPoint 切点调用器(必须)* @return* @throws Throwable 异常*/@Around("logPointCut()")public Object aroundPointCut(ProceedingJoinPoint joinPoint) throws Throwable {String methodName = joinPoint.getSignature().getName();System.out.println("【环绕通知-前环绕】" + methodName);Object result = joinPoint.proceed(); //执行拦截器链上的后续目标(通知或目标方法)System.out.println("【环绕通知-后环绕】" + methodName);return result;}/*** 2.定义通知-返回通知: @AfterReturning(切点/切点表达式)* @param joinPoint 切点对象(可选)*/@AfterReturning("logPointCut()")public void afterReturningPointCut(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();System.out.println("【返回通知】" + methodName);}/*** 2.定义通知-异常通知: @AfterThrowing(切点/切点表达式)* @param joinPoint 切点对象(可选)*/@AfterThrowing("logPointCut()")public void afterThrowingPointCut(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();System.out.println("【异常通知】" + methodName);}}
3.2.3 切面织入
@SpringBootTest
class AopDemoApplicationTests {@ResourceEverythingService everythingService;@Testvoid contextLoads() {// 1.processOrders匹配切点表达式无异常everythingService.processOrders(0);System.out.println("==============================");try{// 2.processOrders匹配切点表达式有异常everythingService.processOrders(1);}catch (Exception e){}System.out.println("==============================");// 3.dealPayments不匹配切点表达式everythingService.dealPayments();}}

由下面的执行结果可以看出,声明的切面仅匹配到了processOrders()方法,并且不同类型通知之间具有特定执行顺序;需要注意的是在目标方法抛出异常时会由异常通知代替返回通知,但后置通知无论是否出现异常都会执行。

【环绕通知-前环绕】processOrders
【前置通知】processOrders
Processing orders success...
【返回通知】processOrders
【后置通知】processOrders
【环绕通知-后环绕】processOrders
==============================
【环绕通知-前环绕】processOrders
【前置通知】processOrders
Processing orders error...
【异常通知】processOrders
【后置通知】processOrders
==============================
Deal payments success...

4. 原理浅析

4.1 通知器执行顺序

Spring AOP是基于Spring动态代理(JDK ProxyCGLIB)动态生成目标代理对象,该代理对象的作用是拦截目标方法调用,并根据切面匹配的通知Advisor进行逻辑增强。在代理类中,首先会将匹配切点Pointcut的所有通知器Advisor转化为拦截器类型MethodInterceptor并构建拦截器链(包含所有通知的列表),在实际执行代理方法时会按照拦截器链中的顺序依次调用拦截执行。切点对应拦截器链的构造顺序如下:

  • 不同切面@Aspect按优先级排序:多个切面之间可以通过@Order注解来指定切面优先级(越小优先级越高),优先级越高则切面下的通知在拦截器链中就越靠前;
  • 相同切面下按通知类型排序:相同切面下的多种通知按照 Around->Before->After->AfterReturning->AfterThrowing 的类型顺序在拦截器链排序
  • 相同类型通知再按字符串升序排序:相同类型的按照通知的全限定名称(包名+类名+方法名)即字符串升序排序;
public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFactory implements Serializable {private static final Comparator<Method> adviceMethodComparator;static {// 类型排序器 Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.classComparator<Method> adviceKindComparator = new ConvertingComparator<>(new InstanceComparator<>(Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),(Converter<Method, Annotation>) method -> {AspectJAnnotation<?> ann = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);return (ann != null ? ann.getAnnotation() : null);});// 字符串排序器 Method::getName 升序Comparator<Method> methodNameComparator = new ConvertingComparator<>(Method::getName);// 先按类型排序,然后通知器类型相同再按方法名称字符串升序排序adviceMethodComparator = adviceKindComparator.thenComparing(methodNameComparator);}// ...
}

需要注意的是: 尽管后置通知@After在返回通知@AfterReturning和异常通知@AfterThrowing之前排序,但@After方法实际上会在@AfterReturning@AfterThrowing方法之后被调用(如上面切面织入时的输出效果),这是因为@After实际上只在相应的finally块中起作用,这里我们后面具体再看它的源码分析。

4.2 通知器执行原理

我们首先来看下基于JDK动态代理的Spring AOP切面织入源码JdkDynamicAopProxy.invoke()如下,可以看到在拦截器链不为空时会创建方法调用器ReflectiveMethodInvocation对象,来启动拦截器链上通知的调用执行:

// 基于JDK动态代理实现的Spring AOP,用于创建动态代理增强
final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {// 目标方法增强的织入过程public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// ...try {// ...target = targetSource.getTarget();Class<?> targetClass = (target != null ? target.getClass() : null);// 筛选出匹配当前方法的切面通知,并将Advisor转换成MethodInterceptor类型的拦截器链(已经按照上述规则排好序)List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);// 如果拦截器链为空直接反射执行目标方法if (chain.isEmpty()) {Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);}else {// 创建方法调用器,并传入需要执行的拦截器链 chainMethodInvocation invocation =new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);// 执行拦截器链,类似洋葱模型的链上递归调用retVal = invocation.proceed();}// ...}}// ...
}

我们这里再看下ReflectiveMethodInvocation的源码,重点是其proceed()方法是如何处理传入的拦截器链List<Object> interceptorsAndDynamicMethodMatchers的:

public class ReflectiveMethodInvocation implements ProxyMethodInvocation, Cloneable {// ...protected ReflectiveMethodInvocation(Object proxy, @Nullable Object target, Method method, @Nullable Object[] arguments,@Nullable Class<?> targetClass, List<Object> interceptorsAndDynamicMethodMatchers) {this.proxy = proxy;this.target = target;this.targetClass = targetClass;this.method = BridgeMethodResolver.findBridgedMethod(method);this.arguments = AopProxyUtils.adaptArgumentsIfNecessary(method, arguments);// 传入的拦截器链this.interceptorsAndDynamicMethodMatchers = interceptorsAndDynamicMethodMatchers;}public Object proceed() throws Throwable {// 1.拦截器链都执行完毕,执行最终目标方法if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {// 调用切点return invokeJoinpoint();}// 2.否则继续执行拦截器通知:获取列表中当前要执行的拦截器 ++this.currentInterceptorIndexObject interceptorOrInterceptionAdvice =this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {// 适用于动态切点匹配: 在方法执行时根据参数动态判断目标方法是否可以应用当前拦截器并执行InterceptorAndDynamicMethodMatcher dm =(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());// 匹配成功: 执行当前拦截器if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {return dm.interceptor.invoke(this);}else {// 匹配失败: 跳过当前拦截器继续proceed()判断下个return proceed();}}else {// 适用于静态切点匹配: 根据先前静态创建的拦截器链应用当前拦截器(默认已经匹配)return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);}}// ...
}

其总体逻辑就是从拦截器链列表的首个通知器开始执行(已经排好序),直到最后的通知器然后执行最终目标方法。需要注意的是,在执行每个拦截器具体通知方法.invoke(this)时,都会将方法调用器本身this作为参数传递过去,这点非常重要! 拦截器链正是通过方法调用器MethodInvocationproceed()方法作为触发锚点,使得拦截器链能够不断递归调用下去。接下来我们分别看下这五类通知器的代理拦截逻辑实现(类似于栈压入、弹出的方式)。

4.2.1 @Before前置通知 MethodBeforeAdviceInterceptor
public class MethodBeforeAdviceInterceptor implements MethodInterceptor, BeforeAdvice, Serializable {private final MethodBeforeAdvice advice;// MethodInvocation 正是传递的方法调用器public Object invoke(MethodInvocation mi) throws Throwable {// 执行前置通知方法this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());// 调用锚点-方法调用器的proceed()方法执行下个拦截器通知return mi.proceed();}
}
4.2.2 @After后置通知 AspectJAfterAdvice

@After后置通知中,是优先执行下个拦截器通知,最后在finally块中才执行后置通知逻辑。因此这也是为什么后置通知@After在返回通知@AfterReturning和异常通知@AfterThrowing之前排序,但@After方法实际上会在@AfterReturning@AfterThrowing方法之后被调用的原因。

public class AspectJAfterAdvice extends AbstractAspectJAdviceimplements MethodInterceptor, AfterAdvice, Serializable {public Object invoke(MethodInvocation mi) throws Throwable {try {// 调用锚点-方法调用器的proceed()方法执行下个拦截器通知return mi.proceed();}finally {// 执行后置通知方法invokeAdviceMethod(getJoinPointMatch(), null, null);}}
}
4.2.3 @Around环绕通知 AspectJAroundAdvice

@Around环绕通知的拦截器逻辑比较特殊,其在invoke方法中是没有主动调用MethodInvocationproceed()方法来触发下个拦截器的,因此需要我们在环绕通知的实现中手动决定方法调用器的触发,才能使得拦截器链继续执行下去,这也是环绕的概念或者说洋葱模型的由来。

public class AspectJAroundAdvice extends AbstractAspectJAdvice implements MethodInterceptor, Serializable {public Object invoke(MethodInvocation mi) throws Throwable {if (!(mi instanceof ProxyMethodInvocation)) {throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);}ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);JoinPointMatch jpm = getJoinPointMatch(pmi);// 执行环绕通知方法return invokeAdviceMethod(pjp, jpm, null, null);}}
4.2.4 @AfterReturning返回通知 AfterReturningAdviceInterceptor
public class AfterReturningAdviceInterceptor implements MethodInterceptor, AfterAdvice, Serializable {private final AfterReturningAdvice advice;public Object invoke(MethodInvocation mi) throws Throwable {// 调用proceed()方法执行下个拦截器通知,并暂存结果Object retVal = mi.proceed();// 执行返回通知方法: 前面没有异常时this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());// 返回结果给上层return retVal;}
}
4.2.5 @AfterThrowing异常通知 AspectJAfterThrowingAdvice
public class AspectJAfterThrowingAdvice extends AbstractAspectJAdviceimplements MethodInterceptor, AfterAdvice, Serializable {public Object invoke(MethodInvocation mi) throws Throwable {try {// 调用proceed()方法执行下个拦截器通知return mi.proceed();}catch (Throwable ex) {// 执行异常通知方法: 前面出现异常时if (shouldInvokeOnThrowing(ex)) {invokeAdviceMethod(getJoinPointMatch(), null, ex);}// 继续抛出异常throw ex;}}}

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

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

相关文章

Android 15 Settings 搜索框:引入关键字过滤功能

在日常使用 Android 手机时,我们经常会用到“设置”应用中的搜索功能来快速定位所需选项。然而,有时搜索结果可能会包含一些我们不希望看到或者过于宽泛的条目。 本文将深入探讨这一变化,通过分析 SearchResultsAdapter.java 文件中的代码修改,揭示 Android 如何实现对特定…

Python-魔术方法-创建、初始化与销毁-hash-bool-可视化-运算符重载-容器和大小-可调用对象-上下文管理-反射-描述器-二分-学习笔记

序 欠4前年的一份笔记 &#xff0c;献给今后的自己。 魔术方法 特殊属性查看属性如果dir&#xff08;lobji&#xff09;参数obj包含方法 __dir__()&#xff0c;该方法将被调用。如果参数obj不包含__dir__()&#xff0c; 该方法将最大限度地收集参数信息。 dir()对于不同类型的对…

redis的一些疑问

spring集成redisCacheEvict(value "commonCache", key "#uniqueid_userInfo")什么时候会执行缓存移除呢&#xff1f;如果方法执行异常是否移除&#xff1f;如果缓存不存在还会移除么&#xff1f;这个移除会在redis的执行历史命令中监控到么&#xff1f;.…

3.检查函数 if (!CheckStart()) return 的妙用 C#例子

在桌面/WPF 开发中&#xff0c;我们经常需要在按钮事件里先判断“能不能做”&#xff0c;再决定“怎么做”。如果校验不过&#xff0c;就直接返回&#xff1b;校验通过&#xff0c;才继续执行业务逻辑。 今天分享一个极简写法&#xff1a;if (!CheckStart()) return;&#xff0…

炎热工厂救援:算法打造安全壁垒

高温天气下智慧工厂&#xff1a;算法赋能&#xff0c;安全救援无忧背景&#xff1a;极端高温下工厂的严峻挑战近年来&#xff0c;极端高温天气频发&#xff0c;部分地区气温接近甚至超过50℃。在这样酷热的环境中&#xff0c;工厂面临着诸多严峻问题。一方面&#xff0c;高温容…

pgsql模板是什么?

查找所有的数据库 select datname from pg_database运行该命令后&#xff0c;我们会发现其中出现了一些其它的数据库接下来&#xff0c;我们分析 template0 和 template1 的作用。template1 template1 是 PostgreSQL 默认用于创建新数据库的模板。当执行 CREATE DATABASE new_d…

LLM 不知道答案,但是知道去调用工具获取答案?

思考&#xff1a; LLM 自己“不知道”某个事实性问题的答案&#xff0c;但仍然能“知道”去调用工具获取正确答案&#xff0c;这听起来确实有点像个悖论该内容触及了大型语言模型&#xff08;LLM&#xff09;的核心局限性以及&#xff08;Agents&#xff09;的智能所在。实际上…

2025年7月11日学习笔记一周归纳——模式识别与机器学习

2025年7月11日学习笔记&一周归纳——模式识别与机器学习一.一周工作二.我的一些笔记汇总三.发现的一些新的学习资料和爱用好物1.百度网盘AI笔记&#xff1a;2.b站资料&#xff1a;3.听说的一些好书&#xff1a;一.一周工作 本周学习了清华大学张学工汪小我老师的模式识别与…

LeetCode 138题解 | 随机链表的复制

随机链表的复制一、题目链接二、题目三、分析四、代码一、题目链接 138.随机链表的复制 二、题目 三、分析 数据结构初阶阶段&#xff0c;为了控制随机指针&#xff0c;我们将拷贝结点链接在原节点的后面解决&#xff0c;后面拷贝节点还得解下来链接&#xff0c;非常麻烦。这…

【计算机存储架构】分布式存储架构

引言&#xff1a;数据洪流时代的存储革命“数据是新时代的石油” —— 但传统存储正成为制约数据价值释放的瓶颈核心矛盾&#xff1a;全球数据量爆炸增长&#xff1a;IDC预测2025年全球数据量将达175ZB&#xff08;1ZB10亿TB&#xff09;传统存储瓶颈&#xff1a;单机IOPS上限仅…

【Linux-云原生-笔记】数据库操作基础

一、什么是数据库&#xff1f;数据库就是一个有组织、可高效访问、管理和更新的电子化信息&#xff08;数据&#xff09;集合库。简单来说&#xff0c;数据库就是一个高级的Excel二、安装数据库并初始化1、安装数据库&#xff08;MySQL&#xff09;dnf search一下mysql数据库的…

HarmonyOS中各种动画的使用介绍

鸿蒙&#xff08;HarmonyOS&#xff09;提供了丰富的动画能力&#xff0c;涵盖属性动画、显式动画、转场动画、帧动画等多种类型&#xff0c;适用于不同场景的交互需求。以下是鸿蒙中各类动画的详细解析及使用示例&#xff1a;1. 属性动画&#xff08;Property Animation&#…

CSP-S 模拟赛 10

T1 洛谷 U490727 返乡 思路 首先要意识到一个问题&#xff0c;就是如果所有人总分一定&#xff0c;那么是不会出现偏序的。 可以感性理解一下&#xff0c;就是对于 i,ji, ji,j&#xff0c; 若 ai≤aj,bi≤bja_i \leq a_j, b_i \leq b_jai​≤aj​,bi​≤bj​&#xff0c;那么…

CMD,PowerShell、Linux/MAC设置环境变量

以下是 CMD&#xff08;Windows&#xff09;、PowerShell&#xff08;Windows&#xff09;、Linux/Mac 在 临时/永久 环境变量操作上的对比表格&#xff1a;环境变量操作对照表&#xff08;CMD vs PowerShell vs Linux/Mac&#xff09;操作CMD&#xff08;Windows&#xff09;P…

MySQL(131)如何解决MySQL CPU使用率过高问题?

解决MySQL CPU使用率过高的问题需要从多个方面进行排查和优化&#xff0c;包括查询优化、索引优化、配置优化和硬件资源的合理使用等。以下是详细的解决方案和相应的代码示例。 一、查询优化 1. 检查慢查询 使用MySQL的慢查询日志来找到执行时间长的查询。 SET GLOBAL slow_que…

docker基础与常用命令

目录 一.docker概述 1.docker与虚拟机区别 2.Linux 六大命名空间 3.Docker 的核心技术及概念 二.docker部署安装 三.docker常用命令 1.搜索镜像 2.获取镜像 3.查看镜像信息 4.添加镜像标签 5.删除镜像 6.存出与载入镜像 7.上传镜像 8.创建容器 9.查看容器状态 1…

Cypress与多语言后端集成指南

Cypress 简介 基于 JavaScript 的前端测试工具,可以对浏览器中运行的任何内容进行快速、简单、可靠的测试Cypress 是自集成的,提供了一套完整的端到端测试,无须借助其他外部工具,安装后即可快速地创建、编写、运行测试用例,且对每一步操作都支持回看不同于其他只能测试 UI…

计算机毕业设计ssm基于JavaScript的餐厅点餐系统 SSM+Vue智慧餐厅在线点餐管理平台 JavaWeb前后端分离式餐饮点餐与桌台调度系统

计算机毕业设计ssm基于JavaScript的餐厅点餐系统0xig8788&#xff08;配套有源码 程序 mysql数据库 论文&#xff09; 本套源码可以在文本联xi,先看具体系统功能演示视频领取&#xff0c;可分享源码参考。扫码点单、手机支付、后厨实时出票已经成为食客对餐厅的基本预期。传统的…

wedo稻草人-----第32节(免费分享图纸)

夸克网盘&#xff1a;https://pan.quark.cn/s/ce4943156861 高清图纸源文件&#xff0c;需要的请自取

Jmeter函数的使用

函数名作用用法${__Random(,,)}${__RandomString(,,)}随机生成一些东西${__Random(000,999,)} ${__Random(${test1},${test2},)}${__RandomString(${__Random(3,9,)},asdfghjkl,)}${__time(,)}获取当前的时间戳&#xff0c;也可以定义格式${__CSVRead(,)}读取CSV文件的格式&…