Design pattern - agent pattern

Posted by dave_c00 on Thu, 30 Dec 2021 02:07:16 +0100

proxy pattern

Proxy Pattern refers to providing a proxy for other objects to control access to this object. It belongs to structural pattern. In some cases, an object is inappropriate or cannot directly reference another object, and the proxy object can act as an intermediary between the client and the target object.

The agent mode generally includes three roles:

  1. Abstract topic role (Subject): the main responsibility of abstract topic class is to declare the common interface method between real topic and agent. This class can be interface or abstract class;
  2. RealSubject: this class is also called the proxied class, which defines the real object represented by the agent and is the real logical business object responsible for executing the system;
  3. Proxy subject role (proxy): also known as proxy class, it holds the reference of RealSubject internally, so it has full proxy right to RealSubject. The client calls the method of the proxy object, and colleagues also call the method of the proxy object, but some processing code will be added before and after the proxy object.

In the code, the general agent will be understood as code enhancement. In fact, it is to add some code logic before and after the original code logic, so that the caller has no perception. Agent mode belongs to structural mode, which is divided into static agent and dynamic agent.

Application scenario of agent mode

Housing agents in life, ticket scalpers, marriage agencies, express delivery, transaction agents, non-invasive log monitoring, etc. are the embodiment of the agency model. When you cannot or do not want to directly reference an object or have difficulty accessing an object, you can access it indirectly by giving a proxy object. There are two main purposes of using proxy mode: one is to protect the target object, and the other is to enhance the target object.

General writing method of agent mode

First create the agent topic role ISubject class:

public interface ISubject {
    void request();
}

Create the RealSubject class of the real theme role:

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

To create a Proxy subject role Proxy class:

public class Proxy implements ISubject {

    private ISubject subject;

    public Proxy(ISubject subject){
        this.subject = subject;
    }
    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().");
    }
}

Client call code:

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

From static proxy to dynamic proxy

For example, parents find objects for their children and create the top-level interface IPerson:

public interface IPerson {
    void findLove();
}

Son Zhang San wants to find an object and implement ZhangSan class:

public class ZhangSan implements IPerson {
    public void findLove() {
        System.out.println("Son requirements: white skin, beautiful big legs");
    }
}

Father Zhang Laosan helped his son Zhang San on a blind date to realize the father class

public class ZhangLaosan implements IPerson {
    private ZhangSan zhangsan;
    public ZhangLaosan(ZhangSan zhangsan) {
        this.zhangsan = zhangsan;
    }
    public void findLove() {
        System.out.println("Zhang Laosan began to look for");
        zhangsan.findLove();
        System.out.println("Start dating");
    }

}

Test code:

public class Test {
    public static void main(String[] args) {
        ZhangLaosan zhangLaosan = new ZhangLaosan(new ZhangSan());
        zhangLaosan.findLove();
    }
}

However, this scenario is a bit bad, that is, his father will only find objects for his children, and others will not care. If it develops into an industry, there are matchmakers, marriage agencies, etc., or use static agency, the cost is very high, and a more general solution is needed. This is the upgrade from static agent to dynamic agent. In Java, the most commonly used are the JDK's own proxy and the class library provided by Cglib.

Dynamic agent based on JDK

First create the matchmaker class JdkMeipo:

public class JdkMeipo implements InvocationHandler {
    private IPerson target;
    public IPerson getInstance(IPerson target){
        this.target = target;
        Class<?> clazz =  target.getClass();
        return (IPerson) Proxy.newProxyInstance(clazz.getClassLoader(),clazz.getInterfaces(),this);
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(this.target,args);
        after();
        return result;
    }
    private void after() { System.out.println("The two sides agreed to start exchanges");}
    private void before() { System.out.println("I'm a matchmaker. I've collected your needs and started looking for them"); }
}

Create another class ZhaoLiu:

public class ZhaoLiu implements IPerson {
    public void findLove() {
        System.out.println("Zhao Liu's requirements: have a car and a house, and have a high degree of education");
    }
}

