详解Spring AOP(二)

news2024/12/23 14:54:56

目录

1.切点表达式

1.1execution表达式

1.2 @annotation

1.2.1自定义注解@MyAspect

1.2.3添加自定义注解

2.Sping AOP原理

2.1代理模式

2.1.1静态代理

2.1.2动态代理

2.1.3JDK动态代理

2.1.4CGLIB动态代理

3.总结


承接上文:详解Spring AOP(一)

1.切点表达式

之前的代码中,我们一直在使用切点表达式来描述切点.下面我们来介绍一下切点表达式的语法.切点表达式常见有两种表达方式:
execution(.....):根据方法的签名来匹配
@annotation(...):根据注解匹配

1.1execution表达式

execution() 是最常用的切点表达式, 用来匹配⽅法, 语法为:

execution(<访问修饰符> <返回类型> <包名 .类名 .方法(方法参数)> <异常>)

其中访问修饰符和异常可以省略

 切点表达式支持通配符表达:
*:匹配任意字符,只匹配一个元素(返回类型,包,类名,方法或者方法参数)
包名使用*表示任意包(一层包使用一个*)
类名使用*表示任意类
返回值使用*表示任意返回值类型
方法名使用*表示任意方法
参数使用*表示一个任意类型的参数
 .. :匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数
使用..配置包名,标识此包以及此包下的所有子包
可以使用..配置参数,任意个任意类型的参数

切点表达式示例:
TestController 下的 public修饰, 返回类型为String 方法名为t1, 无参方法

execution(public String com.example.demo.controller.TestController.t1())

省略访问修饰符

execution(String com.example.demo.controller.TestController.t1())

匹配所有返回类型

execution(* com.example.demo.controller.TestController.t1())

匹配TestController 下的所有无参方法

execution(* com.example.demo.controller.TestController.*())

匹配controller包下所有的类的所有方法

execution(* com.example.demo.controller.*.*(..))

匹配所有包下面的TestController

execution(* com..TestController.*(..))

匹配com.example.demo包下, 子孙包下的所有类的所有方法

execution(* com.example.demo..*(..))

1.2 @annotation

execution表达式更适用有规则的,如果我们要匹配多个无规则的方法,比如:TestController中的t1()和UserController中的u1()这两个方法.
这个时候我们使用execution这种切点表达式来描述就不是很方便了.
我们可以借助自定义注解的方式以及切点表达式@annotation来描述这一类的切点实现步骤:
1.编写自定义注解
2.使用@annotation表达式来描述切点
3.在连接点的方法上添加自定义注解

准备测试代码:

 @RequestMapping("/test") 
 @RestController
 public class TestController { 
     @RequestMapping("/t1")
     public String t1() {
         return "t1";
     } 



	@RequestMapping("/t2")
	public boolean t2() {
	return true;
	}
}

	
    @RequestMapping("/user")
	    @RestController
	    public class UserController {
	    @RequestMapping("/u1")
	    public String u1(){
	        return "u1";
	    }
	    @RequestMapping("/u2")
	    public String u2(){
	        return "u2";
	    }
	}

1.2.1自定义注解@MyAspect

创建⼀个注解类(和创建Class文件⼀样的流程, 选择Annotation就可以了)

 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;

 @Target(ElementType.METHOD)
 @Retention(RetentionPolicy.RUNTIME)
 public @interface MyAspect {

 }

@Target标识了Annotation所修饰的对象范围,即该注解可以用在什么地方.
常用取值:
ElementType.TYPE:用于描述类、接口(包括注解类型)或enum声明
ElementType.METHOD:描述方法
ElementType.PARAMETER:描述参数
ElementType.TYPE_USE:可以标注任意类型


@Retention指Annotation被保留的时间长短,标明注解生命周期@Retention的取值有三种:


1.RetentionPolicy.SOURCE:表示注解仅存在于源代码中,编译成字节码后会被丢弃.这意味着在运行时无法获取到该注解的信息,只能在编译时使用.比如@SuppressWarnings,以及lombok提供的注解@Data,@slf4j
2.RetentionPolicy.CLASS:编译时注解.表示注解存在于源代码和字节码中,但在运行时会被丢弃.这意味着在编译时和字节码中可以通过反射获取到该注解的信息,但在实际运行时无法获取.通常用于一些框架和工具的注解.
3.RetentionPolicy.RUNTIME:运行时注解.表示注解存在于源代码,字节码和运行时中.这意味着在编译时,字节码中和实际运行时都可以通过反射获取到该注解的信息.通常用于一些需要在运行时处理的注解,如Spring的@Controller @ResponseBody

 1.2.2切面类

