diff --git a/pom.xml b/pom.xml index adc56ca..7f7cd1a 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 unitauto unitauto - 2.6.3 + 2.7.1 UnitAuto @@ -27,7 +27,7 @@ com.alibaba fastjson - 1.2.74 + 1.2.79 diff --git a/src/main/java/unitauto/MethodUtil.java b/src/main/java/unitauto/MethodUtil.java index 6600468..d30ff65 100644 --- a/src/main/java/unitauto/MethodUtil.java +++ b/src/main/java/unitauto/MethodUtil.java @@ -46,6 +46,7 @@ import java.util.NavigableMap; import java.util.NavigableSet; import java.util.Objects; +import java.util.Queue; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; @@ -135,14 +136,22 @@ default List> loadClassList(String packageOrFileName, String className, public static String KEY_NAME = "name"; public static String KEY_METHOD = "method"; public static String KEY_MOCK = "mock"; + public static String KEY_QUERY = "query"; public static String KEY_RETURN = "return"; public static String KEY_TIME_DETAIL = "time:start|duration|end"; public static String KEY_CLASS_ARGS = "classArgs"; public static String KEY_METHOD_ARGS = "methodArgs"; public static String KEY_CALLBACK = "callback"; + public static String KEY_GLOBAL = "global"; public static String KEY_CALL_LIST = "call()[]"; public static String KEY_CALL_MAP = "call(){}"; + public static String KEY_PACKAGE_TOTAL = "packageTotal"; + public static String KEY_CLASS_TOTAL = "classTotal"; + public static String KEY_METHOD_TOTAL = "methodTotal"; + public static String KEY_PACKAGE_LIST = "packageList"; + public static String KEY_CLASS_LIST = "classList"; + public static String KEY_METHOD_LIST = "methodList"; @@ -193,6 +202,8 @@ public JSONObject parseJSON(String type, Object value) { + @NotNull + public static Map, InterfaceProxy> GLOBAL_CALLBACK_MAP; // Map> public static final Map, Map> INSTANCE_MAP; public static final Map> PRIMITIVE_CLASS_MAP; @@ -200,6 +211,7 @@ public JSONObject parseJSON(String type, Object value) { public static final Map> CLASS_MAP; public static final Map, Object> DEFAULT_TYPE_VALUE_MAP; static { + GLOBAL_CALLBACK_MAP = new HashMap<>(); INSTANCE_MAP = new HashMap<>(); PRIMITIVE_CLASS_MAP = new HashMap>(); @@ -276,6 +288,7 @@ public JSONObject parseJSON(String type, Object value) { * @param request : { "mock": true, + "query": 0, // 0-数据,1-总数,2-全部 "package": "apijson.demo.server", "class": "DemoFunction", "method": "plus", @@ -292,6 +305,7 @@ public static JSONObject listMethod(String request) { if (req == null) { req = new JSONObject(true); } + int query = req.getIntValue(KEY_QUERY); boolean mock = req.getBooleanValue(KEY_MOCK); String pkgName = req.getString(KEY_PACKAGE); String clsName = req.getString(KEY_CLASS); @@ -313,9 +327,9 @@ public static JSONObject listMethod(String request) { } } - JSONArray list = getMethodListGroupByClass(pkgName, clsName, methodName, argTypes, mock); + JSONObject obj = getMethodListGroupByClass(pkgName, clsName, methodName, argTypes, query, mock); result = JSON_CALLBACK.newSuccessResult(); - result.put("classList", list); //序列化 Class 只能拿到 name result.put("Class[]", JSON.parseArray(JSON.toJSONString(classlist))); + result.putAll(obj); //序列化 Class 只能拿到 name result.put("Class[]", JSON.parseArray(JSON.toJSONString(classlist))); } catch (Throwable e) { e.printStackTrace(); @@ -452,7 +466,7 @@ public static void invokeMethod(JSONObject req, Object instance, Listener> set = req.entrySet(); + for (Entry e : set) { + //判断是否符合 "fun(arg0,arg1...)": { "callback": true } 格式 + String key = e == null ? null : e.getKey(); + JSONObject val = key != null && e.getValue() instanceof JSONObject ? ((JSONObject) e.getValue()) : null; + + int index = val == null || key.endsWith(")") == false ? -1 : key.indexOf("("); + if (index > 0 && StringUtil.isName(key.substring(0, index))) { + boolean isCb = val.getBooleanValue(KEY_CALLBACK); + if (isCb) { + hasGlobalCallback = true; + } + if (globalInterfaceProxy == null) { + globalInterfaceProxy = new InterfaceProxy(); + } + + final JSONObject finalReq = req; + final InterfaceProxy globalProxy = globalInterfaceProxy; + globalInterfaceProxy.$_putCallback(key, new Listener() { + + @Override + public void complete(Object data, Method method, InterfaceProxy proxy, Object... extras) throws Exception { + Log.d(TAG, "invokeMethod LISTENER_QUEUE.poll " + method); + if (isCb && listener != null) { + // JSONObject result = new JSONObject(); + // result.put(method == null ? null : method.toString(), data); + // listener.complete(result, method, proxy, extras); + + finalReq.putAll(globalProxy); + listener.complete(finalReq, method, proxy, extras); + } + } + }); + } + } + // } + + if (globalInterfaceProxy != null && GLOBAL_CALLBACK_MAP.containsValue(globalInterfaceProxy) == false) { + GLOBAL_CALLBACK_MAP.put(clazz, globalInterfaceProxy); + } + + invokeMethod(clazz, instance, pkgName, clsName, methodName, methodArgs, listener, hasGlobalCallback ? globalInterfaceProxy : null); // 后端服务只允许在当前线程执行,只有客户端才允许设置在 UI 线程(主线程) 执行 // if (threadStr == null || THREAD_CURRENT_STRING.equals(threadStr) || THREAD_MAIN_STRING.equals(threadStr)) { @@ -515,7 +575,7 @@ public void run() { public static void invokeMethod(Class clazz, final Object instance, String pkgName, String clsName - , String methodName, List methodArgs, Listener listener) throws Exception { + , String methodName, List methodArgs, Listener listener, InterfaceProxy globalInterfaceProxy) throws Exception { long startTime = System.currentTimeMillis(); try { @@ -532,9 +592,11 @@ public void complete(JSONObject data, Method method, InterfaceProxy proxy, Objec result.put(KEY_THIS, parseJSON(instance.getClass(), instance)); //TODO InterfaceProxy proxy 改成泛型 I instance ? } - listener.complete(result); + if (listener != null) { + listener.complete(result); + } } - }); + }, globalInterfaceProxy); } catch (Throwable e) { completeWithError(pkgName, clsName, methodName, startTime, e, listener); @@ -567,11 +629,13 @@ private static void completeWithError(String pkgName, String clsName, String met result.put("cause", e.getCause()); result.put("trace", e.getStackTrace()); - try { - listener.complete(result); - } - catch (Exception e1) { - e1.printStackTrace(); + if (listener != null) { + try { + listener.complete(result); + } + catch (Exception e1) { + e1.printStackTrace(); + } } } @@ -786,7 +850,7 @@ public static Method getInvokeMethod(@NotNull Class clazz, @NotNull String me * @throws Exception */ public static Object getInvokeResult(@NotNull Class clazz, Object instance, @NotNull String methodName - , List methodArgs, Listener listener) throws Exception { + , List methodArgs, Listener listener, InterfaceProxy globalInterfaceProxy) throws Exception { Objects.requireNonNull(clazz); Objects.requireNonNull(methodName); @@ -858,9 +922,9 @@ else if (t.isInterface()) { } }; - boolean isSync = true; - if (types != null) { + boolean isSync = globalInterfaceProxy == null; + if (types != null) { for (int i = 0; i < types.length; i++) { //当其中有 interface 且用 KEY_CALLBACK 标记了内部至少一个方法,则认为是触发异步回调的方法 Class type = types[i]; Object value = args[i]; @@ -886,8 +950,15 @@ else if (t.isInterface()) { } } + Argument arg = methodArgs.get(i); + if (arg != null && arg.getGlobal() != null && arg.getGlobal()) { + GLOBAL_CALLBACK_MAP.put(clazz, proxy); + } + args[i] = cast(proxy, type, ParserConfig.getGlobalInstance()); - isSync = proxy.$_getCallbackMap().isEmpty(); + if (isSync) { + isSync = proxy.$_getCallbackMap().isEmpty(); + } } catch (Throwable e) { e.printStackTrace(); @@ -925,43 +996,94 @@ else if (t.isInterface()) { * @param clsName * @param methodName * @param argTypes + * @param query + * @param mock * @return * @throws Exception */ - public static JSONArray getMethodListGroupByClass(String pkgName, String clsName - , String methodName, Class[] argTypes, boolean mock) throws Exception { + public static JSONObject getMethodListGroupByClass(String pkgName, String clsName + , String methodName, Class[] argTypes, int query, boolean mock) throws Exception { + if (query != 0 && query != 1 && query != 2) { + throw new IllegalArgumentException("query 取值只能是 [0, 1, 2] 中的一个! 0-数据,1-总数,2-全部"); + } + + boolean queryData = query != 1; + boolean queryTotal = query != 0; + + pkgName = StringUtil.isEmpty(pkgName, true) ? null : StringUtil.getTrimedString(pkgName); + clsName = StringUtil.isEmpty(clsName, true) ? null : StringUtil.getTrimedString(clsName); boolean allMethod = isEmpty(methodName, true); - List> classList = CLASS_LOADER_CALLBACK.loadClassList(pkgName, clsName, true); - JSONArray list = null; - if (classList != null) { - list = new JSONArray(classList.size()); + List> allClassList = CLASS_LOADER_CALLBACK.loadClassList(pkgName, clsName, true); + + int packageTotal = 0; + int classTotal = 0; + int methodTotal = 0; - for (Class cls : classList) { + Map packageMap = new HashMap<>(); + JSONArray packageList = null; + + JSONObject countObj = new JSONObject(true); + if (queryTotal) { + countObj.put(KEY_PACKAGE_TOTAL, packageTotal); + countObj.put(KEY_CLASS_TOTAL, classTotal); + countObj.put(KEY_METHOD_TOTAL, methodTotal); + } + + if (allClassList != null && allClassList.isEmpty() == false) { + packageList = new JSONArray(Math.max(10, allClassList.size()/5)); + + for (Class cls : allClassList) { if (cls == null) { continue; } + classTotal ++; + + int methodCount = 0; try { + String pkg = cls.getPackage().getName(); + JSONObject pkgObj = packageMap.get(pkg); + boolean pkgNotExist = pkgObj == null; + if (pkgNotExist) { + pkgObj = new JSONObject(true); + packageMap.put(pkg, pkgObj); + } + + if (queryTotal) { + int clsCount = pkgObj.getIntValue(KEY_CLASS_TOTAL); + pkgObj.put(KEY_CLASS_TOTAL, clsCount + 1); + } + pkgObj.put(KEY_PACKAGE, pkg); + + JSONArray classList = pkgObj.getJSONArray(KEY_CLASS_LIST); + if (classList == null) { + classList = new JSONArray(); + } + JSONObject clsObj = new JSONObject(true); - clsObj.put(KEY_NAME, cls.getSimpleName()); + clsObj.put(KEY_CLASS, cls.getSimpleName()); clsObj.put(KEY_TYPE, trimType(cls.getGenericSuperclass())); - clsObj.put(KEY_PACKAGE, cls.getPackage().getName()); JSONArray methodList = null; - if (allMethod == false && argTypes != null && argTypes.length > 0) { - Object mObj = parseMethodObject(cls.getMethod(methodName, argTypes), mock); - if (mObj != null) { - methodList = new JSONArray(1); - methodList.add(mObj); + if (allMethod == false && argTypes != null) { + methodList = queryData ? new JSONArray(1) : null; + + JSONObject mObj = parseMethodObject(cls.getMethod(methodName, argTypes), mock); + if (mObj != null && mObj.isEmpty() == false) { + methodCount = 1; + + if (methodList != null) { + methodList.add(mObj); + } } } else { Method[] methods = cls.getDeclaredMethods(); //父类的就用父类去获取 cls.getMethods(); if (methods != null && methods.length > 0) { - methodList = new JSONArray(methods.length); + methodList = queryData ? new JSONArray(methods.length) : null; for (Method m : methods) { String name = m == null ? null : m.getName(); @@ -970,38 +1092,70 @@ public static JSONArray getMethodListGroupByClass(String pkgName, String clsName } if (allMethod || methodName.equals(name)) { + JSONObject mObj = parseMethodObject(m, mock); + if (mObj != null && mObj.isEmpty() == false) { + methodCount ++; - Object mObj = parseMethodObject(m, mock); - if (mObj != null) { - methodList.add(mObj); + if (methodList != null) { + methodList.add(mObj); + } } } } } } - clsObj.put("methodList", methodList); //太多不需要的信息,导致后端返回慢、前端卡 UI clsObj.put("Method[]", JSON.parseArray(methods)); - list.add(clsObj); + if (queryTotal) { + clsObj.put(KEY_METHOD_TOTAL, methodCount); //太多不需要的信息,导致后端返回慢、前端卡 UI clsObj.put("Method[]", JSON.parseArray(methods)); + } + + if (methodList != null && methodList.isEmpty() == false) { + clsObj.put(KEY_METHOD_LIST, methodList); //太多不需要的信息,导致后端返回慢、前端卡 UI clsObj.put("Method[]", JSON.parseArray(methods)); + } + + if (clsObj != null && clsObj.isEmpty() == false) { + classList.add(clsObj); + } + if (classList != null && classList.isEmpty() == false) { + pkgObj.put(KEY_CLASS_LIST, classList); + } + + if (pkgNotExist && pkgObj != null && pkgObj.isEmpty() == false) { + packageList.add(pkgObj); + } } catch (Throwable e) { e.printStackTrace(); } + methodTotal += methodCount; + } + + if (packageList != null && packageList.isEmpty() == false) { + countObj.put(KEY_PACKAGE_LIST, packageList); } } - return list; + packageTotal = packageMap.size(); + + if (query != 0) { + countObj.put(KEY_PACKAGE_TOTAL, packageTotal); + countObj.put(KEY_CLASS_TOTAL, classTotal); + countObj.put(KEY_METHOD_TOTAL, methodTotal); + } + return countObj; } + public static String dot2Separator(String name) { - return name == null ? null : name.replaceAll("\\.", File.separator); + return name == null ? null : name.replaceAll("\\.", "\\".equals(File.separator) ? "\\\\" : File.separator); } public static String separator2dot(String name) { - return name == null ? null : name.replaceAll(File.separator, "."); + return name == null ? null : name.replaceAll("\\".equals(File.separator) ? "\\\\" : File.separator, "."); } // private void initTypesAndValues(JSONArray methodArgs, Class[] types, Object[] args) @@ -1565,7 +1719,7 @@ public static Class getType(String name, Object value, boolean defaultType) t @SuppressWarnings("rawtypes") Collection nc; - if (AbstractSequentialList.class.isAssignableFrom(type)) { // LinkedList + if (Queue.class.isAssignableFrom(type) || AbstractSequentialList.class.isAssignableFrom(type)) { // LinkedList nc = new LinkedList<>(); } else if (Vector.class.isAssignableFrom(type)) { // Stack @@ -1619,11 +1773,11 @@ public static T cast(Object obj, Class type, ParserConfig config) { if (Collection.class.isAssignableFrom(type)) { Collection c = (Collection) obj; - + @SuppressWarnings("rawtypes") Collection nc; - if (AbstractSequentialList.class.isAssignableFrom(type)) { // LinkedList + if (Queue.class.isAssignableFrom(type) || AbstractSequentialList.class.isAssignableFrom(type)) { // LinkedList nc = new LinkedList<>(); } else if (Vector.class.isAssignableFrom(type)) { // Stack @@ -1645,10 +1799,10 @@ else if (Set.class.isAssignableFrom(type)) { // HashSet, LinkedHashSet for (Object o : c) { nc.add(o); } - + return (T) nc; } - + return TypeUtils.cast(obj, type, config); } @@ -1755,7 +1909,7 @@ public static List> findClassList(String packageOrFileName, String clas if (allName || className.equals(name)) { //反射出实例 try { - Class clazz = loader.loadClass(packageOrFileName.replaceAll(File.separator, "\\.") + "." + name); + Class clazz = loader.loadClass(packageOrFileName.replaceAll("\\".equals(File.separator) ? "\\\\" : File.separator, "\\.") + "." + name); list.add(clazz); if (allName == false) { @@ -1835,6 +1989,7 @@ public static class Argument { private Boolean reuse; private String type; private Object value; + private Boolean global; public Argument() { } @@ -1863,6 +2018,12 @@ public Object getValue() { public void setValue(Object value) { this.value = value; } + public Boolean getGlobal() { + return global; + } + public void setGlobal(Boolean global) { + this.global = global; + } } /** @@ -2002,7 +2163,7 @@ private Object onInvoke(Object proxy, Method method, Object[] args, boolean call if (name == null) { return null; } - String key = name + "(" + StringUtil.getString(trimTypes(method.getGenericParameterTypes())) + ")"; + String key = name + "(" + StringUtil.getString(trimTypes(method.getGenericParameterTypes())) + ")"; // 带修饰符,太长 method.toGenericString(); Object handlerValue = get(key); String type = null; diff --git a/src/main/java/unitauto/test/TestSDK.java b/src/main/java/unitauto/test/TestSDK.java new file mode 100644 index 0000000..40f6059 --- /dev/null +++ b/src/main/java/unitauto/test/TestSDK.java @@ -0,0 +1,205 @@ +/*Copyright ©2019 TommyLemon(https://github.com/TommyLemon/UnitAuto) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.*/ + +package unitauto.test; + +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; + +import com.alibaba.fastjson.JSON; + +import unitauto.Log; +import unitauto.MethodUtil; +import unitauto.MethodUtil.InterfaceProxy; +import unitauto.MethodUtil.Listener; +import unitauto.StringUtil; + + +/**模拟微信/支付宝等在 Android/iOS/Window 上的 OpenSDK,回调与支付方法分离 + * @author Lemon + */ +public class TestSDK { + protected static final String TAG = "TestSDK"; + + public interface Callback { + void response(Map info); + } + + + private static TestSDK INSTANCE = new TestSDK(); + + public static TestSDK getInstance() { + return INSTANCE; + } + + public void setLisenter(Callback callback) { + this.callback = callback; + if (callback == null) { + throw new NullPointerException("callback 不允许为 null!"); + } + } + + private Map config; + private Callback callback; + + public void init(Map config) { + this.config = config; + } + + public void init(Map config, Callback callback) { + if (callback == null) { + throw new NullPointerException("callback 不允许为 null!"); + } + + this.config = config; + this.callback = callback; + + Map info = new HashMap<>(); + try { + Log.d(TAG, "init config = " + JSON.toJSONString(this.config)); + + Thread.sleep(1000); + + info.put("return_code", "SUCCESS"); + info.put("return_msg", "初始化成功"); + } catch (InterruptedException e) { + e.printStackTrace(); + info.put("return_code", "ERROR"); + info.put("return_msg", "网络超时"); + } + + callback.response(info); + } + + public void pay(Map req) { + if (callback == null) { + throw new NullPointerException("未初始化!"); + } + + Map info = new HashMap<>(); + + String orderId = req == null ? null : req.get("order_id"); + String price = req == null ? null : req.get("price"); + if (StringUtil.isEmpty(orderId, true) || StringUtil.isEmpty(price, true)) { + info.put("return_code", "PARAM_ERROR"); + info.put("return_msg", StringUtil.isEmpty(orderId, true) ? "参数缺少 order_id!" : "参数缺少 price!"); + callback.response(info); + return; + } + + try { + new BigDecimal(price); + } catch (Exception e) { + info.put("return_code", "PARAM_ERROR"); + info.put("return_msg", "参数 price 的值不是数字!"); + callback.response(info); + return; + } + + Log.d(TAG, "init req = " + JSON.toJSONString(req)); + try { + Thread.sleep(3000); + if (Math.random() > 0.7) { + throw new Exception("请求超时"); + } + if (Math.random() > 0.5) { + throw new Exception("余额不足,请先充值!"); + } + + info.put("return_code", "SUCCESS"); + info.put("return_msg", "支付成功"); + } catch (Exception e) { + e.printStackTrace(); + info.put("return_code", "ERROR"); + info.put("return_msg", "支付失败:" + e.getMessage()); + } + + callback.response(info); + } + + + + public static void main(String[] args) { + InterfaceProxy globalInterfaceProxy = MethodUtil.GLOBAL_CALLBACK_MAP.get(TestSDK.class); + if (globalInterfaceProxy == null) { + globalInterfaceProxy = new InterfaceProxy(); + } + + globalInterfaceProxy.$_putCallback("response(Map)", new Listener() { + + @Override + public void complete(Object data, Method method, InterfaceProxy proxy, Object... extras) throws Exception { + Log.d(TAG, "main globalInterfaceProxy.Listener.complete method = " + method + "; data = " + JSON.toJSONString(data)); + } + }); + MethodUtil.GLOBAL_CALLBACK_MAP.put(TestSDK.class, globalInterfaceProxy); + + + /** + * 初始化 + */ + Map config = new HashMap<>(); + config.put("ip", "192.168.1.1"); //若没有代理,则不需要此行 + config.put("port", "8888");//若没有代理,则不需要此行 + // config.put("user", mEtnUser.getText().toString());//若没有代理,则不需要此行 + // config.put("passwd", mEtnPassword.getText().toString());//若没有代理,则不需要此行 + // config.put("proxy_type", 1 ); //若没有代理,则不需要此行 + // config.put("perform_mode", "LOW_PERFORM");//低性能表现,默认关闭美颜等 + + boolean[] called = new boolean[]{false}; + TestSDK.getInstance().init(config, new Callback() { + @Override + public void response(Map info) { + InterfaceProxy globalCallback = MethodUtil.GLOBAL_CALLBACK_MAP.get(TestSDK.class); + try { + @SuppressWarnings("unchecked") + Listener listener = (Listener) globalCallback.$_getCallback("response(Map)"); + listener.complete(info); + } catch (Exception e) { + e.printStackTrace(); + } + + if (info == null) { + System.out.println("TestSDK.main 调用返回为空, 请查看日志"); + new RuntimeException("调用返回为空").printStackTrace(); + return; + } + + String code = (String) info.get("return_code"); + String msg = (String) info.get("return_msg"); + + Log.d(TAG, "main TestSDK.getInstance().Callback.response " + (called[0] ? "支付回调" : "初始化完成") + ":code = " + code + ";msg = " + msg); + called[0] = true; + } + }); + + + try { + Thread.sleep(2000); + } catch (Exception e) { + e.printStackTrace(); + } + + // 发起支付 + Map req = new HashMap<>(); + req.put("order_id", "123456"); + req.put("price", "15.9"); + TestSDK.getInstance().pay(req); + } + + + +}