2024年4月

论文提出了一种新的ViT位置编码CPE,基于每个token的局部邻域信息动态地生成对应位置编码。CPE由卷积实现,使得模型融合CNN和Transfomer的优点,不仅可以处理较长的输入序列,也可以在视觉任务中保持理想的平移不变性。从实验结果来看,基于CPE的CPVT比以前的位置编码方法效果更好

来源:晓飞的算法工程笔记 公众号

论文: Conditional Positional Encodings for Vision Transformers

Introduction


Transformer的自注意机制可以捕捉长距离的图像信息,根据图像内容动态地调整感受域大小。但自注意操作是顺序不变的,不能利用输入序列中的token顺序信息。为了让Transformer顺序可知,将位置编码加到输入的token序列中成为了常规操作,但这也为Tranformer带来两个比较大的问题:

  • 虽然位置编码很有效,但会降低Transformer的灵活性。位置编码可以是可学习的,也可以是由不同频率的正弦函数直接生成的。如果需要输入更长的token序列,模型当前的位置编码以及权值都会失效,需要调整结构后再fine-tuning以保持性能。
  • 加入位置编码后,绝对位置编码使得Transformer缺乏图像处理所需的平移不变性。如果采用相对位置编码,不仅带来额外的计算成本,还要修改Transformer的标准实现。而且在图像处理中,相对位置编码的效果没有绝对位置编码好。

为了解决上述问题,论文提出了一个用于Vision Transformer的条件位置编码(CPE,conditional positional encoding)。与以往预先定义且输入无关的固定或可学习的位置编码不同,CPE是动态生成的,生成的位置编码中的每个值都与对应输入token的局部邻域相关。因此,CPE可以泛化到更长的输入序列,并且在图像分类任务中保持所需的平移不变性,从而提高分类精度。

CPE通过一个简单的位置编码生成器(PEG,position encoding generator)实现,可以无缝地融入当前的Transformer框架中。在PEG的基础上,论文提出了Conditional Position encoding Vision Transformer(CPVT),CPVT在ImageNet分类任务中达到了SOTA结果。

论文的贡献总结如下:

  • 提出了一种新型的位置编码方案,条件位置编码(CPE)。CPE由位置编码生成器(PEG)动态生成,可以简单地嵌入到深度学习框架中,不涉及对Transformer的修改。
  • CPE以输入token的局部邻域为条件生成对应的位置编码,可适应任意的输入序列长度,从而可以处理更大分辨率的图像。
  • 相对于常见的绝对位置编码,CPE可以保持平移不变性,这有助于提高图像分类的性能。
  • 在CPE的基础上,论文提出了条件位置编码ViT(CPVT),在ImageNet上到达了SOTA结果。
  • 此外,论文还提供了class token的替代方案,使用平移不变的全局平均池(GAP)进行类别预测。通过GAP,CPVT可以实现完全的平移不变性,性能也因此进一步提高约1%。相比之下,基于绝对位置编码的模型只能从GAP中获得很小的性能提升,因为其编码方式本身已经打破了平移不变性。

Vision Transformer with Conditional Position Encodings


Motivations

在Vision Transformer中,尺寸为
\(H\times W\)
的输入图像被分割成
\(N=\frac{HW}{S^2}\)

\(S×S\)
的图像块,随后加上相同大小的可学习绝对位置编码向量。

论文认为常用的绝对位置编码有两个问题:

  • 模型无法处理比训练序列更长的输入序列。
  • 图像块平移后会对应新的位置编码,使得模型不具备平移不变性。

实际上,直接去掉位置编码就能将模型应用于长序列,但这种解决方案会丢失输入序列的位置信息,严重降低了性能。其次,可以像DeiT那样对位置编码进行插值,使其具有与长序列相同的长度。但这种方法需要对模型多做几次fine-tuning,否则性能也是会明显下降。对于高分辨率的输入,最完美的解决方案是在不进行任何fine-tuning的情况下,模型依然有显著的性能改善。

使用相对位置编码虽然可以解决上述两个问题,但不能提供任何绝对位置信息。有研究表明,绝对位置信息对分类任务也很重要。而在替换对比实验中,采用相对位置编码的模型性能也是较差的。

Conditional Positional Encodings

论文认为,一个完美的视觉任务的位置编码应该满足以下要求:

  • 对输入序列顺序可知,但平移不变。
  • 具有归纳能力,能够处理比训练时更长的序列。
  • 能提供一定程度的绝对位置信息,这对性能非常重要。

经过研究,论文发现将位置编码表示为输入的局部领域关系表示,就能够满足上述的所有要求:

  • 首先,它是顺序可知的,输入序列顺序也会影响到局部邻域的顺序。而输入图像中目标的平移可能不会改变其局部邻域的顺序,即平移不变性。
  • 其次,模型可以应用更长的输入序列,因为每个位置编码都由对应token的局部邻域生成。
  • 此外,它也可以表达一定程度的绝对位置信息。只要任意一个输入token的绝对位置是已知的(比如边界的零填充),所有其他token的绝对位置可以通过输入token之间的相互关系推断出来。

因此,论文提出了位置编码生成器(PEG),以输入token的局部邻域为条件,动态地产生位置编码。

  • Positional Encoding Generator