使用@annotation切点表达式定义切点,只对@MyAspect生效

切面类代码如下:

 @Slf4j
 @Component
 @Aspect
 public class MyAspectDemo { 
     //前置通知
     @Before("@annotation(com.example.demo.aspect.MyAspect)")
     public void before(){
        log.info("MyAspect -> before ...");
     }

     //后置通知
     @After("@annotation(com.example.demo.aspect.MyAspect)")
     public void after(){
        log.info("MyAspect -> after ...");
     }
 }

1.2.3添加自定义注解

在TestController中的t1()和UserController中的u1()这两个方法上添加自定义注解@MyAspect,其他方法不添加

 @MyAspect
 @RequestMapping("/t1")
 public String t1() {
     return "t1";
 }

 @MyAspect
 @RequestMapping("/u1")
 public String u1(){
     return "u1";
 }

运行程序测试接口:

http://127.0.0.1:8080/test/t1

观察日志:

可以看到,切面通知被执行了

继续测试:

http://127.0.0.1:8080/test/t2, 切面通知未执行

http://127.0.0.1:8080/user/u1 , 切面通知执行 

2.Sping AOP原理

上面我们主要学习了Spring AOP的应用,接下来我们来学习SpringAOP的原理,也就是Spring是如何实现AOP的.
Spring AOP是基于动态代理来实现AOP的,学习内容主要分以下两部分
1.代理模式
2.Spring AOP源码剖析

2.1代理模式

代理模式,也叫委托模式.
定义:为其他对象提供一种代理以控制对这个对象的访问.它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用.
在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用.

使用代理前:

使用代理后:
生活中的代理
艺人经纪人:广告商找艺人拍广告,需要经过经纪人,由经纪人来和艺人进行沟通.
房屋中介:房屋进行租赁时,卖方会把房屋授权给中介,由中介来代理看房,房屋咨询等服务.
秘书/助理:合作伙伴找老板谈合作,需要先经过秘书/助理预约.
 

代理模式的主要角色:

1.  Subject: 业务接口类. 可以是抽象类或者接口(不⼀定有)

2.  RealSubject: 业务实现类.  具体的业务执行 , 也就是被代理对象.

3.  Proxy: 代理类. RealSubject的代理.

比如房屋租赁

Subject 就是提前定义了房东做的事情, 交给中介代理, 也是中介要做的事情 

RealSubject: 房东

Proxy: 中介

UML类图如下:

代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强.

根据代理的创建时期,代理模式分为静态代理动态代理.
静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的.class文件就已经存在了.
动态代理:在程序运行时,运用反射机制动态创建而成.

2.1.1静态代理

静态代理:在程序运行前,代理类的.class文件就已经存在了.(在出租房子之前,中介已经做好了相关的工作,就等租户来租房子了)
我们通过代码来加深理解.以房租租赁为例
1.定义接口(定义房东要做的事情,也是中介需要做的事情)

 public interface HouseSubject {
     void rentHouse();
}

2.实现接口(房东出租房子)

 public class RealHouseSubject implements HouseSubject{
     @Override
     public void rentHouse() {
         System.out.println("我是房东 , 我出租房⼦");
     }
 }

3.实现代理(中介,帮房东出租房子)

 public class HouseProxy implements HouseSubject{ 
     //将被代理对象声明为成员变量
     private HouseSubject houseSubject;
     public HouseProxy(HouseSubject houseSubject) {
        this.houseSubject = houseSubject;
     } 

     @Override
     public void rentHouse() { 
        //开始代理
         System.out.println("我是中介 , 开始代理"); 
        //代理房东出租房⼦
        houseSubject.rentHouse(); 
        //代理结束
         System.out.println("我是中介 , 代理结束"); 
     }
 }

4.编写测试用例:

 public class StaticMain {
     public static void main(String[] args) {
        HouseSubject subject = new RealHouseSubject(); 
        //创建代理类
        HouseProxy proxy = new HouseProxy(subject); 
        //通过代理类访问⽬标⽅法
        proxy.rentHouse(); 
     }
 }

