公司是采用微服务来作模块化的,各个模块之间采用dubbo通讯。好处就不用提了,省略了以前模块间复杂的http访问。不过也遇到一些问题:前端
PS: Github的代码示例java
测试须要配合写消费者的代码
对于开发来讲,却是挺省劲。可是对于测试来讲就有点麻烦了, 每次还要去写dubbo的消费程序,并且每次新增一个接口,都须要从新改写程序,费时费力。jquery
接口返回的结果没法定制
因为我这边是作一些商品的推荐,每次结果的类型都是相同的,只是内部的算法不一样。不过接口只是返回id,没法直观的判断商品类似程度或者用户的偏好程度,须要一个可视化的返回结果界面。git
因而在这种需求下,我设想了一个小程序,它能够知足下面的功能:github
- 测试能够根据测试须要,在界面自动选择请求的class和方法
- 开发完成后,测试界面自动扫描出dubbo的提供者的class和对应的方法
- 返回结果自动请求对应的图片和文字说明
提早放一个效果图: web
1 扫描某个包下全部的类
小程序开始的第一步就是须要扫描某个包下全部的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包中
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)); } } } } }
2 扫描某个class下全部的方法
得到某个类的全部方法
而后经过反射能够直接经过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的时候,还得经过参数类型把全部的参数都转换类型一下。
3 方法的执行
第三个难点,就是前端传过来的参数都是字符串,好比:
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(); }
4 商品自动请求描述信息
最后就是jquery基于ajax请求,查询对应的接口结果就好了。须要注意ajax的同步问题:
$.ajax({ type : "post", url : "xxxxx", data : {xxx:xxx}, async : false, success : function(r){ //todo } });
总结
总结来讲,下面是遇到的问题和简单的对应办法:
- 1 如何扫描工程或者普通web项目 某个包下的class——经过classloader得到路径,直接遍历file便可
- 2 如何扫描jar中某个包下的class——经过JarFile得到对应的JarEntry
- 3 如何获取Spring Boot中的Bean——经过实现ApplicationContextAware接口,获取applicationContext的引用
- 4 如何动态执行某个对象的特定方法——基于反射method.invoke,须要注意传入的参数与类型问题
经过这样一个小工具,又对反射有了更进一步的了解。:-)))))))))
参考: