Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SpEL incorrectly splits string arguments by comma for Object... varargs method #33013

Closed
weiw005 opened this issue Jun 12, 2024 · 10 comments
Closed
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) status: backported An issue that has been backported to maintenance branches type: bug A general bug
Milestone

Comments

@weiw005
Copy link

weiw005 commented Jun 12, 2024

Affects

4.1.2.RELEASE or higher

Summary

In SpEL, a registered varargs function incorrectly splits string arguments by comma when varargs type is Object / Any.

Overview

This is related to previously reported #27582. The issue was likely not fully fixed yet.

I created a unit test here to demo the bug: https://github.com/weiw005/playground/blob/e6f1ae9336b8ac5e294758a90d2639c6a075df5b/spel-vararg/src/test/kotlin/SpelVarargTest.kt

It seems that when a registered function has vararg args: Any as the argument type, SpEL would have inconsistent behavior when there is single vs. multiple argument(s) of string type:

  • Single argument: SpEL will split the string by comma, and take the first part only
  • Multiple arguments: SpEL will take all arguments as is (expected behavior)

This bug doesn't manifest when the function signature declares vararg args: String instead (all arguments would be taken as is correctly).

I have also confirmed that version 4.1.1.RELEASE or lower did not have this bug.

Related Issues

@weiw005
Copy link
Author

weiw005 commented Jun 12, 2024

@manoharank reported the same observation here: #27582 (comment)

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jun 12, 2024
@jhoeller jhoeller added the in: core Issues in core modules (aop, beans, core, context, expression) label Jun 13, 2024
@sbrannen sbrannen self-assigned this Jul 8, 2024
@sbrannen sbrannen added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Jul 8, 2024
@sbrannen sbrannen changed the title Registered vararg function incorrectly splits string arguments by comma when argument type is Object / Any SpEL incorrectly splits string arguments by comma for Object... varargs method Jul 8, 2024
@sbrannen
Copy link
Member

sbrannen commented Jul 8, 2024

Hi @weiw005,

Congratulations on submitting your first issue for the Spring Framework! 👍

Thanks for the Demo!

Though, please submit sample applications using Java (unless Kotlin is required to reproduce the issue).

I've reduced the demo to the following standalone Java test class.

import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;

class SpelVarargsTests {

	@Test
	void formatStringVarargs_EmptyString() {
		assertThat(evaluateExpression("#formatStringVarargs('Hello %s', '')"))
				.isEqualTo("Hello ");
	}

	@Test
	void formatObjectVarargs_EmptyString() {
		assertThat(evaluateExpression("#formatObjectVarargs('Hello %s', '')"))
				.isEqualTo("Hello ");
	}

	@Test
	void formatStringVarargs_SingleArgumentWithComma() {
		assertThat(evaluateExpression("#formatStringVarargs('Hello %s', 'a, b')"))
				.isEqualTo("Hello a, b");
	}

	@Test
	void formatObjectVarargs_SingleArgumentWithComma() {
		assertThat(evaluateExpression("#formatObjectVarargs('Hello %s', 'a, b')"))
				.isEqualTo("Hello a, b");
	}

	@Test
	void formatStringVarargs_MultiArgumentWithComma() {
		assertThat(evaluateExpression("#formatStringVarargs('Hello %s; %s', 'a, b', 'c, d')"))
				.isEqualTo("Hello a, b; c, d");
	}

	@Test
	void formatObjectVarargs_MultiArgumentWithComma() {
		assertThat(evaluateExpression("#formatObjectVarargs('Hello %s; %s', 'a, b', 'c, d')"))
				.isEqualTo("Hello a, b; c, d");
	}


	private static final Method formatObjectVarargs =
			ReflectionUtils.findMethod(SpelVarargsTests.class, "formatObjectVarargs", String.class, Object[].class);

	private static final Method formatStringVarargs =
			ReflectionUtils.findMethod(SpelVarargsTests.class, "formatStringVarargs", String.class, String[].class);

	private static Object evaluateExpression(String expression) {
		SpelExpressionParser parser = new SpelExpressionParser();
		Expression exp = parser.parseExpression(expression);
		StandardEvaluationContext context = new StandardEvaluationContext();
		context.registerFunction("formatObjectVarargs", formatObjectVarargs);
		context.registerFunction("formatStringVarargs", formatStringVarargs);
		return exp.getValue(context);
	}