Test code

public static void main(String[] args) {
    JdkMeipo jdkMeipo = new JdkMeipo();
    IPerson zhangsan = jdkMeipo.getInstance(new Zhangsan());
    zhangsan.findLove();

    IPerson zhaoliu = jdkMeipo.getInstance(new ZhaoLiu());
    zhaoliu.findLove();
}

Application of static mode in business

Let's look at an actual business scenario. In a distributed business scenario, the database is usually divided into databases and tables. After the database and tables are divided into databases and tables, multiple data sources may be configured during actual Java operations. We can dynamically switch data sources by setting the data source path. Create the Order class first:

@Data
public class Order {
    private Object orderInfo;
    //Order creation time is divided by year
    private Long createTime;
    private String id;
}

Create OrderDao persistence layer operation class:

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

Create IOrderService interface:

public interface IOrderService {
    int createOrder(Order order);
}

Create OrderService implementation class

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. According to the opening and closing principle, we modify the original code logic and complete it through the proxy object. First create the data source routing object, and use the singleton of ThreadLocal to implement the DynamicDataSourceEntity class:

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 currently in use
    public static String get(){return  local.get();}
    // Restore the currently switched data source
    public static void restore(){
         local.set(DEFAULE_SOURCE);
    }
    //DB_2020
    //DB_2021 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);}
}

Create a proxy class OrderServiceSaticProxy for switching data sources“

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

Test code

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

Application of dynamic agent in business scenario

public class OrderServiceDynamicProxy implements GPInvocationHandler {

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

    private Object proxyObj;
    public Object getInstance(Object proxyObj) {
        this.proxyObj = proxyObj;
        Class<?> clazz = proxyObj.getClass();
        return GPProxy.newProxyInstance(new GPClassLoader(),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();
        }
    }
}

Test code

IOrderService orderService = (IOrderService)new OrderServiceDynamicProxy().getInstance(new OrderService());

The same operation effect can still be achieved. However, after using dynamic proxy, we can not only realize the dynamic routing of data source of Order, but also realize the routing of data source of any other class. Of course, there is an important convention that the getCreateTime() method must be implemented, because the routing rules are calculated according to time. We can achieve the purpose of constraint through interface specification.

Implementation principle of handwritten JDK dynamic agent

JDK dynamic agent adopts byte reorganization and regenerates objects to replace the original objects, so as to achieve the purpose of dynamic agent.

The steps for JDK dynamic agent to generate objects are as follows:

  1. Get the reference of the proxy object, get all its interfaces, and get reflection
  2. JDK dynamic proxy class regenerates a new class, and the new class should implement all interfaces implemented by the proxy class
  3. Java code is generated dynamically, and the newly added business logic method is called by a certain logic code (reflected in the code)
  4. Compile the newly generated Java code class file
  5. Reload into the JVM to run

The above process is called bytecode reorganization. There is a specification in JDK, as long as it starts with $under ClassPath class files are generally generated automatically.

/**
 * The main logic of handwritten JDK code is shown in git
 */
public class NewProxy {
    public static Object newProxyInstance(GPClassLoader classLoader, Class<?> [] interfaces, GPInvocationHandler h){
           //1. Dynamically generate source code java file

           //2. Java file output disk

           //3. Put the generated java file compiled into class file

           //4. Compiled The class file is loaded into the JVM

           //5. Returns a new proxy object after bytecode reorganization
    }
 }
   

CGLIB proxy calling API and its principle analysis

public class CGlibMeipo implements MethodInterceptor {
    public Object getInstance(Class<?> clazz) throws Exception{
        //Equivalent to Proxy, Proxy tool class
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        return enhancer.create();
    }
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object obj = methodProxy.invokeSuper(o,objects);
        after();
        return obj;
    }
    private void before(){
        System.out.println("I'm a matchmaker. I want to find you someone. Now I've confirmed your needs");
        System.out.println("Start looking");
    }
    private void after(){
        System.out.println("OK If you want, get ready");
    }
}
public class Customer {
    public void findLove(){
        System.out.println("Son requirements: white skin, beautiful big legs");
    }
}

The target object of CGLIB proxy does not need to implement any interface. It implements dynamic proxy by dynamically inheriting the target object.

public static void main(String[] args) {
    try {
        //CGLib has a pit. CGLib cannot proxy the final method
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"E://cglib_proxy_classes");
        Customer obj = (Customer) new CGlibMeipo().getInstance(Customer.class);
        System.out.println(obj);
        obj.findLove();
    } catch (Exception e) {
        e.printStackTrace();
    }

}

Comparison of CGLIB and JDK dynamic agents

  1. JDK dynamic proxy implements the interface of the proxy object, and CGLIB proxy inherits the proxy object
  2. Both JDK dynamic agent and CGLIB agent generate bytecode during runtime. JDK dynamic agent directly writes Class bytecode, and CGLIB agent uses ASM framework to write Class bytecode. The implementation of CGLIB agent is more complex, and the generation agent is inefficient compared with JDK dynamic agent
  3. JDK dynamic proxy calls proxy methods through reflection mechanism. CGLIB proxy calls methods directly through FastClass mechanism. CGLIB proxy has higher execution efficiency.

Proxy mode and Spring ecology

Application of proxy pattern in Spring

  1. ProxyFactoryBean core method getObject()
  2. When Spring uses dynamic proxy to implement AOP, there are two very important classes: JdkDynamicAopProxy class and CglibAopProxy class

Proxy selection principles in Spring

  1. When the Bean has an implementation interface, Spring will use JDK dynamic proxy
  2. When the Bean does not implement the interface, Spring will choose the CGLIB proxy
  3. Spring can use CGLIB through forced configuration, which only needs to be configured in the configuration file

The essential difference between static agent and dynamic agent

  1. The static proxy can only complete the proxy operation manually. If the proxy class adds new methods, the proxy class needs to be added synchronously, which violates the opening and closing principle.
  2. The dynamic agent adopts the method of dynamically generating code during rerun, cancels the extension restrictions on the proxy class, and follows the opening and closing principle
  3. If the dynamic agent wants to extend the enhanced logic of the target class, combined with the policy mode, it only needs to add a new policy class without modifying the agent class code

Advantages and disadvantages of agent mode

advantage:

  1. Proxy mode can separate the proxy object from the real called target object
  2. It reduces the coupling of the system to a certain extent and has good expansibility
  3. It can protect the target object
  4. You can enhance the functionality of the target object

Disadvantages:

  1. The agent pattern will increase the number of classes in the system design

  2. Adding a proxy object to the client and target objects will slow down the request processing
    Proxy selection principles in Spring

  3. When the Bean has an implementation interface, Spring will use JDK dynamic proxy

  4. When the Bean does not implement the interface, Spring will choose the CGLIB proxy

  5. Spring can use CGLIB through forced configuration, which only needs to be configured in the configuration file

The essential difference between static agent and dynamic agent

  1. The static proxy can only complete the proxy operation manually. If the proxy class adds new methods, the proxy class needs to be added synchronously, which violates the opening and closing principle.
  2. The dynamic agent adopts the method of dynamically generating code during rerun, cancels the extension restrictions on the proxy class, and follows the opening and closing principle
  3. If the dynamic agent wants to extend the enhanced logic of the target class, combined with the policy mode, it only needs to add a new policy class without modifying the agent class code

Advantages and disadvantages of agent mode

advantage:

  1. Proxy mode can separate the proxy object from the real called target object
  2. It reduces the coupling of the system to a certain extent and has good expansibility
  3. It can protect the target object
  4. You can enhance the functionality of the target object

Disadvantages:

  1. The agent pattern will increase the number of classes in the system design
  2. Adding a proxy object to the client and target objects will slow down the request processing
  3. Increase the complexity of the system

Topics: Java Design Pattern Interview Proxy