运行结果:

上面这个代理实现方式就是静态代理
从上述程序可以看出,虽然静态代理也完成了对目标对象的代理,但是由于代码都写死了,对目标对象的每个方法的增强都是手动完成的,非常不灵活.所以日常开发几乎看不到静态代理的场景.
接下来新增需求:中介又新增了其他业务:代理房屋出售我们需要对上述代码进行修改
1.接口定义修改

 public interface HouseSubject {
     void rentHouse();
     void saleHouse();
 }

2.接口实现修改

 public class RealHouseSubject implements HouseSubject{  
     @Override
     public void rentHouse() {
         System.out.println("我是房东 , 我出租房⼦");
      } 

     @Override
     public void saleHouse() {
         System.out.println("我是房东 , 我出售房⼦");
     }
 }

3.代理类修改

 public class HouseProxy implements HouseSubject{ 
     //将被代理对象声明为成员变量
     private HouseSubject houseSubject;
     public HouseProxy(HouseSubject houseSubject) {
        this.houseSubject = houseSubject;
     } 

     @Override
     public void rentHouse() { 
        //开始代理
         System.out.println("我是中介 , 开始代理"); 
        //代理房东出租房⼦
        houseSubject.rentHouse(); 
        //代理结束
         System.out.println("我是中介 , 代理结束"); 
     }

     @Override
     public void saleHouse() { 
        //开始代理
         System.out.println("我是中介 , 开始代理"); 
        //代理房东出租房⼦
        houseSubject.saleHouse(); 
        //代理结束
         System.out.println("我是中介 , 代理结束"); 
     }
 }

从上述代码可以看出,我们修改接口(Subject)和业务实现类(RealSubject)时,还需要修改代理类(Proxy).
同样的,如果有新增接口(Subject)和业务实现类(RealSubject),也需要对每一个业务实现类新增代理类(Proxy).
既然代理的流程是一样的,有没有一种办法,让他们通过一个代理类来实现呢?这就需要用到动态代理技术了

2.1.2动态代理

相比于静态代理来说,动态代理更加灵活.
我们不需要针对每个目标对象都单独创建一个代理对象,而是把这个创建代理对象的工作推迟到程序运行时由JVM来实现.也就是说动态代理在程序运行时,根据需要动态创建生成.比如房屋中介,我不需要提前预测都有哪些业务,而是业务来了我再根据情况创建.我们先看代码再来理解.
Java也对动态代理进行了实现,并给我们提供了一些API,常见的实现方式有两种:

2.1.3JDK动态代理

JDK动态代理类实现步骤
1.定义一个接口及其实现类(静态代理中的HouseSubject和RealHouseSubject)
2.自定义InvocationHandler并重写invoke方法,在invoke方法中我们会调用目标方法(被代理类的方法)并自定义一些处理逻辑
3.通过Proxy.newProxyInstance(ClassLoader loader,Class<?>[ ]  interfaces,InvocationHandlerh)方法创建代理对象

 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method; 

 public class JDKInvocationHandler implements InvocationHandler { 
     //⽬标对象即就是被代理对象
     private Object target; 

     public JDKInvocationHandler(Object target) {
        this.target = target;
     } 

     @Override
     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 代理增强内容
         System.out.println("我是中介 , 开始代理"); 
        //通过反射调⽤被代理类的⽅法
        Object retVal = method.invoke(target, args); 
        //代理增强内容
         System.out.println("我是中介 , 代理结束");
         return retVal; 
     }
  }

创建⼀个代理对象并使用

 public class DynamicMain {
     public static void main(String[] args) {
        HouseSubject target=  new RealHouseSubject();
        //创建⼀个代理类:通过被代理类、被代理实现的接口、⽅法调用处理器来创建
         HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(
                 target.getClass().getClassLoader(),
                new Class[]{HouseSubject.class},
                new JDKInvocationHandler(target)
         );
        proxy.rentHouse();
     }
 }

1.InvocationHandler
InvocationHandler接口是Java动态代理的关键接口之一,它定义了一个单一方法invoke(),用于处理被代理对象的方法调用.
 

 public interface InvocationHandler {
     /**
      * 参数说明
      * proxy:代理对象
      * method:代理对象需要实现的⽅法,即其中需要重写的⽅法
      * args:method所对应⽅法的参数
     */
     public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
 }

