错误提示:java.lang.IllegalArgumentException:Invalid token LIMIT的解决方法

最近在Android R机型上面,很多应用查询通话记录数据库的时候,抛出了下面异常
DatabaseUtils: java.lang.IllegalArgumentException: Invalid token LIMIT,具体如下:

06-11 10:46:40.431 4034 9554 E DatabaseUtils: Writing exception to parcel
06-11 10:46:40.431 4034 9554 E DatabaseUtils: java.lang.IllegalArgumentException: Invalid token LIMIT
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.database.sqlite.SQLiteQueryBuilder.enforceStrictToken(SQLiteQueryBuilder.java:824)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.database.sqlite.SQLiteQueryBuilder.lambda$W2yQ6UjYGqGIu6HEomKgdgvGNKI(Unknown Source:0)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.database.sqlite.-$$Lambda$SQLiteQueryBuilder$W2yQ6UjYGqGIu6HEomKgdgvGNKI.accept(Unknown Source:4)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.database.sqlite.SQLiteTokenizer.tokenize(SQLiteTokenizer.java:108)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.database.sqlite.SQLiteQueryBuilder.enforceStrictGrammar(SQLiteQueryBuilder.java:794)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.database.sqlite.SQLiteQueryBuilder.query(SQLiteQueryBuilder.java:557)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.database.sqlite.SQLiteQueryBuilder.query(SQLiteQueryBuilder.java:502)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at com.android.providers.contacts.CallLogProvider.queryInternal(CallLogProvider.java:427)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at com.android.providers.contacts.CallLogProvider.query(CallLogProvider.java:350)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.content.ContentProvider.query(ContentProvider.java:1447)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.content.ContentProvider.query(ContentProvider.java:1543)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.content.ContentProvider$Transport.query(ContentProvider.java:289)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:106)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.os.Binder.execTransactInternal(Binder.java:1165)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.os.Binder.execTransact(Binder.java:1129)

执行代码块如下:

     /**
         * Get if the query is marked as strict, as last configured by
         * {@link #setStrictGrammar(boolean)}.
         */
        public static String getLastOutgoingCall(Context context) {
            final ContentResolver resolver = context.getContentResolver();
            Cursor c = null;
            try {
                c = resolver.query(Calls.CONTENT_URI,
                                   new String[] {
                                       Calls.NUMBER
                                   }, Calls.TYPE + " = "
                                   + Calls.OUTGOING_TYPE, null,
                                   Calls.DEFAULT_SORT_ORDER + " LIMIT 1");
                if (c == null || !c.moveToFirst()) {
                    return "";
                }
                return c.getString(0);
            } finally {
                if (c != null)
                    c.close();
            }
        }

就是一个简单的数据库查询操作,这一段代码在其他机型上面执行没有问题。
第一次看到这个异常就蒙圈了,百度谷歌查询,都没有找到有用的信息。
接着从limit这个关键字入手,limit是一个查询条件,意思就是获取数据库标准多少行数据,是一个标准的数据库关键字,写法没有任何问题。

没有头绪就只能查看源码,看看为什么抛出异常
首先看SQLiteQueryBuilder.java这个类,异常是这个类跑出来的,从异常堆栈信息中看出,实在
enforceStrictToken()这个方法里面执行出问题了,代码如下:

   private void enforceStrictToken(@NonNull String token) {
        Log.e("limeng","token:"+token);
        if (TextUtils.isEmpty(token)) return;
        if (isTableOrColumn(token)) return;
        if (SQLiteTokenizer.isFunction(token)) return;
        if (SQLiteTokenizer.isType(token)) return;

        // Carefully block any tokens that are attempting to jump across query
        // clauses or create subqueries, since they could leak data that should
        // have been filtered by the trusted where clause
        boolean isAllowedKeyword = SQLiteTokenizer.isKeyword(token);
        switch (token.toUpperCase(Locale.US)) {
            case "SELECT":
            case "FROM":
            case "WHERE":
            case "GROUP":
            case "HAVING":
            case "WINDOW":
            case "VALUES":
            case "ORDER":
            case "LIMIT":
                isAllowedKeyword = false;
                break;
        }
        if (!isAllowedKeyword) {
            throw new IllegalArgumentException("Invalid token " + token);
        }
    }

这段代码可以看出,就是这个token与switch里面的字段相符的话,就会抛出异常,F3快捷键,是在enforceStrictGrammar()中调用到了,代码如下

 private void enforceStrictGrammar(@Nullable String selection, @Nullable String groupBy,
            @Nullable String having, @Nullable String sortOrder, @Nullable String limit) {
        SQLiteTokenizer.tokenize(selection, SQLiteTokenizer.OPTION_NONE,
                new Consumer<String>() {
                    @Override
                    public void accept(String token) {
                        SQLiteQueryBuilder.this.enforceStrictToken(token);
                    }
                });
        SQLiteTokenizer.tokenize(groupBy, SQLiteTokenizer.OPTION_NONE,
                this::enforceStrictToken);
        SQLiteTokenizer.tokenize(having, SQLiteTokenizer.OPTION_NONE,
                this::enforceStrictToken);
        SQLiteTokenizer.tokenize(sortOrder, SQLiteTokenizer.OPTION_NONE,
                this::enforceStrictToken);
        SQLiteTokenizer.tokenize(limit, SQLiteTokenizer.OPTION_NONE,
                new Consumer<String>() {
                    @Override
                    public void accept(String token) {
                        SQLiteQueryBuilder.this.enforceStrictToken(token);
                    }
                });
    }

这里面也没啥特殊的,接着F3,来到了query()方法中,代码如下:

public Cursor query(SQLiteDatabase db, String[] projectionIn,
            String selection, String[] selectionArgs, String groupBy,
            String having, String sortOrder, String limit, CancellationSignal cancellationSignal) {
        if (mTables == null) {
            return null;
        }

        final String sql;
        final String unwrappedSql = buildQuery(
                projectionIn, selection, groupBy, having,
                sortOrder, limit);

        if (isStrictColumns()) {
            enforceStrictColumns(projectionIn);
        }
        if (isStrictGrammar()) {
            enforceStrictGrammar(selection, groupBy, having, sortOrder, limit);
        }
        if (isStrict()) {
            // Validate the user-supplied selection to detect syntactic anomalies
            // in the selection string that could indicate a SQL injection attempt.
            // The idea is to ensure that the selection clause is a valid SQL expression
            // by compiling it twice: once wrapped in parentheses and once as
            // originally specified. An attacker cannot create an expression that
            // would escape the SQL expression while maintaining balanced parentheses
            // in both the wrapped and original forms.

            // NOTE: The ordering of the below operations is important; we must
            // execute the wrapped query to ensure the untrusted clause has been
            // fully isolated.

            // Validate the unwrapped query
            db.validateSql(unwrappedSql, cancellationSignal); // will throw if query is invalid

            // Execute wrapped query for extra protection
            final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy,
                    wrap(having), sortOrder, limit);
            sql = wrappedSql;
        } else {
            // Execute unwrapped query
            sql = unwrappedSql;
        }

        final String[] sqlArgs = selectionArgs;
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            if (Build.IS_DEBUGGABLE) {
                Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
            } else {
                Log.d(TAG, sql);
            }
        }
        return db.rawQueryWithFactory(
                mFactory, sql, sqlArgs,
                SQLiteDatabase.findEditTable(mTables),
                cancellationSignal); // will throw if query is invalid
    }

在这里插入图片描述

在query方法中的555行代码那调用了enforceStrictGrammar()方法,但是要执行这个方法有个判断条件,看看这个判断条件是啥,F3进去,代码如下:在这里插入图片描述
就两个成员变量,下来看mStrictFlags这个,看看他是在哪个地方赋值的,F3快捷键可以知道,代码如下:

