公司是採用微服務來作模塊化的,各個模塊之間採用dubbo通訊。好處就不用提了,省略了以前模塊間複雜的http訪問。不過也遇到一些問題:前端
PS: Github的代碼示例java
對於開發來講,卻是挺省勁。可是對於測試來講就有點麻煩了, 每次還要去寫dubbo的消費程序,並且每次新增一個接口,都須要從新改寫程序,費時費力。jquery
因爲我這邊是作一些商品的推薦,每次結果的類型都是相同的,只是內部的算法不一樣。不過接口只是返回id,沒法直觀的判斷商品類似程度或者用戶的偏好程度,須要一個可視化的返回結果界面。git
因而在這種需求下,我設想了一個小程序,它能夠知足下面的功能:github
提早放一個效果圖:
web
小程序開始的第一步就是須要掃描某個包下全部的dubbo實現類。ajax
因爲工程是springboot,所以最終部署是在jar中。這時,就須要面臨兩個問題,若是是在開發工具中,如何獲取包下的全部類;若是是在jar中,如何獲取包下全部的類。算法
首先經過classloader能夠加載特定路徑下的全部URL:spring
Enumeration<URL> dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName); while (dirs.hasMoreElements()) { URL url = dirs.nextElement(); //若是是jar,則採用JarURLConnection的方式鏈接,得到class文件 if("jar".equals(url.getProtocol())){ findClassesInJar(url,classes,pack); }else{ findClassesInSrc(url,classes,pack); } }
在工程中,class實際上是以目錄形式存放在本地的,直接按照file的方式遍歷掃描class文件就好了:小程序
public static void findClassesInSrc(URL url, Set<Class<?>> classes, String basePackage) throws UnsupportedEncodingException { File dir = new File(URLDecoder.decode(url.getFile(), "UTF-8")); if (!dir.exists() || !dir.isDirectory()) { return; } Arrays.stream(dir.listFiles()) .forEach(file -> { String className = file.getName().substring(0, file.getName().length() - 6); try { classes.add(Thread.currentThread().getContextClassLoader().loadClass(basePackage + '.' + className)); } catch (ClassNotFoundException e) { e.printStackTrace(); } }); }
jar包是一種特殊的壓縮包,java提供了JarFile類的entries方法,能夠遍歷jar中全部的文件。不過這裏就沒有目錄或者文件的區別了,由於都是一個zip包中的資源而已。所以最後須要針對報名進行一下過濾:
public static void findClassesInJar(URL url,Set<Class<?>> classes,String basePackage) throws IOException, ClassNotFoundException { //轉換爲JarURLConnection JarURLConnection connection = (JarURLConnection) url.openConnection(); if (connection != null) { JarFile jarFile = connection.getJarFile(); if (jarFile != null) { //獲得該jar文件下面的類實體 Enumeration<JarEntry> jarEntryEnumeration = jarFile.entries(); while (jarEntryEnumeration.hasMoreElements()) { JarEntry entry = jarEntryEnumeration.nextElement(); String jarEntryName = entry.getName(); //這裏咱們須要過濾不是class文件和不在basePack包名下的類 if (jarEntryName.contains(".class") && jarEntryName.replaceAll("/",".").startsWith(basePackage)) { String className = jarEntryName .substring(0, jarEntryName.lastIndexOf(".")) .replace("/", "."); classes.add(Thread.currentThread().getContextClassLoader().loadClass(className)); } } } } }
而後經過反射能夠直接經過class的名字,拿到它的全部方法,這些方法裏面包含了一些通用的方法,如wait,notify等,須要給過濾掉。
public static Set<String> NORMAL_METHODS = new HashSet<>(Arrays.asList("wait","equals","toString","hashCode","getClass","notify","notifyAll")); public static List<Method> getMethod(String className){ try { Class clazz = Thread.currentThread().getContextClassLoader().loadClass(className); Method[] methods = clazz.getMethods(); return Arrays.stream(methods) .filter(method -> !NORMAL_METHODS.contains(method.getName())) .collect(Collectors.toList()); } catch (ClassNotFoundException e) { e.printStackTrace(); } return new ArrayList<>(); }
這裏須要注意,兩個不一樣參數的方法,雖然名字相同,可是他們的parameterTypes是不一樣的。所以這裏最好直接返回method,把name和parameterTypes一同做爲結果返回。由於最終invoke的時候,還得經過參數類型把全部的參數都轉換類型一下。
第三個難點,就是前端傳過來的參數都是字符串,好比:
com.xingoo.test.Provider1Impl
是對應的classtest1
是對應的方法100
是對應的參數java.lang.Long
是參數對應的類型怎麼能把請求經過正確的dubbo provider執行呢?——答案 就是Bean
由於在Spring的項目中,dubbo的provider都是一個單例的bean。所以能夠直接經過applicationContext得到對應的bean,只要保證bean的名字能規律的映射過來就行。
能夠參考下面的獲取bean的方法:
import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @Component public class SpringUtils implements ApplicationContextAware { private static ApplicationContext applicationContext = null; // 非@import顯式注入,@Component是必須的,且該類必須與main同包或子包 // 若非同包或子包,則需手動import 注入,有沒有@Component都同樣 // 可複製到Test同包測試 @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { if(SpringUtils.applicationContext == null){ SpringUtils.applicationContext = applicationContext; } } //獲取applicationContext public static ApplicationContext getApplicationContext() { return applicationContext; } //經過name獲取 Bean. public static Object getBean(String name){ return getApplicationContext().getBean(name); } //經過class獲取Bean. public static <T> T getBean(Class<T> clazz){ return getApplicationContext().getBean(clazz); } //經過name,以及Clazz返回指定的Bean public static <T> T getBean(String name,Class<T> clazz){ return getApplicationContext().getBean(name, clazz); } }
在真正的實現類上,須要指定bean的名字:
@Service("Provider1Impl") public class Provider1Impl implements ProviderApi { ... }
而後利用反射,就能夠執行這個bean的特定方法了:
// 反射拿到對應的class Class cla = Thread.currentThread().getContextClassLoader().loadClass(clazz); // 在appContext中拿到對應的bean Object bean = SpringUtils.getBean(cla.getSimpleName()); // 格式化參數與參數類型 Class<?>[] parameterTypes = DubboApiUtils.paramTypeFormat(types); Object[] parameters = DubboApiUtils.paramsFormat(params,types); // 經過反射調用對應的方法 return cla.getMethod(method, parameterTypes).invoke(bean,parameters);
對應參數處理的兩個方法是:
/** * 根據字符串拼接,得到對應的參數類型數組 * @param types * @return */ public static Class<?>[] paramTypeFormat(String types){ List<Class<?>> paramsClasses = new ArrayList<>(); for(String type : types.split(",")){ try { paramsClasses.add(Class.forName(type)); } catch (ClassNotFoundException e) { e.printStackTrace(); } } return paramsClasses.toArray(new Class[]{}); } /** * 根據參數類型,轉換類型 * @param paramStr * @param types * @return */ public static Object[] paramsFormat(String paramStr,String types){ Class<?>[] classes = paramTypeFormat(types); List<Object> formats = new ArrayList<>(); String[] params = paramStr.split(","); for(int i =0;i<classes.length; i++){ //todo 簡單粗暴,有其餘的須要再加吧 if("Long".equals(classes[i].getSimpleName())){ formats.add(Long.valueOf(params[i])); }else{ formats.add(params[i]); } } return formats.toArray(); }
最後就是jquery基於ajax請求,查詢對應的接口結果就好了。須要注意ajax的同步問題:
$.ajax({ type : "post", url : "xxxxx", data : {xxx:xxx}, async : false, success : function(r){ //todo } });
總結來講,下面是遇到的問題和簡單的對應辦法:
經過這樣一個小工具,又對反射有了更進一步的瞭解。:-)))))))))
參考: