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管理。
按照思路我们一步一步来:
- 先将接口的动态实现成对象,至于如何让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;
}
}
- FactoryBean,也是一个bean,这个bean是啥bean,取决于 该方法的getObject()方法的实现返回,该返回是啥就是啥bean。有了bean,就要想办法把bean交给spring。
- Proxy.newProxyInstance(params1,params2,params3) 这个方法就是jdk动态代理生成动态类的方式。我这里简单的件是下 3个参数分分别代表什么意思:
- Thread.currentThread().getContextClassLoader(): 表示jdk动态代理生成的类需要放在什么地方。
- new Class[]{type}: jdk动态代理是基于接口才能实现的,而java是多继承的,所以这个参数传入的是需要实现的接口。多继承,所以可能会有多个接口,这里接受的是数组
- 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; }
- 通过上述操作,我们可以就可以创建对象,又通过实现 FactoryBean 把生成的对象变成了spring的bean.
- 最后我们就该思考,如果把这个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
上一篇: Java框架技术核心基石系列教程—利用Class全面解析类信息
下一篇: Java基础语法题目练习