SpringBoot 动态代理实现三方接口调用


目录
  • 一、定义注解
  • 二、建立动态代理类
  • 三、注入spring容器
  • 四、编写拦截器
  • 五、创建客户端调用类
  • 六、main方法测试
  • 七、启动项目


在某些业务场景中,我们只需要业务代码中定义相应的接口或者相应的注解,并不需要实现对应的逻辑。

比如 mybatis和feign: 在 mybatis 中,我们只需要定义对应的mapper接口;在 feign 中,我们只需要定义对应业务系统中的接口即可。

那么在这种场景下,具体的业务逻辑时怎么执行的呢,其实原理都是动态代理。

我们这里不具体介绍动态代理,主要看一下它在springboot项目中的实际应用,下面我们模仿feign来实现一个调用三方接口的 httpclient。

一、定义注解

定义好注解,方便后面扫描使用了该注解的类和方法

package com.mysgk.blogdemo.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * @author mysgk
 * @link https://www.cnblogs.com/mysgk/p/15619895.html
 * 

* 这里应该还有一个注解来标识方法,demo为了方便都使用该注解 */ @Retention(RUNTIME) @Target({TYPE, METHOD}) @Documented public @interface MyHttpClient { }

二、建立动态代理类

用于产生代理类并执行对应方法

package com.mysgk.blogdemo.proxy;

import org.springframework.beans.factory.FactoryBean;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @author mysgk
 * @link https://www.cnblogs.com/mysgk/p/15619895.html
 */
public class RibbonAopProxyFactory implements FactoryBean, InvocationHandler {

	private Class interfaceClass;

	public Class getInterfaceClass() {
		return interfaceClass;
	}

	public void setInterfaceClass(Class interfaceClass) {
		this.interfaceClass = interfaceClass;
	}

	@Override
	public T getObject() throws Exception {
		final Class[] interfaces = {interfaceClass};
		return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), interfaces, this);
	}

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

	@Override
	public boolean isSingleton() {
		return true;
	}

	/**
	 * 真正执行的方法,会被aop拦截
	 */
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		return "invoke " + interfaceClass.getName() + "." + method.getName() + " , do anything ..";
	}
}

三、注入spring容器

在项目启动时,扫描第一步定义的注解,生成该类的实现类,并将其注入到spring容器

package com.mysgk.blogdemo.start;

import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.StrUtil;
import com.mysgk.blogdemo.annotation.MyHttpClient;
import com.mysgk.blogdemo.proxy.RibbonAopProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.stereotype.Component;
import java.util.Set;

/**
 * @author mysgk
 * @link https://www.cnblogs.com/mysgk/p/15619895.html
 */
@Component
public class ScanHttpClients implements BeanDefinitionRegistryPostProcessor {

	public void run(BeanDefinitionRegistry registry) {

		Set> scanPackage = ClassUtil.scanPackageByAnnotation("com.mysgk", MyHttpClient.class);

		for (Class<?> cls : scanPackage) {
			BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(cls);
			GenericBeanDefinition definition = (GenericBeanDefinition) builder.getRawBeanDefinition();
			definition.getPropertyValues().add("interfaceClass", definition.getBeanClassName());
			definition.setBeanClass(RibbonAopProxyFactory.class);
			definition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);
			String beanName = StrUtil.removePreAndLowerFirst(cls.getSimpleName(), 0) + "RibbonClient";
			registry.registerBeanDefinition(beanName, definition);
		}
	}

	@Override
	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
		run(registry);
	}

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

	}
}

四、编写拦截器

拦截动态代理生成的方法,实现我们真正的业务逻辑


package com.mysgk.blogdemo.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

/**
 * @author mysgk
 * @link https://www.cnblogs.com/mysgk/p/15619895.html
 */
@Component
@Aspect
public class InterceptAnnotation {

	@Autowired
	RestTemplate ribbonLoadBalanced;

	@Pointcut("@annotation(com.mysgk.blogdemo.annotation.MyHttpClient)")
	public void execute() {

	}

	@Around("execute()")
	public Object interceptAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
		/**
		 * 此处省略 获取 url, httpMethod, requestEntity, responseType 等参数的处理过程
		 */
		ResponseEntity<?> exchange = ribbonLoadBalanced.exchange("http://www.baidu.com", HttpMethod.GET, HttpEntity.EMPTY, Object.class);
		return exchange.getBody();
	}

}

五、创建客户端调用类

此类实现将远程接口当做本地方法调用

package com.mysgk.blogdemo.client;

import com.mysgk.blogdemo.annotation.MyHttpClient;
import org.springframework.web.bind.annotation.PostMapping;

/**
 * @author mysgk
 * @link https://www.cnblogs.com/mysgk/p/15619895.html
 */
@MyHttpClient
public interface MyHttpClientTest {

	@MyHttpClient
	@PostMapping(value = "test/t1")
	Object test(String param);

}

六、main方法测试

注入客户端进行调用

package com.mysgk.blogdemo;

import com.mysgk.blogdemo.client.MyHttpClientTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @author mysgk
 * @link https://www.cnblogs.com/mysgk/p/15619895.html
 */
@SpringBootApplication
@RestController
public class Main {

	@Bean
	RestTemplate restTemplate() {
		return new RestTemplate();
	}

	@Autowired
	MyHttpClientTest myHttpClientTest;

	public static void main(String[] args) {
		SpringApplication.run(Main.class, args);
	}

	@GetMapping("/t1")
	public Object t1() {
		return myHttpClientTest.test("1");
	}
}

七、启动项目

访问 127.0.0.1:8080/t1,得到如下结果

此时我们调用的是

myHttpClientTest.test

但是真正运行的是

RibbonAopProxyFactory.invoke 

此时返回的应该是

invoke com.mysgk.blogdemo.client.MyHttpClientTest.test  , do anything ..

由于我们使用aop拦截了该方法,所以最终的结果是访问到了baidu
调用流程图

最后附上pom

<?xml version="1.0" encoding="UTF-8"?>

    4.0.0

    org.example
    bolgdemo
    1.0-SNAPSHOT

    

        
            org.springframework.boot
            spring-boot-starter-web
            2.1.6.RELEASE
        
        
            org.springframework.boot
            spring-boot-starter-aop
            2.1.6.RELEASE
        
        
            cn.hutool
            hutool-all
            5.3.10
        
        
            org.springframework.cloud
            spring-cloud-starter-netflix-ribbon
            2.1.5.RELEASE
            
                
                    httpclient
                    org.apache.httpcomponents
                
            
        
        
            httpclient
            org.apache.httpcomponents
            4.5.13
        
    

    
        
            
                org.apache.maven.plugins
                maven-compiler-plugin
                3.5.1
                
                    1.8
                    1.8
                
            
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            repackage