Detailed explanation of agency mode

Keywords: Java Design Pattern

1. Agent mode

Agent pattern is a design pattern that is easy to understand. Simply put, we use proxy objects instead of accessing real objects, so that we can provide additional function operations and expand the functions of the target object without modifying the original target object.

The agent mode contains three roles:

Abstract topic role (Subject): the main responsibility of an abstract topic class is to declare the common interface method between the real topic and the agent. This class can be either an interface or an abstract class.;

RealSubject: this class defines the real object represented by the agent, which is the real logical business object responsible for executing the system;

Proxy: proxy class; It internally holds a reference to RealSubject, so it has full proxy to RealSubject. The client calls the method of the proxy object and the method of the proxy object, but some code processing will be added before and after the proxy object.

In the code, the general agent will be understood as code enhancement, which actually extends the functions of the target object. For example, you can add some custom operations before and after the execution of a method of the target object.

Agent mode belongs to structural mode: it is divided into static agent and dynamic agent.

2. Static proxy

In static proxy, we enhance each method of the target object manually, which is very inflexible (for example, once a new method is added to the interface, the target object and proxy object must be modified, and it is troublesome to write a proxy class for each target class). There are very few practical application scenarios, and we can hardly see the scenario of using static proxy in daily development.

The above is a static proxy from the perspective of implementation and application. From the JVM level, the static proxy will turn the interface, implementation class and proxy class into actual class files during compilation.

Static proxy implementation steps:

  1. Define an interface and its implementation class;
  2. Creating a proxy class also implements this interface
  3. Inject the target object into the proxy class, and then call the corresponding method in the target class in the corresponding method of the proxy class. In this way, we can shield the access to the target object through the proxy class, and do something we want to do before and after the execution of the target method.

Here's the code!

2.1 simple example

  • Define an interface

    public interface ISubject {
    
        void request();
        
    }
    
  • Implementation class

    public class RealSubject implements ISubject {
    
        public void request() {
            System.out.println("real service is called.");
        }
    
    }
    
  • Create proxy class

    public class Proxy implements ISubject {
    
        private ISubject subject;
    
        public Proxy(ISubject subject) {
            this.subject = subject;
        }
    
    
        //Code enhancement
        public void request() {
            before();
            subject.request();
            after();
        }
    
        public void before() {
            System.out.println("called before request().");
        }
    
        public void after() {
            System.out.println("called after request().");
        }
    }
    
  • Test class

    public class Client {
    
        public static void main(String[] args) {
    
            Proxy proxy = new Proxy(new RealSubject());
            proxy.request();
    
        }
    
    }
    
  • Output results

2.2 business application

In distributed business scenarios, databases are usually divided into databases and tables. We can dynamically switch data sources by setting data source routing.

Note: for the convenience of operation, no relevant database is actually created;

  • First create the order class

    public class Order {
    
        private Object orderInfo;
    
        //Order creation time is divided by year
        private Long createTime;
        
        private String id;
    
        public Object getOrderInfo() {
            return orderInfo;
        }
    
        public void setOrderInfo(Object orderInfo) {
            this.orderInfo = orderInfo;
        }
    
        public Long getCreateTime() {
            return createTime;
        }
    
        public void setCreateTime(Long createTime) {
            this.createTime = createTime;
        }
    
        public String getId() {
            return id;
        }
    
        public void setId(String id) {
            this.id = id;
        }
    }
    
  • Persistence layer class

    public class OrderDao {
    
        public int insert(Order order) {
            System.out.println("OrderDao establish Order success!");
            return 1;
        }
        
    }
    
  • Business class

    public interface IOrderService {
    
        int createOrder(Order order);
        
    }
    
    public class OrderService implements IOrderService {
        
        private OrderDao orderDao;
    
        public OrderService() {
            //If Spring is used, it should be injected automatically
            //For convenience, we initialize orderDao directly in the constructor
            orderDao = new OrderDao();
        }
    
        public int createOrder(Order order) {
            System.out.println("OrderService call orderDao Create order");
            return orderDao.insert(order);
        }
    }
    

