Spring09_ Dynamic agent

Keywords: Java Junit Maven Apache

For the source code of this tutorial, please visit: tutorial_demo

1, What is dynamic agent

1.1 concept

The purpose of dynamic proxy is similar to decoration mode, which is to enhance an object. All cases that use decorator mode can be replaced with dynamic proxies.

Features: bytecode is created on demand and loaded on demand;

Function: enhance the method without modifying the source code;

Learning purpose: to prepare for learning the principle of AOP.

1.2 implementation mode

There are two ways:

  1. The dynamic proxy based on interface is provided by JDK officially. The proxy class implements at least one interface. If not, it cannot be used;
  2. Dynamic agent based on subclass, provided by the third-party cglib library.

In this tutorial, we use the interface based dynamic proxy method, which is used in all cases.

1.3. Several concepts to be defined

Target object: the enhanced object.

Proxy object: the target object is required, and then the enhanced object is added to the target object.

Target method: the enhanced method.

Proxy object = target object + enhancement

So far, we need to know that there is a way to enhance the method without changing the target object method. This way is dynamic proxy. With it, we need to provide the target object and the enhanced build proxy object.

Getting a proxy object is equivalent to having an enhanced version of the target object, running the relevant methods, in addition to the running method itself, the enhanced content will also be run, so that the method can be enhanced without changing the source code.

1.4 detailed explanation of dynamic agent mode based on interface

1.4.1 how to generate proxy objects

Use the newProxyInstance method in the Proxy class.

1.4.2. Parameter details of newProxyInstance method

ClassLoader loader:

Class loader type, you don't need to pay attention to it, you just need to know how to get it. Get method:

this.class.getClassLoader();

As long as you have a Class object, you can get the ClassLoader object.

Class[] interfaces:

Specifies which interfaces the object returned by the newProxyInstance() method implements, because it is an array, multiple interfaces can be specified.

InvocationHandler h:

The most important of the three parameters is an interface, called call handler. This interface has only one method, the invoke() method. It is the only implementation of all methods of the proxy object. That is to say, no matter which method you call on the proxy object, you are actually calling the invoke() method of the InvocationHandler.

1.4.3. Details of the parameters of invoke() method

Any interface method that executes the proxied object passes through that method.

Object proxy: proxy object, that is Proxy.newProxyInstance() method, which is usually not used by us.

Method method: indicates the reflection object of the currently called method, for example, m.fun(), then M is the reflection object of fun();

Object[] args: indicates the parameters of the currently called method. Of course, the m.fun() call has no parameters, so args is an array with a length of 0.

2, Dynamic agent case

Here is a case to illustrate the use of dynamic agents.

2.1 create Maven project and add coordinates

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.codeaction</groupId>
    <artifactId>proxy</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

2.2. Create an IWaiter interface

package org.codeaction.proxy;

//Interface for server
public interface IWaiter {
    //How to provide services
    void serve();
}

2.3. Create an implementation class of IWaiter interface

package org.codeaction.proxy;

//For the waiter
public class ManWaiter implements IWaiter {
    @Override
    public void serve() {
        System.out.println("service...");
    }
}

For the existing problems, I want ManWaiter to print the following information (call the serve method) when providing services:

Hello...
Services
 bye...

We can do this:

package org.codeaction.proxy;

//For the waiter
public class ManWaiter implements IWaiter {
    @Override
    public void serve() {
        System.out.println("Hello...");
        System.out.println("service...");
        System.out.println("bye...");
    }
}

But in this way, we modify the serve method. If there are other requirements in the future, we need to modify the serve method again, which is obviously cumbersome and undesirable. We can use the dynamic agent method to enhance the serve method without modifying the source code.

2.4 create test class and use dynamic agent

package org.codeaction.test;


import org.codeaction.proxy.IWaiter;
import org.codeaction.proxy.ManWaiter;
import org.junit.Test;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class MyTest {
    @Test
    public void TestProxy() {
        //Target object
        IWaiter manWaiter = new ManWaiter();

        /**
         * Three parameters to create a proxy object
         */
        ClassLoader loader = this.getClass().getClassLoader();
        Class[] interfaces = {IWaiter.class};
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object resultValue = null;

                System.out.println("Hello...");
                resultValue = method.invoke(manWaiter, args);//Call the target method of the target object
                System.out.println("bye...");
                return resultValue;
            }
        };
        //Get the proxy object, which is the object enhanced on the basis of the target object
        IWaiter waiter = (IWaiter) Proxy.newProxyInstance(loader, interfaces, handler);
        //Add "hello" in the front and "goodbye" in the back
        waiter.serve();
    }
}

Run the test method with the output as follows:

Hello...
Services
 bye...

Through the above code and running results, we find that:

  1. Using dynamic agent needs to provide: target object, three parameters;
  2. The generated proxy object is the object that implements all interfaces of the second parameter of the three parameters;
  3. The way to run the proxy object is to run the invoke method;
  4. Implement enhancements in the invoke method.

3, Dynamic agent using agent factory

In the above case, the target object and the enhancement are bound together, unable to switch freely and inflexibly. Next, we create a proxy factory to realize dynamic proxy.

3.1. Create pre enhanced interface

package org.codeaction.proxy;

//Pre enhancement
public interface BeforeAdvice {
    void before();
}

3.2 create a post enhanced interface

package org.codeaction.proxy;

//Post enhancement
public interface AfterAdvice {
    void after();
}

3.3 create agent factory

package org.codeaction.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * This class is used to generate proxy objects
 * Required parameters:
 *  * Target object
 *  * enhance
 * How to use it?
 *  1.Create agent factory
 *  2.Set up three things for the factory:
 *      * Target object: setTargetObject(xxx);
 *      * Pre enhancement: setbeforeadvice (implementation of the interface)
 *      * Post enhancement: setafteradvice (implementation of this interface)
 *  3.Call createProxy() to get the proxy object
 *      * When executing a proxy object method:
 *          > before() of implementing BeforeAdvice
 *          > Target method of target object
 *          > after() of AfterAdvice
 */
public class ProxyFactory {
    private Object targetObject;//Target object
    private BeforeAdvice beforeAdvice;//Pre enhancement
    private AfterAdvice afterAdvice;//Post enhancement

    public Object getTargetObject() {
        return targetObject;
    }

    public void setTargetObject(Object targetObject) {
        this.targetObject = targetObject;
    }

    public BeforeAdvice getBeforeAdvice() {
        return beforeAdvice;
    }

    public void setBeforeAdvice(BeforeAdvice beforeAdvice) {
        this.beforeAdvice = beforeAdvice;
    }

    public AfterAdvice getAfterAdvice() {
        return afterAdvice;
    }

    public void setAfterAdvice(AfterAdvice afterAdvice) {
        this.afterAdvice = afterAdvice;
    }

    //Used to generate proxy objects
    public Object createProxyObject() {
        //Three parameters
        ClassLoader classLoader = this.getClass().getClassLoader();
        Class[] interfaces = this.targetObject.getClass().getInterfaces();
        InvocationHandler handler = new InvocationHandler() {
            //This is done when the method of the proxy object is called
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object resultValue = null;

                if(beforeAdvice != null) {
                    //Perform pre enhancement
                    beforeAdvice.before();
                }
                //Target method of executing target object
                resultValue = method.invoke(targetObject, args);
                if(afterAdvice != null) {
                    //Perform post enhancement
                    afterAdvice.after();
                }
                //Returns the return value of the target object
                return resultValue;
            }
        };

        //Get proxy object
        return Proxy.newProxyInstance(classLoader, interfaces, handler);
    }
}

3.4 add test method to test class

@Test
public void testProxyFactory() {
    //Create factory
    ProxyFactory factory = new ProxyFactory();
    //Set target object
    factory.setTargetObject(new ManWaiter());
    //Set pre enhancement
    factory.setBeforeAdvice(new BeforeAdvice() {
        @Override
        public void before() {
            System.out.println("Hello...");
        }
    });
    //Set post enhancement
    factory.setAfterAdvice(new AfterAdvice() {
        @Override
        public void after() {
            System.out.println("bye...");
        }
    });
    //Create proxy object
    IWaiter waiter = (IWaiter) factory.createProxyObject();
    //Execute proxy object method
    waiter.serve();
}

Run the test method with the output as follows:

Hello...
Services
 bye...

4, Use the proxy factory to modify the code in the previous section

In the previous article, we combined pure annotation with Apache Commons The code modification of dbutils to implement CRUD operation of single table has become the version supporting transaction. Every Service method needs to start transaction, commit transaction and roll back transaction code redundancy. If the method name of relevant method in JdbcUtils is modified, then every call location in Service is modified. To solve the above problem, we use dynamic agent to modify the code in the previous section .

4.1. Create pre enhanced interface

package org.codeaction.proxy;

public interface BeforeAdvice {
    void before() throws Exception;
}

4.2 create a post enhanced interface

package org.codeaction.proxy;

public interface AfterAdvice {
    void after() throws Exception;
}

4.3 create special enhanced interface

This is for rollback. Teach him special enhancement.

package org.codeaction.proxy;

public interface ActAdvice {
    void act() throws Exception;
}

4.4. Create agent factory

package org.codeaction.proxy;

import org.springframework.stereotype.Component;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

@Component
public class ProxyFactory {
    private Object targetObject;
    private BeforeAdvice beforeAdvice;
    private AfterAdvice afterAdvice;
    private ActAdvice actAdvice;

    public ActAdvice getActAdvice() {
        return actAdvice;
    }

    public void setActAdvice(ActAdvice actAdvice) {
        this.actAdvice = actAdvice;
    }

    public Object getTargetObject() {
        return targetObject;
    }

    public void setTargetObject(Object targetObject) {
        this.targetObject = targetObject;
    }

    public BeforeAdvice getBeforeAdvice() {
        return beforeAdvice;
    }

    public void setBeforeAdvice(BeforeAdvice beforeAdvice) {
        this.beforeAdvice = beforeAdvice;
    }

    public AfterAdvice getAfterAdvice() {
        return afterAdvice;
    }

    public void setAfterAdvice(AfterAdvice afterAdvice) {
        this.afterAdvice = afterAdvice;
    }

    public Object createProxyObject() {

        ClassLoader classLoader = this.getClass().getClassLoader();
        Class[] interfaces = this.targetObject.getClass().getInterfaces();
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object resultValue = null;

                try {
                    if(beforeAdvice != null) {
                        beforeAdvice.before();
                    }
                    resultValue = method.invoke(targetObject, args);
                    if(afterAdvice != null) {
                        afterAdvice.after();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    if(actAdvice != null) {
                        actAdvice.act();
                    }
                }
                return resultValue;
            }
        };

        return Proxy.newProxyInstance(classLoader, interfaces, handler);
    }
}

4.5 modify the implementation class AccountServiceImpl of Service interface

Remove all transaction related code and let the Service focus on business only

package org.codeaction.service.impl;

import org.codeaction.dao.IAccountDao;
import org.codeaction.domain.Account;
import org.codeaction.service.IAccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("accountService")
public class AccountServiceImpl implements IAccountService {
    @Autowired
    private IAccountDao accountDao;

    @Override
    public List<Account> findAll() throws Exception {
        return accountDao.findAll();
    }

    @Override
    public Account findById(Integer id) throws Exception {
        return accountDao.findById(id);
    }

    @Override
    public void save(Account account) throws Exception {
        accountDao.save(account);
    }

    @Override
    public void update(Account account) throws Exception {
        accountDao.update(account);
    }

    @Override
    public void delete(Integer id) throws Exception {
        accountDao.delete(id);
    }

    @Override
    public void transfer(Integer srcId, Integer dstId, Float money) throws Exception {
        Account src = accountDao.findById(srcId);
        Account dst = accountDao.findById(dstId);

        if(src == null) {
            throw new RuntimeException("Transfer out user does not exist");
        }

        if(dst == null) {
            throw new RuntimeException("Transfer in user does not exist");
        }

        if(src.getMoney() < money) {
            throw new RuntimeException("Insufficient balance of transfer out account");
        }

        src.setMoney(src.getMoney() - money);
        dst.setMoney(dst.getMoney() + money);

        accountDao.update(src);

       //int x = 1/0;

        accountDao.update(dst);
    }
}

4.6 modify the main configuration class

package org.codeaction.config;

import org.codeaction.proxy.ActAdvice;
import org.codeaction.proxy.AfterAdvice;
import org.codeaction.proxy.BeforeAdvice;
import org.codeaction.proxy.ProxyFactory;
import org.codeaction.service.IAccountService;
import org.codeaction.util.JdbcUtils;
import org.springframework.context.annotation.*;

import java.sql.SQLException;

@Configuration
@ComponentScan(basePackages = "org.codeaction")
@PropertySource("classpath:jdbc.properties")
@Import(JdbcConfig.class)
public class MyConfig {
    /**
     *
     * @param factory Agency factory
     * @param accountService Target object
     * @return
     */
    @Bean("proxyAccountService")
    public IAccountService createProxyAccountService(ProxyFactory factory, IAccountService accountService) {
        factory.setTargetObject(accountService);
        factory.setBeforeAdvice(new BeforeAdvice() {
            @Override
            public void before() throws Exception {
                //Open transaction
                JdbcUtils.beginTransaction();
            }
        });

        factory.setAfterAdvice(new AfterAdvice() {
            @Override
            public void after() throws Exception {
                //Commit transaction
                JdbcUtils.commitTransaction();
            }
        });

        factory.setActAdvice(new ActAdvice() {
            @Override
            public void act() throws Exception {
                //RollBACK 
                JdbcUtils.rollbackTransaction();
            }
        });
		//Build proxy object
        return (IAccountService)factory.createProxyObject();
    }
}

4.7 modify test class

package org.codeaction.test;

import org.codeaction.config.MyConfig;
import org.codeaction.domain.Account;
import org.codeaction.service.IAccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.List;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = MyConfig.class)
public class MyTest {

    //Injection agent factory object
    @Autowired
    @Qualifier("proxyAccountService")
    private IAccountService accountService;

    @Test
    public void testFindAll() throws Exception {
        List<Account> accounts = accountService.findAll();
        for (Account account : accounts) {
            System.out.println(account);
        }
    }

    @Test
    public void testFindById() throws Exception {
        Account account = accountService.findById(3);
        System.out.println(account);
    }

    @Test
    public void testSave() throws Exception {
        Account account = new Account();
        account.setName("abc");
        account.setMoney(10000F);

        accountService.save(account);

        System.out.println(account);
    }

    @Test
    public void testDelete() throws Exception {
        accountService.delete(4);
    }

    @Test
    public void testUpdate() throws Exception {
        Account account = new Account();
        account.setId(5);
        account.setName("ab111111111c111");
        account.setMoney(10000F);
        accountService.update(account);
    }

    @Test
    public void testTrans() throws Exception {
        accountService.transfer(1, 2, 10F);
    }
}

Note that the accountServie injected here is the object of the agent factory class, run the test method and test.

Posted by Davy on Mon, 01 Jun 2020 06:53:12 -0700