欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

FeignClient原理解析,100行代码实现feign功能,mybatis的mapper、dubbo、feign实现原理模拟。spring扫描自定义注解原理。Javassist实现动态代理原理

程序员文章站 2022-06-28 20:27:19
本片文章重在理解spring的扩展机制。理解了扩展机制。今后可以自行灵活对spring进行扩展。Fegin的功能需要有一定的认识,简单的说Fegin承担的责任就是让服务A去调用服务B的接口,比如服务B写了一个controler,服务A想要调用这个controller,就可以通过fegin直接调用,大部分使用场景是在现象:在使用feign,或者mybatis等框架的时候,都有一个特点就是会先定义一个接口,比如:mybaits的mapper, feign的 client。但是我们知道,spring的在帮我们...

本片文章重在理解spring的扩展机制。理解了扩展机制。今后可以自行灵活对spring进行扩展。

  • 背景介绍:Fegin的功能需要有一定的认识,简单的说Fegin承担的责任就是让服务A去调用服务B的接口,比如服务B写了一个controler,服务A想要调用这个controller,就可以通过fegin直接调用,大部分使用场景是在 Spring Cloud中做RPC调用时候使用。
  • 实际现象:在使用feign,或者mybatis等框架的时候,都有一个特点就是会先定义一个接口,比如:mybaits的mapper, feign的 client。但是我们知道,spring的在帮我们管理bean的时候,也只能管理具体的一个类,而不能管理接口,就算是接口,在管理的时候,也只是会去找接口的实现,如果没有实现,或者存在多个实现,就会报错,当然可以通过注入方式,来指定管理哪个具体的对象,这块属于spring的注入方式,以后可以单独拿出来讲解。

基于以上现象我们需要思考几个问题:

  • 这些框架使用的接口是如何被spring所管理的?
  • 像@Mapper 以及 @RestClient 等注解,spring是如何认识这些注解的?我们能不能自己写个注解也让spring扫描呢?
  • 想让spring来帮我们管理bean,除了使用@Compent 或者 @Bean等,spring本身支持,也是我们常用的注解外,还有什么办法可以把我们的bean交给管理??
  • .在使用Feign的时候,我们都是通过一个@EnableFeignClients 这又是怎么回是?

带着上述的问题,我们来通过代码一步步的去实现一个feign,从而一步一步解开spring的真相。

  • 大神都是从模仿开始的。那么我们也从模仿框架开始
    • 模仿框架,我们也写一个@EnableFeignClients注解, 就叫做 @EnableSongFeignClient
	@Target(ElementType.TYPE)
	@Retention(RetentionPolicy.RUNTIME)
	@Documented
	@Import(RestClientTest.class)
	public static @interface EnableSongFeignClient {}
  • 有了上述这个注解,我们也可以像springBoot一样,把这个注解写到启动类的上面。

    • 原理很简单,为什么要使用这个才能开启我们的Fegin?

    目前就是要@Import(RestClientTest.class),导入SongFeignClientTest.class这个类,只有这样写了,spring在启动的时候,才会去执行SongFeignClientTest该类。

  • 那么该类又做了什么事情呢?

  • 为什么要启动的时候,要去执行这个类,这个类又和我们实现Feign有啥关系呢?

  • 正如上面所解释的内容一样,我们看看SongFeignClientTestRegistrar .class这个类又做了什么事情?

带着问题,我们继续往下看?

public class SongFeignClientTestRegistrar implements ImportBeanDefinitionRegistrar{
@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		
	}
}

ImportBeanDefinitionRegistrar接口定义的类,其复写的方法只有在通过{@code @Import}方式注入时,才会被执行。

  • 看到这个类,恍然大悟,原来这个类还实现了ImportBeanDefinitionRegistrar 这个方法,通过类名字,我们不难理解,就是给spring注册一个BD,(我们知道,spring管理bean,第一步就是把bean扫描成BD),所以我们可以得出,这个方法就是用来让spring帮我们管理bean的,只要注册了BD,spring自然就会帮我们创建bean。那他这里帮我们管理什么对象,我们还没有对象呢??
  • 那么我们就继续模仿:写另一个注解:@FeignClient , 就叫做 @SongFeignClient
@Target(ElementType.TYPE)
	@Retention(RetentionPolicy.RUNTIME)
	@Documented
	public static @interface SongFeignClient {
		string baseUrl()
	}
}

我们要实现的功能就是:凡是加了@SongFeignClient这个注解的接口,接口里面的方法,就会去帮我们调用方法指定的服务。类似于@Mapping,加了这个注解,mybatis,就会去执行sql,一个道理。

整理思路:

  • 找到加有@SongFeignClient 注解的接口
  • 对接口进行一个动态实现,重写接口里面的方法,方法内容就是进行RPC调用的具体逻辑。
  • 将接口实现的对象交给spring管理。
按照思路我们一步一步来:
  1. 先将接口的动态实现成对象,至于如何让spring管理,我们等实现成对象在说。思考:像让一个接口 变成对象,又不通过直接写java去实现,可以做到的技术还是有很多的,而Feign等相关技术的源码都是通过Javassist,而我们可以使用 jdk动态代理去实现。之后我会把使用Javassist的方式也写在文章最后。

	public static class SongClientFactoryBean implements FactoryBean<Object> {
		
		private final Class<?> type ;
		private final String baseUrl ;
		
		public RestClientFactoryBean(Class<?> type, String baseUrl) {
			this.type = type;
			this.baseUrl = baseUrl;
		}

		@Override
		public Object getObject() throws Exception {
			return Proxy.newProxyInstance(
					Thread.currentThread().getContextClassLoader(),
					new Class[]{type},
					new SongClientImpl(baseUrl));
		}

		@Override
		public Class<?> getObjectType() {
			return type;
		}
		
	}
  1. FactoryBean,也是一个bean,这个bean是啥bean,取决于 该方法的getObject()方法的实现返回,该返回是啥就是啥bean。有了bean,就要想办法把bean交给spring。
  2. Proxy.newProxyInstance(params1,params2,params3) 这个方法就是jdk动态代理生成动态类的方式。我这里简单的件是下 3个参数分分别代表什么意思:
  3. Thread.currentThread().getContextClassLoader(): 表示jdk动态代理生成的类需要放在什么地方。
  4. new Class[]{type}: jdk动态代理是基于接口才能实现的,而java是多继承的,所以这个参数传入的是需要实现的接口。多继承,所以可能会有多个接口,这里接受的是数组
  5. InvocationHandler 第三个参数,是接口的方法,具体的实现内容。写的是我们的实现接口后要做的事情。因为方法名字森罗万象,所以必须要统一起来,统一的办法就是必须实现InvocationHandler的类才能传入。我这里实现InvocationHandler这个接口的对象叫:SongClientImpl :具体内容如下:
public static class SongClientImpl implements InvocationHandler{
		  private final RestTemplate restTemplate = new RestTemplate();
		  private final String baseUrl ;
		  public RestClientImpl(String baseUrl){
		 		 this.baseUrl = baseUrl;
		}
		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			GetMapping get = AnnotationUtils.findAnnotation(method, GetMapping.class);
			if (get != null) {
				String url = get.value()[0];
				// 如果有ribbon,会在这里发挥作用
				return restTemplate.getForObject(baseUrl + url, method.getReturnType());
			}
			PostMapping post = AnnotationUtils.findAnnotation(method, PostMapping.class);
			if (post != null) {
				String url = post.value()[0];
				return restTemplate.postForObject(baseUrl + url, args[0], method.getReturnType());
			}
			return null;
	}
  1. 通过上述操作,我们可以就可以创建对象,又通过实现 FactoryBean 把生成的对象变成了spring的bean.
  2. 最后我们就该思考,如果把这个bean,交给spring管理了,这里我们就用到最开始通过@EnableSongFeignClient 注册进来的那个 SongFeignClientTestRegistrar 类了。
public class SongFeignClientTestRegistrar implements ImportBeanDefinitionRegistrar{
	/*
	*
	* 交给spring的办法就是在这里,这个就是像的办法,把这个类通过register注册给spring
	* 注册的思路:
	* 	1.通过扫描剋,把我们自己添加的注解扫描到。
	* 	2.扫描到之后,把扫描的添加的注解的 接口交给上面的 RestClientFactoryBean 让他帮忙生成 代理对象
	*   3.把生成的代理对象交给spring管理
	*
	* */
	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		// 扫描类
		ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false){
			// 将接口也扫进来
			@Override
			protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
				if (beanDefinition.getMetadata().isInterface()) {
					return true ;
				}
				return super.isCandidateComponent(beanDefinition);
			}
		};
		// 只扫描RestClient注解注释的接口
		scanner.addIncludeFilter(new AnnotationTypeFilter(RestClient.class));
		
		String scanPackage;
		try {
			// 设置扫描路径
			scanPackage = Class.forName(importingClassMetadata.getClassName()).getPackage().getName();
		} catch (ClassNotFoundException e1) {
			throw new RuntimeException(e1);
		}
		
		for (BeanDefinition b : scanner.findCandidateComponents(scanPackage)) {
			AnnotatedBeanDefinition abd = (AnnotatedBeanDefinition) b;
			// 获取RestClient注解值
			String baseUrl = (String) abd.getMetadata().getAnnotationAttributes(RestClient.class.getName(),true).get("baseUrl");
			try {
				
				// 动态定义bean
				registry.registerBeanDefinition("rest-client--" + b.getBeanClassName(),
						BeanDefinitionBuilder.genericBeanDefinition(RestClientFactoryBean.class)
						.addConstructorArgValue(Class.forName(b.getBeanClassName()))
						.addConstructorArgValue(baseUrl)
						.getBeanDefinition());
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}
}

整理思路

  • 通过扫描剋,把我们自己添加的注解扫描到。
  • 扫描到之后,把扫描的添加的注解的 接口交给上面的 RestClientFactoryBean 让他帮忙生成 代理对象
  • 把生成的代理对象交给spring管理

至此我们就完成了,一个基本原理的feign。

关于spring是如果扫描我们自定义的注解原理

  • ClassPathScanningCandidateComponentProvider类是spring中用来做类/包扫描的工具.
  • 包括classpath下所有的包,比如jar中的。你平时使用的ComponentScan注解用的就是这个类处理的。
  • 关键他会通过asm解析类,而不是通过动态加载解析类,这意味着类不会被初始化。
  • 而且你可以自定义扫描规则,例如只返回指定注解的类等等。
public class ClassPathScanningCandidateComponentProvider类妙用 {
	
	public static void main(String[] args) {
		// new的参数设置false,表示不适用spring自己内置的规则,通常自定义规范的时候都不需要设置true。
		ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false){
			// 因为默认不回去返回接口,这样写可以将接口也扫进来
			// 但是注意,这只能说是让接口作为候选类,并不一定会返回,关键还是要看规则是否匹配
			@Override
			protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
				if (beanDefinition.getMetadata().isInterface()) {
					return true ;
				}
				return super.isCandidateComponent(beanDefinition);
			}
		};
		// 自定义规则,扫描具有指定注解的类
		scanner.addIncludeFilter(new AnnotationTypeFilter(RestClient.class));
		scanner.findCandidateComponents("com.dragonsoft").forEach(System.out :: println);
		
	}

彩蛋

如何看到这里,那么久在附送一个Javassist实现动态代理原理

public class JavassistTest {
	
	public static void main(String[] args) throws Exception{
		// 创建一个新的ClassPool,这样它可以被回收,同时它内部的CtClass也可以被回收,以避免内存溢出
		ClassPool cp = new ClassPool(true);
		CtClass cls = cp.makeClass("com.gframework.samplecode.BB");
		cls.addInterface(cp.get(AA.class.getName()));
		CtMethod f = new CtMethod(CtClass.voidType, "fun", null, cls);
		f.setModifiers(Modifier.PUBLIC);
		f.setBody("{"
				+ "System.out.println(\"Hello\");"
				+ "System.out.println(\"World\");"
				+ "}");
		cls.addMethod(f);
		
		CtMethod f2 = new CtMethod(CtClass.voidType, "fun2", new CtClass[]{cp.get("java.lang.String")}, cls);
		f2.setModifiers(Modifier.PUBLIC);
		f2.setBody("{"
				+ "String ddd = $1;"
				+ "System.out.println(\"Hello22\" + ddd + new java.util.Date());"
				+ "System.out.println(\"World22\");"
				+ "}");
		cls.addMethod(f2);
		
		
		AA a = (AA)cls.toClass().newInstance();
		a.fun();
		a.fun2("AAAAA");
	}

}
interface AA{
	public void fun();
	public void fun2(String str);
}
  • CtClass需要关注的方法:
  • freeze : 冻结一个类,使其不可修改;
  • isFrozen : 判断一个类是否已被冻结;
  • prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
  • defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
  • detach : 将该class从ClassPool中删除;
  • writeFile : 根据CtClass生成 .class 文件;
  • toClass : 通过类加载器加载该CtClass。
  • CtMethod中的一些重要方法:
  • insertBefore : 在方法的起始位置插入代码;
  • insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
  • insertAt : 在指定的位置插入代码;
  • setBody : 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
  • make : 创建一个新的方法。

***全是干货,不知道自己有没有说清楚,希望各位可以有所收获。期待下次更新,pice

本文地址:https://blog.csdn.net/qq_33990603/article/details/112671800