diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java index 0e1d829b52..90e960de67 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java @@ -19,7 +19,10 @@ package org.springframework.expression.spel.support; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.springframework.core.SmartClassLoader; import org.springframework.expression.EvaluationException; import org.springframework.expression.TypeLocator; import org.springframework.expression.spel.SpelEvaluationException; @@ -48,6 +51,8 @@ public class StandardTypeLocator implements TypeLocator { private final List importPrefixes = new ArrayList<>(1); + private final Map> typeCache = new ConcurrentHashMap<>(); + /** * Create a {@code StandardTypeLocator} for the default {@link ClassLoader} @@ -110,6 +115,21 @@ public class StandardTypeLocator implements TypeLocator { */ @Override public Class findType(String typeName) throws EvaluationException { + Class cachedType = this.typeCache.get(typeName); + if (cachedType != null) { + return cachedType; + } + Class loadedType = loadType(typeName); + if (loadedType != null && + !(this.classLoader instanceof SmartClassLoader scl && scl.isClassReloadable(loadedType))) { + this.typeCache.put(typeName, loadedType); + return loadedType; + } + throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, typeName); + } + + @Nullable + private Class loadType(String typeName) { try { return ClassUtils.forName(typeName, this.classLoader); } @@ -125,7 +145,7 @@ public class StandardTypeLocator implements TypeLocator { // might be a different prefix } } - throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, typeName); + return null; } } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java index d1cb75e02d..f0c97dc573 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,7 +55,7 @@ public abstract class NamedParameterUtils { * Set of characters that qualify as parameter separators, * indicating that a parameter name in an SQL String has ended. */ - private static final String PARAMETER_SEPARATORS = "\"':&,;()|=+-*%/\\<>^]"; + private static final String PARAMETER_SEPARATORS = "\"':&,;()|=+-*%/\\<>^"; /** * An index with separator flags per character code. @@ -142,16 +142,25 @@ public abstract class NamedParameterUtils { j++; } else { - while (j < statement.length && !isParameterSeparator(statement[j])) { + boolean paramWithSquareBrackets = false; + while (j < statement.length) { + c = statement[j]; + if (isParameterSeparator(c)) { + break; + } + if (c == '[') { + paramWithSquareBrackets = true; + } + else if (c == ']') { + if (!paramWithSquareBrackets) { + break; + } + paramWithSquareBrackets = false; + } j++; } if (j - i > 1) { parameter = sql.substring(i + 1, j); - if (j < statement.length && statement[j] == ']' && parameter.contains("[")) { - // preserve end bracket for index/key - j++; - parameter = sql.substring(i + 1, j); - } namedParameterCount = addNewNamedParameter( namedParameters, namedParameterCount, parameter); totalParameterCount = addNamedParameter( diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java index 23c4e6e838..1fcf399bf6 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -330,6 +330,18 @@ public class NamedParameterUtilsTests { assertThat(sqlToUse).isEqualTo("SELECT ARRAY[?]"); } + @Test // gh-31596 + void paramNameWithNestedSquareBrackets() { + String sql = "insert into GeneratedAlways (id, first_name, last_name) values " + + "(:records[0].id, :records[0].firstName, :records[0].lastName), " + + "(:records[1].id, :records[1].firstName, :records[1].lastName)"; + + ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); + assertThat(parsedSql.getParameterNames()).containsOnly( + "records[0].id", "records[0].firstName", "records[0].lastName", + "records[1].id", "records[1].firstName", "records[1].lastName"); + } + @Test // gh-27925 void namedParamMapReference() { String sql = "insert into foos (id) values (:headers[id])"; diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java index b1b80ea306..306de12f87 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java @@ -67,7 +67,7 @@ abstract class NamedParameterUtils { * Set of characters that qualify as parameter separators, * indicating that a parameter name in an SQL String has ended. */ - private static final String PARAMETER_SEPARATORS = "\"':&,;()|=+-*%/\\<>^]"; + private static final String PARAMETER_SEPARATORS = "\"':&,;()|=+-*%/\\<>^"; /** * An index with separator flags per character code. @@ -83,12 +83,12 @@ abstract class NamedParameterUtils { // ------------------------------------------------------------------------- - // Core methods used by NamedParameterSupport. + // Core methods used by NamedParameterExpander // ------------------------------------------------------------------------- /** * Parse the SQL statement and locate any placeholders or named parameters. - * Named parameters are substituted for a R2DBC placeholder. + * Named parameters are substituted for an R2DBC placeholder. * @param sql the SQL statement * @return the parsed statement, represented as {@link ParsedSql} instance */ @@ -154,16 +154,25 @@ abstract class NamedParameterUtils { j++; } else { - while (j < statement.length && !isParameterSeparator(statement[j])) { + boolean paramWithSquareBrackets = false; + while (j < statement.length) { + c = statement[j]; + if (isParameterSeparator(c)) { + break; + } + if (c == '[') { + paramWithSquareBrackets = true; + } + else if (c == ']') { + if (!paramWithSquareBrackets) { + break; + } + paramWithSquareBrackets = false; + } j++; } if (j - i > 1) { parameter = sql.substring(i + 1, j); - if (j < statement.length && statement[j] == ']' && parameter.contains("[")) { - // preserve end bracket for index/key - j++; - parameter = sql.substring(i + 1, j); - } namedParameterCount = addNewNamedParameter( namedParameters, namedParameterCount, parameter); totalParameterCount = addNamedParameter( @@ -261,7 +270,7 @@ abstract class NamedParameterUtils { /** * Parse the SQL statement and locate any placeholders or named parameters. Named - * parameters are substituted for a R2DBC placeholder, and any select list is expanded + * parameters are substituted for an R2DBC placeholder, and any select list is expanded * to the required number of placeholders. Select lists may contain an array of objects, * and in that case the placeholders will be grouped and enclosed with parentheses. * This allows for the use of "expression lists" in the SQL statement like: