It doesn't matter if you don't understand Nacos, you can see how it uses the proxy mode

background

When looking at the source code of Nacos, I found that there is an application of the proxy mode , and it is used well. It can be used as a typical case to talk about, so that everyone can experience the application of the proxy mode with a more real case. If you don't know about Nacos, it doesn't affect your reading and learning of this article.

This article involves knowledge points: definition of proxy mode, application scenarios of proxy mode, service registration of Nacos, static proxy mode, dynamic proxy mode, Cglib dynamic proxy, proxy used by AOP in Spring, etc.

What is proxy mode

Proxy Pattern (Proxy Pattern) is a structural design pattern that usually uses a proxy object to execute the method of the target object and enhance the method of the target object in the proxy object.

There are some twists and turns in the definition. Let me give you a simple example in life: when you rent a house, you can go to the landlord directly, or you can go to an intermediary. The agency model is that you do not need to find a landlord to rent a house, but rent through an intermediary, and the intermediary can not only provide house rental services (the method of the target object), but also provide house cleaning services (enhancement of the method of the target object).

In the above example, the intermediary is the proxy object, and the landlord is the target object (or entrusted object). The intermediary provides the rental function for the landlord, and the agent can provide enhanced house cleaning functions in terms of the rental function.

Why use proxy mode?

There are two reasons:

  • The role of intermediary isolation: In the above example, whether it is because the customer thinks it is troublesome to go directly to the landlord, or the landlord thinks it is troublesome to rent out the client, a special role is needed to handle this matter, which is the agent. That is to say, the client class does not want or cannot directly refer to a delegate object, and the proxy object can act as an intermediary between the two.
  • Open-closed principle: In the above example, the landlord only wants to rent out the house, and the tenant also wants to enjoy the cleaning service when renting the house, and this cleaning service needs to be handled through the proxy class. In this way, there is no need to directly modify (add) the cleaning service on the landlord’s rental function, and it can be completed only through the proxy class, which conforms to the principle of opening and closing. The above example is to provide some specific services. In practice, some public services such as authentication, timing, cache, log, transaction processing, etc. can be completed in the proxy class.

Classification of Proxy Patterns

Proxy patterns can generally be divided into two categories: static proxies and dynamic proxies . There are two ways to implement dynamic proxy : JDK dynamic proxy and CGLIB dynamic proxy .

The static proxy is written by the developer directly, and the relationship between the proxy class and the delegate class has been determined before running. When it is necessary to modify or shield some functions of one or several classes and reuse another part of functions, static proxy can be used.

The proxy class of the dynamic proxy is dynamically generated by the compiler during runtime (for example, the reflection mechanism of the JVM generates a proxy class), and the relationship between the proxy class and the delegate class is determined at runtime. When it is necessary to intercept some methods in a batch of classes and add some public operations before and after the methods, dynamic proxy can be used.

static proxy

The proxy mode used by the service registration interface in Nacos is static proxy. The static proxy mode needs to define an interface first, and the delegate class and the proxy class implement the interface together, and then indirectly call the corresponding method of the delegate class by calling the corresponding method of the proxy class.

Common static proxy class data models are as follows:

Static proxy (picture source network)

In the figure above, the method of the delegate class is extended through the proxy class, and some logic processing is added before and after the execution of the method, such as logging, timing, etc. This is the simplest implementation of the proxy mode.

The scenario where the static proxy mode is used in Nacos is the registration and logout of the client instance to Nacos. Since the instance registration method supports both temporary instance and persistent instance, the proxy class can judge whether to use the temporary instance registration service or the persistent instance registration service.

The following directly analyzes and explains with the relevant source code of Nacos.

The first step is to define the interface. The static proxy needs to define a common implementation interface first.

public interface ClientOperationService {
    
    /**
     * Register instance to service.
     *
     */
    void registerInstance(Service service, Instance instance, String clientId) throws NacosException;
    
    // ...
}

An interface is defined in Nacos ClientOperationService, which provides functions such as registration and cancellation of instances. For the convenience of reading, only the registration instance code is shown here (the subsequent codes are the same).

The second step is to define two delegate classes, one delegate class implements temporary instance registration, and the other delegate class implements persistent instance registration.

@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) {
       // ... 永久实例注册逻辑实现
    }
    // ...
}    

EphemeralClientOperationServiceImplThe class implements ClientOperationServicethe interface for the temporary instance operation service. PersistentClientOperationServiceImplThe class implements the permanent instance operation service and also implements ClientOperationServicethe interface.

The third step is to define the proxy class. Usually, a proxy class represents a delegate class, but in Nacos, the proxy class realizes the logic of distinguishing whether it is a temporary instance or a permanent instance, so the proxy class represents the above two delegate classes at the same time.

@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;
    }
    // ...
}

The proxy class ClientOperationServiceProxypasses in two delegate classes through the construction method, and uses chooseClientOperationServicethe method to determine which delegate class to use according to the parameters, thus realizing registerInstancethe way of dynamically judging the registration instance according to the parameters in the method.

Nacos's proxy mode implementation is in line with the scenario we mentioned earlier that "the client class does not want or cannot directly reference a delegate object". Here is (each) client class " doesn't want " to judge which way to register every time it is called. Thus, the judgment logic is handed over to the proxy class for processing.

The implementation in Nacos belongs to the static proxy mode. Before the program runs, the specific proxy class implementation has been implemented through code. The advantage of the static proxy is very obvious, it can extend the function of the target object without changing the target object.

But the disadvantages are equally obvious:

  • Repetition: If there are more business or methods that need to be proxied, the more duplicate template codes will be;
  • Fragility: Once the method of the target object (interface) changes, such as adding an interface, the proxy object and the target object need to be modified at the same time. If the target object has multiple proxy objects, the scope of influence can be imagined.

JDK dynamic proxy

Static proxy means that the proxy class has been implemented in the coding stage, so is it possible to dynamically build the proxy class at runtime to realize the proxy function? JDK dynamic proxy provides such a function.

It should be noted that JDK dynamic proxy is not equivalent to dynamic proxy, it is only one of the implementation methods of dynamic proxy, that is, the Cglib dynamic proxy we will talk about later is also one of the implementations of dynamic proxy.

When using JDK dynamic proxy, the proxy object does not need to implement the interface, but the target object still needs to implement the interface. Two classes are required when using JDK dynamic proxy: java.lang.reflect.Proxyand java.lang.reflect.InvocationHandler.

Let's take the log printing before and after the login operation when the user logs in as an example to experience the function of the JDK dynamic proxy.

The first step is to create a business interface.

public interface UserService {
	void login(String username, String password);
}

The second step is to create a business implementation class.

public class UserServiceImpl implements UserService{

	@Override
	public void login(String username, String password) {
		System.out.println("User Login Service!");
	}
}

The third step is to create a business logic processor and implement InvocationHandlerthe interface.

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;
	}
}

Here we write a LogHandler class, implement the InvocationHandler interface, and rewrite the invoke method.

The invoke method defines the action that the proxy object wants to perform when calling the method, and is used to focus on the method invocation on the dynamic proxy class object.

Here, you can add corresponding log information printing or other operations before and after executing the target class method. In the above code, the information of "Before Login" and "After Login" are printed respectively.

The fourth step is to simulate the use of the client.

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");
   }
}

In the above test class, the object of the proxy class is created first, and then the proxy object is constructed through the newProxyInstance method of Proxy. The generated proxy object implements all the interfaces of the target class and proxies the methods of the interface.

When we call a specific method through the proxy object, the bottom layer will call the invoke method we implemented through reflection, and finally call the login method of the target object.

Execute the above method, the console prints the log as follows:

Before Login---
User Login Service!
After Login---

It can be seen that the corresponding logs are printed before and after the login operation.

When building a proxy object, the newProxyInstance method of Proxy is used, which receives three parameters:

  • ClassLoader loader: Specifies that the current target object uses the class loader, and the method of obtaining the loader is fixed.
  • Class<?>[] interfaces: The type of the interface implemented by the target object, using the generic method to confirm the type.
  • InvocationHandler h: Event processing, when the method of the target object is executed, the method of the event handler will be triggered, and the method of the currently executing target object will be passed in as a parameter.

Through the above method, we have realized the dynamic agent based on JDK. JDK dynamic proxy has the following characteristics:

  • The proxy logic is completed by implementing the InvocationHandler interface. All function calls are forwarded through the invoke function, where custom operations can be performed, such as log systems, transactions, interceptors, and permission control.
  • Through the reflection proxy method, the system performance is relatively consumed, but the number of proxy classes can be reduced and the use is more flexible.
  • The proxy class must implement the interface .

It can be seen that a fatal shortcoming of JDK dynamic proxy is that the target class must implement a certain interface. To solve this problem, it can be realized through Cglib proxy, which we will talk about later.

JDK dynamic proxy class

In the process of the above practice, have we considered what the proxy class generated by the JDK dynamic proxy looks like? We can find out through the following tool classes.

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();
      }
   }
}

The above code defines a utility class that persists the proxy class to disk. Then, at the end of the JdkProxyTest class, call this method to print out the proxy class dynamically generated by the 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");

	}
}

Other codes remain unchanged, and the last line adds a call to the tool class ProxyUtils.

Executing the above code will generate a class file named "UserServiceProxy" under the target of the project directory. When I execute it, the printed path is ".../target/classes/com/secbro2/proxy/".

Find the UserServiceProxy.class class file in this directory. Through the decompilation function of the IDE, you can see the following code:

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());
        }
    }
}

From the decompiled proxy class, we can get the following information:

  • UserServiceProxy inherits the Proxy class and implements the UserService interface. Of course, the login method defined in the interface is also implemented. At the same time, methods such as equals, hashCode, and toString are also implemented.
  • Since UserServiceProxy inherits the Proxy class, each proxy class will be associated with an InvocationHandler method invocation processor.
  • The class and all methods are public finaldecorated , so the proxy class can only be used and cannot be inherited.
  • Each method is described by a Method object, which is created in the static code block and named m + 数字in format of .
  • When the method is called, it is called by super.h.invoke(this, m1, (Object[])null);calling , in which it super.h.invokeis actually the LogHandler object Proxy.newProxyInstancepassed , which inherits the InvocationHandler class and is responsible for the actual call processing logic.

After receiving parameters such as method and args, LogHandler's invoke method performs some processing, and then makes the proxied object target execute the method through reflection.

So far, we have understood the use of JDK-based dynamic proxy and the structure of the generated proxy class. Let's take a look at the Cglib dynamic proxy implementation that does not require the target class to implement the interface.

Cglib dynamic proxy

In the above example, it can be seen that no matter whether static proxy or JDK dynamic proxy is used, the target class needs to implement an interface. In some cases, the target class may not implement the interface, then Cglib dynamic proxy can be used.

Cglib (Code Generation Library) is a powerful, high-performance, open source code generation package that can provide proxies for classes that do not implement interfaces.

The Cglib proxy can be called a subclass proxy. Specifically, Cglib will build a subclass of the target class in memory and rewrite its business methods, so as to realize the extension of the function of the target object. Because of the inheritance mechanism, final modified classes cannot be proxied.

Cglib Enhancergenerates a proxy class through a class, implements an interface, and enhances the method of the target object in MethodInterceptorits method, and can call the original method through the Method or MethodProxy inheritance class.intercept

This time, take the following order (OrderService) as an example to show how to add log information before and after placing an order through Cglib.

Before using Cglib, you first need to introduce the corresponding dependent jar package. In most projects, Cglib has been indirectly imported, and you can check whether its version is the expected version. Here, the Maven form is used to introduce Cglib dependencies.

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

The first step is to define the business class OrderService, which does not need to implement any interface.

public class OrderService {
	public void order(String orderNo){
		System.out.println("order something... ");
	}
}

The second step is to define the creation and business implementation of dynamic proxy classes.

/**
 * 动态代理类,实现方法拦截器接口
 **/
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;
	}
}

The LogInterceptor class implements the MethodInterceptor interface, and adds business logic to be extended in the rewritten intercept method. It should be noted that the MethodProxy#invokeSuper method is called in the intercept method, not the invoke method.

At the same time, the tool method getProxyInstance for creating the proxy object of the target object is defined in the LogInterceptor class. It is worth noting that the parameter this of the Enhancer#setCallback method refers to the current object of the LogInterceptor.

The third step is to write a test client.

public class CglibTest {

	public static void main(String[] args) {
		OrderService orderService = (OrderService) new LogInterceptor().getProxyInstance(OrderService.class);
		orderService.order("123");
	}
}

Execute the above method and print the log as follows:

LogInterceptor:Before Login---
order something... 
LogInterceptor:After Login---

Successfully implant log information before and after the method of the target object.

The Cglib dynamic proxy has the following characteristics:

  • The dependent jar package of Cglib needs to be introduced, and usually the core package of Spring already includes the Cglib function.
  • Cglib dynamic proxy does not need interface information, but it intercepts and wraps all methods of the proxy class.
  • The delegate class cannot be final, otherwise an error java.lang.IllegalArgumentException: Cannot subclass final class xxx will be reported.
  • It will not intercept final/static methods that cannot be overloaded in the delegate class, but skip such methods and only proxy other methods.
  • Implement the MethodInterceptor interface to handle requests for all methods on the proxy class.

Comparison of three agents

Static proxy: Both the proxy class and the target class need to implement the interface, so that the proxy can enhance its functions.

JDK dynamic proxy: based on the Java reflection mechanism, the target class must implement the interface to generate the proxy object. Use Proxy.newProxyInstancethe method to generate a proxy class, and implement the method InvocationHandlerin invokethe method to realize the enhanced function.

Cglib dynamic proxy: based on the ASM mechanism, by generating a subclass of the target class as a proxy class. Instead of implementing the interface, use Cblibthe Enhancersubclass to generate a proxy object, and implement MethodInterceptorthe interceptmethod to implement the enhanced functionality.

Advantages of JDK dynamic proxy: JDK supports itself, reduces dependencies, can be smoothly upgraded with JDK, and code implementation is simple.

Advantages of Cglib dynamic proxy: no need to implement interface, achieve no intrusion; only operate the classes we care about, without adding workload to other related classes;

Dynamic proxy support in Spring

Spring's AOP implementation mainly uses JDK dynamic proxy and Cglib dynamic proxy, and the corresponding implementation class is located in the jar package of spring-aop.

// 基于JDK的动态代理实现类
org.springframework.aop.framework.JdkDynamicAopProxy
// 基于Cglib的动态代理实现类
org.springframework.aop.framework.CglibAopProxy

Spring uses JDK dynamic proxy to implement AOP by default. If a class implements an interface, Spring will use this dynamic proxy. If the target object does not implement the interface, you need to use Cglib dynamic proxy to achieve it.

After understanding the use and characteristics of JDK dynamic proxy and Cglib dynamic proxy, you can think about some scenarios where Spring transactions fail. Spring's transaction implementation is based on AOP, such as:

  • The method is defined using private, resulting in transaction failure: the proxy method must be public.
  • The method is decorated with final: If the method is defined as final, the JDK dynamic agent or Cglib cannot rewrite the method.
  • The same type of internal method call: directly use this object to call the method, and the proxy method cannot be generated, which will cause the transaction to fail.

Regarding other content of dynamic proxy in Spring, this article will not expand, and interested readers can directly read the corresponding source code.

summary

From the implementation of the static proxy mode in Nacos, this article extends and expands the definition of proxy mode, the application scenarios of proxy mode, static proxy mode, dynamic proxy mode, Cglib dynamic proxy, and the proxy used by AOP in Spring, etc.

Through the knowledge points associated in the article and the practical cases in projects of different spans, you should be able to perceive the proxy mode, especially the importance in practice based on JDK dynamic proxy and Cglib dynamic proxy. Hurry up and learn a wave.

Reference article:

https://segmentfault.com/a/1190000040407024

https://juejin.cn/post/6844903744954433544

https://www.cnblogs.com/clover-toeic/p/11715583.html

Guess you like

Origin blog.csdn.net/wo541075754/article/details/128440089