自己动手实现一个简单的OpenFeign

自己动手实现一个简单的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注解中,ImportFeignClientsRegistrar类,该类由于实现了 SpringBoot 规范接口 ImportBeanDefinitionRegistrar,从而在 SpringBoot 有名的 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;
    }


}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值