Next, use the static agent. The main function is to automatically divide the inventory by year according to the order creation time.

  • Create data source routing object

    //Dynamically switch data sources
    public class DynamicDataSourceEntity {
    
        //Default data source
        public final static String DEFAULE_SOURCE = null;
    
        private final static ThreadLocal<String> local = new ThreadLocal<String>();
    
        private DynamicDataSourceEntity() {
        }
    
        //Gets the name of the data source being used
        public static String get() {
            return local.get();
        }
    
        //Restore the currently switched data source
        public static void restore() {
            local.set(DEFAULE_SOURCE);
        }
    
        //Set a data source with a known name
        public static void set(String source) {
            local.set(source);
        }
    
        //Dynamically set the data source according to the year
        public static void set(int year) {
            local.set("DB_" + year);
        }
    
        //Clear data source
        public static void clear() {
            local.remove();
        }
    
    }
    
  • Create proxy class

    public class OrderServiceStaticProxy implements IOrderService {
    
        private SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");
    
        private IOrderService orderService;
    
        public OrderServiceStaticProxy(IOrderService orderService) {
            this.orderService = orderService;
        }
    
        public int createOrder(Order order) {
            before();
    
            Long time = order.getCreateTime();
            Integer dbRouter = Integer.valueOf(yearFormat.format(new Date(time)));
            System.out.println("Static proxy classes are automatically assigned to[ DB_" + dbRouter + "]Data source processing data");
            DynamicDataSourceEntity.set(dbRouter);
            this.orderService.createOrder(order);
            DynamicDataSourceEntity.restore();
    
            after();
            return 0;
        }
    
        private void before() {
            System.out.println("Proxy before");
        }
    
        private void after() {
            System.out.println("Proxy after");
        }
    }
    
  • Test class

    public class DbRouteProxyTest {
        public static void main(String[] args) {
            try {
                Order order = new Order();
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
                Date date = sdf.parse("2021/12/01");
                order.setCreateTime(date.getTime());
                IOrderService orderService = new OrderServiceStaticProxy(new OrderService());
                orderService.createOrder(order);
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    
    }
    
  • Output results

The results meet our expectations, and the data is stored in the corresponding database according to the date.

3. Dynamic agent

Compared with static proxy, dynamic proxy is more flexible. We don't need to create a proxy class for each target class, and we don't need to implement the interface. We can directly proxy the implementation class (CGLIB dynamic proxy mechanism).

From the perspective of JVM, dynamic agent dynamically generates class bytecode at runtime and loads it into the JVM.

When it comes to dynamic proxy, Spring AOP and RPC frameworks should be mentioned. Their implementation depends on dynamic proxy.

Dynamic agent is rarely used in our daily development, but it is almost a necessary technology in the framework. After learning dynamic agent, it is also very helpful for us to understand and learn the principles of various frameworks.

As far as Java is concerned, there are many ways to implement dynamic agents, such as JDK dynamic agent, CGLIB dynamic agent and so on.

3.1 optimizing static agents

We can understand the dynamic agent by modifying the data source path in the static agent.

To create a dynamic proxy class:

public class OrderServiceDynamicProxy implements InvocationHandler {

    private SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");

    Object proxyObj;

    public Object getInstance(Object proxyObj) {
        this.proxyObj = proxyObj;
        Class<?> clazz = proxyObj.getClass();
        return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before(args[0]);
        Object object = method.invoke(proxyObj, args);
        after();
        return object;
    }

    private void after() {
        System.out.println("Proxy after method");
        //Restore to default data source
        DynamicDataSourceEntity.restore();
    }

    //target should be Order object
    private void before(Object target) {
        try {
            //Switch data sources
            System.out.println("Proxy before method");

            //Convention over configuration
            Long time = (Long) target.getClass().getMethod("getCreateTime").invoke(target);
            Integer dbRouter = Integer.valueOf(yearFormat.format(new Date(time)));
            System.out.println("Static proxy classes are automatically assigned to[ DB_" + dbRouter + "]Data source processing data");
            DynamicDataSourceEntity.set(dbRouter);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

The test code is as follows:

public class DbRouteProxyTest {
    public static void main(String[] args) {
        try {
            Order order = new Order();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
            Date date = sdf.parse("2021/12/01");
            order.setCreateTime(date.getTime());
            IOrderService orderService = (IOrderService) new OrderServiceDynamicProxy().getInstance(new OrderService());
            orderService.createOrder(order);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

We can see that the relevant effects are still achieved. However, after using dynamic proxy, we can not only realize the dynamic switching of data sources of Order table, but also realize the routing of other tables.

3.2 JDK dynamic proxy mechanism

3.2.1 introduction

InvocationHandler interface and Proxy class are the core of Java dynamic Proxy mechanism.

The most frequently used method in Proxy class is newProxyInstance(), which is mainly used to generate a Proxy object.

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

This method has three parameters:

  1. Loader: class loader for loading proxy objects.
  2. Interfaces: some interfaces implemented by the proxy class;
  3. h: the object that implements the InvocationHandler interface;

To implement the dynamic proxy, we must also implement InvocationHandler to define the processing logic. When our dynamic proxy object calls a method, the call of this method will be forwarded to the invoke method of the InvocationHandler interface class.

public interface InvocationHandler {

    /**
     * When you use a proxy object to call a method, you will actually call this method
     */
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

The invoke() method has the following three parameters:

  1. Proxy: dynamically generated proxy class
  2. Method: corresponds to the method called by the proxy class object
  3. args: parameter of current method method

That is to say, when the Proxy object created through newProxyInstance() of Proxy class calls the method, it will actually call the invoke() method of the class that implements the InvocationHandler interface. You can customize the processing logic in the invoke() method, such as what to do before and after the method is executed.

3.2.2 use steps

  1. Define an interface and its implementation class;

  2. Customize the InvocationHandler and override the invoke method. In the invoke method, we will call the native method (the method of the proxy class) and customize some processing logic;

  3. Create proxy objects through proxy.newproxyinstance (classloader, loader, class <? > [] interfaces, invocationhandler h) method;

3.2.3 simple example

  • Define interface

    public interface SmsService {
        String send(String message);
    }
    
  • Implementation class

    public class SmsServiceImpl implements SmsService {
        public String send(String message) {
            System.out.println("send message:" + message);
            return message;
        }
    }
    
  • proxy class

    public class JDkInvocationHandler implements InvocationHandler {
    
        /**
         * Real objects in proxy classes
         */
        private final Object target;
    
        public JDkInvocationHandler(Object target) {
            this.target = target;
        }
    
        public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
            //Before calling the method, we can add our own operations
            System.out.println("before method " + method.getName());
            Object result = method.invoke(target, args);
            //After calling the method, we can also add our own operations
            System.out.println("after method " + method.getName());
            return result;
        }
    }
    

    invoke() method: when our dynamic proxy object calls the native method, it actually calls the invoke() method, and then the invoke() method calls the native method of the proxy object instead of us.

  • Gets the factory class of the proxy object

    public class JdkProxyFactory {
        public static Object getProxy(Object target) {
            return Proxy.newProxyInstance(
                    target.getClass().getClassLoader(), // Class loading of target class
                    target.getClass().getInterfaces(),  // The interface that the agent needs to implement. You can specify multiple interfaces
                    new JDkInvocationHandler(target)   // Custom InvocationHandler corresponding to proxy object
            );
        }
    }
    

    getProxy(): get the proxy object of a class mainly through the Proxy.newProxyInstance() method

  • test

    public class JDKTest {
    
        public static void main(String[] args) {
            SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl());
            smsService.send("java");
        }
    }
    

3.3 CGLIB dynamic proxy mechanism

3.3.1 introduction

One of the most fatal problems of JDK dynamic proxy is that it can only proxy classes that implement interfaces.

In order to solve this problem, we can use CGLIB dynamic proxy mechanism to avoid it.

CGLIB (opens new window) (Code Generation Library) is based on ASM (opens new window) Bytecode generation library, which allows us to modify and dynamically generate bytecode at runtime. CGLIB implements proxy through inheritance. Many well-known open source frameworks are used CGLIB (opens new window) For example, in the AOP module in Spring: if the target object implements the interface, JDK dynamic proxy is used by default, otherwise CGLIB dynamic proxy is used.

In CGLIB dynamic proxy mechanism, MethodInterceptor interface and Enhancer class are the core.

You need to customize the MethodInterceptor and override the intercept method, which is used to intercept methods that enhance the proxy class.

public interface MethodInterceptor
extends Callback{
    // Intercept methods in the proxied class
    public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
                               MethodProxy proxy) throws Throwable;
}
  1. obj: represented object (object to be enhanced)
  2. Method: intercepted method (method to be enhanced)
  3. args: method input parameter
  4. proxy: used to call the original method

You can dynamically obtain the proxy class through the Enhancer class. When the class calls the method, it actually calls the intercept method in the MethodInterceptor.

3.3.2 use steps

  1. Define a class;
  2. Customize the MethodInterceptor and override the intercept method. Intercept is used to intercept methods that enhance the proxy class, which is similar to the invoke method in JDK dynamic proxy;
  3. Create a proxy class through create() of Enhancer class;

3.3.3 simple example

  • Introduce dependency

    Unlike JDK, dynamic agents do not require additional dependencies. CGLIB (opens new window) The code generation library actually belongs to an open source project. If you want to use it, you need to add related dependencies manually.

    <dependency>
      <groupId>cglib</groupId>
      <artifactId>cglib</artifactId>
      <version>3.3.0</version>
    </dependency>
    
  • Define class

    public class SmsService {
        public String send(String message) {
            System.out.println("send message:" + message);
            return message;
        }
    }
    
  • Custom MethodInterceptor

    public class CGMethodInterceptor implements MethodInterceptor {
    
        /**
         * @param o           Proxy object (enhanced object)
         * @param method      Intercepted methods (methods requiring enhancement)
         * @param args        Method input parameter
         * @param methodProxy Used to call the original method
         */
        @Override
        public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            //Before calling the method, we can add our own operations
            System.out.println("before method " + method.getName());
            Object object = methodProxy.invokeSuper(o, args);
            //After calling the method, we can also add our own operations
            System.out.println("after method " + method.getName());
            return object;
        }
    }
    
  • Get proxy class

    public class CglibProxyFactory {
    
        public static Object getProxy(Class<?> clazz) {
            // Create dynamic proxy enhancement class
            Enhancer enhancer = new Enhancer();
            // Set class loader
            enhancer.setClassLoader(clazz.getClassLoader());
            // Set proxy class
            enhancer.setSuperclass(clazz);
            // Set method interceptor
            enhancer.setCallback(new CGMethodInterceptor());
            // Create proxy class
            return enhancer.create();
        }
    }
    
  • test

    public class CGTest {
    
        public static void main(String[] args) {
            SmsService aliSmsService = (SmsService) CglibProxyFactory.getProxy(SmsService.class);
            aliSmsService.send("java");
        }
    }
    

3.4 comparison between JDK dynamic agent and CGLIB dynamic agent

  • JDK dynamic proxy can only proxy classes that implement interfaces or directly proxy interfaces, while CGLIB can proxy classes that do not implement any interfaces. In addition, CGLIB dynamic proxy intercepts the method calls of the proxy class by generating a subclass of the proxy class. Therefore, it cannot proxy classes and methods declared as final.

  • Both JDK dynamic agent and CGLIB agent generate bytecode during operation. JDK dynamic agent directly writes Class bytecode. CGLIB agent uses ASM framework to write Class bytecode. It is arbitrary and relatively complex, and the efficiency of generating agent Class is relatively low.

  • JDK invokes proxy methods through reflection mechanism, and CGLIB directly invokes methods through FastClass mechanism, which has high execution efficiency.

  • In terms of their efficiency, JDK dynamic agent is better in most cases. With the upgrade of JDK version, this advantage is more obvious.

4. Comparison between static agent and dynamic agent

  • Flexibility: dynamic proxy is more flexible. It does not need to implement interfaces. It can implement classes directly, and it does not need to create a proxy class for each target class. In addition, in static proxy, once a new method is added to the interface, the target object and proxy object must be modified, which is very troublesome!

  • JVM level: static agents turn interfaces, implementation classes, and agent classes into actual class files at compile time. The dynamic agent dynamically generates class bytecode at runtime and loads it into the JVM.

Posted by PC Nerd on Wed, 01 Dec 2021 08:59:27 -0800