使用Spring Cloud做項目的同學會使用Feign這個組件進行遠程服務的調用,Feign這個組件采用模板的方式,有着優雅的代碼書寫規范。核心原理對Feign等相關注解進行解析,並提取信息,在Spring Boot工程啟動時,通過反射生產Request的bean,並將提取的信息,設置到bean中,最后注入到ioc容器中。
現在有這樣的場景,服務A提高RestApi接口,服務B、C、D等服務需要調用服務A提供的RestApi接口,這時最常見的做法是在服務B、C、D分別寫一個FeignClient,並需要寫RestApi接口的接收參數的實體和接收響應的實體DTo類。這樣的做法就是需要不停復制代碼。
有沒有辦法簡潔上面的操作呢?有一種最常見的做法是將將服務A進行模塊拆分,將FeignClient和常見的model、dto對外輸出的類單獨寫一個模塊,可以類似於取名a-service-open_share。這樣將服務A服務分為兩個模塊,即A服務的業務模塊和A服務需要被其他服務引用的公共類的模塊。服務B、C、D只需要引用服務A的a-service-open_share就具備調用服務A的能力。
筆者在這里遇到一個有趣的其問題。首先看問題:
寫一個FeignClient:
1 @FeignClient(name = "user-service") 2 public interface UserClient { 3 4 @GetMapping("/users") 5 List<User> getUsers(); 6 }
寫一個實現類:
1 @RestController 2 public class UserController implements UserClient { 3 @Autowired 4 UserService userService; 5 6 @OverRide 7 List<User> getUsers(){ 8 return userService.getUsers(); 9 } 10 }
啟動工程,瀏覽器訪問接口localhost:8008/users,竟然能正確訪問?!明明我在UserController類的getUsers方法沒有加RequestMapping這樣的注解。為何能正確的映射?!
帶着這樣的疑問,我進行了一番的分析和探索!
首先就是自己寫了一個demo,首先創建一個接口類:
1 public interface ITest { 2 @GetMapping("/test/hi") 3 public String hi(); 4 }
寫一個Controller類TestController
1 @RestController 2 public class TestController implements ITest { 3 @Override 4 public String hi() { 5 return "hi you !"; 6 } 7 }
啟動工程,瀏覽器訪問:http://localhost:8762/test/hi,瀏覽器顯示:
hi you !
我去,TestController類的方法 hi()能夠得到ITest的方法hi()的 @GetMapping("/test/hi")注解嗎? 答案肯定是獲取不到的。
特意編譯了TestController字節碼文件:
javap -c TestController
1 public class com.example.demo.web.TestController implements com.example.demo.web.ITest { 2 public com.example.demo.web.TestController(); 3 Code: 4 0: aload_0 5 1: invokespecial #1 // Method java/lang/Object."<init>":()V 6 4: return 7 8 public java.lang.String hi(); 9 Code: 10 0: ldc #2 // String hi you ! 11 2: areturn 12 }
上面的字節碼沒有任何關於@GetMapping("/test/hi")的信息,可見TestController直接獲取不到@GetMapping("/test/hi")的信息。
那應該是Spring MVC在啟動時在向容器注入Controller的Bean(HandlerAdapter)時做了處理。初步判斷應該是通過反射獲取到這些信息,並組裝到Controller的Bean中。首先看通過反射能不能獲取ITest的注解信息:
1 public static void main(String[] args) throws ClassNotFoundException { 2 Class c = Class.forName("com.example.demo.web.TestController"); 3 Class[] i=c.getInterfaces(); 4 System.out.println("start interfaces.." ); 5 for(Class clz:i){ 6 System.out.println(clz.getSimpleName()); 7 Method[] methods = clz.getMethods(); 8 for (Method method : methods) { 9 if (method.isAnnotationPresent(GetMapping.class)) { 10 GetMapping w = method.getAnnotation(GetMapping.class); 11 System.out.println("value:" + w.value()[0] ); 12 } 13 } 14 } 15 System.out.println("end interfaces.." ); 16 17 Method[] methods = c.getMethods(); 18 for (Method method : methods) { 19 if (method.isAnnotationPresent(GetMapping.class)) { 20 GetMapping w = method.getAnnotation(GetMapping.class); 21 System.out.println("value:" + w.value()); 22 } 23 } 24 }
允運行上面的代碼:
start interfaces…
ITest
value:/test/hi
end interfaces…
可見通過反射是TestController類是可以獲取其實現的接口的注解信息的。為了驗證Spring Mvc 在注入Controller的bean時通過反射獲取了其實現的接口的注解信息,並作為urlMapping進行了映射。於是查看了Spring Mvc 的源碼,經過一系列的跟蹤在RequestMappingHandlerMapping.java類找到了以下的方法:
1 protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) { 2 RequestMappingInfo info = createRequestMappingInfo(method); 3 if (info != null) { 4 RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); 5 if (typeInfo != null) { 6 info = typeInfo.combine(info); 7 } 8 } 9 return info; 10 }
繼續跟蹤源碼在AnnotatedElementUtils 類的searchWithFindSemantics()方法中發現了如下代碼片段:
1 // Search on methods in interfaces declared locally 2 Class<?>[] ifcs = method.getDeclaringClass().getInterfaces(); 3 result = searchOnInterfaces(method, annotationType, annotationName, containerType, processor, 4 visited, metaDepth, ifcs); 5 if (result != null) { 6 return result; 7 }
這就是我要尋找的代碼片段,驗證了我的猜測。
寫這篇文章我想告訴讀者兩件事:
- 可以將服務的對外類進行一個模塊的拆分,比如很多服務都需要用的FeignClient、model、dto、常量信息等,這些信息單獨打Jar,其他服務需要使用,引用下即可。
-
url映射不一定要寫在Contreller類的方法上,也可以寫在它實現的接口里面。貌似並沒有是luan用,哈。