• 微信公众号接入之排序问题小记 Arrays.sort()


      微信公众号作为强大的自媒体工具,对接一下是很正常的了。不过这不是本文的方向,本文的方向公众号接入的排序问题。

      最近接了一个重构的小项目,需要将原有的php的公众号后台系统,转换为java系统。当然,也很简单的了。

      

      不过,在接入的时候,遇到有一个有趣的问题,可以分享下。

      大家知道,要将微信在接到用户的请求之后,可以将消息转发给咱们在公众号后台指定的 server 地址,而在指定这个地址的时候,又需要先校验下这个地址是否连通的,是不是开发者自己的用来处理微信转发消息的地址。因此有一个服务器 token 的校验过程。
    校验token的过程,算法很简单,引用微信文档原文如下:

    开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:

    1)将token、timestamp、nonce三个参数进行字典序排序

    2)将三个参数字符串拼接成一个字符串进行sha1加密

    3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信

    php 示例如下:

    private function checkSignature()
    {
        _GET["signature"];
        _GET["timestamp"];
        _GET["nonce"];
    
        tmpArr = array(timestamp, $nonce);
        sort($tmpArr);        // 官方最新版demo已经修复该排序问题了 sort($tmpArr, SORT_STRING);
        $tmpStr = implode( $tmpArr );
        $tmpStr = sha1( $tmpStr );
    
        if( signature ){
            return true;
        }else{
            return false;
        }
    }

    而且,下方demo也给的妥妥的。好吧,对接是不会有问题了!

    我也按照java版的demo,给整了个接入进来!java 的验证样例如下:

        public String validate(@ModelAttribute WxValidateBean validateBean) {
            String myValidToken = signatureWxReq(validateBean.getToken(), validateBean.getTimestamp(), validateBean.getNonce());
            if(myValidToken.equals(validateBean.getSignature())) {
                return validateBean.getEchostr();
            }
            return "";
        }
        
        public String signatureWxReq(String token, String timestamp, String nonce) {
            try {
                String[] array = new String[] { token, timestamp, nonce };
                StringBuffer sb = new StringBuffer();
                // 字符串排序
                Arrays.sort(array);
                for (int i = 0; i < 4; i++) {
                    sb.append(array[i]);
                }
                String str = sb.toString();
                // SHA1签名生成
                MessageDigest md = MessageDigest.getInstance("SHA-1");
                md.update(str.getBytes());
                byte[] digest = md.digest();
    
                StringBuffer hexstr = new StringBuffer();
                String shaHex = "";
                for (int i = 0; i < digest.length; i++) {
                    shaHex = Integer.toHexString(digest[i] & 0xFF);
                    if (shaHex.length() < 2) {
                        hexstr.append(0);
                    }
                    hexstr.append(shaHex);
                }
                return hexstr.toString();
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException("加密sign异常!");
            }
        }
        

      按理肯定也不会有问题了。不过为了保险起见,我还是写了个测试用例!自测一下!

        private final String oldSysWxAddress = "http://a.com/wx";
        private final String newSysWxAddress = "http://localhost:8080/wx";
    
        @Test
        public void testWxValidateToken() throws IOException {
    
            String token = "abc123";
            String timestamp = new Date().getTime() / 1000 + "";
            String nonce = "207665";        // 这个就随便一写的数字
            String echoStr = "1kjdslfj";
            String signature = signatureWxReq(token, timestamp, nonce);
    
            Map<String, String> params = new HashMap<>();
            params.put("token", token);
            params.put("timestamp", timestamp);
            params.put("nonce", nonce);
            params.put("signature", signature);
            params.put("echostr", echoStr);
    
            String oldSysResponse = HttpClientOp.doGet(oldSysWxAddress, params);
            Assert.assertEquals("老系统验证不通过,请检查加密算法!", oldSysResponse, echoStr);
    
            String newSysResponse = HttpClientOp.doGet(newSysWxAddress, params);
            Assert.assertEquals("新系统验证不通过,出bug了!", newSysResponse, echoStr);
    
            Assert.assertEquals("新老返回不一致,测试不通过!", newSysResponse, oldSysResponse);
            System.out.println("OK");
    
        }
        

      想着吧,也就走个过场得了。结果,还真不是这样!出问题了,"老系统验证不通过,请检查加密算法!" 。

      按理不应该啊!但是代码是理智的,咱们得找到bug不是。

      最后,通过一步步排查,终于发现了,原来是 php 的排序结果,与 java 的排序结果不一致,因此得到的加密串就不对了。

      为啥呢?sort($tmpArr); php是弱类型语言,我们请求的虽然看起来是字符串,但是解析后,因为得到的是一整串数字,因此就认为是可以用整型或这种比较数字大小方式了。而修复方式自然就是,指明是使用字符串方式来排序就行了,如上官方修正。

    所以,一比较时间和 nonce 随机数,因为随机数位数小,因此自然就应该排在时间戳的前面了。

      

    而对于 java 的排序呢? Arrays.sort(Object[] a), 我们来看一下源码!

        public static void sort(Object[] a) {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a);
            else
                // 默认使用 ComparableTimSort 排序
                ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
        }
        
        // ComparableTimSort.sort()
        /**
         * Sorts the given range, using the given workspace array slice
         * for temp storage when possible. This method is designed to be
         * invoked from public methods (in class Arrays) after performing
         * any necessary array bounds checks and expanding parameters into
         * the required forms.
         *
         * @param a the array to be sorted
         * @param lo the index of the first element, inclusive, to be sorted
         * @param hi the index of the last element, exclusive, to be sorted
         * @param work a workspace array (slice)
         * @param workBase origin of usable space in work array
         * @param workLen usable size of work array
         * @since 1.8
         */
        static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) {
            assert a != null && lo >= 0 && lo <= hi && hi <= a.length;
    
            int nRemaining  = hi - lo;
            if (nRemaining < 2)
                return;  // Arrays of size 0 and 1 are always sorted
    
            // If array is small, do a "mini-TimSort" with no merges
            if (nRemaining < MIN_MERGE) {
                // 二分插入排序
                int initRunLen = countRunAndMakeAscending(a, lo, hi);
                binarySort(a, lo, hi, lo + initRunLen);
                return;
            }
    
            /**
             * March over the array once, left to right, finding natural runs,
             * extending short natural runs to minRun elements, and merging runs
             * to maintain stack invariant.
             */
            ComparableTimSort ts = new ComparableTimSort(a, work, workBase, workLen);
            int minRun = minRunLength(nRemaining);
            do {
                // Identify next run
                int runLen = countRunAndMakeAscending(a, lo, hi);
    
                // If run is short, extend to min(minRun, nRemaining)
                if (runLen < minRun) {
                    int force = nRemaining <= minRun ? nRemaining : minRun;
                    binarySort(a, lo, lo + force, lo + runLen);
                    runLen = force;
                }
    
                // Push run onto pending-run stack, and maybe merge
                ts.pushRun(lo, runLen);
                ts.mergeCollapse();
    
                // Advance to find next run
                lo += runLen;
                nRemaining -= runLen;
            } while (nRemaining != 0);
    
            // Merge all remaining runs to complete sort
            assert lo == hi;
            ts.mergeForceCollapse();
            assert ts.stackSize == 1;
        }
    
    
        /**
         * Returns the length of the run beginning at the specified position in
         * the specified array and reverses the run if it is descending (ensuring
         * that the run will always be ascending when the method returns).
         *
         * A run is the longest ascending sequence with:
         *
         *    a[lo] <= a[lo + 1] <= a[lo + 2] <= ...
         *
         * or the longest descending sequence with:
         *
         *    a[lo] >  a[lo + 1] >  a[lo + 2] >  ...
         *
         * For its intended use in a stable mergesort, the strictness of the
         * definition of "descending" is needed so that the call can safely
         * reverse a descending sequence without violating stability.
         *
         * @param a the array in which a run is to be counted and possibly reversed
         * @param lo index of the first element in the run
         * @param hi index after the last element that may be contained in the run.
                  It is required that {@code lo < hi}.
         * @return  the length of the run beginning at the specified position in
         *          the specified array
         */
        @SuppressWarnings({"unchecked", "rawtypes"})
        private static int countRunAndMakeAscending(Object[] a, int lo, int hi) {
            assert lo < hi;
            int runHi = lo + 1;
            if (runHi == hi)
                return 1;
    
            // Find end of run, and reverse range if descending
            // 调用的是 XXObject.compareTo() 方法,找出第一个比后续值小的index, 作为快排的基点
            if (((Comparable) a[runHi++]).compareTo(a[lo]) < 0) { // Descending
                while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) < 0)
                    runHi++;
                reverseRange(a, lo, runHi);
            } else {                              // Ascending
                while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) >= 0)
                    runHi++;
            }
    
            return runHi - lo;
        }
        
        /**
         * 反转元素
         * Reverse the specified range of the specified array.
         *
         * @param a the array in which a range is to be reversed
         * @param lo the index of the first element in the range to be reversed
         * @param hi the index after the last element in the range to be reversed
         */
        private static void reverseRange(Object[] a, int lo, int hi) {
            hi--;
            while (lo < hi) {
                Object t = a[lo];
                a[lo++] = a[hi];
                a[hi--] = t;
            }
        }
        
        
        /**
         * 二分插入排序
         * Sorts the specified portion of the specified array using a binary
         * insertion sort.  This is the best method for sorting small numbers
         * of elements.  It requires O(n log n) compares, but O(n^2) data
         * movement (worst case).
         *
         * If the initial part of the specified range is already sorted,
         * this method can take advantage of it: the method assumes that the
         * elements from index {@code lo}, inclusive, to {@code start},
         * exclusive are already sorted.
         *
         * @param a the array in which a range is to be sorted
         * @param lo the index of the first element in the range to be sorted
         * @param hi the index after the last element in the range to be sorted
         * @param start the index of the first element in the range that is
         *        not already known to be sorted ({@code lo <= start <= hi})
         */
        @SuppressWarnings({"fallthrough", "rawtypes", "unchecked"})
        private static void binarySort(Object[] a, int lo, int hi, int start) {
            assert lo <= start && start <= hi;
            if (start == lo)
                start++;
            for ( ; start < hi; start++) {
                Comparable pivot = (Comparable) a[start];
    
                // Set left (and right) to the index where a[start] (pivot) belongs
                int left = lo;
                int right = start;
                assert left <= right;
                /*
                 * Invariants:
                 *   pivot >= all in [lo, left).
                 *   pivot <  all in [right, start).
                 */
                while (left < right) {
                    int mid = (left + right) >>> 1;
                    if (pivot.compareTo(a[mid]) < 0)
                        right = mid;
                    else
                        left = mid + 1;
                }
                assert left == right;
    
                /*
                 * The invariants still hold: pivot >= all in [lo, left) and
                 * pivot < all in [left, start), so pivot belongs at left.  Note
                 * that if there are elements equal to pivot, left points to the
                 * first slot after them -- that's why this sort is stable.
                 * Slide elements over to make room for pivot.
                 */
                int n = start - left;  // The number of elements to move
                // Switch is just an optimization for arraycopy in default case
                switch (n) {
                    case 2:  a[left + 2] = a[left + 1];
                    case 1:  a[left + 1] = a[left];
                             break;
                    default: System.arraycopy(a, left, a, left + 1, n);
                }
                a[left] = pivot;
            }
        }
    
        
        // String.compareTo() 方法,比较 char大小,即字典顺序
        public int compareTo(String anotherString) {
            int len1 = value.length;
            int len2 = anotherString.value.length;
            int lim = Math.min(len1, len2);
            char v1[] = value;
            char v2[] = anotherString.value;
    
            int k = 0;
            while (k < lim) {
                char c1 = v1[k];
                char c2 = v2[k];
                if (c1 != c2) {
                    return c1 - c2;
                }
                k++;
            }
            return len1 - len2;
        }

      可以看到,Arrays.sort(), 对于小数的排序,是使用二分插入排序来做的,而具体排序先后,则是调用具体类的 compareTo() 方法,也就是说要进行比较的类,须实现Comparable 接口。另外,从String的比较方法中,我们也可以看出 char 其实能保存很多东西而不只是 1-128。比如 char s = (char)"中"; 

          而对于排序算法,则针对不同情况,选择不同的合适算法,从而提高运行效率!

          而对于String.compareTo() 则是比较字符的ascii顺序!

    算法说明:

    1. 对于小数的排序,使用二分插入排序,原理:

      1. 先从最开始出发,找出最大的递增或者递减的个数,如果递减,则倒序处理排列,从而使前 n 个元素呈排列状态;

      2. 从之前排好序之后的元素开始,使用二分查找法,得到需要移动的个数,进行相应元素的转换。因为 lo 一直是初始值,所以,在 start 循环到 hi 操作完成交换之后,就完成了所有的排序了。

    2. 而对大些的数组排序,则使用分而治之的方法,拆分处理,原理:

      1. 先算出一个合适的大小,在将输入按其升序和降序特点进行了分区,拆分的原则是大小需要小 MIN_MERGE,也就是32。

      2. 针对这些第个分区序列,先按照小数的排序方式排好,然后将结果按规则进行合并。

      3. 每次合并会将两个run合并成一个 run。合并的结果保存到栈中。合并直到消耗掉所有的run,这时将栈上剩余的 run合并到只剩一个 run 为止。这时这个仅剩的 run 便是排好序的结果。

  • 相关阅读:
    进制转化
    递归小结
    Java异常处理面试题归纳
    字符串相加 内存分配
    递归与循环
    cookie
    会话管理
    在javaweb中通过servlet类和普通类读取资源文件
    JS中遍历EL表达式中后台传过来的Java集合
    Ztree加载完成后显示勾选节点
  • 原文地址:https://www.cnblogs.com/yougewe/p/10012898.html
Copyright © 2020-2023  润新知