	static String formatObjectVarargs(String format, Object... args) {
		return String.format(format, args);
	}

	static String formatStringVarargs(String format, String... args) {
		return String.format(format, (Object[]) args);
	}

}

When running against main, formatObjectVarargs_EmptyString() and formatObjectVarargs_SingleArgumentWithComma() fail.

We'll look into fixing this.

Cheers,

Sam

@sbrannen
Copy link
Member

sbrannen commented Jul 9, 2024

While investigating this, I noticed that (in theory) the same bug must exist for a function registered as a MethodHandle, since that code is effectively a copy of the code for Method and Constructor invocations.

@sbrannen
Copy link
Member

This has been fixed in 6.1.x and main and will be available in the 6.1.11 and 6.2.0-M5 releases later this week.

This will also be backported to 5.3.38 and 6.0.23.

@weiw005
Copy link
Author

weiw005 commented Jul 22, 2024

Thanks, @sbrannen

I did some testing in our project with the latest release (6.1.11) and things are working properly now AFAICT.

As part of the verification, I found another behavior also changed in terms of how array arguments work as vararg. The change probably makes sense, but just wanted to share the observation FWIW:

  • Previously, if the expression code passed arguments as an array, e.g. {'hello', 123}, it worked not only for functions that take an array argument, but also functions that accept vararg in which case the array elements would be flattened.
  • After this fix, the array would be treated as a single argument for vararg functions.

The new behavior is probably more clear and consistent IMHO.

Thanks again for the fix!

@sbrannen
Copy link
Member

Hi @weiw005,

I did some testing in our project with the latest release (6.1.11) and things are working properly now AFAICT.

Great! Glad to hear that resolved your issues.

  • Previously, if the expression code passed arguments as an array, e.g. {'hello', 123}, it worked not only for functions that take an array argument, but also functions that accept vararg in which case the array elements would be flattened.
  • After this fix, the array would be treated as a single argument for vararg functions.

The new behavior is probably more clear and consistent IMHO.

Perhaps, but just to be certain... would you mind creating a new issue pointing out what used to work and what now doesn't (with simple, concrete examples that we can run (preferably in Java))?

That would at least allow us to assess if the change in behavior is intentional or acceptable.

Cheers,

Sam

@sbrannen
Copy link
Member

@weiw005,

  • Previously, if the expression code passed arguments as an array, e.g. {'hello', 123}, it worked not only for functions that take an array argument, but also functions that accept vararg in which case the array elements would be flattened.
  • After this fix, the array would be treated as a single argument for vararg functions.

In org.springframework.expression.spel.MethodInvocationTests.testVarargsWithObjectArrayType(), we have following test.

evaluate("formatObjectVarargs('x -> %s %s %s', new String[]{'a', 'b', 'c'})", "x -> a b c", String.class);

That passes as expected.

Thus, I assume you mean you are supplying an inline list as follows.

evaluate("formatObjectVarargs('x -> %s %s %s', {'hello', 123, 456})", "x -> hello 123 456", String.class);

That fails, because the inline list (an instance of java.util.Collections$UnmodifiableRandomAccessList) is supplied as the first argument in the Object[] varargs array passed to the formatObjectVarargs() method/function, and that's the expected behavior.

Thus, perhaps you are saying that a List was previously converted to an array in such scenarios, but if that's the case, I believe that was merely coincidental (and accidental).

Can you please confirm that the above scenario is the change in behavior you experienced?

Thanks,

Sam

weiw005 added a commit to weiw005/playground that referenced this issue Jul 26, 2024
weiw005 added a commit to weiw005/playground that referenced this issue Jul 26, 2024
@weiw005
Copy link
Author

weiw005 commented Jul 26, 2024

Hi @sbrannen

Yes, it's about inline list. Here is a self-contained unit test to demonstrate the difference:

import org.junit.jupiter.api.Test;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.ReflectionUtils;

import static org.assertj.core.api.Assertions.assertThat;

import java.lang.reflect.Method;

public class SpelVarargArrayTest {
    @Test
    public void arrayFlattenedAsVararg_Before_6_1_10() {
        assertThat(evaluateExpression("#objectVarargDemo({'World', 123})"))
                .isEqualTo("There are 2 argument(s): World 123");
    }

    @Test
    public void arrayFlattenedAsVararg_After_6_1_11() {
        assertThat(evaluateExpression("#objectVarargDemo({'World', 123})"))
                .isEqualTo("There are 1 argument(s): [World, 123]");
    }

    private static final Method demoMethod =
            ReflectionUtils.findMethod(SpelVarargArrayTest.class, "objectVarargDemo", Object[].class);

    private static Object evaluateExpression(String expression) {
        SpelExpressionParser parser = new SpelExpressionParser();
        Expression exp = parser.parseExpression(expression);
        StandardEvaluationContext context = new StandardEvaluationContext();
        context.registerFunction("objectVarargDemo", demoMethod);
        return exp.getValue(context);
    }

    static String objectVarargDemo(Object... args) {
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("There are %d argument(s): ", args.length));
        for (Object arg : args) {
            sb.append(arg).append(" ");
        }
        return sb.toString().trim();
    }
}

Code link: https://github.com/weiw005/playground/blob/fe749cf911c0212e7f0892fdf06ee29250699aa7/spel-vararg/src/test/java/SpelVarargArrayTest.java

You can see that earlier versions would flatten the inline list as separate elements for vararg functions, and the new version simply treat the whole list as a single argument.

@sbrannen
Copy link
Member

Thanks for the feedback and example, @weiw005.

You can see that earlier versions would flatten the inline list as separate elements for vararg functions, and the new version simply treat the whole list as a single argument.

OK. I can see how it used to do that.

As I mentioned before, I believe that behavior was accidental and that recent bug fixes in the varargs processing now implement the correct behavior.

Of course, if the recent changes result in major regressions within the community, we may potentially attempt to support the list (or potentially only inline list) to array conversion there.

Having said that, however, I think we should aim to stick with the current behavior since a list is in fact not an array, and the current behavior in SpEL aligns with the standard Java behavior for varargs invocations.

sbrannen added a commit that referenced this issue Jul 26, 2024
Prior to this commit, the Spring Expression Language (SpEL) incorrectly
split single String arguments by comma for Object... varargs method and
constructor invocations.

This commit addresses this by checking if the single argument type is
already "assignable" to the varargs component type instead of "equal"
to the varargs component type.

See gh-33013
Closes gh-33189
sbrannen added a commit that referenced this issue Jul 26, 2024
Prior to this commit, the Spring Expression Language (SpEL) incorrectly
split single String arguments by comma for Object... varargs method and
constructor invocations.

This commit addresses this by checking if the single argument type is
already "assignable" to the varargs component type instead of "equal"
to the varargs component type.

See gh-33013
Closes gh-33188

(cherry picked from commit d33f66d)
@sbrannen
Copy link
Member

sbrannen commented Aug 4, 2024

This will also be backported to 5.3.38 and 6.0.23.

Due to #33315, we have decided to revert the backports to 5.3.38 and 6.0.23.

sbrannen added a commit that referenced this issue Aug 5, 2024
The changes made in conjunction with gh-33013 resulted in a regression
for varargs support in SpEL expressions. Specifically, before gh-33013
one could supply a list -- for example, an "inline list" -- as the
varargs array when invoking a varargs constructor, method, or function
within a SpEL expression. However, after gh-33013 an inline list (or
collection in general) is no longer converted to an array for varargs
invocations. Instead, the list is supplied as a single argument of the
resulting varargs array which breaks applications that depend on the
previous behavior.

Although it was never intended that one could supply a collection as
the set of varargs, we concede that this is a regression in existing
behavior, and this commit therefore restores support for supplying a
java.util.List as the varargs "array".

In addition, this commit introduces the same "list to array" conversion
support for MethodHandle-based functions that accept varargs.

Note, however, that this commit does not restore support for converting
arbitrary single objects to an array for varargs invocations if the
single object is already an instance of the varargs array's component
type.

See gh-33013
Closes gh-33315
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) status: backported An issue that has been backported to maintenance branches type: bug A general bug
Projects
None yet
Development

No branches or pull requests

4 participants