通过实现InvocationHandler接口,可以对被代理对象的方法进行功能增强.
2.Proxy
Proxy类中使用频率最高的方法是:newProxyInstance(),主要用来生成一个代理对象


 public static Object newProxyInstance(ClassLoader loader,
                                         Class<?>[] interfaces,
                                         InvocationHandler h)
        throws IllegalArgumentException
     {
         //...代码省略
     }

这个方法一共有3个参数:
Loader:类加载器,用于加载代理对象.
interfaces:被代理类实现的一些接口(这个参数的定义,也决定了JDK动态代理只能代理实现了接口的一些类)
h:实现了InvocationHandler接口的对象

2.1.4CGLIB动态代理

JDK动态代理有一个最致命的问题是其只能代理实现了接口的类.
有些场景下,我们的业务代码是直接实现的,并没有接口定义.为了解决这个问题,我们可以用CGLIB动态代理机制来解决.
CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成.CGLIB通过继承方式实现代理,很多知名的开源框架都使用到了CGLIB.例如Spring中的AOP模块中:如果目标对象实现了接口,则默认采用JDK动态代理,否则采用CGLIB动态代理
CGLIB动态代理类实现步骤
1.定义一个类(被代理类)
2.自定义MethodInterceptor并重写intercept方法,intercept用于增强目标方法,和JDK动态代理中的invoke方法类似
3.通过Enhancer类的create()创建代理类
添加依赖

JDK 动态代理不同, CGLIB(Code Generation Library) 实际是属于⼀个开源项⽬ ,如果你要使用它的话 ,需要手动添加相关依赖

 <dependency>
   <groupId>cglib</groupId>
   <artifactId>cglib</artifactId>
   <version>3.3.0</version> 
 </dependency>

⾃定义 MethodInterceptor法拦截器) 实现MethodInterceptor接口

 public class CGLIBInterceptor implements MethodInterceptor {
     //⽬标对象, 即被代理对象
     private Object target; 

     public CGLIBInterceptor(Object target){
        this.target = target;
     } 

     @Override
     public Object intercept(Object o, Method method, Object[] objects, MethodPro 
        // 代理增强内容
         System.out.println("我是中介 , 开始代理"); 
        //通过反射调⽤被代理类的⽅法
         Object retVal = methodProxy.invoke(target, objects); 
        //代理增强内容
         System.out.println("我是中介 , 代理结束");
         return retVal; 
     }
 }