/**
     * When set, the selection is verified against malicious arguments. When
     * using this class to create a statement using
     * {@link #buildQueryString(boolean, String, String[], String, String, String, String, String)},
     * non-numeric limits will raise an exception. If a projection map is
     * specified, fields not in that map will be ignored. If this class is used
     * to execute the statement directly using
     * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String)}
     * or
     * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String, String)},
     * additionally also parenthesis escaping selection are caught. To
     * summarize: To get maximum protection against malicious third party apps
     * (for example content provider consumers), make sure to do the following:
     * <ul>
     * <li>Set this value to true</li>
     * <li>Use a projection map</li>
     * <li>Use one of the query overloads instead of getting the statement as a
     * sql string</li>
     * </ul>
     * <p>
     * This feature is disabled by default on each newly constructed
     * {@link SQLiteQueryBuilder} and needs to be manually enabled.
     */
    public void setStrict(boolean strict) {
        if (strict) {
            mStrictFlags |= STRICT_PARENTHESES;
        } else {
            mStrictFlags &= ~STRICT_PARENTHESES;
        }
    }

就是一个set方法,这里重点看下注释,注释里面有一段是最新添加的

   * additionally also parenthesis escaping selection are caught. To
     * summarize: To get maximum protection against malicious third party apps
     * (for example content provider consumers), make sure to do the following:

谷歌翻译下结果如下:

此外还捕获括号转义选择。总结:为了最大限度地防止恶意第三方应用程序(例如内容提供商消费者),请确保执行以下操作:将此值设置为 true,使用投影图使用查询重载之一,而不是将语句作为 sql 字符串获取

这段话的意思就是谷歌让咱们查询的时候,不要用sql语句字符串,如果用到了sql语句字符串,就会将里面的参数解析出来,执行到enforceStrictToken方式时,就会出现异常。上面的查询操作,limit就用到了sql语句字符串。到了这就知道为啥导致了查询异常,接下来就要修改查询语句,谷歌的官方文档推荐写法如下:

// Request 20 records starting at row index 30.
 Bundle queryArgs = new Bundle();
 queryArgs.putInt(ContentResolver.QUERY_ARG_OFFSET, 0);
 queryArgs.putInt(ContentResolver.QUERY_ARG_LIMIT, 1);
 
 Cursor cursor = getContentResolver().query(
       contentUri,    // Content Uri is specific to individual content providers.
       projection,    // String[] describing which columns to return.
       queryArgs,     // Query arguments.
       null);         // Cancellation signal.

修改成上面的写法,引入了一个新的问题,就是limit条件不生效,设置为1,当时还是会将所有数据都会查询出来。

这就引入了另外一个问题,就不在这个文章里面写了,我就直接给出解决方案把,limit条件使用如下写法:可以解决问题

Uri limitedCallLogUri = CallLog.Calls.CONTENT_URI.buildUpon()
        .appendQueryParameter(CallLog.Calls.LIMIT_PARAM_KEY, "1").build();

对于query通话数据库,设置limit参数不生效的问题,还是得看CallLogProvider源码,首先来看CallLogProvider里面的query方法,代码如下:

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
            String sortOrder) {
        // Note don't use mCallingUid here. That's only used by mutation functions.
        final int callingUid = Binder.getCallingUid();

        mStats.incrementQueryStats(callingUid);
        try {
            return queryInternal(uri, projection, selection, selectionArgs, sortOrder);
        } finally {
            mStats.finishOperation(callingUid);
        }
    }

这里面代码比较简单,主要看queryInternal()这个方法,F3进去,代码如下:

 private Cursor queryInternal(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        if (VERBOSE_LOGGING) {
            Log.v(TAG, "query: uri=" + uri + "  projection=" + Arrays.toString(projection) +
                    "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
                    "  order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() +
                    " User=" + UserUtils.getCurrentUserHandle(getContext()));
        }

        queryForTesting(uri);

        waitForAccess(mReadAccessLatch);
        final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        final SelectionBuilder selectionBuilder = new SelectionBuilder(selection);

        final int match = sURIMatcher.match(uri);
        switch (match) {
            case CALLS:
                mzSetCallTableQueryArgs(uri, qb, selectionBuilder);
                break;

            case CALLS_ID: {
                mzSetCallTableQueryArgs(uri, qb, selectionBuilder);
                selectionBuilder.addClause(getEqualityClause(Calls._ID,
                        parseCallIdFromUri(uri)));
                break;
            }

            case CALLS_FILTER: {
                mzSetCallTableQueryArgs(uri, qb, selectionBuilder);
                List<String> pathSegments = uri.getPathSegments();
                String phoneNumber = pathSegments.size() >= 2 ? pathSegments.get(2) : null;
                if (!TextUtils.isEmpty(phoneNumber)) {
                    qb.appendWhere("PHONE_NUMBERS_EQUAL(number, ");
                    qb.appendWhereEscapeString(phoneNumber);
                    qb.appendWhere(mUseStrictPhoneNumberComparation ? ", 1)" : ", 0)");
                } else {
                    qb.appendWhere(Calls.NUMBER_PRESENTATION + "!="
                            + Calls.PRESENTATION_ALLOWED);
                }
                break;
            }

            case MZ_CALLS_SYNC:
                qb.setTables(Tables.MZ_CALLS_SYNC);
                qb.setProjectionMap(sCallsSyncProjectionMap);
                break;

            default:
                throw new IllegalArgumentException("Unknown URL " + uri);
        }

        final int limit = getIntParam(uri, Calls.LIMIT_PARAM_KEY, 0);
        final int offset = getIntParam(uri, Calls.OFFSET_PARAM_KEY, 0);
        String limitClause = null;
        if (limit > 0) {
            limitClause = offset + "," + limit;
        }

        final SQLiteDatabase db = mDbHelper.getReadableDatabase();

        Cursor c = null;
        try {
            c = qb.query(db, projection, selectionBuilder.build(), selectionArgs, null,
                    null, sortOrder, limitClause);
        }catch (SQLiteException e){
            if (c!=null){
                c.close();
            }
            e.printStackTrace();
        }

        if (c != null) {
            c.setNotificationUri(getContext().getContentResolver(), CallLog.CONTENT_URI);
        }
        return c;
    }

这个方法里面,书要是对数据库查询的一些参数设置,这会问题出在limit,咱们主要来看limit参数的设置,从上面的代码中可以看出,在53行代码出,通过getIntParam获取了limit,咱们在进到getIntParam()方法中,代码如下:

 /**
     * Gets an integer query parameter from a given uri.
     *
     * @param uri The uri to extract the query parameter from.
     * @param key The query parameter key.
     * @param defaultValue A default value to return if the query parameter does not exist.
     * @return The value from the query parameter in the Uri.  Or the default value if the parameter
     * does not exist in the uri.
     * @throws IllegalArgumentException when the value in the query parameter is not an integer.
     */
    private int getIntParam(Uri uri, String key, int defaultValue) {
        String valueString = uri.getQueryParameter(key);
        if (valueString == null) {
            return defaultValue;
        }

        try {
            return Integer.parseInt(valueString);
        } catch (NumberFormatException e) {
            String msg = "Integer required for " + key + " parameter but value '" + valueString +
                    "' was found instead.";
            throw new IllegalArgumentException(msg, e);
        }
    }

在12行代码出,通过URI的getQueryParameter方法获取value,但是咱们在查询的时候并没有设置这个值,所以valueString会返回默认值0,回到queryInternal方法中,在56行代码出,只有limit>0才会设置limit,这样就知道为什么通过下面方法设置limit有效,而其他方法设置无效了:

Uri limitedCallLogUri = CallLog.Calls.CONTENT_URI.buildUpon()
        .appendQueryParameter(CallLog.Calls.LIMIT_PARAM_KEY, "1").build();

总结
第一,关于直接用sql语句字符串设置limit的问题,这是因为谷歌在Android R上面对其做了限制,不能直接使用ql语句字符串,不然就会抛出异常
第二,关于limit参数设置问题,因为在CallLogProvider中,是通过getQueryParameter方法获取limit值的,如果通过其他方法设置,而不是通过appendQueryParameter方法设置,就会导致limit设置无效

献丑了,谢谢观看!!!!!!!!!!!

  • 12
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值