自己动手实现一个简单的OpenFeign
写这个东西只是为了搞明白OpenFeign的实现原理,功能不全,仅供学习参考
定义注解
/**
* 模拟 SpringBoot 扫描包
**/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface EnableFeignClient {
String[] value();
}
/**
* 模拟 Feign 客户端配置,其它复杂的参数都省略了
**/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
String value() default "";
String url() default "";
}
/**
* 模拟 SpringMvc 注解,供后续程序提取参数使用
**/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GetMapping {
String value();
}
/**
* 模拟 SpringMvc 注解,供后续程序提取参数使用
**/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
String value();
}
实现逻辑
核心思想是利用java的动态代理,生成代理对象,就是下面的代码
// java.lang.reflect.Proxy
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h);
围绕上面的核心逻辑,依次扩展出下面的逻辑:
// feign 上下文对象
public class Feign {
InvocationFactory factory;
public Feign(InvocationFactory factory) {
this.factory = factory;
}
public <T> T newInstance(Class<T> type) {
FeignClient client = type.getAnnotation(FeignClient.class);
if (client == null) {
return newInstance(type, "");
} else {
return newInstance(type, client.url());
}
}
public <T> T newInstance(Class<T> type, String url) {
HardCodedTarget<T> target = new HardCodedTarget<>(type, url);
InvocationHandler invocationHandler = factory.create(target);
return (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type() }, invocationHandler);
}
}
// 代理执行器工厂类
public interface InvocationFactory {
InvocationHandler create(Target target);
static class DefaultInvocationFactory implements InvocationFactory {
@Override
public InvocationHandler create(Target target) {
return new MyInvocationHandler(target, null);
}
}
}
// 代理实现方法
public class MyInvocationHandler implements InvocationHandler {
private Target target;
private MyHandler handler;
public MyInvocationHandler(Target target, MyHandler handler) {
this.target = target;
this.handler = handler;
}
// 核心逻辑在这里
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
FeignClient client = (FeignClient) target.type().getAnnotation(FeignClient.class);
if (client == null) {
throw new Exception("这不是一个标准类");
}
// 用 get 请求进行测试
GetMapping get = method.getAnnotation(GetMapping.class);
if (get == null) {
return method.invoke(proxy, args);
}
StringBuilder sb = new StringBuilder();
sb.append(client.url()).append(get.value()).append("?");
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
Parameter p = parameters[i];
RequestParam requestParam = p.getAnnotation(RequestParam.class);
sb.append(requestParam.value()).append('=').append(args[i]).append('&');
}
// 用 hutool 包中的工具类去执行远程调用
String response = HttpUtil.get(sb.substring(0, sb.length() - 1));
// 处理返回值
Class<?> returnType = method.getReturnType();
if (returnType == String.class) {
return response;
} else {
return JSON.parseObject(response, returnType);
}
}
}
// 抄过来的客户端申明抽象
public interface Target<T> {
Class<T> type();
String name();
String url();
}
public class HardCodedTarget<T> implements Target<T> {
private Class<T> type;
private String name, url;
public HardCodedTarget(Class<T> type, String url) {
this.type = type;
this.url = url;
}
@Override
public Class<T> type() {
return type;
}
@Override
public String name() {
return name;
}
@Override
public String url() {
return url;
}
}
开始测试
申明测试接口:
// 本地的一个服务
@FeignClient(value = "testClient", url = "http://localhost:9995")
public interface TestClient {
@GetMapping("/order/detail")
String queryOrder(@RequestParam("orderId") String orderId);
}
测试类的主要工作就是模拟 OpenFeign 在 SpringBoot 中的工作流程
@EnableFeignClient({
"com.fang.test"
})
public class Test {
public static void main(String[] args) {
// 读取当前 main 注解信息 ->
// 对应 FeignClientsRegistrar.registerBeanDefinitions()
EnableFeignClient client = Test.class.getAnnotation(EnableFeignClient.class);
// 构建 环境上下文 ->
// 对应 FeignAutoConfiguration,OpenFeign 的自动装配配置
InvocationFactory.DefaultInvocationFactory factory = new InvocationFactory.DefaultInvocationFactory();
Feign feign = new Feign(factory);
// 根据注解加载出 需要的 class ->
// 对应 ClassPathScanningCandidateComponentProvider.scanCandidateComponents()
List<Class<?>> validClass = ClassUtil.getPackageAllClassName(client.value(), FeignClient.class);
// 生成代理对象
for (Class<?> c : validClass) {
// 在 SpringBoot 中,生成实例对象后,会放入单例池中,供后续注入使用
Object o = feign.newInstance(c);
if (o instanceof TestClient) {
TestClient test = (TestClient) o;
System.out.println(test.queryOrder("2651086162633039725"));
}
}
}
}
执行结果如下:
开始扫描包: com.fang.test
{"code":0,"data":{"actualPay":10...}}
顺道补一下 SpringBoot 加载 OpenFeign 的流程
1、启动类使用
注解,告诉
EnableFeignClient
SpringBoot
程序开启了这个模块
2、在
注解中,
EnableFeignClient
了
Import
类,该类由于实现了 SpringBoot 规范接口
FeignClientsRegistrar
,从而在 SpringBoot 有名的
ImportBeanDefinitionRegistrar
方法中(
refresh()
invokeBeanFactoryPostProcessors
)注册相关的Bean定义
3、在
中的
refresh()
阶段生成动态代理对象,最终调用到
finishBeanFactoryInitialization
生成代理对象,并放入
ReflectiveFeign.newInstance()
DefaultSingletonBeanRegistry.singletonObjects
中(IOC的单例池)
4、SpringBoot 构建对象的依赖关系时,会将代理对象进行注入;代理对象拦截该接口行为,翻译成远程调用,然后生成该接口的返回值类型即可。
补全工具类
往上抄来的,来源: https://blog.csdn.net/jdzms23/article/details/17550119
public class ClassUtil {
public static List<Class<?>> getPackageAllClassName(String[] packages, Class annotation) {
List<Class<?>> result = new ArrayList<>();
for (String p : packages) {
System.err.println("开始扫描包: " + p);
List<Class<?>> aClass = getClass(p);
if (aClass.isEmpty()) {
continue;
}
for (Class<?> cc : aClass) {
// 过滤我需要的东西
Annotation client = cc.getAnnotation(annotation);
if (client != null) {
result.add(cc);
}
}
}
return result;
}
public static List<Class<?>> getClass(String packageName) {
List<Class<?>> result = new ArrayList<>();
String dir = packageName.replace(".", "/");
try {
Enumeration<URL> files = Thread.currentThread().getContextClassLoader().getResources(dir);
while (files.hasMoreElements()) {
URL url = files.nextElement();
String protocol = url.getProtocol();
if ("file".equals(protocol)) {
String filePath = URLDecoder.decode(url.getFile(), "utf-8");
result.addAll(findClassInPackageByFile(packageName, filePath, true));
} else if ("jar".equals(protocol)) {
JarFile jar = ((JarURLConnection) url.openConnection()).getJarFile();
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
if (name.charAt(0) == '/') {
name = name.substring(1);
}
if (name.startsWith(dir)) {
int idx = name.lastIndexOf('/');
if (idx != -1) {
packageName = name.substring(0, idx).replace('/', '.');
}
if (idx != -1) {
if (name.endsWith(".class") && !entry.isDirectory()) {
String className = name.substring(packageName.length() + 1, name.length() - 6);
result.add(Class.forName(packageName + '.' + className));
}
}
}
}
}
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return result;
}
private static List<Class<?>> findClassInPackageByFile(String packageName, String packagePath, final boolean recursive) {
List<Class<?>> result = new ArrayList<>();
File dir = new File(packagePath);
if (!dir.exists() || !dir.isDirectory()) {
return result;
}
File[] files = dir.listFiles(file -> (recursive && file.isDirectory()) || file.getName().endsWith(".class") || file.getName().endsWith(".java"));
if (files == null || files.length <= 0) {
return result;
}
for (File file : files) {
if (file.isDirectory()) {
List<Class<?>> childResult = findClassInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive);
result.addAll(childResult);
} else {
String className = file.getName().substring(0, file.getName().length() - 6);
try {
result.add(Class.forName(packageName + "." + className));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
return result;
}
}