公司是采用微服務來做子產品化的,各個子產品之間采用dubbo通信。好處就不用提了,省略了之前子產品間複雜的http通路。不過也遇到一些問題:
PS: Github的代碼示例
測試需要配合寫消費者的代碼
對于開發來說,倒是挺省勁。但是對于測試來說就有點麻煩了, 每次還要去寫dubbo的消費程式,而且每次新增一個接口,都需要重新改寫程式,費時費力。
接口傳回的結果無法定制
由于我這邊是做一些商品的推薦,每次結果的類型都是相同的,隻是内部的算法不同。不過接口隻是傳回id,無法直覺的判斷商品相似程度或者使用者的偏好程度,需要一個可視化的傳回結果界面。
于是在這種需求下,我設想了一個小程式,它可以滿足下面的功能:
- 測試可以根據測試需要,在界面自動選擇請求的class和方法
- 開發完成後,測試界面自動掃描出dubbo的提供者的class和對應的方法
- 傳回結果自動請求對應的圖檔和文字說明
提前放一個效果圖:

1 掃描某個包下所有的類
小程式開始的第一步就是需要掃描某個包下所有的dubbo實作類。
由于工程是springboot,是以最終部署是在jar中。這時,就需要面臨兩個問題,如果是在開發工具中,如何擷取包下的所有類;如果是在jar中,如何擷取包下所有的類。
首先通過classloader可以加載特定路徑下的所有URL:
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 方法的執行
第三個難點,就是前端傳過來的參數都是字元串,比如:
-
是對應的classcom.xingoo.test.Provider1Impl
-
是對應的方法test1
-
是對應的參數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,需要注意傳入的參數與類型問題
通過這樣一個小工具,又對反射有了更進一步的了解。😃))))))))
參考:
- Java掃描特定包下的類
- Dubbo官方文檔
作者:xingoo
出處:http://www.cnblogs.com/xing901022
本文版權歸作者和部落格園共有。歡迎轉載,但必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接!