PEG的处理过程如图2所示。为了将局部领域作为条件,先将DeiT的输入序列
\(X\in \mathbb{R}^{B\times N\times C}\)
重塑为二维图像形状
\(X^{'} \in\mathbb{R}^{B\times H\times W\times C}\)
,然后通过函数
\(\mathcal{F}\)

\(X^{'}\)
的局部图像中生成产生条件性位置编码
\(E^{B\times H\times W\times C}\)

PEG可以由一个核大小为
\(k(k\ge 3)\)
、零填充为
\(\frac{k-1}{2}\)
的二维卷积来实现。需要注意的是,零填充是为了位置编码包含绝对位置信息,从而提升模型性能。函数
\(\mathcal{F}\)
也可以是其它形式,如可分离卷积等。

Conditional Positional Encoding Vision Transformers

基于条件性位置编码,论文提出了条件位置编码Vision Transformer(CPVT),除了条件位置编码之外,其它完全遵循ViT和DeiT来设计。CPVT一共有三种尺寸:CPVT-Ti、CPVT-S和CPVT-B。

有趣的是,论文发现PEG的插入位置对性能也会有大影响。在第一个encoder之后插入的性能最佳,而不是直接在开头插入。

此外,DeiT和ViT都使用一个额外的可学习class token进行分类。但根据其结构设计,该token不是平移不变的,只能靠训练来尽可能学习平移不变性。一个简单的替代方法是直接换为全局平均池(GAP),因为GAP本身就是平移不变的。因此,论文也提出了CVPT-GAP,去掉class token,改为采用GAP输出进行预测。在与平移不变的位置编码配套使用时,CVPT-GAP是完全平移不变的,可以实现更好的性能。

Experiment


训练配置。

直接将224x224模型改为384x384输入进行测试。

class token与GAP的性能对比。

对第二个encoder的自注意力图进行可视化,CPVT的自注意力图明显更加多样化。

与SOTA网络进行对比,⚗为使用DeiT的蒸馏策略的结果。

PEG插入位置对比,第一个encoder之后插入效果最好。

PEG的-1插入场景可能是由于原始图片需要更大的感受域,通过实验验证增大卷积核能显著提高性能。

插入PEG个数的对比实验。

不同位置编码方式的对比实验。

PEG生成位置编码时零填充的对比实验。

对PEG性能提升来源进行对比实验,PEG的确跟输入的领域关系有关,但跟卷积参数是否对应当前网络关系不大。

不同配置下的性能对比。

应用到PVT上的性能提升。

作为目标监测网络主干的性能对比。

Conclusion


论文提出了一种新的ViT位置编码CPE,基于每个token的局部邻域信息动态地生成对应位置编码。CPE由卷积实现,使得模型融合CNN和Transfomer的优点,不仅可以处理较长的输入序列,也可以在视觉任务中保持理想的平移不变性。从实验结果来看,基于CPE的CPVT比以前的位置编码方法效果更好。



如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

PrimiHub
一款由密码学专家团队打造的开源隐私计算平台,专注于分享数据安全、密码学、联邦学习、同态加密等隐私计算领域的技术和内容。

在数字安全领域,加密算法扮演着至关重要的角色。它们确保了信息的机密性、完整性和不可否认性。RSA算法和椭圆曲线算法(ECC)是当前最广泛使用的两种非对称加密技术。本文将深入探讨这两种算法的加密过程。

RSA算法

算法概述

RSA算法是一种基于大整数因数分解难题的非对称加密算法。由Ron Rivest、Adi Shamir 和 Leonard Adleman在1977年提出。RSA算法的安全性依赖于分解一个大整数的难度,该整数是两个大质数的乘积。

加密过程

  1. 密钥生成


    • 随机选择两个大质数 ( p ) 和 ( q )。
    • 计算 ( n = p \times q )。
    • 计算欧拉函数 ( \phi(n) = (p-1) \times (q-1) )。
    • 选择一个小于 ( \phi(n) ) 的整数 ( e ),通常 ( e ) 为65537,因为它具有一些有利的数学性质。
    • 计算 ( d ),使得 ( d \times e \equiv 1 \mod \phi(n) ),即 ( d ) 是 ( e ) 关于模 ( \phi(n) ) 的乘法逆元。
    • 公钥为 ( (n, e) ),私钥为 ( (n, d) )。
  2. 加密


    • 设明文消息为 ( M ),且 ( 0 \leq M < n )。
    • 计算密文 ( C ) 为 ( C = M^e \mod n )。
  3. 解密


    • 使用私钥解密密文 ( C ) 得到明文 ( M )。
    • 计算 ( M = C^d \mod n )。
  • 质数
    :一个大于1的自然数,除了1和它本身外,不能被其他自然数整除的数。
  • 欧拉函数
    :对于正整数 ( n ),欧拉函数 ( \phi(n) ) 表示小于或等于 ( n ) 且与 ( n ) 互质的正整数的数量。


椭圆曲线算法(ECC)

算法概述

椭圆曲线密码学是一种基于椭圆曲线数学的公钥加密技术。它提供了相同密钥长度下比RSA更高的安全性。ECC的安全性基于椭圆曲线离散对数问题(ECDLP)的难度。

加密过程

  1. 密钥生成


    • 选择一个椭圆曲线方程 ( y^2 = x^3 + ax + b )。
    • 选择一个基点 ( G ),它是一个在椭圆曲线上的点,且满足群的性质。
    • 随机选择一个私钥 ( d )。
    • 计算公钥 ( Q = dG ),即 ( d ) 倍的基点 ( G )。
    • 公钥为 ( (G, Q) ),私钥为 ( d )。
  2. 加密


    • 设明文消息为 ( M )。
    • 选择一个随机数 ( k )。
    • 计算 ( C_1 = kG )。
    • 计算 ( C_2 = M + kQ )。
    • 密文为 ( (C_1, C_2) )。
  3. 解密


    • 给定密文 ( (C_1, C_2) )。
    • 计算 ( k = (C_1 - Q) \times d^{-1} \mod n )。
    • 计算 ( M = C_2 - kG )。
  • 椭圆曲线
    :一个由 ( y^2 = x^3 + ax + b ) 定义的平面上的点集,加上一个额外的点“无穷远点”。
  • 离散对数问题
    :在有限域上,给定一个基元素 ( g ) 和它的幂 ( g^k ),求整数 ( k ) 是非常困难的。


结论

RSA和椭圆曲线算法都是现代密码学中非常重要的加密技术。RSA算法因其历史悠久和广泛的应用而广为人知,而椭圆曲线算法则因其在相同安全级别的更高效率而受到关注。了解这些算法的工作原理对于保护数据安全至关重要。

PrimiHub
一款由密码学专家团队打造的开源隐私计算平台,专注于分享数据安全、密码学、联邦学习、同态加密等隐私计算领域的技术和内容。

本文分享自华为云社区《
Spring高手之路17——动态代理的艺术与实践
》,作者: 砖业洋__。

1. 背景

动态代理是一种强大的设计模式,它允许开发者在运行时创建代理对象,用于拦截对真实对象的方法调用。这种技术在实现面向切面编程(
AOP
)、事务管理、权限控制等功能时特别有用,因为它可以在不修改原有代码结构的前提下,为程序动态地注入额外的逻辑。

2. JDK动态代理

2.1 定义和演示

JDK
动态代理是
Java
语言提供的一种基于接口的代理机制,允许开发者在运行时动态地创建代理对象,而无需为每个类编写具体的代理实现。

这种机制主要通过
java.lang.reflect.Proxy
类和
java.lang.reflect.InvocationHandler
接口实现。下面是
JDK
动态代理的核心要点和如何使用它们的概述。

使用步骤

  1. 定义接口:首先定义一个或多个接口,代理对象将实现这些接口。

  2. 实现接口:创建一个类,它实现上述接口,提供具体的实现逻辑。

  3. 创建
    InvocationHandler
    实现:定义一个
    InvocationHandler
    的实现,这个实现中的
    invoke
    方法可以包含自定义逻辑。

  4. 创建代理对象:使用
    Proxy.newProxyInstance
    方法,传入目标对象的类加载器、需要代理的接口数组以及
    InvocationHandler
    的实现,来创建一个实现了指定接口的代理对象。

用简单的例子来说明这个过程,全部代码如下:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interfaceHelloWorld {voidsayHello();
}
classHelloWorldImpl implements HelloWorld {public voidsayHello() {
System.
out.println("Hello world!");
}
}
public classDemoApplication {public static voidmain(String[] args) {
HelloWorldImpl realObject
= newHelloWorldImpl();
HelloWorld proxyInstance
=(HelloWorld) Proxy.newProxyInstance(
HelloWorldImpl.
class.getClassLoader(), //使用目标类的类加载器 new Class[]{HelloWorld.class}, //代理类需要实现的接口列表 newInvocationHandler() {
@Override
publicObject invoke(Object proxy, Method method, Object[] args) throws Throwable {//在调用目标方法前可以插入自定义逻辑 System.out.println("Before method call");//调用目标对象的方法 Object result =method.invoke(realObject, args);//在调用目标方法后可以插入自定义逻辑 System.out.println("After method call");returnresult;
}
});

proxyInstance.sayHello();
}
}

运行结果如下:

InvocationHandler
是动态代理的核心接口之一,当我们使用动态代理模式创建代理对象时,任何对代理对象的方法调用都会被转发到一个实现了
InvocationHandler
接口的实例的
invoke
方法上。

我们经常看到
InvocationHandler
动态代理的匿名内部类,这会在代理对象的相应方法被调用时执行。具体地说,每当对代理对象执行方法调用时,调用的方法不会直接执行,而是转发到实现了
InvocationHandler

invoke
方法上。在这个
invoke
方法内部,我们可以定义拦截逻辑、调用原始对象的方法、修改返回值等操作。

在这个例子中,当调用
proxyInstance.sayHello()
方法时,实际上执行的是
InvocationHandler
的匿名内部类中的
invoke
方法。这个方法中,我们可以在调用实际对象的
sayHello
方法前后添加自定义逻辑(比如这里的打印消息)。这就是动态代理和
InvocationHandler
的工作原理。

我们来看关键的一句代码

Object result = method.invoke(realObject, args);


Java
的动态代理中,
method.invoke(realObject, args)
这句代码扮演着核心的角色,因为它实现了代理对象方法调用的转发机制。下面分别解释一下这行代码的两个主要部分:
method.invoke()
方法和
args
参数。

method.invoke(realObject, args)

  • 作用:这行代码的作用是调用目标对象(
    realObject
    )的具体方法。在动态代理的上下文中,
    invoke
    方法是在代理实例上调用方法时被自动调用的。通过这种方式可以在实际的方法执行前后加入自定义的逻辑,比如日志记录、权限检查等。

  • method:
    method
    是一个
    java.lang.reflect.Method
    类的实例,代表了正在被调用的方法。在
    invoke
    方法中,这个对象用来标识代理对象上被调用的具体方法。

注意:如果尝试直接在
invoke
方法内部使用
method.invoke(proxy, args)
调用代理对象的方法,而不是调用原始目标对象的方法,则会导致无限循环。这是因为调用
proxy
实例上的方法会再次被代理拦截,从而无限调用
invoke
方法,传参可别传错了。

  • invoke:
    Method
    类的
    invoke
    方法用于执行指定方法。第一个参数是指明方法应该在哪个对象上调用(在这个例子中是
    realObject
    ),后续的参数
    args
    是调用方法时传递的参数。

args

  • 定义:
    args
    是一个对象数组,包含了调用代理方法时传递给方法的参数值。如果被调用的方法没有参数,
    args
    将会是
    null
    或者空数组。

  • 作用:
    args
    允许在
    invoke
    方法内部传递参数给实际要执行的方法。这意味着可以在动态代理中不仅控制是否调用某个方法,还可以修改调用该方法时使用的参数。

2.2 不同方法分别代理

我们继续通过扩展
HelloWorld
接口来包含多个方法,并通过
JDK
动态代理演示权限控制和功能开关操作的一种实现方式

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interfaceHelloWorld {voidsayHello();voidsayGoodbye();
}
classHelloWorldImpl implements HelloWorld {public voidsayHello() {
System.
out.println("Hello world!");
}
public voidsayGoodbye() {
System.
out.println("Goodbye world!");
}
}
public classDemoApplication {public static voidmain(String[] args) {
HelloWorld realObject
= newHelloWorldImpl();
HelloWorld proxyInstance
=(HelloWorld) Proxy.newProxyInstance(
HelloWorldImpl.
class.getClassLoader(),new Class[]{HelloWorld.class},newInvocationHandler() {//添加一个简单的权限控制演示 private boolean accessAllowed = true;//简单的功能开关 private boolean goodbyeFunctionEnabled = true;

@Override
publicObject invoke(Object proxy, Method method, Object[] args) throws Throwable {//权限控制 if (!accessAllowed) {
System.
out.println("Access denied");return null; //在实际场景中,可以抛出一个异常 }//功能开关 if (method.getName().equals("sayGoodbye") && !goodbyeFunctionEnabled) {
System.
out.println("Goodbye function is disabled");return null;
}
//方法执行前的通用逻辑 System.out.println("Before method:" +method.getName());//执行方法 Object result =method.invoke(realObject, args);//方法执行后的通用逻辑 System.out.println("After method:" +method.getName());returnresult;
}
});
//正常执行 proxyInstance.sayHello();//可以根据goodbyeFunctionEnabled变量决定是否执行 proxyInstance.sayGoodbye();
}
}

运行如下:

如果
accessAllowed
变量为
false

如果
goodbyeFunctionEnabled
变量为
false

在这个例子中:

  • 权限控制:通过检查
    accessAllowed
    变量,我们可以模拟简单的权限控制。如果没有权限,可以直接返回或抛出异常,避免执行方法。

  • 功能开关:通过检查方法名称和
    goodbyeFunctionEnabled
    变量,我们可以控制
    sayGoodbye
    方法是否被执行。这可以用来根据配置启用或禁用特定功能。

这个例子展示了
JDK
动态代理在实际应用中如何进行方法级别的细粒度控制,同时保持代码的灵活性和可维护性。通过动态代理,我们可以在不修改原始类代码的情况下,为对象动态地添加额外的行为。

2.3 熔断限流和日志监控

为了更全面地展示
JDK
动态代理的能力,我们在先前的示例中添加熔断限流和日志监控的逻辑。这些是在高并发和分布式系统中常见的需求,可以通过动态代理以非侵入式的方式实现。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
interfaceHelloWorld {voidsayHello();
}
classHelloWorldImpl implements HelloWorld {public voidsayHello() {
System.
out.println("Hello world!");
}
}
public classDemoApplication {public static voidmain(String[] args) {
HelloWorld realObject
= newHelloWorldImpl();
HelloWorld proxyInstance
=(HelloWorld) Proxy.newProxyInstance(
HelloWorldImpl.
class.getClassLoader(),new Class[]{HelloWorld.class},newAdvancedInvocationHandler(realObject));//模拟多次调用以观察限流和熔断效果 for (int i = 0; i < 10; i++) {
proxyInstance.sayHello();
}
}
static classAdvancedInvocationHandler implements InvocationHandler {privatefinal Object target;private AtomicInteger requestCount = new AtomicInteger(0);private AtomicLong lastTimestamp = newAtomicLong(System.currentTimeMillis());private volatile boolean circuitBreakerOpen = false;private final long cooldownPeriod = 10000; //冷却时间10秒 publicAdvancedInvocationHandler(Object target) {this.target =target;
}

@Override
publicObject invoke(Object proxy, Method method, Object[] args) throws Throwable {long now =System.currentTimeMillis();//检查熔断器是否应该被重置 if (circuitBreakerOpen && (now - lastTimestamp.get() >cooldownPeriod)) {
circuitBreakerOpen
= false; //重置熔断器 requestCount.set(0); //重置请求计数 System.out.println("Circuit breaker has been reset.");
}
//熔断检查 if(circuitBreakerOpen) {
System.
out.println("Circuit breaker is open. Blocking method execution for:" +method.getName());return null; //在实际场景中,可以返回一个兜底的响应或抛出异常 }//限流检查 if (requestCount.incrementAndGet() > 5) {if (now - lastTimestamp.get() < cooldownPeriod) { //10秒内超过5次请求,触发熔断 circuitBreakerOpen = true;
lastTimestamp.
set(now); //更新时间戳 System.out.println("Too many requests. Opening circuit breaker.");return null; //触发熔断时的处理 } else{//重置计数器和时间戳 requestCount.set(0);
lastTimestamp.
set(now);
}
}
//执行实际方法 Object result =method.invoke(target, args);//方法执行后的逻辑 System.out.println("Executed method:" +method.getName());returnresult;
}
}
}

在这个扩展示例中,我们实现了:

  • 熔断机制:通过一个简单的计数器和时间戳来模拟。如果在
    10
    秒内对任一方法的调用次数超过
    5
    次,我们就"打开"熔断器,阻止进一步的方法调用。在实际应用中,熔断逻辑可能更加复杂,可能包括错误率的检查、调用延迟的监控等。

  • 限流:这里使用的限流策略很简单,通过计数和时间戳来判断是否在短时间内请求过多。在更复杂的场景中,可以使用令牌桶或漏桶算法等更高级的限流策略。

  • 日志监控:在方法调用前后打印日志,这对于监控系统的行为和性能是非常有用的。在实际项目中,这些日志可以集成到日志管理系统中,用于问题诊断和性能分析。

通过在
invoke
方法中加入这些逻辑,我们能够在不修改原有业务代码的情况下,为系统添加复杂的控制和监控功能。如果到达流量阈值或系统处于熔断状态,可以阻止对后端服务的进一步调用,直接返回一个默认值或错误响应,避免系统过载。

3. CGLIB动态代理

CGLIB

Code Generation Library
)是一个强大的高性能代码生成库,它在运行时动态生成新的类。与
JDK
动态代理不同,
CGLIB
能够代理那些没有实现接口的类。这使得
CGLIB
成为那些因为设计限制或其他原因不能使用接口的场景的理想选择。

3.1 定义和演示

工作原理

CGLIB
通过继承目标类并在运行时生成子类来实现动态代理。代理类覆盖了目标类的非
final
方法,并在调用方法前后提供了注入自定义逻辑的能力。这种方法的一个关键优势是它不需要目标对象实现任何接口。

使用CGLIB的步骤

添加CGLIB依赖:首先,需要在项目中添加
CGLIB
库的依赖。

如果使用
Maven
,可以添加如下依赖到
pom.xml
中:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version> <!-- 目前最新的版本 -->
</dependency>

创建MethodInterceptor:实现
MethodInterceptor
接口,这是
CGLIB
提供的回调类型,用于定义方法调用的拦截逻辑。

生成代理对象:使用
Enhancer
类来创建代理对象。
Enhancer

CGLIB
中用于生成新类的类。

改造一下
1.1
节的例子,可以对比看看,全部示例代码如下:

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
classHelloWorld {public voidsayHello() {
System.
out.println("Hello world!");
}
}
public classDemoApplication {public static voidmain(String[] args) {
Enhancer enhancer
= newEnhancer();//设置需要代理的类 enhancer.setSuperclass(HelloWorld.class);

enhancer.setCallback(
newMethodInterceptor() {
@Override
publicObject intercept(Object obj, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.
out.println("Before method call");
Object result
= proxy.invokeSuper(obj, args); //调用父类的方法 System.out.println("After method call");returnresult;
}
});

HelloWorld proxy
= (HelloWorld) enhancer.create(); //创建代理对象 proxy.sayHello(); //通过代理对象调用方法 }
}

运行结果如下:

CGLIB vs JDK动态代理

  • 接口要求:
    JDK
    动态代理只能代理实现了接口的对象,而
    CGLIB
    能够直接代理类。
  • 性能:
    CGLIB
    在生成代理对象时通常比
    JDK
    动态代理要慢,因为它需要动态生成新的类。但在调用代理方法时,
    CGLIB
    通常会提供更好的性能。
  • 方法限制:
    CGLIB
    不能代理
    final
    方法,因为它们不能被子类覆盖。

CGLIB
是一个强大的工具,特别适用于需要代理没有实现接口的类的场景。然而,选择
JDK
动态代理还是
CGLIB
主要取决于具体的应用场景和性能要求。

注意:在
CGLIB
中,如果使用
MethodProxy.invoke(obj, args)
,而不是
MethodProxy.invokeSuper(obj, args)
,并且
obj
是代理实例本身(
CGLIB
通过
Enhancer
创建的代理对象,而不是原始的被代理的目标对象),就会导致无限循环。
invoke
方法实际上是尝试在传递的对象上调用方法,如果该对象是代理对象,则调用会再次被拦截,造成无限循环。


  • JDK
    动态代理中,确保调用
    method.invoke
    时使用的是目标对象,而不是代理对象。


  • CGLIB
    代理中,使用
    MethodProxy.invokeSuper
    而不是
    MethodProxy.invoke
    来调用被代理的方法,以避免无限循环。

3.2 不同方法分别代理(对比JDK动态代理写法)

我们改写
1.2
节的例子

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;
classHelloWorldImpl {public voidsayHello() {
System.
out.println("Hello world!");
}
public voidsayGoodbye() {
System.
out.println("Goodbye world!");
}
}
public classDemoApplication {public static voidmain(String[] args) {
Enhancer enhancer
= newEnhancer();
enhancer.setSuperclass(HelloWorldImpl.
class); //设置被代理的类 enhancer.setCallback(newMethodInterceptor() {//添加一个简单的权限控制演示 private boolean accessAllowed = true;//简单的功能开关 private boolean goodbyeFunctionEnabled = true;

@Override
publicObject intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {//权限控制 if (!accessAllowed) {
System.
out.println("Access denied");return null; //在实际场景中,可以抛出一个异常 }//功能开关 if (method.getName().equals("sayGoodbye") && !goodbyeFunctionEnabled) {
System.
out.println("Goodbye function is disabled");return null;
}
//方法执行前的通用逻辑 System.out.println("Before method:" +method.getName());//执行方法 Object result =proxy.invokeSuper(obj, args);//方法执行后的通用逻辑 System.out.println("After method:" +method.getName());returnresult;
}
});

