Explain in detail how Java creates proxies and makes http requests through interfaces

  • 2021-09-12 01:27:23
  • OfStack

Scene

Now I want to do such a thing. The company's dubbo services are all intranet, but it provides an external exit, and I can request the corresponding dubbo service through links. (What should be done specifically is a gateway, and then turn http request into dubbo request, and call it through generalization call. Code can't see it.) Now, for the convenience of testing, I need to request the corresponding link from the configured interface through http request.

Analysis

The idea of the project is similar to the idea of the mybatis-spring integration package, which is to generate proxies to execute interface methods.
https://www.ofstack.com/article/153378.htm
The project is a simple spring project, and then the project introduces the api of the project, and then configures the corresponding service name, generates the agent through spring, injects it into the spring container, and then executes the method according to the corresponding domain name + interface full path + method name, and the parameter is json. For the convenience of the project using the hutool tool class, directly use fastjson to serialize.

Operation

First, create the factory bean, which is the FactoryBean used to return the proxy


import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;

import java.lang.reflect.Proxy;

/**
 * @Title Relative to BeanFactory This big factory , This is 1 A small factory dedicated to creating a certain type of bean( Singletons are created by default bean)
 * @Description  Create a proxy object 
 * @Version
 */
public class HttpProxyFactoryBean<T> implements FactoryBean<T> {

    /**
     * [Attention] 
     *  The reason why automatic assembly can be carried out here is because the current HttpProxyFactoryBean Will be registered to Spring In 
     *  It's just that the way it's registered   Follow 1 Like no 1 Sample ( 1 Will be on the class, add 1 Such as @Component , @Service Such annotations   ) 
     *  It is done by registering BeanDefinition There may be multiple registrations, and each of them 1 A HttpProxyFactoryBean Instances will be automatically assembled with the same 1 A HttpProxyInvocationHandler Instances 
     *
     *  There are also equivalent practices: 
     *  Utilization ApplicationContextAware Interface's setApplicationContext Get to applicationContext , 
     *  And then put applicationContext  Set as a property to the current class 
     *   Reuse applicationContext Adj. getBean Method to get the InvocationHandler Example of 
     */
    @Autowired
    private HttpProxyInvocationHandler httpProxyInvocationHandler;

    private Class<T> rpcInterface;

    public HttpProxyFactoryBean(Class<T> rpcInterface){
        this.rpcInterface = rpcInterface;
    }


    @Override
    public T getObject() throws Exception {
        // It should be put here ComputerService Interface 
        return (T)Proxy.newProxyInstance(rpcInterface.getClassLoader(),new Class[]{rpcInterface} ,httpProxyInvocationHandler);
    }

    @Override
    public Class<?> getObjectType() {
        return rpcInterface;
    }

}

Every dynamic proxy class must implement the InvocationHandler interface

Every dynamic proxy class must implement the interface InvocationHandler, and every instance of the proxy class is associated with an handler. When we call a method through a proxy object, the call of this method will be forwarded to the invoke method of the interface InvocationHandler for call.

We can directly inject the implementation class that implements InvocationHandler into the spring container, and then go with one innvoke method every 1 interface, of course, we can also have new1 every 1 interface, and then we can insert a specific number of parameters into the constructor. On my side, because every corresponding agent has nothing special, I will go to the same one:
Define 1 parameter, requested urlproxy. serverUrl, and requested item, proxy. project


import cn.hutool.http.HttpRequest;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description  Converts a call to a service method to a remote service http Request 
 * @Version
 */
@Component
public class HttpProxyInvocationHandler implements InvocationHandler {

    @Value("${proxy.serverUrl}")
    private String serverUrl;

    @Value("${proxy.project}")
    private String serverProject;

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Class<?> declaringClass = method.getDeclaringClass();
        if (Object.class.equals(declaringClass)) {
            return method.invoke(this, args);
        }

        String methodName = method.getName();
        String name = method.getDeclaringClass().getName();

        // Splice request address 
        String url = serverUrl + name + "/" + methodName;
//        String url = "http://test:8080/soa/com.rdd.TestService/createActivity";
        HashMap<String, String> paramMap = new HashMap<>();
    
//        String result = HttpRequest.post(url).headerMap(paramMap, true).body("[" + JSONObject.toJSONString(args) + "]").execute().body();
        String result = HttpRequest.post(url).headerMap(paramMap, true).body(JSONObject.toJSONString(args)).execute().body();

        System.out.println(">>>" + url + " The response result of is :" + result);

        // Converts the response result to the return value type of the interface method 
        Class<?> returnType = method.getReturnType();
        if (returnType.isPrimitive() || String.class.isAssignableFrom(returnType)) {
            if (returnType == int.class || returnType == Integer.class) {
                return Integer.valueOf(result);
            } else if (returnType == long.class || returnType == Long.class) {
                return Long.valueOf(result);
            }
            return result;
        } else if (Collection.class.isAssignableFrom(returnType)) {
            return JSONArray.parseArray(result, Object.class);
        } else if (Map.class.isAssignableFrom(returnType)) {
            return JSON.parseObject(result, Map.class);
        } else {
            return JSONObject.parseObject(result, returnType);
        }
    }
}

Finally, the corresponding factory bean is encapsulated into an bean definition and injected into an spring container

Our interface 1 is generally in the form of jar, so I simply write it in an proxy. txt file, and then read the corresponding interface full path and inject it into the spring container. Of course, it can also be realized by scanning a package, customizing annotations and so on.


import cn.hutool.core.io.file.FileReader;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.*;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @Title : bean Post-processor of factory for dynamic registration bean
 * @Date 2021/3/23 10:13
 * @Description
 * @Version
 */
@Component
@PropertySource("classpath:application.properties")
public class HttpProxyRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {

    /**
     *  This method is used to register more bean To spring In a container 
     *
     * @param beanDefinitionRegistry
     * @throws BeansException
     */
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        // Default UTF-8 Encoding, you can pass the first in the construction 2 Parameters as encoding 
        FileReader fileReader = new FileReader("proxy.txt");
        List<String> classStrList = fileReader.readLines();
        Set<Class<?>> proxyClazzSet = new HashSet<>();
        for (String s : classStrList) {
            if (StringUtils.isBlank(s)) {
                continue;
            }
            try {
                Class<?> aClass = Class.forName(s);
                proxyClazzSet.add(aClass);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }

        for (Class<?> targetClazz : proxyClazzSet) {
            BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(targetClazz);
            GenericBeanDefinition definition = (GenericBeanDefinition) beanDefinitionBuilder.getRawBeanDefinition();
            // Setting the parameters of the constructor    For Class<?>, You can either set it to Class, It can also be transmitted Class The full class name of 
            //definition.getConstructorArgumentValues().addGenericArgumentValue(targetClazz);
            definition.getConstructorArgumentValues().addGenericArgumentValue(targetClazz.getName());

            //Bean Specifies the type of a proxy interface 
            definition.setBeanClass(HttpProxyFactoryBean.class);
            // Denote   Automatically assemble according to the type of proxy interface 
            definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
            beanDefinitionRegistry.registerBeanDefinition(targetClazz.getName(),definition);
        }
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {

    }
}

Related articles: