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

本片文章重在理解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

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值