HelloWorldImpl proxyInstance
= (HelloWorldImpl) enhancer.create(); //创建代理对象 proxyInstance.sayHello(); //正常执行 proxyInstance.sayGoodbye(); //可以根据goodbyeFunctionEnabled变量决定是否执行 }
}

运行结果如下:

我们需要注意几点更改:

  1. 因为
    CGLIB
    不是基于接口的代理,而是通过生成目标类的子类来实现代理,所以我们不再需要接口
    HelloWorld

  2. 我们将使用
    Enhancer
    类来创建代理实例,并提供一个
    MethodInterceptor
    来处理方法调用。

3.3 熔断限流和日志监控(对比JDK动态代理写法)

我们改写
1.3
节的例子

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
classHelloWorld {voidsayHello() {
System.
out.println("Hello world!");
}
}
public classDemoApplication {public static voidmain(String[] args) {
HelloWorld realObject
= newHelloWorld();
HelloWorld proxyInstance
=(HelloWorld) createProxy(realObject);//模拟多次调用以观察限流和熔断效果 for (int i = 0; i < 10; i++) {
proxyInstance.sayHello();
}
}
public staticObject createProxy(final Object realObject) {
Enhancer enhancer
= newEnhancer();
enhancer.setSuperclass(HelloWorld.
class);
enhancer.setCallback(
newAdvancedMethodInterceptor(realObject));returnenhancer.create();
}
static classAdvancedMethodInterceptor implements MethodInterceptor {privatefinal Object target;private final AtomicInteger requestCount = new AtomicInteger(0);private final AtomicLong lastTimestamp = newAtomicLong(System.currentTimeMillis());private volatile boolean circuitBreakerOpen = false;private final long cooldownPeriod = 10000; //冷却时间10秒 publicAdvancedMethodInterceptor(Object target) {this.target =target;
}

@Override
publicObject intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {long now =System.currentTimeMillis();//检查熔断器是否应该被重置 if (circuitBreakerOpen && (now - lastTimestamp.get() >cooldownPeriod)) {
circuitBreakerOpen
= false; //重置熔断器 requestCount.set(0); //重置请求计数 System.out.println("Circuit breaker has been reset.");
}
//熔断检查 if(circuitBreakerOpen) {
System.
out.println("Circuit breaker is open. Blocking method execution for:" +method.getName());return null; //在实际场景中,可以返回一个兜底的响应或抛出异常 }//限流检查 if (requestCount.incrementAndGet() > 5) {if (now - lastTimestamp.get() < cooldownPeriod) { //10秒内超过5次请求,触发熔断 circuitBreakerOpen = true;
lastTimestamp.
set(now); //更新时间戳 System.out.println("Too many requests. Opening circuit breaker.");return null; //触发熔断时的处理 } else{//重置计数器和时间戳 requestCount.set(0);
lastTimestamp.
set(now);
}
}
//执行实际方法 Object result = proxy.invokeSuper(obj, args); //注意这里调用的是invokeSuper//方法执行后的逻辑 System.out.println("Executed method:" +method.getName());returnresult;
}
}
}

运行结果

在这个改写中,我们使用
CGLIB

Enhancer

MethodInterceptor
来代替了
JDK

Proxy

InvocationHandler

MethodInterceptor

intercept
方法与
InvocationHandler

invoke
方法在概念上是相似的,但它使用
MethodProxy

invokeSuper
方法来调用原始类的方法,而不是使用反射。这允许
CGLIB
在运行时生成代理类的字节码,而不是依赖于反射,从而提高了性能。此外,
circuitBreakerOpen
被声明为
volatile
,是确保其在多线程环境中的可见性。

4. 动态代理图示

方法调用拦截:

客户端通过代理对象调用方法,此时方法调用被代理对象拦截。

转发给处理器或方法拦截器:

代理对象将方法调用转发给一个特定的处理器,这取决于所使用的代理类型。对于
JDK
动态代理,这个处理器是
InvocationHandler
;对于
CGLIB
代理,是
MethodInterceptor

执行额外操作(调用前):

在实际执行目标对象的方法之前,处理器有机会执行一些额外的操作,例如日志记录、安全检查或事务管理等。

调用目标对象的方法:

处理器在必要时直接调用目标对象的方法。在
JDK
动态代理中,这通常通过反射实现;而在
CGLIB
中,可以通过
MethodProxy.invokeSuper
方法调用。

执行额外操作(调用后):

方法调用完成后,处理器再次有机会执行额外操作,比如修改返回值、记录执行时间或进行事务的提交或回滚。

