自己動手實作一個簡單的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;
}
}