不懂Nacos没关系,可以看看它是怎么运用代理模式的
背景
看Nacos的源代码时,发现其中有对代理模式的运用,而且用得还不错,可以作为一个典型案例来聊聊,方便大家以更真实的案例来体验一下代理模式的运用。如果你对Nacos不了解,也并不影响对本篇文章的阅读和学习。
本文涉及知识点:代理模式的定义、代理模式的运用场景、Nacos的服务注册、静态代理模式、动态代理模式、Cglib动态代理、Spring中AOP所使用的代理等。
何谓代理模式
代理模式(Proxy Pattern)是一种结构型设计模式,通常使用代理对象来执行目标对象的方法并在代理对象中增强目标对象的方法。
定义有一些绕口,举个生活中的简单例子:你去租房,可以直接找房东,也可以找中介。而代理模式就是你租房不用找房东,通过中介来租,而中介呢,不仅仅能够提供房屋出租服务(目标对象的方法),还可以提供房屋清洁的服务(对目标对象方法的增强)。
在上述例子中,中介是代理对象,房东是目标对象(或委托对象),中介为房东提供了出租的功能,在出租的功能上代理又可以提供增强的房屋清洁功能。
为什么要使用代理模式呢?
原因有二:
- 中介隔离作用:在上述例子中,无论是因为客户嫌直接找房东麻烦,还是房东嫌出租客户麻烦,中间都需要一个专门的角色来处理这事,它就是代理。也就是说,客户类不想或者不能直接引用一个委托对象,代理对象就可以在二者之间起到中介的作用。
- 开闭原则:在上面的例子中,房东只想出租房屋,而租户租房时还想享受清洁服务,而这个清洁服务就需要通过代理类来处理。这样不用直接在房东出租功能上修改(新增)清洁服务,仅通过代理类就可以完成,符合开闭原则。上面的例子是提供一些特定的服务,在实践中,像鉴权、计时、缓存、日志、事务处理等一些公共服务都可以在代理类中完成。
代理模式的分类
代理模式通常可分为两类:静态代理和动态代理。动态代理的实现又有JDK动态代理和CGLIB动态代理两种实现方式。
静态代理是由开发人员直接编写代理类,代理类和委托类之间的关系在运行前已经确定好的。当需要修改或屏蔽一个或若干类的部分功能,复用另一部分功能时,可使用静态代理。
动态代理的代理类是在运行时期间由编译器动态生成(比如,JVM的反射机制生成代理类),在运行时确定代理类和委托类之间的关系。当需要拦截一批类中的某些方法,在方法前后加入一些公共操作时,可使用动态代理。
静态代理
在Nacos中服务注册接口使用的代理模式为静态代理。静态代理模式需要先定义接口,委托类和代理类一起实现该接口,然后通过调用代理类对应的方法间接调用委托类的对应方法。
常见的静态代理类数据模型如下:
上图中通过代理类对委托类的方法进行拓展,在方法执行前后新增一些逻辑处理,比如日志、计时等,这是最简单的一种代理模式实现。
在Nacos中静态代理模式运用的场景是客户端实例向Nacos的注册、注销等操作。由于实例的注册方式支持临时实例和持久实例两种方式,代理类就起到了判断到底是采用临时实例注册服务,还是使用持久实例注册服务。
下面直接以Nacos相关源码来进行解析说明。
第一步,定义接口,静态代理是需要先定义一个共同的实现接口的。
public interface ClientOperationService {
/**
* Register instance to service.
*
*/
void registerInstance(Service service, Instance instance, String clientId) throws NacosException;
// ...
}
在Nacos中定义了一个ClientOperationService
的接口,其中提供了实例的注册、注销等功能,这里为了方便阅读,仅展示注册实例代码(后续代码相同)。
第二步,定义两个委托类,一个委托类实现临时实例注册,一个委托类实现持久实例注册。
@Component("ephemeralClientOperationService")
public class EphemeralClientOperationServiceImpl implements ClientOperationService {
@Override
public void registerInstance(Service service, Instance instance, String clientId) throws NacosException {
// ... 临时实例注册逻辑实现
}
// ...
}
@Component("persistentClientOperationServiceImpl")
public class PersistentClientOperationServiceImpl extends RequestProcessor4CP implements ClientOperationService {
@Override
public void registerInstance(Service service, Instance instance, String clientId) {
// ... 永久实例注册逻辑实现
}
// ...
}
EphemeralClientOperationServiceImpl
类为临时实例操作服务实现,实现了ClientOperationService
接口。PersistentClientOperationServiceImpl
类为永久实例操作服务实现,同样实现了ClientOperationService
接口。
第三步,定义代理类。通常情况下,一个代理类代理一个委托类,但在Nacos中,代理类实现了区分到底是临时实例还是永久实例的逻辑,因此代理类同时代理了上述两个委托类。
@Component
public class ClientOperationServiceProxy implements ClientOperationService {
private final ClientOperationService ephemeralClientOperationService;
private final ClientOperationService persistentClientOperationService;
public ClientOperationServiceProxy(EphemeralClientOperationServiceImpl ephemeralClientOperationService,
PersistentClientOperationServiceImpl persistentClientOperationService) {
this.ephemeralClientOperationService = ephemeralClientOperationService;
this.persistentClientOperationService = persistentClientOperationService;
}
@Override
public void registerInstance(Service service, Instance instance, String clientId) throws NacosException {
final ClientOperationService operationService = chooseClientOperationService(instance);
operationService.registerInstance(service, instance, clientId);
}
private ClientOperationService chooseClientOperationService(final Instance instance) {
return instance.isEphemeral() ? ephemeralClientOperationService : persistentClientOperationService;
}
// ...
}
代理类ClientOperationServiceProxy
通过构造方法传入了两个委托类,通过chooseClientOperationService
方法根据参数来判断具体使用哪个委托类,从而实现了在registerInstance
方法中,根据参数动态的判断注册实例的方式。
Nacos的代理模式实现,符合我们前面提到的“客户类不想或者不能直接引用一个委托对象”的场景,这里是(每个)客户类“不想”每次调用时都判断采用何种方式注册,从而把这个判断逻辑交给了代理类才进行处理。
像Nacos中的这种实现就属于静态代理模式,在程序运行之前,已经通过代码实现了具体的代理类实现。静态代理的优点非常明显,可以在不改变目标对象的前提下,扩展目标对象的功能。
但缺点也同样明显:
- 重复性:如果需要代理的业务或方法越多,则重复的模板代码就越多;
- 脆弱性:一旦目标对象(接口)的方法有所变动,比如新增接口,代理对象和目标对象需要同时修改。如果目标对象有多个代理对象,影响范围可想而知。
JDK动态代理
静态代理是在编码阶段已经把代理类实现好了,那么是否可以在运行时动态构建代理类,来实现代理的功能呢?JDK动态代理便提供了这样的功能。
需要注意的是,JDK动态代理并不等价于动态代理,它只是动态代理的实现方式之一,即我们后面要讲到的Cglib动态代理也是动态代理的实现之一。
使用JDK动态代理时,代理对象不需要再实现接口,而目标对象依旧需要实现接口。使用JDK动态代理时需要用到两个类:java.lang.reflect.Proxy
和 java.lang.reflect.InvocationHandler
。
下面以用户登录时,在登录操作前后打印日志为例,体验一下JDK动态代理的功能。
第一步,创建业务接口。
public interface UserService {
void login(String username, String password);
}
第二步,创建业务实现类。
public class UserServiceImpl implements UserService{
@Override
public void login(String username, String password) {
System.out.println("User Login Service!");
}
}
第三步,创建业务逻辑处理器,实现InvocationHandler
接口。
public class LogHandler implements InvocationHandler {
/**
* 被代理的对象,实际的方法执行者
*/
Object target;
public LogHandler(Object object) {
this.target = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before Login---");
// 调用target的method方法
Object result = method.invoke(target, args);
System.out.println("After Login---");
return result;
}
}
这里我们编写了一个LogHandler类,实现InvocationHandler接口,重写invoke方法。
invoke方法中定义了代理对象调用方法时希望执行的动作,用于集中处理在动态代理类对象上的方法调用。
这里,在执行目标类方法前后可添加对应的日志信息打印或其他操作,在上述代码中分别打印了“Before Login”和“After Login”的信息。
第四步,模拟客户端使用。
public class JdkProxyTest {
public static void main(String[] args) {
// 创建被代理的对象,UserService接口的实现类
UserServiceImpl userService = new UserServiceImpl();
// 创建代理对象,包含三个参数:ClassLoader、目标类实现接口数组、事件处理器
UserService userProxy = (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(),
userService.getClass().getInterfaces(),
new LogHandler(userService));
userProxy.login("admin", "123456");
}
}
在上述测试类中,先创建了被代理类的对象,然后通过Proxy的newProxyInstance方法构建了代理对象,生成的代理对象实现了目标类的所有接口,并对接口的方法进行了代理。
当我们通过代理对象调用具体方法时,底层将通过反射,调用我们实现的invoke方法,最后通过调用目标对象的登录方法。
执行上述方法,控制台打印日志如下:
Before Login---
User Login Service!
After Login---
可以看到,在登录操作前后,打印了对应的日志。
在构建代理对象时,用到了Proxy的newProxyInstance方法,该方法接收三个参数:
ClassLoader loader
:指定当前目标对象使用类加载器,获取加载器的方法是固定的。Class<?>[] interfaces
:目标对象实现的接口的类型,使用泛型方式确认类型。InvocationHandler h
:事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入。
通过上述方式,我们实现了基于JDK的动态代理。JDK动态代理有以下特点:
- 通过实现InvocationHandler接口完成代理逻辑,所有函数调用都经过invoke函数转发,可在此进行自定义操作,比如日志系统、事务、拦截器、权限控制等。
- 通过反射代理方法,比较消耗系统性能,但可以减少代理类的数量,使用更灵活。
- 代理类必须实现接口。
可以看出,JDK动态代理的一个致命缺点就是目标类必须实现某个接口。而要解决这个问题,可以通过Cglib代理来实现,我们后面会具体讲到。
JDK动态代理类
在上述实践的过程中,我们是否考虑过,通过JDK动态代理生成的代理类到底是什么样子呢?我们通过下面的工具类,可以一探究竟。
public class ProxyUtils {
/**
* 将根据类信息动态生成的二进制字节码保存到硬盘中,默认的是clazz目录下
* params: clazz 需要生成动态代理类的类
* proxyName: 为动态生成的代理类的名称
*/
public static void generateClassFile(Class clazz, String proxyName) {
// 根据类信息和提供的代理类名称,生成字节码
byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, clazz.getInterfaces());
String paths = clazz.getResource(".").getPath();
System.out.println(paths);
try (FileOutputStream out = new FileOutputStream(paths + proxyName + ".class")) {
//保留到硬盘中
out.write(classFile);
out.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
上面代码定义了一个将代理类保持到磁盘中的工具类。然后,在JdkProxyTest类的最后,调用该方法,将JDK动态生成的代理类打印出来。
public class JdkProxyTest {
public static void main(String[] args) {
// 创建被代理的对象,UserService接口的实现类
UserServiceImpl userService = new UserServiceImpl();
// 创建代理对象,包含三个参数:ClassLoader、目标类实现接口数组、事件处理器
UserService userProxy = (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(),
userService.getClass().getInterfaces(),
new LogHandler(userService));
userProxy.login("admin", "123456");
// 保存JDK动态代理生成的代理类,类名保存为 UserServiceProxy
ProxyUtils.generateClassFile(userService.getClass(), "UserServiceProxy");
}
}
其他代码未变,最后一行添加了工具类ProxyUtils的调用。
执行上述代码,会在项目目录的target下生成名为“UserServiceProxy”的class文件。本人执行时,打印的路径为“…/target/classes/com/secbro2/proxy/”。
在该目录下找到UserServiceProxy.class类文件,通过IDE的反编译功能,可看到如下代码:
public final class UserServiceProxy extends Proxy implements UserService {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
public UserServiceProxy(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final void login(String var1, String var2) throws {
try {
super.h.invoke(this, m3, new Object[]{var1, var2});
} catch (RuntimeException | Error var4) {
throw var4;
} catch (Throwable var5) {
throw new UndeclaredThrowableException(var5);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("com.secbro2.proxy.UserService").getMethod("login", Class.forName("java.lang.String"), Class.forName("java.lang.String"));
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
从反编译的代理类中,我们可以得到以下信息:
- UserServiceProxy继承了Proxy类,实现了UserService接口,当然接口中定义的login方法也同样实现了。同时,还实现了equals、hashCode、toString等方法。
- 由于UserServiceProxy继承了Proxy类,所以每个代理类都会关联一个InvocationHandler方法调用处理器。
- 类和所有方法都被
public final
修饰,所以代理类只可被使用,不可以再被继承。 - 每个方法都有一个 Method对象来描述,Method对象在static静态代码块中创建,以
m + 数字
的格式命名。 - 调用方法时通过
super.h.invoke(this, m1, (Object[])null);
调用,其中的super.h.invoke
实际上是在创建代理时传递给Proxy.newProxyInstance
的LogHandler对象,它继承InvocationHandler类,负责实际的调用处理逻辑。
而LogHandler的 invoke 方法接收到method、args 等参数后,进行一些处理,然后通过反射让被代理的对象 target 执行方法。
至此,我们已经了解了基于JDK动态代理的使用以及所生成代理类的结构,下面就来看看无需目标类实现接口的Cglib动态代理实现。
Cglib动态代理
在上面的实例中可以看到无论使用静态代理或是JDK动态代理,目标类都需要实现一个接口。在某些情况下,目标类可能并没有实现接口,这时就可以使用Cglib动态代理。
Cglib(Code Generation Library)是一个功能强大、高性能、开源的代码生成包,它可以为没有实现接口的类提供代理。
Cglib代理可以称为子类代理,具体而言,Cglib会在内存中构建一个目标类的子类,重写其业务方法,从而实现对目标对象功能的扩展。因为采用继承机制,所以不能对final修饰的类进行代理。
Cglib通过Enhancer
类来生成代理类,通过实现MethodInterceptor
接口,在其intercept
方法中对目标对象的方法进行增强,并可通过Method或MethodProxy继承类来调用原有方法。
这次以下订单(OrderService)为例来展示一下通过Cglib在下订单操作前后添加日志信息。
在使用Cglib之前,首先需要引入对应的依赖jar包,大多数项目中往往Cglib已经被间接引入了,可核实其版本是否是预期版本。这里采用Maven形式,引入Cglib依赖。
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.1</version>
</dependency>
第一步,定义业务类OrderService,不需要实现任何接口。
public class OrderService {
public void order(String orderNo){
System.out.println("order something... ");
}
}
第二步,定义动态代理类的创建及业务实现。
/**
* 动态代理类,实现方法拦截器接口
**/
public class LogInterceptor implements MethodInterceptor {
/**
* 给目标对象创建一个代理对象
*/
public Object getProxyInstance(Class targetClass){
// 1.工具类
Enhancer enhancer = new Enhancer();
// 2.设置父类
enhancer.setSuperclass(targetClass);
// 3.设置回调函数
enhancer.setCallback(this);
// 4.创建子类(代理对象)
return enhancer.create();
// 上述方法也可以直接使用如下代码替代
// return Enhancer.create(targetClass,this);
}
/**
*
* @param o 要进行增强的对象
* @param method 拦截的方法
* @param objects 方法参数列表(数组)
* @param methodProxy 方法的代理,invokeSuper方法表示对被代理对象方法的调用
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 扩展日志记录
System.out.println("LogInterceptor:Before Login---");
// 注意:调用的invokeSuper而不是invoke,否则死循环。
// methodProxy.invokeSuper执行的是原始类的方法,method.invoke执行的是子类的方法
Object object = methodProxy.invokeSuper(o, objects);
// 扩展日志记录
System.out.println("LogInterceptor:After Login---");
return object;
}
}
LogInterceptor类实现了MethodInterceptor接口,在重写的intercept方法中添加了要扩展的业务内逻辑。其中需要注意的是,intercept方法内调用的是MethodProxy#invokeSuper方法,而不是invoke方法。
同时,在LogInterceptor类中定义了创建目标对象的代理对象的工具方法getProxyInstance,值得留意的是Enhancer#setCallback方法的参数this,指的便是LogInterceptor的当前对象。
第三步,编写测试客户端。
public class CglibTest {
public static void main(String[] args) {
OrderService orderService = (OrderService) new LogInterceptor().getProxyInstance(OrderService.class);
orderService.order("123");
}
}
执行上述方法,打印日志如下:
LogInterceptor:Before Login---
order something...
LogInterceptor:After Login---
成功的在目标对象的方法前后植入日志信息。
关于Cglib动态代理有以下特点:
- 需要引入Cglib的依赖jar包,通常Spring的核心包已包含Cglib功能。
- Cglib动态代理不需要接口信息,但是它拦截并包装被代理类的所有方法。
- 委托类不能为final,否则报错java.lang.IllegalArgumentException: Cannot subclass final class xxx。
- 不会拦截委托类中无法重载的final/static方法,而是跳过此类方法只代理其他方法。
- 实现 MethodInterceptor接口,用来处理对代理类上所有方法的请求。
三种代理对比
静态代理:代理类和目标类都需要实现接口,从而达到代理增强其功能。
JDK动态代理:基于Java反射机制实现,目标类必须实现接口才能生成代理对象。使用Proxy.newProxyInstance
方法生成代理类,并实现InvocationHandler
中的invoke
方法,实现增强功能。
Cglib动态代理:基于ASM机制实现,通过生成目标类的子类作为代理类。无需实现接口,使用Cblib
中的Enhancer
来生成代理对象子类,并实现MethodInterceptor
的intercept
方法来实现增强功能。
JDK动态代理的优势:JDK自身支持,减少依赖,可随着JDK平滑升级,代码实现简单。
Cglib动态代理的优势:无需实现接口,达到无侵入;只操作我们关心的类,而不必为其他相关类增加工作量;
Spring中动态代理支持
Spring的AOP实现中主要应用了JDK动态代理以及Cglib动态代理,对应的实现类位于spring-aop的jar包中。
// 基于JDK的动态代理实现类
org.springframework.aop.framework.JdkDynamicAopProxy
// 基于Cglib的动态代理实现类
org.springframework.aop.framework.CglibAopProxy
Spring默认使用JDK动态代理实现AOP,类如果实现了接口,Spring就会使用这种方式的动态代理。如果目标对象没有实现接口,则需要使用Cglib动态代理来实现。
在了解了JDK动态代理及Cglib动态代理的使用及特性之后,大家可以对照思考一下Spring事务失效的一些场景,Spring的事务实现便是基于AOP来实现的,比如:
- 方法使用private定义,导致事务失效:被代理方法必须是public。
- 方法使用final修饰:如果方法被定义为final,JDK动态代理或Cglib无法重写该方法。
- 同一类内部方法调用:直接使用this对象调用方法,无法生成代理方法,会导致事务失效。
关于Spring中动态代理的其他内容,本文就不再展开了,感兴趣的读者可直接阅读对应的源码。
小结
本文从Nacos中的静态代理模式实现,延伸拓展讲解了代理模式的定义、代理模式的运用场景、静态代理模式、动态代理模式、Cglib动态代理、Spring中AOP所使用的代理等。
通过文章中关联的知识点,以及在不同跨度的项目中的实践案例,大家应该能够感知到到代理模式,特别是基于JDK动态代理和Cglib动态代理在实践中的重要性。抓紧学一波吧。
关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台
除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接
本文链接:https://choupangxia.com/2022/12/26/nacos-proxy-pattern/