创建代理类, 并使用

 public class DynamicMain {
     public static void main(String[] args) {
        HouseSubject target=  new RealHouseSubject();
         HouseSubject proxy= (HouseSubject) Enhancer.create(target.getClass(),new
        proxy.rentHouse();
     }
 }

1.MethodInterceptor

MethodInterceptor和JDK动态代理中的InvocationHandler类似,它只定义了一个方法intercept(),用于增强目标方法.
 

 public interface MethodInterceptor extends Callback {
     /**
      * 参数说明:
      * o: 被代理的对象
      * method: ⽬标⽅法(被拦截的⽅法, 也就是需要增强的⽅法) 
      * objects: ⽅法⼊参
      * methodProxy: ⽤于调⽤原始⽅法 
     */
     Object intercept(Object o, Method method, Object[] objects, 
MethodProxy methodProxy) throws Throwable; 
 }

2. Enhancer.create()

 Enhancer.create()用来生成一个代理对象

 public static Object create(Class type, Callback callback) { 
    //...代码省略
 }

参数说明:

type: 被代理类的类型(类或接口)

callback: ⾃定义⽅法拦截器 MethodInterceptor

3.总结

1.AOP是一种思想,是对某一类事情的集中处理.Spring框架实现了AOP,称之为SpringAOP
2.SpringAOP常见实现方式有两种:1.基于注解@Aspect来实现2.基于自定义注解来实现,还有一些更原始的方式,比如基于代理,基于xml配置的方式,但目标比较少见
3.SpringAOP是基于动态代理实现的,有两种方式:1.基本JDK动态代理实现2.基于CGLIB动态代理实现.运行时使用哪种方式与项目配置和代理的对象有关.

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

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

相关文章

【GO】rotatelogs库和sirupsen/logrus库实现日志功能的实践用例

“github.com/sirupsen/logrus” 是一个 Go 语言的日志库&#xff0c;它提供了一种简单、灵活的方式来记录日志。该库的主要特点包括&#xff1a; 支持多种日志输出目标&#xff0c;如控制台、文件等。 支持日志轮转&#xff0c;可以按照时间或文件大小进行轮转。 支持日志格式…

【投稿优惠|稳定出版】2024年体育、健康与食品安全国际学术会议(ICSHFS 2024)

【投稿优惠|稳定出版】2024年体育、健康与食品安全国际学术会议&#xff08;ICSHFS 2024&#xff09; 2024 International Conference on Sports, Health, and Food Safety(ICSHFS 2024) 会议简介&#xff1a; 2024年体育、健康与食品安全国际学术会议&#xff08;ICSHFS 2024…

Navicat和SQLynx功能比较三(数据导出:使用MySQL近千万数据测试)

数据导出的功能在数据库管理工具中是最普遍的功能之一。所以数据导出的功能稳定性和性能也是数据库管理工具是否能很好地满足应用需求的一个考虑因素。 目录 1. 整体比较 2. 示例 2.1 前置环境 2.2 Navicat导出 2.3 SQLynx导出 2.4 性能对比结果&#xff08;690万行数据&…

用友 打印模版增加打印次数,以付款申请单为例

一些公司需要在纸质单据上加上和电子发票一样的打印次数&#xff0c;具体做法如下&#xff1a; 找到要增加的单据&#xff0c;点击【格式设置】 找到打印&#xff0c;活动文本 设置活动文本&#xff0c;高级属性&#xff0c;在下拉框里找到【打印或显示操作员/次数/时间】或【…

NetSuite 审批工作流与事务处理类型的限制关系

在最近的实践中&#xff0c;用户提出可否对Credit Memo与Vendor Prepayment Application两种事务处理类型进行审批参与&#xff0c;当提出来的时候我们并没有直接在系统中进行测试&#xff0c;而是以常规事务处理的角度认为可以满足客户的需求&#xff1b; 但在沙盒环境中讨论…

【ARMv8/v9 GIC 系列 2.1 -- GIC SPI 中断的 pending 和 clear pending 配置】

文章目录 GIC Pending 和 Clear PendingGICD_ISPENDR<n>GICD_ICPENDR<n>参数<n>编号解释使用举例设置中断ID 100为挂起状态清除中断ID 100的挂起状态 代码实现小结 GIC Pending 和 Clear Pending 在ARMv8体系结构中&#xff0c;GICD_ISPENDR<n> 和 GI…

网页抓取和网页爬取之间有何区别?

随着互联网的发展和信息的爆炸式增长&#xff0c;数据收集和处理已成为企业和个人不可或缺的需求。在此背景下&#xff0c;网页抓取和网络爬虫已成为两种常见的数据收集方法。虽然这两种方法看似相似&#xff0c;但它们的方法和目标存在显著差异。本文将为您详细介绍网页抓取和…

看见未来社区:视频孪生技术打造智慧社区

智慧社区的建设需要创新的技术支撑。智汇云舟创新升级数字孪生为视频孪生技术&#xff0c;通过将真实世界的视频监控与数字模型实时融合&#xff0c;实现了对物理空间的实时实景动态模拟。 针对智慧社区管理业务&#xff0c;以智汇云舟视频孪生平台为支撑&#xff0c;综合承载…

从零开始搭建创业公司全新技术栈解决方案

从零开始搭建创业公司全新技术栈解决方案 关于猫头虎 大家好&#xff0c;我是猫头虎&#xff0c;别名猫头虎博主&#xff0c;擅长的技术领域包括云原生、前端、后端、运维和AI。我的博客主要分享技术教程、bug解决思路、开发工具教程、前沿科技资讯、产品评测图文、产品使用体…

python如何做报表系统

首先我们安装的python和PyQt5要保持一致&#xff0c;要么都是32位或者都是64位。 下载安装&#xff0c;安装完成之后我们记得要设置环境变量。 一路选择“下一步”就可以了。 安装完成之后我们需要验证是否成功。 pyqt5的安装直接安装就可以的&#xff0c;主要更改环境变量~~\p…

使用 Python 中的美丽汤进行网络数据解析的完整指南

Beautiful Soup 是一个广泛使用的 Python 库&#xff0c;在数据提取方面发挥着重要作用。它为解析 HTML 和 XML 文档提供了强大的工具&#xff0c;使从网页中轻松提取有价值的数据成为可能。该库简化了处理互联网上非结构化内容的复杂过程&#xff0c;使您可以将原始网页数据转…

房间灰尘多怎么办?资深保洁推荐除尘最有效的空气净化器

家中的灰尘问题一直是许多人的烦恼&#xff0c;尤其是对尘螨过敏的人来说&#xff0c;灰尘简直是“心头之患”。常言道&#xff1a;“家有尘埃&#xff0c;心头有累。”每天打扫灰尘成了许多人的烦恼&#xff0c;尤其是对尘螨过敏的人来说&#xff0c;灰尘简直是“心头之患”。…

健身器械行业外贸ERP管理降本增效解决方案

随着经济的迅速发展&#xff0c;以及健身锻炼的普及&#xff0c;人们对健身器材的需求量也在大幅度增加。欧美市场增长迅猛&#xff0c;家用健身器材热度飙升&#xff0c;尤其是跑步机、健身单车等轻便型家用健身器材&#xff0c;备受消费者青睐。 出口的主要国家包括&#xf…

主存储器的基本组成+容量扩展+与CPU的连接

1.基本组成 1.主存储器的基本组成和读写操作 主存储器被称为主存/内存。是计算机中存储程序的重要部件 主存储器内部包含了存储体、各种逻辑部件以及控制电路等。 主存是通过寻址的方式对存储体内的存储单元进行读写操作的。 主存首先要从MAR获取地址&#xff0c;之后译码器…

Paper Reading: EfficientAD:毫秒级延迟的准确视觉异常检测

EfficientAD 简介方法高效的patch描述PDN教师pretraining 轻量级的师生模型逻辑异常检测异常图像的标准化 实验局限性 EfficientAD: Accurate Visual Anomaly Detection at Millisecond-Level Latencies EfficientAD&#xff1a;毫秒级延迟的准确视觉异常检测, WACV 2024 paper…

1.4自然语言的分布式表示-word2vec实操

文章目录 0写在前面1数据准备2CBOW模型结构的实现3交叉熵损失函数的前向计算3.1关于cross_entropy_error的计算3.2关于softmax 0写在前面 代码都位于&#xff1a;nlp&#xff1b;其他相关内容详见专栏&#xff1a;深度学习自然语言处理基础_骑着蜗牛环游深度学习世界的博客-CS…

家有老人小孩,室内灰尘危害大!资深家政教你选对除尘空气净化器

哈喽&#xff0c;各位亲爱的朋友们&#xff01;今天我们来聊聊每次大扫除时最让人头疼的问题——灰尘。你有没有发现&#xff0c;两天不打扫&#xff0c;桌子上就能积上一层灰&#xff1b;阳光一照&#xff0c;地板上的灰尘都在跳舞&#xff1b;整理被子的时候&#xff0c;空气…

ONLYOFFICE 文档 8.1 现已发布:功能全面的 PDF 编辑器、幻灯片版式等等

最新版本的 ONLYOFFICE 在线编辑器已经发布&#xff0c;整个套件带来了30多个新功能和432个 bug 修复。阅读本文了解全部更新。 什么是 ONLYOFFICE 文档 ONLYOFFICE 文档是一套功能强大的文档编辑器&#xff0c;支持编辑处理文本文档、电子表格、演示文稿、可填写的表单、PDF&…

基 CanMV 的 C 开发环境搭建(Linux,Ubuntu篇)

不论是使用 CanMV 提供的基于 C 语言和 FreeRTOS 的应用开发方式开发应用程序或是编译 CanMV 固件&#xff0c;都需要搭建基于 CanMV 的 C 开发环境&#xff0c;用于编译 CanMV 源码。 1. 开发环境搭建说明 CanMV 提供了基于 C 语言和 FreeRTOS 的应用开发…

IO-LiNK简介

什么是IO-Link&#xff1f; IO-Link&#xff08; IEC 61131-9 &#xff09;是一种开放式标准串行通信协议&#xff0c;允许支持 IO-Link 的传感器、设备进行双向数据交换&#xff0c;并连接到主站。 IO-Link 主站可以通过各种网络&#xff0c;如现场总线进行传输。每个 IO-L…