返回给客户端:

最终,方法的返回值被通过代理对象返回给客户端。

5. JDK动态代理 VS CGLIB动态代理对比

JDK动态代理

JDK
动态代理是
Java
自带的代理机制,它直接使用反射
API
来调用方法。

优点:

  • 无需第三方依赖:作为
    Java
    标准
    API
    的一部分,使用
    JDK
    动态代理不需要添加额外的库或依赖。

  • 接口导向:强制使用接口进行代理,这符合面向接口编程的原则,有助于保持代码的清晰和灵活。

缺点:

  • 仅限接口:只能代理实现了接口的类,这在某些情况下限制了它的使用。

  • 性能开销:由于使用反射
    API
    进行方法调用,可能会有一定的性能开销,尤其是在大量调用时。

CGLIB动态代理

CGLIB

Code Generation Library
)通过在运行时生成被代理对象的子类来实现代理。

优点:

  • 不需要接口:可以代理没有实现任何接口的类,这提供了更大的灵活性。

  • 性能较好:通常认为
    CGLIB
    的性能比
    JDK
    动态代理要好,特别是在代理方法的调用上,因为
    CGLIB
    使用了字节码生成技术,减少了使用反射的需要。

缺点:

  • 第三方库:需要添加
    CGLIB
    库作为项目依赖。

  • 无法代理final方法:由于
    CGLIB
    是通过生成子类的方式来代理的,所以无法代理那些被声明为
    final
    的方法。

性能比较

  • 调用速度:
    CGLIB
    在代理方法调用方面通常比
    JDK
    动态代理更快。这是因为
    CGLIB
    通过直接操作字节码来生成新的类,避免了反射带来的性能开销。

  • 启动性能:
    CGLIB
    在生成代理对象时可能会比
    JDK
    动态代理慢,因为它需要在运行时生成新的字节码。如果代理对象在应用启动时就被创建,这可能会略微影响启动时间。

选择建议

  • 如果类已经实现了接口,或者希望强制使用接口编程,那么
    JDK
    动态代理是一个好选择。

  • 如果需要代理没有实现接口的类,或者对性能有较高的要求,特别是在代理方法的调用上,
    CGLIB
    可能是更好的选择。

  • 在现代的
    Java
    应用中,很多框架(如
    Spring
    )都提供了对这两种代理方式的透明支持,并且可以根据实际情况自动选择使用哪一种。例如,
    Spring AOP
    默认会使用
    JDK
    动态代理,但如果遇到没有实现接口的类,它会退回到
    CGLIB

6. 动态代理的实际应用场景

面向切面编程(
AOP
):

  • 问题解决:在不改变原有业务逻辑代码的情况下,为程序动态地添加额外的行为(如日志记录、性能监测、事务管理等)。

  • 应用实例:
    Spring AOP
    使用动态代理为方法调用提供了声明式事务管理、安全性检查和日志记录等服务。根据目标对象是否实现接口,
    Spring AOP
    可以选择使用
    JDK
    动态代理或
    CGLIB
    代理。

事务管理:

  • 问题解决:自动化处理数据库事务的边界,如开始、提交或回滚事务。

  • 应用实例:
    Spring
    框架中的声明式事务管理使用代理技术拦截那些被
    @Transactional
    注解标记的类或方法,确保方法执行在正确的事务管理下进行。

权限控制和安全性:

  • 问题解决:在执行敏感操作之前自动检查用户权限,确保只有拥有足够权限的用户才能执行某些操作。

  • 应用实例:企业应用中,使用代理技术拦截用户的请求,进行权限验证后才允许访问特定的服务或执行操作。

延迟加载:

  • 问题解决:对象的某些属性可能加载成本较高,通过代理技术,可以在实际使用这些属性时才进行加载。

  • 应用实例:
    Hibernate
    和其他
    ORM
    框架使用代理技术实现了延迟加载(懒加载),以提高应用程序的性能和资源利用率。

服务接口调用的拦截和增强:

  • 问题解决:对第三方库或已有服务进行包装,添加额外的逻辑,如缓存结果、参数校验等。

  • 应用实例:在微服务架构中,可以使用代理技术对服务客户端进行增强,实现如重试、熔断、限流等逻辑。

在现代框架中的应用

  • Spring框架:
    Spring

    AOP
    模块和事务管理广泛使用了动态代理技术。根据目标对象的类型(是否实现接口),
    Spring
    可以自动选择
    JDK
    动态代理或
    CGLIB
    代理。

  • Hibernate:
    Hibernate
    使用动态代理技术实现懒加载,代理实体类的关联对象,在实际访问这些对象时才从数据库中加载它们的数据。

  • MyBatis:
    MyBatis
    框架使用动态代理技术映射接口和
    SQL
    语句,允许开发者通过接口直接与数据库交互,而无需实现类。

欢迎一键三连~

有问题请留言,大家一起探讨学习

点击关注,第一时间了解华为云新鲜技术~

原因:我之所以想做这个项目,是因为在之前查找关于C#/WPF相关资料时,我发现讲解图像滤镜的资源非常稀缺。此外,我注意到许多现有的开源库主要基于CPU进行图像渲染。这种方式在处理大量图像时,会导致CPU的渲染负担过重。因此,我将在下文中介绍如何通过GPU渲染来有效实现图像的各种滤镜效果。

生成的效果



生成效果的方法:我主要是通过参考Shazzam Shader Editor来编写HLSL像素着色器。

HLSL(High Level Shader Language,高级着色器语言)是Direct3D着色器模型所需的一种语言。WPF不仅支持Direct3D 9,还支持使用HLSL来创建着色器。虽然可以使用多种编辑器来编写HLSL,但Shazzam Shader Editor是一款专为WPF设计的编辑器,它专门用于实现像素着色器。使用Shazzam可以简化将像素着色器集成到WPF项目中所需的各种手动操作。(关于如何使用Shazzam,可在线查找详细教程。)

在我的项目中,我根据所需的效果生成相应的.PS和.CS文件,并将这些文件添加到类库中。接着,我会在具体的项目中引入这个库来实现效果

项目实现细节

开发环境

  • 使用的MVVM库:CommunityToolkit.Mvvm
  • 目标框架:.NET 8.0
  • 开发工具:Visual Studio 2022

使用的样式库

项目中采用了AduSkin样式库。
个人建议:​
我不特别推荐使用AduSkin,主要是因为它缺乏官方文档,需要通过查看源代码来学习使用。此外,一些样式的命名与源代码中的不一致,这可能会导致一些困惑。(备注:我最初选择使用AduSkin是因为其UI设计在网络上获得了好评,尽管某些效果确实很吸引人,但缺少文档导致使用上的不便。)

项目结构概述

项目在构建过程中,考虑到几种特效之间存在一些共同的重复元素,如图片展示和图片导入功能,因此我将这些共用功能模块化。

  • 操作区域的定制化:​
    对于每种不同的特效,操作区的需求也不尽相同。在Common控件中,我使用了Option控件来进行替代,以便于在外部进行定制。
  • 特效的动态调整:​
    每种特效的具体实现都有所不同,因此我设定了一个独立的属性
    ImageEffect
    ,允许从外部动态修改。
  • 公共控件:
    CommonEffectControl
    作为一个公共控件,用于整合图片显示和图片导入的共通功能。

具体引用的步骤

需要添加命令空间

  • xmlns:common="clr-namespace:CT.WPF.MagicEffects.Demo.UserControls.Common"

前台代码

查看代码

 <common:CommonEffectControl ImageEffect="{Binding SelectedOrdinary.ObjectShaderEffect}">
     <common:CommonEffectControl.Option>
         <StackPanel Orientation="Vertical">
             <Border BorderBrush="Transparent" BorderThickness="0">
                 <Skin:MetroScrollViewer ScrollViewer.VerticalScrollBarVisibility="Visible">
                     <ListView
                         Width="240"
                         Height="550"
                         BorderThickness="0"
                         ItemsSource="{Binding Ordinarys}"
                         ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                         ScrollViewer.VerticalScrollBarVisibility="Hidden"
                         SelectedItem="{Binding SelectedOrdinary, Mode=TwoWay}"
                         SelectionMode="Single">
                         <ListView.ItemTemplate>
                             <DataTemplate>
                                 <StackPanel
                                     MinHeight="110"
                                     VerticalAlignment="Center"
                                     Orientation="Vertical">
                                     <Viewbox
                                         Width="99"
                                         Height="78"
                                         Margin="2">
                                         <Image Effect="{Binding ObjectShaderEffect}" Source="{Binding Path=Main.SelectedImagePath, Source={StaticResource Locator}}" />
                                     </Viewbox>
                                     <TextBlock
                                         HorizontalAlignment="Center"
                                         FontSize="14"
                                         Foreground="{Binding RelativeSource={RelativeSource AncestorType={x:Type Skin:MetroWindow}}, Path=BorderBrush, Mode=TwoWay}"
                                         Text="{Binding Title}"
                                         TextWrapping="Wrap" />
                                 </StackPanel>
                             </DataTemplate>
                         </ListView.ItemTemplate>
                         <ListView.ItemsPanel>
                             <ItemsPanelTemplate>
                                 <WrapPanel Orientation="Horizontal" />
                             </ItemsPanelTemplate>
                         </ListView.ItemsPanel>
                     </ListView>

                 </Skin:MetroScrollViewer>
             </Border>
         </StackPanel>
     </common:CommonEffectControl.Option>
 </common:CommonEffectControl>

ViewModel部分

查看代码

   partial class OrdinaryEffectViewModel : ObservableObject {
      public OrdinaryEffectViewModel() {
          Ordinarys = new ObservableCollection<Ordinarys> {
              new Ordinarys(){ Title="灰度", ObjectShaderEffect= new GrayScaleEffect ()},
              new Ordinarys(){ Title="位移", ObjectShaderEffect= new DirectionalBlurEffect ()},
              new Ordinarys(){ Title="老电影", ObjectShaderEffect= new OldMovieEffect ()},
              new Ordinarys(){ Title="锐化", ObjectShaderEffect= new SharpenEffect ()},
          };
      }
      [ObservableProperty]
      private ObservableCollection<Ordinarys> ordinarys;
      [ObservableProperty]
      private Ordinarys selectedOrdinary;
  }

model部分

查看代码

  partial class Ordinarys : ObservableObject {
     [ObservableProperty]
     private string? title;
     [ObservableProperty]
     private ShaderEffect? objectShaderEffect;
 }

还有摄像头滤镜覆盖的效果欢迎大家体验!!!

已经发布nuget:
dotnet add package MagicEffects --version 1.0.0

github:
CT.WPF.MagicEffects